Find Caret Position Anywhere on page - javascript

I'm a part time newish developer working on a Chrome extension designed to work as an internal CR tool.
The concept is simple, on a keyboard shortcut, the extension gets the word next to the caret, checks it for a pattern match, and if the match is 'true' replaces the word with a canned response.
To do this, I mostly used a modified version of this answer.
I've hit a roadblock in that using this works for the active element, but it doesn't appear to work for things such as the 'compose' window in Chrome, or consistently across other services (Salesforce also seems to not like it, for example). Poking about a bit I thought this might be an issue with iFrames, so I tinkered about a bit and modified this peace of code:
function getActiveElement(document){
document = document || window.document;
if( document.body === document.activeElement || document.activeElement.tagName == 'IFRAME' ){// Check if the active element is in the main web or iframe
var iframes = document.getElementsByTagName('iframe');// Get iframes
for(var i = 0; i<iframes.length; i++ ){
var focused = getActiveElement( iframes[i].contentWindow.document );// Recall
if( focused !== false ){
return focused; // The focused
}
}
}
else return document.activeElement;
};
(Which I originally got from another SO post I can no longer find). Seems as though I'm out of luck though, as no dice.
Is there a simple way to always get the active element with the active caret on every page, even for the Gmail compose window and similar services, or am I going to be stuck writting custom code for a growing list of servcies that my code can't fetch the caret on?
My full code is here. It's rough while I just try to get this to work, so I understand there's sloppy parts of it that need tidied up:
function AlertPrevWord() {
//var text = document.activeElement; //Fetch the active element on the page, cause that's where the cursor is.
var text = getActiveElement();
console.log(text);
var caretPos = text.selectionStart;//get the position of the cursor in the element.
var word = ReturnWord(text.value, caretPos);//Get the word before the cursor.
if (word != null) {//If it's not blank
return word //send it back.
}
}
function ReturnWord(text, caretPos) {
var index = text.indexOf(caretPos);//get the index of the cursor
var preText = text.substring(0, caretPos);//get all the text between the start of the element and the cursor.
if (preText.indexOf(" ") > 0) {//if there's more then one space character
var words = preText.split(" ");//split the words by space
return words[words.length - 1]; //return last word
}
else {//Otherwise, if there's no space character
return preText;//return the word
}
}
function getActiveElement(document){
document = document || window.document;
if( document.body === document.activeElement || document.activeElement.tagName == 'IFRAME' ){// Check if the active element is in the main web or iframe
var iframes = document.getElementsByTagName('iframe');// Get iframes
for(var i = 0; i<iframes.length; i++ ){
var focused = getActiveElement( iframes[i].contentWindow.document );// Recall
if( focused !== false ){
return focused; // The focused
}
}
}
else return document.activeElement;
};

I've got it working for the Gmail window (and presumably other contenteditable elements, rather than just input elements).
Edit: the failure around linebreaks was because window.getSelection().anchorOffset returns the offset relative to that particular element, whereas ReturnWord was getting passed the text of the entire compose window (which contained multiple elements). window.getSelection().anchorNode returns the node that the offset is being calculated within.
function AlertPrevWord() {
var text = getActiveElement();
var caretPos = text.selectionStart || window.getSelection().anchorOffset;
var word = ReturnWord(text.value || window.getSelection().anchorNode.textContent, caretPos);
if (word != null) {return word;}
}
I originally used a MutationObserver to account for the Gmail compose div being created after the page load, just to attach an event listener to it.
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var nodes = mutation.addedNodes; //list of new nodes in the DOM
for (var i = 0; i < nodes.length; ++i) {
//attach key listener to nodes[i] that calls AlertPrevWord
}
});
});
observer.observe(document, {childList: true, subtree:true });
//childList:true notifies observer when nodes are added or removed
//subtree:true observes all the descendants of document as well
Edit: The delegated click handler I've been testing with. Key event handlers so far not working.
$(document).on( "click", ":text,[contenteditable='true']", function( e ) {
e.stopPropagation();
console.log(AlertPrevWord());
});

Related

Get position of click within text (non-input)

Goal: get text position of a click within non-input elements (spans/divs) with IE11
I have a workbench for users to add and modify content using widgets. There is a main widget that has a list of different types of content, for example it might have something like:
<div class='summaryView'>
<div class='summaryHead'>Summary 1</div>
<div class='summaryContent'>Foo bar baz</div>
</div>
A user would click within this and it would open up another widget in which the summary content would be within a textarea (actually a codemirror instance). A new requirement is that if the user clicks between the a and b in bar (for example), when the widget opens up for them to start editing content, the cursor would be in that same place.
The issue I'm having with doing this is knowing the actual position they clicked. I can get the coordinates from the event... but this doesn't really help me (unless maybe I used a monospace font, which is not an option).
I also found document.caretPositionFromPoint and document.caretRangeFromPoint which seemed helpful... and I could do something like:
if (document.caretPositionFromPoint) {
range = document.caretPositionFromPoint(e.pageX, e.pageY);
textNode = range.offsetNode;
offset = range.offset;
} else if (document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(e.pageX, e.pageY);
textNode = range.startContainer;
offset = range.startOffset;
}
...and then go from there... Except these are not working for IE11 (which my users are unfortunately stuck with).
Even though documentation seems to indicate it should work with IE. I'm assuming it has something to do with "This is an experimental technology".
I was able to piece some stuff together and come up with this which seems to be working for me in IE11, giving me the parentElement and the text offset of the click:
if (document.body.createTextRange) {
range = document.body.createTextRange ();
range.moveToPoint (event.clientX, event.clientY);
var end = Math.abs( range.duplicate().moveEnd('character', -1000000) );
var rDup = range.duplicate();
range.collapse( false );
var parentElement = rDup.parentElement();
var children = parentElement.getElementsByTagName('*');
for (var i = children.length - 1; i >= 0; i--) {
rDup.moveToElementText( children[i] );
if ( rDup.inRange(range) ) {
parentElement = children[i];
break;
}
}
rDup.moveToElementText( parentElement );
var offset = end - Math.abs( rDup.moveStart('character', -1000000) );
}

Erratic mouseover behavior with nested items inside mouseover layer

So let's say we have:
A container for everything
A baseDiv inside that container
//let's create a base layer
var container = document.getElementById('container')
var baseDiv = document.createElement('div')
baseDiv.id = 'baseDiv'
baseDiv.innerText = 'this is the base div'
baseDiv.addEventListener('mouseover', createLayer)
container.appendChild(baseDiv)
When the user mouses over:
A layerOnTop, of the same size is put on top of the baseDiv.
When the user mouses out:
The layerOnTop is removed.
function createLayer(){
console.log('creating layer')
layerOnTop = document.createElement('div')
layerOnTop.id = 'layerOnTop'
layerOnTop.addEventListener('mouseout',
function(){
console.log('removing layer')
return layerOnTop.parentElement.removeChild(layerOnTop)
})
container.appendChild(layerOnTop) }
Simple and works great.
However, when layerOnTop contains elements as well (buttons, inputs), the behavior gets very erratic and starts flicking as you're technically exiting the layerOnTop.
//it contains two textareas
layerOnTop.appendChild(document.createElement('textarea'))
layerOnTop.appendChild(document.createElement('textarea'))
I wish I could use mouseenter but it doesn't seem to be supported by Chrome.
Here's my jsfiddle: http://jsfiddle.net/DjRBP/
How can I stop this? I wish I could merge the textareas and layerOnTop into one large mouseover-handling conglomerate.
You need to check in your mouse out event that it's actually leaving the element. Change your mouseout function to:
function(event) {
var e = event.toElement || event.relatedTarget;
if (e.parentNode == this || e == this) {
// We're not actually leaving the parent node so don't remove layer
return;
}
console.log('removing layer')
return layerOnTop.parentElement.removeChild(layerOnTop)
})

In contenteditable how do you add a paragraph after blockquote on Enter key press?

I have the following problem. Once I add a blockquote in contenteditable, by pressing Enter key it moves to a new line and adds another blockquote element. It goes on forever, and I can’t escape the formatting. The desired functionality would be that of the unordered list. When you press the Enter key it adds a new empty <li> element, but if you press Enter again, it escapes the formatting, removes the previously created <li> and adds a <p>.
Check out the demo: http://jsfiddle.net/wa9pM/
One hack I found was to create an empty <p> under the blockquote, before you create a blockquote. But is there a way to break this formatting behaviour with JavaScript? No idea how I would check: if where the cursor is, it’s the end of the line and if it’s a blockquote and on Enter key press, don’t add a new blockquote.
I’m using this code to generate a blockquote in JS:
document.execCommand('formatBlock', false, 'blockquote');
While creating a rich text editor for an iOS application i faced the same problem. Every time i've inserted a <blockquote> tag in my text field and pressed Enter, it was impossible to get rid off the block-quote.
After researching a bit, i've found a working solution.
Finding inner HTML tags:
function whichTag(tagName){
var sel, containerNode;
var tagFound = false;
tagName = tagName.toUpperCase();
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
containerNode = sel.getRangeAt(0).commonAncestorContainer;
}
}else if( (sel = document.selection) && sel.type != "Control" ) {
containerNode = sel.createRange().parentElement();
}
while (containerNode) {
if (containerNode.nodeType == 1 && containerNode.tagName == tagName) {
tagFound = true;
containerNode = null;
}else{
containerNode = containerNode.parentNode;
}
}
return tagFound;
}
Checking for occurrences of the block-quote tag:
function checkBlockquote(){
var input = document.getElementById('text_field_id');
input.onkeydown = function() {
var key = event.keyCode || event.charCode;
if( key == 13){
if (whichTag("blockquote")){
document.execCommand('InsertParagraph');
document.execCommand('Outdent');
}
}
};
}
Triggering the key down events:
<body onLoad="checkBlockquote();">
<!-- stuff... -->
</body>
I believe the code above can be adjusted to fit your needs easily. If you need further help, feel free to ask.
Something like this did the work for me (at least on Chrome and Safari).
Demo at http://jsfiddle.net/XLPrw/
$("[contenteditable]").on("keypress", function(e) {
var range = window.getSelection().getRangeAt();
var element = range.commonAncestorContainer;
if(element.nodeName == "BLOCKQUOTE") {
element.parentElement.removeChild(element);
}
});
Didn't make any extensive test, but it looks like range.commonAncestorElement returns the current textnode in case the blockquote contains text, or the blockquote element itself in case it contains no textnode (on Chrome, a <br> is added and caret is positioned after it). You can remove the newly created blockquote in this case. Anyway, after deleting the element the caret looks like getting positioned somewhere upon the contenteditable, although typing confirms that it's right after the original blackquote.
Hope this points you to a more conclusive solution.
Super late answer, but this was a much simpler solution for me. Hopefully it helps anyone else looking. Browser compatibility may vary.
YOUR_EDITABLE_ELEMENT.addEventListener('keyup', e => {
if (e.which || e.keyCode === 13) {
if (document.queryCommandValue('formatBlock') === 'blockquote') {
exec('formatBlock', '<P>')
}
}
})

How can I get the element in which highlighted text is in?

I am trying to learn how to write a bookmarklet where I can highlight some text, click on the bookmarklet and have it tell me what got highlighted. I can get that far, but next I want to know what element that text is in.
For example:
<div id="some-id">to be highlighted</div>
The bookmarklet code:
javascript:(function(){alert(window.getSelection();})()
If I highlight the text "to be highlighted" and then click on the bookmarklet, it will alert the text. But how can I get the element in which the text is in, in this case the element after that?
So the flow is: highlight text, click bookmarklet, bookmarklet tells you what you highlighted and the element it's in.
Thanks!
Try something similar to this to get the dom element that contains the selected text.
window.getSelection().anchorNode.parentNode
It works on firefox and Chrome, you should test it into the remaining browsers.
It have a quirk, if you select text that beholds to more than an element, only the first one is returned. But maybe you can live with this.
Just for reference on what is the anchorNode property:
http://help.dottoro.com/ljkstboe.php
On internet explorer this snippet should do the trick (I can't test it)
document.selection.createRange().parentElement();
as stated into
http://msdn.microsoft.com/en-us/library/ms535872.aspx and
http://msdn.microsoft.com/en-us/library/ms536654.aspx
A range explanation on quirksmode: http://www.quirksmode.org/dom/range_intro.html
You can do this relatively simply in all major browsers. Code is below, live example: http://jsfiddle.net/timdown/Q9VZT/
function getSelectionTextAndContainerElement() {
var text = "", containerElement = null;
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var node = sel.getRangeAt(0).commonAncestorContainer;
containerElement = node.nodeType == 1 ? node : node.parentNode;
text = sel.toString();
}
} else if (typeof document.selection != "undefined" &&
document.selection.type != "Control") {
var textRange = document.selection.createRange();
containerElement = textRange.parentElement();
text = textRange.text;
}
return {
text: text,
containerElement: containerElement
};
}
I don't think you can, he only way to know which element is currently being used would be to use a onclick event on the element. Other than that there is no sure way. You can however search each element until you find an element with the same text,
jQuery('*:contains(' + selected + ').
You can add an event to get the current element with this code though (untested)
var all = document.getElementsByTagName('*');
for (var i = 0; i < all.length; i++)
all[i].onclick = function(e){
window.selectedElement = all[i];
//preventDefault $ StopBubble &
return false;
}
And it will be stored in selectedElement
Ok try This:
var all = document.getElementsByTagName('*');
for (var i = 0; i < all.length; i++)
all[i].onclick = function(e) {
window.selectedElement = this;
((e && e.stopPropagation && e.stopPropagation()) ||
(window.event && (window.event.cancelBubble = true)));
return false;
}
DEMO: http://jsfiddle.net/HQC6Z/1/
Better yet: http://jsfiddle.net/HQC6Z/
After looking at the other answers, I take back my solution and offer theirs:
How can I get the element in which highlighted text is in?
How can I get the element in which highlighted text is in?

Tag-like autocompletion and caret/cursor movement in contenteditable elements

I'm working on a jQuery plugin that will allow you to do #username style tags, like Facebook does in their status update input box.
My problem is, that even after hours of researching and experimenting, it seems REALLY hard to simply move the caret. I've managed to inject the <a> tag with someone's name, but placing the caret after it seems like rocket science, specially if it's supposed work in all browsers.
And I haven't even looked into replacing the typed #username text with the tag yet, rather than just injecting it as I'm doing right now... lol
There's a ton of questions about working with contenteditable here on Stack Overflow, and I think I've read all of them, but they don't really cover properly what I need. So any more information anyone can provide would be great :)
You could use my Rangy library, which attempts with some success to normalize browser range and selection implementations. If you've managed to insert the <a> as you say and you've got it in a variable called aElement, you can do the following:
var range = rangy.createRange();
range.setStartAfter(aElement);
range.collapse(true);
var sel = rangy.getSelection();
sel.removeAllRanges();
sel.addRange(range);
I got interested in this, so I've written the starting point for a full solution. The following uses my Rangy library with its selection save/restore module to save and restore the selection and normalize cross browser issues. It surrounds all matching text (#whatever in this case) with a link element and positions the selection where it had been previously. This is triggered after there has been no keyboard activity for one second. It should be quite reusable.
function createLink(matchedTextNode) {
var el = document.createElement("a");
el.style.backgroundColor = "yellow";
el.style.padding = "2px";
el.contentEditable = false;
var matchedName = matchedTextNode.data.slice(1); // Remove the leading #
el.href = "http://www.example.com/?name=" + matchedName;
matchedTextNode.data = matchedName;
el.appendChild(matchedTextNode);
return el;
}
function shouldLinkifyContents(el) {
return el.tagName != "A";
}
function surroundInElement(el, regex, surrounderCreateFunc, shouldSurroundFunc) {
var child = el.lastChild;
while (child) {
if (child.nodeType == 1 && shouldSurroundFunc(el)) {
surroundInElement(child, regex, surrounderCreateFunc, shouldSurroundFunc);
} else if (child.nodeType == 3) {
surroundMatchingText(child, regex, surrounderCreateFunc);
}
child = child.previousSibling;
}
}
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
var parent = textNode.parentNode;
var result, surroundingNode, matchedTextNode, matchLength, matchedText;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchedText = result[0];
matchLength = matchedText.length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
parent.insertBefore(surroundingNode, matchedTextNode);
parent.removeChild(matchedTextNode);
}
}
function updateLinks() {
var el = document.getElementById("editable");
var savedSelection = rangy.saveSelection();
surroundInElement(el, /#\w+/, createLink, shouldLinkifyContents);
rangy.restoreSelection(savedSelection);
}
var keyTimer = null, keyDelay = 1000;
function keyUpLinkifyHandler() {
if (keyTimer) {
window.clearTimeout(keyTimer);
}
keyTimer = window.setTimeout(function() {
updateLinks();
keyTimer = null;
}, keyDelay);
}
HTML:
<p contenteditable="true" id="editable" onkeyup="keyUpLinkifyHandler()">
Some editable content for #someone or other
</p>
As you say you can already insert an tag at the caret, I'm going to start from there. The first thing to do is to give your tag an id when you insert it. You should then have something like this:
<div contenteditable='true' id='status'>I went shopping with <a href='#' id='atagid'>Jane</a></div>
Here is a function that should place the cursor just after the tag.
function setCursorAfterA()
{
var atag = document.getElementById("atagid");
var parentdiv = document.getElementById("status");
var range,selection;
if(window.getSelection) //FF,Chrome,Opera,Safari,IE9+
{
parentdiv.appendChild(document.createTextNode(""));//FF wont allow cursor to be placed directly between <a> tag and the end of the div, so a space is added at the end (this can be trimmed later)
range = document.createRange();//create range object (like an invisible selection)
range.setEndAfter(atag);//set end of range selection to just after the <a> tag
range.setStartAfter(atag);//set start of range selection to just after the <a> tag
selection = window.getSelection();//get selection object (list of current selections/ranges)
selection.removeAllRanges();//remove any current selections (FF can have more than one)
parentdiv.focus();//Focuses contenteditable div (necessary for opera)
selection.addRange(range);//add our range object to the selection list (make our range visible)
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createRange();//create a "Text Range" object (like an invisible selection)
range.moveToElementText(atag);//select the contents of the a tag (i.e. "Jane")
range.collapse(false);//collapse selection to end of range (between "e" and "</a>").
while(range.parentElement() == atag)//while ranges cursor is still inside <a> tag
{
range.move("character",1);//move cursor 1 character to the right
}
range.move("character",-1);//move cursor 1 character to the left
range.select()//move the actual cursor to the position of the ranges cursor
}
/*OPTIONAL:
atag.id = ""; //remove id from a tag
*/
}
EDIT:
Tested and fixed script. It definitely works in IE6, chrome 8, firefox 4, and opera 11. Don't have other browsers on hand to test, but it doesn't use any functions that have changed recently so it should work in anything that supports contenteditable.
This button is handy for testing:
<input type='button' onclick='setCursorAfterA()' value='Place Cursor After <a/> tag' >
Nico

Categories

Resources