1/** 2 * JSON Tree library (a part of jsonTreeViewer) 3 * http://github.com/summerstyle/jsonTreeViewer 4 * 5 * Copyright 2017 Vera Lobacheva (http://iamvera.com) 6 * Released under the MIT license (LICENSE.txt) 7 */ 8 9var jsonTree = (function() { 10 11 /* ---------- Utilities ---------- */ 12 var utils = { 13 14 /* 15 * Returns js-"class" of value 16 * 17 * @param val {any type} - value 18 * @returns {string} - for example, "[object Function]" 19 */ 20 getClass : function(val) { 21 return Object.prototype.toString.call(val); 22 }, 23 24 /** 25 * Checks for a type of value (for valid JSON data types). 26 * In other cases - throws an exception 27 * 28 * @param val {any type} - the value for new node 29 * @returns {string} ("object" | "array" | "null" | "boolean" | "number" | "string") 30 */ 31 getType : function(val) { 32 if (val === null) { 33 return 'null'; 34 } 35 36 switch (typeof val) { 37 case 'number': 38 return 'number'; 39 40 case 'string': 41 return 'string'; 42 43 case 'boolean': 44 return 'boolean'; 45 } 46 47 switch(utils.getClass(val)) { 48 case '[object Array]': 49 return 'array'; 50 51 case '[object Object]': 52 return 'object'; 53 } 54 55 throw new Error('Bad type: ' + utils.getClass(val)); 56 }, 57 58 /** 59 * Applies for each item of list some function 60 * and checks for last element of the list 61 * 62 * @param obj {Object | Array} - a list or a dict with child nodes 63 * @param func {Function} - the function for each item 64 */ 65 forEachNode : function(obj, func) { 66 var type = utils.getType(obj), 67 isLast; 68 69 switch (type) { 70 case 'array': 71 isLast = obj.length - 1; 72 73 obj.forEach(function(item, i) { 74 func(i, item, i === isLast); 75 }); 76 77 break; 78 79 case 'object': 80 var keys = Object.keys(obj).sort(); 81 82 isLast = keys.length - 1; 83 84 keys.forEach(function(item, i) { 85 func(item, obj[item], i === isLast); 86 }); 87 88 break; 89 } 90 91 }, 92 93 /** 94 * Implements the kind of an inheritance by 95 * using parent prototype and 96 * creating intermediate constructor 97 * 98 * @param Child {Function} - a child constructor 99 * @param Parent {Function} - a parent constructor 100 */ 101 inherits : (function() { 102 var F = function() {}; 103 104 return function(Child, Parent) { 105 F.prototype = Parent.prototype; 106 Child.prototype = new F(); 107 Child.prototype.constructor = Child; 108 }; 109 })(), 110 111 /* 112 * Checks for a valid type of root node* 113 * 114 * @param {any type} jsonObj - a value for root node 115 * @returns {boolean} - true for an object or an array, false otherwise 116 */ 117 isValidRoot : function(jsonObj) { 118 switch (utils.getType(jsonObj)) { 119 case 'object': 120 case 'array': 121 return true; 122 default: 123 return false; 124 } 125 }, 126 127 /** 128 * Extends some object 129 */ 130 extend : function(targetObj, sourceObj) { 131 for (var prop in sourceObj) { 132 if (sourceObj.hasOwnProperty(prop)) { 133 targetObj[prop] = sourceObj[prop]; 134 } 135 } 136 } 137 }; 138 139 140 /* ---------- Node constructors ---------- */ 141 142 /** 143 * The factory for creating nodes of defined type. 144 * 145 * ~~~ Node ~~~ is a structure element of an onject or an array 146 * with own label (a key of an object or an index of an array) 147 * and value of any json data type. The root object or array 148 * is a node without label. 149 * {... 150 * [+] "label": value, 151 * ...} 152 * 153 * Markup: 154 * <li class="jsontree_node [jsontree_node_expanded]"> 155 * <span class="jsontree_label-wrapper"> 156 * <span class="jsontree_label"> 157 * <span class="jsontree_expand-button" /> 158 * "label" 159 * </span> 160 * : 161 * </span> 162 * <(div|span) class="jsontree_value jsontree_value_(object|array|boolean|null|number|string)"> 163 * ... 164 * </(div|span)> 165 * </li> 166 * 167 * @param label {string} - key name 168 * @param val {Object | Array | string | number | boolean | null} - a value of node 169 * @param isLast {boolean} - true if node is last in list of siblings 170 * 171 * @return {Node} 172 */ 173 function Node(label, val, isLast) { 174 var nodeType = utils.getType(val); 175 176 if (nodeType in Node.CONSTRUCTORS) { 177 return new Node.CONSTRUCTORS[nodeType](label, val, isLast); 178 } else { 179 throw new Error('Bad type: ' + utils.getClass(val)); 180 } 181 } 182 183 Node.CONSTRUCTORS = { 184 'boolean' : NodeBoolean, 185 'number' : NodeNumber, 186 'string' : NodeString, 187 'null' : NodeNull, 188 'object' : NodeObject, 189 'array' : NodeArray 190 }; 191 192 193 /* 194 * The constructor for simple types (string, number, boolean, null) 195 * {... 196 * [+] "label": value, 197 * ...} 198 * value = string || number || boolean || null 199 * 200 * Markup: 201 * <li class="jsontree_node"> 202 * <span class="jsontree_label-wrapper"> 203 * <span class="jsontree_label">"age"</span> 204 * : 205 * </span> 206 * <span class="jsontree_value jsontree_value_(number|boolean|string|null)">25</span> 207 * , 208 * </li> 209 * 210 * @abstract 211 * @param label {string} - key name 212 * @param val {string | number | boolean | null} - a value of simple types 213 * @param isLast {boolean} - true if node is last in list of parent childNodes 214 */ 215 function _NodeSimple(label, val, isLast) { 216 if (this.constructor === _NodeSimple) { 217 throw new Error('This is abstract class'); 218 } 219 220 var self = this, 221 el = document.createElement('li'), 222 labelEl, 223 template = function(label, val) { 224 var str = '\ 225 <span class="jsontree_label-wrapper">\ 226 <span class="jsontree_label">"' + 227 label + 228 '"</span> : \ 229 </span>\ 230 <span class="jsontree_value-wrapper">\ 231 <span class="jsontree_value jsontree_value_' + self.type + '">' + 232 val + 233 '</span>' + 234 (!isLast ? ',' : '') + 235 '</span>'; 236 237 return str; 238 }; 239 240 self.label = label; 241 self.isComplex = false; 242 243 el.classList.add('jsontree_node'); 244 el.innerHTML = template(label, val); 245 246 self.el = el; 247 248 labelEl = el.querySelector('.jsontree_label'); 249 250 labelEl.addEventListener('click', function(e) { 251 if (e.altKey) { 252 self.toggleMarked(); 253 return; 254 } 255 256 if (e.shiftKey) { 257 document.getSelection().removeAllRanges(); 258 alert(self.getJSONPath()); 259 return; 260 } 261 }, false); 262 } 263 264 _NodeSimple.prototype = { 265 constructor : _NodeSimple, 266 267 /** 268 * Mark node 269 */ 270 mark : function() { 271 this.el.classList.add('jsontree_node_marked'); 272 }, 273 274 /** 275 * Unmark node 276 */ 277 unmark : function() { 278 this.el.classList.remove('jsontree_node_marked'); 279 }, 280 281 /** 282 * Mark or unmark node 283 */ 284 toggleMarked : function() { 285 this.el.classList.toggle('jsontree_node_marked'); 286 }, 287 288 /** 289 * Expands parent node of this node 290 * 291 * @param isRecursive {boolean} - if true, expands all parent nodes 292 * (from node to root) 293 */ 294 expandParent : function(isRecursive) { 295 if (!this.parent) { 296 return; 297 } 298 299 this.parent.expand(); 300 this.parent.expandParent(isRecursive); 301 }, 302 303 /** 304 * Returns JSON-path of this 305 * 306 * @param isInDotNotation {boolean} - kind of notation for returned json-path 307 * (by default, in bracket notation) 308 * @returns {string} 309 */ 310 getJSONPath : function(isInDotNotation) { 311 if (this.isRoot) { 312 return "$"; 313 } 314 315 var currentPath; 316 317 if (this.parent.type === 'array') { 318 currentPath = "[" + this.label + "]"; 319 } else { 320 currentPath = isInDotNotation ? "." + this.label : "['" + this.label + "']"; 321 } 322 323 return this.parent.getJSONPath(isInDotNotation) + currentPath; 324 } 325 }; 326 327 328 /* 329 * The constructor for boolean values 330 * {... 331 * [+] "label": boolean, 332 * ...} 333 * boolean = true || false 334 * 335 * @constructor 336 * @param label {string} - key name 337 * @param val {boolean} - value of boolean type, true or false 338 * @param isLast {boolean} - true if node is last in list of parent childNodes 339 */ 340 function NodeBoolean(label, val, isLast) { 341 this.type = "boolean"; 342 343 _NodeSimple.call(this, label, val, isLast); 344 } 345 utils.inherits(NodeBoolean,_NodeSimple); 346 347 348 /* 349 * The constructor for number values 350 * {... 351 * [+] "label": number, 352 * ...} 353 * number = 123 354 * 355 * @constructor 356 * @param label {string} - key name 357 * @param val {number} - value of number type, for example 123 358 * @param isLast {boolean} - true if node is last in list of parent childNodes 359 */ 360 function NodeNumber(label, val, isLast) { 361 this.type = "number"; 362 363 _NodeSimple.call(this, label, val, isLast); 364 } 365 utils.inherits(NodeNumber,_NodeSimple); 366 367 368 /* 369 * The constructor for string values 370 * {... 371 * [+] "label": string, 372 * ...} 373 * string = "abc" 374 * 375 * @constructor 376 * @param label {string} - key name 377 * @param val {string} - value of string type, for example "abc" 378 * @param isLast {boolean} - true if node is last in list of parent childNodes 379 */ 380 function NodeString(label, val, isLast) { 381 this.type = "string"; 382 383 _NodeSimple.call(this, label, '"' + val + '"', isLast); 384 } 385 utils.inherits(NodeString,_NodeSimple); 386 387 388 /* 389 * The constructor for null values 390 * {... 391 * [+] "label": null, 392 * ...} 393 * 394 * @constructor 395 * @param label {string} - key name 396 * @param val {null} - value (only null) 397 * @param isLast {boolean} - true if node is last in list of parent childNodes 398 */ 399 function NodeNull(label, val, isLast) { 400 this.type = "null"; 401 402 _NodeSimple.call(this, label, val, isLast); 403 } 404 utils.inherits(NodeNull,_NodeSimple); 405 406 407 /* 408 * The constructor for complex types (object, array) 409 * {... 410 * [+] "label": value, 411 * ...} 412 * value = object || array 413 * 414 * Markup: 415 * <li class="jsontree_node jsontree_node_(object|array) [expanded]"> 416 * <span class="jsontree_label-wrapper"> 417 * <span class="jsontree_label"> 418 * <span class="jsontree_expand-button" /> 419 * "label" 420 * </span> 421 * : 422 * </span> 423 * <div class="jsontree_value"> 424 * <b>{</b> 425 * <ul class="jsontree_child-nodes" /> 426 * <b>}</b> 427 * , 428 * </div> 429 * </li> 430 * 431 * @abstract 432 * @param label {string} - key name 433 * @param val {Object | Array} - a value of complex types, object or array 434 * @param isLast {boolean} - true if node is last in list of parent childNodes 435 */ 436 function _NodeComplex(label, val, isLast) { 437 if (this.constructor === _NodeComplex) { 438 throw new Error('This is abstract class'); 439 } 440 441 var self = this, 442 el = document.createElement('li'), 443 template = function(label, sym) { 444 var comma = (!isLast) ? ',' : '', 445 str = '\ 446 <div class="jsontree_value-wrapper">\ 447 <div class="jsontree_value jsontree_value_' + self.type + '">\ 448 <b>' + sym[0] + '</b>\ 449 <span class="jsontree_show-more">…</span>\ 450 <ul class="jsontree_child-nodes"></ul>\ 451 <b>' + sym[1] + '</b>' + 452 '</div>' + comma + 453 '</div>'; 454 455 if (label !== null) { 456 str = '\ 457 <span class="jsontree_label-wrapper">\ 458 <span class="jsontree_label">' + 459 '<span class="jsontree_expand-button"></span>' + 460 '"' + label + 461 '"</span> : \ 462 </span>' + str; 463 } 464 465 return str; 466 }, 467 childNodesUl, 468 labelEl, 469 moreContentEl, 470 childNodes = []; 471 472 self.label = label; 473 self.isComplex = true; 474 475 el.classList.add('jsontree_node'); 476 el.classList.add('jsontree_node_complex'); 477 el.innerHTML = template(label, self.sym); 478 479 childNodesUl = el.querySelector('.jsontree_child-nodes'); 480 481 if (label !== null) { 482 labelEl = el.querySelector('.jsontree_label'); 483 moreContentEl = el.querySelector('.jsontree_show-more'); 484 485 labelEl.addEventListener('click', function(e) { 486 if (e.altKey) { 487 self.toggleMarked(); 488 return; 489 } 490 491 if (e.shiftKey) { 492 document.getSelection().removeAllRanges(); 493 alert(self.getJSONPath()); 494 return; 495 } 496 497 self.toggle(e.ctrlKey || e.metaKey); 498 }, false); 499 500 moreContentEl.addEventListener('click', function(e) { 501 self.toggle(e.ctrlKey || e.metaKey); 502 }, false); 503 504 self.isRoot = false; 505 } else { 506 self.isRoot = true; 507 self.parent = null; 508 509 el.classList.add('jsontree_node_expanded'); 510 } 511 512 self.el = el; 513 self.childNodes = childNodes; 514 self.childNodesUl = childNodesUl; 515 516 utils.forEachNode(val, function(label, node, isLast) { 517 self.addChild(new Node(label, node, isLast)); 518 }); 519 520 self.isEmpty = !Boolean(childNodes.length); 521 if (self.isEmpty) { 522 el.classList.add('jsontree_node_empty'); 523 } 524 } 525 526 utils.inherits(_NodeComplex, _NodeSimple); 527 528 utils.extend(_NodeComplex.prototype, { 529 constructor : _NodeComplex, 530 531 /* 532 * Add child node to list of child nodes 533 * 534 * @param child {Node} - child node 535 */ 536 addChild : function(child) { 537 this.childNodes.push(child); 538 this.childNodesUl.appendChild(child.el); 539 child.parent = this; 540 }, 541 542 /* 543 * Expands this list of node child nodes 544 * 545 * @param isRecursive {boolean} - if true, expands all child nodes 546 */ 547 expand : function(isRecursive){ 548 if (this.isEmpty) { 549 return; 550 } 551 552 if (!this.isRoot) { 553 this.el.classList.add('jsontree_node_expanded'); 554 } 555 556 if (isRecursive) { 557 this.childNodes.forEach(function(item, i) { 558 if (item.isComplex) { 559 item.expand(isRecursive); 560 } 561 }); 562 } 563 }, 564 565 /* 566 * Collapses this list of node child nodes 567 * 568 * @param isRecursive {boolean} - if true, collapses all child nodes 569 */ 570 collapse : function(isRecursive) { 571 if (this.isEmpty) { 572 return; 573 } 574 575 if (!this.isRoot) { 576 this.el.classList.remove('jsontree_node_expanded'); 577 } 578 579 if (isRecursive) { 580 this.childNodes.forEach(function(item, i) { 581 if (item.isComplex) { 582 item.collapse(isRecursive); 583 } 584 }); 585 } 586 }, 587 588 /* 589 * Expands collapsed or collapses expanded node 590 * 591 * @param {boolean} isRecursive - Expand all child nodes if this node is expanded 592 * and collapse it otherwise 593 */ 594 toggle : function(isRecursive) { 595 if (this.isEmpty) { 596 return; 597 } 598 599 this.el.classList.toggle('jsontree_node_expanded'); 600 601 if (isRecursive) { 602 var isExpanded = this.el.classList.contains('jsontree_node_expanded'); 603 604 this.childNodes.forEach(function(item, i) { 605 if (item.isComplex) { 606 item[isExpanded ? 'expand' : 'collapse'](isRecursive); 607 } 608 }); 609 } 610 }, 611 612 /** 613 * Find child nodes that match some conditions and handle it 614 * 615 * @param {Function} matcher 616 * @param {Function} handler 617 * @param {boolean} isRecursive 618 */ 619 findChildren : function(matcher, handler, isRecursive) { 620 if (this.isEmpty) { 621 return; 622 } 623 624 this.childNodes.forEach(function(item, i) { 625 if (matcher(item)) { 626 handler(item); 627 } 628 629 if (item.isComplex && isRecursive) { 630 item.findChildren(matcher, handler, isRecursive); 631 } 632 }); 633 } 634 }); 635 636 637 /* 638 * The constructor for object values 639 * {... 640 * [+] "label": object, 641 * ...} 642 * object = {"abc": "def"} 643 * 644 * @constructor 645 * @param label {string} - key name 646 * @param val {Object} - value of object type, {"abc": "def"} 647 * @param isLast {boolean} - true if node is last in list of siblings 648 */ 649 function NodeObject(label, val, isLast) { 650 this.sym = ['{', '}']; 651 this.type = "object"; 652 653 _NodeComplex.call(this, label, val, isLast); 654 } 655 utils.inherits(NodeObject,_NodeComplex); 656 657 658 /* 659 * The constructor for array values 660 * {... 661 * [+] "label": array, 662 * ...} 663 * array = [1,2,3] 664 * 665 * @constructor 666 * @param label {string} - key name 667 * @param val {Array} - value of array type, [1,2,3] 668 * @param isLast {boolean} - true if node is last in list of siblings 669 */ 670 function NodeArray(label, val, isLast) { 671 this.sym = ['[', ']']; 672 this.type = "array"; 673 674 _NodeComplex.call(this, label, val, isLast); 675 } 676 utils.inherits(NodeArray, _NodeComplex); 677 678 679 /* ---------- The tree constructor ---------- */ 680 681 /* 682 * The constructor for json tree. 683 * It contains only one Node (Array or Object), without property name. 684 * CSS-styles of .tree define main tree styles like font-family, 685 * font-size and own margins. 686 * 687 * Markup: 688 * <ul class="jsontree_tree clearfix"> 689 * {Node} 690 * </ul> 691 * 692 * @constructor 693 * @param jsonObj {Object | Array} - data for tree 694 * @param domEl {DOMElement} - DOM-element, wrapper for tree 695 */ 696 function Tree(jsonObj, domEl) { 697 this.wrapper = document.createElement('ul'); 698 this.wrapper.className = 'jsontree_tree clearfix'; 699 700 this.rootNode = null; 701 702 this.sourceJSONObj = jsonObj; 703 704 this.loadData(jsonObj); 705 this.appendTo(domEl); 706 } 707 708 Tree.prototype = { 709 constructor : Tree, 710 711 /** 712 * Fill new data in current json tree 713 * 714 * @param {Object | Array} jsonObj - json-data 715 */ 716 loadData : function(jsonObj) { 717 if (!utils.isValidRoot(jsonObj)) { 718 alert('The root should be an object or an array'); 719 return; 720 } 721 722 this.sourceJSONObj = jsonObj; 723 724 this.rootNode = new Node(null, jsonObj, 'last'); 725 this.wrapper.innerHTML = ''; 726 this.wrapper.appendChild(this.rootNode.el); 727 }, 728 729 /** 730 * Appends tree to DOM-element (or move it to new place) 731 * 732 * @param {DOMElement} domEl 733 */ 734 appendTo : function(domEl) { 735 domEl.appendChild(this.wrapper); 736 }, 737 738 /** 739 * Expands all tree nodes (objects or arrays) recursively 740 * 741 * @param {Function} filterFunc - 'true' if this node should be expanded 742 */ 743 expand : function(filterFunc) { 744 if (this.rootNode.isComplex) { 745 if (typeof filterFunc == 'function') { 746 this.rootNode.childNodes.forEach(function(item, i) { 747 if (item.isComplex && filterFunc(item)) { 748 item.expand(); 749 } 750 }); 751 } else { 752 this.rootNode.expand('recursive'); 753 } 754 } 755 }, 756 757 /** 758 * Collapses all tree nodes (objects or arrays) recursively 759 */ 760 collapse : function() { 761 if (typeof this.rootNode.collapse === 'function') { 762 this.rootNode.collapse('recursive'); 763 } 764 }, 765 766 /** 767 * Returns the source json-string (pretty-printed) 768 * 769 * @param {boolean} isPrettyPrinted - 'true' for pretty-printed string 770 * @returns {string} - for exemple, '{"a":2,"b":3}' 771 */ 772 toSourceJSON : function(isPrettyPrinted) { 773 if (!isPrettyPrinted) { 774 return JSON.stringify(this.sourceJSONObj); 775 } 776 777 var DELIMETER = "[%^$#$%^%]", 778 jsonStr = JSON.stringify(this.sourceJSONObj, null, DELIMETER); 779 780 jsonStr = jsonStr.split("\n").join("<br />"); 781 jsonStr = jsonStr.split(DELIMETER).join(" "); 782 783 return jsonStr; 784 }, 785 786 /** 787 * Find all nodes that match some conditions and handle it 788 */ 789 findAndHandle : function(matcher, handler) { 790 this.rootNode.findChildren(matcher, handler, 'isRecursive'); 791 }, 792 793 /** 794 * Unmark all nodes 795 */ 796 unmarkAll : function() { 797 this.rootNode.findChildren(function(node) { 798 return true; 799 }, function(node) { 800 node.unmark(); 801 }, 'isRecursive'); 802 } 803 }; 804 805 806 /* ---------- Public methods ---------- */ 807 return { 808 /** 809 * Creates new tree by data and appends it to the DOM-element 810 * 811 * @param jsonObj {Object | Array} - json-data 812 * @param domEl {DOMElement} - the wrapper element 813 * @returns {Tree} 814 */ 815 create : function(jsonObj, domEl) { 816 return new Tree(jsonObj, domEl); 817 } 818 }; 819})(); 820