I am currently working in native JS and I am trying to build the highlight text feature in a contenteditable div. I have successfully built the highlight feature but I am encountering a problem when I want to toggle between the highlight and unhighlight text using a single button. So I am getting the selected text and the range of the selected text via
var selectedText = window.getSelection();
var range = selectedText.getRangeAt(0);
and I am wrapping the selected text using surroundContents that is a function of range object.
var wrapper = document.createElement("span");
wrapper.setAttribute("class","highlight");
But now when I am trying to unhighlight some part of the highlighted text and some part of plain text the natural behavior should unhighlight the highlighted text and highlight the plain text. To achieve this I am cloning the range via
var clone = range.cloneContents()
var nodeInBetween = clone.childNodes //array of nodes between the start and end nodes.
Now there are two problems I am facing. First I need to remove the span.highlight nodes and replace it with a TextNode again in order to make it unhighlight-ed and I need some method to wrap a textnode with a span. Unfortunately there is no way to wrap a textnode as one can for range variable.
I have experimented with a (recursive) highlighter method in this jsFiddle. It may be of use to you. The actual method:
function highLight(term,root,forElements,styleclass){
root = root || document.querySelector('body');
term = term instanceof Array ? term.join('|') : term;
if (!term) {throw TypeError('Highlighter needs a term to highlight anything');}
forElements = forElements && forElements instanceof Array
? forElements.join(',')
: /string/i.test(typeof forElements) ? forElements : '*';
styleclass = styleclass || 'highlight';
var allDiv = root.querySelectorAll(forElements),
re = RegExp(term,'gi'),
highlighter = function(a){return '<span class="'+styleclass+'">'+a+'</span>'};
for (var i=0; i<allDiv.length; i+=1){
// recurse children
if (allDiv[i].querySelectorAll(forElements).length){
highLight.call(null,term, allDiv[i],forElements,styleclass);
}
// replace term(s) in text nodes
var node = allDiv[i];
for (node=node.firstChild; node; node=node.nextSibling) {
if (node.nodeType===3){
var re = RegExp(term,'gi');
node.data = node.data.replace(re,highlighter);
}
}
}
//finally, replace all text data html encoded brackets
root.innerHTML = root.innerHTML
.replace(/</gi,'<')
.replace(/>/gi,'>');
}
Related
The following code I have is looking for a specific text in the DOM, based on a pattern.
Instead of replacing the text I would like to transform it to a link where the text is a parameter in the link.
Let's say the link would look like: https://google.com/search?q=matched_text_added_here
var allTextNodes = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT),
// some temp references for performance
tmptxt,
tmpnode,
// compile the RE and cache the replace string, for performance
identify = /ABC\d{7}/g,
replaceValue = "changed";
// iterate through all text nodes
while (allTextNodes.nextNode()) {
tmpnode = allTextNodes.currentNode;
tmptxt = tmpnode.nodeValue;
tmpnode.nodeValue = tmptxt.replace(identify, replaceValue);
}
Instead of replacing the text I would like to transform it to a link
So, instead of replacing the text:
create a link
let newLink = document.createElement('a')
configure the link
newLink.href = 'https://google.com/search?q=' + encodeURIComponent(nodeValue)
newLink.textContent = nodeValue
replace the text node with the link node
tmpnode.replaceWith(newLink)
I have a text composed of two <div> inside one <body> saved as raw_text as following:
var raw_text = "<body><div>This is the 'div' text that I don't want.</div> <div>This is the 'div' text that I want to print.</div></body>";
I need a script for print on the screen only the <div> present in raw-text that include a certain string.
if the string wanted is:
var x = "that I want";
the script should take:
<div>This is the 'div' text that I want to print.</div>
and the output should be:
This is the 'div' text that I want to print.
This is the proper way to do it:
Use a DOM parser
Iterate the text nodes
Check if they contain the desired string
var html = "<body><div>This is the 'div' text that I don't want.</div> <div>This is the 'div' text that I want to print.</div></body>";
var x = "that I want";
var doc = new DOMParser().parseFromString(html, 'text/html');
var it = doc.createNodeIterator(doc.body, NodeFilter.SHOW_TEXT);
var node;
while (node = it.nextNode()) if(node.nodeValue.includes(x)) {
console.log(node.nodeValue);
break;
}
var raw_text = "<body><div>This is the 'div' text that I don't want.</div> <div>This is the 'div' text that I want to print.</div></body>";
var x = "that I want";
var homework_solution = raw_text.match(new RegExp("<div>([^<>]*?"+x+"[^<>]*?)</div>"))[1];
This should do the job. The regex could possibly be made a bit more robust.
The "proper" way to do this would be to use DOMParser to search for the node you want.
You can use jQuery to convert your string to proper DOM elements, and then parse them easily, as #Retr0spectrum says on their comment. You have the HTML in a plain string:
var htmlString = "<body><div>This is the 'div' text that I don't want.</div> <div>This is the 'div' text that I want to print.</div></body>";
Now you have to:
parse it to DOM,
filter the elements, and
get the text
Like this:
// Process the string through jQuery so it parses the DOM elements
var dom = $(htmlString);
// and then we convert to array...
var array = dom.toArray();
// ... so we can filter it, using RegEx to find the
// <div>(s) we are interested in:
var matchingDivs = array.filter(function (div, i) {
return $(div).text().match(/that I want/g) !== null;
});
// we pop the last matched div from the filtered array (the first
// one would also work, since normally you will find just one)
var theDiv = matchingDivs.pop();
// Then get the <div>'s text:
var theText = selectedDiv.textContent;
The beautiful thing is you can chain all the methods so you can write the above like this:
var theText = $(htmlString).toArray().filter(function (div, i) {
return $(div).text().match(/that I want/g) !== null;
})[0].textContent;
Note: In the chained methods example I took the first element instead of the last one, using the bracket operator [0] instead of pop().
Hope this helps understanding how it works.
I am trying to write a test to work out whether the text rendered inside an <input> has the same baseline as a label:
In order to do this, I would like to compute the baseline of the text that has been rendered in each element and compare the two values. Is this possible and if so, how is it done? If not, is there a better way to establish that the baseline of the two elements is the same?
I have found a way to determine the baseline of the label which seems to work reliably:
function getBaseline(element) {
var span = document.createElement("span");
span.setAttribute("style", "font-size:0");
span.innerText = "A";
jQuery(element).prepend(span);
return span.getBoundingClientRect().bottom;
}
However, this method doesn't work for the <input>, as I cannot insert the span in that case.
I found a way to get the computed baseline based on your function. The trick is to create a wrapper and apply all styles from the element to it. I can confirm that it works in Firefox (v38) and Chrome (v44) on OSX. Unfortunately it doesn't work proper in Safari.
DEMO
function getBaseline(element) {
element = jQuery(element);
var span = document.createElement("span");
span.setAttribute("style", "font-size:0");
span.innerText = "A";
var wrapper = document.createElement("span");
applyCSSProperties.call(wrapper, element);
element.wrap(wrapper);
element.before(span);
var computed = span.getBoundingClientRect().bottom;
span.remove();
element.unwrap(wrapper);
return computed;
}
function applyCSSProperties(element) {
var styles = {};
var comp = getComputedStyle(element[0]);
for(var i = 0; i < comp.length; i++){
styles[comp[i]] = comp.getPropertyValue(comp[i]);
}
jQuery(this).css(styles);
}
I am trying to use Javascript to modify an existing HTML document so that I can surround every word of text in the web page with a span tag that would have a counter. This is a very specific problem so I am going to provide an example case:
<body><p>hello, <br>
change this</p>
<img src="lorempixel.com/200/200> <br></body></html>
This should change to:
<body><p><span id="1">hello,</span>
<br> <span id="2"> change</span><span id="3"> this</span> </p>
<br> <img src="lorempixel.com/200/200> <br></body></html>
I am thinking or regex solutions but they get truly complicated and I am not sure of how to ignore tags and change text without completely breaking the page.
Any thoughts appreciated!
Don't use regex on raw HTML. Use it only on text. This is because regex is a context free parser but HTML is a recursive language. You need a recursive descent parser to properly handle HTML.
First a few useful features of the DOM:
document.body is the root of the DOM
Every node of the DOM has a childNodes array (even comments, text, and attributes)
Element nodes such as <span> or <h> don't contain text, instead they contain text nodes that contain text.
All nodes have a nodeType property and text node is type 3.
All nodes have a nodeValue property that holds different things depending on what kind of node it is. For text nodes nodeValue contains the actual text.
So, using the information above we can surround all words with a span.
First a simple utility function that allows us to process the DOM:
// First a simple implementation of recursive descent,
// visit all nodes in the DOM and process it with a callback:
function walkDOM (node,callback) {
if (node.nodeName != 'SCRIPT') { // ignore javascript
callback(node);
for (var i=0; i<node.childNodes.length; i++) {
walkDOM(node.childNodes[i],callback);
}
}
}
Now we can walk the DOM and find text nodes:
var textNodes = [];
walkDOM(document.body,function(n){
if (n.nodeType == 3) {
textNodes.push(n);
}
});
Note that I'm doing this in two steps to avoid wrapping words twice.
Now we can process the text nodes:
// simple utility functions to avoid a lot of typing:
function insertBefore (new_element, element) {
element.parentNode.insertBefore(new_element,element);
}
function removeElement (element) {
element.parentNode.removeChild(element);
}
function makeSpan (txt, attrs) {
var s = document.createElement('span');
for (var i in attrs) {
if (attrs.hasOwnProperty(i)) s[i] = attrs[i];
}
s.appendChild(makeText(txt));
return s;
}
function makeText (txt) {return document.createTextNode(txt)}
var id_count = 1;
for (var i=0; i<textNodes.length; i++) {
var n = textNodes[i];
var txt = n.nodeValue;
var words = txt.split(' ');
// Insert span surrounded words:
insertBefore(makeSpan(words[0],{id:id_count++}),n);
for (var j=1; j<words.length; j++) {
insertBefore(makeText(' '),n); // join the words with spaces
insertBefore(makeSpan(words[j],{id:id_count++}),n);
}
// Now remove the original text node:
removeElement(n);
}
There you have it. It's cumbersome but is 100% safe - it will never corrupt other tags of javascript in your page. A lot of the utility functions I have above can be replaced with the library of your choice. But don't take the shortcut of treating the entire document as a giant innerHTML string. Not unless you're willing to write an HTML parser in pure javascript.
This sort of processing is always a lot more complex than you think. The following will wrap sequences of characters that match \S+ (sequence of non–whitespace) and not wrap sequences that match \s+ (whitespace).
It also allows the content of certain elements to be skipped, such as script, input, button, select and so on. Note that the live collection returned by childNodes must be converted to a static array, otherwise it is affected by the new nodes being added. An alternative is to use element.querySelectorAll() but childNodes has wider support.
// Copy numeric properties of Obj from 0 to length
// to an array
function toArray(obj) {
var arr = [];
for (var i=0, iLen=obj.length; i<iLen; i++) {
arr.push(obj[i]);
}
return arr;
}
// Wrap the words of an element and child elements in a span
// Recurs over child elements, add an ID and class to the wrapping span
// Does not affect elements with no content, or those to be excluded
var wrapContent = (function() {
var count = 0;
return function(el) {
// If element provided, start there, otherwise use the body
el = el && el.parentNode? el : document.body;
// Get all child nodes as a static array
var node, nodes = toArray(el.childNodes);
var frag, parent, text;
var re = /\S+/;
var sp, span = document.createElement('span');
// Tag names of elements to skip, there are more to add
var skip = {'script':'', 'button':'', 'input':'', 'select':'',
'textarea':'', 'option':''};
// For each child node...
for (var i=0, iLen=nodes.length; i<iLen; i++) {
node = nodes[i];
// If it's an element, call wrapContent
if (node.nodeType == 1 && !(node.tagName.toLowerCase() in skip)) {
wrapContent(node);
// If it's a text node, wrap words
} else if (node.nodeType == 3) {
// Match sequences of whitespace and non-whitespace
text = node.data.match(/\s+|\S+/g);
if (text) {
// Create a fragment, handy suckers these
frag = document.createDocumentFragment();
for (var j=0, jLen=text.length; j<jLen; j++) {
// If not whitespace, wrap it and append to the fragment
if (re.test(text[j])) {
sp = span.cloneNode(false);
sp.id = count++;
sp.className = 'foo';
sp.appendChild(document.createTextNode(text[j]));
frag.appendChild(sp);
// Otherwise, just append it to the fragment
} else {
frag.appendChild(document.createTextNode(text[j]));
}
}
}
// Replace the original node with the fragment
node.parentNode.replaceChild(frag, node);
}
}
}
}());
window.onload = wrapContent;
The above addresses only the most common cases, it will need more work and thorough testing.
<p>Lorem Ipsum Link <div ... </div> </p>
I want to put a span around 'Lorem Ipsum' without using jQuery, so the result looks like:
<p><span>Lorem Ipsum </span>Link <div ... </div> </p>
Any ideas? Thanks
First, you need some way of accessing the paragraph. You might want to give it an id attribute, such as "foo":
<p id="foo">Lorem Ipsum Link <div ... </div> </p>
Then, you can use document.getElementById to access that element and replace its children as required:
var p = document.getElementById('foo'),
firstTextNode = p.firstChild,
newSpan = document.createElement('span');
// Append "Lorem Ipsum" text to new span:
newSpan.appendChild( document.createTextNode(firstTextNode.nodeValue) );
// Replace old text node with new span:
p.replaceChild( newSpan, firstTextNode );
To make it more reliable, you might want to call p.normalize() before accessing the first child, to ensure that all text nodes before the anchor are merged as one.
Oook, So you want to replace a part of a text node with an element. Here's how I'd do it:
function giveMeDOM(html) {
var div = document.createElement('div'),
frag = document.createDocumentFragment();
div.innerHTML = html;
while (div.firstChild) {
frag.appendChild( div.firstChild );
}
return frag;
}
var p = document.getElementById('foo'),
firstChild = p.firstChild;
// Merge adjacent text nodes:
p.normalize();
// Get new DOM structure:
var newStructure = giveMeDOM( firstChild.nodeValue.replace(/Lorem Ipsum/i, '<span>$&</span>') );
// Replace first child with new DOM structure:
p.replaceChild( newStructure, firstChild );
Working with nodes at the low level is a bit of a nasty situation to be in; especially without any abstraction to help you out. I've tried to retain a sense of normality by creating a DOM node out of an HTML string produced from the replaced "Lorem Ipsum" phrase. Purists probably don't like this solution, but I find it perfectly suitable.
EDIT: Now using a document fragment! Thanks Crescent Fresh!
UPDATE:
The method below will search through the subtree headed by container and wrap all instances of textin a span element. The words can occur anywhere within a text node, and the text node can occur at any position in the subtree.
(OK, so it took more than a few minor tweaks. :P)
function wrapText(container, text) {
// Construct a regular expression that matches text at the start or end of a string or surrounded by non-word characters.
// Escape any special regex characters in text.
var textRE = new RegExp('(^|\\W)' + text.replace(/[\\^$*+.?[\]{}()|]/, '\\$&') + '($|\\W)', 'm');
var nodeText;
var nodeStack = [];
// Remove empty text nodes and combine adjacent text nodes.
container.normalize();
// Iterate through the container's child elements, looking for text nodes.
var curNode = container.firstChild;
while (curNode != null) {
if (curNode.nodeType == Node.TEXT_NODE) {
// Get node text in a cross-browser compatible fashion.
if (typeof curNode.textContent == 'string')
nodeText = curNode.textContent;
else
nodeText = curNode.innerText;
// Use a regular expression to check if this text node contains the target text.
var match = textRE.exec(nodeText);
if (match != null) {
// Create a document fragment to hold the new nodes.
var fragment = document.createDocumentFragment();
// Create a new text node for any preceding text.
if (match.index > 0)
fragment.appendChild(document.createTextNode(match.input.substr(0, match.index)));
// Create the wrapper span and add the matched text to it.
var spanNode = document.createElement('span');
spanNode.appendChild(document.createTextNode(match[0]));
fragment.appendChild(spanNode);
// Create a new text node for any following text.
if (match.index + match[0].length < match.input.length)
fragment.appendChild(document.createTextNode(match.input.substr(match.index + match[0].length)));
// Replace the existing text node with the fragment.
curNode.parentNode.replaceChild(fragment, curNode);
curNode = spanNode;
}
} else if (curNode.nodeType == Node.ELEMENT_NODE && curNode.firstChild != null) {
nodeStack.push(curNode);
curNode = curNode.firstChild;
// Skip the normal node advancement code.
continue;
}
// If there's no more siblings at this level, pop back up the stack until we find one.
while (curNode != null && curNode.nextSibling == null)
curNode = nodeStack.pop();
// If curNode is null, that means we've completed our scan of the DOM tree.
// If not, we need to advance to the next sibling.
if (curNode != null)
curNode = curNode.nextSibling;
}
}
Combine these 2 tutorials:
PPK on JavaScript: The DOM - Part 3
Adding elements to the DOM
Basically you need to access the node value, remove it, and create a new child element who's node value is the value of the parent item's node, then append that element (span in this case) to the parent (paragraph in this case)
How about using a regular expression in javascript and replacing "Lorem Ipsum" with "<span>Lorem Ipsum</span>" (just remember that you will have to get the "innerHTML" of the element and then replace the whole lot again which may be a bit slow)