1
0
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:
Ian Storm Taylor
2016-08-30 15:32:08 -07:00
parent c35630d6ba
commit a6b248fbe9
6 changed files with 170 additions and 140 deletions

View File

@@ -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 })
}
}

View File

@@ -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 })
}
/**

View File

@@ -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
View 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
}

View File

@@ -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,
}