Scan the DOM upward to find element matching selector (no jQuery) - javascript

I wanted a function that could scan the DOM upward from a DOMElement and also scan the children of each parent as it's going up.
It had to keep going until it would found any <element> matching the selector received in parameter. The selector had to be any type of valid CSS selector.
It was also needed to be done in pure JS (no jQuery)

I ended up making a modified version of this function that I found on this site. You can use this function as you wish, scale it up, claim it yours, whatever you want.
Here's the solution I found
GetClosest = function (elem, selector) {
for (; elem && elem !== document.body; elem = elem.parentNode) {
// If the elem matches at first iteration.
if(elem.matches(selector)) {
return elem;
} else {
// Scans all the childs of current iterated element (always higher in DOM until found).
// If one matches the selector it'll stop and return it.
child = elem.parentNode.firstChild;
do {
if(child.nodeType === 3) continue; // text node
if(child.matches(selector)) return child;
} while (child = child.nextElementSibling);
}
}
return null;
};

Related

Counting parent nodes

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.

How can I parse a document and remove all elements except for one that matches and ID and its children

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 ().

Replace all the ocurrance of a string in an element

I want to replace a particular string in (the text of) all the descendant elements of a given element.
innerHTML cannot be used as this sequence can appear in attributes. I have tried using XPath, but it seems the interface is essentially read-only. Because this is limited to one element, functions like document.getElementsByTagName cannot be used either.
Could any suggest any way to do this? Any jQuery or pure DOM method is acceptable.
Edit:
Some of the answers are suggesting the problem I was trying to work around: modifying the text directly on an Element will cause all non-Text child nodes to be removed.
So the problem essentially comes down to how to efficiently select all the Text nodes in a tree. In XPath, you can easily do it as //text(), but the current XPath interface does not allow you to change these Text nodes it seems.
One way to do this is by recursion as shown in the answer by Bergi. Another way is to use the find('*') selector of jQuery, but this is a bit more expensive. Still waiting to see if there' are better solutions.
Just use a simple selfmade DOM-iterator, which walks recursively over all nodes:
(function iterate_node(node) {
if (node.nodeType === 3) { // Node.TEXT_NODE
var text = node.data.replace(/any regular expression/g, "any replacement");
if (text != node.data) // there's a Safari bug
node.data = text;
} else if (node.nodeType === 1) { // Node.ELEMENT_NODE
for (var i = 0; i < node.childNodes.length; i++) {
iterate_node(node.childNodes[i]); // run recursive on DOM
}
}
})(content); // any dom node
A solution might be to surf through all available nodes (TextNodes included) and apply a regexp pattern on the results. To grab TextNodes as well, you need to invoke jQuerys .contents(). For instance:
var search = "foo",
replaceWith = 'bar',
pattern = new RegExp( search, 'g' );
function searchReplace( root ) {
$( root ).contents().each(function _repl( _, node ) {
if( node.nodeType === 3 )
node.nodeValue = node.nodeValue.replace( pattern, replaceWith );
else searchReplace( node );
});
}
$('#apply').on('click', function() {
searchReplace( document.getElementById('rootNode') );
});
Example: http://jsfiddle.net/h8Rxu/3/
Reference: .contents()
Using jQuery:
$('#parent').children().each(function () {
var that = $(this);
that.text(that.text().replace('test', 'foo'));
});
If you prefer to search through all children instead of just immediate children, use .find() instead.
http://jsfiddle.net/ExwDx/
Edit: Documentation for children, each, text, and find.
Sorry, just got it myself:
$('#id').find('*').each(function(){
$.each(this.childNodes, function() {
if (this.nodeType === 3) {
this.data = this.data.toUpperCase();
}
})
})
I used toUpperCase() here to make the result more obvious, but any String operation would be valid there.

jquery, how to check if a specific ID is a child of an other id?

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

Hide an element's next sibling with Javascript

I have an element grabbed from document.getElementById('the_id'). How can I get its next sibling and hide it? I tried this but it didn't work:
elem.nextSibling.style.display = 'none';
Firebug error was elem.nextSibling.style is undefined.
it's because Firefox considers the whitespace between element nodes to be text nodes (whereas IE does not) and therefore using .nextSibling on an element gets that text node in Firefox.
It's useful to have a function to use to get the next element node. Something like this
/*
Credit to John Resig for this function
taken from Pro JavaScript techniques
*/
function next(elem) {
do {
elem = elem.nextSibling;
} while (elem && elem.nodeType !== 1);
return elem;
}
then you can do
var elem = document.getElementById('the_id');
var nextElem = next(elem);
if (nextElem)
nextElem.style.display = 'none';
Take a look at the Element Traversal API, that API moves between Element nodes only. This Allows the following:
elem.nextElementSibling.style.display = 'none';
And thus avoids the problem inherent in nextSibling of potentially getting non-Element nodes (e.g. TextNode holding whitespace)
Firebug error was elem.nextSibling.style is undefined.
because nextSibling can be a text-node or other node type
do {
elem = elem.nextSibling;
} while(element && elem.nodeType !== 1); // 1 == Node.ELEMENT_NODE
if(elem) elem.style.display = 'none';
Try looping through the children of this element using something like:
var i=0;
(foreach child in elem)
{
if (i==0)
{
document.getElementByID(child.id).style.display='none';
}
}
Please make appropriate corrections to the syntax.

Categories

Resources