jsoneditor
Version:
A web-based tool to view, edit, format, and validate JSON
334 lines (302 loc) • 9.91 kB
JavaScript
import { findUniqueName } from './util'
/**
* @constructor History
* Store action history, enables undo and redo
* @param {JSONEditor} editor
*/
export class NodeHistory {
constructor (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) {
const parentNode = findNode(params.parentPath)
const node = parentNode.childs[params.index]
node.updateField(params.oldValue)
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
const 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) {
const parentNode = findNode(params.parentPath)
params.paths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
params.nodes.forEach(node => {
parentNode.appendChild(node)
})
}
},
insertBeforeNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
params.paths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
const beforeNode = findNode(params.beforePath)
params.nodes.forEach(node => {
parentNode.insertBefore(node, beforeNode)
})
}
},
insertAfterNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
params.paths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
let afterNode = findNode(params.afterPath)
params.nodes.forEach(node => {
parentNode.insertAfter(node, afterNode)
afterNode = node
})
}
},
removeNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
const beforeNode = parentNode.childs[params.index] || parentNode.append
params.nodes.forEach(node => {
parentNode.insertBefore(node, beforeNode)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
params.paths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
}
},
duplicateNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
params.clonePaths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
let afterNode = findNode(params.afterPath)
const nodes = params.paths.map(findNode)
nodes.forEach(node => {
const clone = node.clone()
if (parentNode.type === 'object') {
const existingFieldNames = parentNode.getFieldNames()
clone.field = findUniqueName(node.field, existingFieldNames)
}
parentNode.insertAfter(clone, afterNode)
afterNode = clone
})
}
},
moveNodes: {
undo: function (params) {
const oldParentNode = findNode(params.oldParentPath)
const newParentNode = findNode(params.newParentPath)
const oldBeforeNode = oldParentNode.childs[params.oldIndex] || oldParentNode.append
// first copy the nodes, then move them
const nodes = newParentNode.childs.slice(params.newIndex, params.newIndex + params.count)
nodes.forEach((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) {
const oldParentNode = findNode(params.oldParentPathRedo)
const newParentNode = findNode(params.newParentPathRedo)
const newBeforeNode = newParentNode.childs[params.newIndexRedo] || newParentNode.append
// first copy the nodes, then move them
const nodes = oldParentNode.childs.slice(params.oldIndexRedo, params.oldIndexRedo + params.count)
nodes.forEach((node, index) => {
node.field = params.fieldNames[index]
newParentNode.moveBefore(node, newBeforeNode)
})
}
},
sort: {
undo: function (params) {
const node = findNode(params.path)
node.hideChilds()
node.childs = params.oldChilds
node.updateDom({ updateIndexes: true })
node.showChilds()
},
redo: function (params) {
const 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.
*/
onChange () {}
/**
* 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.
*/
add (action, params) {
this.index++
this.history[this.index] = {
action,
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
*/
clear () {
this.history = []
this.index = -1
// fire onchange event
this.onChange()
}
/**
* Check if there is an action available for undo
* @return {Boolean} canUndo
*/
canUndo () {
return (this.index >= 0)
}
/**
* Check if there is an action available for redo
* @return {Boolean} canRedo
*/
canRedo () {
return (this.index < this.history.length - 1)
}
/**
* Undo the last action
*/
undo () {
if (this.canUndo()) {
const obj = this.history[this.index]
if (obj) {
const 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
*/
redo () {
if (this.canRedo()) {
this.index++
const obj = this.history[this.index]
if (obj) {
const 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
*/
destroy () {
this.editor = null
this.history = []
this.index = -1
}
}