UNPKG

jsoneditor

Version:

A web-based tool to view, edit, format, and validate JSON

338 lines (305 loc) 9.81 kB
'use strict'; var util = require('./util'); /** * @constructor History * Store action history, enables undo and redo * @param {JSONEditor} editor */ function History (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. */ History.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. */ History.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 */ History.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 */ 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 () { 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 */ History.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 */ History.prototype.destroy = function () { this.editor = null; this.history = []; this.index = -1; }; module.exports = History;