Recursing for elements by order - javascript

Today in class we were given a task to 'recreate' the jQuery functionality (I don't know why, I guess the teacher is sadistic).
We were given the following instruction:
Using only pure javascript (not even ECMEScript 2015) create a function '$' that will receive a tag / id / class.
If you're given only one token [ e.g $("#id)] return the suitable elements.
If you're given more than one token [ e.g $("nav div div p")]- search hierarchically for the last token.
Input: $('nav div p');
Goal: Get all the p elements that has a div above them which has a nav above the div.
if (query.split(" ").length > 1) {
var first = query.split(" ")[0];
var rest = query.split(" ").slice(1);
var curr_elements;
if (first.match(/^#.*/i)) {
curr_elements = (document.getElementById(first.substring(1)));
} else if (query.match(/^\..*/i)) {
curr_elements = document.getElementsByClassName(first.substring(1));
} else if (query.match(/^\w.*/i)) {
curr_elements = document.getElementsByTagName(first);
}
curr_elements = [].slice.call(curr_elements);
for (var e = 0; e < curr_elements.length; e++) {
if (curr_elements[e].hasChildNodes()) {
for (var i = 0; i < rest.length; i++) {
var temp = rest[i];
var children;
if (temp.match(/^#.*/i)) {
children = (document.getElementById(temp.substring(1)));
} else if (temp.match(/^\..*/i)) {
children = document.getElementsByClassName(temp.substring(1));
} else if (temp.match(/^\w.*/i)) {
children = document.getElementsByTagName(temp);
}
alert(children);
//curr_elements += children;
}
}
}
this.elements = curr_elements;
} else {
if (query.match(/^#.*/i)) {
this.elements.push(document.getElementById(query.substring(1)));
} else if (query.match(/^\..*/i)) {
this.elements = document.getElementsByClassName(query.substring(1));
} else if (query.match(/^\w.*/i)) {
this.elements = document.getElementsByTagName(query);
}
}
<nav>
<p id="1"></p>
</nav>
<nav>
<div>
</div>
<p>
<div>
<p id=2></p>
</div>
</p>
</nav>
<nav>
<div>
<p id="3"></p>
<p id="4"></p>
<div>
</div>
</div>
</nav>
<nav>
<div>
<div>
<div>
<p id="5"></p>
</div>
</div>
</div>
</nav>
Variables:
query: the first arg, is tag / id / class name.
curr_elements: array-to-be for temporarily storing the elements I get.
this.elements: the final HTMLCollection.
The part relating to single token ($("p");) works fine, but I'm having trouble figuring out how to recurse / iterate over the elements to get the paragraphs.
Hoping to get someone's idea / advice on how to continue.

Your code is fairly solid, you just are not using recursion which in this case is quite useful.
I've changed your code to use a recursive approach:
function $(selector, context) {
if (!selector) return false;
// context should be an array of previous nodes we have found. If it's undefined, assume the single-item array [document] as the starting context
if (!context) context = [document];
var s = selector.split(" ");
var first = s.shift();
var curr_elements = [], els;
for (var i=0; i < context.length; i++) {
var c = context[i];
// make sure els gets converted into a real array of nodes
if (first.match(/^#.*/i)) {
els = [c.getElementById(first.substring(1))];
} else if (first.match(/^\..*/i)) {
els = [].slice.call(c.getElementsByClassName(first.substring(1)));
} else if (first.match(/^\w.*/i)) {
els = [].slice.call(c.getElementsByTagName(first));
}
curr_elements = curr_elements.concat(els);
}
// if there are more items in s, then curr_elements is the context in which to find them. Otherwise, curr_elements is the array of elements we were looking for.
if (s.length) return $(s.join(" "), curr_elements);
return curr_elements;
}
var a = $(".test span");
console.log(a);
<div class="test">
<span>1</span><span>2</span><span>3</span>
</div>
<div class="not_test">
<span>4</span><span>5</span><span>6</span>
</div>
<p class="test">
<span>7</span><span>8</span><span>9</span>
</p>

Related

How to insert an element when its available?

I'm struggling on write my code.
What I am trying to do:
Insert random numbers every few sec next to the "show" elements, when its say its available.
Expected Result
When its "Not available", no numbers can be inserted in.
When its "Available", numbers can be inserted in.
var myVar = setInterval(breakdown, 2000);
/*
var myVar1 = setInterval(random, 1000);
function random() {
var n = Math.floor((Math.random() * 10) + 1);
let loops = Array.from(document.querySelectorAll('.show'));
for (const loop of loops) {
if () {
loop.innerHTML = n;
}
}
}
*/
function breakdown() {
let elems = Array.from(document.querySelectorAll('.demo'));
for (const elems1 of elems) {
let d = Math.random();
if (d < 0.50) {
let str = "Available";
text = str.fontcolor("green");
x = true;
} else {
let str = "Not Available";
text = str.fontcolor("red");
y = false;
}
elems1.innerHTML = text;
}
}
<p id="demo1" class="demo">
<p id="show1" class="show"></p>
<p id="demo2" class="demo">
<p id="show2" class="show"></p>
<p id="demo3" class="demo">
<p id="show3" class="show"></p>
In order to read other elements' (such as sibling in this case) state ('Available' or 'Not Available') for switching work, you can call the previousSibling of the target property then access its innerHTML. Read more about previousSibling dom property. https://www.w3schools.com/jsref/prop_node_previoussibling.asp
fiddle

Better way of targeting parent node 3 levels up?

I currently have a working variable that targets the parent node 3 levels up
searchClearSelector: '.search-form__clear', //The clear button on my input
//Checks if there is a value in the input to clear then calls a function
self._ClearActions = document.querySelectorAll(self.searchClearSelector);
if (self._ClearActions && self._ClearActions.length > 0) {
for (const _Action of self._ClearActions) {
_Action.addEventListener('click', self.ClearActionHandler);
}
}
//Clear function
ClearActionHandler() {
const self = this;
const _Handle = this;
const _HandleParent = parentNode.parentNode.parentNode;
const _SearchInput = _HandleParent.querySelector('.search-form__input');
}
<input className="search-form__input" placeholder="Enter text" />
<div className="search-form__actions">
<button type="button" className="search-form__clear">Clear</button>
</div>
It works fine, but seems a little hacky - is there a better way for me to do this?
Thanks!
EDIT - Updated code, i had to take a big chunk of the other stuff out as it is a huge file, the issue is with IE not clearing it (as it doesn't work with closest in vanilla JS)
You can implement a function like this:
const _HandleParent = parentsUntil(3, this); // 3rd parent
const parentsUntil = (num, elem) => {
if(num <= 0) return;
let node = elem;
while(num--) node = node.parentNode;
return node;
}
You could use a recursive function:
function upperParents(element, levels) {
if (levels == 1) return element.parentNode || element;
if (levels < 1) return element;
return upperParents(element.parentNode || element, levels - 1);
}
document.getElementById('btnwithparents').addEventListener('click', function() {
console.log(upperParents(this, 2));
});
<div class="theparent">
<span>
<button id="btnwithparents">Search my parent 2 levels above</button>
</span>
</div>
You can do this with while loop.
var node = document.getElementById("id");
var levels = 3;
while (levels >= 0) {
node = node.parentNode;
levels--;
}
Everyone gave solutions, here's another:
function getUpstream(el,lvl){
if(!(el instanceof Node)){
throw("Not a node!");
}
while((--lvl>=0)&&el){
el = el.parentNode;
}
return el;
}
getUpstream(document.body,1)

Iterate through HTML DOM and get depth

How is it possible to iterate through the HTML DOM and list all nodes with there depth in javascript.
Example:
<div>
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
</div>
would result in
div 0
img 1
p 1
span 2
Write a recursive function which tracks the depth:
function element_list(el,depth) {
console.log(el+' '+depth);
for(var i=0; i<el.children.length; i++) {
element_list(el.children[i],depth+1);
}
}
element_list(document,0);
As CodeiSir points out, this will also list text nodes, but we can filter them out by testing the nodeType. Variations on this code will allow/ignore other node types as desired.
function element_list(el,depth) {
if (el.nodeType === 3) return;
Note that the other answers are/where not realy correct ...
This will also filter out "TEXT" Nodes, and not output the BODY tag.
function getDef(element, def) {
var str = ""
var childs = element.childNodes
for (var i = 0; i < childs.length; ++i) {
if (childs[i].nodeType != 3) {
str += childs[i].nodeName + " " + def + "<br />"
str += getDef(childs[i], def + 1)
}
}
return str
}
// Example
document.body.innerHTML = getDef(document.body, 0)
<div>
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
</div>
Yes, you can! You would have to iterate and some logic to create this tree, but for your example, you could do something like:
var tracker = {};
Array.from(document.querySelectorAll("*")).forEach(node => {
if (!tracker[node.tagName]) tracker[node.tagName] = 1;
else tracker[node.tagName]++;
});
console.log(tracker);
You can modify this to run on a recrusive subset of childNodes. This just iterates the entire document.
Check this fiddle and open the console to see the output of tracker which counts and lists tag names. To add the depth, just grab the parentNode.length all the way up.
Here's an updated script which I think does the depth count propery;
var tracker = {};
var depth = 0;
var prevNode;
Array.from(document.querySelectorAll("*")).forEach(node => {
if (!tracker[node.tagName]) tracker[node.tagName] = 1;
else tracker[node.tagName]++;
console.log("Node depth:", node.tagName, depth);
if (node.parentNode != prevNode) depth++;
prevNode = node;
});
console.log(tracker);
getElementDepth returns the absolute depth of the node (starting from the html node), to get the difference of depth between two nodes you can just subtract an absolute depth from another.
function getElementDepthRec(element,depth)
{
if(element.parentNode==null)
return depth;
else
return getElementDepthRec(element.parentNode,depth+1);
}
function getElementDepth(element)
{
return getElementDepthRec(element,0);
}
function clickEvent() {
alert(getElementDepth(document.getElementById("d1")));
}
<!DOCTYPE html>
<html>
<body>
<div>
<div id="d1">
</div>
</div>
<button onclick="clickEvent()">calculate depth</button>
</body>
</html>
My original solution walked each element up the DOM using a while loop to determine its depth:
var el = document.querySelectorAll('body *'), //all element nodes, in document order
depth,
output= document.getElementById('output'),
obj;
for (var i = 0; i < el.length; i++) {
depth = 0;
obj = el[i];
while (obj.parentNode !== document.body) { //walk the DOM
depth++;
obj = obj.parentNode;
}
output.textContent+= depth + ' ' + el[i].tagName + '\n';
}
<div>
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
</div>
<hr>
<pre id="output"></pre>
I've come up with a new solution, which stores the depths of each element in an object. Since querySelectorAll() returns elements in document order, parent nodes always appear before child nodes. So a child node's depth can be calculated as the depth of its parent node plus one.
This way, we can determine the depths in a single pass without recursion:
var el = document.querySelectorAll('body *'), //all element nodes, in document order
depths= { //stores the depths of each element
[document.body]: -1 //initialize the object
},
output= document.getElementById('output');
for (var i = 0; i < el.length; i++) {
depths[el[i]] = depths[el[i].parentNode] + 1;
output.textContent+= depths[el[i]] + ' ' + el[i].tagName + '\n';
}
<div>
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
</div>
<hr>
<pre id="output"></pre>
Anyone looking for something which iterates through the tree under a node without using recursion* but which also gives you depth (relative to the head node) ... as well as sibling-ancestor coordinates at all times:
function walkDOM( headNode ){
const stack = [ headNode ];
const depthCountDowns = [ 1 ];
while (stack.length > 0) {
const node = stack.pop();
console.log( '\ndepth ' + ( depthCountDowns.length - 1 ) + ', node: ');
console.log( node );
let lastIndex = depthCountDowns.length - 1;
depthCountDowns[ lastIndex ] = depthCountDowns[ lastIndex ] - 1;
if( node.childNodes.length ){
depthCountDowns.push( node.childNodes.length );
stack.push( ... Array.from( node.childNodes ).reverse() );
}
while( depthCountDowns[ depthCountDowns.length - 1 ] === 0 ){
depthCountDowns.splice( -1 );
}
}
}
walkDOM( el );
PS it will be understood that I've put in > 0 and === 0 to try to improve clarity... first can be omitted and second can be replaced with leading ! of course.
* look here for the appalling truth about the cost of recursion in JS (contemporary implementations at 2018-02-01 anyway!)
You can do this by using Jquery
$('#divId').children().each(function () {
// "this" is the current element
});
and the html should be like the following:
<div id="divId">
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
(() => {
const el = document.querySelectorAll('body *');
const depths = new Map();
depths.set(document.body, -1)
el.forEach((e) => {
const p = e.parentNode;
const d = depths.get(p);
depths.set(e, d + 1);
})
return depths;
})()
This is Rick Hitchcocks answer but using Map instead of object
Results in Map(5) {body => -1, div => 0, img => 1, p => 1, span => 2}

JQuery/Javascript - Search DOM for text and insert HTML

How do I search the DOM for a certain string in the document's text (say, "cheese") then insert some HTML immediately after that string (say, "< b >is fantastic< /b >").
I have tried the following:
for (var tag in document.innerHTML) {
if (tag.matches(/cheese/) != undefined) {
document.innerHTML.append(<b>is fantastic</b>
}
}
(The above is more of an illustration of what I have tried, not the actual code. I expect the syntax is horribly wrong so please excuse any errors, they are not the problem).
Cheers,
Pete
There are native methods for finding text inside a document:
MSIE:textRange.findText()
Others: window.find()
Manipulate the given textRange if something was found.
Those methods should provide much more performance than the traversing of the whole document.
Example:
<html>
<head>
<script>
function fx(a,b)
{
if(window.find)
{
while(window.find(a))
{
var node=document.createElement('b');
node.appendChild(document.createTextNode(b));
var rng=window.getSelection().getRangeAt(0);
rng.collapse(false);
rng.insertNode(node);
}
}
else if(document.body.createTextRange)
{
var rng=document.body.createTextRange();
while(rng.findText(a))
{
rng.collapse(false);
rng.pasteHTML('<b>'+b+'</b>');
}
}
}
</script>
</head>
<body onload="fx('cheese','is wonderful')">
<p>I've made a wonderful cheesecake with some <i>cheese</i> from my <u>chees</u>e-factory!</p>
</body>
</html>
This is crude and not the way to do it, but;
document.body.innerHTML = document.body.innerHTML.replace(/cheese/, 'cheese <b>is fantastic</b>');
You can use this with JQuery:
$('*:contains("cheese")').each(function (idx, elem) {
var changed = $(elem).html().replace('cheese', 'cheese <b>is fantastic</b>');
$(elem).html(changed);
});
I haven't tested this, but something along these lines should work.
Note that * will match all elements, even html, so you may want to use body *:contains(...) instead to make sure only elements that are descendants of the document body are looked at.
Sample Solution:
<ul>
<li>cheese</li>
<li>cheese</li>
<li>cheese</li>
</ul>
Jquery codes:
$('ul li').each(function(index) {
if($(this).text()=="cheese")
{
$(this).text('cheese is fantastic');
}
});
The way to do this is to traverse the document and search each text node for the desired text. Any way involving innerHTML is hopelessly flawed.
Here's a function that works in all browsers and recursively traverses the DOM within the specified node and replaces occurrences of a piece of text with nodes copied from the supplied template node replacementNodeTemplate:
function replaceText(node, text, replacementNodeTemplate) {
if (node.nodeType == 3) {
while (node) {
var textIndex = node.data.indexOf(text), currentNode = node;
if (textIndex == -1) {
node = null;
} else {
// Split the text node after the text
var splitIndex = textIndex + text.length;
var replacementNode = replacementNodeTemplate.cloneNode(true);
if (splitIndex < node.length) {
node = node.splitText(textIndex + text.length);
node.parentNode.insertBefore(replacementNode, node);
} else {
node.parentNode.appendChild(replacementNode);
node = null;
}
currentNode.deleteData(textIndex, text.length);
}
}
} else {
var child = node.firstChild, nextChild;
while (child) {
nextChild = child.nextSibling;
replaceText(child, text, replacementNodeTemplate);
child = nextChild;
}
}
}
Here's an example use:
replaceText(document.body, "cheese", document.createTextNode("CHEESE IS GREAT"));
If you prefer, you can create a wrapper function to allow you to specify the replacement content as a string of HTML instead:
function replaceTextWithHtml(node, text, html) {
var div = document.createElement("div");
div.innerHTML = html;
var templateNode = document.createDocumentFragment();
while (div.firstChild) {
templateNode.appendChild(div.firstChild);
}
replaceText(node, text, templateNode);
}
Example:
replaceTextWithHtml(document.body, "cheese", "cheese <b>is fantastic</b>");
I've incorporated this into a jsfiddle example: http://jsfiddle.net/timdown/azZsa/
Works in all browsers except IE I think, need confirmation though.
This supports content in iframes as well.
Note, other examples I have seen, like the one above, are RECURSIVE which is potentially bad in javascript which can end in stack overflows, especially in a browser client which has limited memory for such things. Too much recursion can cause javascript to stop executing.
If you don't believe me, try the examples here yourself...
If anyone would like to contribute, the code is here.
function grepNodes(searchText, frameId) {
var matchedNodes = [];
var regXSearch;
if (typeof searchText === "string") {
regXSearch = new RegExp(searchText, "g");
}
else {
regXSearch = searchText;
}
var currentNode = null, matches = null;
if (frameId && !window.frames[frameId]) {
return null;
}
var theDoc = (frameId) ? window.frames[frameId].contentDocument : document;
var allNodes = (theDoc.all) ? theDoc.all : theDoc.getElementsByTagName('*');
for (var nodeIdx in allNodes) {
currentNode = allNodes[nodeIdx];
if (!currentNode.nodeName || currentNode.nodeName === undefined) {
break;
}
if (!(currentNode.nodeName.toLowerCase().match(/html|script|head|meta|link|object/))) {
matches = currentNode.innerText.match(regXSearch);
var totalMatches = 0;
if (matches) {
var totalChildElements = 0;
for (var i=0;i<currentNode.children.length;i++) {
if (!(currentNode.children[i].nodeName.toLowerCase().match(/html|script|head|meta|link|object/))) {
totalChildElements++;
}
}
matchedNodes.push({node: currentNode, numMatches: matches.length, childElementsWithMatch: 0, nodesYetTraversed: totalChildElements});
}
for (var i = matchedNodes.length - 1; i >= 0; i--) {
previousElement = matchedNodes[i - 1];
if (!previousElement) {
continue;
}
if (previousElement.nodesYetTraversed !== 0 && previousElement.numMatches !== previousElement.childElementsWithMatch) {
previousElement.childElementsWithMatch++;
previousElement.nodesYetTraversed--;
}
else if (previousElement.nodesYetTraversed !== 0) {
previousElement.nodesYetTraversed--;
}
}
}
}
var processedMatches = [];
for (var i =0; i < matchedNodes.length; i++) {
if (matchedNodes[i].numMatches > matchedNodes[i].childElementsWithMatch) {
processedMatches.push(matchedNodes[i].node);
}
}
return processedMatches;
};

How can I find all text nodes between two element nodes with JavaScript/jQuery?

Given the following HTML-Fragment:
<div>
<p>
abc <span id="x">[</span> def <br /> ghi
</p>
<p>
<strong> jkl <span id="y">]</span> mno </strong>
</p>
</div>
I need an algorithm to fetch all nodes of type Text between #x and #y with Javascript. Or is there a JQuery function that does exactly that?
The resulting Text nodes (whitespace nodes ignored) for the example above would then be:
['def', 'ghi', 'jkl']
The following works in all major browsers using DOM methods and no library. It also ignores whitespace text nodes as mentioned in the question.
Obligatory jsfiddle: http://jsfiddle.net/timdown/a2Fm6/
function getTextNodesBetween(rootNode, startNode, endNode) {
var pastStartNode = false, reachedEndNode = false, textNodes = [];
function getTextNodes(node) {
if (node == startNode) {
pastStartNode = true;
} else if (node == endNode) {
reachedEndNode = true;
} else if (node.nodeType == 3) {
if (pastStartNode && !reachedEndNode && !/^\s*$/.test(node.nodeValue)) {
textNodes.push(node);
}
} else {
for (var i = 0, len = node.childNodes.length; !reachedEndNode && i < len; ++i) {
getTextNodes(node.childNodes[i]);
}
}
}
getTextNodes(rootNode);
return textNodes;
}
var x = document.getElementById("x"),
y = document.getElementById("y");
var textNodes = getTextNodesBetween(document.body, x, y);
console.log(textNodes);
The following example uses jQuery to find any two elements that are next to each other and may or may not have text nodes between them. This foreach loop will check the resulted elements to find any text nodes and add them to the list.
function getTextNodes() {
var list = [];
$(document.body).find("*+*").toArray().forEach(function (el) {
var prev = el.previousSibling;
while (prev != null && prev.nodeType == 3) {
list.push(prev);
prev = prev.previousSibling;
}
});
return list;
}

Categories