Implemented history in preview mode

This commit is contained in:
jos 2019-07-17 12:17:08 +02:00
commit e3e3886da8
5 changed files with 577 additions and 380 deletions

View File

@ -1,337 +1,88 @@
'use strict';
var util = require('./util');
/** /**
* @constructor History * Keep track on any history, be able
* Store action history, enables undo and redo * @param {function} onChange
* @param {JSONEditor} editor * @param {function} calculateItemSize
* @param {number} limit Maximum size of all items in history
* @constructor
*/ */
function History (editor) { function History (onChange, calculateItemSize, limit) {
this.editor = editor; this.onChange = onChange;
this.history = []; this.calculateItemSize = calculateItemSize || function () {
this.index = -1; return 1;
this.clear();
// helper function to find a Node from a path
function findNode(path) {
return editor.node.findNodeByInternalPath(path)
}
// map with all supported actions
this.actions = {
'editField': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
var node = parentNode.childs[params.index];
node.updateField(params.oldValue);
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
var node = parentNode.childs[params.index];
node.updateField(params.newValue);
}
},
'editValue': {
'undo': function (params) {
findNode(params.path).updateValue(params.oldValue);
},
'redo': function (params) {
findNode(params.path).updateValue(params.newValue);
}
},
'changeType': {
'undo': function (params) {
findNode(params.path).changeType(params.oldType);
},
'redo': function (params) {
findNode(params.path).changeType(params.newType);
}
},
'appendNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
params.paths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
params.nodes.forEach(function (node) {
parentNode.appendChild(node);
});
}
},
'insertBeforeNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
params.paths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
var beforeNode = findNode(params.beforePath);
params.nodes.forEach(function (node) {
parentNode.insertBefore(node, beforeNode);
});
}
},
'insertAfterNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
params.paths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
var afterNode = findNode(params.afterPath);
params.nodes.forEach(function (node) {
parentNode.insertAfter(node, afterNode);
afterNode = node;
});
}
},
'removeNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
var beforeNode = parentNode.childs[params.index] || parentNode.append;
params.nodes.forEach(function (node) {
parentNode.insertBefore(node, beforeNode);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
params.paths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
}
},
'duplicateNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
params.clonePaths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
var afterNode = findNode(params.afterPath);
var nodes = params.paths.map(findNode);
nodes.forEach(function (node) {
var clone = node.clone();
if (parentNode.type === 'object') {
var existingFieldNames = parentNode.getFieldNames();
clone.field = util.findUniqueName(node.field, existingFieldNames);
}
parentNode.insertAfter(clone, afterNode);
afterNode = clone;
});
}
},
'moveNodes': {
'undo': function (params) {
var oldParentNode = findNode(params.oldParentPath);
var newParentNode = findNode(params.newParentPath);
var oldBeforeNode = oldParentNode.childs[params.oldIndex] || oldParentNode.append;
// first copy the nodes, then move them
var nodes = newParentNode.childs.slice(params.newIndex, params.newIndex + params.count);
nodes.forEach(function (node, index) {
node.field = params.fieldNames[index];
oldParentNode.moveBefore(node, oldBeforeNode);
});
// This is a hack to work around an issue that we don't know tha original
// path of the new parent after dragging, as the node is already moved at that time.
if (params.newParentPathRedo === null) {
params.newParentPathRedo = newParentNode.getInternalPath();
}
},
'redo': function (params) {
var oldParentNode = findNode(params.oldParentPathRedo);
var newParentNode = findNode(params.newParentPathRedo);
var newBeforeNode = newParentNode.childs[params.newIndexRedo] || newParentNode.append;
// first copy the nodes, then move them
var nodes = oldParentNode.childs.slice(params.oldIndexRedo, params.oldIndexRedo + params.count);
nodes.forEach(function (node, index) {
node.field = params.fieldNames[index];
newParentNode.moveBefore(node, newBeforeNode);
});
}
},
'sort': {
'undo': function (params) {
var node = findNode(params.path);
node.hideChilds();
node.childs = params.oldChilds;
node.updateDom({updateIndexes: true});
node.showChilds();
},
'redo': function (params) {
var node = findNode(params.path);
node.hideChilds();
node.childs = params.newChilds;
node.updateDom({updateIndexes: true});
node.showChilds();
}
},
'transform': {
'undo': function (params) {
findNode(params.path).setInternalValue(params.oldValue);
// TODO: would be nice to restore the state of the node and childs
},
'redo': function (params) {
findNode(params.path).setInternalValue(params.newValue);
// TODO: would be nice to restore the state of the node and childs
}
}
// TODO: restore the original caret position and selection with each undo
// TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
}; };
this.limit = limit;
this.items = [];
this.index = -1;
} }
/** History.prototype.add = function (item) {
* The method onChange is executed when the History is changed, and can // limit number of items in history so that the total size doesn't
* be overloaded. // always keep at least one item in memory
*/ while (this._calculateHistorySize() > this.limit && this.items.length > 1) {
History.prototype.onChange = function () {}; this.items.shift();
this.index--;
}
/** // cleanup any redo action that are not valid anymore
* Add a new action to the history this.items = this.items.slice(0, this.index + 1);
* @param {String} action The executed action. Available actions: "editField",
* "editValue", "changeType", "appendNode", this.items.push(item);
* "removeNode", "duplicateNode", "moveNode"
* @param {Object} params Object containing parameters describing the change.
* The parameters in params depend on the action (for
* example for "editValue" the Node, old value, and new
* value are provided). params contains all information
* needed to undo or redo the action.
*/
History.prototype.add = function (action, params) {
this.index++; this.index++;
this.history[this.index] = {
'action': action,
'params': params,
'timestamp': new Date()
};
// remove redo actions which are invalid now
if (this.index < this.history.length - 1) {
this.history.splice(this.index + 1, this.history.length - this.index - 1);
}
// fire onchange event
this.onChange(); this.onChange();
}; };
/** History.prototype._calculateHistorySize = function () {
* Clear history var calculateItemSize = this.calculateItemSize;
*/ var totalSize = 0;
History.prototype.clear = function () {
this.history = [];
this.index = -1;
// fire onchange event this.items.forEach(item => {
this.onChange(); totalSize += calculateItemSize(item);
}; });
/** return totalSize;
* Check if there is an action available for undo }
* @return {Boolean} canUndo
*/
History.prototype.canUndo = function () {
return (this.index >= 0);
};
/**
* Check if there is an action available for redo
* @return {Boolean} canRedo
*/
History.prototype.canRedo = function () {
return (this.index < this.history.length - 1);
};
/**
* Undo the last action
*/
History.prototype.undo = function () { History.prototype.undo = function () {
if (this.canUndo()) { if (!this.canUndo()) {
var obj = this.history[this.index]; return;
if (obj) {
var action = this.actions[obj.action];
if (action && action.undo) {
action.undo(obj.params);
if (obj.params.oldSelection) {
try {
this.editor.setDomSelection(obj.params.oldSelection);
}
catch (err) {
console.error(err);
}
}
}
else {
console.error(new Error('unknown action "' + obj.action + '"'));
}
} }
this.index--; this.index--;
// fire onchange event
this.onChange(); this.onChange();
}
return this.items[this.index];
}; };
/**
* Redo the last action
*/
History.prototype.redo = function () { History.prototype.redo = function () {
if (this.canRedo()) { if (!this.canRedo()) {
return;
}
this.index++; this.index++;
var obj = this.history[this.index];
if (obj) {
var action = this.actions[obj.action];
if (action && action.redo) {
action.redo(obj.params);
if (obj.params.newSelection) {
try {
this.editor.setDomSelection(obj.params.newSelection);
}
catch (err) {
console.error(err);
}
}
}
else {
console.error(new Error('unknown action "' + obj.action + '"'));
}
}
// fire onchange event
this.onChange(); this.onChange();
}
return this.items[this.index];
}; };
/** History.prototype.canUndo = function () {
* Destroy history return this.index > 0;
*/ };
History.prototype.destroy = function () {
this.editor = null;
this.history = []; History.prototype.canRedo = function () {
return this.index < this.items.length - 1;
};
History.prototype.clear = function () {
this.items = [];
this.index = -1; this.index = -1;
this.onChange();
}; };
module.exports = History; module.exports = History;

337
src/js/NodeHistory.js Normal file
View File

@ -0,0 +1,337 @@
'use strict';
var util = require('./util');
/**
* @constructor History
* Store action history, enables undo and redo
* @param {JSONEditor} editor
*/
function NodeHistory (editor) {
this.editor = editor;
this.history = [];
this.index = -1;
this.clear();
// helper function to find a Node from a path
function findNode(path) {
return editor.node.findNodeByInternalPath(path)
}
// map with all supported actions
this.actions = {
'editField': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
var node = parentNode.childs[params.index];
node.updateField(params.oldValue);
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
var node = parentNode.childs[params.index];
node.updateField(params.newValue);
}
},
'editValue': {
'undo': function (params) {
findNode(params.path).updateValue(params.oldValue);
},
'redo': function (params) {
findNode(params.path).updateValue(params.newValue);
}
},
'changeType': {
'undo': function (params) {
findNode(params.path).changeType(params.oldType);
},
'redo': function (params) {
findNode(params.path).changeType(params.newType);
}
},
'appendNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
params.paths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
params.nodes.forEach(function (node) {
parentNode.appendChild(node);
});
}
},
'insertBeforeNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
params.paths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
var beforeNode = findNode(params.beforePath);
params.nodes.forEach(function (node) {
parentNode.insertBefore(node, beforeNode);
});
}
},
'insertAfterNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
params.paths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
var afterNode = findNode(params.afterPath);
params.nodes.forEach(function (node) {
parentNode.insertAfter(node, afterNode);
afterNode = node;
});
}
},
'removeNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
var beforeNode = parentNode.childs[params.index] || parentNode.append;
params.nodes.forEach(function (node) {
parentNode.insertBefore(node, beforeNode);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
params.paths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
}
},
'duplicateNodes': {
'undo': function (params) {
var parentNode = findNode(params.parentPath);
params.clonePaths.map(findNode).forEach(function (node) {
parentNode.removeChild(node);
});
},
'redo': function (params) {
var parentNode = findNode(params.parentPath);
var afterNode = findNode(params.afterPath);
var nodes = params.paths.map(findNode);
nodes.forEach(function (node) {
var clone = node.clone();
if (parentNode.type === 'object') {
var existingFieldNames = parentNode.getFieldNames();
clone.field = util.findUniqueName(node.field, existingFieldNames);
}
parentNode.insertAfter(clone, afterNode);
afterNode = clone;
});
}
},
'moveNodes': {
'undo': function (params) {
var oldParentNode = findNode(params.oldParentPath);
var newParentNode = findNode(params.newParentPath);
var oldBeforeNode = oldParentNode.childs[params.oldIndex] || oldParentNode.append;
// first copy the nodes, then move them
var nodes = newParentNode.childs.slice(params.newIndex, params.newIndex + params.count);
nodes.forEach(function (node, index) {
node.field = params.fieldNames[index];
oldParentNode.moveBefore(node, oldBeforeNode);
});
// This is a hack to work around an issue that we don't know tha original
// path of the new parent after dragging, as the node is already moved at that time.
if (params.newParentPathRedo === null) {
params.newParentPathRedo = newParentNode.getInternalPath();
}
},
'redo': function (params) {
var oldParentNode = findNode(params.oldParentPathRedo);
var newParentNode = findNode(params.newParentPathRedo);
var newBeforeNode = newParentNode.childs[params.newIndexRedo] || newParentNode.append;
// first copy the nodes, then move them
var nodes = oldParentNode.childs.slice(params.oldIndexRedo, params.oldIndexRedo + params.count);
nodes.forEach(function (node, index) {
node.field = params.fieldNames[index];
newParentNode.moveBefore(node, newBeforeNode);
});
}
},
'sort': {
'undo': function (params) {
var node = findNode(params.path);
node.hideChilds();
node.childs = params.oldChilds;
node.updateDom({updateIndexes: true});
node.showChilds();
},
'redo': function (params) {
var node = findNode(params.path);
node.hideChilds();
node.childs = params.newChilds;
node.updateDom({updateIndexes: true});
node.showChilds();
}
},
'transform': {
'undo': function (params) {
findNode(params.path).setInternalValue(params.oldValue);
// TODO: would be nice to restore the state of the node and childs
},
'redo': function (params) {
findNode(params.path).setInternalValue(params.newValue);
// TODO: would be nice to restore the state of the node and childs
}
}
// TODO: restore the original caret position and selection with each undo
// TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
};
}
/**
* The method onChange is executed when the History is changed, and can
* be overloaded.
*/
NodeHistory.prototype.onChange = function () {};
/**
* Add a new action to the history
* @param {String} action The executed action. Available actions: "editField",
* "editValue", "changeType", "appendNode",
* "removeNode", "duplicateNode", "moveNode"
* @param {Object} params Object containing parameters describing the change.
* The parameters in params depend on the action (for
* example for "editValue" the Node, old value, and new
* value are provided). params contains all information
* needed to undo or redo the action.
*/
NodeHistory.prototype.add = function (action, params) {
this.index++;
this.history[this.index] = {
'action': action,
'params': params,
'timestamp': new Date()
};
// remove redo actions which are invalid now
if (this.index < this.history.length - 1) {
this.history.splice(this.index + 1, this.history.length - this.index - 1);
}
// fire onchange event
this.onChange();
};
/**
* Clear history
*/
NodeHistory.prototype.clear = function () {
this.history = [];
this.index = -1;
// fire onchange event
this.onChange();
};
/**
* Check if there is an action available for undo
* @return {Boolean} canUndo
*/
NodeHistory.prototype.canUndo = function () {
return (this.index >= 0);
};
/**
* Check if there is an action available for redo
* @return {Boolean} canRedo
*/
NodeHistory.prototype.canRedo = function () {
return (this.index < this.history.length - 1);
};
/**
* Undo the last action
*/
NodeHistory.prototype.undo = function () {
if (this.canUndo()) {
var obj = this.history[this.index];
if (obj) {
var action = this.actions[obj.action];
if (action && action.undo) {
action.undo(obj.params);
if (obj.params.oldSelection) {
try {
this.editor.setDomSelection(obj.params.oldSelection);
}
catch (err) {
console.error(err);
}
}
}
else {
console.error(new Error('unknown action "' + obj.action + '"'));
}
}
this.index--;
// fire onchange event
this.onChange();
}
};
/**
* Redo the last action
*/
NodeHistory.prototype.redo = function () {
if (this.canRedo()) {
this.index++;
var obj = this.history[this.index];
if (obj) {
var action = this.actions[obj.action];
if (action && action.redo) {
action.redo(obj.params);
if (obj.params.newSelection) {
try {
this.editor.setDomSelection(obj.params.newSelection);
}
catch (err) {
console.error(err);
}
}
}
else {
console.error(new Error('unknown action "' + obj.action + '"'));
}
}
// fire onchange event
this.onChange();
}
};
/**
* Destroy history
*/
NodeHistory.prototype.destroy = function () {
this.editor = null;
this.history = [];
this.index = -1;
};
module.exports = NodeHistory;

View File

@ -3,3 +3,5 @@ exports.DEFAULT_MODAL_ANCHOR = document.body;
exports.SIZE_LARGE = 10 * 1024 * 1024; // 10 MB exports.SIZE_LARGE = 10 * 1024 * 1024; // 10 MB
exports.MAX_PREVIEW_CHARACTERS = 20000; exports.MAX_PREVIEW_CHARACTERS = 20000;
exports.PREVIEW_HISTORY_LIMIT = 2 * 1024 * 1024 * 1024; // 2 GB

View File

@ -8,7 +8,9 @@ var showTransformModal = require('./showTransformModal');
var MAX_PREVIEW_CHARACTERS = require('./constants').MAX_PREVIEW_CHARACTERS; var MAX_PREVIEW_CHARACTERS = require('./constants').MAX_PREVIEW_CHARACTERS;
var DEFAULT_MODAL_ANCHOR = require('./constants').DEFAULT_MODAL_ANCHOR; var DEFAULT_MODAL_ANCHOR = require('./constants').DEFAULT_MODAL_ANCHOR;
var SIZE_LARGE = require('./constants').SIZE_LARGE; var SIZE_LARGE = require('./constants').SIZE_LARGE;
var PREVIEW_HISTORY_LIMIT = require('./constants').PREVIEW_HISTORY_LIMIT;
var util = require('./util'); var util = require('./util');
var History = require('./History');
var jsonUtils = require('./jsonUtils'); var jsonUtils = require('./jsonUtils');
// create a mixin with the functions for text mode // create a mixin with the functions for text mode
@ -102,7 +104,6 @@ previewmode.create = function (container, options) {
me.executeWithBusyMessage(function () { me.executeWithBusyMessage(function () {
try { try {
me.format(); me.format();
me._onChange();
} }
catch (err) { catch (err) {
me._onError(err); me._onError(err);
@ -119,9 +120,7 @@ previewmode.create = function (container, options) {
buttonCompact.onclick = function handleCompact() { buttonCompact.onclick = function handleCompact() {
me.executeWithBusyMessage(function () { me.executeWithBusyMessage(function () {
try { try {
me.compact(); me.compact();
me._onChange();
} }
catch (err) { catch (err) {
me._onError(err); me._onError(err);
@ -163,7 +162,6 @@ previewmode.create = function (container, options) {
me.executeWithBusyMessage(function () { me.executeWithBusyMessage(function () {
try { try {
me.repair(); me.repair();
me._onChange();
} }
catch (err) { catch (err) {
me._onError(err); me._onError(err);
@ -171,6 +169,51 @@ previewmode.create = function (container, options) {
}, 'repairing...'); }, 'repairing...');
}; };
// create history and undo/redo buttons
if (this.options.history !== false) { // default option value is true
var onHistoryChange = function () {
me.dom.undo.disabled = !me.history.canUndo();
me.dom.redo.disabled = !me.history.canRedo();
};
var calculateItemSize = function (item) {
return item.text.length * 2; // times two to account for the json object
}
this.history = new History(onHistoryChange, calculateItemSize, PREVIEW_HISTORY_LIMIT);
// create undo button
var undo = document.createElement('button');
undo.type = 'button';
undo.className = 'jsoneditor-undo jsoneditor-separator';
undo.title = translate('undo');
undo.onclick = function () {
var action = me.history.undo();
if (action) {
me._applyHistory(action);
}
};
this.menu.appendChild(undo);
this.dom.undo = undo;
// create redo button
var redo = document.createElement('button');
redo.type = 'button';
redo.className = 'jsoneditor-redo';
redo.title = translate('redo');
redo.onclick = function () {
var action = me.history.redo();
if (action) {
me._applyHistory(action);
}
};
this.menu.appendChild(redo);
this.dom.redo = redo;
// force enabling/disabling the undo/redo button
this.history.onChange();
}
// create mode box // create mode box
if (this.options && this.options.modes && this.options.modes.length) { if (this.options && this.options.modes && this.options.modes.length) {
this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch(mode) { this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch(mode) {
@ -203,12 +246,12 @@ previewmode.create = function (container, options) {
statusBar.appendChild(this.dom.arrayInfo); statusBar.appendChild(this.dom.arrayInfo);
} }
this.renderPreview(); this._renderPreview();
this.setSchema(this.options.schema, this.options.schemaRefs); this.setSchema(this.options.schema, this.options.schemaRefs);
}; };
previewmode.renderPreview = function () { previewmode._renderPreview = function () {
var text = this.getText(); var text = this.getText();
this.dom.previewText.nodeValue = util.limitCharacters(text, MAX_PREVIEW_CHARACTERS); this.dom.previewText.nodeValue = util.limitCharacters(text, MAX_PREVIEW_CHARACTERS);
@ -232,7 +275,7 @@ previewmode.renderPreview = function () {
me.executeWithBusyMessage(function () { me.executeWithBusyMessage(function () {
try { try {
me.get(); me.get();
me.renderPreview(); me._renderPreview();
} }
catch (err) { catch (err) {
me._onError(err); me._onError(err);
@ -259,10 +302,6 @@ previewmode.renderPreview = function () {
* @private * @private
*/ */
previewmode._onChange = function () { previewmode._onChange = function () {
if (this.onChangeDisabled) {
return;
}
// validate JSON schema (if configured) // validate JSON schema (if configured)
this._debouncedValidate(); this._debouncedValidate();
@ -306,31 +345,29 @@ previewmode._showSortModal = function () {
function onSort (json, sortedBy) { function onSort (json, sortedBy) {
if (Array.isArray(json)) { if (Array.isArray(json)) {
var sortedJson = util.sort(json, sortedBy.path, sortedBy.direction); var sortedArray = util.sort(json, sortedBy.path, sortedBy.direction);
me.sortedBy = sortedBy me.sortedBy = sortedBy
me.set(sortedJson); me._setAndFireOnChange(sortedArray);
} }
if (util.isObject(json)) { if (util.isObject(json)) {
var sortedJson = util.sortObjectKeys(json, sortedBy.direction); var sortedObject = util.sortObjectKeys(json, sortedBy.direction);
me.sortedBy = sortedBy; me.sortedBy = sortedBy;
me.set(sortedJson); me._setAndFireOnChange(sortedObject);
} }
me._onChange();
} }
this.executeWithBusyMessage(function () { this.executeWithBusyMessage(function () {
var container = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR; var container = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR;
var json = me.get(); var json = me.get();
me.renderPreview(); // update array count me._renderPreview(); // update array count
showSortModal(container, json, function (sortedBy) { showSortModal(container, json, function (sortedBy) {
me.executeWithBusyMessage(function () { me.executeWithBusyMessage(function () {
onSort(json, sortedBy); onSort(json, sortedBy);
}, 'stringifying...'); }, 'sorting...');
}, me.sortedBy) }, me.sortedBy)
}, 'parsing...'); }, 'parsing...');
} }
@ -345,14 +382,12 @@ previewmode._showTransformModal = function () {
this.executeWithBusyMessage(function () { this.executeWithBusyMessage(function () {
var anchor = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR; var anchor = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR;
var json = me.get(); var json = me.get();
me.renderPreview(); // update array count me._renderPreview(); // update array count
showTransformModal(anchor, json, function (query) { showTransformModal(anchor, json, function (query) {
me.executeWithBusyMessage(function () { me.executeWithBusyMessage(function () {
var updatedJson = jmespath.search(json, query); var updatedJson = jmespath.search(json, query);
me.set(updatedJson); me._setAndFireOnChange(updatedJson);
me._onChange();
}, 'transforming...') }, 'transforming...')
}) })
}, 'parsing...') }, 'parsing...')
@ -362,7 +397,7 @@ previewmode._showTransformModal = function () {
* Destroy the editor. Clean up DOM, event listeners, and web workers. * Destroy the editor. Clean up DOM, event listeners, and web workers.
*/ */
previewmode.destroy = function () { previewmode.destroy = function () {
if (this.frame && this.container && this.frame.parentNode == this.container) { if (this.frame && this.container && this.frame.parentNode === this.container) {
this.container.removeChild(this.frame); this.container.removeChild(this.frame);
} }
@ -372,6 +407,9 @@ previewmode.destroy = function () {
} }
this._debouncedValidate = null; this._debouncedValidate = null;
this.history.clear();
this.history = null;
}; };
/** /**
@ -380,12 +418,9 @@ previewmode.destroy = function () {
previewmode.compact = function () { previewmode.compact = function () {
var json = this.get(); var json = this.get();
var text = JSON.stringify(json); var text = JSON.stringify(json);
this.setText(text);
// we know that in this case the json is still the same // we know that in this case the json is still the same, so we pass json too
this.json = json; this._setTextAndFireOnChange(text, json);
this.renderPreview();
}; };
/** /**
@ -394,12 +429,9 @@ previewmode.compact = function () {
previewmode.format = function () { previewmode.format = function () {
var json = this.get(); var json = this.get();
var text = JSON.stringify(json, null, this.indentation); var text = JSON.stringify(json, null, this.indentation);
this.setText(text);
// we know that in this case the json is still the same // we know that in this case the json is still the same, so we pass json too
this.json = json; this._setTextAndFireOnChange(text, json);
this.renderPreview();
}; };
/** /**
@ -408,28 +440,27 @@ previewmode.format = function () {
previewmode.repair = function () { previewmode.repair = function () {
var text = this.getText(); var text = this.getText();
var sanitizedText = util.sanitize(text); var sanitizedText = util.sanitize(text);
this.setText(sanitizedText);
this._setTextAndFireOnChange(sanitizedText);
}; };
/** /**
* Set focus to the formatter * Set focus to the editor
*/ */
previewmode.focus = function () { previewmode.focus = function () {
// TODO: implement method focus // TODO: implement method focus
}; };
/** /**
* Set json data in the formatter * Set json data in the editor
* @param {*} json * @param {*} json
*/ */
previewmode.set = function(json) { previewmode.set = function(json) {
this.text = undefined; if (this.history) {
this.json = json; this.history.clear();
}
this.renderPreview(); this._set(json);
// validate JSON schema
this._debouncedValidate();
}; };
/** /**
@ -437,11 +468,32 @@ previewmode.set = function(json) {
* @param {*} json * @param {*} json
*/ */
previewmode.update = function(json) { previewmode.update = function(json) {
this.set(json); this._set(json);
}; };
/** /**
* Get json data from the formatter * Set json data
* @param {*} json
*/
previewmode._set = function(json) {
this.text = undefined;
this.json = json;
this._renderPreview();
this._pushHistory();
// validate JSON schema
this._debouncedValidate();
};
previewmode._setAndFireOnChange = function (json) {
this._set(json);
this._onChange();
}
/**
* Get json data
* @return {*} json * @return {*} json
*/ */
previewmode.get = function() { previewmode.get = function() {
@ -490,20 +542,11 @@ previewmode.getText = function() {
* @param {String} jsonText * @param {String} jsonText
*/ */
previewmode.setText = function(jsonText) { previewmode.setText = function(jsonText) {
if (this.options.escapeUnicode === true) { if (this.history) {
console.time('escape') // TODO: cleanup this.history.clear();
this.text = util.escapeUnicodeChars(jsonText);
console.timeEnd('escape') // TODO: cleanup
} }
else {
this.text = jsonText;
}
this.json = undefined;
this.renderPreview(); this._setText(jsonText);
// validate JSON schema
this._debouncedValidate();
}; };
/** /**
@ -516,11 +559,75 @@ previewmode.updateText = function(jsonText) {
return; return;
} }
this.onChangeDisabled = true; // don't fire an onChange event this._setText(jsonText);
this.setText(jsonText);
this.onChangeDisabled = false;
}; };
/**
* Set the text contents of the editor
* @param {string} jsonText
* @param {*} [json] Optional JSON instance of the text
* @private
*/
previewmode._setText = function(jsonText, json) {
if (this.options.escapeUnicode === true) {
console.time('escape') // TODO: cleanup
this.text = util.escapeUnicodeChars(jsonText);
console.timeEnd('escape') // TODO: cleanup
}
else {
this.text = jsonText;
}
this.json = json;
this._renderPreview();
this._pushHistory();
this._debouncedValidate();
};
/**
* Set text and fire onChange callback
* @param {string} jsonText
* @param {*} [json] Optional JSON instance of the text
* @private
*/
previewmode._setTextAndFireOnChange = function (jsonText, json) {
this._setText(jsonText, json);
this._onChange();
}
/**
* Apply history to the current state
* @param {{json?: JSON, text?: string}} action
* @private
*/
previewmode._applyHistory = function (action) {
this.json = action.json;
this.text = action.text;
this._renderPreview();
this._debouncedValidate();
};
/**
* Push the current state to history
* @private
*/
previewmode._pushHistory = function () {
if (!this.history) {
return;
}
var action = {
text: this.text,
json: this.json
};
this.history.add(action);
}
/** /**
* Execute a heavy, blocking action. * Execute a heavy, blocking action.
* Before starting the action, show a message on screen like "parsing..." * Before starting the action, show a message on screen like "parsing..."

View File

@ -2,7 +2,7 @@
var VanillaPicker = require('./vanilla-picker'); var VanillaPicker = require('./vanilla-picker');
var Highlighter = require('./Highlighter'); var Highlighter = require('./Highlighter');
var History = require('./History'); var NodeHistory = require('./NodeHistory');
var SearchBox = require('./SearchBox'); var SearchBox = require('./SearchBox');
var ContextMenu = require('./ContextMenu'); var ContextMenu = require('./ContextMenu');
var TreePath = require('./TreePath'); var TreePath = require('./TreePath');
@ -47,7 +47,7 @@ treemode.create = function (container, options) {
this.autocomplete = new autocomplete(options.autocomplete); this.autocomplete = new autocomplete(options.autocomplete);
if (this.options.history && this.options.mode !== 'view') { if (this.options.history && this.options.mode !== 'view') {
this.history = new History(this); this.history = new NodeHistory(this);
} }
this._createFrame(); this._createFrame();