I use the javascript code below to find all occurrences of a string in UIWebView:
UIWebViewSearch.js:
var uiWebview_SearchResultCount = 0;
/*!
#method uiWebview_HighlightAllOccurencesOfStringForElement
#abstract // helper function, recursively searches in elements and their child nodes
#discussion // helper function, recursively searches in elements and their child nodes
element - HTML elements
keyword - string to search
*/
function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) {
if (element) {
if (element.nodeType == 3) { // Text node
var count = 0;
var elementTmp = element;
while (true) {
var value = elementTmp.nodeValue; // Search for keyword in text node
var idx = value.toLowerCase().indexOf(keyword);
if (idx < 0) break;
count++;
elementTmp = document.createTextNode(value.substr(idx+keyword.length));
}
uiWebview_SearchResultCount += count;
var index = uiWebview_SearchResultCount;
while (true) {
var value = element.nodeValue; // Search for keyword in text node
var idx = value.toLowerCase().indexOf(keyword);
if (idx < 0) break; // not found, abort
//we create a SPAN element for every parts of matched keywords
var span = document.createElement("span");
var text = document.createTextNode(value.substr(idx,keyword.length));
span.appendChild(text);
span.setAttribute("class","uiWebviewHighlight");
span.style.backgroundColor="yellow";
span.style.color="black";
index--;
span.setAttribute("id", "SEARCH WORD"+(index));
//span.setAttribute("id", "SEARCH WORD"+uiWebview_SearchResultCount);
//element.parentNode.setAttribute("id", "SEARCH WORD"+uiWebview_SearchResultCount);
//uiWebview_SearchResultCount++; // update the counter
text = document.createTextNode(value.substr(idx+keyword.length));
element.deleteData(idx, value.length - idx);
var next = element.nextSibling;
//alert(element.parentNode);
element.parentNode.insertBefore(span, next);
element.parentNode.insertBefore(text, next);
element = text;
}
} else if (element.nodeType == 1) { // Element node
if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') {
for (var i=element.childNodes.length-1; i>=0; i--) {
uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword);
}
}
}
}
}
// the main entry point to start the search
function uiWebview_HighlightAllOccurencesOfString(keyword) {
uiWebview_RemoveAllHighlights();
uiWebview_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());
}
// helper function, recursively removes the highlights in elements and their childs
function uiWebview_RemoveAllHighlightsForElement(element) {
if (element) {
if (element.nodeType == 1) {
if (element.getAttribute("class") == "uiWebviewHighlight") {
var text = element.removeChild(element.firstChild);
element.parentNode.insertBefore(text,element);
element.parentNode.removeChild(element);
return true;
} else {
var normalize = false;
for (var i=element.childNodes.length-1; i>=0; i--) {
if (uiWebview_RemoveAllHighlightsForElement(element.childNodes[i])) {
normalize = true;
}
}
if (normalize) {
element.normalize();
}
}
}
}
return false;
}
// the main entry point to remove the highlights
function uiWebview_RemoveAllHighlights() {
uiWebview_SearchResultCount = 0;
uiWebview_RemoveAllHighlightsForElement(document.body);
}
function uiWebview_ScrollTo(idx) {
var scrollTo = document.getElementById("SEARCH WORD" + idx);
if (scrollTo) scrollTo.scrollIntoView();
}
And in my ViewController.swift file, I load the javascript file and perform a search like so:
func highlightAllOccurencesOfString(str:String) -> Int {
let path = Bundle.main.path(forResource: "UIWebViewSearch", ofType: "js")
var jsCode = ""
do{
jsCode = try String(contentsOfFile: path!)
myWebView.stringByEvaluatingJavaScript(from: jsCode)
let startSearch = "uiWebview_HighlightAllOccurencesOfString('\(str)')"
myWebView.stringByEvaluatingJavaScript(from: startSearch)
let result = myWebView.stringByEvaluatingJavaScript(from: "uiWebview_SearchResultCount")!
}catch
{
// do something
}
return Int(result)!
}
This finds and highlights all occurrences of a string within the webview, but I only want the first occurrence highlighted.
For example, with this code when I call:
highlightAllOccurencesOfString(str:"Hello")
All instances of "Hello" gets highlighted in the webview:
Hello Frank, Hello Noah
But I want this result:
Hello Frank, Hello Noah
How do I modify the javascript code to highlight only the first occurrence of a searched string?
UPDATE: I tried JonLuca's answer below but nothing got highlighted.
Any help would be greatly appreciated
The function is operating like a tree, where it checks if the element is a text node, and if not it calls itself on every one of its children.
You have to let the caller know that you found a single element to highlight, and then leave.
Also, the way this searches through it does not guarantee that what you consider is the "first" occurrence of the word will be the first occurence within the document.body tree.
Within the code, have it return true if it found a word to highlight, and false otherwise. That'll prevent it from continuing its search. Modify the js like so:
function uiWebview_HighlightAllOccurencesOfStringForElement(element, keyword) {
if (element) {
if (element.nodeType == 3) { // Text node
var count = 0;
var elementTmp = element;
var value = elementTmp.nodeValue; // Search for keyword in text node
var idx = value.toLowerCase().indexOf(keyword);
if (idx < 0) {
return false;
}
count++;
elementTmp = document.createTextNode(value.substr(idx + keyword.length));
uiWebview_SearchResultCount += count;
var index = uiWebview_SearchResultCount;
var value = element.nodeValue; // Search for keyword in text node
var idx = value.toLowerCase().indexOf(keyword);
if (idx < 0) {
return false;
} // not found, abort
//we create a SPAN element for every parts of matched keywords
var span = document.createElement("span");
var text = document.createTextNode(value.substr(idx, keyword.length));
span.appendChild(text);
span.setAttribute("class", "uiWebviewHighlight");
span.style.backgroundColor = "yellow";
span.style.color = "black";
index--;
span.setAttribute("id", "SEARCH WORD" + (index));
//span.setAttribute("id", "SEARCH WORD"+uiWebview_SearchResultCount);
//element.parentNode.setAttribute("id", "SEARCH WORD"+uiWebview_SearchResultCount);
//uiWebview_SearchResultCount++; // update the counter
text = document.createTextNode(value.substr(idx + keyword.length));
element.deleteData(idx, value.length - idx);
var next = element.nextSibling;
//alert(element.parentNode);
element.parentNode.insertBefore(span, next);
element.parentNode.insertBefore(text, next);
element = text;
return true;
} else if (element.nodeType == 1) { // Element node
if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') {
for (var i = element.childNodes.length - 1; i >= 0; i--) {
if (uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i], keyword)) {
return true;
}
}
}
}
}
}
This isn't guaranteed to be correct, or that it covers all of your use cases. However, I had it return true on the first occurrence of finding the highlighted word. It should stop executing and not highlight any other words.
Let me know if this works or not - I don't have the ability to test it right now.
Related
I want to be able to let the user wile typing in the text area if he typed a hash-tag followed by one or more character as he type the text get highlighted till he hits space.
The thing is i want to achieve something like Facebook's new hash-tag feature, I've done the logic the coding but still not able to achieve it visually.
The approach i tried is by using Jquery as follows:
<textarea id="txtArea">Here is my #Hash</textarea>
$("#txtArea").onkeyup(function(){
var result = $("#txtArea").match(/ #[\w]+/g);
//result = '#Hash'
});
But i couldn't complete & i don't know where to go from here so any solution, advice, plugin that i could use i'll be very grateful.
I don't believe there is any way to highlight words (other than one single highlight) within a basic textarea as it does not accept markup, you could turn the textarea into a small Rich Text editor, but that seems a little overcomplicated. I would probably go for similar approach to the editor here on SO, and have a preview window below so that you can see what is being marked. You could use something like this, of course you may want to change how it works a little to suit your exact needs. It should at least give you some ideas.
CSS
#preview {
height: 2em;
width: 12em;
border-style: solid;
border-width: 1px;
}
.hashSymbol {
color: #f90;
}
HTML
<textarea id="userInput"></textarea>
<div id="preview"></div>
Javascript
/*jslint maxerr: 50, indent: 4, browser: true */
(function () {
"use strict";
function walkTheDOM(node, func) {
func(node);
node = node.firstChild;
while (node) {
walkTheDOM(node, func);
node = node.nextSibling;
}
}
function getTextNodes(element) {
var nodes = [];
walkTheDOM(element, function (node) {
if (node.nodeType === 3) {
nodes.push(node);
}
});
return nodes;
}
function escapeRegex(string) {
return string.replace(/[\[\](){}?*+\^$\\.|]/g, "\\$&");
}
function highlight(element, string, classname) {
var nodes = getTextNodes(element),
length = nodes.length,
stringLength = string.length,
rx = new RegExp("\\B" + escapeRegex(string)),
i = 0,
index,
text,
newContent,
span,
node;
while (i < length) {
node = nodes[i];
newContent = document.createDocumentFragment();
text = node.nodeValue;
index = text.search(rx);
while (index !== -1) {
newContent.appendChild(document.createTextNode(text.slice(0, index)));
text = text.slice(index + stringLength);
span = document.createElement("span");
span.className = classname;
span.appendChild(document.createTextNode(string));
newContent.appendChild(span);
index = text.search(rx);
}
newContent.appendChild(document.createTextNode(text));
node.parentNode.replaceChild(newContent, node);
i += 1;
}
}
function addEvent(elem, event, fn) {
if (typeof elem === "string") {
elem = document.getElementById(elem);
}
function listenHandler(e) {
var ret = fn.apply(null, arguments);
if (ret === false) {
e.stopPropagation();
e.preventDefault();
}
return ret;
}
function attachHandler() {
window.event.target = window.event.srcElement;
var ret = fn.call(elem, window.event);
if (ret === false) {
window.event.returnValue = false;
window.event.cancelBubble = true;
}
return ret;
}
if (elem.addEventListener) {
elem.addEventListener(event, listenHandler, false);
} else {
elem.attachEvent("on" + event, attachHandler);
}
}
function emptyNode(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
function toPreviewHighlight(e, to) {
if (typeof to === "string") {
to = document.getElementById(to);
}
var value = e.target.value,
tags = value.match(/\B#\w+/g) || [],
index = tags.length - 1,
lookup = {},
fragment,
length,
tag;
while (index >= 0) {
tag = tags[index];
if (!tag.length || tag === "#" || tag.charAt(0) !== "#" || lookup[tag]) {
tags.splice(index, 1);
} else {
lookup[tag] = true;
}
index -= 1;
}
fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode(value));
index = 0;
length = tags.length;
while (index < length) {
tag = tags[index];
highlight(fragment, tag, "hashSymbol");
index += 1;
}
emptyNode(to);
to.appendChild(fragment);
}
addEvent("userInput", "keyup", function (e) {
toPreviewHighlight(e, "preview");
});
}());
On jsfiddle
This code is slightly modified from other questions and answers here on SO (resusing code is good)
if text contains '#' change color of '#'
How to extract value between # and space in textarea value
Using JavaScript, I want to use XPath to look for the presence of a string, then refresh the page based on that
Say I've selected a span tag in a large html document. If I treat the entire html document as a big nested array, I can find the position of the span tag through array indexes. How can I output the index path to that span tag? eg: 1,2,0,12,7 using JavaScript.
Also, how can I select the span tag by going through the index path?
This will work. It returns the path as an array instead of a string.
Updated per your request.
You can check it out here: http://jsbin.com/isata5/edit (hit preview)
// get a node's index.
function getIndex (node) {
var parent=node.parentElement||node.parentNode, i=-1, child;
while (parent && (child=parent.childNodes[++i])) if (child==node) return i;
return -1;
}
// get a node's path.
function getPath (node) {
var parent, path=[], index=getIndex(node);
(parent=node.parentElement||node.parentNode) && (path=getPath(parent));
index > -1 && path.push(index);
return path;
}
// get a node from a path.
function getNode (path) {
var node=document.documentElement, i=0, index;
while ((index=path[++i]) > -1) node=node.childNodes[index];
return node;
}
This example should work on this page in your console.
var testNode=document.getElementById('comment-4007919');
console.log("testNode: " + testNode.innerHTML);
var testPath=getPath(testNode);
console.log("testPath: " + testPath);
var testFind=getNode(testPath);
console.log("testFind: " + testFind.innerHTML);
Using jquery:
var tag = $('#myspan_id');
var index_path = [];
while(tag) {
index_path.push(tag.index());
tag = tag.parent();
}
index_path = index_path.reverse();
Using the DOM
node = /*Your span element*/;
var currentNode = node;
var branch = [];
var cn; /*Stores a Nodelist of the current parent node's children*/
var i;
while (currentNode.parentNode !== null)
{
cn = currentNode.parentNode.childNodes;
for (i=0;i<cn.length;i++)
{
if (cn[i] === currentNode)
{
branch.push(i);
break;
}
}
currentNode = currentNode.parentNode;
}
cn = document.childNodes;
for (i=0;i<cn.length;i++)
{
if (cn[i] === currentNode)
{
branch.push(i);
break;
}
}
node.innerHTML = branch.reverse().join(",");
composedPath for native event.
(function (E, d, w) {
if (!E.composedPath) {
E.composedPath = function () {
if (this.path) {
return this.path;
}
var target = this.target;
this.path = [];
while (target.parentNode !== null) {
this.path.push(target);
target = target.parentNode;
}
this.path.push(d, w);
return this.path;
};
}
})(Event.prototype, document, window);
use:
var path = event.composedPath()
When I try to pass text which spreads throughout a few block elements the window.find method dosent work:
HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
</head>
<body>
<p>search me</p><b> I could be the answer</b>
</body>
</html>
JavaScript:
window.find("meI could be");
Or:
str = "me";
str+= "\n";
str+="I could be t";
window.find(str);
This happens when the <p> element is present between the search term.
The end result should be a GUI selection on the text in the page, I do not want to search if it exists.
I would like to know how to achieve this in Firefox(at least) and internet explorer.
Note: I can't change the dom (e.g. change to display inline).
Edit:
Here is something I tried after #Alexey Lebedev's comment, but it also finds the script (tag [...] text):
Can I make it more simple? (better)?
function nativeTreeWalker(startNode) {
var walker = document.createTreeWalker(
startNode,
NodeFilter.SHOW_TEXT,
null,
false
);
var node;
var textNodesV = [];
var textNodes = [];
node = walker.nextNode();
while(node ) {
if(node.nodeValue.trim()){
textNodes.push(node);
textNodesV.push(node.nodeValue);
//console.log(node.nodeValue);
}
node = walker.nextNode();
}
return [textNodes,textNodesV];
}
var result = nativeTreeWalker(document.body);
var textNodes = result[0];
var textNodesV = result[1];
var param = " Praragraph.Test 3 Praragr";
paramArr = param.split(/(?=[\S])(?!\s)(?=[\W])(?=[\S])/g);
//Fix split PARAM
for(i=0;i<paramArr.length-1;i++){
paramArr[i]= paramArr[i]+paramArr[i+1].charAt(0);
paramArr[i+1] = paramArr[i+1].substring(1,paramArr[i+1].length);
}
//Fix last element PARAM
if(paramArr[paramArr.length-1] === ""){
paramArr.splice(paramArr.length-1,1);
}
//console.log(paramArr);
var startNode,startOffset,sFound=false,
endNode,endOffset;
for(i=0;i<paramArr.length;i++){
for(j=0;j<textNodesV.length;j++){
//Fully Equal
var pos = textNodesV[j].indexOf(paramArr[i]);
if(pos != -1){
if(!sFound){
startNode = textNodes[j];
startOffset = pos;
sFound=true;
}else{
endNode = textNodes[j];
endOffset = pos+paramArr[i].length;
break;
}
}
}
}
console.log(startNode);
console.log(startOffset);
console.log(endNode);
console.log(endOffset);
var range = document.createRange();
range.setStart(startNode,startOffset);
range.setEnd(endNode,endOffset);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
Note: No jQuery (only Raw JS).
JS Bin demo: http://jsbin.com/aqiciv/1/
If you want this to work in IE < 9 you'll need to add MS-specific selection code (nightmare), or use Rangy.js (pretty heavy).
function visibleTextNodes() {
var walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ALL,
function(node) {
if (node.nodeType == 3) {
return NodeFilter.FILTER_ACCEPT;
} else if (node.offsetWidth && node.offsetHeight && node.style.visibility != 'hidden') {
return NodeFilter.FILTER_SKIP;
} else {
return NodeFilter.FILTER_REJECT;
}
},
false
);
for (var nodes = []; walker.nextNode();) {
nodes.push(walker.currentNode);
}
return nodes;
}
// Find the first match, select and scroll to it.
// Case- and whitespace- insensitive.
// For better scrolling to selection see https://gist.github.com/3744577
function highlight(needle) {
needle = needle.replace(/\s/g, '').toLowerCase();
var textNodes = visibleTextNodes();
for (var i = 0, texts = []; i < textNodes.length; i++) {
texts.push(textNodes[i].nodeValue.replace(/\s/g, '').toLowerCase());
}
var matchStart = texts.join('').indexOf(needle);
if (matchStart < 0) {
return false;
}
var nodeAndOffsetAtPosition = function(position) {
for (var i = 0, consumed = 0; consumed + texts[i].length < position; i++) {
consumed += texts[i].length;
}
var whitespacePrefix = textNodes[i].nodeValue.match(/^\s*/)[0];
return [textNodes[i], position - consumed + whitespacePrefix.length];
};
var range = document.createRange();
range.setStart.apply(range, nodeAndOffsetAtPosition(matchStart));
range.setEnd.apply( range, nodeAndOffsetAtPosition(matchStart + needle.length));
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
range.startContainer.parentNode.scrollIntoView();
}
highlight('hello world');
Say I've selected a span tag in a large html document. If I treat the entire html document as a big nested array, I can find the position of the span tag through array indexes. How can I output the index path to that span tag? eg: 1,2,0,12,7 using JavaScript.
Also, how can I select the span tag by going through the index path?
This will work. It returns the path as an array instead of a string.
Updated per your request.
You can check it out here: http://jsbin.com/isata5/edit (hit preview)
// get a node's index.
function getIndex (node) {
var parent=node.parentElement||node.parentNode, i=-1, child;
while (parent && (child=parent.childNodes[++i])) if (child==node) return i;
return -1;
}
// get a node's path.
function getPath (node) {
var parent, path=[], index=getIndex(node);
(parent=node.parentElement||node.parentNode) && (path=getPath(parent));
index > -1 && path.push(index);
return path;
}
// get a node from a path.
function getNode (path) {
var node=document.documentElement, i=0, index;
while ((index=path[++i]) > -1) node=node.childNodes[index];
return node;
}
This example should work on this page in your console.
var testNode=document.getElementById('comment-4007919');
console.log("testNode: " + testNode.innerHTML);
var testPath=getPath(testNode);
console.log("testPath: " + testPath);
var testFind=getNode(testPath);
console.log("testFind: " + testFind.innerHTML);
Using jquery:
var tag = $('#myspan_id');
var index_path = [];
while(tag) {
index_path.push(tag.index());
tag = tag.parent();
}
index_path = index_path.reverse();
Using the DOM
node = /*Your span element*/;
var currentNode = node;
var branch = [];
var cn; /*Stores a Nodelist of the current parent node's children*/
var i;
while (currentNode.parentNode !== null)
{
cn = currentNode.parentNode.childNodes;
for (i=0;i<cn.length;i++)
{
if (cn[i] === currentNode)
{
branch.push(i);
break;
}
}
currentNode = currentNode.parentNode;
}
cn = document.childNodes;
for (i=0;i<cn.length;i++)
{
if (cn[i] === currentNode)
{
branch.push(i);
break;
}
}
node.innerHTML = branch.reverse().join(",");
composedPath for native event.
(function (E, d, w) {
if (!E.composedPath) {
E.composedPath = function () {
if (this.path) {
return this.path;
}
var target = this.target;
this.path = [];
while (target.parentNode !== null) {
this.path.push(target);
target = target.parentNode;
}
this.path.push(d, w);
return this.path;
};
}
})(Event.prototype, document, window);
use:
var path = event.composedPath()
I'm trying to get all the DOM nodes that are within a range object, what's the best way to do this?
var selection = window.getSelection(); //what the user has selected
var range = selection.getRangeAt(0); //the first range of the selection
var startNode = range.startContainer;
var endNode = range.endContainer;
var allNodes = /*insert magic*/;
I've been been thinking of a way for the last few hours and came up with this:
var getNextNode = function(node, skipChildren){
//if there are child nodes and we didn't come from a child node
if (node.firstChild && !skipChildren) {
return node.firstChild;
}
if (!node.parentNode){
return null;
}
return node.nextSibling
|| getNextNode(node.parentNode, true);
};
var getNodesInRange = function(range){
var startNode = range.startContainer.childNodes[range.startOffset]
|| range.startContainer;//it's a text node
var endNode = range.endContainer.childNodes[range.endOffset]
|| range.endContainer;
if (startNode == endNode && startNode.childNodes.length === 0) {
return [startNode];
};
var nodes = [];
do {
nodes.push(startNode);
}
while ((startNode = getNextNode(startNode))
&& (startNode != endNode));
return nodes;
};
However when the end node is the parent of the start node it returns everything on the page. I'm sure I'm overlooking something obvious? Or maybe going about it in totally the wrong way.
MDC/DOM/range
Here's an implementation I came up with to solve this:
function getNextNode(node)
{
if (node.firstChild)
return node.firstChild;
while (node)
{
if (node.nextSibling)
return node.nextSibling;
node = node.parentNode;
}
}
function getNodesInRange(range)
{
var start = range.startContainer;
var end = range.endContainer;
var commonAncestor = range.commonAncestorContainer;
var nodes = [];
var node;
// walk parent nodes from start to common ancestor
for (node = start.parentNode; node; node = node.parentNode)
{
nodes.push(node);
if (node == commonAncestor)
break;
}
nodes.reverse();
// walk children and siblings from start until end is found
for (node = start; node; node = getNextNode(node))
{
nodes.push(node);
if (node == end)
break;
}
return nodes;
}
The getNextNode will skip your desired endNode recursively if its a parent node.
Perform the conditional break check inside of the getNextNode instead:
var getNextNode = function(node, skipChildren, endNode){
//if there are child nodes and we didn't come from a child node
if (endNode == node) {
return null;
}
if (node.firstChild && !skipChildren) {
return node.firstChild;
}
if (!node.parentNode){
return null;
}
return node.nextSibling
|| getNextNode(node.parentNode, true, endNode);
};
and in while statement:
while (startNode = getNextNode(startNode, false , endNode));
The Rangy library has a Range.getNodes([Array nodeTypes[, Function filter]]) function.
I made 2 additional fixes based on MikeB's answer to improve the accuracy of the selected nodes.
I'm particularly testing this on select all operations, other than range selection made by dragging the cursor along text spanning across multiple elements.
In Firefox, hitting select all (CMD+A) returns a range where it's startContainer & endContainer is the contenteditable div, the difference is in the startOffset & endOffset where it's respectively the index of the first and the last child node.
In Chrome, hitting select all (CMD+A) returns a range where it's startContainer is the first child node of the contenteditable div, and the endContainer is the last child node of the contenteditable div.
The modifications I've added work around the discrepancies between the two. You can see the comments in the code for additional explanation.
function getNextNode(node) {
if (node.firstChild)
return node.firstChild;
while (node) {
if (node.nextSibling) return node.nextSibling;
node = node.parentNode;
}
}
function getNodesInRange(range) {
// MOD #1
// When the startContainer/endContainer is an element, its
// startOffset/endOffset basically points to the nth child node
// where the range starts/ends.
var start = range.startContainer.childNodes[range.startOffset] || range.startContainer;
var end = range.endContainer.childNodes[range.endOffset] || range.endContainer;
var commonAncestor = range.commonAncestorContainer;
var nodes = [];
var node;
// walk parent nodes from start to common ancestor
for (node = start.parentNode; node; node = node.parentNode)
{
nodes.push(node);
if (node == commonAncestor)
break;
}
nodes.reverse();
// walk children and siblings from start until end is found
for (node = start; node; node = getNextNode(node))
{
// MOD #2
// getNextNode might go outside of the range
// For a quick fix, I'm using jQuery's closest to determine
// when it goes out of range and exit the loop.
if (!$(node.parentNode).closest(commonAncestor)[0]) break;
nodes.push(node);
if (node == end)
break;
}
return nodes;
};
below code solve your problem
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>payam jabbari</title>
<script src="http://code.jquery.com/jquery-2.0.2.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function(){
var startNode = $('p.first').contents().get(0);
var endNode = $('span.second').contents().get(0);
var range = document.createRange();
range.setStart(startNode, 0);
range.setEnd(endNode, 5);
var selection = document.getSelection();
selection.addRange(range);
// below code return all nodes in selection range. this code work in all browser
var nodes = range.cloneContents().querySelectorAll("*");
for(var i=0;i<nodes.length;i++)
{
alert(nodes[i].innerHTML);
}
});
</script>
</head>
<body>
<div>
<p class="first">Even a week ago, the idea of a Russian military intervention in Ukraine seemed far-fetched if not totally alarmist. But the arrival of Russian troops in Crimea over the weekend has shown that he is not averse to reckless adventures, even ones that offer little gain. In the coming days and weeks</p>
<ol>
<li>China says military will respond to provocations.</li>
<li >This Man Has Served 20 <span class="second"> Years—and May Die—in </span> Prison for Marijuana.</li>
<li>At White House, Israel's Netanyahu pushes back against Obama diplomacy.</li>
</ol>
</div>
</body>
</html>
Annon, great work. I've modified the original plus included Stefan's modifications in the following.
In addition, I removed the reliance on Range, which converts the function into an generic algorithm to walk between two nodes. Plus, I've wrapped everything into a single function.
Thoughts on other solutions:
Not interested in relying on jquery
Using cloneNode lifts the results to a fragment, which prevents many operations one might want to conduct during filtering.
Using querySelectAll on a cloned fragment is wonky because the start or end nodes may be within a wrapping node, hence the parser may not have the closing tag?
Example:
<div>
<p>A</p>
<div>
<p>B</p>
<div>
<p>C</p>
</div>
</div>
</div>
Assume start node is the "A" paragraph, and the end node is the "C" paragraph
. The resulting cloned fragment would be:
<p>A</p>
<div>
<p>B</p>
<div>
<p>C</p>
and we're missing closing tags? resulting in funky DOM structure?
Anyway, here's the function, which includes a filter option, which should return TRUE or FALSE to include/exclude from results.
var getNodesBetween = function(startNode, endNode, includeStartAndEnd, filter){
if (startNode == endNode && startNode.childNodes.length === 0) {
return [startNode];
};
var getNextNode = function(node, finalNode, skipChildren){
//if there are child nodes and we didn't come from a child node
if (finalNode == node) {
return null;
}
if (node.firstChild && !skipChildren) {
return node.firstChild;
}
if (!node.parentNode){
return null;
}
return node.nextSibling || getNextNode(node.parentNode, endNode, true);
};
var nodes = [];
if(includeStartAndEnd){
nodes.push(startNode);
}
while ((startNode = getNextNode(startNode, endNode)) && (startNode != endNode)){
if(filter){
if(filter(startNode)){
nodes.push(startNode);
}
} else {
nodes.push(startNode);
}
}
if(includeStartAndEnd){
nodes.push(endNode);
}
return nodes;
};
I wrote the perfect code for this and it works 100% for every node :
function getNodesInSelection() {
var range = window.getSelection().getRangeAt(0);
var node = range.startContainer;
var ranges = []
var nodes = []
while (node != null) {
var r = document.createRange();
r.selectNode(node)
if(node == range.startContainer){
r.setStart(node, range.startOffset)
}
if(node == range.endContainer){
r.setEnd(node, range.endOffset)
}
ranges.push(r)
nodes.push(node)
node = getNextElementInRange(node, range)
}
// do what you want with ranges and nodes
}
here are some helper functions
function getClosestUncle(node) {
var parent = node.parentElement;
while (parent != null) {
var uncle = parent.nextSibling;
if (uncle != null) {
return uncle;
}
uncle = parent.nextElementSibling;
if (uncle != null) {
return uncle;
}
parent = parent.parentElement
}
return null
}
function getFirstChild(_node) {
var deep = _node
while (deep.firstChild != null) {
deep = deep.firstChild
}
return deep
}
function getNextElementInRange(currentNode, range) {
var sib = currentNode.nextSibling;
if (sib != null && range.intersectsNode(sib)) {
return getFirstChild(sib)
}
var sibEl = currentNode.nextSiblingElemnent;
if (sibEl != null && range.intersectsNode(sibEl)) {
return getFirstChild(sibEl)
}
var uncle = getClosestUncle(currentNode);
var nephew = getFirstChild(uncle)
if (nephew != null && range.intersectsNode(nephew)) {
return nephew
}
return null
}
bob. the function only returns the startNode and endNode. the nodes in between do not get pushed to the array.
seems the while loop returns null on getNextNode() hence that block never gets executed.
here is function return you array of sub-ranges
function getSafeRanges(range) {
var doc = document;
var commonAncestorContainer = range.commonAncestorContainer;
var startContainer = range.startContainer;
var endContainer = range.endContainer;
var startArray = new Array(0),
startRange = new Array(0);
var endArray = new Array(0),
endRange = new Array(0);
// ##### If start container and end container is same
if (startContainer == endContainer) {
return [range];
} else {
for (var i = startContainer; i != commonAncestorContainer; i = i.parentNode) {
startArray.push(i);
}
for (var i = endContainer; i != commonAncestorContainer; i = i.parentNode) {
endArray.push(i);
}
}
if (0 < startArray.length) {
for (var i = 0; i < startArray.length; i++) {
if (i) {
var node = startArray[i - 1];
while ((node = node.nextSibling) != null) {
startRange = startRange.concat(getRangeOfChildNodes(node));
}
} else {
var xs = doc.createRange();
var s = startArray[i];
var offset = range.startOffset;
var ea = (startArray[i].nodeType == Node.TEXT_NODE) ? startArray[i] : startArray[i].lastChild;
xs.setStart(s, offset);
xs.setEndAfter(ea);
startRange.push(xs);
}
}
}
if (0 < endArray.length) {
for (var i = 0; i < endArray.length; i++) {
if (i) {
var node = endArray[i - 1];
while ((node = node.previousSibling) != null) {
endRange = endRange.concat(getRangeOfChildNodes(node));
}
} else {
var xe = doc.createRange();
var sb = (endArray[i].nodeType == Node.TEXT_NODE) ? endArray[i] : endArray[i].firstChild;
var end = endArray[i];
var offset = range.endOffset;
xe.setStartBefore(sb);
xe.setEnd(end, offset);
endRange.unshift(xe);
}
}
}
var topStartNode = startArray[startArray.length - 1];
var topEndNode = endArray[endArray.length - 1];
var middleRange = getRangeOfMiddleElements(topStartNode, topEndNode);
startRange = startRange.concat(middleRange);
response = startRange.concat(endRange);
return response;
}
With generator and document.createTreeWalker:
function *getNodeInRange(range) {
let [start, end] = [range.startContainer, range.endContainer]
if (start.nodeType < Node.TEXT_NODE || Node.COMMENT_NODE < start.nodeType) {
start = start.childNodes[range.startOffset]
}
if (end.nodeType < Node.TEXT_NODE || Node.COMMENT_NODE < end.nodeType) {
end = end.childNodes[range.endOffset-1]
}
const relation = start.compareDocumentPosition(end)
if (relation & Node.DOCUMENT_POSITION_PRECEDING) {
[start, end] = [end, start]
}
const walker = document.createTreeWalker(
document, NodeFilter.SHOW_ALL
)
walker.currentNode = start
yield start
while (walker.parentNode()) yield walker.currentNode
if (!start.isSameNode(end)) {
walker.currentNode = start
while (walker.nextNode()) {
yield walker.currentNode
if (walker.currentNode.isSameNode(end)) break
}
}
const subWalker = document.createTreeWalker(
end, NodeFilter.SHOW_ALL
)
while (subWalker.nextNode()) yield subWalker.currentNode
}