Splitting node content in JavaScript DOM - javascript

I have a scenario where I need to split a node up to a given ancestor, e.g.
<strong>hi there, how <em>are <span>you</span> doing</em> today?</strong>
needs to be split into:
<strong>hi there, how <em>are <span>y</span></em></strong>
and
<strong><em><span>ou</span> doing</em> today?</strong>
How would I go about doing this?

Here is a solution that will work for modern browsers using Range. Something similar could be done for IE < 9 using TextRange, but I use Linux so I don't have easy access to those browsers. I wasn't sure what you wanted the function to do, return the nodes or just do a replace inline. I just took a guess and did the replace inline.
function splitNode(node, offset, limit) {
var parent = limit.parentNode;
var parentOffset = getNodeIndex(parent, limit);
var doc = node.ownerDocument;
var leftRange = doc.createRange();
leftRange.setStart(parent, parentOffset);
leftRange.setEnd(node, offset);
var left = leftRange.extractContents();
parent.insertBefore(left, limit);
}
function getNodeIndex(parent, node) {
var index = parent.childNodes.length;
while (index--) {
if (node === parent.childNodes[index]) {
break;
}
}
return index;
}
Demo: jsbin
It expects a TextNode for node, although it will work with an Element; the offset will just function differently based on the behavior of Range.setStart

See the method Text.splitText.

Not sure if this helps you, but this is what I came up with...
Pass the function an element and a node tag name string you wish to move up to.
<strong>hi there, how <em>are <span id="span">you</span> doing</em> today?</strong>
<script type="text/javascript">
function findParentNode(element,tagName){
tagName = tagName.toUpperCase();
var parentNode = element.parentNode;
if (parentNode.tagName == tagName){
//Erase data up to and including the node name we passed
console.log('Removing node: '+parentNode.tagName+' DATA: '+parentNode.firstChild.data);
parentNode.firstChild.data = '';
return parentNode;
}
else{
console.log('Removing node: '+parentNode.tagName+' DATA: '+parentNode.firstChild.data);
//Erase the first child's data (the first text node and leave the other nodes intact)
parentNode.firstChild.data = '';
//Move up chain of parents to find the tag we want. Return the results so we can do things with it after
return findParentNode(parentNode, tagName)
}
}
var ourNode = document.getElementById("span");
alert(findParentNode(ourNode,'strong').innerHTML);
</script>

Related

How do I replace all instances of prices (beginning with "US$") in html page using jquery?

Say I have the following HTML:
<div class="L-shaped-icon-container">
<span class="L-shaped-icon">Something is US$50.25 and another thing is US$10.99.</span>
</div>
What I'd like to do is replace all instances of US$XX.xx with GBP£YY.yy on the live page using jquery.
The value of GBP would be determined by my own currency conversion ratio.
So I'm assuming what I'd first need to do is use a regular expression to get all instances of the prices which would be anything beginning with USD$ and ending after .xx? Prices will always have cents displayed.
Then I'm stuck what would be the best way to accomplish the next part.
Should I wrap these instances in a span tag with a class, then use jquery.each() function to loop through each and replace the contents with a jquery(this).html("GBP£YY.yy")?
Any help setting me on the right path would be greatly appreciated. Thanks guys.
base method for text replacements:
var textWalker = function (node, callback) {
var nodes = [node];
while (nodes.length > 0) {
node = nodes.shift();
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if (child.nodeType === child.TEXT_NODE)
callback(child);
else
nodes.push(child);
}
}
};
stuff you need to do:
var USDinGBP = 0.640573954;
textWalker(document, function (n) {
n.nodeValue = n.nodeValue.replace(/(USD?\$(\d+(\.\d+)?))/g, function($0, $1, $2){
return "GBP£" + (parseFloat($2) * USDinGBP).toFixed(2);
})
})
you can fire that on ANY site. it will even replace titles etc.
so.. to tell you about the benefits of not using jquery for this:
jquery will process and wrap every single element in a browser compatible way.
using a native javascript solution would speed up this process alot.
using native textnodes also is benefitial since it will not break event handlers for child elements.
you should also consider using fastdom.
it does not matter if you are using jquery or native js. after writing to elements the dom has to do certain tasks before it can be read again. in the end you will loose some time for each edited element.
to give you a fastdom example:
var textWalker = function (node, callback) {
var nodes = [node];
while (nodes.length > 0) {
node = nodes.shift();
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if (child.nodeType === child.TEXT_NODE)
callback(child);
else
nodes.push(child);
}
}
};
var textReplace = function (node, regex, callback) {
textWalker(node, function (n) {
fastdom.read(function () {
var text = n.nodeValue;
if (!regex.test(text)) {
return;
}
text = text.replace(regex, callback);
fastdom.write(function () {
n.nodeValue = text;
});
});
});
};
// put this function call into your onload function:
var USDinGBP = 0.640573954;
textReplace(document, /(USD?\$(\d+(\.\d+)?))/g, function($0, $1, $2){
return "GBP£" + (parseFloat($2) * USDinGBP).toFixed(2);
});
this will basically do the job in an instant.
if you want to go even further you could add this to jquery as following:
jQuery.fn.textReplace = function (regex, callback) {
this.each(function () {
textReplace(this, regex, callback);
});
};
and call it like that:
var USDinGBP = 0.640573954;
jQuery(".L-shaped-icon").textReplace(/(USD?\$(\d+(\.\d+)?))/g, function($0, $1, $2){
return "GBP£" + (parseFloat($2) * USDinGBP).toFixed(2);
});
If all of these values are directly in the span, if not you can give them a unique class and use it to iterate over them, you can use the following
You first get the numeric part of the string in a variable
convert the currency store it in other variable.
replace US$ with GBP
replace numeric part of the string with converted value
jQuery:
("span").each(function() {
var currencyVal=$(this).text().match(/\d/);
var convertedVal=currencyVal * 100; // just for example.
$(this).text($(this).text().replace(/^US$/,'GBP£'));
$(this).text($(this).text().replace(/\d/,convertedVal));
});
I hope this will helps you. Here is working Fiddle
HTML:
<div class="L-shaped-icon-container">
<span class="L-shaped-icon">Something is US$50.25 and another thing is US$10.99.</span>
<span class="L-shaped-icon">Something is BR$20.25 and another thing is US$10.99.</span>
<span class="L-shaped-icon">Something is US$10.25 and another thing is US$10.99.</span>
<span class="L-shaped-icon">Something is US$50.20 and another thing is GR$10.99.</span>
</div>
<button class="btnUpdate">Update</button>
JavaScript Code:
function UpdateCurrency(){
var UStoGB=10;
$('span').each(function(e){
var matchedText = $(this).text().match(/US\$\S+/g);
var updatedText = $(this).text();
if(matchedText){
for(var i=0;i<= matchedText.length;i++){
if(matchedText[i]){
var currentValue=matchedText[i].replace('US$','');
if(!currentValue) currentValue=0;
var newCurrency = ( parseFloat(currentValue) * UStoGB);
updatedText= updatedText.replace(matchedText[i],'GBP£'+newCurrency);
}
}
}
$(this).text(updatedText);
});
return false;
}

Replace text in the middle of a TextNode with an element

I want to insert html tags within a text node with TreeWalker, but TreeWalker forces my html brackets into & lt; & gt; no matter what I've tried. Here is the code:
var text;
var tree = document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);
while (tree.nextNode()) {
text = tree.currentNode.nodeValue;
text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
tree.currentNode.nodeValue = text;
}
Using \< or " instead of ' won't help. My workaround is to copy all of the DOM tree to a string and to replace the html body with that. It works on very simple webpages and solves my first problem, but is a bad hack and won't work on anything more than a trivial page. I was wondering if I could just work straight with the text node rather than use a workaround. Here is the code for the (currently buggy) workaround:
var text;
var newHTML = "";
var tree = document.createTreeWalker(document.body);
while (tree.nextNode()) {
text = tree.currentNode.nodeValue;
if (tree.currentNode.nodeType == 3){
text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
}
newHTML += text
}
document.body.innerHTML = newHTML;
Edit: I realize a better workaround would be to custom tag the text nodes ((Customtag_Start_Here) etc.), copy the whole DOM to a string, and use my customs tags to identify text nodes and modify them that way. But if I don't have to, I'd rather not.
To 'change' a text node into an element, you must replace it with an element. For example:
var text = tree.currentNode;
var el = document.createElement('foo');
el.setAttribute('bar','yes');
text.parentNode.replaceChild( el, text );
If you want to retain part of the text node, and inject an element "in the middle", you need to create another text node and insert it and the element into the tree at the appropriate places in the tree.
Edit: Here's a function that might be super useful to you. :)
Given a text node, it runs a regex on the text values. For each hit that it finds it calls a custom function that you supply. If that function returns a string, then the match is replaced. However, if that function returns an object like:
{ name:"element", attrs{onmouseover:"sendWord('foo')"}, content:"foo" }
then it will split the text node around the match and inject an element in that location. You can also return an array of strings or those objects (and can recursively use arrays, strings, or objects as the content property).
Demo: http://jsfiddle.net/DpqGH/8/
function textNodeReplace(node,regex,handler) {
var mom=node.parentNode, nxt=node.nextSibling,
doc=node.ownerDocument, hits;
if (regex.global) {
while(node && (hits=regex.exec(node.nodeValue))){
regex.lastIndex = 0;
node=handleResult( node, hits, handler.apply(this,hits) );
}
} else if (hits=regex.exec(node.nodeValue))
handleResult( node, hits, handler.apply(this,hits) );
function handleResult(node,hits,results){
var orig = node.nodeValue;
node.nodeValue = orig.slice(0,hits.index);
[].concat(create(mom,results)).forEach(function(n){
mom.insertBefore(n,nxt);
});
var rest = orig.slice(hits.index+hits[0].length);
return rest && mom.insertBefore(doc.createTextNode(rest),nxt);
}
function create(el,o){
if (o.map) return o.map(function(v){ return create(el,v) });
else if (typeof o==='object') {
var e = doc.createElementNS(o.namespaceURI || el.namespaceURI,o.name);
if (o.attrs) for (var a in o.attrs) e.setAttribute(a,o.attrs[a]);
if (o.content) [].concat(create(e,o.content)).forEach(e.appendChild,e);
return e;
} else return doc.createTextNode(o+"");
}
}
It's not quite perfectly generic, as it does not support namespaces on attributes. But hopefully it's enough to get you going. :)
You would use it like so:
findAllTextNodes(document.body).forEach(function(textNode){
replaceTextNode( textNode, /\b\w+/g, function(match){
return {
name:'element',
attrs:{onmouseover:"sendWord('"+match[0]+"')"},
content:match[0]
};
});
});
function findAllTextNodes(node){
var walker = node.ownerDocument.createTreeWalker(node,NodeFilter.SHOW_TEXT);
var textNodes = [];
while (walker.nextNode())
if (walker.currentNode.parentNode.tagName!='SCRIPT')
textNodes.push(walker.currentNode);
return textNodes;
}
or if you want something closer to your original regex:
replaceTextNode( textNode, /(^|\W)(\w+)/g, function(match){
return [
match[1], // might be an empty string
{
name:'element',
attrs:{onmouseover:"sendWord('"+match[2]+"')"},
content:match[2]
}
];
});
Function that returns the parent element of any text node including partial match of passed string:
function findElByText(text, mainNode) {
let textEl = null;
const traverseNodes = function (n) {
if (textEl) {
return;
}
for (var nodes = n.childNodes, i = nodes.length; i--;) {
if (textEl) {
break;
}
var n = nodes[i], nodeType = n.nodeType;
// Its a text node, check if it matches string
if (nodeType == 3) {
if (n.textContent.includes(text)) {
textEl = n.parentElement;
break;
}
}
else if (nodeType == 1 || nodeType == 9 || nodeType == 11) {
traverseNodes(n);
}
}
}
traverseNodes(mainNode);
return textEl;
}
Usage:
findElByText('Some string in document', document.body);

What are valid node types for the Range setEndAfter function in Safari?

I am writing a module that should allow users to select parts of an HTML document. To get the internals to work I expand the Range of the selection to a valid HTML snippet.
For the case where B is a descendant of A I find the ancestor of B which is a child of A and want to set the range to end after that node using setEndAfter. This is what I have now:
var closestChild = function (node, descendant) {
var parent;
if (descendant.parentElement) {
parent = descendant.parentElement;
if ( node === parent ) {
return descendant;
}
return closestChild(node, parent);
}
return false;
}
var legalRange = function (range) {
var newRange = range.cloneRange(),
child;
if (range.startContainer === range.endContainer) {
return newRange;
}
child = closestChild(range.startContainer.parentElement, range.endContainer.parentElement);
if (child) {
newRange.setEndAfter(child);
return newRange;
}
return null;
};
But this throws a INVALID_NODE_TYPE_ERR: DOM Range Exception 2 when I try to set the end point. I have also tried using parentNode instead of parentElement with the same exception thrown. This is not a problem if i use setEnd(). What types of nodes should I pass to do this.
PS: It turns out that the code works in FireFox, so my problem is now with Safari and Chrome.
I found the solution.
When I set up my test cases, I didn't add the elements to the document. It seems that Chrome and Safari treated the nodes as invalid when using setEndAfter if the nodes were not part of the document.
As answered by Eivind, the problem is that the node used to set the range position is not attached to the document. So one solution is to attach it before you set the range position.
Another solution, if you can't attach the node to the DOM for some reason, is to use setStart() and setEnd() instead.
// Instead of `range.setStartBefore(node)`
var parent = node.parent;
range.setStart(parent, Array.from(parent.childNodes).indexOf(node))
// Instead of `range.setStartAfter(node)`
var parent = node.parent;
range.setStart(parent, Array.from(parent.childNodes).indexOf(node) + 1)
// Instead of `range.setEndBefore(node)`
var parent = node.parent;
range.setEnd(parent, Array.from(parent.childNodes).indexOf(node))
// Instead of `range.setEndAfter(node)`
var parent = node.parent;
range.setEnd(parent, Array.from(parent.childNodes).indexOf(node) + 1)
Note: Array.from(arrayLike) is not supported in Internet Explorer <= 11. Use Array.prototype.slice.call(arrayLike) instead of you need IE support.

How do i get the xpath of an element in an X/HTML file

I am a beginner to Xpath and was wondering if there is any way to get the xpath of an element in javascript/jquery. I need an absolute way to identify an element and I knw Xpath is used for this,but can't figure how.
The scenario is that I have a jquery reference of an element. I want its xpath to store in a database on mouse click.
How do I get the Xpath of an HTML Element once I have a jquery reference. I need to be able to translate the Xpath into an absolute element later
function clickTrack(event){
offset=event.pageX;
var xpath=getXpath(this);//I need the xpath here
data={'xpath':xpath,'offset':offset};
}
You can extract this functionality from an XPath tool I once wrote:
http://webkitchen.cz/lab/opera/xpath-tool/xpath-tool.js
Edit: here you go:
function getXPath(node) {
var comp, comps = [];
var parent = null;
var xpath = '';
var getPos = function(node) {
var position = 1, curNode;
if (node.nodeType == Node.ATTRIBUTE_NODE) {
return null;
}
for (curNode = node.previousSibling; curNode; curNode = curNode.previousSibling) {
if (curNode.nodeName == node.nodeName) {
++position;
}
}
return position;
}
if (node instanceof Document) {
return '/';
}
for (; node && !(node instanceof Document); node = node.nodeType == Node.ATTRIBUTE_NODE ? node.ownerElement : node.parentNode) {
comp = comps[comps.length] = {};
switch (node.nodeType) {
case Node.TEXT_NODE:
comp.name = 'text()';
break;
case Node.ATTRIBUTE_NODE:
comp.name = '#' + node.nodeName;
break;
case Node.PROCESSING_INSTRUCTION_NODE:
comp.name = 'processing-instruction()';
break;
case Node.COMMENT_NODE:
comp.name = 'comment()';
break;
case Node.ELEMENT_NODE:
comp.name = node.nodeName;
break;
}
comp.position = getPos(node);
}
for (var i = comps.length - 1; i >= 0; i--) {
comp = comps[i];
xpath += '/' + comp.name;
if (comp.position != null) {
xpath += '[' + comp.position + ']';
}
}
return xpath;
}
It might need some changes if you want it to work in IE as well.
There is no such thing as "the" XPath for an element.
When people ask this question they usually want one of three things:
(a) the names of the elements in the ancestry of the element, for example /a/b/c/d
(b) as (a) but with positional information added, for example /a[2]/b[3]/c[1]/d[4]. (A variant is to want the positional information only where it's not redudant)
(c) as (b) but with no namespace dependencies, for example /[namespace-uri()='x' and local-name()='y'][1]/[namespace-uri()='x' and local-name='z'][5]/...
All three are easy enough to construct with a simple recursive function in whatever language takes your fancy.
Well you can work it out with a Github library at https://github.com/bermi/element-xpath
Include the script
<script src="https://raw.github.com/bermi/element-xpath/master/element-xpath.min.js" type="text/javascript"></script>
Use this code
document.body.addEventListener("click", function (ev) {
console.log(ev);
console.log(ev.toElement);
var xpath = getElementXpath(ev.toElement);
console.log(xpath);
});
Need more info, but it's easy if you have an id for the element. If for instance your jquery reference is grabbing a div with id="myDiv", a suitable xpath would be
//div[#id="myDiv"]
Here's an xpath tutorial: http://www.tizag.com/xmlTutorial/xpathtutorial.php
And a good ref: https://developer.mozilla.org/en/XPath
EDIT: I see you're wanting absolute xpath given only the node. In that case, I believe you'll have to build the xpath by walking the DOM tree backwards using parentNode (reference here). You'll also need to do some work to check which child number each node is based on tag name. Remember also, in xpath, indices start with 1, not 0, per the spec (reference here).
EDIT #2: See getPathTo() in this post. It doesn't go all the way up the tree, but it gets the position relative to siblings. Also, there's an unaccepted answer with a full-blown attempt at the absolute path here.

how to get all parent nodes of given element in pure javascript?

I mean an array of them. That is a chain from top HTML to destination element including the element itself.
for example for element <A> it would be:
[HTML, BODY, DIV, DIV, P, SPAN, A]
A little shorter (and safer, since target may not be found):
var a = document.getElementById("target");
var els = [];
while (a) {
els.unshift(a);
a = a.parentNode;
}
You can try something like:
var nodes = [];
var element = document.getElementById('yourelement');
nodes.push(element);
while(element.parentNode) {
nodes.unshift(element.parentNode);
element = element.parentNode;
}
I like this method:
[...(function*(e){do { yield e; } while (e = e.parentNode);})($0)]
... where $0 is your element.
An upside of this method is that it can be used as a value in expressions.
To get an array without the target element:
[...(function*(e){while (e = e.parentNode) { yield e; }})($0)]
You can walk the chain of element.parentNodes until you reach an falsey value, appending to an array as you go:
const getParents = el => {
for (var parents = []; el; el = el.parentNode) {
parents.push(el);
}
return parents;
};
const el = document.querySelector("b");
console.log(getParents(el).reverse().map(e => e.nodeName));
<div><p><span><b>Foo</b></span></div>
Note that reversing is done in the caller because it's not essential to the lineage algorithm. Mapping to e.nodeName is purely for presentation and also non-essential.
Note that this approach means you'll wind up with the document element as the last element in the chain. If you don't want that, you can add && el !== document to the loop stopping condition.
The overall time complexity of the code above is linear and reverse() is in-place, so it doesn't require an extra allocation. unshift in a loop, as some of the other answers recommend, is quadratic and may harm scalability on uncommonly-deep DOM trees in exchange for a negligible gain in elegance.
Another alternative (based on this):
for(var e = document.getElementById("target"),p = [];e && e !== document;e = e.parentNode)
p.push(e);
I believe this will likely be the most performant in the long run in the most scenarios if you are making frequent usage of this function. The reason for why t will be more performant is because it initially checks to see what kind of depths of ancestry it might encounter. Also, instead of creating a new array every time you call it, this function will instead efficiently reuse the same array, and slice it which is very optimized in some browsers. However, since there is no really efficient way I know of to check the maximum depth, I am left with a less efficient query-selector check.
// !IMPORTANT! When moving this coding snippet over to your production code,
// do not run the following depthtest more than once, it is not very performant
var kCurSelector="*|*", curDepth=3;
while (document.body.querySelector(kCurSelector += '>*|*')) curDepth++;
curDepth = Math.pow(2, Math.ceil(Math.log2(startDepth))),
var parentsTMP = new Array(curDepth);
function getAllParentNodes(Ele){
var curPos = curDepth;
if (Ele instanceof Node)
while (Ele !== document){
if (curPos === 0){
curPos += curDepth;
parentsTMP.length <<= 1;
parentsTMP.copyWithin(curDepth, 0, curDepth);
curDepth <<= 1;
}
parentsTMP[--curPos] = Ele;
Ele = Ele.parentNode;
}
return retArray.slice(curPos)
}
The browser compatibility for the above function is that it will work in Edge, but not in IE. If you want IE support, then you will need a Array.prototype.copyWithin polyfill.
get all parent nodes of child in javascript array
let selectedTxtElement = document.getElementById("target");
let els = [];
while (selectedTxtElement) {
els.unshift(selectedTxtElement);
selectedTxtElement = selectedTxtElement.parentNode;
}
know more

Categories

Resources