From 904110d141f0699f2dcc669ec731894254e46d62 Mon Sep 17 00:00:00 2001 From: jos Date: Sun, 6 May 2018 19:42:19 +0200 Subject: [PATCH] Implementing "show more" for large arrays (WIP) --- HISTORY.md | 2 +- src/css/jsoneditor.css | 25 ++++- src/js/Node.js | 127 +++++++++++++++++++----- src/js/appendNodeFactory.js | 4 +- src/js/showMoreNodeFactory.js | 178 ++++++++++++++++++++++++++++++++++ test/test_large_array.html | 72 ++++++++++++++ 6 files changed, 377 insertions(+), 31 deletions(-) create mode 100644 src/js/showMoreNodeFactory.js create mode 100644 test/test_large_array.html diff --git a/HISTORY.md b/HISTORY.md index 97e3c05..bc1d776 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,7 +5,7 @@ https://github.com/josdejong/jsoneditor ## not yet released, version 5.15.1 -- Fixed index numbers of Array itesm not being updated after sorting. +- Fixed index numbers of Array items not being updated after sorting. ## 2018-05-02, version 5.15.0 diff --git a/src/css/jsoneditor.css b/src/css/jsoneditor.css index 54a482f..802a66d 100644 --- a/src/css/jsoneditor.css +++ b/src/css/jsoneditor.css @@ -27,11 +27,11 @@ div.jsoneditor-value { div.jsoneditor-readonly { min-width: 16px; - color: gray; + color: #808080; } div.jsoneditor-empty { - border-color: lightgray; + border-color: #d3d3d3; border-style: dashed; border-radius: 2px; } @@ -39,7 +39,7 @@ div.jsoneditor-empty { div.jsoneditor-field.jsoneditor-empty::after, div.jsoneditor-value.jsoneditor-empty::after { pointer-events: none; - color: lightgray; + color: #d3d3d3; font-size: 8pt; } @@ -71,7 +71,7 @@ a.jsoneditor-value.jsoneditor-url:focus { div.jsoneditor td.jsoneditor-separator { padding: 3px 0; vertical-align: top; - color: gray; + color: #808080; } div.jsoneditor-field[contenteditable=true]:focus, @@ -176,6 +176,23 @@ div.jsoneditor-tree button.jsoneditor-invisible { background: none; } +div.jsoneditor-tree div.jsoneditor-show-more { + display: inline-block; + padding: 3px 4px; + margin: 2px 0; + + background-color: #e5e5e5; + border-radius: 3px; + color: #808080; + + font-family: arial, sans-serif; + font-size: 10pt; +} + +div.jsoneditor-tree div.jsoneditor-show-more a { + color: #808080; +} + div.jsoneditor { color: #1A1A1A; border: 1px solid #3883fa; diff --git a/src/js/Node.js b/src/js/Node.js index 417f7f7..606c8cf 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -3,6 +3,7 @@ var naturalSort = require('javascript-natural-sort'); var ContextMenu = require('./ContextMenu'); var appendNodeFactory = require('./appendNodeFactory'); +var showMoreNodeFactory = require('./showMoreNodeFactory'); var util = require('./util'); var translate = require('./i18n').translate; @@ -39,6 +40,12 @@ function Node (editor, params) { // debounce interval for keyboard input in milliseconds Node.prototype.DEBOUNCE_INTERVAL = 150; +Node.prototype.MAX_VISIBLE_CHILDS = 100; +Node.prototype.MAX_VISIBLE_CHILDS = 10; // TODO: remove this line, use 100 instead of 10 + +// default value for the max visible childs of large arrays +Node.prototype.maxVisibleChilds = Node.prototype.MAX_VISIBLE_CHILDS; + /** * Determine whether the field and/or value of this node are editable * @private @@ -440,6 +447,7 @@ Node.prototype.clone = function() { clone.value = this.value; clone.valueInnerText = this.valueInnerText; clone.expanded = this.expanded; + clone.maxVisibleChilds = this.maxVisibleChilds; if (this.childs) { // an object or array @@ -527,20 +535,45 @@ Node.prototype.showChilds = function() { var table = tr ? tr.parentNode : undefined; if (table) { // show row with append button - var append = this.getAppend(); - var nextTr = tr.nextSibling; - if (nextTr) { - table.insertBefore(append, nextTr); - } - else { - table.appendChild(append); + var append = this.getAppendDom(); + if (!append.parentNode) { + var nextTr = tr.nextSibling; + if (nextTr) { + table.insertBefore(append, nextTr); + } + else { + table.appendChild(append); + } } // show childs - this.childs.forEach(function (child) { - table.insertBefore(child.getDom(), append); + var iMax = Math.min(this.childs.length, this.maxVisibleChilds); + var nextTr = this._getNextTr(); + for (var i = 0; i < iMax; i++) { + var child = this.childs[i]; + if (!child.getDom().parentNode) { + table.insertBefore(child.getDom(), nextTr); + } child.showChilds(); - }); + } + + // show "show more childs" if limited + var showMore = this.getShowMoreDom(); + var nextTr = this._getNextTr(); + if (!showMore.parentNode) { + table.insertBefore(showMore, nextTr); + } + this.showMore.updateDom(); // to update the counter + } +}; + +Node.prototype._getNextTr = function() { + if (this.showMore && this.showMore.getDom().parentNode) { + return this.showMore.getDom(); + } + + if (this.append && this.append.getDom().parentNode) { + return this.append.getDom(); } }; @@ -570,7 +603,7 @@ Node.prototype.hideChilds = function() { } // hide append row - var append = this.getAppend(); + var append = this.getAppendDom(); if (append.parentNode) { append.parentNode.removeChild(append); } @@ -579,6 +612,15 @@ Node.prototype.hideChilds = function() { this.childs.forEach(function (child) { child.hide(); }); + + // hide "show more" row + var showMore = this.getShowMoreDom(); + if (showMore.parentNode) { + showMore.parentNode.removeChild(showMore); + } + + // reset max visible childs + delete this.maxVisibleChilds; }; @@ -614,7 +656,7 @@ Node.prototype.appendChild = function(node) { if (this.expanded) { // insert into the DOM, before the appendRow var newTr = node.getDom(); - var appendTr = this.getAppend(); + var appendTr = this.getAppendDom(); var table = appendTr ? appendTr.parentNode : undefined; if (appendTr && table) { table.insertBefore(newTr, appendTr); @@ -1084,7 +1126,7 @@ Node.prototype.changeType = function (newType) { var table = this.dom.tr ? this.dom.tr.parentNode : undefined; var lastTr; if (this.expanded) { - lastTr = this.getAppend(); + lastTr = this.getAppendDom(); } else { lastTr = this.getDom(); @@ -1719,9 +1761,9 @@ Node.onDrag = function (nodes, event) { topThis += 27; // TODO: dangerous to suppose the height of the appendNode a constant of 27 px. } - } - trNext = trNext.nextSibling; + trNext = trNext.nextSibling; + } } while (trNext && mouseY > topThis + heightNext); @@ -2033,6 +2075,30 @@ Node.prototype.updateDom = function (options) { this._updateDomIndexes(); } + // show/hide childs exceeding the maxVisibleChilds + if (this.childs) { + var iMax = Math.min(this.childs.length, this.maxVisibleChilds); + var child; + + // append childs to DOM when not reaching maxVisibleChilds + var i = iMax - 1; + var nextTr = this._getNextTr(); + if (nextTr) + while (this.childs[i] && !this.childs[i].getDom().parentNode) { + child = this.childs[i].getDom(); + nextTr.parentNode.insertBefore(child, nextTr); + i--; + } + + // remove childs from DOM when exceeding maxVisibleChilds + var j = iMax; + while (this.childs[j] && this.childs[j].getDom().parentNode) { + child = this.childs[j].getDom(); + child.parentNode.removeChild(child); + i++; + } + } + if (options && options.recurse === true) { // recurse is true or undefined. update childs recursively if (this.childs) { @@ -2046,6 +2112,11 @@ Node.prototype.updateDom = function (options) { if (this.append) { this.append.updateDom(); } + + // update "show more" text at the bottom of large arrays + if (this.showMore) { + this.showMore.updateDom(); + } }; /** @@ -2577,7 +2648,7 @@ Node.prototype.onKeyDown = function (event) { } else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow left if (lastNode.expanded) { - var appendDom = lastNode.getAppend(); + var appendDom = lastNode.getAppendDom(); nextDom = appendDom ? appendDom.nextSibling : undefined; } else { @@ -3004,12 +3075,7 @@ Node.prototype.sort = function (direction) { this.sortOrder = (order == 1) ? 'asc' : 'desc'; // update the index numbering - if (this.type === 'array') { - this.childs.forEach(function (child, index) { - child.index = index; - child.updateDom(); - }); - } + this._updateDomIndexes(); this.editor._onAction('sort', { node: this, @@ -3024,9 +3090,9 @@ Node.prototype.sort = function (direction) { /** * Create a table row with an append button. - * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable + * @return {HTMLElement | undefined} tr with the AppendNode contents */ -Node.prototype.getAppend = function () { +Node.prototype.getAppendDom = function () { if (!this.append) { this.append = new AppendNode(this.editor); this.append.setParent(this); @@ -3034,6 +3100,17 @@ Node.prototype.getAppend = function () { return this.append.getDom(); }; +/** + * Create a table row with an showMore button and text + * @return {HTMLElement | undefined} tr with the AppendNode contents + */ +Node.prototype.getShowMoreDom = function () { + if (!this.showMore) { + this.showMore = new ShowMoreNode(this.editor, this); + } + return this.showMore.getDom(); +}; + /** * Find the node from an event target * @param {Node} target @@ -3648,6 +3725,8 @@ Node.prototype._escapeJSON = function (text) { }; // TODO: find a nicer solution to resolve this circular dependency between Node and AppendNode +// idea: introduce properties .isAppendNode and .isNode and use that instead of instanceof AppendNode checks var AppendNode = appendNodeFactory(Node); +var ShowMoreNode = showMoreNodeFactory(Node); module.exports = Node; diff --git a/src/js/appendNodeFactory.js b/src/js/appendNodeFactory.js index f9c9b4b..d99f95b 100644 --- a/src/js/appendNodeFactory.js +++ b/src/js/appendNodeFactory.js @@ -77,7 +77,7 @@ function appendNodeFactory(Node) { /** * Update the HTML dom of the Node */ - AppendNode.prototype.updateDom = function () { + AppendNode.prototype.updateDom = function(options) { var dom = this.dom; var tdAppend = dom.td; if (tdAppend) { @@ -189,7 +189,7 @@ function appendNodeFactory(Node) { }; /** - * Handle an event. The event is catched centrally by the editor + * Handle an event. The event is caught centrally by the editor * @param {Event} event */ AppendNode.prototype.onEvent = function (event) { diff --git a/src/js/showMoreNodeFactory.js b/src/js/showMoreNodeFactory.js new file mode 100644 index 0000000..b95f192 --- /dev/null +++ b/src/js/showMoreNodeFactory.js @@ -0,0 +1,178 @@ +'use strict'; + +var translate = require('./i18n').translate; + +/** + * A factory function to create an ShowMoreNode, which depends on a Node + * @param {function} Node + */ +function showMoreNodeFactory(Node) { + /** + * @constructor ShowMoreNode + * @extends Node + * @param {TreeEditor} editor + * @param {Node} parent + * Create a new ShowMoreNode. This is a special node which is created + * for arrays or objects having more than 100 items + */ + function ShowMoreNode (editor, parent) { + /** @type {TreeEditor} */ + this.editor = editor; + this.parent = parent; + this.dom = {}; + } + + ShowMoreNode.prototype = new Node(); + + /** + * Return a table row with an append button. + * @return {Element} dom TR element + */ + ShowMoreNode.prototype.getDom = function () { + if (this.dom.tr) { + return this.dom.tr; + } + + this._updateEditability(); + + // display "show more" + if (!this.dom.tr) { + var me = this; + var parent = this.parent; + var showMoreButton = document.createElement('a'); + showMoreButton.appendChild(document.createTextNode('show\u00A0more')); + showMoreButton.href = '#'; + showMoreButton.onclick = function (event) { + // TODO: use callback instead of accessing a method of the parent + parent.maxVisibleChilds += Node.prototype.MAX_VISIBLE_CHILDS; + me.updateDom(); + parent.showChilds(); + + event.preventDefault(); + return false; + }; + + var showAllButton = document.createElement('a'); + showAllButton.appendChild(document.createTextNode('show\u00A0all')); + showAllButton.href = '#'; + showAllButton.onclick = function (event) { + // TODO: use callback instead of accessing a method of the parent + parent.maxVisibleChilds = Infinity; + me.updateDom(); + parent.showChilds(); + + event.preventDefault(); + return false; + }; + + var moreContents = document.createElement('div'); + var moreText = document.createTextNode(this._getShowMoreText()); + moreContents.className = 'jsoneditor-show-more'; + moreContents.appendChild(moreText); + moreContents.appendChild(showMoreButton); + moreContents.appendChild(document.createTextNode('. ')); + moreContents.appendChild(showAllButton); + moreContents.appendChild(document.createTextNode('. ')); + + var tdContents = document.createElement('td'); + tdContents.appendChild(moreContents); + + var moreTr = document.createElement('tr'); + moreTr.appendChild(document.createElement('td')); + moreTr.appendChild(document.createElement('td')); + moreTr.appendChild(tdContents); + this.dom.tr = moreTr; + this.dom.moreContents = moreContents; + this.dom.moreText = moreText; + } + + this.updateDom(); + + return this.dom.tr; + }; + + /** + * Update the HTML dom of the Node + */ + ShowMoreNode.prototype.updateDom = function(options) { + if (this.isVisible()) { + if (!this.dom.tr.parentNode) { + var nextTr = this.parent._getNextTr(); + nextTr.parentNode.insertBefore(this.dom.tr, nextTr); + } + // this.dom.tr.style.display = ''; + + // update the counts in the text + this.dom.moreText.nodeValue = this._getShowMoreText(); + + // update left margin + this.dom.moreContents.style.marginLeft = (this.getLevel() + 2) * 24 + 'px'; + } + else { + if (this.dom.tr && this.dom.tr.parentNode) { + this.dom.tr.parentNode.removeChild(this.dom.tr); + } + + // this.dom.tr.style.display = 'none'; + } + }; + + ShowMoreNode.prototype._getShowMoreText = function() { + // TODO: implement in translate + var childs = this.type === 'array' ? 'items' : 'properties'; + return 'displaying ' + this.parent.maxVisibleChilds + + ' of ' + this.parent.childs.length + ' ' + childs + '. '; + }; + + /** + * Check whether the ShowMoreNode is currently visible. + * the ShowMoreNode is visible when it's parent has more childs than + * the current maxVisibleChilds + * @return {boolean} isVisible + */ + ShowMoreNode.prototype.isVisible = function () { + return this.parent.expanded && this.parent.childs.length > this.parent.maxVisibleChilds; + }; + + /** + * Handle an event. The event is caught centrally by the editor + * @param {Event} event + */ + ShowMoreNode.prototype.onEvent = function (event) { + var type = event.type; + // var target = event.target || event.srcElement; + // var dom = this.dom; + + // // highlight the append nodes parent + // var menu = dom.menu; + // if (target == menu) { + // if (type == 'mouseover') { + // this.editor.highlighter.highlight(this.parent); + // } + // else if (type == 'mouseout') { + // this.editor.highlighter.unhighlight(); + // } + // } + + // // context menu events + // if (type == 'click' && target == dom.menu) { + // var highlighter = this.editor.highlighter; + // highlighter.highlight(this.parent); + // highlighter.lock(); + // util.addClassName(dom.menu, 'jsoneditor-selected'); + // this.showContextMenu(dom.menu, function () { + // util.removeClassName(dom.menu, 'jsoneditor-selected'); + // highlighter.unlock(); + // highlighter.unhighlight(); + // }); + // } + + if (type === 'keydown') { + this.onKeyDown(event); + } + }; + + return ShowMoreNode; +} + +module.exports = showMoreNodeFactory; diff --git a/test/test_large_array.html b/test/test_large_array.html new file mode 100644 index 0000000..5c478f9 --- /dev/null +++ b/test/test_large_array.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + +

+ Switch editor mode using the mode box. + Note that the mode can be changed programmatically as well using the method + editor.setMode(mode), try it in the console of your browser. +

+ +
+
+
+ + + +