/**
* SVG Tree Drawer
* by Weston Ruter , 2009
* License: GPL 3.0
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* @todo Diagrammer: SVG + XML + XSLT + MathML + ContentEditable + hashchange + JSON.parse/stringify
* @todo Get the text vertical spacing working
* @todo Add a style property which gets assigned to the
* @todo Add the ability to draw lines between any two nodes, curved or straight: each node would need an id
* @todo When collapsing a node that has leaf nodes at different depts, treat it like it has been extended.
*/
(function(){
if(typeof TreeDrawer != 'undefined')
return;
var svgns = 'http://www.w3.org/2000/svg';
var xlinkns = 'http://www.w3.org/1999/xlink';
/**
* Class that is associated with a given SVG element and contains methods and
* properties that relate to the tree as a whole.
* @todo Should draw() automatically be called?
*/
var T = window.TreeDrawer = function(fallbackElement, treeData){
if(typeof fallbackElement == 'string')
fallbackElement = document.getElementById(fallbackElement);
if(!fallbackElement || !fallbackElement.nodeType == 1)
throw Error("The param 'fallbackElement' is not valid.");
this.root = treeData;
var isNativeSVG = !!document.createElementNS(svgns, 'text').getComputedTextLength;
// Create the SVG document
if(isNativeSVG){
var svg = document.createElementNS(svgns, 'svg');
svg.id = fallbackElement.id;
svg.setAttribute('width', 0);
svg.setAttribute('height', 0);
fallbackElement.parentNode.replaceChild(svg, fallbackElement);
this.svgElement = svg;
// Add the stylesheet
var defs = document.createElementNS(svgns, 'defs');
var style = document.createElementNS(svgns, 'style');
style.setAttribute('type', 'text/css');
//style.appendChild(document.createTextNode(this.cssStylesheet));
defs.appendChild(style);
this.svgElement.appendChild(defs);
style.appendChild(document.createTextNode(this.cssStylesheet))
//for(var i = 0, len = this.cssStyleRules.length; i < len; i++){
//style.sheet.insertRule(this.cssStyleRules[i], i);
//console.info(this.cssStyleRules[i])
//}
//if(treeData)
// this.populate(treeData);
}
// Utilize svgweb
else {
//if(typeof svgweb == 'undefined')
throw Error("The SVGWeb implementation is not currently ready.");
var obj = document.createElement('object', true);
obj.setAttribute('type', 'image/svg+xml');
//obj.setAttribute('data', 'data:image/svg+xml,');
obj.setAttribute('data', 'blank.svg');
obj.setAttribute('width', 0);
obj.setAttribute('height', 0);
this.svgObject = obj;
var that = this;
obj.addEventListener('load', function(e){ //TODO: We need to get rid of this async event
try{
that.svgDocument = this.contentDocument;
that.svgElement = that.svgDocument.documentElement;
// Add the stylesheet
var defs = document.createElementNS(svgns, 'defs');
var style = document.createElementNS(svgns, 'style');
style.setAttribute('type', 'text/css');
style.appendChild(document.createTextNode(that.cssStylesheet));
defs.appendChild(style);
//that.svgElement.appendChild(defs); //TODO
if(treeData)
that.populate(treeData);
}
catch(e){
console.error(e)
}
}, false);
svgweb.appendChild(obj, svgContainerElement);
}
};
//T.prototype.svgDocument = null; //readonly (only set when using svgweb)
//T.prototype.svgObject = null; //readonly (only set when using svgweb)
T.prototype.svgElement = null; //readonly
T.prototype.collapsed = false; //readonly
T.prototype.width = 0; //readonly
T.prototype.height = 0; //readonly
T.prototype.isDrawn = false;
//T.prototype.cssStylesheet = 'line, path { dominant-baseline:middle; }';
T.prototype.cssStylesheet = [
"line, path, polyline { stroke-width:1px; stroke:black; }",
//"text { dominant-baseline:text-after-edge !important; }", //TODO: Does not work in WebKit
//"svg {font-size:20px; }",
//"svg > g > g > text { font-size:120px; }"
].join("\n");
/**
* The root TreeDrawer.Node
*/
T.prototype.root = null;
/**
* Blow away all of the nodes
*/
T.prototype.empty = function empty(){
var svg = this.svgElement;
//while(svg.firstChild){
// svg.parentNode.removeChild(svg.firstChild)
//}
for(var i = 0; i < svg.childNodes.length; i++){
if(svg.childNodes[i].nodeName.toLowerCase() == 'g'){
svg.removeChild(svg.childNodes[i]);
i--;
}
}
//Now that all of the nodes have been removed from the document, make sure
// that references to those nodes are removed as well
function freeMemory(treeNode){
treeNode.branchElement = null;
treeNode.labelElement = null;
forEach(treeNode.children, freeMemory); //recursive
};
freeMemory(this.root);
this.isDrawn = false;
//Give the SVG image zero dimensions
this.width = 0;
this.height = 0;
if(this.svgObject){
this.svgObject.width = this.width;
this.svgObject.height = this.height;
}
else {
this.svgElement.setAttribute('width', this.width);
this.svgElement.setAttribute('height', this.height);
}
};
/**
* Renders the tree onto the SVG canvas, resizing the canvas as necessary
* This function does the heavy lifting of the code
* @param optional treeData The data structure to be drawn; if not specified, uses this.root
* @todo Should we save the data before redrawing? In case of error, we could then restore the original?
* @see _drawNode()
*/
T.prototype.draw = function draw(treeData){
var isRefresh = (!treeData && this.isDrawn);
//Get the tree data set up
if(treeData instanceof Object)
this.root = treeData;
if(!this.root)
throw Error("No tree data has been supplied.");
if(!(this.root instanceof TN))
this.root = new TN(this.root);
//If we're not doing a refresh, the blow away the existing nodes
if(!isRefresh && this.isDrawn)
this.empty();
//var fontSize = parseFloat(window.getComputedStyle(this.svgElement, null).fontSize);
this.height = this.width = 0; //reset
var info = _drawNode(this, isRefresh, this.svgElement, 0, this.root, 0, 0, this.labelPadding, this.branchHeight);
//Fire 'done' event
this.isDrawn = true;
if(this.svgObject){
this.svgObject.width = this.width;
this.svgObject.height = this.height;
}
else {
this.svgElement.setAttribute('width', this.width);
this.svgElement.setAttribute('height', this.height);
}
};
//Die for loops, die!
var forEach = Array.forEach || function forEach(object, block, context) {
for (var i = 0; i < object.length; i++) {
block.call(context, object[i], i, object);
}
};
/**
* Depending on the type of element, get its width and height
*/
function getDimensions(el){
try {
if(el.getBBox){
return el.getBBox();
}
else if(el.getBoundingClientRect){
var _rect = el.getBoundingClientRect();
var rect = {
width: _rect.width || el.offsetWidth || el.clientWidth,
height: _rect.height || el.offsetHeight || el.clientHeight,
x: _rect.x,
y: _rect.y
};
//if(!rect.width)
// rect.width = el.offsetWidth;
//if(!rect.height)
// rect.height = el.offsetHeight;
if(isNaN(rect.width) || isNaN(rect.height))
throw Error("getBoundingClientRect() didn't return the width or height! Are you using an old version of Firefox?");
return rect;
}
//else if(el.width && el.width.baseVal && el.height && el.height.baseVal){
// return {
// width:el.width.baseVal.value,
// height:el.height.baseVal.value,
// x:el.x.baseVal.value,
// y:el.y.baseVal.value
// }
//}
//else if(el.offsetParent){
// return {
// width:el.offsetWidth,
// height:el.offsetHeight
// }
//}
}
catch(e){
return {
width:0,
height:0,
x:null,
y:null
};
}
throw Error("Unable to determine the element's dimensions");
}
TreeDrawer._getElementDimensions = getDimensions;
/**
* This is necessary because el.getBBox().x is not always the same as el.x.baseVal.value
*/
function getX(el){
var baseValX = el.x.baseVal;
if(baseValX.getItem)
return baseValX.getItem(0).value;
else
return baseValX.value;
}
/**
* This is necessary because el.getBBox().y is not always the same as el.y.baseVal.value
*/
function getY(el){
var baseValY = el.y.baseVal;
if(baseValY.getItem)
return baseValY.getItem(0).value;
else
return baseValY.value;
}
/**
* For when a node is collapsed, we need to gather up all of the leaf nodes
* @todo What happens when isRefresh?
*/
//function gatherLeafNodes(treeNode){
// var children = [];
// if(treeNode.children && treeNode.children.length){
// forEach(treeNode.children, function(tn){
// forEach(gatherLeafNodes(tn), function(leaf){
// children.push(leaf);
// });
// });
// }
// else {
// children.push(treeNode);
// }
// return children;
//}
/**
* Recursive function called by TreeDrawer.draw()
* @param offsetTop The distance from the top to the bottom of the lower end of the branches
* @todo In Firefox <3 getBoundingClientRect doesn't include width and height
* @todo should be instead
*/
function _drawNode(tree, isRefresh, parentElement, siblingPosition, treeNode, offsetLeft, offsetTop, isAncestorExtended, collapsedAncestor){
var g, label, labelRect, labelFontSize, branch, branchHeight, labelPadding;
//if(collapsedAncestor && treeNode.children && treeNode.children.length)
// throw Error("Unexpected that tree node has children; all of the leaf nodes have been gathered.");
var isNodeDisplayed = !collapsedAncestor || collapsedAncestor && (!treeNode.children || !treeNode.children.length);
treeNode.leafDecendantsInfo = []; //Reset
//Get or create the node container
if(isRefresh){
g = treeNode.labelElement.parentNode;
}
else {
g = document.createElementNS(svgns, 'g');
if(!treeNode.children || !treeNode.children.length)
g.setAttribute('class', 'leaf');
else
g.setAttribute('class', 'noleaf');
parentElement.appendChild(g);
}
var gClass = g.hasAttribute('class') ? g.getAttribute('class').replace(/\s*collapsed/) : '';
g.setAttribute('class', treeNode.collapsed ? gClass + " collapsed" : gClass);
//Get or create the branch which will connect this label with the parent label
if(isRefresh){
branch = treeNode.branchElement;
}
else if(parentElement.localName != 'svg'){ //not the root
branch = document.createElementNS(svgns, 'polyline');
branch.setAttribute('class', branch.hasAttribute('class') ? branch.getAttribute('class') + " branch" : "branch");
treeNode.branchElement = branch;
g.appendChild(branch);
}
if(branch)
branch.style.display = isNodeDisplayed ? '' : 'none';
if(isNodeDisplayed && branch){
var branchStyle = window.getComputedStyle(branch, null);
branchHeight = parseFloat(branchStyle.fontSize) || 0;
offsetTop += branchHeight;
}
//Get or make the label
if(isRefresh){
label = treeNode.labelElement;
label.style.display = isNodeDisplayed ? '' : 'none';
//The dimensions of the foreignObject may have been changed, so re-get
if(label.nodeName.toLowerCase() == 'foreignobject'){
labelRect = getDimensions(label.firstChild);
label.setAttribute('width', labelRect.width);
label.setAttribute('height', labelRect.height);
}
}
else {
var sourceLabel = _applyFilters.apply(tree, ['label', treeNode.label, treeNode]);
if(sourceLabel.nodeType == 1/*Element*/) {
if(sourceLabel.namespaceURI == svgns){
//@todo Should this be wrapped in a element so we can translate its position?
label = sourceLabel;
g.appendChild(label);
}
else {
label = document.createElementNS(svgns, 'foreignObject');
// Set width/height to non-zero value so that display isn't disabled;
// after the label is inserted into the SVG tree, then the offsetHeight
// and offsetWidth will be used to provide the proper dimensions.
// This is to facilitate writing CSS style rule selectors.
label.setAttribute('width', 1);
label.setAttribute('height', 1);
label.appendChild(sourceLabel);
g.appendChild(label);
// Now that element has been inserted into the DOM, calculate the
// size of the foreignObject's contents, and use these as the width
// and height.
if(isNodeDisplayed){
labelRect = getDimensions(sourceLabel);
label.setAttribute('width', labelRect.width);
label.setAttribute('height', labelRect.height);
}
}
}
else {
label = document.createElementNS(svgns, 'text');
label.appendChild(document.createTextNode(sourceLabel.toString(), true));
g.appendChild(label);
}
label.setAttribute('class', label.hasAttribute('class') ? label.getAttribute('class') + " tree-label" : "tree-label");
treeNode.labelElement = label;
g.svgTreeDrawerNode = treeNode;
//TODO: Allow this node to be filtered before insertion (i.e. replace with foreignobject)
}
label.style.display = isNodeDisplayed ? '' : 'none';
if(!labelRect)
labelRect = getDimensions(label);
//Get styles and dimensions
$label = $(label);
labelFontSize = parseFloat($label.css("font-size")) || 0;
labelPadding = {
top:parseFloat($label.css("padding-top")) || 0,
right:parseFloat($label.css("padding-right")) || 2,
bottom:parseFloat($label.css("padding-bottom")) || 0,
left:parseFloat($label.css("padding-left")) || 2
};
//Process each of the children
var subtreeElements = [label];
var leafNodes = []; //{label:, branch:}
if(branch)
subtreeElements.push(branch);
var childrenWidth = 0;
var childrenInfo = [];
treeNode.maxOffsetTop = offsetTop;
//Get the nodes that appear as children under this node
var childTreeNodes = treeNode.children;
for(var i = 0, len = childTreeNodes.length; i < len; i++){ //forEach
var childInfo = _drawNode(
tree,
isRefresh,
g,
i, //siblingPosition
childTreeNodes[i],
offsetLeft + childrenWidth,
/*collapsedAncestor ? offsetTop :*/ offsetTop + labelPadding.top + labelRect.height + labelPadding.bottom, //+ branchHeight //value of child branch's height is added
treeNode.extended || isAncestorExtended,
collapsedAncestor || (treeNode.collapsed ? treeNode : null) //must be collapsed parent node!
);
childrenWidth += childInfo.width;
forEach(childInfo.subtreeElements, function(el){
subtreeElements.push(el);
});
forEach(childInfo.leafNodes, function(el){
leafNodes.push(el);
});
treeNode.maxOffsetTop = Math.max(treeNode.maxOffsetTop, childInfo.maxOffsetTop); //TODO
childrenInfo.push(childInfo);
}
//Get coordinates for label and position
var labelY = offsetTop + labelPadding.top;
var labelX;
//If there are children, then x is in the middle of their first and last children
//TODO: if labelRect.width > childrenWidth, we could pass in the labelRect.width
var decendantsInfo = treeNode.collapsed && treeNode.leafDecendantsInfo.length ? treeNode.leafDecendantsInfo : childrenInfo;
if(decendantsInfo.length){
var leftX, rightX;
var firstInfo = decendantsInfo[0];
var lastInfo = decendantsInfo[decendantsInfo.length-1];
if(firstInfo.label == lastInfo.label){
leftX = rightX = getX(firstInfo.label) + firstInfo.labelRect.width/2;
}
else {
leftX = getX(firstInfo.label) /*+ firstInfo.labelRect.width*/;
rightX = getX(lastInfo.label) + lastInfo.labelRect.width;
}
labelX = Math.max(
0,
leftX + (rightX - leftX)/2 - labelRect.width/2,
offsetLeft + labelPadding.left
);
// If the children were narrower than the the parent label, then distribute
// the children out under the parent. Requires that all subtree graphic
// elements to be shifted over to the right
var labelWidthBeyondChildrenWidth = labelRect.width + labelPadding.left + labelPadding.right - childrenWidth;
if(labelWidthBeyondChildrenWidth > 0){
var shiftLeft = labelWidthBeyondChildrenWidth/(decendantsInfo.length+1);
forEach(decendantsInfo, function(child, i){
forEach(child.subtreeElements, function(el){
if(el.namespaceURI == svgns){
var elDimensions = getDimensions(el);
var thisShiftLeft = shiftLeft*(i+1)/* - elDimensions.width/2*/;
switch(el.localName.toLowerCase()){
case 'circle':
el.cx.baseVal.value += thisShiftLeft;
break;
case 'line':
el.x1.baseVal.value += thisShiftLeft;
el.x2.baseVal.value += thisShiftLeft;
break;
case 'path':
case 'polygon':
case 'polyline':
for(var j = 0, points = el.points, len = points.numberOfItems; j < len; j++){
points.getItem(j).x += thisShiftLeft;
}
break;
case 'rect':
default:
var elRect = getDimensions(el);
el.setAttribute('x', elRect.x + thisShiftLeft);
break;
}
}
else {
throw Error("Expected an SVG element to shift.");
}
});
});
}
}
//Leaf node: no children, so left edge is simply offsetLeft
else {
labelX = offsetLeft + labelPadding.left;
leafNodes.push({
label:label,
branch:branch,
offsetTop:offsetTop
});
}
//Add to the x/y position
if(label.namespaceURI == svgns){
switch(label.localName.toLowerCase()){
case 'text':
label.setAttribute('x', labelX + 'px');
label.setAttribute('y', (labelY + labelFontSize) + 'px'); //Cannot be + labelRect.height
break;
case 'circle':
label.setAttribute('cx', (labelX + labelRect.width/2) + 'px');
label.setAttribute('cy', (labelY + labelRect.height/2) + 'px');
break;
//case 'path':
// break;
//case 'polygon':
// break;
case 'rect':
case 'foreignobject':
label.setAttribute('x', labelX + 'px');
label.setAttribute('y', labelY + 'px');
break;
default:
throw Error("Cannot use a '" + label.nodeName + "' element as a node label.");
}
}
else {
throw Error("Expected an SVG element to position.");
}
//var rect = document.createElementNS(svgns, 'rect');
//rect.setAttribute('x', labelX + 'px');
//rect.setAttribute('y', (offsetTop + labelPadding.top) + 'px');
//rect.setAttribute('height', labelFontSize + 'px');
//rect.setAttribute('width', labelRect.width + 'px');
//rect.setAttribute('style', 'fill:none; stroke:red; stroke-width:1px;');
//g.appendChild(rect);
//TEMP: offsetLeft
//var line = document.createElementNS(svgns, 'line');
//line.setAttribute('style', 'stroke:red; stroke-width:2px; fill:none;');
//line.setAttribute('x1', offsetLeft + 'px');
//line.setAttribute('x2', offsetLeft + 'px');
//line.setAttribute('y1', offsetTop + labelPadding.left + 'px');
//line.setAttribute('y2', offsetTop + labelPadding.left + labelRect.height + 'px');
//g.appendChild(line);
//Position branch directly above the label
var branchX = (labelX + labelRect.width/2);
if(branch){
branch.points.clear();
var point;
if(!collapsedAncestor) {
point = tree.svgElement.createSVGPoint();
point.x = branchX;
point.y = offsetTop;
branch.points.appendItem(point);
}
}
if(isNodeDisplayed){
//Connect the triangle under collapsed node
if(treeNode.collapsed){
var point;
//Set the left bottom point for each branch; this is to help with
// keeping track of the
//forEach(treeNode.leafDecendantsInfo, function(info){
// point = tree.svgElement.createSVGPoint();
// point.x = getX(firstLeafInfo.label);
// info.branch.points.appendItem(point);
//});
var firstLeafInfo = treeNode.leafDecendantsInfo[0];
var lastLeafInfo = treeNode.leafDecendantsInfo[treeNode.leafDecendantsInfo.length-1];
var leftBottomX = getX(firstLeafInfo.label);
var rightBottomX = getX(lastLeafInfo.label)+lastLeafInfo.labelRect.width;
//Left bottom
point = tree.svgElement.createSVGPoint();
point.x = leftBottomX;
point.y = firstLeafInfo.offsetTop;
firstLeafInfo.branch.points.appendItem(point);
//Right bottom
point = tree.svgElement.createSVGPoint();
point.x = rightBottomX;
point.y = lastLeafInfo.offsetTop;
firstLeafInfo.branch.points.appendItem(point);
//Top
point = tree.svgElement.createSVGPoint();
point.x = branchX;
point.y = offsetTop + labelPadding.top + labelPadding.bottom + labelRect.height;
firstLeafInfo.branch.points.appendItem(point);
//Closepath
point = tree.svgElement.createSVGPoint();
point.x = leftBottomX;
point.y = firstLeafInfo.offsetTop;
firstLeafInfo.branch.points.appendItem(point);
}
//Connect branches from child labels to parent label
else if(!collapsedAncestor){
for(var i = 0, len = childrenInfo.length; i < len; i++){
var childBranch = childrenInfo[i].branch;
var point = tree.svgElement.createSVGPoint();
point.x = branchX;
point.y = offsetTop + labelPadding.top + labelPadding.bottom + labelRect.height;
childBranch.points.appendItem(point);
//If odd number, then this should be collapsed!
if(childBranch.points.numberOfItems % 2 == 1){
throw Error("Unexpected");
}
}
}
}
// If all of the leaves are supposed to be at the same vertical axis, then
// push them down now
if(childrenInfo.length && !isAncestorExtended && treeNode.extended){
//@todo: Increase the y/y2 of all leafNodes... can we just increment?
//@todo: Make the svg image higher if it gets higher if one of the leafNodes is taller!!!
forEach(leafNodes, function(leafNode){
//Get the points that are on the bottom half
var bottomPoints = [];
for(var j = 0, points = leafNode.branch.points, len = points.numberOfItems; j < len; j++){
bottomPoints.push(points.getItem(j));
}
bottomPoints.sort(function(a,b){
return b.y - a.y;
});
bottomPoints.pop();
//Reposition the branch bottom points and the label
var offsetTopDiff = treeNode.maxOffsetTop - leafNode.offsetTop;
var newOffsetTop = leafNode.offsetTop + offsetTopDiff;
if(Math.round(newOffsetTop) <= Math.round(treeNode.maxOffsetTop)){
//leafNode.branch.y2.baseVal.value = newBranchY;
forEach(bottomPoints, function(point){
point.y = newOffsetTop;
});
leafNode.label.setAttribute('y', getY(leafNode.label) + offsetTopDiff);
leafNode.offsetTop += offsetTopDiff;
}
});
}
//Update the dimensions of the entire "canvas"
tree.height = Math.max(
tree.height,
offsetTop + labelPadding.top + labelRect.height + labelPadding.bottom,
offsetTop + labelPadding.top + labelFontSize + labelPadding.bottom //@todo
);
tree.width = Math.max(
tree.width,
offsetLeft + childrenWidth,
offsetLeft + labelRect.width + labelPadding.left + labelPadding.right,
labelX + labelRect.width + labelPadding.left + labelPadding.right
);
var info = {
label:label,
labelRect:labelRect,
labelX:labelX, //@todo: this should be part of labelRect
labelY:labelY, //@todo: this should be part of labelRect
offsetTop:offsetTop,
maxOffsetTop:treeNode.maxOffsetTop,
branch:branch,
leafNodes:leafNodes,
subtreeElements:subtreeElements,
width:Math.max(labelRect.width + labelPadding.left + labelPadding.right, childrenWidth)
};
if(isNodeDisplayed && collapsedAncestor)
collapsedAncestor.leafDecendantsInfo.push(info);
return info;
}
/**
* Class that represents a node in a tree
*/
var TN = T.Node = function(obj){
this.label = obj.label;
this.collapsed = !!obj.collapsed;
this.extended = !!obj.extended;
if(obj.children && obj.children.length){
this.children = [];
for(var i = 0, len = obj.children.length; i < len; i++){
this.children.push(new TN(obj.children[i]));
}
}
};
TN.prototype.label = "";
TN.prototype.collapsed = false;
TN.prototype.extended = false;
TN.prototype.children = [];
TN.prototype.maxOffsetTop = 0; //readonly
TN.prototype.branchElement = null;
TN.prototype.labelElement = null;
TN.prototype.leafDecendantsInfo = [];
/**
* If the node is collapsed, then expand it and draw.
* @todo We need to keep track of the containing tree because there is no this.draw()
*/
//TN.prototype.expand = function expand(){
// if(!this.collapsed)
// return;
// this.collapsed = false;
// this.draw();
//};
/**
* If the node is expanded, then collapse it and draw.
* @todo We need to keep track of the containing tree because there is no this.draw()
*/
//TN.prototype.collapse = function collapse(){
// if(this.collapsed)
// return;
// tree.draw();
//};
/**
* Filters and actions (inspired by WordPress)
*/
T.prototype.filters = {};
/**
* Applied onto the tree as: _applyFilters.apply(this, [hookname, value, ...])
*/
function _applyFilters(hookname, value /*...*/){
var filters = this.filters[hookname];
if(filters && filters.length){
var filterArgs = [];
for(var i = 1; i < arguments.length; i++)
filterArgs.push(arguments[i]);
var that = this;
forEach(filters, function(filter){
filterArgs[0] = filter.apply(that, filterArgs);
});
value = filterArgs[0];
}
return value;
}
/**
* Add a filter callback for a particular hook
*/
T.prototype.addFilter = function(hookname, callback, position){
if(!this.filters[hookname])
this.filters[hookname] = [];
if(isNaN(position))
this.filters[hookname].push(callback);
else
this.filters[hookname].splice(position, 0, callback);
};
T.prototype.actions = {};
/*
1. Convert SVG to Canvas via standard canvas API informed by positions and dimensions
available from SVG? Note Canvas has a measureText() method.
http://uupaa-js-spinoff.googlecode.com/svn/trunk/uupaa-excanvas.js/demo/8_2_canvas_measureText.html
2. Do SVG and the have a button to export to Canvas, which will iterate over all of the
elements in the SVG document and draw them onto a corresponding canvas element.
NOTE: Must be valid JSON, otherwise someone could inject some bad JavaScript in a bad URL
QUESTION: Can we do Packer without the self-extraction code included? We can do a JavaScript implementation of GZip and then store result in hash after Base64
*/
})();