jsonTree.js 24 KB

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