In jQuery to traverse DOM several steps upward instead of
$(this).parent().parent().parent().parent().parent().click();
I can write short version:
$(this).parent(5).click();
So, I want to know, if there's a way to shorten code instead of spamming '.parentNode'?
Rather trivial:
function parent (element, n = 1) {
let {parentNode} = element;
for (let i = 1; parentNode && i < n; i++) {
({parentNode} = parentNode);
}
return parentNode;
}
const span = document.querySelector('span');
const directParent = parent(span); // direct parent node
const greatGreatGreatGrandParent = parent(span, 5); // parent node 5 times
console.log(directParent.getAttribute('data-foo')); // baz
console.log(greatGreatGreatGrandParent.getAttribute('data-foo')); // bar
<div data-foo="bar">
<div>
<div>
<div>
<div data-foo="baz">
<span>Hello, World!</span>
</div>
</div>
</div>
</div>
</div>
I check for parentNode because it might be null. In that case, I break the loop and return null, because continuing the loop would result in an error:
({parentNode} = null); // TypeError: can't convert null to object
The parentheses are necessary to indicate that the opening { doesn't start a block, but a destructuring assignment:
{parentNode} = parentNode; // SyntaxError: expected expression, got '='
Edit:
I must admit that my solution is rather condensed and uses some new JavaScript features to make it even more succinct.
What does let {parentNode} = element; mean?
Object destructuring lets you read properties from an object in a more succinct way:
let {parentNode} = element;
is equivalent to:
let parentNode = element.parentNode;
As explained above,
({parentNode} = parentNode);
is equivalent to:
parentNode = parentNode.parentNode;
What does parentNode && i < n exactly?
Every for-loop has a condition. If this condition is true, the loop is continued. If the condition is false, it breaks out of the loop.
I could write:
for (let i = 1; i < n; i++) {
// ...
}
That would run the loop n - 1 iterations. After the last iteration, i is equal to n, so the condition evaluates to false and it stops, which is fine.
parentNode && i < n extends this condition. It not only checks whether i is less than n, but it also checks if parentNode contains a truthy value. If parentNode is null (which might happen is an element has no parent node), the loop will break, because it can't read the property parentNode of null.
I hope you understand the explanations with the following function which is equivalent to the original one:
function parent (element, times) {
var n; // how many times 'parentNode'?
if (times) {
n = times;
} else {
n = 1;
}
var elementToReturn = element.parentNode;
for (var i = 1; i < n; i++) {
if (elementToReturn === null) {
break;
} else {
elementToReturn = elementToReturn.parentNode;
}
}
return elementToReturn;
}
The sorcerer's one-liner...
It's the same as #petermader's only shorter. I guess there's less of a need to be super explicit here, as this would probably be just an imported utility function, and it's still more reliable than a for loop.
const getParentElement = ({ parentNode }, n = 1) =>
Array.from({ length: n - 1 }, () => ({ parentNode } = parentNode)) && parentNode
Related
The snippet below is from MDN - A reintroduction to Javascript, it is supposed to demonstrate IIFE. I kinda see that it is supposed to count the characters in this text node but I am not sure about a couple things. The first is why does the for statement have 2 arguments in the first argument section var i=0, child. The second is more general, how does it work with this function calling itself .. can someone explain the overall flow to me please?
var charsInBody = (function counter(elm) {
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += counter(child);
}
return count;
})(document.body);
The first is why does the for statement have 2 arguments in the first argument section var i=0,child ?
A for loop is just a condensed version of a while loop, that means that:
for(declarations; condition; last) {
body
}
is the same as:
declarations
while(condition) {
body
last
}
That means that in your case it is as:
var i = 0, child;
while(child = elm.childNodes[i]) {
count += counter(child);
i++
}
So actually child just defines a new variable before the loop
I'm very new at recursion, and have been tasked with writing getElementsByClassName in JavaScript without libraries or the DOM API.
There are two matching classes, one of which is in the body tag itself, the other is in a p tag.
The code I wrote isn't working, and there must be a better way to do this. Your insight would be greatly appreciated.
var elemByClass = function(className) {
var result = [];
var nodes = document.body; //<body> is a node w/className, it needs to check itself.
var childNodes = document.body.childNodes; //then there's a <p> w/className
var goFetchClass = function(nodes) {
for (var i = 0; i <= nodes; i++) { // check the parent
if (nodes.classList == className) {
result.push(i);
console.log(result);
}
for (var j = 0; j <= childNodes; j++) { // check the children
if (childNodes.classList == className) {
result.push(j);
console.log(result);
}
goFetchClass(nodes); // recursion for childNodes
}
goFetchClass(nodes); // recursion for nodes (body)
}
return result;
};
};
There are some errors, mostly logical, in your code, here's what it should have looked like
var elemByClass = function(className) {
var result = [];
var pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
(function goFetchClass(nodes) {
for (var i = 0; i < nodes.length; i++) {
if ( pattern.test(nodes[i].className) ) {
result.push(nodes[i]);
}
goFetchClass(nodes[i].children);
}
})([document.body]);
return result;
};
Note the use of a regex instead of classList, as it makes no sense to use classList which is IE10+ to polyfill getElementsByClassName
Firstly, you'd start with the body, and check it's className property.
Then you'd get the children, not the childNodes as the latter includes text-nodes and comments, which can't have classes.
To recursively call the function, you'd pass the children in, and do the same with them, check for a class, get the children of the children, and call the function again, until there are no more children.
Here are some reasons:
goFetchClass needs an initial call after you've defined it - for example, you need a return goFetchClass(nodes) statement at the end of elemByClass function
the line for (var i = 0; i <= nodes; i++) { will not enter the for loop - did you mean i <= nodes.length ?
nodes.classList will return an array of classNames, so a direct equality such as nodes.classList == className will not work. A contains method is better.
Lastly, you may want to reconsider having 2 for loops for the parent and children. Why not have 1 for loop and then call goFetchClass on the children? such as, goFetchClass(nodes[i])?
Hope this helps.
I'm having an issue with a function I've written to "clean" up, see the code below and I'll explain how it works underneath.
clean: function (e) {
var
els = null,
i = 0;
if (e === undefined) {
e = this.cont;
}
els = e.getElementsByTagName('*');
for (i=0;i<els.length;i++) {
if (els[i].className.search('keep') === -1) {
e.removeChild(els[i]);
}
}
return this;
},
The argument e is a dom element, if it isn't supplied this.cont is also a dom element stored earlier in the whole function and e is defaulted to it.
The function loops through all of the child elements and checks it doesn't have the class keep (fairly obvious what this does) and removes any that don't match.
It all seemed to be working but I have an element which has 2 images and 2 inputs none with the class 'keep' but the variable i only gets to 2 and the loop stops (it should reach 4 and remove all four elements)
any help would be greatly appreciated.
/* UPDATE */
Thanks to #pimvb and and #Brett Walker the final code which works great is below.
clean: function (e) {
var
els = null,
i = 0;
if (e === undefined) {
e = this.cont;
}
els = e.getElementsByTagName('*');
i = els.length;
while (i--) {
if (els[i].className.search('keep') === -1) {
els[i].parentNode.removeChild(els[i]);
}
}
return this;
},
The .getElementsByTagName function returns a NodeList which is basically an array but is 'live', which means it's updated automatically if you e.g. remove a child. So when iterating, els.length is changing, resulting in being 2 when you remove 2 children (there are 4 - 2 = 2 left). When having removed 2 children, i == 2 so the loop will end prematurely to what you expect.
To circumvent this and make it a 'static' array, you can convert it into an array like this, which does not update itself:
els = [].slice.call(e.getElementsByTagName('*')); // [].slice.call is a trick to
// convert something like a NodeList
// into a static, real array
As Brett Walker pointed out, you can also iterate backwards, like this:
http://jsfiddle.net/pimvdb/cYKxU/1/
var elements = document.getElementsByTagName("a"),
i = elements.length;
while(i--) { // this will stop as soon as i == 0 because 0 is treated as false
var elem = elements[i]; // current element
if(elem.className == "test") // remove if it should be removed
elem.parentNode.removeChild(elem);
}
This will start at the last element. The .length still gets updated (i.e. becomes less), but this does not matter as you only used it at the beginning, and not during iterating. As a result, you don't suffer from this 'quirk'.
I've got an in page text search using JS, which is here:
$.fn.eoTextSearch = function(pat) {
var out = []
var textNodes = function(n) {
if (!window['Node']) {
window.Node = new Object();
Node.ELEMENT_NODE = 1;
Node.ATTRIBUTE_NODE = 2;
Node.TEXT_NODE = 3;
Node.CDATA_SECTION_NODE = 4;
Node.ENTITY_REFERENCE_NODE = 5;
Node.ENTITY_NODE = 6;
Node.PROCESSING_INSTRUCTION_NODE = 7;
Node.COMMENT_NODE = 8;
Node.DOCUMENT_NODE = 9;
Node.DOCUMENT_TYPE_NODE = 10;
Node.DOCUMENT_FRAGMENT_NODE = 11;
Node.NOTATION_NODE = 12;
}
if (n.nodeType == Node.TEXT_NODE) {
var t = typeof pat == 'string' ?
n.nodeValue.indexOf(pat) != -1 :
pat.test(n.nodeValue);
if (t) {
out.push(n.parentNode)
}
}
else {
$.each(n.childNodes, function(a, b) {
textNodes(b)
})
}
}
this.each(function() {
textNodes(this)
})
return out
};
And I've got the ability to hide columns and rows in a table. When I submit a search and get the highlighted results, there would be in this case, the array length of the text nodes found would be 6, but there would only be 3 highlighted on the page. When you output the array to the console you get this:
So you get the 3 tags which I was expecting, but you see that the array is actually consisting of a [span,undefined,span,undefined,undefined,span]. Thus giving me the length of 6.
<span>
<span>
<span>
[span, undefined, span, undefined, undefined, span]
I don't know why it's not stripping out all of the undefined text nodes when I do the check for them. Here's what I've got for the function.
performTextSearch = function(currentObj){
if($.trim(currentObj.val()).length > 0){
var n = $("body").eoTextSearch($.trim(currentObj.val())),
recordTitle = "matches",
arrayRecheck = new Array(),
genericElemArray = new Array()
if(n.length == 1){
recordTitle = "match"
}
//check to see if we need to do a recount on the array length.
//if it's more than 0, then they're doing a compare and we need to strip out all of the text nodes that don't have a visible parent.
if($(".rows:checked").length > 0){
$.each(n,function(i,currElem){
if($(currElem).length != 0 && typeof currElem != 'undefined'){
if($(currElem).closest("tr").is(":visible") || $(currElem).is(":visible")){
//remove the element from the array
console.log(currElem)
arrayRecheck[i] = currElem
}
}
})
}
if(arrayRecheck.length > 0){
genericElemArray.push(arrayRecheck)
console.log(arrayRecheck)
}
else{
genericElemArray.push(n)
}
genericElemArray = genericElemArray[0]
$("#recordCount").text(genericElemArray.length + " " +recordTitle)
$(".searchResults").show()
for(var i = 0; i < genericElemArray.length; ++i){
void($(genericElemArray[i]).addClass("yellowBkgd").addClass("highLighted"))
}
}
else{
$(".highLighted").css("background","none")
}
}
If you look at the code below "//check to see if we need to do a recount on the array length. ", you'll see where I'm stripping out the text nodes based off of the display and whether or not the object is defined. I'm checking the length instead of undefined because the typeof == undefined wasn't working at all for some reason. Apparently, things are still slipping by though.
Any idea why I'm still getting undefined objects in the array?
My apologies for such a big post!
Thanks in advance
I've modified your eoTextSearch() function to remove dependencies on global variables in exchange for closures:
$.fn.extend({
// helper function
// recurses into a DOM object and calls a custom function for every descendant
eachDescendant: function (callback) {
for (var i=0, j=this.length; i<j; i++) {
callback.call(this[i]);
$.fn.eachDescendant.call(this[i].childNodes, callback);
}
return this;
},
// your text search function, revised
eoTextSearch: function () {
var text = document.createTextNode("test").textContent
? "textContent" : "innerText";
// the "matches" function uses an out param instead of a return value
var matches = function (pat, outArray) {
var isRe = typeof pat.test == "function";
return function() {
if (this.nodeType != 3) return; // ...text nodes only
if (isRe && pat.test(this[text]) || this[text].indexOf(pat) > -1) {
outArray.push(this.parentNode);
}
}
};
// this is the function that will *actually* become eoTextSearch()
return function (stringOrPattern) {
var result = $(); // start with an empty jQuery object
this.eachDescendant( matches(stringOrPattern, result) );
return result;
}
}() // <- instant calling is important here
});
And then you can do something like this:
$("body").eoTextSearch("foo").filter(function () {
return $(this).closest("tr").is(":visible");
});
To remove unwanted elements from the search result. No "recounting the array length" necessary. Or you use each() directly and decide within what to do.
I cannot entirely get my head around your code, but the most likely issue is that you are removing items from the array, but not shrinking the array afterwards. Simply removing items will return you "undefined", and will not collapse the array.
I would suggest that you do one of the following:
Copy the array to a new array, but only copying those items that are not undefined
Only use those array items that are not undefined.
I hope this is something of a help.
Found the answer in another post.
Remove empty elements from an array in Javascript
Ended up using the answer's second option and it worked alright.
I'm using JS on firefox 4 and get the "too much recursion error" for the following code:
extractText: function(domObj) {
if (domObj == null) {
return "";
} else {
var acc = "";
if (domObj.nodeType == Node.TEXT_NODE) {
acc += domObj.nodeValue;
}
if (domObj.hasChildNodes()) {
var children = currentObj.childNodes;
for (var i = 0; i < children.length; i++) {
acc += sui.extractText(children[i]);
}
}
return acc;
}
}
};
Anyone?
I think that this line:
var children = currentObj.childNodes;
should be:
var children = domObj.childNodes;
It looks to me as if your reference to "currentObj" is starting over at the top instead of descending from the element under examination. It's hard to tell of course because you didn't include the relevant definition or initialization of "currentObj".
You can also try an iterative approach instead of recursion:
extractText: function(domObj) {
if (!(domObj instanceof Node)) return null;
var stack = [domObj], node, tf = [];
while (stack.length > 0) {
node = stack.pop();
switch (node.nodeType) {
case Node.TEXT_NODE:
tf.push(node.nodeValue);
break;
case Node.ELEMENT_NODE:
for (var i=node.childNodes.length-1; i>=0; i--)
stack.push(node.childNodes[i]);
break;
}
}
return tf.join("");
}
This algorithm implements a depth first search using a stack for the nodes that still must be visited. The first item on the stack is domObj if it’s a Node instance. Then for each node on the stack: if it is a Text node, its value is added to the text fragments array tf; if it’s an Element node, its child nodes are put on the stack in reverse order so that the first child is on top of the stack. These steps are repeated until the stack is empty. At the end, the text fragments in tf are put together using the array’s join method.