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
Related
As in the title,
how can i define nth parentElement in js in other way than:
var theComment = e.target.parentElement.parentElement.parentElement.parentElement;
You can write a loop for it:
const n = 4
let elem = e.target
for(let i = 0; i < n; i++)
elem = elem.parentElement
console.log(elem)
However, as mentioned in the comments, if you're looking for a specific element, it might be better to just write a selector for the element you're looking for.
This applies to your case even more, as event.target can return child nodes of the watched element (if the event bubbles), making your parent count shift around a bit.
You might define a function which ascends up in the parent hierarchy with a loop, e.g.:
function nthParent(n, el) {
while (n--) {
el = el.parentElement;
if (!el) return null;
}
return el;
}
List only rendered styles, not arbitrary ones that aren't applied
I've tried many things to get the styles applied to an element but have come up blank.
Please do not cite getComputedStyle as being a solution unless you can solve the junk returns issue.
The primary problem is that window.getComputedStyle(document.querySelector('ANY ELEMENT')).fill will return "rgb(0, 0, 0)", which is not the correct style in almost any instances, and has no apparent way to destinguish if its actually being applied or not.
The above example is not the only problem case; there are tons of rules returned by getComputedStyle which are wrong and will drastically change the look of the page if they are applied.
Static parsing is not an option as there are cases where the .css files are on another server with no cross-origin headers; which also hides styles usually found in document.styleSheets.
Is there any way to get a list of the applied styles and nothing else?
As requested this code will demonstrate the problem (on Chrome):
var all = document.getElementsByTagName('*');
for(var i in all)
if (all[i].style) all[i].style.cssText = window.getComputedStyle(all[i]).cssText;
EDIT: My answer has code which works on all browsers. I keep above to preserve comment thread.
Here are the version that don't need to check depth.
The problem in your code is the assign of inline style in the previous element will affect the getComputedStyle result of the next result. It mean the value of getComputedStyle is always changing in the loop. You can first store it in an array like this.
var all = document.getElementsByTagName('*');
tmpArr = []
for(var i in all) {
if (all[i].style) {
tmpArr[i] = window.getComputedStyle(all[i]).cssText;
}
}
for(var i in all) {
if (all[i].style) {
all[i].style.cssText = tmpArr[i]; ;
}
}
console.log("finish");
You can change tmpArr[i] = window.getComputedStyle(all[i]).cssText; to tmpArr[i] = window.getComputedStyle(all[i]).cssText + "-webkit-text-fill-color:#691099!important"; to test whether it work
It will be slow if you open the inspector since there are too much inline style, but it will solve the problem if all you need is just put the style to be inline style.
Partial Answer (Updated):
It is possible to get only the active styles by calling my function getRenderedStyles:
getRenderedStyles now bypasses active stylesheets for more accurate output.
function getRenderedStyles(element) {
var tmpele, tmpstyle, elestyle, varstyle, elecolor, eletag;
var styles = {};
var defstyle = {};
elestyle = window.getComputedStyle(element);
elecolor = elestyle.color;
eletag = element.tagName;
var frag = document.createDocumentFragment();
frag.appendChild(document.documentElement);
tmpele = document.appendChild(document.createElement(eletag));
tmpstyle = window.getComputedStyle(tmpele);
styles['color'] = elecolor===tmpstyle.color?undefined:elecolor;
tmpele.style.color = elecolor; // workaround for color propagation on other styles
for (var i in tmpstyle)
defstyle[i] = tmpstyle[i];
tmpele.remove();
document.appendChild(frag);
varstyle = element.style;
for (var i in varstyle) {
if ((((typeof varstyle[i])==="string"))&&(i!=="cssText")) {
if ((defstyle[i]!==elestyle[i]))
styles[i] = elestyle[i];
}
}
return styles;
}
Sadly there's a caviat as the browser still seemingly returns invalid styles in some cases. Often shifting the locations of elements.
To verify this you may run the following code, which takes into account parent/child inheritance, in an attempt to properly apply the current styles to the page:
function DOMDepth(element) {
var cur = element;
var deep = 0;
while(cur.parentNode)
deep++, cur = cur.parentNode;
return deep;
}
function getElementsByDepth() {
var all = document.getElementsByTagName('*');
var depth_map = {};
var deepest = 0;
for(var i in all) {
var depth = DOMDepth(all[i]);
deepest = depth>deepest?depth:deepest;
depth_map[depth] = depth_map[depth] || [];
depth_map[depth].push(all[i]);
}
depth_map['deepest'] = deepest;
return depth_map;
}
function inlineComputedStyles() {
var depth_map = getElementsByDepth();
for (var i = depth_map.deepest; i>0; i--) {
var elements = depth_map[i];
for (var j in elements) {
var styles = getRenderedStyles(elements[j]);
for (var k in styles) {
elements[j].style[k] = styles[k];
}
}
}
}
I have tested the preceeding and can confirm it does not suffer the color problems of the snippet in the question. Sadly I am uncertain as to why some elements still shift or if there's a way to fix it.
Special thanks to Kit Fung for pointing out the inheritance problem.
I would like to have a document fragment/element on the shelf to which I've connected a bunch of other elements. Then whenever I want to add one of these element-systems to the DOM, I copy the fragment, add the unique DOM ID and attach it.
So, for example:
var doc = document,
prototype = doc.createElement(), // or fragment
ra = doc.createElement("div"),
rp = doc.createElement("div"),
rp1 = doc.createElement("a"),
rp2 = doc.createElement("a"),
rp3 = doc.createElement("a");
ra.appendChild(rp);
rp.appendChild(rp1);
rp.appendChild(rp2);
rp.appendChild(rp3);
rp1.className = "rp1";
rp2.className = "rp2";
rp3.className = "rp3";
prototype.appendChild(ra);
This creates the prototype. Then I want to be able to copy the prototype, add an id, and attach. Like so:
var fr = doc.createDocumentFragment(),
to_use = prototype; // This step is illegal, but what I want!
// I want prototype to remain to be copied again.
to_use.id = "unique_id75";
fr.appendChild(to_use);
doc.getElementById("container").appendChild(fr);
I know it's not legal as it stands. I've done fiddles and researched and so on, but it ain't working. One SO post suggested el = doc.appendChild(el); returns el, but that didn't get me far.
So... is it possible? Can you create an on-the-shelf element which can be reused? Or do you have to build the DOM structure you want to add from scratch each time?
Essentially I'm looking for a performance boost 'cos I'm creating thousands of these suckers :)
Thanks.
Use Node.cloneNode:
var container = document.getElementById('container');
var prototype = document.createElement('div');
prototype.innerHTML = "<p>Adding some <strong>arbitrary</strong> HTML in"
+" here just to illustrate.</p> <p>Some <span>nesting</span> too.</p>"
+"<p>CloneNode doesn't care how the initial nodes are created.</p>";
var prototype_copy = prototype.cloneNode(true);
prototype_copy.id = 'whatever'; //note--must be an Element!
container.appendChild(prototype_copy);
Speed Tips
There are three operations you want to minimize:
String Parsing
This occurs when you use innerHTML. innerHTML is fast when you use it in isolation. It's often faster than the equivalent manual-DOM construction because of the overhead of all those DOM method calls. However, you want to keep innerHTML out of inner loops and you don't want to use it for appending. element.innerHTML += 'more html' in particular has catastrophic run-time behavior as the element's contents get bigger and bigger. It also destroys any event or data binding because all those nodes are destroyed and recreated.
So use innerHTML to create your "prototype" nodes for convenience, but for inner loops use DOM manipulation. To clone your prototypes, use prototype.cloneNode(true) which does not invoke the parser. (Be careful with id attributes in cloned prototypes--you need to make sure yourself that they are unique when you append them to the document!)
Document tree modification (repeated appendChild calls)
Every time you modify the document tree you might trigger a repaint of the document window and update the document DOM node relationships, which can be slow. Instead, batch your appends up into a DocumentFragment and append that to the document DOM only once.
Node lookup
If you already have an in-memory prototype object and want to modify pieces of it, you will need to navigate the DOM to find and modify those pieces whether you use DOM traversal, getElement*, or querySelector*.
Keep these searches out of your inner loops by keeping a reference to the nodes you want to modify when you create the prototype. Then whenever you want to clone a near-identical copy of the prototype, modify the nodes you have references to already and then clone the modified prototype.
Sample Template object
For the heck of it, here is a basic (and probably fast) template object illustrating the use of cloneNode and cached node references (reducing the use of string parsing and Node lookups).
Supply it with a "prototype" node (or string) with class names and data-attr="slotname attributename" attributes. The class names become "slots" for text-content replacement; the elements with data-attr become slots for attribute name setting/replacement. You can then supply an object to the render() method with new values for the slots you have defined, and you will get back a clone of the node with the replacements done.
Example usage is at the bottom.
function Template(proto) {
if (typeof proto === 'string') {
this.proto = this.fromString(proto);
} else {
this.proto = proto.cloneNode(true);
}
this.slots = this.findSlots(this.proto);
}
Template.prototype.fromString = function(str) {
var d = document.createDocumentFragment();
var temp = document.createElement('div');
temp.innerHTML = str;
while (temp.firstChild) {
d.appendChild(temp.firstChild);
}
return d;
};
Template.prototype.findSlots = function(proto) {
// textContent slots
var slots = {};
var tokens = /^\s*(\w+)\s+(\w+)\s*$/;
var classes = proto.querySelectorAll('[class]');
Array.prototype.forEach.call(classes, function(e) {
var command = ['setText', e];
Array.prototype.forEach.call(e.classList, function(c) {
slots[c] = command;
});
});
var attributes = proto.querySelectorAll('[data-attr]');
Array.prototype.forEach.call(attributes, function(e) {
var matches = e.getAttribute('data-attr').match(tokens);
if (matches) {
slots[matches[1]] = ['setAttr', e, matches[2]];
}
e.removeAttribute('data-attr');
});
return slots;
};
Template.prototype.render = function(data) {
Object.getOwnPropertyNames(data).forEach(function(name) {
var cmd = this.slots[name];
if (cmd) {
this[cmd[0]].apply(this, cmd.slice(1).concat(data[name]));
}
}, this);
return this.proto.cloneNode(true);
};
Template.prototype.setText = (function() {
var d = document.createElement('div');
var txtprop = (d.textContent === '') ? 'textContent' : 'innerText';
d = null;
return function(elem, val) {
elem[txtprop] = val;
};
}());
Template.prototype.setAttr = function(elem, attrname, val) {
elem.setAttribute(attrname, val);
};
var tpl = new Template('<p data-attr="cloneid id">This is clone number <span class="clonenumber">one</span>!</p>');
var tpl_data = {
cloneid: 0,
clonenumber: 0
};
var df = document.createDocumentFragment();
for (var i = 0; i < 100; i++) {
tpl_data.cloneid = 'id' + i;
tpl_data.clonenumber = i;
df.appendChild(tpl.render(tpl_data));
}
document.body.appendChild(df);
I'd be shocked if innerHTML wasn't faster. Pre-compiled templates such as those provided by lo-dash or doT seem like a great way to go!
Check out this simple example:
http://jsperf.com/lodash-template
It shows you can get 300,000 ops/sec for a fairly complex template with a loop using lo-dash's pre-compiled templates. Seems pretty fast to me and way cleaner JS.
Obviously, this is only one part of the problem. This generates the HTML, actually inserting the HTML is another problem, but once again, innerHTML seems to win over cloneNode and other DOM-based approaches and generally the code is way cleaner.
http://jsperf.com/clonenode-vs-innerhtml-redo/2
Obviously you can take these benchmarks worth a grain of salt. What really matters is your actual app. But I'd recommend giving multiple approaches a try and benchmarking them yourself before making up your mind.
Note: A lot of the benchmarks about templates on JSPerf are doing it wrong. They're re-compiling the template on every iteration, which is obviously going to be way slow.
For the purpose of this question lets say we need to append() 1000 objects to the body element.
You could go about it like this:
for(x = 0; x < 1000; x++) {
var element = $('<div>'+x+'</div>');
$('body').append(element);
}
This works, however it seems inefficient to me as AFAIK this will cause 1000 document reflows. A better solution would be:
var elements = [];
for(x = 0; x < 1000; x++) {
var element = $('<div>'+x+'</div>');
elements.push(element);
}
$('body').append(elements);
However this is not an ideal world and this throws an error Could not convert JavaScript argument arg 0 [nsIDOMDocumentFragment.appendChild]. I understand that append() can't handle arrays.
How would I using jQuery (I know about the DocumentFragment node, but assume I need to use other jQuery functions on the element such as .css()) add a bunch of objects to the DOM at once to improve performance?
You could use an empty jQuery object instead of an array:
var elements = $();
for(x = 0; x < 1000; x++) {
elements = elements.add('<div>'+x+'</div>');
// or
// var element = $('<div>'+x+'</div>');
// elements = elements.add(element);
}
$('body').append(elements);
This might be useful if you want to do stuff with newly generated element inside the loop. But note that this will create a huge internal stack of elements (inside the jQuery object).
It seems though that your code works perfectly fine with jQuery 1.8.
You could just call
$('body').append(elements.join(''));
Or you can just create a large string in the first place.
var elements = '';
for(x = 0; x < 1000; x++) {
elements = elements + '<div>'+x+'</div>';
}
$(document.body).append(elements);
Like you mentioned, probably the most "correct" way is the usage of a DocFrag. This could look like
var elements = document.createDocumentFragment(),
newDiv;
for(x = 0; x < 1000; x++) {
newDiv = document.createElement('div');
newDiv.textContent = x;
elements.append( newDiv );
}
$(document.body).append(elements);
.textContent is not supported by IE<9 and would need an conditional check to use .innerText or .text instead.
Upgrade to jQuery 1.8, this works as intended:
$('body').append([
'<b>1</b>',
'<i>2</i>'
]);
Since $.fn.append takes a variable number of elements we can use apply to pass the array as arguments to it:
el.append.apply(el, myArray);
This works if you have an array of jQuery objects. According to the spec though you can append an array of elements if you have the DOM elements. If you have an array of html strings you can just .join('') them and append them all at once.
A slight change to your second approach:
var elements = [],
newDiv;
for (x = 0; x < 1000; x++) {
newDiv = $('<div/>').text(x);
elements.push(newDiv);
}
$('body').append(elements);
$.append() certainly can append an array: http://api.jquery.com/append/
.append(content) | content: One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements.
Sometimes, jQuery isn't the best solution. If you have a lot of elements to append to the DOM, documentFragment is a viable solution:
var fragment = document.createDocumentFragment();
for(var i = 0; i < 1000; i++) {
fragment.appendChild(document.createElement('div'));
}
document.getElementsByTagName('body')[0].appendChild(fragment);
If you're going for raw performance then I would suggest pure JS, though some would argue that your development performance is more important than your site's/program performance.
Check this link for benchmarks and a showcase of different DOM insertion techniques.
edit:
As a curiosity, documentFragment proves to be one of the slowest methods.
I would use native Javascript, normally much faster:
var el = document.getElementById('the_container_id');
var aux;
for(x = 0; x < 1000; x++) {
aux = document.createElement('div');
aux.innerHTML = x;
el.appendChild(aux);
}
EDIT:
There you go a jsfiddle with different options implemented. The #jackwander's solution is, clearly, the most effective one.
I know, the question is old, but maybe it helps others.
Or simple use ECMA6 spread operator:
$('body').append(...elements);
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>