after skimming all possible questions and answers, i'll try it this way.
I'm programming an RTE, but didn't manage to successfully extract text in a contenteditable element.
The reason for this is, that each browser handles nodes and keypress (#13) events in a slightly different way (as ex.g. one creates 'br', the other 'div', 'p', etc.)
To keep this all consistent, I'm writing a cross-browser editor which kills all default action with e.preventDefault();
Following scenario:
1) User hits the #13 key (check)
2) Caret position detected (check)
3) create new p(aragraph) after the element the caret's in (check)
4) if text(node(s)) between caret and the element's end, extract it (? ? ?)
5) put text(node(s)) to newly created p(aragraph) (check)
As you can imagine, everything works except point 4.
There's a similar functionality in the well-known js rangy library, where a specific selection is being extracted.
What i need is following: Get and extract all text (with tags of course, so splitBoundaries) from the caret on to the end of the contenteditable paragraph (p, h1, h2, ...) element.
Any clues, hints or snippets are welcome!
Thanks in advance.
Kind regards,
p
You can do this by creating a Range object that encompasses the content from the caret to the end of the containing block element and calling its extractContents() method:
function getBlockContainer(node) {
while (node) {
// Example block elements below, you may want to add more
if (node.nodeType == 1 && /^(P|H[1-6]|DIV)$/i.test(node.nodeName)) {
return node;
}
node = node.parentNode;
}
}
function extractBlockContentsFromCaret() {
var sel = window.getSelection();
if (sel.rangeCount) {
var selRange = sel.getRangeAt(0);
var blockEl = getBlockContainer(selRange.endContainer);
if (blockEl) {
var range = selRange.cloneRange();
range.selectNodeContents(blockEl);
range.setStart(selRange.endContainer, selRange.endOffset);
return range.extractContents();
}
}
}
This won't work in IE <= 8, which doesn't support Range or the same selection object as other browsers. You can use Rangy (which you mentioned) to provide IE support. Just replace window.getSelection() with rangy.getSelection().
jsFiddle: http://jsfiddle.net/LwXvk/3/
Related
I created a function that wraps the selected text in the h1 tag, and also deletes the tags when it is called again. The function also deletes tags and creates again if you select an area larger than the previous selection.
However, if after creating the heading from the selection, select another text, and then select the word again, then the tags do not include in the selection (although the selection completely coincides with the created heading). I use the Selection and Range methods.
It turns out that with repeated selection, the tags do not fall into Selection.
I also tried to do auto-replace using a regular expression, but this method is not good, because a phrase or a word can occur several times in the text.
Please tell me, is there any way in Javascript that allows us to find a node that matches this selection?
Maybe I didn’t explain well, and to better show, I created a snippet that demonstrates that. what's happening:
https://jsfiddle.net/narzantaria/2ex6bnq3/2/
Also here is the same code. The function is called by clicking the button:
document.getElementById('h1-trigger').addEventListener('click', function () {
headerFormatter();
});
function headerFormatter() {
let newTag = document.createElement('h1');
if (window.getSelection) {
let sel = window.getSelection();
if (sel.rangeCount) {
let range = sel.getRangeAt(0).cloneRange();
// create an object from range for querying tags
let rangeProxy = sel.getRangeAt(0).cloneContents();
if (rangeProxy.querySelector('h1')) {
let tagContent = rangeProxy.querySelector('h1').innerHTML;
// compare selection length with queried tag length
if (range.startOffset == 1) {
tagContent = tagContent.replace(/(<([^>]+)>)/ig, "");
range.deleteContents();
range.insertNode(document.createTextNode(tagContent));
sel.removeAllRanges();
sel.addRange(range);
return;
}
else {
let rangeToString = range.toString().replace(/(<([^>]+)>)/ig, "");
range.deleteContents();
range.insertNode(document.createTextNode(rangeToString));
sel.removeAllRanges();
sel.addRange(range);
return;
}
} else {
range.surroundContents(newTag);
sel.removeAllRanges();
sel.addRange(range);
}
}
}
}
Regards.
You'll have to do a few things to achieve the type of functionality you desire:
When you insert an H1 tag, add an identifier to it. Store this identifier along with the selected range values. Also, use a boolean flag variable to show the text is being highlighted.
Next time when you click the button, check whether the flag variable is true. If it is, using the stored range values from the previous selection, remove the h1 highlighting.
Once done, you can now add the highlighting to the new range and repeat step 1.
This might be a lengthier approach, but it may be more effective than your current approach.
I hope this helps. Thanks!
A flowchart to explain the steps above
You can get tag where the selection starts/ends using getSelection().anchorNode and getSelection().focusNode.
Is it possible to make something like the autocorrect feature in iOS for contenteditable fields? As the user types, spelling would be checked (I already have this part), but then I need a way to correct the word. I could replace the whole html of the contenteditable, but then cursor position is lost, and there is a possibility that the user would try to type before html is rewritten. Ideally, this would just replace x characters before the user's cursor. Is this possible?
the core replace-in-node-being-typed in feature can be powered by this function:
function runRep(from, to) {
var sel = document.getSelection(),
nd = sel.anchorNode,
text = nd.textContent.slice(0, sel.focusOffset),
newText = text.replace(from, to),
wholeNew = nd.textContent.replace(text, newText),
range = document.createRange();
nd.textContent = wholeNew;
range.setStart(nd, newText.length);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
nd.parentNode.focus()
}
usage: runRep("helo", "hello");, which will replace a mis-spelled "hello" to the left of the cursor in the current node. you need to be careful about sub-string matches. Upgrading the replace() to use a RegExp would likely allow more precise targeting (like only whole-words, ignoring cAsE, etc), but this will work as-is and the RegExp upgrade doesn't change the rest of the code.
not shown is how one actually detects the mis-spelled words, which is another topic...
obligatory fiddle: http://jsfiddle.net/p0dxdnub/
Is it possible to use Javascript in Safari/Firefox/Chrome to search a particular div container for a given text string. I know you can use window.find(str) to search the entire page but is it possible to limit the search area to the div only?
Thanks!
Once you look up your div (which you might do via document.getElementById or any of the other DOM functions, various specs here), you can use either textContent or innerText to find the text of that div. Then you can use indexOf to find the string in that.
Alternately, at a lower level, you can use a recursive function to search through all text nodes in the window, which sounds a lot more complicated than it is. Basically, starting from your target div (which is an Element), you can loop through its childNodes and search their nodeValue string (if they're Texts) or recurse into them (if they're Elements).
The trick is that a naive version would fail to find "foo" in this markup:
<p><span>fo</span>o</p>
...since neither of the two Text nodes there has a nodeValue with "foo" in it (one of them has "fo", the other "o").
Depending on what you are trying to do, there is an interesting way of doing this that does work (does require some work).
First, searching starts at the location where the user last clicked. So to get to the correct context, you can force a click on the div. This will place the internal pointer at the beginning of the div.
Then, you can use window.find as usual to find the element. It will highlight and move toward the next item found. You could create your own dialog and handle the true or false returned by find, as well as check the position. So for example, you could save the current scroll position, and if the next returned result is outside of the div, you restore the scroll. Also, if it returns false, then you can say there were no results found.
You could also show the default search box. In that case, you would be able to specify the starting position, but not the ending position because you lose control.
Some example code to help you get started. I could also try putting up a jsfiddle if there is enough interest.
Syntax:
window.find(aStringToFind, bCaseSensitive, bBackwards, bWrapAround, bWholeWord, bSearchInFrames, bShowDialog);
For example, to start searching inside of myDiv, try
document.getElementById("myDiv").click(); //Place cursor at the beginning
window.find("t", 0, 0, 0, 0, 0, 0); //Go to the next location, no wrap around
You could set a blur (lose focus) event handler to let you know when you leave the div so you can stop the search.
To save the current scroll position, use document.body.scrollTop. You can then set it back if it trys to jump outside of the div.
Hope this helps!
~techdude
As per the other answer you won't be able to use the window.find functionality for this. The good news is, you won't have to program this entirely yourself, as there nowadays is a library called rangy which helps a lot with this. So, as the code itself is a bit too much to copy paste into this answer I will just refer to a code example of the rangy library that can be found here. Looking in the code you will find
searchScopeRange.selectNodeContents(document.body);
which you can replace with
searchScopeRange.selectNodeContents(document.getElementById("content"));
To search only specifically in the content div.
If you are still looking for someting I think I found a pretty nice solution;
Here it is : https://www.aspforums.net/Threads/211834/How-to-search-text-on-web-page-similar-to-CTRL-F-using-jQuery/
And I'm working on removing jQuery (wip) : codepen.io/eloiletagant/pen/MBgOPB
Hope it's not too late :)
You can make use of Window.find() to search for all occurrences in a page and Node.contains() to filter out unsuitable search results.
Here is an example of how to find and highlight all occurrences of a string in a particular element:
var searchText = "something"
var container = document.getElementById("specificContainer");
// selection object
var sel = window.getSelection()
sel.collapse(document.body, 0)
// array to store ranges found
var ranges = []
// find all occurrences in a page
while (window.find(searchText)) {
// filter out search results outside of a specific element
if (container.contains(sel.anchorNode)){
ranges.push(sel.getRangeAt(sel.rangeCount - 1))
}
}
// remove selection
sel.collapseToEnd()
// Handle ranges outside of the while loop above.
// Otherwise Safari freezes for some reason (Chrome doesn't).
if (ranges.length == 0){
alert("No results for '" + searchText + "'")
} else {
for (var i = 0; i < ranges.length; i++){
var range = ranges[i]
if (range.startContainer == range.endContainer){
// Range includes just one node
highlight(i, range)
} else {
// More complex case: range includes multiple nodes
// Get all the text nodes in the range
var textNodes = getTextNodesInRange(
range.commonAncestorContainer,
range.startContainer,
range.endContainer)
var startOffset = range.startOffset
var endOffset = range.endOffset
for (var j = 0; j < textNodes.length; j++){
var node = textNodes[j]
range.setStart(node, j==0? startOffset : 0)
range.setEnd(node, j==textNodes.length-1?
endOffset : node.nodeValue.length)
highlight(i, range)
}
}
}
}
function highlight(index, range){
var newNode = document.createElement("span")
// TODO: define CSS class "highlight"
// or use <code>newNode.style.backgroundColor = "yellow"</code> instead
newNode.className = "highlight"
range.surroundContents(newNode)
// scroll to the first match found
if (index == 0){
newNode.scrollIntoView()
}
}
function getTextNodesInRange(rootNode, firstNode, lastNode){
var nodes = []
var startNode = null, endNode = lastNode
var walker = document.createTreeWalker(
rootNode,
// search for text nodes
NodeFilter.SHOW_TEXT,
// Logic to determine whether to accept, reject or skip node.
// In this case, only accept nodes that are between
// <code>firstNode</code> and <code>lastNode</code>
{
acceptNode: function(node) {
if (!startNode) {
if (firstNode == node){
startNode = node
return NodeFilter.FILTER_ACCEPT
}
return NodeFilter.FILTER_REJECT
}
if (endNode) {
if (lastNode == node){
endNode = null
}
return NodeFilter.FILTER_ACCEPT
}
return NodeFilter.FILTER_REJECT
}
},
false
)
while(walker.nextNode()){
nodes.push(walker.currentNode);
}
return nodes;
}
For the Range object, see https://developer.mozilla.org/en-US/docs/Web/API/Range.
For the TreeWalker object, see https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
var elements = [];
$(document).find("*").filter(function () {
if($(this).text().contains(yourText))
elements.push($(this));
});
console.log(elements);
I didn't try it, but according the jQuery documentation it should work.
Here is how I am doing with jquery:
var result = $('#elementid').text().indexOf('yourtext') > -1
it will return true or false
Maybe you are trying to not use jquery...but if not, you can use this $('div:contains(whatyouarelookingfor)') the only gotcha is that it could return parent elements that also contain the child div that matches.
I am trying to highlight a part of the text on my website. This highlighted text will be saved for the specific user in a database and when the document is opened again it will show the previously highlighted text. I assumed I would be using javascript to highlight the text but I cannot seem to find a way to pinpoint where the word is that I am highlighting.
function getSelText()
{
var txt = '';
if (window.getSelection)
{
txt = window.getSelection();
}
else if (document.getSelection)
{
txt = document.getSelection();
}
else if (document.selection)
{
txt = document.selection.createRange().text;
}
else return "";
return txt;
}
I am using that to get the selection but I cannot figure out where the selection is in the text. The biggest annoyance is when I have duplicates within the line or text so if I were to use search then I would find the first instance and not the one I was looking for.
So the question is : How do you pinpoint a word or a selection in the entire document?
You can use my Rangy library and its selection serialization module for this. Rangy's core provides a consistent Selection and Range API for all browsers and the serializer module builds on this by converting each selection boundary into a path through the document. See the linked documentation for more details.
This certainly isn't a trivial task, since you're faced with the two major problems of locating the text in the current document, and then being able to find it again on a subsequent page load. The problem is further complicated if the content of your page is subject to change, since you can't even rely on the relative position of the text to stay the same.
You may want to consider whether or not this is the best approach for whatever you're trying to accomplish given the effort required, but here's something that might get you started in the right direction:
function getSelection() {
var selection, position;
if (window.getSelection) {
selection = window.getSelection();
if (selection && !selection.isCollapsed) {
position = {
'offset': selection.anchorOffset,
'length': selection.toString().length,
// We're assuming this will be a text node
'node': selection.anchorNode.parentNode
};
}
} else if (document.selection) {
selection = document.selection.createRange();
if (selection && selection.text.length) {
var text = selection.parentElement().innerText,
range = document.body.createTextRange(),
last = 0, index = -1;
range.moveToElementText(selection.parentElement());
// Figure out which instance of the selected text in the overall
// text is the correct one by walking through the occurrences
while ((index = text.indexOf(selection.text, ++index)) !== -1) {
range.moveStart('character', index - last);
last = index;
if (selection.offsetLeft == range.offsetLeft && selection.offsetTop == range.offsetTop) {
break;
}
}
position = {
'offset': index,
'length': selection.text.length,
'node': selection.parentElement()
};
}
}
return position;
}
As well as a method to select the text again:
function setSelection(position) {
if (!position || !position.node) {
return;
}
var selection, range, element;
if (document.createRange) {
element = position.node.childNodes[0];
range = document.createRange();
range.setStart(element, position.offset);
range.setEnd(element, position.offset + position.length);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
} else if (document.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(position.node);
range.collapse(true);
range.move('character', position.offset);
range.moveEnd('character', position.length);
range.select();
}
}
This code makes the rather naïve assumption that the selected text all resides in the same DOM element. Chances are high that if a user is selecting arbitrary text this won't be the case.
Given that the Selection object accounts for this with the anchorNode and focusNode properties, you could try and work around this, though dealing with the TextRange object in Internet Explorer might prove to be a bit more problematic.
There's also the problem of how to keep track of the position.node value across page requests. In my jsFiddle sample, I've used a slightly modified version of a selector-generating jQuery function to generate a selector string that can be saved and used to reselect the correct node later on. Note that the process is relatively trivial, so you could easily do it without jQuery – it just happened to save some effort in this case.
Of course if you're changing the DOM between visits, this approach will likely be fairly unstable. If you aren't though, I feel like it's probably one of the more reliable options.
createRange creates a textRange object. It has all kinds of useful information:
var range = document.selection.createRange();
var left = range.boundingLeft;
var top = range.boundingTop;
The Problem
I am trying to figure out the offset of a selection from a particular node with javascript.
Say I have the following HTML
<p>Hi there. This <strong>is blowing my mind</strong> with difficulty.</p>
If I select from blowing to difficulty, it gives me the offset from the #text node inside of the <strong>. I need the string offset from the <p>'s innerHTML and the length of the selection. In this case, the offset would be 26 and the length would be 40.
My first thought was to do something with string offsets, etc. but you could easily have something like
<p> Hi there. This <strong>is awesome</strong>. For real. It <strong>is awesome</strong>.</p>
which would break that method because there are identical nodes. I also need the option to throw out nodes. Say I have something like this
<p>Hi there. This <strong>is blowing my mind</strong> with difficulty.</p>
I want to throw out an elements with rel="inserted" when I do the calculation. I still want 26 and 40 as the result.
What I'm looking for
The solution needs to be recursive. If there was a <span> with a <strong> in it, it would still need to traverse to the <p>.
The solution needs to remove the length of any element with rel="inserted". The contents are important, but the tags themselves are not. All other tags are important. I'd strongly prefer not to remove any elements from the DOM when I do all of this.
I am using document.getSelection() to get the selection object. This solution only has to work in WebKit. jQuery is an option, but I'd prefer to it without it if possible.
Any ideas would be greatly appreciated.
I have no control over the HTML I doing all of this on.
I think I solved my issue. I ended not calculating the offset like I originally planned. I am storing the "path" from the chunk (aka <p>). Here is the code:
function isChunk(node) {
if (node == undefined || node == null) {
return false;
}
return node.nodeName == "P";
}
function pathToChunk(node) {
var components = new Array();
// While the last component isn't a chunk
var found = false;
while (found == false) {
var childNodes = node.parentNode.childNodes;
var children = new Array(childNodes.length);
for (var i = 0; i < childNodes.length; i++) {
children[i] = childNodes[i];
}
components.unshift(children.indexOf(node));
if (isChunk(node.parentNode) == true) {
found = true
} else {
node = node.parentNode;
}
}
return components.join("/");
}
function nodeAtPathFromChunk(chunk, path) {
var components = path.split("/");
var node = chunk;
for (i in components) {
var component = components[i];
node = node.childNodes[component];
}
return node;
}
With all of that, you can do something like this:
var p = document.getElementsByTagName('p')[0];
var piece = nodeAtPathFromChunk(p, "1/0"); // returns desired node
var path = pathToChunk(piece); // returns "1/0"
Now I just need to expand all of that to support the beginning and the end of a selection. This is a great building block though.
What does this offset actually mean? An offset within the innerHTML of an element is going to be extremely fragile: any insertion of a new node or change to an attribute of an element preceding the point in the document the offset represents is going to make that offset invalid.
I strongly recommend using the browser's built-in support for this in the form of DOM Range. You can get hold of a range representing the current selection as follows:
var range = window.getSelection().getRangeAt(0);
If you're going to be manipulating the DOM based on this offset that you want, you're best off doing so using nodes instead of string representations of those nodes.
you can use the following java script code:
var text = window.getSelection();
var start = text.anchorOffset;
alert(start);
var end = text.focusOffset - text.anchorOffset;
alert(end);
Just check if your selected element is a paragraph, and if not use something like Prototype's Element.up() method to select the first paragraph parent.
For example:
if(selected_element.nodeName != 'P') {
parent_paragraph = $(selected_element).up('p');
}
Then just find the difference between the parent_paragraph's text offset and your selected_element's text offset.
Maybe you could use the jQuery selectors to ignore the rel="inserted"?
$('a[rel!=inserted]').doSomething();
http://api.jquery.com/attribute-not-equal-selector/
What code are you using now to select from blowing to difficulty?