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">&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})();
820