From f45fefe38fff5e8abb2c0591a49c3fa68423cb4d Mon Sep 17 00:00:00 2001 From: jos Date: Tue, 12 Jan 2016 18:11:56 +0100 Subject: [PATCH] Implemented debouncing of keyboard input, resulting in much less history actions whilst typing --- HISTORY.md | 3 +++ src/js/JSONEditor.js | 5 ++++- src/js/Node.js | 29 +++++++++++++++++++++----- src/js/textmode.js | 4 +--- src/js/treemode.js | 7 ++----- src/js/util.js | 47 +++++++++++++++++++++++++++++++++++++++++++ test/test_schema.html | 1 - 7 files changed, 81 insertions(+), 15 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index cfd733f..58ec3a9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,6 +10,8 @@ https://github.com/josdejong/jsoneditor - Implemented #197: display an error in case of duplicate keys in an object. - Implemented #183: display a checkbox left from boolean values, so you can easily switch between true/false. +- Implemented debouncing of keyboard input, resulting in much less history + actions whilst typing. - Added a minimalist bundle to the `dist` folder, excluding `ace` and `ajv`. - Fixed #222: editor throwing `onChange` events when switching mode. - Fixed an error throw when switching to mode "code" via the menu. @@ -17,6 +19,7 @@ https://github.com/josdejong/jsoneditor from `Shift+Arrow Up/Down` to `Ctrl+Shift+Arrow Up/Down`. + ## 2015-12-31, version 5.0.1 - Fixed a bug in positioning of the context menu for multiple selected nodes. diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index 8b97938..21f08bd 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -75,7 +75,7 @@ function JSONEditor (container, options, json) { if (options) { var VALID_OPTIONS = [ 'ace', 'theme', - 'ajv', 'schema', 'debounceInterval', + 'ajv', 'schema', 'onChange', 'onEditable', 'onError', 'onModeChange', 'escapeUnicode', 'history', 'mode', 'modes', 'name', 'indentation' ]; @@ -110,6 +110,9 @@ function JSONEditor (container, options, json) { */ JSONEditor.modes = {}; +// debounce interval for JSON schema vaidation in milliseconds +JSONEditor.prototype.DEBOUNCE_INTERVAL = 150; + /** * Create the JSONEditor * @param {Element} container Container element diff --git a/src/js/Node.js b/src/js/Node.js index 757ae79..ad89fff 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -28,9 +28,12 @@ function Node (editor, params) { this.setValue(null); } - this._debouncedGetDomValue = util.debounce(this._getDomValue.bind(this), 100); + this._debouncedGetDomValue = util.debounce(this._getDomValue.bind(this), Node.prototype.DEBOUNCE_INTERVAL); } +// debounce interval for keyboard input in milliseconds +Node.prototype.DEBOUNCE_INTERVAL = 150; + /** * Determine whether the field and/or value of this node are editable * @private @@ -1115,12 +1118,28 @@ Node.prototype._getDomValue = function(silent) { if (value !== this.value) { var oldValue = this.value; this.value = value; + var selection = this.editor.getSelection(); + var undoDiff = util.textDiff(value, oldValue); + var redoDiff = util.textDiff(oldValue, value); + console.log('selection', selection, oldValue, value, util.textDiff(oldValue, value), util.textDiff(value, oldValue)) this.editor._onAction('editValue', { 'node': this, 'oldValue': oldValue, 'newValue': value, - 'oldSelection': this.editor.selection, - 'newSelection': this.editor.getSelection() + 'oldSelection': util.extend({}, selection, { + range: { + container: selection.range.container, + startOffset: undoDiff.start, + endOffset: undoDiff.end + } + }), + 'newSelection': util.extend({}, selection, { + range: { + container: selection.range.container, + startOffset: redoDiff.start, + endOffset: redoDiff.end + } + }) }); } } @@ -2080,7 +2099,7 @@ Node.prototype.onEvent = function (event) { break; case 'input': - this._getDomValue(true); + this._debouncedGetDomValue(true); this._updateDomValue(); break; @@ -2098,7 +2117,7 @@ Node.prototype.onEvent = function (event) { break; case 'keyup': - this._getDomValue(true); + this._debouncedGetDomValue(true); this._updateDomValue(); break; diff --git a/src/js/textmode.js b/src/js/textmode.js index 77b00aa..2e09695 100644 --- a/src/js/textmode.js +++ b/src/js/textmode.js @@ -71,9 +71,7 @@ textmode.create = function (container, options) { this.validateSchema = null; // create a debounced validate function - var wait = this.options.debounceInterval; - var immediate = true; - this._debouncedValidate = util.debounce(this.validate.bind(this), wait, immediate); + this._debouncedValidate = util.debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL); this.width = container.clientWidth; this.height = container.clientHeight; diff --git a/src/js/treemode.js b/src/js/treemode.js index 0285803..e2d0986 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -75,8 +75,7 @@ treemode._setOptions = function (options) { history: true, mode: 'tree', name: undefined, // field name of root node - schema: null, - debounceInterval: 100 // debounce interval for schema validation in milliseconds + schema: null }; // copy all options @@ -92,9 +91,7 @@ treemode._setOptions = function (options) { this.setSchema(this.options.schema); // create a debounced validate function - var wait = this.options.debounceInterval; - var immediate = true; - this._debouncedValidate = util.debounce(this.validate.bind(this), wait, immediate); + this._debouncedValidate = util.debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL); }; // node currently being edited diff --git a/src/js/util.js b/src/js/util.js index a290135..071d256 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -695,3 +695,50 @@ exports.debounce = function debounce(func, wait, immediate) { if (callNow) func.apply(context, args); }; }; + +/** + * Determines the difference between two texts. + * Can only detect one removed or inserted block of characters. + * @param {string} oldText + * @param {string} newText + * @return {{start: number, end: number}} Returns the start and end + * of the changed part in newText. + */ +exports.textDiff = function textDiff(oldText, newText) { + var len = newText.length; + var start = 0; + var oldEnd = oldText.length; + var newEnd = newText.length; + + while (newText.charAt(start) === oldText.charAt(start) + && start < len) { + start++; + } + + while (newText.charAt(newEnd - 1) === oldText.charAt(oldEnd - 1) + && newEnd > start && oldEnd > 0) { + newEnd--; + oldEnd--; + } + + return {start: start, end: newEnd}; +}; + +/** + * Extend object a with the properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Object} a + * @param {... Object} b + * @return {Object} a + */ +exports.extend = function (a, b) { + for (var i = 1; i < arguments.length; i++) { + var other = arguments[i]; + for (var prop in other) { + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } + } + } + return a; +}; diff --git a/test/test_schema.html b/test/test_schema.html index e48dee0..531ebdd 100644 --- a/test/test_schema.html +++ b/test/test_schema.html @@ -87,7 +87,6 @@ console.log('json', json); console.log('schema', schema); - console.log('string', JSON.stringify(json));