github.com/whiteboxio/flow@v0.0.3-0.20190918184116-508d75d68a2c/web/static/js/jsonTree.js (about)

     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  
     9  var 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">&hellip;</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("&nbsp;&nbsp;&nbsp;&nbsp;");
   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  })();