My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Sunday, August 02, 2009

PyramiDOM - from Alpha to Beta

As promised, I have commented and improved my last experiment: PyramiDOM, (IE version) now with some new feature:

  • One click from bookmark to add PyramiDOM, another one to remove it

  • In a single session, colors are preserved even if PyramiDOM is added and removed more than once

  • Good browsers with W3C DOM events support and/or a console, will experience a better PyramiDOM thanks to automatic update feature and related dom nodes logged in the console

  • last part of the bookmark is the size of each brick. Right now it is 2 but you can easily change it to 4, 6, 8, whatever multiple of 2 you prefer



The PyramiDOM Source Code


It is really simple and the main logic is behind the Brick constructor and every brick instance.

(function(size){

var // shortcut to attach/detach events
add = document.addEventListener ?
function(node, name, callback){
node.addEventListener(name, callback, false);
}:
function(node, name, callback){
node.attachEvent("on" + name, callback);
}
,
del = document.removeEventListener ?
function(node, name, callback){
node.removeEventListener(name, callback, false);
}:
function(node, name, callback){
node.detachEvent("on" + name, callback);
}
,
// scope shortcut for document.body
body = document.body,
// scope shortcut for parseInt
i = parseInt,
// render timeout varible
t = 0,
// events filter (enable/disable)
exec = true,
// internal shortcuts
node, style
;

// main function, just a loop over each node to create a new Brick stack
function PyramiDOM(/* Brick */ parent, /* HTMLElement */ dom){
for(var child, node, childNodes = dom.childNodes, i = 0, length = childNodes.length; i < length; ++i){
if((node = childNodes[i]).nodeType === 1)
parent.add(PyramiDOM(new Brick(node, parent), node));
};
return parent;
};

// via brick info create a link (for alt/title easy tooltip)
// and append into the dom node
PyramiDOM.render = function render(/* Brick */ parent, /* HTMLElement */ dom){
var a = document.createElement("a"),
style = a.style,
alt = parent.dom.nodeName
;
if(parent.dom.id)
alt += " #" + parent.dom.id;
if(parent.dom.className)
alt += " " + parent.dom.className;

// the id contains this indexOf related brick in the Brick.register stack
a.id = parent.id + "-pyramidom";
// using parseInt will guarantee a quick integer transformation

a.title = a.alt = alt;
style.position = "absolute";
style.margin = style.padding = 0;
style.top = parent.top + "px";
style.left = parent.left + "px";
style.height = parent.height + "px";
style.width = parent.width + "px";
style.background = parent.color;
dom.appendChild(a);
// recursion: for each brick children render a link
for(var children = parent.children, i = 0, length = children.length; i < length; ++i)
render(children[i], dom);
};

// check if the node was already there
// in order to be able to remove PyramiDOM with another click
// in the bookmark menu
PyramiDOM.node = document.getElementById("pyramidom");

// if present (re-clicked) ...
if(node = PyramiDOM.node){
// ... remove node
del(document, "DOMNodeInserted", document.PyramiDOM);
del(document, "DOMNodeRemoved", document.PyramiDOM);
body.removeChild(node);
node.innerHTML = "";
PyramiDOM.node = node = null;
return;
} else {
// create the node
node = PyramiDOM.node = body.appendChild(document.createElement("div"));
style = node.style;
node.id = "pyramidom";
style.top = "0px";
style.left = "0px";
style.position = "absolute";
style.zIndex = 2147483647;
// attach events
add(node, "mousedown", function(e){
var target = (e || event).target;
if(target.nodeName === "A"){
// try to show the related node in the console
try{console.log(Brick.register[i(target.id)].dom)}catch(e){};
}
});
add(node, "mouseover", function(e){
var target = (e || event).target;
if(target.nodeName === "A"){
target = Brick.register[i(target.id)];
if(target.dom.style){
target.background = target.dom.style.background || "";
target.dom.style.background = "yellow";
}
}
});
add(node, "mouseout", function(e){
var target = (e || event).target;
if(target.nodeName === "A"){
target = Brick.register[i(target.id)];
if(target.dom.style)
target.dom.style.background = target.background;
}
});
add(document, "DOMNodeInserted", document.PyramiDOM = function(){
// if modification is NOT PyramiDOM itself
if(exec){
// avoid greedy computations
if(0 < t)
clearTimeout(t);
// be sure last timeout will represent the current DOM
t = setTimeout(function(){
// disable insert/remove events
exec = false;
body.removeChild(node);
PyramiDOM.render(PyramiDOM(new Brick(document), document), node);
body.appendChild(node);
// enable insert/remove events
exec = true;
}, 250);
};
});
add(document, "DOMNodeRemoved", document.PyramiDOM);
};

// each brick is an instance with some computated property
function Brick(dom, parent){
this.dom = dom;
// register this instance (reference via link id)
this.id = Brick.register.push(this) - 1;
// set color via nodeName
this.color = Brick.color(dom.nodeName);
// level means how nested is related dom element
this.level = (this.parent = parent) ? parent.level + 1 : 0;
// calculate the left position of this brick
this.left = this.level * this.width;
// set an empty children stack
this.children = [];
// if this is not the root
if(this.level){
// if the parent has other children
if(parent.children.length){
// get latest child position via top, height, plus 1 pixel as space
var child = parent.children[parent.children.length - 1];
this.top = child.top + child.height + 1;
} else
// this brick is the first for this level
// let's put it a bit more down than its parent
// to obtain the Pyramid effect
this.top = parent.top + (this.width / 2) << 0;
} else
// root, top is 0
this.top = 0;
};

// generate random unique colors based on a generic string, nodeName in this case
// the reason it is cached is to avoid epileptic effect each time the pyramid
// is regenerated (new in this beta version)
Brick.color = document.PyramiDOMColor || (document.PyramiDOMColor = (function(){
function color(hex){return new Array(7 - hex.length).join("0") + hex};
function random(nodeName){
// quickly avoid duplicated
do{var c = (Math.random()*0x1000000)<<0}while(cache[c]);
cache[c] = true;
// cache result to avoid this operation for same nodeName
return hash[nodeName] = "#" + color(c.toString(16));
};
var cache = {}, hash = {};
return function(nodeName){return hash[nodeName] || random(nodeName)};
})());

// every brick object is registered in a global public stack
Brick.register = [];

// every brick has a default size
Brick.prototype.width =
Brick.prototype.height = size;

// every brick has a children stack
Brick.prototype.add = function(child){
this.children.push(child);
this.height += child.height + 1;
};

// create PyramiDOM
document.PyramiDOM();

// size of each brick (2, 4, 6 up to whatever % 2 === 0)
})(2);


Have fun with PyramiDOM :)

No comments: