mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-30 10:29:48 +02:00
refactor history into transforms
This commit is contained in:
@@ -16,13 +16,9 @@ class Transform {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
constructor(properties) {
|
constructor(properties) {
|
||||||
const { state, operations = [] } = properties
|
const { state } = properties
|
||||||
this.state = state
|
this.state = state
|
||||||
this.operations = []
|
this.operations = []
|
||||||
|
|
||||||
operations.forEach(op => {
|
|
||||||
this.applyOperation(op)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,157 +36,48 @@ class Transform {
|
|||||||
*
|
*
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @property {Boolean} isNative
|
* @property {Boolean} isNative
|
||||||
* @property {Boolean} snapshot
|
* @property {Boolean} merge
|
||||||
|
* @property {Boolean} save
|
||||||
* @return {State} state
|
* @return {State} state
|
||||||
*/
|
*/
|
||||||
|
|
||||||
apply(options = {}) {
|
apply(options = {}) {
|
||||||
|
let { merge, isNative = false, save = true } = options
|
||||||
let { state, operations } = this
|
let { state, operations } = this
|
||||||
let { history } = state
|
let { history } = state
|
||||||
let { undos, redos } = history
|
let { undos, redos } = history
|
||||||
|
const previous = undos.peek()
|
||||||
|
|
||||||
// If there are no operations, abort early.
|
// If there are no operations, abort early.
|
||||||
if (!operations.length) return state
|
if (!operations.length) return state
|
||||||
|
|
||||||
// The `isNative` flag allows for natively-handled changes to skip
|
// If there's a previous save point, determine if the new operations should
|
||||||
// rerendering the editor for improved performance.
|
// be merged into the previous ones.
|
||||||
const isNative = !!options.isNative
|
if (previous && merge == null) {
|
||||||
|
const types = operations.map(op => op.type)
|
||||||
|
const prevTypes = previous.map(op => op.type)
|
||||||
|
const edits = types.filter(type => type != 'set_selection')
|
||||||
|
const prevEdits = prevTypes.filter(type => type != 'set_selection')
|
||||||
|
const onlySelections = types.every(type => type == 'set_selection')
|
||||||
|
const onlyInserts = edits.length && edits.every(type => type == 'insert_text')
|
||||||
|
const onlyRemoves = edits.length && edits.every(type => type == 'remove_text')
|
||||||
|
const prevOnlyInserts = prevEdits.length && prevEdits.every(type => type == 'insert_text')
|
||||||
|
const prevOnlyRemoves = prevEdits.length && prevEdits.every(type => type == 'remove_text')
|
||||||
|
|
||||||
// Determine whether we need to create a new snapshot.
|
merge = (
|
||||||
const shouldSnapshot = options.snapshot == null
|
(onlySelections) ||
|
||||||
? this.shouldSnapshot()
|
(onlyInserts && prevOnlyInserts) ||
|
||||||
: options.snapshot
|
(onlyRemoves && prevOnlyRemoves)
|
||||||
|
)
|
||||||
// Either create a new snapshot, or push the operations into the previous.
|
|
||||||
if (shouldSnapshot) {
|
|
||||||
const snapshot = { operations }
|
|
||||||
undos = undos.push(snapshot)
|
|
||||||
} else {
|
|
||||||
const snapshot = undos.peek()
|
|
||||||
snapshot.operations = snapshot.operations.concat(operations)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the redo stack and constrain the undos stack.
|
// Save the new operations.
|
||||||
if (undos.size > 100) undos = undos.take(100)
|
if (save || !previous) {
|
||||||
redos = redos.clear()
|
this.save({ merge })
|
||||||
|
|
||||||
// Update the state.
|
|
||||||
history = history.merge({ undos, redos })
|
|
||||||
state = state.merge({ history, isNative })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the current transform operations should create a snapshot.
|
|
||||||
*
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
|
|
||||||
shouldSnapshot() {
|
|
||||||
const { state, operations } = this
|
|
||||||
const { history, selection } = state
|
|
||||||
const { undos, redos } = history
|
|
||||||
const previous = undos.peek()
|
|
||||||
|
|
||||||
// If there isn't a previous state, snapshot.
|
|
||||||
if (!previous) return true
|
|
||||||
|
|
||||||
const types = operations.map(op => op.type)
|
|
||||||
const prevTypes = previous.operations.map(op => op.type)
|
|
||||||
const edits = types.filter(type => type != 'set_selection')
|
|
||||||
const prevEdits = prevTypes.filter(type => type != 'set_selection')
|
|
||||||
|
|
||||||
const onlySelections = types.every(type => type == 'set_selection')
|
|
||||||
const onlyInsert = edits.every(type => type == 'insert_text')
|
|
||||||
const onlyRemove = edits.every(type => type == 'remove_text')
|
|
||||||
const prevOnlySelections = prevTypes.every(type => type == 'set_selection')
|
|
||||||
const prevOnlyInsert = prevEdits.every(type => type == 'insert_text')
|
|
||||||
const prevOnlyRemove = prevEdits.every(type => type == 'remove_text')
|
|
||||||
|
|
||||||
// If the only operations applied are selection operations, or if the
|
|
||||||
// current operations are all text editing, don't snapshot.
|
|
||||||
if (
|
|
||||||
(onlySelections) ||
|
|
||||||
(!prevOnlySelections && onlyInsert && prevOnlyInsert) ||
|
|
||||||
(!prevOnlySelections && onlyRemove && prevOnlyRemove)
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, snapshot.
|
// Return the new state with the `isNative` flag set.
|
||||||
return true
|
return this.state.merge({ isNative: !!isNative })
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redo to the next state in the history.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
redo() {
|
|
||||||
let { state } = this
|
|
||||||
let { history } = state
|
|
||||||
let { undos, redos } = history
|
|
||||||
|
|
||||||
// If there's no next snapshot, return the current state.
|
|
||||||
let next = redos.peek()
|
|
||||||
if (!next) return state
|
|
||||||
|
|
||||||
// Shift the next state into the undo stack.
|
|
||||||
redos = redos.pop()
|
|
||||||
undos = undos.push(next)
|
|
||||||
|
|
||||||
// Replay the next operations.
|
|
||||||
const { operations } = next
|
|
||||||
operations.forEach(op => {
|
|
||||||
this.applyOperation(op)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the state's history and force `isNative` to false.
|
|
||||||
history = history.merge({ undos, redos })
|
|
||||||
state = this.state.merge({
|
|
||||||
history,
|
|
||||||
isNative: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Undo the previous operations in the history.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
undo() {
|
|
||||||
let { state } = this
|
|
||||||
let { history } = state
|
|
||||||
let { undos, redos } = history
|
|
||||||
|
|
||||||
// If there's no previous snapshot, return the current state.
|
|
||||||
let previous = undos.peek()
|
|
||||||
if (!previous) return state
|
|
||||||
|
|
||||||
// Shift the previous operations into the redo stack.
|
|
||||||
undos = undos.pop()
|
|
||||||
redos = redos.push(previous)
|
|
||||||
|
|
||||||
// Replay the inverse of the previous operations.
|
|
||||||
const operations = previous.operations.slice().reverse()
|
|
||||||
operations.forEach(op => {
|
|
||||||
op.inverse.forEach(inv => {
|
|
||||||
this.applyOperation(inv)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the state's history and force `isNative` to false.
|
|
||||||
history = history.merge({ undos, redos })
|
|
||||||
state = this.state.merge({
|
|
||||||
history,
|
|
||||||
isNative: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -467,6 +467,7 @@ function Plugin(options = {}) {
|
|||||||
return state
|
return state
|
||||||
.transform()
|
.transform()
|
||||||
.redo()
|
.redo()
|
||||||
|
.apply({ save: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -486,6 +487,7 @@ function Plugin(options = {}) {
|
|||||||
return state
|
return state
|
||||||
.transform()
|
.transform()
|
||||||
[data.isShift ? 'redo' : 'undo']()
|
[data.isShift ? 'redo' : 'undo']()
|
||||||
|
.apply({ save: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,15 @@
|
|||||||
|
|
||||||
|
import Debug from 'debug'
|
||||||
import uid from '../utils/uid'
|
import uid from '../utils/uid'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debug = Debug('slate:operation')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operations.
|
* Operations.
|
||||||
*
|
*
|
||||||
@@ -42,6 +51,8 @@ export function applyOperation(transform, operation) {
|
|||||||
throw new Error(`Unknown operation type: "${type}".`)
|
throw new Error(`Unknown operation type: "${type}".`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug(type, operation)
|
||||||
|
|
||||||
transform.state = fn(state, operation)
|
transform.state = fn(state, operation)
|
||||||
transform.operations = operations.concat([operation])
|
transform.operations = operations.concat([operation])
|
||||||
return transform
|
return transform
|
||||||
|
112
lib/transforms/history.js
Normal file
112
lib/transforms/history.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Redo to the next state in the history.
|
||||||
|
*
|
||||||
|
* @param {Transform} transform
|
||||||
|
* @return {Transform}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function redo(transform) {
|
||||||
|
let { state } = transform
|
||||||
|
let { history } = state
|
||||||
|
let { undos, redos } = history
|
||||||
|
|
||||||
|
// If there's no next snapshot, abort.
|
||||||
|
let next = redos.peek()
|
||||||
|
if (!next) return transform
|
||||||
|
|
||||||
|
// Shift the next state into the undo stack.
|
||||||
|
redos = redos.pop()
|
||||||
|
undos = undos.push(next)
|
||||||
|
|
||||||
|
// Replay the next operations.
|
||||||
|
next.forEach(op => {
|
||||||
|
transform.applyOperation(op)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the history.
|
||||||
|
state = transform.state
|
||||||
|
history = history.merge({ undos, redos })
|
||||||
|
state = state.merge({ history })
|
||||||
|
|
||||||
|
// Update the transform.
|
||||||
|
transform.state = state
|
||||||
|
return transform
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the operations into the history.
|
||||||
|
*
|
||||||
|
* @param {Transform} transform
|
||||||
|
* @param {Object} options
|
||||||
|
* @return {Transform}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function save(transform, options = {}) {
|
||||||
|
const { merge = false } = options
|
||||||
|
let { state, operations } = transform
|
||||||
|
let { history } = state
|
||||||
|
let { undos, redos } = history
|
||||||
|
|
||||||
|
// If there are no operations, abort.
|
||||||
|
if (!operations.length) return transform
|
||||||
|
|
||||||
|
// Create a new save point or merge the operations into the previous one.
|
||||||
|
if (merge) {
|
||||||
|
let previous = undos.peek()
|
||||||
|
undos = undos.pop()
|
||||||
|
previous = previous.concat(operations)
|
||||||
|
undos = undos.push(previous)
|
||||||
|
} else {
|
||||||
|
undos = undos.push(operations)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the redo stack and constrain the undos stack.
|
||||||
|
if (undos.size > 100) undos = undos.take(100)
|
||||||
|
redos = redos.clear()
|
||||||
|
|
||||||
|
// Update the state.
|
||||||
|
history = history.merge({ undos, redos })
|
||||||
|
state = state.merge({ history })
|
||||||
|
|
||||||
|
// Update the transform.
|
||||||
|
transform.state = state
|
||||||
|
return transform
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo the previous operations in the history.
|
||||||
|
*
|
||||||
|
* @param {Transform} transform
|
||||||
|
* @return {Transform}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function undo(transform) {
|
||||||
|
let { state } = transform
|
||||||
|
let { history } = state
|
||||||
|
let { undos, redos } = history
|
||||||
|
|
||||||
|
// If there's no previous snapshot, abort.
|
||||||
|
let previous = undos.peek()
|
||||||
|
if (!previous) return transform
|
||||||
|
|
||||||
|
// Shift the previous operations into the redo stack.
|
||||||
|
undos = undos.pop()
|
||||||
|
redos = redos.push(previous)
|
||||||
|
|
||||||
|
// Replay the inverse of the previous operations.
|
||||||
|
previous.slice().reverse().forEach(op => {
|
||||||
|
op.inverse.forEach(inv => {
|
||||||
|
transform.applyOperation(inv)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the history.
|
||||||
|
state = transform.state
|
||||||
|
history = history.merge({ undos, redos })
|
||||||
|
state = state.merge({ history })
|
||||||
|
|
||||||
|
// Update the transform.
|
||||||
|
transform.state = state
|
||||||
|
return transform
|
||||||
|
}
|
@@ -135,6 +135,16 @@ import {
|
|||||||
normalizeSelection,
|
normalizeSelection,
|
||||||
} from './normalize'
|
} from './normalize'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
redo,
|
||||||
|
save,
|
||||||
|
undo,
|
||||||
|
} from './history'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export.
|
* Export.
|
||||||
*
|
*
|
||||||
@@ -265,4 +275,12 @@ export default {
|
|||||||
normalizeDocument,
|
normalizeDocument,
|
||||||
normalizeSelection,
|
normalizeSelection,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History.
|
||||||
|
*/
|
||||||
|
|
||||||
|
redo,
|
||||||
|
save,
|
||||||
|
undo,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user