Is there a native method of DOM element in ECMAScript that will allow to count all ancestors of a given element (up to window object or DOM element specified by Id,Name etc.)?
Example use is to check all ancestors of a given element and remove a specified attribute.
There is the node iterator ( https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator ) which could be used for this purpose
You can use xpath:
document.evaluate('ancestor::*', x, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null)
See https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript for more details.
I've wrote a simple function that does an action described in a question. It gets all ancestors of a given element and removes a given attribute from every element that is between "starting" and "ending" element (it does not perform an removeAttribute method on "ending" element).
var modifyAncestors = function(startingelementid,endingelementId,searchedattribute) {
var nodecount = document.getElementById(endingelementId).childNodes.length;
console.log(nodecount);
var currentid = startingelementid;
console.log(currentid);
console.log(searchedattribute);
for(var i = 0; i < nodecount; i++) {
if(currentid == endingelementId) {
break;
}
else {
document.getElementById(currentid).removeAttribute(searchedattribute);
currentid = document.getElementById(currentid).parentNode.id;
}
}
}
Working example: http://www.blacktieseo.com/so/js/test.html (couldn't get it to work with Fiddle JS).
Any comments, bugs etc. will be highly appreciated.
Related
Normally I'm doing it this way:
for(i=0;i<elem.parentNode.length;i++) {
if (elem.parentNode[i] == elem) //.... etc.. etc...
}
function getChildIndex(node) {
return Array.prototype.indexOf.call(node.parentNode.childNodes, node);
}
This seems to work in Opera 11, Firefox 4, Chromium 10. Other browsers untested. It will throw TypeError if node has no parent (add a check for node.parentNode !== undefined if you care about that case).
Of course, Array.prototype.indexOf does still loop, just within the function call. It's impossible to do this without looping.
Note: If you want to obtain the index of a child Element, you can modify the function above by changing childNodes to children.
function getChildElementIndex(node) {
return Array.prototype.indexOf.call(node.parentNode.children, node);
}
Option #1
You can use the Array.from() method to convert an HTMLCollection of elements to an array. From there, you can use the native .indexOf() method in order to get the index:
function getElementIndex (element) {
return Array.from(element.parentNode.children).indexOf(element);
}
If you want the node index (as oppose to the element's index), then replace the children property with the childNodes property:
function getNodeIndex (element) {
return Array.from(element.parentNode.childNodes).indexOf(element);
}
Option #2
You can use the .call() method to invoke the array type's native .indexOf() method. This is how the .index() method is implemented in jQuery if you look at the source code.
function getElementIndex(element) {
return [].indexOf.call(element.parentNode.children, element);
}
Likewise, using the childNodes property in place of the children property:
function getNodeIndex (element) {
return [].indexOf.call(element.parentNode.childNodes, element);
}
Option #3
You can also use the spread operator:
function getElementIndex (element) {
return [...element.parentNode.children].indexOf(element);
}
function getNodeIndex (element) {
return [...element.parentNode.childNodes].indexOf(element);
}
You could count siblings...
The childNodes list includes text and element nodes-
function whichChild(elem){
var i= 0;
while((elem=elem.previousSibling)!=null) ++i;
return i;
}
There is no way to get the index of a node within its parent without looping in some manner, be that a for-loop, an Array method like indexOf or forEach, or something else. An index-of operation in the DOM is linear-time, not constant-time.
More generally, if list mutations are possible (and the DOM certainly supports mutation), it's generally impossible to provide an index-of operation that runs in constant time. There are two common implementation tactics: linked lists (usually doubly) and arrays. Finding an index using a linked list requires a walk. Finding an index using an array requires a scan. Some engines will cache indexes to reduce time needed to compute node.childNodes[i], but this won't help you if you're searching for a node. Not asking the question is the best policy.
I think you've got it, but:
make sure that variable "i" is declared with var
use === instead of == in the comparison
If you have a collection input elements with the same name (like <textarea name="text_field[]"…) in your form and you want to get the exact numeric index of the field that triggered an event:
function getElementIdxFromName(elem, parent) {
var elms = parent[elem.name];
var i = 0;
if (elms.length === undefined) // there is only one element with this name in the document
return 0;
while((elem!=elms[i])) i++;
return i;
}
Getting numeric id of an element from a collection of elements with the same class name:
function getElementIdxFromClass(elem, cl) {
var elems = document.getElementsByClassName(cl);
var i = 0;
if (elems.length > 0) {
while((elem!=elems[i])) i++;
return i;
}
return 0;
}
Try this:
let element = document.getElementById("your-element-id");
let indexInParent = Array.prototype.slice.call(element.parentNode.parentNode.children).indexOf(element.parentNode));
I have a webpage with many divs nested. How can I strip away all elements except 1 that has a certain ID and its children.
I want to keep that div and its children and remove everything else even its parents
The following code doesn't work it deletes the children as well
var elms = document.getElementsByTagName('*');
for (var i = 0; i < elms.length; i++) {
if (elms[i].id != "div63") {
elms[i].parentNode.removeChild(elms[i])
}
};
I would like a non-jQuery solution.
You can save a reference to your node, remove all, and then put your node in the body :
var saved = document.getElementById('div63');
var elms = document.body.childNodes;
while (elms.length) document.body.removeChild(elms[0]);
document.body.appendChild(saved);
Demonstration
A slightly alternative approach to that provided by dystroy, in that the following moves the element you wish to keep, placing it as the first child of the parent from which you want to remove all other children (defaulting to the body element if no parent is provided), as opposed to the alternate 'remove everything and then put it back' approach. Following the move, this then removes all subsequent child-nodes from that parent (this includes a rather ugly function to retrieve a given element, albeit with no attempt made to compensate for the lack of document.querySelector() in browsers without that feature)):
function retrieveElem(ref) {
if (!ref) {
return document.body;
} else {
if (typeof ref === 'string') {
if (document.querySelector) {
var dQSresult = document.querySelector(ref);
if (dQSresult) {
return dQSresult;
} else {
return document.querySelector('#' + ref);
}
}
} else {
switch (ref.nodeType) {
case 1:
// it's an element
return ref;
case 9:
// it's the document node
return document.body;
}
}
}
}
function clearAllExcept(el, from) {
el = retrieveElem(el);
from = retrieveElem(from);
from.insertBefore(el, from.firstChild);
while (from.firstChild.nextSibling) {
from.removeChild(from.firstChild.nextSibling);
}
}
clearAllExcept('#id63','.aChild');
JS Fiddle demo.
This can be called as above, using CSS selector strings, or as follows:
// with a CSS selector to identify the `id` of the child
clearAllExcept('#id63');
JS Fiddle demo.
// with a string to identify the `id` of the child
clearAllExcept('id63');
JS Fiddle demo.
// with a node-reference to the child:
clearAllExcept(document.getElementById('id63'));
JS Fiddle demo.
Similar selectors can be used to identify the parent, also:
// using the `document`:
clearAllExcept('#id63', document);
JS Fiddle demo.
// with a string to identify the `id` of the parent
clearAllExcept('#id63','#test');
JS Fiddle demo.
// with a node-reference to the parent:
clearAllExcept('#id63', document.getElementById('test'));
JS Fiddle demo.
References:
document.querySelector().
Node.firstChild.
Node.insertBefore.
Node.nextSibling.
Node.nodeType.
switch ().
I'm trying to add a event listener for clicking and I want to know the position of the node that was clicked
function evl(etna) {
document.addEventListener("click", function (el) {
alert("You clicked on " + 'the name of element that was clicked or his array code');
}, false);
};
where etna is:
document.getElementsByTagName("*");
function evl(etna){
document.addEventListener("click",function (el) {
var clickedElement = el.target || el.srcElement;
alert("Link detected a click. Cancellable: "+clickedElement.name);
for(var i = 0; i < etna.length; i++) {
if(etna[i] === clickedElement) {
//i is a position of an element in etna
break;
}
}
},false);
};
You can use this which will point to a clicked element. As to Phil H IE 8 does not work that way. But anyway, there should be used .target or .srcElement. And maybe it will be better to get its id. Name attribute is not valid for divs, spans, etc.
But also you are attaching an event to a document. And this will point to a document.
Instead of that you should use el.target || el.srcElement where .target/.srcElement is a pointer to a node where click actually happened.
Also, I do not think you can get index of an element in array (actually, node list) returned by document.getElementsByTagName("*") (well, you can get that list and iterate through it in loop and check each element if it is eaqual to this). Plus, I have no idea why it could be needed.
Add a loop and set the event listener differently for each item in the etna array:
function evl(etna){
for(var i=0; i < etna.length; ++i) {
(function() {
var thisNode = etna[i];
var localval = i;
etna[i].addEventListener("click",function (el) {
alert("Link detected a click. Cancellable: "+ thisNode.id + " which is id " + localval );
},false);
})();
}
}
Working jsfiddle: http://jsfiddle.net/5xDjE/
The function that is immediately called is merely to force the scoping of thisNode and localval, otherwise all the elements get references to the same variable (javascript scoping is interesting).
I would advise against using the index (scoped via localval) because it requires retaining the original array of nodes. Since nodes change over time and javascript does reference counting for you, you want to avoid these kinds of long arrays of nodes.
Note that this doesn't always have element that was clicked, in IE8 and below this points to the global window object.
I have an XHTML page where each HTML element has a unique custom attribute, like this:
<div class="logo" tokenid="14"></div>
I need a way to find this element by ID, similar to document.getElementById(), but instead of using a general ID, I want to search for the element using my custom "tokenid" attribute. Something like this:
document.getElementByTokenId('14');
Is that possible? If yes - any hint would be greatly appreciated.
Thanks.
It is not good to use custom attributes in the HTML. If any, you should use HTML5's data attributes.
Nevertheless you can write your own function that traverses the tree, but that will be quite slow compared to getElementById because you cannot make use of any index:
function getElementByAttribute(attr, value, root) {
root = root || document.body;
if(root.hasAttribute(attr) && root.getAttribute(attr) == value) {
return root;
}
var children = root.children,
element;
for(var i = children.length; i--; ) {
element = getElementByAttribute(attr, value, children[i]);
if(element) {
return element;
}
}
return null;
}
In the worst case, this will traverse the whole tree. Think about how to change your concept so that you can make use browser functions as much as possible.
In newer browsers you use of the querySelector method, where it would just be:
var element = document.querySelector('[tokenid="14"]');
This will be much faster too.
Update: Please note #Andy E's comment below. It might be that you run into problems with IE (as always ;)). If you do a lot of element retrieval of this kind, you really should consider using a JavaScript library such as jQuery, as the others mentioned. It hides all these browser differences.
<div data-automation="something">
</div>
document.querySelector("div[data-automation]")
=> finds the div
document.querySelector("div[data-automation='something']")
=> finds the div with a value
If you're using jQuery, you can use some of their selector magic to do something like this:
$('div[tokenid=14]')
as your selector.
You can accomplish this with JQuery:
$('[tokenid=14]')
Here's a fiddle for an example.
If you're willing to use JQuery, then:
var myElement = $('div[tokenid="14"]').get();
Doing this with vanilla JavaScript will do the trick:
const something = document.querySelectorAll('[data-something]')
Use this more stable Function:
function getElementsByAttribute(attr, value) {
var match = [];
/* Get the droids we are looking for*/
var elements = document.getElementsByTagName("*");
/* Loop through all elements */
for (var ii = 0, ln = elements.length; ii < ln; ii++) {
if (elements[ii].nodeType === 1){
if (elements[ii].name != null){
/* If a value was passed, make sure it matches the elements */
if (value) {
if (elements[ii].getAttribute(attr) === value)
match.push(elements[ii]);
} else {
/* Else, simply push it */
match.push(elements[ii]);
}
}
}
}
return match;
};
I have a specific id ("mysubid"), now I want to check if this element (this id) is in a child path of an other id ("mymainid").
Is there an easy way to do this or will I go upwards, element by element, to see if the element is in a child path.
By child path I am talking about something like this:
A > B > C > D
So D is in the Child Path of A,B and C
You all are making this very complicated. Use the descendant selector:
if ($('#mymainid #mysubid').length) {
// #mysubid is inside #mymainid
}
var isInPath = $("#mysubid").closest("#mymainid").length > 0;
if( $("#mymainid").find("#mysubid").length > 0 )
if($('#mysubid','#mymainid').length)
{
}
This will check to see if #mysubid is within #mymainid
jQuery( selector, [ context ] )
selector: A string containing a selector expression
context: A DOM Element, Document, or jQuery to use as context
This is a just an overlaod for $('#mymainid').find('#mysubid').lentgh btw, verified from: http://github.com/jquery/jquery/blob/master/src/core.js#L162
On another note, using a method such as $('#a #b') resorts to using the Sizzle Selector witch is slower than doing $('#a',$('#b')), witch uses purely javascript's getElementById
Note: as jQuery returns an empty object by default if the selection is not found you should always use length.
If you want to see the entire chain as an array use elm.parentNode and work backwards. So, to answer your question (and the depth or distance between the elements) in POJ, you can use:
var doc = document,
child = doc.getElementById("mysubid"),
parent = doc.getElementById("mymainid"),
getParents = function (elm) {
var a = [], p = elm.parentNode;
while (p) {
a.push(p);
p = p.parentNode;
}
return a;
};
getParents(child).indexOf(parent);
I tried on various browsers and the DOM function below is between 3 to 10 times faster than the selector methods(jQuery or document.querySelectorAll)
function is(parent){
return {
aParentOf:function(child){
var cp = child.parentNode;
if(cp){
return cp.id === parent.id ?
true : is(parent).aParentOf(cp);
}
}
}
}
The call below will return true if A is a parent of D
is(document.getElementById('A')).aParentOf(document.getElementById('D'))
For just few calls I would use the $('#A #D').length
For very frequent calls I would use the DOM one.
Using the 'is' method actually returns a boolean.
if($('#mymainid').is(':has(#mysubid)')) // true
Going the other direction...
if($('#mysubid').parents('#mymainid').length) // 1