Implementing "show more" for large arrays (WIP)

This commit is contained in:
jos 2018-05-06 19:42:19 +02:00
parent 358ef3086c
commit 904110d141
6 changed files with 377 additions and 31 deletions

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -0,0 +1,72 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
<script src="../dist/jsoneditor.js"></script>
<style type="text/css">
body {
font: 10.5pt arial;
color: #4d4d4d;
line-height: 150%;
width: 500px;
padding-left: 40px;
}
code {
background-color: #f5f5f5;
}
#jsoneditor {
width: 500px;
height: 500px;
}
</style>
</head>
<body>
<p>
Switch editor mode using the mode box.
Note that the mode can be changed programmatically as well using the method
<code>editor.setMode(mode)</code>, try it in the console of your browser.
</p>
<form>
<div id="jsoneditor"></div>
</form>
<script>
var container = document.getElementById('jsoneditor');
var options = {
mode: 'tree',
modes: ['code', 'form', 'text', 'tree', 'view'], // allowed modes
onError: function (err) {
console.error(err);
alert(err.toString());
},
onChange: function () {
console.log('change');
},
indentation: 4,
escapeUnicode: true
};
var json = {
array: [],
object: { a: 2, b: 3}
};
for (var i = 0; i < 10000; i++) {
json.array.push({
name: 'Item ' + i,
index: i,
time: new Date().toISOString()
});
}
var editor = new JSONEditor(container, options, json);
</script>
</body>
</html>