/** * 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)"> * ... * *
  • * * @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); } }; })();