diff --git a/package-lock.json b/package-lock.json index bdc504d..be40c61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2789,6 +2789,11 @@ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", "integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=" }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "json-loader": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.4.tgz", diff --git a/package.json b/package.json index a93963a..d2961d9 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "ajv": "5.5.2", "brace": "0.11.0", "javascript-natural-sort": "0.7.1", + "jmespath": "0.15.0", "picomodal": "3.0.0" }, "devDependencies": { diff --git a/src/css/contextmenu.css b/src/css/contextmenu.css index 2a80f2f..d8287ee 100644 --- a/src/css/contextmenu.css +++ b/src/css/contextmenu.css @@ -177,6 +177,14 @@ div.jsoneditor-contextmenu button.jsoneditor-sort-desc:focus > div.jsoneditor-ic background-position: -192px 0; } +div.jsoneditor-contextmenu button.jsoneditor-transform > div.jsoneditor-icon { + background-position: -216px -24px; +} +div.jsoneditor-contextmenu button.jsoneditor-transform:hover > div.jsoneditor-icon, +div.jsoneditor-contextmenu button.jsoneditor-transform:focus > div.jsoneditor-icon { + background-position: -216px 0; +} + /* ContextMenu - sub menu */ div.jsoneditor-contextmenu ul li button.jsoneditor-selected, @@ -278,6 +286,9 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { border-radius: 2px !important; padding: 45px 15px 15px 15px !important; box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3) !important; + + color: #4d4d4d; + line-height: 1.3em; } .jsoneditor-modal .pico-modal-header { @@ -305,7 +316,6 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { vertical-align: middle; font-size: 10pt; font-family: arial, sans-serif; - color: #4d4d4d; } .jsoneditor-modal table td:first-child { @@ -338,6 +348,15 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { cursor: pointer; } +.jsoneditor-modal input { + padding: 4px 20px; +} + +.jsoneditor-modal input[type="text"] { + padding: 4px; + cursor: inherit; +} + .jsoneditor-modal .jsoneditor-select-wrapper { position: relative; } @@ -398,7 +417,3 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { border-color: #3883fa; color: white; } - -.jsoneditor-modal input { - padding: 4px 20px; -} diff --git a/src/css/img/jsoneditor-icons.svg b/src/css/img/jsoneditor-icons.svg index 9e48bdc..ad25fbc 100644 --- a/src/css/img/jsoneditor-icons.svg +++ b/src/css/img/jsoneditor-icons.svg @@ -7,7 +7,7 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="216" + width="240" height="144" id="svg4136" version="1.1" @@ -30,7 +30,7 @@ + + + + + + id="g4299" + style="stroke:none"> + x="7.0000048" + y="10.999998" + width="9.9999924" + height="1.9999986" + id="svg_1-1" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0" /> + x="11.000005" + y="7.0000114" + width="1.9999955" + height="9.9999838" + id="svg_1-1-1" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0" /> + + + x="7.0000048" + y="10.999998" + width="9.9999924" + height="1.9999986" + id="svg_1-1-0" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0" /> - - - - - - - - - + x="11.000005" + y="7.0000114" + width="1.9999955" + height="9.9999838" + id="svg_1-1-1-9" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0" /> + + + + + + x="198" + y="10.999999" + width="7.9999909" + height="1.9999965" + id="svg_1-7-5-3" /> + id="rect4374" + height="1.9999946" + width="11.999995" + y="7.0000005" + x="198" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" /> - - - - - - - - - - - - - - - - - - - - - - + id="rect4376" + height="1.9999995" + width="3.9999928" + y="14.999996" + x="198" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" /> + + + + + + + + + + + + + + + + + + + + + diff --git a/src/js/History.js b/src/js/History.js index a363700..3a5f275 100644 --- a/src/js/History.js +++ b/src/js/History.js @@ -120,19 +120,19 @@ function History (editor) { } }, - 'sort': { + 'transform': { 'undo': function (params) { var node = params.node; node.hideChilds(); node.childs = params.oldChilds; - node._updateDomIndexes(); + node.updateDom({updateIndexes: true}); node.showChilds(); }, 'redo': function (params) { var node = params.node; node.hideChilds(); node.childs = params.newChilds; - node._updateDomIndexes(); + node.updateDom({updateIndexes: true}); node.showChilds(); } } diff --git a/src/js/Node.js b/src/js/Node.js index ca53f56..c17921e 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -1,10 +1,12 @@ 'use strict'; +var jmespath = require('jmespath'); var naturalSort = require('javascript-natural-sort'); var ContextMenu = require('./ContextMenu'); var appendNodeFactory = require('./appendNodeFactory'); var showMoreNodeFactory = require('./showMoreNodeFactory'); var showSortModal = require('./showSortModal'); +var showTransformModal = require('./showTransformModal'); var util = require('./util'); var translate = require('./i18n').translate; @@ -326,17 +328,16 @@ Node.prototype.getField = function() { */ Node.prototype.setValue = function(value, type) { var childValue, child, visible; + var notUpdateDom = false; // first clear all current childs (if any) var childs = this.childs; if (childs) { while (childs.length) { - this.removeChild(childs[0]); + this.removeChild(childs[0], notUpdateDom); } } - // TODO: remove the DOM of this Node - this.type = this._getType(value); // check if type corresponds with the provided type @@ -362,7 +363,7 @@ Node.prototype.setValue = function(value, type) { value: childValue }); visible = i < this.MAX_VISIBLE_CHILDS; - this.appendChild(child, visible); + this.appendChild(child, visible, notUpdateDom); } } this.value = ''; @@ -381,7 +382,7 @@ Node.prototype.setValue = function(value, type) { value: childValue }); visible = i < this.MAX_VISIBLE_CHILDS; - this.appendChild(child, visible); + this.appendChild(child, visible, notUpdateDom); } i++; } @@ -398,6 +399,8 @@ Node.prototype.setValue = function(value, type) { this.childs = undefined; this.value = value; } + + this.updateDom({'updateIndexes': true}); this.previousValue = this.value; }; @@ -664,9 +667,12 @@ Node.prototype.expandTo = function() { * Add a new child to the node. * Only applicable when Node value is of type array or object * @param {Node} node - * @param {boolean} [visible] If true, the child will be rendered + * @param {boolean} [visible] If true (default), the child will be rendered + * @param {boolean} [updateDom] If true (default), the DOM of both parent + * node and appended node will be updated + * (child count, indexes) */ -Node.prototype.appendChild = function(node, visible) { +Node.prototype.appendChild = function(node, visible, updateDom) { if (this._hasChilds()) { // adjust the link to the parent node.setParent(this); @@ -690,8 +696,10 @@ Node.prototype.appendChild = function(node, visible) { this.visibleChilds++; } - this.updateDom({'updateIndexes': true}); - node.updateDom({'recurse': true}); + if (updateDom !== false) { + this.updateDom({'updateIndexes': true}); + node.updateDom({'recurse': true}); + } } }; @@ -1105,15 +1113,19 @@ Node.prototype._move = function(node, beforeNode) { * Remove a child from the node. * Only applicable when Node value is of type array or object * @param {Node} node The child node to be removed; + * @param {boolean} [updateDom] If true (default), the DOM of the parent + * node will be updated (like child count) * @return {Node | undefined} node The removed node on success, * else undefined */ -Node.prototype.removeChild = function(node) { +Node.prototype.removeChild = function(node, updateDom) { if (this.childs) { var index = this.childs.indexOf(node); if (index !== -1) { - this.visibleChilds--; + if (index < this.visibleChilds && this.expanded) { + this.visibleChilds--; + } node.hide(); @@ -1124,7 +1136,9 @@ Node.prototype.removeChild = function(node) { var removedNode = this.childs.splice(index, 1)[0]; removedNode.parent = null; - this.updateDom({'updateIndexes': true}); + if (updateDom !== false) { + this.updateDom({'updateIndexes': true}); + } return removedNode; } @@ -3147,7 +3161,37 @@ Node.prototype.sort = function (path, direction) { // update the index numbering this._updateDomIndexes(); - this.editor._onAction('sort', { + this.editor._onAction('transform', { + node: this, + oldChilds: oldChilds, + newChilds: this.childs + }); + + this.showChilds(); +}; + +/** + * Transform the node given a JMESPath query. + * @param {String} query JMESPath query to apply + * @private + */ +Node.prototype.transform = function (query) { + if (!this._hasChilds()) { + return; + } + + this.hideChilds(); // sorting is faster when the childs are not attached to the dom + + // copy the childs array (the old one will be kept for an undo action + var oldChilds = this.childs; + this.childs = this.childs.concat(); + + // apply the JMESPath query + var transformed = jmespath.search(this.getValue(), query); + + this.setValue(transformed); + + this.editor._onAction('transform', { node: this, oldChilds: oldChilds, newChilds: this.childs @@ -3583,12 +3627,21 @@ Node.prototype.showContextMenu = function (anchor, onClose) { if (this._hasChilds()) { items.push({ text: translate('sort'), - title: translate('sortTitle') + this.type, + title: translate('sortTitle', {type: this.type}), className: 'jsoneditor-sort-asc', click: function () { showSortModal(node, node.editor.frame) } }); + + items.push({ + text: translate('transform'), + title: translate('transformTitle', {type: this.type}), + className: 'jsoneditor-transform', + click: function () { + showTransformModal(node, node.editor.frame) + } + }); } if (this.parent && this.parent._hasChilds()) { diff --git a/src/js/i18n.js b/src/js/i18n.js index 62a1f9b..1ef3e8b 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -38,7 +38,7 @@ var _defs = { 'showMore': 'show more', 'showMoreStatus': 'displaying ${visibleChilds} of ${totalChilds} items.', 'sort': 'Sort', - 'sortTitle': 'Sort the childs of this ', + 'sortTitle': 'Sort the childs of this ${type}', 'sortFieldLabel': 'Field:', 'sortDirectionLabel': 'Direction:', 'sortFieldTitle': 'Select the nested field by which to sort the array or object', @@ -47,6 +47,10 @@ var _defs = { 'sortDescending': 'Descending', 'sortDescendingTitle': 'Sort the selected field in descending order', 'string': 'String', + 'transform': 'Transform', + 'transformTitle': 'Filter, sort, or transform the childs of this ${type}', + 'transformQueryTitle': 'Enter a JMESPath query', + 'transformQueryLabel': 'Query', 'type': 'Type', 'typeTitle': 'Change the type of this field', 'openUrl': 'Ctrl+Click or Ctrl+Enter to open url in new window', @@ -103,7 +107,7 @@ var _defs = { // TODO: correctly translate showMoreStatus 'showMoreStatus': 'exibindo ${visibleChilds} de ${totalChilds} itens.', 'sort': 'Organizar', - 'sortTitle': 'Organizar os filhos deste ', + 'sortTitle': 'Organizar os filhos deste ${type}', // TODO: correctly translate sortFieldLabel 'sortFieldLabel': 'Field:', // TODO: correctly translate sortDirectionLabel diff --git a/src/js/showTransformModal.js b/src/js/showTransformModal.js new file mode 100644 index 0000000..35b79d3 --- /dev/null +++ b/src/js/showTransformModal.js @@ -0,0 +1,66 @@ +var picoModal = require('picomodal'); +var translate = require('./i18n').translate; + +/** + * Show advanced filter and transform modal using JMESPath + * @param {Node} node the node to be transformed + * @param {HTMLElement} container The container where to center + * the modal and create an overlay + */ +function showTransformModal (node, container) { + var content = '
' + + '
' + translate('transform') + '
' + + '
' + + '

' + + 'Enter a JMESPath query to filter, sort, or transform the JSON data. ' + + 'To learn JMESPath, go to the interactive tutorial.' + + '

' + + '' + + '' + + '' + + ' ' + + ' ' + + '' + + '' + + '' + + '' + + '' + + '
' + translate('transformQueryLabel') + ' ' + + ' ' + + '
' + + ' ' + + '
' + + '
' + + '
'; + + picoModal({ + parent: container, + content: content, + overlayClass: 'jsoneditor-modal-overlay', + modalClass: 'jsoneditor-modal' + }) + .afterCreate(function (modal) { + var form = modal.modalElem().querySelector('form'); + var ok = modal.modalElem().querySelector('#ok'); + var query = modal.modalElem().querySelector('#query'); + + ok.onclick = function (event) { + event.preventDefault(); + event.stopPropagation(); + + modal.close(); + + node.transform(query.value) + }; + + if (form) { // form is not available when JSONEditor is created inside a form + form.onsubmit = ok.onclick; + } + }) + .afterClose(function (modal) { + modal.destroy(); + }) + .show(); +} + +module.exports = showTransformModal;