From 74b816a55466115ddafe6c212e7efc9e3a3fdcfd Mon Sep 17 00:00:00 2001 From: jos Date: Wed, 19 Jun 2019 11:16:30 +0200 Subject: [PATCH] Implemented sort button in code/text mode --- src/js/Node.js | 28 ++++++++++- src/js/showSortModal.js | 36 ++++++++------ src/js/textmode.js | 68 ++++++++++++++++++++++++++ src/js/treemode.js | 4 +- src/js/util.js | 95 ++++++++++++++++++++++++++++++++++++ test/test_code_mode.html | 89 +++++++++++++++++++++++++++++++++ test/util.test.js | 103 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 402 insertions(+), 21 deletions(-) create mode 100644 test/test_code_mode.html diff --git a/src/js/Node.js b/src/js/Node.js index 28540a3..efd01f4 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -4292,8 +4292,7 @@ Node.prototype.showContextMenu = function (anchor, onClose) { title: translate('sortTitle', {type: this.type}), className: 'jsoneditor-sort-asc', click: function () { - var anchor = node.editor.options.modalAnchor || DEFAULT_MODAL_ANCHOR; - showSortModal(node, anchor) + node.showSortModal() } }); } @@ -4455,6 +4454,31 @@ Node.prototype.showContextMenu = function (anchor, onClose) { menu.show(anchor, this.editor.frame); }; + +/** + * Show advanced sorting modal + */ +Node.prototype.showSortModal = function () { + var node = this; + var container = this.editor.options.modalAnchor || DEFAULT_MODAL_ANCHOR; + var paths = node.type === 'array' + ? node.getChildPaths() + : ['.']; + + showSortModal(container, { + paths: paths, + path: node.sortedBy ? node.sortedBy.path : paths[0], + direction: node.sortedBy ? node.sortedBy.direction : 'asc', + onSort: function (sortedBy) { + var path = sortedBy.path; + var pathArray = (path === '.') ? [] : path.split('.').slice(1); + + node.sortedBy = sortedBy + node.sort(pathArray, sortedBy.direction) + } + }) +} + /** * get the type of a value * @param {*} value diff --git a/src/js/showSortModal.js b/src/js/showSortModal.js index 920573a..17dee10 100644 --- a/src/js/showSortModal.js +++ b/src/js/showSortModal.js @@ -3,11 +3,24 @@ var translate = require('./i18n').translate; /** * Show advanced sorting modal - * @param {Node} node the node to be sorted * @param {HTMLElement} container The container where to center * the modal and create an overlay + * @param {Object} options + * Available options: + * - {Array} paths The available paths + * - {string} path The selected path + * - {'asc' | 'desc'} direction The selected direction + * - {function} onSort Callback function, + * invoked with an object + * containing the selected + * path and direction */ -function showSortModal (node, container) { +function showSortModal (container, options) { + var paths = options && options.paths || ['.'] + var selectedPath = options && options.path || paths[0] + var selectedDirection = options && options.direction || 'asc' + var onSort = options && options.onSort || function () {} + var content = '
' + '
' + translate('sort') + '
' + '
' + @@ -61,10 +74,6 @@ function showSortModal (node, container) { var field = modal.modalElem().querySelector('#field'); var direction = modal.modalElem().querySelector('#direction'); - var paths = node.type === 'array' - ? node.getChildPaths() - : ['.']; - paths.forEach(function (path) { var option = document.createElement('option'); option.text = path; @@ -77,8 +86,8 @@ function showSortModal (node, container) { direction.className = 'jsoneditor-button-group jsoneditor-button-group-value-' + direction.value; } - field.value = node.sortedBy ? node.sortedBy.path : paths[0]; - setDirection(node.sortedBy ? node.sortedBy.direction : 'asc'); + field.value = selectedPath || paths[0]; + setDirection(selectedDirection || 'asc'); direction.onclick = function (event) { setDirection(event.target.getAttribute('data-value')); @@ -90,15 +99,10 @@ function showSortModal (node, container) { modal.close(); - var path = field.value; - var pathArray = (path === '.') ? [] : path.split('.').slice(1); - - node.sortedBy = { - path: path, + onSort({ + path: field.value, direction: direction.value - }; - - node.sort(pathArray, direction.value) + }) }; if (form) { // form is not available when JSONEditor is created inside a form diff --git a/src/js/textmode.js b/src/js/textmode.js index e9ba2f0..9fb5f2a 100644 --- a/src/js/textmode.js +++ b/src/js/textmode.js @@ -1,13 +1,17 @@ 'use strict'; var ace = require('./ace'); +var translate = require('./i18n').translate; var ModeSwitcher = require('./ModeSwitcher'); +var showSortModal = require('./showSortModal'); +var showTransformModal = require('./showTransformModal'); var util = require('./util'); // create a mixin with the functions for text mode var textmode = {}; var DEFAULT_THEME = 'ace/theme/jsoneditor'; +var DEFAULT_MODAL_ANCHOR = document.body; // TODO: this constant is defined multiple times /** * Create a text editor @@ -47,6 +51,8 @@ textmode.create = function (container, options) { // setting default for textmode options.mainMenuBar = options.mainMenuBar !== false; + options.enableSort = options.enableSort !== false; + options.enableTransform = options.enableTransform !== false; this.options = options; @@ -160,6 +166,32 @@ textmode.create = function (container, options) { } }; + // create sort button + if (this.options.enableSort) { + var sort = document.createElement('button'); + sort.type = 'button'; + sort.className = 'jsoneditor-sort'; + sort.title = translate('sortTitleShort'); + sort.onclick = function () { + me._showSortModal() + }; + this.menu.appendChild(sort); + } + + // TODO + // // create transform button + // if (this.options.enableTransform) { + // var transform = document.createElement('button'); + // transform.type = 'button'; + // transform.title = translate('transformTitleShort'); + // transform.className = 'jsoneditor-transform'; + // transform.onclick = function () { + // var anchor = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR; + // showTransformModal(me.node, anchor) + // }; + // this.menu.appendChild(transform); + // } + // create repair button var buttonRepair = document.createElement('button'); buttonRepair.type = 'button'; @@ -400,6 +432,42 @@ textmode._onChange = function () { } }; +/** + * Open a sort modal + * @private + */ +textmode._showSortModal = function () { + var me = this; + var container = this.options.modalAnchor || DEFAULT_MODAL_ANCHOR; + var json = this.get() + var paths = Array.isArray(json) + ? util.getChildPaths(json) + : ['.']; + + showSortModal(container, { + paths: paths, + path: (me.sortedBy && util.contains(paths, me.sortedBy.path)) + ? me.sortedBy.path + : paths[0], + direction: me.sortedBy ? me.sortedBy.direction : 'asc', + onSort: function (sortedBy) { + if (Array.isArray(json)) { + var sortedJson = util.sort(json, sortedBy.path, sortedBy.direction); + + me.sortedBy = sortedBy + me.set(sortedJson); + } + + if (util.isObject(json)) { + var sortedJson = util.sortObjectKeys(json, sortedBy.direction); + + me.sortedBy = sortedBy; + me.set(sortedJson); + } + } + }) +} + /** * Handle text selection * Calculates the cursor position and selection range and updates menu diff --git a/src/js/treemode.js b/src/js/treemode.js index f02c8ff..8035209 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -10,7 +10,6 @@ var Node = require('./Node'); var ModeSwitcher = require('./ModeSwitcher'); var util = require('./util'); var autocomplete = require('./autocomplete'); -var showSortModal = require('./showSortModal'); var showTransformModal = require('./showTransformModal'); var translate = require('./i18n').translate; var setLanguages = require('./i18n').setLanguages; @@ -1022,8 +1021,7 @@ treemode._createFrame = function () { sort.className = 'jsoneditor-sort'; sort.title = translate('sortTitleShort'); sort.onclick = function () { - var anchor = editor.options.modalAnchor || DEFAULT_MODAL_ANCHOR; - showSortModal(editor.node, anchor) + editor.node.showSortModal() }; this.menu.appendChild(sort); } diff --git a/src/js/util.js b/src/js/util.js index 016d60c..ff4db6f 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -1,6 +1,7 @@ 'use strict'; require('./polyfills'); +var naturalSort = require('javascript-natural-sort'); var jsonlint = require('./assets/jsonlint/jsonlint'); var jsonMap = require('json-source-map'); var translate = require('./i18n').translate; @@ -1187,3 +1188,97 @@ exports.findUniqueName = function(name, existingPropNames) { return validName } + +/** + * Get the child paths of an array + * @param {JSON} json + * @param {boolean} [includeObjects=false] If true, object and array paths are returned as well + * @return {string[]} + */ +exports.getChildPaths = function (json, includeObjects) { + var pathsMap = {}; + + function getObjectChildPaths (json, pathsMap, rootPath, includeObjects) { + var isValue = !Array.isArray(json) && !exports.isObject(json) + + if (isValue || includeObjects) { + pathsMap[rootPath || '.'] = true; + } + + if (exports.isObject(json)) { + Object.keys(json).forEach(function (field) { + getObjectChildPaths(json[field], pathsMap, rootPath + '.' + field, includeObjects); + }); + } + } + + if (Array.isArray(json)) { + json.forEach(function (item) { + getObjectChildPaths(item, pathsMap, '', includeObjects); + }); + } + else { + pathsMap['.'] = true; + } + + return Object.keys(pathsMap).sort(); +} + +/** + * Sort object keys using natural sort + * @param {Array} array + * @param {String} [path] JSON pointer + * @param {'asc' | 'desc'} [direction] + */ +exports.sort = function (array, path, direction) { + var parsedPath = path && path !== '.' ? exports.parsePath(path) : [] + var sign = direction === 'desc' ? -1: 1 + + var sortedArray = array.slice() + sortedArray.sort(function (a, b) { + const aValue = exports.get(a, parsedPath); + const bValue = exports.get(b, parsedPath); + + return sign * (aValue > bValue ? 1 : aValue < bValue ? -1 : 0); + }) + + return sortedArray; +} + +/** + * Sort object keys using natural sort + * @param {Object} object + * @param {'asc' | 'desc'} [direction] + */ +exports.sortObjectKeys = function (object, direction) { + var sign = (direction === 'desc') ? -1 : 1; + var sortedFields = Object.keys(object).sort(function (a, b) { + return sign * naturalSort(a, b); + }); + + var sortedObject = {}; + sortedFields.forEach(function (field) { + sortedObject[field] = object[field]; + }); + + return sortedObject; +} + +/** + * Test whether a value is an Object + * @param {*} value + * @return {boolean} + */ +exports.isObject = function (value) { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** + * Helper function to test whether an array contains an item + * @param {Array} array + * @param {*} item + * @return {boolean} Returns true if `item` is in `array`, returns false otherwise. + */ +exports.contains = function (array, item) { + return array.indexOf(item) !== -1; +} diff --git a/test/test_code_mode.html b/test/test_code_mode.html new file mode 100644 index 0000000..c849fdb --- /dev/null +++ b/test/test_code_mode.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + +

+ 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. +

+ + +
+
+ + + + diff --git a/test/util.test.js b/test/util.test.js index 6c80ccf..3c5a7b4 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -250,6 +250,109 @@ describe('util', function () { }); }); + describe('getChildPaths', function () { + it('should extract all child paths of an array containing objects', function () { + var json = [ + { name: 'A', location: {latitude: 1, longitude: 2} }, + { name: 'B', location: {latitude: 1, longitude: 2} }, + { name: 'C', timestamp: 0 }, + ]; + + assert.deepStrictEqual(util.getChildPaths(json), [ + '.location.latitude', + '.location.longitude', + '.name', + '.timestamp', + ]) + }); + + it('should extract all child paths of an array containing objects, including objects', function () { + var json = [ + { name: 'A', location: {latitude: 1, longitude: 2} }, + { name: 'B', location: {latitude: 1, longitude: 2} }, + { name: 'C', timestamp: 0 }, + ]; + + assert.deepStrictEqual(util.getChildPaths(json, true), [ + '.', + '.location', + '.location.latitude', + '.location.longitude', + '.name', + '.timestamp', + ]) + }); + + it('should extract all child paths of an array containing values', function () { + var json = [ 1, 2, 3 ]; + + assert.deepStrictEqual(util.getChildPaths(json), [ + '.' + ]) + }); + + it('should extract all child paths of a non-array', function () { + assert.deepStrictEqual(util.getChildPaths({a: 2, b: {c: 3}}), ['.']) + assert.deepStrictEqual(util.getChildPaths('foo'), ['.']) + assert.deepStrictEqual(util.getChildPaths(123), ['.']) + }); + }) + + it('should test whether something is an object', function () { + assert.strictEqual(util.isObject({}), true); + assert.strictEqual(util.isObject(new Date()), true); + assert.strictEqual(util.isObject([]), false); + assert.strictEqual(util.isObject(2), false); + assert.strictEqual(util.isObject(null), false); + assert.strictEqual(util.isObject(undefined), false); + assert.strictEqual(util.isObject(), false); + }); + + describe('sort', function () { + it('should sort an array', function () { + var array = [4, 1, 10, 2]; + assert.deepStrictEqual(util.sort(array), [1, 2, 4, 10]); + assert.deepStrictEqual(util.sort(array, '.', 'desc'), [10, 4, 2, 1]); + }); + + it('should sort an array containing objects', function () { + var array = [ + { value: 4 }, + { value: 1 }, + { value: 10 }, + { value: 2 } + ]; + + assert.deepStrictEqual(util.sort(array, '.value'), [ + { value: 1 }, + { value: 2 }, + { value: 4 }, + { value: 10 } + ]); + + assert.deepStrictEqual(util.sort(array, '.value', 'desc'), [ + { value: 10 }, + { value: 4 }, + { value: 2 }, + { value: 1 } + ]); + }); + }); + + describe('sortObjectKeys', function () { + it('should sort the keys of an object', function () { + var object = { + c: 'c', + a: 'a', + b: 'b' + } + assert.strictEqual(JSON.stringify(object), '{"c":"c","a":"a","b":"b"}') + assert.strictEqual(JSON.stringify(util.sortObjectKeys(object)), '{"a":"a","b":"b","c":"c"}') + assert.strictEqual(JSON.stringify(util.sortObjectKeys(object, 'asc')), '{"a":"a","b":"b","c":"c"}') + assert.strictEqual(JSON.stringify(util.sortObjectKeys(object, 'desc')), '{"c":"c","b":"b","a":"a"}') + }); + }); + it('should find a unique name', function () { assert.strictEqual(util.findUniqueName('other', [ 'a',