Generic tree implementation in Javascript - javascript

Is anyone aware of a generic tree (nodes may have multiple children) implementation for JavaScript?
It should be able to do atleast these things,
get parent node.
get children nodes.
get all the descendants.
remove all the descendants.
remove children nodes.
Some implementation similar to Adjacency List Model.
Background: I needed JavaScript based hierarchical data storing for my webpage i could not find a good JavaScript implementation of generic trees so what i did is i used ajax to store hierarchical data into database using Adjacency List Model and php. The problem comes when the user is opening the same page in two tabs of same browser or opened the page in two different browsers because both the instances are writing to same table reading from same table which is causing me problems any possible work around for this also answers my question.
Edit: Performance is really not my constraint at any point of time i will not have more than 50 entries.

You can try this: https://github.com/afiore/arboreal
Or this: https://github.com/mauriciosantos/buckets/ (only Binary Searched Trees, but olso other data structures)
If you need anything more sophisticated, you will need to write your own library (or at least one object with all methods you desribed).
EDIT:
This is my simple code to achieve tree functionality. Remove all descendants and remove all children is in fact the same... so:
function Node(value) {
this.value = value;
this.children = [];
this.parent = null;
this.setParentNode = function(node) {
this.parent = node;
}
this.getParentNode = function() {
return this.parent;
}
this.addChild = function(node) {
node.setParentNode(this);
this.children[this.children.length] = node;
}
this.getChildren = function() {
return this.children;
}
this.removeChildren = function() {
this.children = [];
}
}
var root = new Node('root');
root.addChild(new Node('child 0'));
root.addChild(new Node('child 1'));
var children = root.getChildren();
for(var i = 0; i < children.length; i++) {
for(var j = 0; j < 5; j++) {
children[i].addChild(new Node('second level child ' + j));
}
}
console.log(root);
children[0].removeChildren();
console.log(root);
console.log(root.getParentNode());
console.log(children[1].getParentNode());
Run it in Chrome (or other browser which supports console).

Although you did say "generic tree", what your specific requirement sounds simple enough for an already built-in DOMParser.
I respect other programmers' opinions, but still I think you can give DOM a try and see if it fits you.
Here's an simple illustration on how it works:
var tXML="<root><fruit><name>apple</name><color>red</color></fruit><fruit><name>pear</name><color>yellow</color></fruit></root>";
var tree=(new DOMParser).parseFromString(tXML,"text/xml");
//get descendants
var childs=tree.documentElement.childNodes;
for(var i=0;i<childs.length;i++)
{
if(childs[i].nodeName=="fruit")
{
document.write(childs[i].getElementsByTagName("name")[0].textContent);
document.write(": ");
document.write(childs[i].getElementsByTagName("color")[0].textContent);
document.write("<br />");
}
}
//get child node
var appl=tree.documentElement.getElementsByTagName("fruit")[0];
document.write(appl.getElementsByTagName("name")[0].textContent+"<br />");
//get parent node
document.write(appl.parentNode.nodeName);
document.write("<br />");
//remove child node
if(tree.documentElement.getElementsByTagName("color").length>1)
{
var clr=tree.documentElement.getElementsByTagName("color")[1];
clr.parentNode.removeChild(clr);
}
document.write("<textarea>"+(new XMLSerializer).serializeToString(tree)+"</textarea><br />");
//remove descendants
while(tree.documentElement.childNodes.length>0)
{
tree.documentElement.removeChild(tree.documentElement.childNodes[0]);
}
document.write("<textarea>"+(new XMLSerializer).serializeToString(tree)+"</textarea>");
I didn't "simplified" those long function names, so you may get a better idea.

Related

Javascript - Collection of objects with parent properties in common

I'd like to create a collection of objects that works like an array. Some time ago, I made this question and I came up with the following solution with the help of the people that helped me:
Newobj.Collection = function(){
Array.apply(this);
for(var i = 0; i < arguments.length; i++){
for(var j = 0; j < arguments[i].length; j++){
this.push(arguments[i][j]);
}
}
return this
}
Newobj.Collection.prototype = Object.create(Array.prototype);
Newobj.Collection.prototype.push = function(o){
Array.prototype.push.call(this, new Newobj.Element(o));
}
However, this leaves the children unconnected from the parent. For example, imagine this collection has a render() function, which makes its children to print some HTML onto the page. Well, I'd like to be able to say something like:
Newobj.Collection.html_container = '#cont';
Newobj.Collection.render = function(){
$.each(this, function(i, el){
el.render()
})
}
Newobj.Element.render = function(){
$(parent.html_container).html('.......')
}
It should be able to set different collections in one page, so make a global container for all the Newobj.Collections is not a solution. This is an example, and I need this for more complex processes than just a render() function.
Anyone has an idea how can I make an array to be able to access a parent class which it is part of?
If the solution could be JSON.stringifyed and be seen as an array on the server side, it would be great too, though it's not the main problem for this question. Right now, if I set a property to the array, it is seen as an object with size > 0 on the server side.
Thank you!
Create reference to collection in element:
Newobj.Collection.prototype.push = function(o){
Array.prototype.push.call(this, new Newobj.Element(o,this));
}
//element constructor gets as second paramater instance of collection
Newobj.Element=function(o,collection){
//this.parent in every element is collection reference
this.parent=collection;
}
Newobj.Element.prototype.render = function(){
$(this.parent.html_container).html('.......')
}
or no reference in Element option:
Newobj.Collection.render = function(){
var parent=this;
$.each(this, function(i, el){
el.render(parent.html_container)
})
}
Newobj.Element.render = function(html_container){
$(html_container).html('.......')
}
But this version need to have methods parameters.

Fill an array recursively walking a DOM tree

I have to fill an array of strings as I walk a generic tree recursively from one node to all his children. In practice at each node that match from a node to a leaf, insert a string in the DOM tree.
I know it is a trivial problem but I could not solve.
This is the code that I wrote:
function operationsToInsert(node) {
var operations = [];
operationsToInsertRec(node, operations);
return operations;
}
function operationsToInsertRec(node, operations) {
var childNodes = node.childNodes;
operations.push("i(" + node.nodeName + ") ");
for(var i = 0; i < childNodes.length; i++) {
operationsToInsertRec(childNodes[i], operations);
operations.push("i(" + childNodes[i].nodeName + ")");
}
}
But there is the following error:
Uncaught TypeError: Cannot read property 'push' of undefined at line operations.push("insert(" + node.nodeName + ") ");
How can I fix?
Thanks
Here's a way to walk a tree using the handy Array.prototype.reduce function using a trick that lets it work on array-likes:
function flatten(ops, n) {
ops.push("i(" + n.nodeName + ") ");
if (n.childNodes && n.childNodes.length) {
[].reduce.call(n.childNodes, flatten, ops);
}
return ops;
}
var node = document.getElementById("start_here");
var ops = [node].reduce(flatten, []);
fiddle
The problem is you have only one function, and it isn't what you think it is! You redefined it, so when you call what you thought was the first one you provided only one argument, and the remaining arguments are implicitly undefined.
Here is your same code, reduced to a demonstratable example:
function operationsToInsert(node) {
console.log("Definition #1");
}
function operationsToInsert(node, operations) {
console.log("Definintion #2");
}
operationsToInsert();
You need to change the name of one of your functions so that you don't have a collision.
Edit to address new question:
I think you are saying that your new issue is most nodes appear in the list twice. Trace through the code and you'll see that you process every node except the root node twice. In operationsToInsertRec() you put the node in the list (childNodes[i]), then you pass it to operationsToInsertRec() where it puts it in the list (node).
Here is a simple change to address that:
function operationsToInsert(node) {
var operations = [];
operations.push("i(" + node.nodeName + ") ");
operationsToInsertRec(node, operations);
return operations;
}
function operationsToInsertRec(node, operations) {
var childNodes = node.childNodes;
for(var i = 0; i < childNodes.length; i++) {
operationsToInsertRec(childNodes[i], operations);
operations.push("i(" + childNodes[i].nodeName + ")");
}
}
In operationsToInsert() I push the root node. Then operationsToInsertRec() only handles the children, and so handles each node only once.
In a comment on a different answer I see you touch on the topic of traversal order. When traversing a tree, there are several different classifications of these algorithms: depth-first, which is subdivided into pre-order, in-order, and post-order, and breadth-first. You can find more information in the wikipedia article on tree traversal.

How can I make this function more efficent, contains 3 loops?

I have created this function which loops through a bunch of elements in a gantt chart that represent tasks. Each task has class of "link" and the attributes "id" and "pre".
"pre" represents the tasks predecessor.
The function is used to work out what each tasks predecessor is and then call another function which draws an arrow between them.
The only way I could figure how to do this was to first create an array of the tasks then loop through that array and get each tasks predecessor, loop through the array of tasks again and find the task thats id matches the predecessor and then call the draw function as below.
But this resulted in three loops with one loop inside another as below and I cant help thinking there would be a more efficient way of doing the same thing? My application is slow as it is, this will just make it worse.
Can anybody suggest a way of rewriting this function to be more efficient?
P.S the application is to big to do a jsfiddle for this and the code is fairly self explanatory.
//Adds relationship link arrows between tasks
function add_arrows()
{
var ttask = new Array();
var pre = 0;
//loop through all task elements with the class link and add them to an array
$(".link").each(function(i)
{
ttask[i] = $(this);
});
//loop through the array if tasks
for (var i=0, l=ttask.length; i < l; i++ )
{
//if its not the first task get its predecessor value
if(i != 0)
{
pre = ttask[i].attr('pre');
}
//loop through the array of tasks again and get the task with an id that matches the predecessor value
for (var j=0, k=ttask.length; j < k; j++ )
{
if(ttask[j].attr('id') == pre)
{
var predecessor = ttask[j];
}
}
//if its not the first task, draw a link between the predecessor and current task
if(i != 0)
{
drawlink(predecessor, ttask[i], ttask[i].attr('link')); //function takes: predecessor, current task, link type
}
}
}
After a suggestion below this looks the most efficient so far? Unless somebody can say why another method is computationally better?
function add_arrows(){
$(".link").each(function(i) {
var $el = $(this);
if(i) drawlink($('#' + $el.attr('pre')), $el, $el.attr('link'));
});
}
Surely its just finding all the elements that have a predecessor (judging by your code, that have a pre attribute) and adding a link to the element that is indicated by their link attribute.
something like:
$('[pre]').each(function(i,el){
var self = $(el),
link = self.attr('link'),
predecessor = $('#' + link);
if (predecessor.length)
{
//i.e. A predecessor has been found, assuming that #0 is not an element
drawlink(predecessor, self , link );
}
});
Try this :
function add_arrows(){
$(".link").each(function(i) {
var predecessor = (typeof ($(this).attr('pre')) !== "undefined") ? $("#"+($(this).attr('pre'))) : null;
drawlink(predecessor, $(this), $(this).attr('link'));
});
}
It seems that elements with a class of "link" can have a "pre" attribute that specifies the element that they are a predecessor for. Presumably each element only has one predecessor. Each link element also has a link attribute.
The drawlink function takes three arguments: the predecessor, the element and its link attribute value.
Get a list of the elements with class link, then iterate over it and call drawlink for each element that has a not empty pre attribute.
function joinPredecessors() {
$('.link').each(function() {
var el = $(this);
var pre = el.attr('pre');
// Only draw a line if the element has a predecessor
if (pre != '' && pre != null) {
drawlink($('#' + pre), el, el.attr('link'));
}
});
}
Given you asked for "efficient", the above is concise but likely slow, a plain javscript method is:
function joinPredecessors() {
// Work with plain JS and DOM
var links = document.querySelectorAll('.link');
var el, pre;
for (var i=0, iLen=links.length; i<iLen; i++) {
el = links[i];
pre = el.getAttribute('pre');
if (pre != '' && pre != null) {
// Pass jQuery objects
drawlink($('#' + pre), $(el), el.getAttribute('link'));
}
}
}
which is hardly any more code and about twice as fast, at least in IE and mobile Safari.

How can I loop through ALL DOM elements on a page?

I'm trying to loop over ALL elements on a page, so I want to check every element that exists on this page for a special class.
So, how do I say that I want to check EVERY element?
You can pass a * to getElementsByTagName() so that it will return all elements in a page:
var all = document.getElementsByTagName("*");
for (var i=0, max=all.length; i < max; i++) {
// Do something with the element here
}
Note that you could use querySelectorAll(), if it's available (IE9+, CSS in IE8), to just find elements with a particular class.
if (document.querySelectorAll)
var clsElements = document.querySelectorAll(".mySpeshalClass");
else
// loop through all elements instead
This would certainly speed up matters for modern browsers.
Browsers now support foreach on NodeList. This means you can directly loop the elements instead of writing your own for loop.
document.querySelectorAll('*').forEach(function(node) {
// Do whatever you want with the node object.
});
Performance note - Do your best to scope what you're looking for by using a specific selector. A universal selector can return lots of nodes depending on the complexity of the page. Also, consider using document.body.querySelectorAll instead of document.querySelectorAll when you don’t care about <head> children.
Was looking for same. Well, not exactly. I only wanted to list all DOM Nodes.
var currentNode,
ni = document.createNodeIterator(document.documentElement, NodeFilter.SHOW_ELEMENT);
while(currentNode = ni.nextNode()) {
console.log(currentNode.nodeName);
}
To get elements with a specific class, we can use filter function.
var currentNode,
ni = document.createNodeIterator(
document.documentElement,
NodeFilter.SHOW_ELEMENT,
function(node){
return node.classList.contains('toggleable') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
);
while(currentNode = ni.nextNode()) {
console.log(currentNode.nodeName);
}
Found solution on
MDN
As always the best solution is to use recursion:
loop(document);
function loop(node){
// do some thing with the node here
var nodes = node.childNodes;
for (var i = 0; i <nodes.length; i++){
if(!nodes[i]){
continue;
}
if(nodes[i].childNodes.length > 0){
loop(nodes[i]);
}
}
}
Unlike other suggestions, this solution does not require you to create an array for all the nodes, so its more light on the memory. More importantly, it finds more results. I am not sure what those results are, but when testing on chrome it finds about 50% more nodes compared to document.getElementsByTagName("*");
Here is another example on how you can loop through a document or an element:
function getNodeList(elem){
var l=new Array(elem),c=1,ret=new Array();
//This first loop will loop until the count var is stable//
for(var r=0;r<c;r++){
//This loop will loop thru the child element list//
for(var z=0;z<l[r].childNodes.length;z++){
//Push the element to the return array.
ret.push(l[r].childNodes[z]);
if(l[r].childNodes[z].childNodes[0]){
l.push(l[r].childNodes[z]);c++;
}//IF
}//FOR
}//FOR
return ret;
}
For those who are using Jquery
$("*").each(function(i,e){console.log(i+' '+e)});
Andy E. gave a good answer.
I would add, if you feel to select all the childs in some special selector (this need happened to me recently), you can apply the method "getElementsByTagName()" on any DOM object you want.
For an example, I needed to just parse "visual" part of the web page, so I just made this
var visualDomElts = document.body.getElementsByTagName('*');
This will never take in consideration the head part.
from this link
javascript reference
<html>
<head>
<title>A Simple Page</title>
<script language="JavaScript">
<!--
function findhead1()
{
var tag, tags;
// or you can use var allElem=document.all; and loop on it
tags = "The tags in the page are:"
for(i = 0; i < document.all.length; i++)
{
tag = document.all(i).tagName;
tags = tags + "\r" + tag;
}
document.write(tags);
}
// -->
</script>
</head>
<body onload="findhead1()">
<h1>Heading One</h1>
</body>
</html>
UPDATE:EDIT
since my last answer i found better simpler solution
function search(tableEvent)
{
clearResults()
document.getElementById('loading').style.display = 'block';
var params = 'formAction=SearchStocks';
var elemArray = document.mainForm.elements;
for (var i = 0; i < elemArray.length;i++)
{
var element = elemArray[i];
var elementName= element.name;
if(elementName=='formAction')
continue;
params += '&' + elementName+'='+ encodeURIComponent(element.value);
}
params += '&tableEvent=' + tableEvent;
createXmlHttpObject();
sendRequestPost(http_request,'Controller',false,params);
prepareUpdateTableContents();//function js to handle the response out of scope for this question
}
Getting all elements using var all = document.getElementsByTagName("*"); for (var i=0, max=all.length; i < max; i++); is ok if you need to check every element but will result in checking or looping repeating elements or text.
Below is a recursion implementation that checks or loop each element of all DOM elements only once and append:
(Credits to #George Reith for his recursion answer here: Map HTML to JSON)
function mapDOMCheck(html_string, json) {
treeObject = {}
dom = new jsdom.JSDOM(html_string) // use jsdom because DOMParser does not provide client-side Window for element access
document = dom.window.document
element = document.querySelector('html')
// Recurse and loop through DOM elements only once
function treeHTML(element, object) {
var nodeList = element.childNodes;
if (nodeList != null) {
if (nodeList.length) {
object[element.nodeName] = []; // IMPT: empty [] array for parent node to push non-text recursivable elements (see below)
for (var i = 0; i < nodeList.length; i++) {
console.log("nodeName", nodeList[i].nodeName);
if (nodeList[i].nodeType == 3) { // if child node is **final base-case** text node
console.log("nodeValue", nodeList[i].nodeValue);
} else { // else
object[element.nodeName].push({}); // push {} into empty [] array where {} for recursivable elements
treeHTML(nodeList[i], object[element.nodeName][object[element.nodeName].length - 1]);
}
}
}
}
}
treeHTML(element, treeObject);
}
Use *
var allElem = document.getElementsByTagName("*");
for (var i = 0; i < allElem.length; i++) {
// Do something with all element here
}
i think this is really quick
document.querySelectorAll('body,body *').forEach(function(e) {
You can try with
document.getElementsByClassName('special_class');

JS: How do we work with classes in CSS?

How do we manipulate the class of DOM elements with javascript? Is there a getElementsByClassName function?
Standard way is
error_message.className = 'error-message';
But you'll find these functions can simplify things a lot:
function hasClass(ele,cls) {
return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
}
//chekcs if selected element has class "cls", works for elements with multiple classes
function addClass(ele,cls) {
if (!this.hasClass(ele,cls)) ele.className += " "+cls;
}
//adds new class to element
function removeClass(ele,cls) {
if (hasClass(ele,cls)) {
var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
ele.className=ele.className.replace(reg,' ');
}
}
//removes class from element
Usage in a stackoverflow greasemonkey script to show all questions on page, regardless if they're ignored or not:
var childNodes=document.getElementById("questions").childNodes; //array of all questions
for (var i=1; i<childNodes.length; i+=2) //iterates through all questions on page.
{
removeClass(childNodes[i],"tagged-ignored-hidden");
addClass(childNodes[i],"user_defined_class");
}
(Don't worry if the for loop looks weird in that it skips every other element; the specifics of Stackoverflow's DOM layout with extra nodes between questions aren't important here.)
As to document.getElementsByClassName, it returns an array of DOM elements with the specific class (as you would suspect). BUT:
Safari 3.1 has native
getElmentsByClassName support, and
upcoming Firefox 3 and Opera 9.5 will
have it too. It only leaves out,
you’ve guessed it, Internet Explorer.
source
You can change a class in plain-old JavaScript using something like:
document.getElementById('myElement').className = 'myClass';
Or, if you're using JQuery, you can just use the "Class" functions.
Addressing the added details to the question about 'getElementsByClassName' and your comment:
It would probably be safest (and easiest) to use your favourite JavaScript library for this.
JQuery example:
$(".myClassName").each(function() {
//do what you want with the current element $(this)
});
Hope that helps.
Many JavaScript implementations do have a getElementsByClassName method built in. But if they don’t, you can implement it for yourself:
if (typeof Element.prototype.getElementsByClassName == "undefined") {
Element.prototype.getElementsByClassName = function(className) {
var elems = document.getElementsByTagName("*"),
matches = [];
for (var i=0, n=elems.length; i<n; ++i) {
if (elems[i].hasAttribute("class")) {
var classNames = elems[i].getAttribute("class").split(/\s+/);
for (var j=0,m=classNames.length; j<m; ++j) {
if (classNames[j] == className) {
matches.push(elems[i]);
break;
}
}
}
}
return new NodeList(matches);
};
}

Categories