From a6b248fbe94cd3fe9b7070c518eeca0a5afb282d Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Tue, 30 Aug 2016 15:32:08 -0700 Subject: [PATCH] refactor history into transforms --- lib/models/transform.js | 167 +++++------------------------- lib/plugins/core.js | 2 + lib/transforms/apply-operation.js | 11 ++ lib/transforms/by-current-keys.js | 0 lib/transforms/history.js | 112 ++++++++++++++++++++ lib/transforms/index.js | 18 ++++ 6 files changed, 170 insertions(+), 140 deletions(-) delete mode 100644 lib/transforms/by-current-keys.js create mode 100644 lib/transforms/history.js diff --git a/lib/models/transform.js b/lib/models/transform.js index 327cbf8c2..9886df566 100644 --- a/lib/models/transform.js +++ b/lib/models/transform.js @@ -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 }) } } diff --git a/lib/plugins/core.js b/lib/plugins/core.js index ea393bfc6..a76fe049d 100644 --- a/lib/plugins/core.js +++ b/lib/plugins/core.js @@ -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 }) } /** diff --git a/lib/transforms/apply-operation.js b/lib/transforms/apply-operation.js index d5e37a315..bdc3dc8b4 100644 --- a/lib/transforms/apply-operation.js +++ b/lib/transforms/apply-operation.js @@ -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 diff --git a/lib/transforms/by-current-keys.js b/lib/transforms/by-current-keys.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/transforms/history.js b/lib/transforms/history.js new file mode 100644 index 000000000..eb4b2631c --- /dev/null +++ b/lib/transforms/history.js @@ -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 +} diff --git a/lib/transforms/index.js b/lib/transforms/index.js index 9bb9a207a..9493cbaeb 100644 --- a/lib/transforms/index.js +++ b/lib/transforms/index.js @@ -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, + }