/**
* JSON Tree library (a part of jsonTreeViewer)
* http://github.com/summerstyle/jsonTreeViewer
*
* Copyright 2017 Vera Lobacheva (http://iamvera.com)
* Released under the MIT license (LICENSE.txt)
*/
var jsonTree = (function() {
/* ---------- Utilities ---------- */
var utils = {
/*
* Returns js-"class" of value
*
* @param val {any type} - value
* @returns {string} - for example, "[object Function]"
*/
getClass : function(val) {
return Object.prototype.toString.call(val);
},
/**
* Checks for a type of value (for valid JSON data types).
* In other cases - throws an exception
*
* @param val {any type} - the value for new node
* @returns {string} ("object" | "array" | "null" | "boolean" | "number" | "string")
*/
getType : function(val) {
if (val === null) {
return 'null';
}
switch (typeof val) {
case 'number':
return 'number';
case 'string':
return 'string';
case 'boolean':
return 'boolean';
}
switch(utils.getClass(val)) {
case '[object Array]':
return 'array';
case '[object Object]':
return 'object';
}
throw new Error('Bad type: ' + utils.getClass(val));
},
/**
* Applies for each item of list some function
* and checks for last element of the list
*
* @param obj {Object | Array} - a list or a dict with child nodes
* @param func {Function} - the function for each item
*/
forEachNode : function(obj, func) {
var type = utils.getType(obj),
isLast;
switch (type) {
case 'array':
isLast = obj.length - 1;
obj.forEach(function(item, i) {
func(i, item, i === isLast);
});
break;
case 'object':
var keys = Object.keys(obj).sort();
isLast = keys.length - 1;
keys.forEach(function(item, i) {
func(item, obj[item], i === isLast);
});
break;
}
},
/**
* Implements the kind of an inheritance by
* using parent prototype and
* creating intermediate constructor
*
* @param Child {Function} - a child constructor
* @param Parent {Function} - a parent constructor
*/
inherits : (function() {
var F = function() {};
return function(Child, Parent) {
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
};
})(),
/*
* Checks for a valid type of root node*
*
* @param {any type} jsonObj - a value for root node
* @returns {boolean} - true for an object or an array, false otherwise
*/
isValidRoot : function(jsonObj) {
switch (utils.getType(jsonObj)) {
case 'object':
case 'array':
return true;
default:
return false;
}
},
/**
* Extends some object
*/
extend : function(targetObj, sourceObj) {
for (var prop in sourceObj) {
if (sourceObj.hasOwnProperty(prop)) {
targetObj[prop] = sourceObj[prop];
}
}
}
};
/* ---------- Node constructors ---------- */
/**
* The factory for creating nodes of defined type.
*
* ~~~ Node ~~~ is a structure element of an onject or an array
* with own label (a key of an object or an index of an array)
* and value of any json data type. The root object or array
* is a node without label.
* {...
* [+] "label": value,
* ...}
*
* Markup:
*
*
*
*
* "label"
*
* :
*
* <(div|span) class="jsontree_value jsontree_value_(object|array|boolean|null|number|string)">
* ...
* (div|span)>
*
*
* @param label {string} - key name
* @param val {Object | Array | string | number | boolean | null} - a value of node
* @param isLast {boolean} - true if node is last in list of siblings
*
* @return {Node}
*/
function Node(label, val, isLast) {
var nodeType = utils.getType(val);
if (nodeType in Node.CONSTRUCTORS) {
return new Node.CONSTRUCTORS[nodeType](label, val, isLast);
} else {
throw new Error('Bad type: ' + utils.getClass(val));
}
}
Node.CONSTRUCTORS = {
'boolean' : NodeBoolean,
'number' : NodeNumber,
'string' : NodeString,
'null' : NodeNull,
'object' : NodeObject,
'array' : NodeArray
};
/*
* The constructor for simple types (string, number, boolean, null)
* {...
* [+] "label": value,
* ...}
* value = string || number || boolean || null
*
* Markup:
*
*
* "age"
* :
*
* 25
* ,
*
*
* @abstract
* @param label {string} - key name
* @param val {string | number | boolean | null} - a value of simple types
* @param isLast {boolean} - true if node is last in list of parent childNodes
*/
function _NodeSimple(label, val, isLast) {
if (this.constructor === _NodeSimple) {
throw new Error('This is abstract class');
}
var self = this,
el = document.createElement('li'),
labelEl,
template = function(label, val) {
var str = '\
\
"' +
label +
'" : \
\
\
' +
val +
'' +
(!isLast ? ',' : '') +
'';
return str;
};
self.label = label;
self.isComplex = false;
el.classList.add('jsontree_node');
el.innerHTML = template(label, val);
self.el = el;
labelEl = el.querySelector('.jsontree_label');
labelEl.addEventListener('click', function(e) {
if (e.altKey) {
self.toggleMarked();
return;
}
if (e.shiftKey) {
document.getSelection().removeAllRanges();
alert(self.getJSONPath());
return;
}
}, false);
}
_NodeSimple.prototype = {
constructor : _NodeSimple,
/**
* Mark node
*/
mark : function() {
this.el.classList.add('jsontree_node_marked');
},
/**
* Unmark node
*/
unmark : function() {
this.el.classList.remove('jsontree_node_marked');
},
/**
* Mark or unmark node
*/
toggleMarked : function() {
this.el.classList.toggle('jsontree_node_marked');
},
/**
* Expands parent node of this node
*
* @param isRecursive {boolean} - if true, expands all parent nodes
* (from node to root)
*/
expandParent : function(isRecursive) {
if (!this.parent) {
return;
}
this.parent.expand();
this.parent.expandParent(isRecursive);
},
/**
* Returns JSON-path of this
*
* @param isInDotNotation {boolean} - kind of notation for returned json-path
* (by default, in bracket notation)
* @returns {string}
*/
getJSONPath : function(isInDotNotation) {
if (this.isRoot) {
return "$";
}
var currentPath;
if (this.parent.type === 'array') {
currentPath = "[" + this.label + "]";
} else {
currentPath = isInDotNotation ? "." + this.label : "['" + this.label + "']";
}
return this.parent.getJSONPath(isInDotNotation) + currentPath;
}
};
/*
* The constructor for boolean values
* {...
* [+] "label": boolean,
* ...}
* boolean = true || false
*
* @constructor
* @param label {string} - key name
* @param val {boolean} - value of boolean type, true or false
* @param isLast {boolean} - true if node is last in list of parent childNodes
*/
function NodeBoolean(label, val, isLast) {
this.type = "boolean";
_NodeSimple.call(this, label, val, isLast);
}
utils.inherits(NodeBoolean,_NodeSimple);
/*
* The constructor for number values
* {...
* [+] "label": number,
* ...}
* number = 123
*
* @constructor
* @param label {string} - key name
* @param val {number} - value of number type, for example 123
* @param isLast {boolean} - true if node is last in list of parent childNodes
*/
function NodeNumber(label, val, isLast) {
this.type = "number";
_NodeSimple.call(this, label, val, isLast);
}
utils.inherits(NodeNumber,_NodeSimple);
/*
* The constructor for string values
* {...
* [+] "label": string,
* ...}
* string = "abc"
*
* @constructor
* @param label {string} - key name
* @param val {string} - value of string type, for example "abc"
* @param isLast {boolean} - true if node is last in list of parent childNodes
*/
function NodeString(label, val, isLast) {
this.type = "string";
_NodeSimple.call(this, label, '"' + val + '"', isLast);
}
utils.inherits(NodeString,_NodeSimple);
/*
* The constructor for null values
* {...
* [+] "label": null,
* ...}
*
* @constructor
* @param label {string} - key name
* @param val {null} - value (only null)
* @param isLast {boolean} - true if node is last in list of parent childNodes
*/
function NodeNull(label, val, isLast) {
this.type = "null";
_NodeSimple.call(this, label, val, isLast);
}
utils.inherits(NodeNull,_NodeSimple);
/*
* The constructor for complex types (object, array)
* {...
* [+] "label": value,
* ...}
* value = object || array
*
* Markup:
*
*
*
*
* "label"
*
* :
*
*
*
*
* @abstract
* @param label {string} - key name
* @param val {Object | Array} - a value of complex types, object or array
* @param isLast {boolean} - true if node is last in list of parent childNodes
*/
function _NodeComplex(label, val, isLast) {
if (this.constructor === _NodeComplex) {
throw new Error('This is abstract class');
}
var self = this,
el = document.createElement('li'),
template = function(label, sym) {
var comma = (!isLast) ? ',' : '',
str = '\
\
\
' + sym[0] + '\
…\
\
' + sym[1] + '' +
'
' + comma +
'
';
if (label !== null) {
str = '\
\
' +
'' +
'"' + label +
'" : \
' + str;
}
return str;
},
childNodesUl,
labelEl,
moreContentEl,
childNodes = [];
self.label = label;
self.isComplex = true;
el.classList.add('jsontree_node');
el.classList.add('jsontree_node_complex');
el.innerHTML = template(label, self.sym);
childNodesUl = el.querySelector('.jsontree_child-nodes');
if (label !== null) {
labelEl = el.querySelector('.jsontree_label');
moreContentEl = el.querySelector('.jsontree_show-more');
labelEl.addEventListener('click', function(e) {
if (e.altKey) {
self.toggleMarked();
return;
}
if (e.shiftKey) {
document.getSelection().removeAllRanges();
alert(self.getJSONPath());
return;
}
self.toggle(e.ctrlKey || e.metaKey);
}, false);
moreContentEl.addEventListener('click', function(e) {
self.toggle(e.ctrlKey || e.metaKey);
}, false);
self.isRoot = false;
} else {
self.isRoot = true;
self.parent = null;
el.classList.add('jsontree_node_expanded');
}
self.el = el;
self.childNodes = childNodes;
self.childNodesUl = childNodesUl;
utils.forEachNode(val, function(label, node, isLast) {
self.addChild(new Node(label, node, isLast));
});
self.isEmpty = !Boolean(childNodes.length);
if (self.isEmpty) {
el.classList.add('jsontree_node_empty');
}
}
utils.inherits(_NodeComplex, _NodeSimple);
utils.extend(_NodeComplex.prototype, {
constructor : _NodeComplex,
/*
* Add child node to list of child nodes
*
* @param child {Node} - child node
*/
addChild : function(child) {
this.childNodes.push(child);
this.childNodesUl.appendChild(child.el);
child.parent = this;
},
/*
* Expands this list of node child nodes
*
* @param isRecursive {boolean} - if true, expands all child nodes
*/
expand : function(isRecursive){
if (this.isEmpty) {
return;
}
if (!this.isRoot) {
this.el.classList.add('jsontree_node_expanded');
}
if (isRecursive) {
this.childNodes.forEach(function(item, i) {
if (item.isComplex) {
item.expand(isRecursive);
}
});
}
},
/*
* Collapses this list of node child nodes
*
* @param isRecursive {boolean} - if true, collapses all child nodes
*/
collapse : function(isRecursive) {
if (this.isEmpty) {
return;
}
if (!this.isRoot) {
this.el.classList.remove('jsontree_node_expanded');
}
if (isRecursive) {
this.childNodes.forEach(function(item, i) {
if (item.isComplex) {
item.collapse(isRecursive);
}
});
}
},
/*
* Expands collapsed or collapses expanded node
*
* @param {boolean} isRecursive - Expand all child nodes if this node is expanded
* and collapse it otherwise
*/
toggle : function(isRecursive) {
if (this.isEmpty) {
return;
}
this.el.classList.toggle('jsontree_node_expanded');
if (isRecursive) {
var isExpanded = this.el.classList.contains('jsontree_node_expanded');
this.childNodes.forEach(function(item, i) {
if (item.isComplex) {
item[isExpanded ? 'expand' : 'collapse'](isRecursive);
}
});
}
},
/**
* Find child nodes that match some conditions and handle it
*
* @param {Function} matcher
* @param {Function} handler
* @param {boolean} isRecursive
*/
findChildren : function(matcher, handler, isRecursive) {
if (this.isEmpty) {
return;
}
this.childNodes.forEach(function(item, i) {
if (matcher(item)) {
handler(item);
}
if (item.isComplex && isRecursive) {
item.findChildren(matcher, handler, isRecursive);
}
});
}
});
/*
* The constructor for object values
* {...
* [+] "label": object,
* ...}
* object = {"abc": "def"}
*
* @constructor
* @param label {string} - key name
* @param val {Object} - value of object type, {"abc": "def"}
* @param isLast {boolean} - true if node is last in list of siblings
*/
function NodeObject(label, val, isLast) {
this.sym = ['{', '}'];
this.type = "object";
_NodeComplex.call(this, label, val, isLast);
}
utils.inherits(NodeObject,_NodeComplex);
/*
* The constructor for array values
* {...
* [+] "label": array,
* ...}
* array = [1,2,3]
*
* @constructor
* @param label {string} - key name
* @param val {Array} - value of array type, [1,2,3]
* @param isLast {boolean} - true if node is last in list of siblings
*/
function NodeArray(label, val, isLast) {
this.sym = ['[', ']'];
this.type = "array";
_NodeComplex.call(this, label, val, isLast);
}
utils.inherits(NodeArray, _NodeComplex);
/* ---------- The tree constructor ---------- */
/*
* The constructor for json tree.
* It contains only one Node (Array or Object), without property name.
* CSS-styles of .tree define main tree styles like font-family,
* font-size and own margins.
*
* Markup:
*
*
* @constructor
* @param jsonObj {Object | Array} - data for tree
* @param domEl {DOMElement} - DOM-element, wrapper for tree
*/
function Tree(jsonObj, domEl) {
this.wrapper = document.createElement('ul');
this.wrapper.className = 'jsontree_tree clearfix';
this.rootNode = null;
this.sourceJSONObj = jsonObj;
this.loadData(jsonObj);
this.appendTo(domEl);
}
Tree.prototype = {
constructor : Tree,
/**
* Fill new data in current json tree
*
* @param {Object | Array} jsonObj - json-data
*/
loadData : function(jsonObj) {
if (!utils.isValidRoot(jsonObj)) {
alert('The root should be an object or an array');
return;
}
this.sourceJSONObj = jsonObj;
this.rootNode = new Node(null, jsonObj, 'last');
this.wrapper.innerHTML = '';
this.wrapper.appendChild(this.rootNode.el);
},
/**
* Appends tree to DOM-element (or move it to new place)
*
* @param {DOMElement} domEl
*/
appendTo : function(domEl) {
domEl.appendChild(this.wrapper);
},
/**
* Expands all tree nodes (objects or arrays) recursively
*
* @param {Function} filterFunc - 'true' if this node should be expanded
*/
expand : function(filterFunc) {
if (this.rootNode.isComplex) {
if (typeof filterFunc == 'function') {
this.rootNode.childNodes.forEach(function(item, i) {
if (item.isComplex && filterFunc(item)) {
item.expand();
}
});
} else {
this.rootNode.expand('recursive');
}
}
},
/**
* Collapses all tree nodes (objects or arrays) recursively
*/
collapse : function() {
if (typeof this.rootNode.collapse === 'function') {
this.rootNode.collapse('recursive');
}
},
/**
* Returns the source json-string (pretty-printed)
*
* @param {boolean} isPrettyPrinted - 'true' for pretty-printed string
* @returns {string} - for exemple, '{"a":2,"b":3}'
*/
toSourceJSON : function(isPrettyPrinted) {
if (!isPrettyPrinted) {
return JSON.stringify(this.sourceJSONObj);
}
var DELIMETER = "[%^$#$%^%]",
jsonStr = JSON.stringify(this.sourceJSONObj, null, DELIMETER);
jsonStr = jsonStr.split("\n").join("
");
jsonStr = jsonStr.split(DELIMETER).join(" ");
return jsonStr;
},
/**
* Find all nodes that match some conditions and handle it
*/
findAndHandle : function(matcher, handler) {
this.rootNode.findChildren(matcher, handler, 'isRecursive');
},
/**
* Unmark all nodes
*/
unmarkAll : function() {
this.rootNode.findChildren(function(node) {
return true;
}, function(node) {
node.unmark();
}, 'isRecursive');
}
};
/* ---------- Public methods ---------- */
return {
/**
* Creates new tree by data and appends it to the DOM-element
*
* @param jsonObj {Object | Array} - json-data
* @param domEl {DOMElement} - the wrapper element
* @returns {Tree}
*/
create : function(jsonObj, domEl) {
return new Tree(jsonObj, domEl);
}
};
})();