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?
Related
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());
});
I've made this tinymce fiddle to show what I say.
Highlight text in the editor, then click on the input text, highlight in tinyMCE is lost (obviously).
Now, I know it's not easy since both, the inline editor and the input text are in the same document, thus, the focus is only one. But is there any tinymce way to get like an "unfocused" highlight (gray color) whenever I click in an input text?
I'm saying this because I have a customized color picker, this color picker has an input where you can type in the HEX value, when clicking OK it would execCommand a color change on the selected text, but it looks ugly because the highlight is lost.
I don't want to use an iframe, I know that by using the non-inline editor (iframe) is one of the solutions, but for a few reasons, i can't use an iframe text editor.
Any suggestion here? Thanks.
P.S: Out of topic, does any of you guys know why I can't access to tinymce object in the tinyMCE Fiddle ? looks like the tinyMCE global var was overwritten by the tinymce select dom element of the page itself. I can't execute a tinyMCE command lol.
Another solution:
http://fiddle.tinymce.com/sBeaab/5
P.S: Out of topic, does any of you guys know why I can't access to
tinymce object in the tinyMCE Fiddle ? looks like the tinyMCE global
var was overwritten by the tinymce select dom element of the page
itself. I can't execute a tinyMCE command lol.
Well, you can access the tinyMCE variable and even execute commands.
this line is wrong
var colorHex = document.getElementById("colorHex")
colorHex contains input element, not value.
var colorHex = document.getElementById("colorHex").value
now it works ( neolist couldn't load, so I removed it )
http://fiddle.tinymce.com/DBeaab/1
I had to do something similar recently.
First off, you can't really have two different elements "selected" simultaneously. So in order to accomplish this you're going to need to mimic the browser's built-in 'selected text highlight'. To do this, you're going to have to insert spans into the text to simulate highlighting, and then capture the mousedown and mouseup events.
Here's a fiddle from StackOverflow user "fullpipe" which illustrates the technique I used.
http://jsfiddle.net/fullpipe/DpP7w/light/
$(document).ready(function() {
var keylist = "abcdefghijklmnopqrstuvwxyz123456789";
function randWord(length) {
var temp = '';
for (var i=0; i < length; i++)
temp += keylist.charAt(Math.floor(Math.random()*keylist.length));
return temp;
}
for(var i = 0; i < 500; i++) {
var len = Math.round(Math.random() * 5 + 3);
document.body.innerHTML += '<span id="'+ i +'">' + randWord(len) + '</span> ';
}
var start = null;
var end = null;
$('body').on('mousedown', function(event) {
start = null;
end = null;
$('span.s').removeClass('s');
start = $(event.target);
start.addClass('s');
});
$('body').on('mouseup', function(event) {
end = $(event.target);
end.addClass('s');
if(start && end) {
var between = getAllBetween(start,end);
for(var i=0, len=between.length; i<len;i++)
between[i].addClass('s');
alert('You select ' + (len) + ' words');
}
});
});
function getAllBetween(firstEl,lastEl) {
var firstIdx = $('span').index($(firstEl));
var lastIdx = $('span').index($(lastEl));
if(lastIdx == firstIdx)
return [$(firstEl)];
if(lastIdx > firstIdx) {
var firstElement = $(firstEl);
var lastElement = $(lastEl);
} else {
var lastElement = $(firstEl);
var firstElement = $(lastEl);
}
var collection = new Array();
collection.push(firstElement);
firstElement.nextAll().each(function(){
var siblingID = $(this).attr("id");
if (siblingID != $(lastElement).attr("id")) {
collection.push($(this));
} else {
return false;
}
});
collection.push(lastElement);
return collection;
}
As you can see in the fiddle, the gibberish text in the right pane stays highlighted regardless of focus elsewhere on the page.
At that point, you're going to have to apply your color changes to all matching spans.
I'm using this solution by Tim Down to get selected html in a contenteditable div, and it's working fine (thank you Tim!)
But using Chrome, if I select a html string exactly at the boundaries of a html tag, as in this image: http://i.imgur.com/UiYzrcp.png?1:
what I get it's just plain text (test in this case).
If I expand the selection to a next character (letter c for example), instead I get the correct html (<strong>test</strong> c).
Can I get the full html in Webkit by selecting a word like in the image?
Thanks
Not really. WebKit normalizes each boundary of any range when it's added to the selection so that it conforms to WebKit's idea of valid selection/caret positions in the document. You could change the original function so that it detects the case of a selection containing all the text within an element and expanding the selection range to surround that element (without actually changing the selection). Here's a simple example (you may need something cleverer for a more general case, such as when the text is inside nested elements, detecting block/inline elements, etc.):
Demo: http://jsfiddle.net/btLeg/
Code:
function adjustRange(range) {
range = range.cloneRange();
// Expand range to encompass complete element if element's text
// is completely selected by the range
var container = range.commonAncestorContainer;
var parentElement = container.nodeType == 3 ?
container.parentNode : container;
if (parentElement.textContent == range.toString()) {
range.selectNode(parentElement);
}
return range;
}
function getSelectionHtml() {
var html = "", sel, range;
if (typeof window.getSelection != "undefined") {
sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
range = adjustRange( sel.getRangeAt(i) );
container.appendChild(range.cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
}
return html;
}
Le Code:
http://jsfiddle.net/frf7w/12/
So right now, the current method will take the selected text exactly as... selected, and add tags so that when it is displayed, the page doesn't blow up.
But what I want to do:
Is to, when a user selects a portion of a page, if there are un-matched tags within the selection, the selection will either jump forward or backward (depending on what unmatched tag is in the selection) to the tag(s) that make the selection valid html.
The reason why I want to do this, is because I want a user to be able te select text on a page, and be able to edit that text in a WYSIWYG editor (I can currently do this with the linked code), and then put what they've edited back into the page (currently can't do this, because the method I use adds tags).
The coverAll method in this SO answer has exactly what you want Use javascript to extend a DOM Range to cover partially selected nodes. For some reason extending Selection prototype does not work for me on my chrome, so I extracted the code and substituted this with window.getSelection(). Final code looks like this:
function coverAll() {
var ranges = [];
for(var i=0; i<window.getSelection().rangeCount; i++) {
var range = window.getSelection().getRangeAt(i);
while(range.startContainer.nodeType == 3
|| range.startContainer.childNodes.length == 1)
range.setStartBefore(range.startContainer);
while(range.endContainer.nodeType == 3
|| range.endContainer.childNodes.length == 1)
range.setEndAfter(range.endContainer);
ranges.push(range);
}
window.getSelection().removeAllRanges();
for(var i=0; i<ranges.length; i++) {
window.getSelection().addRange(ranges[i]);
}
return;
}
You can change the boundaries of the selection by adding a range:
var sel = window.getSelection(),
range = sel.getRangeAt(0);
var startEl = sel.anchorNode;
if (startEl != range.commonAncestorContainer) {
while (startEl.parentNode != range.commonAncestorContainer) {
startEl = startEl.parentNode;
}
}
var endEl = sel.focusNode;
if (endEl != range.commonAncestorContainer) {
while (endEl.parentNode != range.commonAncestorContainer) {
endEl = endEl.parentNode;
}
}
range.setStartBefore(startEl);
range.setEndAfter(endEl);
sel.addRange(range);
The above example will give you a selection that is expanded to cover the entire of the tree between the start and end nodes, inclusive (thanks to commonAncestorContainer()).
This treats text nodes as equal to dom elements, but this shouldn't be a problem for you.
Demo: http://jsfiddle.net/Nq6hr/2/
You should work with the nodes given by the selection. It seems extentNode and anchorNode represents the end and the beginning of nodes of the selection both can help you having the "full" selection. https://developer.mozilla.org/fr/DOM/Selection
For the inline editing you should give a try to contentEditable attribute. You can surround the elements of your selection with a span containing this attribute https://developer.mozilla.org/en/DOM/element.contentEditable
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