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) {
|
||||
const { state, operations = [] } = properties
|
||||
const { state } = properties
|
||||
this.state = state
|
||||
this.operations = []
|
||||
|
||||
operations.forEach(op => {
|
||||
this.applyOperation(op)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,157 +36,48 @@ class Transform {
|
||||
*
|
||||
* @param {Object} options
|
||||
* @property {Boolean} isNative
|
||||
* @property {Boolean} snapshot
|
||||
* @property {Boolean} merge
|
||||
* @property {Boolean} save
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
apply(options = {}) {
|
||||
let { merge, isNative = false, save = true } = options
|
||||
let { state, operations } = this
|
||||
let { history } = state
|
||||
let { undos, redos } = history
|
||||
const previous = undos.peek()
|
||||
|
||||
// If there are no operations, abort early.
|
||||
if (!operations.length) return state
|
||||
|
||||
// The `isNative` flag allows for natively-handled changes to skip
|
||||
// rerendering the editor for improved performance.
|
||||
const isNative = !!options.isNative
|
||||
// If there's a previous save point, determine if the new operations should
|
||||
// be merged into the previous ones.
|
||||
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.
|
||||
const shouldSnapshot = options.snapshot == null
|
||||
? this.shouldSnapshot()
|
||||
: options.snapshot
|
||||
|
||||
// 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)
|
||||
merge = (
|
||||
(onlySelections) ||
|
||||
(onlyInserts && prevOnlyInserts) ||
|
||||
(onlyRemoves && prevOnlyRemoves)
|
||||
)
|
||||
}
|
||||
|
||||
// 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, 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
|
||||
// Save the new operations.
|
||||
if (save || !previous) {
|
||||
this.save({ merge })
|
||||
}
|
||||
|
||||
// Otherwise, snapshot.
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
// Return the new state with the `isNative` flag set.
|
||||
return this.state.merge({ isNative: !!isNative })
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -467,6 +467,7 @@ function Plugin(options = {}) {
|
||||
return state
|
||||
.transform()
|
||||
.redo()
|
||||
.apply({ save: false })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -486,6 +487,7 @@ function Plugin(options = {}) {
|
||||
return state
|
||||
.transform()
|
||||
[data.isShift ? 'redo' : 'undo']()
|
||||
.apply({ save: false })
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,15 @@
|
||||
|
||||
import Debug from 'debug'
|
||||
import uid from '../utils/uid'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:operation')
|
||||
|
||||
/**
|
||||
* Operations.
|
||||
*
|
||||
@@ -42,6 +51,8 @@ export function applyOperation(transform, operation) {
|
||||
throw new Error(`Unknown operation type: "${type}".`)
|
||||
}
|
||||
|
||||
debug(type, operation)
|
||||
|
||||
transform.state = fn(state, operation)
|
||||
transform.operations = operations.concat([operation])
|
||||
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,
|
||||
} from './normalize'
|
||||
|
||||
/**
|
||||
* History.
|
||||
*/
|
||||
|
||||
import {
|
||||
redo,
|
||||
save,
|
||||
undo,
|
||||
} from './history'
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
@@ -265,4 +275,12 @@ export default {
|
||||
normalizeDocument,
|
||||
normalizeSelection,
|
||||
|
||||
/**
|
||||
* History.
|
||||
*/
|
||||
|
||||
redo,
|
||||
save,
|
||||
undo,
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user