From f1f07da5e52d33a758161a66c13590a597548785 Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Thu, 16 Nov 2017 11:32:13 -0800 Subject: [PATCH] add immutable operation model, with serialization (#1409) * add immutable operation model, with serialization * fix split node operations, and deserializing operations --- examples/syncing-operations/index.js | 10 +- packages/slate/src/changes/by-key.js | 11 + packages/slate/src/changes/on-history.js | 30 +- packages/slate/src/changes/on-selection.js | 22 +- packages/slate/src/constants/model-types.js | 1 + .../src/constants/operation-attributes.js | 94 ++++++ packages/slate/src/index.js | 3 + packages/slate/src/models/change.js | 9 + packages/slate/src/models/operation.js | 296 ++++++++++++++++++ packages/slate/src/models/range.js | 4 +- packages/slate/src/operations/apply.js | 86 ++--- packages/slate/src/operations/invert.js | 133 ++++---- 12 files changed, 542 insertions(+), 157 deletions(-) create mode 100644 packages/slate/src/constants/operation-attributes.js create mode 100644 packages/slate/src/models/operation.js diff --git a/examples/syncing-operations/index.js b/examples/syncing-operations/index.js index 1188a9024..3848926ee 100644 --- a/examples/syncing-operations/index.js +++ b/examples/syncing-operations/index.js @@ -244,7 +244,10 @@ class SyncingOperationsExample extends React.Component { */ onOneChange = (change) => { - const ops = change.operations.filter(o => o.type != 'set_selection' && o.type != 'set_value') + const ops = change.operations + .filter(o => o.type != 'set_selection' && o.type != 'set_value') + .map(o => o.toJSON()) + this.two.applyOperations(ops) } @@ -255,7 +258,10 @@ class SyncingOperationsExample extends React.Component { */ onTwoChange = (change) => { - const ops = change.operations.filter(o => o.type != 'set_selection' && o.type != 'set_value') + const ops = change.operations + .filter(o => o.type != 'set_selection' && o.type != 'set_value') + .map(o => o.toJSON()) + this.one.applyOperations(ops) } diff --git a/packages/slate/src/changes/by-key.js b/packages/slate/src/changes/by-key.js index 0d149f99c..270ffe5ec 100644 --- a/packages/slate/src/changes/by-key.js +++ b/packages/slate/src/changes/by-key.js @@ -56,6 +56,7 @@ Changes.addMarkByKey = (change, key, offset, length, mark, options = {}) => { operations.push({ type: 'add_mark', + value, path, offset: start, length: end - start, @@ -113,6 +114,7 @@ Changes.insertNodeByKey = (change, key, index, node, options = {}) => { change.applyOperation({ type: 'insert_node', + value, path: [...path, index], node, }) @@ -144,6 +146,7 @@ Changes.insertTextByKey = (change, key, offset, text, marks, options = {}) => { change.applyOperation({ type: 'insert_text', + value, path, offset, text, @@ -180,6 +183,7 @@ Changes.mergeNodeByKey = (change, key, options = {}) => { change.applyOperation({ type: 'merge_node', + value, path, position, }) @@ -211,6 +215,7 @@ Changes.moveNodeByKey = (change, key, newKey, newIndex, options = {}) => { change.applyOperation({ type: 'move_node', + value, path, newPath: [...newPath, newIndex], }) @@ -265,6 +270,7 @@ Changes.removeMarkByKey = (change, key, offset, length, mark, options = {}) => { operations.push({ type: 'remove_mark', + value, path, offset: start, length: end - start, @@ -320,6 +326,7 @@ Changes.removeNodeByKey = (change, key, options = {}) => { change.applyOperation({ type: 'remove_node', + value, path, node, }) @@ -371,6 +378,7 @@ Changes.removeTextByKey = (change, key, offset, length, options = {}) => { removals.push({ type: 'remove_text', + value, path, offset: start, text: string, @@ -434,6 +442,7 @@ Changes.setMarkByKey = (change, key, offset, length, mark, properties, options = change.applyOperation({ type: 'set_mark', + value, path, offset, length, @@ -467,6 +476,7 @@ Changes.setNodeByKey = (change, key, properties, options = {}) => { change.applyOperation({ type: 'set_node', + value, path, node, properties, @@ -495,6 +505,7 @@ Changes.splitNodeByKey = (change, key, position, options = {}) => { change.applyOperation({ type: 'split_node', + value, path, position, target, diff --git a/packages/slate/src/changes/on-history.js b/packages/slate/src/changes/on-history.js index 0f64419f7..00530bde3 100644 --- a/packages/slate/src/changes/on-history.js +++ b/packages/slate/src/changes/on-history.js @@ -31,13 +31,15 @@ Changes.redo = (change) => { // Replay the next operations. next.forEach((op) => { - // When the operation mutates selection, omit its `isFocused` props to - // prevent editor focus changing during continuously redoing. - let { type, properties } = op - if (type === 'set_selection') { - properties = omit(properties, 'isFocused') + const { type, properties } = op + + // When the operation mutates the selection, omit its `isFocused` value to + // prevent the editor focus from changing during redoing. + if (type == 'set_selection') { + op = op.set('properties', omit(properties, 'isFocused')) } - change.applyOperation({ ...op, properties }, { save: false }) + + change.applyOperation(op, { save: false }) }) // Update the history. @@ -67,14 +69,16 @@ Changes.undo = (change) => { redos = redos.push(previous) // Replay the inverse of the previous operations. - previous.slice().reverse().map(invert).forEach((inverseOp) => { - // When the operation mutates selection, omit its `isFocused` props to - // prevent editor focus changing during continuously undoing. - let { type, properties } = inverseOp - if (type === 'set_selection') { - properties = omit(properties, 'isFocused') + previous.slice().reverse().map(invert).forEach((inverse) => { + const { type, properties } = inverse + + // When the operation mutates the selection, omit its `isFocused` value to + // prevent the editor focus from changing during undoing. + if (type == 'set_selection') { + inverse = inverse.set('properties', omit(properties, 'isFocused')) } - change.applyOperation({ ...inverseOp, properties }, { save: false }) + + change.applyOperation(inverse, { save: false }) }) // Update the history. diff --git a/packages/slate/src/changes/on-selection.js b/packages/slate/src/changes/on-selection.js index 3cb0a24c7..2853fb294 100644 --- a/packages/slate/src/changes/on-selection.js +++ b/packages/slate/src/changes/on-selection.js @@ -38,29 +38,12 @@ Changes.select = (change, properties, options = {}) => { props[k] = properties[k] } - // Resolve the selection keys into paths. - sel.anchorPath = sel.anchorKey == null ? null : document.getPath(sel.anchorKey) - delete sel.anchorKey - - if (props.anchorKey) { - props.anchorPath = props.anchorKey == null ? null : document.getPath(props.anchorKey) - delete props.anchorKey - } - - sel.focusPath = sel.focusKey == null ? null : document.getPath(sel.focusKey) - delete sel.focusKey - - if (props.focusKey) { - props.focusPath = props.focusKey == null ? null : document.getPath(props.focusKey) - delete props.focusKey - } - // If the selection moves, clear any marks, unless the new selection // properties change the marks in some way. const moved = [ - 'anchorPath', + 'anchorKey', 'anchorOffset', - 'focusPath', + 'focusKey', 'focusOffset', ].some(p => props.hasOwnProperty(p)) @@ -76,6 +59,7 @@ Changes.select = (change, properties, options = {}) => { // Apply the operation. change.applyOperation({ type: 'set_selection', + value, properties: props, selection: sel, }, snapshot ? { skip: false, merge: false } : {}) diff --git a/packages/slate/src/constants/model-types.js b/packages/slate/src/constants/model-types.js index 9071923f4..16362e4f1 100644 --- a/packages/slate/src/constants/model-types.js +++ b/packages/slate/src/constants/model-types.js @@ -14,6 +14,7 @@ const MODEL_TYPES = { INLINE: '@@__SLATE_INLINE__@@', LEAF: '@@__SLATE_LEAF__@@', MARK: '@@__SLATE_MARK__@@', + OPERATION: '@@__SLATE_OPERATION__@@', RANGE: '@@__SLATE_RANGE__@@', SCHEMA: '@@__SLATE_SCHEMA__@@', STACK: '@@__SLATE_STACK__@@', diff --git a/packages/slate/src/constants/operation-attributes.js b/packages/slate/src/constants/operation-attributes.js new file mode 100644 index 000000000..6f07057d2 --- /dev/null +++ b/packages/slate/src/constants/operation-attributes.js @@ -0,0 +1,94 @@ + +/** + * Slate operation attributes. + * + * @type {Array} + */ + +const OPERATION_ATTRIBUTES = { + add_mark: [ + 'value', + 'path', + 'offset', + 'length', + 'mark', + ], + insert_node: [ + 'value', + 'path', + 'node', + ], + insert_text: [ + 'value', + 'path', + 'offset', + 'text', + 'marks', + ], + merge_node: [ + 'value', + 'path', + 'position', + ], + move_node: [ + 'value', + 'path', + 'newPath', + ], + remove_mark: [ + 'value', + 'path', + 'offset', + 'length', + 'mark', + ], + remove_node: [ + 'value', + 'path', + 'node', + ], + remove_text: [ + 'value', + 'path', + 'offset', + 'text', + 'marks', + ], + set_mark: [ + 'value', + 'path', + 'offset', + 'length', + 'mark', + 'properties', + ], + set_node: [ + 'value', + 'path', + 'node', + 'properties', + ], + set_selection: [ + 'value', + 'selection', + 'properties', + ], + set_value: [ + 'value', + 'properties', + ], + split_node: [ + 'value', + 'path', + 'position', + 'target', + ], +} + +/** + * Export. + * + * @type {Object} + */ + +export default OPERATION_ATTRIBUTES diff --git a/packages/slate/src/index.js b/packages/slate/src/index.js index 1a30cbab6..c2e25ab68 100644 --- a/packages/slate/src/index.js +++ b/packages/slate/src/index.js @@ -9,6 +9,7 @@ import Inline from './models/inline' import Leaf from './models/leaf' import Mark from './models/mark' import Node from './models/node' +import Operation from './models/operation' import Operations from './operations' import Range from './models/range' import Schema from './models/schema' @@ -34,6 +35,7 @@ export { Leaf, Mark, Node, + Operation, Operations, Range, Schema, @@ -55,6 +57,7 @@ export default { Leaf, Mark, Node, + Operation, Operations, Range, Schema, diff --git a/packages/slate/src/models/change.js b/packages/slate/src/models/change.js index c12ecc6a7..af2c58253 100644 --- a/packages/slate/src/models/change.js +++ b/packages/slate/src/models/change.js @@ -1,9 +1,11 @@ import Debug from 'debug' +import isPlainObject from 'is-plain-object' import pick from 'lodash/pick' import MODEL_TYPES from '../constants/model-types' import Changes from '../changes' +import Operation from './operation' import apply from '../operations/apply' /** @@ -71,6 +73,13 @@ class Change { let { value } = this let { history } = value + // Add in the current `value` in case the operation was serialized. + if (isPlainObject(operation)) { + operation = { ...operation, value } + } + + operation = Operation.create(operation) + // Default options to the change-level flags, this allows for setting // specific options for all of the operations of a given change. options = { ...flags, ...options } diff --git a/packages/slate/src/models/operation.js b/packages/slate/src/models/operation.js new file mode 100644 index 000000000..6cc29fd63 --- /dev/null +++ b/packages/slate/src/models/operation.js @@ -0,0 +1,296 @@ + +import isPlainObject from 'is-plain-object' +import { List, Record } from 'immutable' + +import MODEL_TYPES from '../constants/model-types' +import OPERATION_ATTRIBUTES from '../constants/operation-attributes' +import Mark from './mark' +import Node from './node' +import Range from './range' +import Value from './value' + +/** + * Default properties. + * + * @type {Object} + */ + +const DEFAULTS = { + length: undefined, + mark: undefined, + marks: undefined, + newPath: undefined, + node: undefined, + offset: undefined, + path: undefined, + position: undefined, + properties: undefined, + selection: undefined, + target: undefined, + text: undefined, + type: undefined, + value: undefined, +} + +/** + * Operation. + * + * @type {Operation} + */ + +class Operation extends Record(DEFAULTS) { + + /** + * Create a new `Operation` with `attrs`. + * + * @param {Object|Array|List|String|Operation} attrs + * @return {Operation} + */ + + static create(attrs = {}) { + if (Operation.isOperation(attrs)) { + return attrs + } + + if (isPlainObject(attrs)) { + return Operation.fromJSON(attrs) + } + + throw new Error(`\`Operation.create\` only accepts objects or operations, but you passed it: ${attrs}`) + } + + /** + * Create a list of `Operations` from `elements`. + * + * @param {Array|List} elements + * @return {List} + */ + + static createList(elements = []) { + if (List.isList(elements) || Array.isArray(elements)) { + const list = new List(elements.map(Operation.create)) + return list + } + + throw new Error(`\`Operation.createList\` only accepts arrays or lists, but you passed it: ${elements}`) + } + + /** + * Create a `Operation` from a JSON `object`. + * + * @param {Object|Operation} object + * @return {Operation} + */ + + static fromJSON(object) { + if (Operation.isOperation(object)) { + return object + } + + const { type, value } = object + const ATTRIBUTES = OPERATION_ATTRIBUTES[type] + const attrs = { type } + + if (!ATTRIBUTES) { + throw new Error(`\`Operation.fromJSON\` was passed an unrecognized operation type: "${type}"`) + } + + for (const key of ATTRIBUTES) { + let v = object[key] + + if (v === undefined) { + // Skip keys for objects that should not be serialized, and are only used + // for providing the local-only invert behavior for the history stack. + if (key == 'document') continue + if (key == 'selection') continue + if (key == 'node' && type != 'insert_node') continue + if (key == 'target' && type == 'split_node') continue + + throw new Error(`\`Operation.fromJSON\` was passed a "${type}" operation without the required "${key}" attribute.`) + } + + if (key == 'mark') { + v = Mark.create(v) + } + + if (key == 'marks' && v != null) { + v = Mark.createSet(v) + } + + if (key == 'node') { + v = Node.create(v) + } + + if (key == 'selection') { + v = Range.create(v) + } + + if (key == 'value') { + v = Value.create(v) + } + + if (key == 'properties' && type == 'set_mark') { + v = Mark.createProperties(v) + } + + if (key == 'properties' && type == 'set_node') { + v = Node.createProperties(v) + } + + if (key == 'properties' && type == 'set_selection') { + const { anchorKey, focusKey, ...rest } = v + v = Range.createProperties(rest) + + if (anchorKey !== undefined) { + v.anchorPath = anchorKey === null + ? null + : value.document.getPath(anchorKey) + } + + if (focusKey !== undefined) { + v.focusPath = focusKey === null + ? null + : value.document.getPath(focusKey) + } + } + + if (key == 'properties' && type == 'set_value') { + v = Value.createProperties(v) + } + + attrs[key] = v + } + + const node = new Operation(attrs) + return node + } + + /** + * Alias `fromJS`. + */ + + static fromJS = Operation.fromJSON + + /** + * Check if `any` is a `Operation`. + * + * @param {Any} any + * @return {Boolean} + */ + + static isOperation(any) { + return !!(any && any[MODEL_TYPES.OPERATION]) + } + + /** + * Check if `any` is a listĀ of operations. + * + * @param {Any} any + * @return {Boolean} + */ + + static isOperationList(any) { + return List.isList(any) && any.every(item => Operation.isOperation(item)) + } + + /** + * Get the node's kind. + * + * @return {String} + */ + + get kind() { + return 'operation' + } + + /** + * Return a JSON representation of the operation. + * + * @param {Object} options + * @return {Object} + */ + + toJSON(options = {}) { + const { kind, type } = this + const object = { kind, type } + const ATTRIBUTES = OPERATION_ATTRIBUTES[type] + + for (const key of ATTRIBUTES) { + let value = this[key] + + // Skip keys for objects that should not be serialized, and are only used + // for providing the local-only invert behavior for the history stack. + if (key == 'document') continue + if (key == 'selection') continue + if (key == 'value') continue + if (key == 'node' && type != 'insert_node') continue + if (key == 'target' && type == 'split_node') continue + + if (key == 'mark' || key == 'marks' || key == 'node') { + value = value.toJSON() + } + + if (key == 'properties' && type == 'set_mark') { + const v = {} + if ('data' in value) v.data = value.data.toJS() + if ('type' in value) v.type = value.type + value = v + } + + if (key == 'properties' && type == 'set_node') { + const v = {} + if ('data' in value) v.data = value.data.toJS() + if ('isVoid' in value) v.isVoid = value.isVoid + if ('type' in value) v.type = value.type + value = v + } + + if (key == 'properties' && type == 'set_selection') { + const v = {} + if ('anchorOffset' in value) v.anchorOffset = value.anchorOffset + if ('anchorPath' in value) v.anchorPath = value.anchorPath + if ('focusOffset' in value) v.focusOffset = value.focusOffset + if ('focusPath' in value) v.focusPath = value.focusPath + if ('isBackward' in value) v.isBackward = value.isBackward + if ('isFocused' in value) v.isFocused = value.isFocused + if ('marks' in value) v.marks = value.marks == null ? null : value.marks.toJSON() + value = v + } + + if (key == 'properties' && type == 'set_value') { + const v = {} + if ('data' in value) v.data = value.data.toJS() + if ('decorations' in value) v.decorations = value.decorations.toJS() + if ('schema' in value) v.schema = value.schema.toJS() + value = v + } + + object[key] = value + } + + return object + } + + /** + * Alias `toJS`. + */ + + toJS(options) { + return this.toJSON(options) + } + +} + +/** + * Attach a pseudo-symbol for type checking. + */ + +Operation.prototype[MODEL_TYPES.OPERATION] = true + +/** + * Export. + * + * @type {Operation} + */ + +export default Operation diff --git a/packages/slate/src/models/range.js b/packages/slate/src/models/range.js index 5b6216545..2460c3016 100644 --- a/packages/slate/src/models/range.js +++ b/packages/slate/src/models/range.js @@ -89,11 +89,13 @@ class Range extends Record(DEFAULTS) { const props = {} if ('anchorKey' in attrs) props.anchorKey = attrs.anchorKey if ('anchorOffset' in attrs) props.anchorOffset = attrs.anchorOffset + if ('anchorPath' in attrs) props.anchorPath = attrs.anchorPath if ('focusKey' in attrs) props.focusKey = attrs.focusKey if ('focusOffset' in attrs) props.focusOffset = attrs.focusOffset + if ('focusPath' in attrs) props.focusPath = attrs.focusPath if ('isBackward' in attrs) props.isBackward = attrs.isBackward if ('isFocused' in attrs) props.isFocused = attrs.isFocused - if ('marks' in attrs) props.marks = attrs.marks + if ('marks' in attrs) props.marks = attrs.marks == null ? null : Mark.createSet(attrs.marks) return props } diff --git a/packages/slate/src/operations/apply.js b/packages/slate/src/operations/apply.js index 3e3333eea..90bbcacff 100644 --- a/packages/slate/src/operations/apply.js +++ b/packages/slate/src/operations/apply.js @@ -1,8 +1,7 @@ import Debug from 'debug' -import Node from '../models/node' -import Mark from '../models/mark' +import Operation from '../models/operation' /** * Debug. @@ -24,13 +23,12 @@ const APPLIERS = { * Add mark to text at `offset` and `length` in node by `path`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ add_mark(value, operation) { - const { path, offset, length } = operation - const mark = Mark.create(operation.mark) + const { path, offset, length, mark } = operation let { document } = value let node = document.assertPath(path) node = node.addMark(offset, length, mark) @@ -43,13 +41,12 @@ const APPLIERS = { * Insert a `node` at `index` in a node by `path`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ insert_node(value, operation) { - const { path } = operation - const node = Node.create(operation.node) + const { path, node } = operation const index = path[path.length - 1] const rest = path.slice(0, -1) let { document } = value @@ -64,16 +61,12 @@ const APPLIERS = { * Insert `text` at `offset` in node by `path`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ insert_text(value, operation) { - const { path, offset, text } = operation - - let { marks } = operation - if (Array.isArray(marks)) marks = Mark.createSet(marks) - + const { path, offset, text, marks } = operation let { document, selection } = value const { anchorKey, focusKey, anchorOffset, focusOffset } = selection let node = document.assertPath(path) @@ -98,7 +91,7 @@ const APPLIERS = { * Merge a node at `path` with the previous node. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ @@ -146,7 +139,7 @@ const APPLIERS = { * Move a node by `path` to `newPath`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ @@ -202,13 +195,12 @@ const APPLIERS = { * Remove mark from text at `offset` and `length` in node by `path`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ remove_mark(value, operation) { - const { path, offset, length } = operation - const mark = Mark.create(operation.mark) + const { path, offset, length, mark } = operation let { document } = value let node = document.assertPath(path) node = node.removeMark(offset, length, mark) @@ -221,7 +213,7 @@ const APPLIERS = { * Remove a node by `path`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ @@ -230,6 +222,7 @@ const APPLIERS = { let { document, selection } = value const { startKey, endKey } = selection const node = document.assertPath(path) + // If the selection is set, check to see if it needs to be updated. if (selection.isSet) { const hasStartNode = node.hasNode(startKey) @@ -282,7 +275,7 @@ const APPLIERS = { * Remove `text` at `offset` in node by `path`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ @@ -294,7 +287,6 @@ const APPLIERS = { const { anchorKey, focusKey, anchorOffset, focusOffset } = selection let node = document.assertPath(path) - // Update the selection. if (anchorKey == node.key && anchorOffset >= rangeOffset) { selection = selection.moveAnchor(-length) } @@ -313,13 +305,12 @@ const APPLIERS = { * Set `properties` on mark on text at `offset` and `length` in node by `path`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ set_mark(value, operation) { - const { path, offset, length, properties } = operation - const mark = Mark.create(operation.mark) + const { path, offset, length, mark, properties } = operation let { document } = value let node = document.assertPath(path) node = node.updateMark(offset, length, mark, properties) @@ -332,7 +323,7 @@ const APPLIERS = { * Set `properties` on a node by `path`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ @@ -340,11 +331,6 @@ const APPLIERS = { const { path, properties } = operation let { document } = value let node = document.assertPath(path) - - // Delete properties that are not allowed to be updated. - delete properties.nodes - delete properties.key - node = node.merge(properties) document = document.updateNode(node) value = value.set('document', document) @@ -355,33 +341,24 @@ const APPLIERS = { * Set `properties` on the selection. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ set_selection(value, operation) { - const properties = { ...operation.properties } + const { properties } = operation + const { anchorPath, focusPath, ...props } = properties let { document, selection } = value - if (properties.marks != null) { - properties.marks = Mark.createSet(properties.marks) + if (anchorPath !== undefined) { + props.anchorKey = anchorPath === null ? null : document.assertPath(anchorPath).key } - if (properties.anchorPath !== undefined) { - properties.anchorKey = properties.anchorPath === null - ? null - : document.assertPath(properties.anchorPath).key - delete properties.anchorPath + if (focusPath !== undefined) { + props.focusKey = focusPath === null ? null : document.assertPath(focusPath).key } - if (properties.focusPath !== undefined) { - properties.focusKey = properties.focusPath === null - ? null - : document.assertPath(properties.focusPath).key - delete properties.focusPath - } - - selection = selection.merge(properties) + selection = selection.merge(props) selection = selection.normalize(document) value = value.set('selection', selection) return value @@ -391,18 +368,12 @@ const APPLIERS = { * Set `properties` on `value`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ set_value(value, operation) { const { properties } = operation - - // Delete properties that are not allowed to be updated. - delete properties.document - delete properties.selection - delete properties.history - value = value.merge(properties) return value }, @@ -411,7 +382,7 @@ const APPLIERS = { * Split a node by `path` at `offset`. * * @param {Value} value - * @param {Object} operation + * @param {Operation} operation * @return {Value} */ @@ -462,11 +433,12 @@ const APPLIERS = { * Apply an `operation` to a `value`. * * @param {Value} value - * @param {Object} operation + * @param {Object|Operation} operation * @return {Value} value */ function applyOperation(value, operation) { + operation = Operation.create(operation) const { type } = operation const apply = APPLIERS[type] diff --git a/packages/slate/src/operations/invert.js b/packages/slate/src/operations/invert.js index 08a7970c0..22913616a 100644 --- a/packages/slate/src/operations/invert.js +++ b/packages/slate/src/operations/invert.js @@ -2,6 +2,8 @@ import Debug from 'debug' import pick from 'lodash/pick' +import Operation from '../models/operation' + /** * Debug. * @@ -18,6 +20,7 @@ const debug = Debug('slate:operation:invert') */ function invertOperation(op) { + op = Operation.create(op) const { type } = op debug(type, op) @@ -26,10 +29,8 @@ function invertOperation(op) { */ if (type == 'insert_node') { - return { - ...op, - type: 'remove_node', - } + const inverse = op.set('type', 'remove_node') + return inverse } /** @@ -37,10 +38,8 @@ function invertOperation(op) { */ if (type == 'remove_node') { - return { - ...op, - type: 'insert_node', - } + const inverse = op.set('type', 'insert_node') + return inverse } /** @@ -48,11 +47,9 @@ function invertOperation(op) { */ if (type == 'move_node') { - return { - ...op, - path: op.newPath, - newPath: op.path, - } + const { newPath, path } = op + const inverse = op.set('path', newPath).set('newPath', path) + return inverse } /** @@ -63,11 +60,9 @@ function invertOperation(op) { const { path } = op const { length } = path const last = length - 1 - return { - ...op, - type: 'split_node', - path: path.slice(0, last).concat([path[last] - 1]), - } + const inversePath = path.slice(0, last).concat([path[last] - 1]) + const inverse = op.set('type', 'split_node').set('path', inversePath) + return inverse } /** @@ -78,11 +73,9 @@ function invertOperation(op) { const { path } = op const { length } = path const last = length - 1 - return { - ...op, - type: 'merge_node', - path: path.slice(0, last).concat([path[last] + 1]), - } + const inversePath = path.slice(0, last).concat([path[last] + 1]) + const inverse = op.set('type', 'merge_node').set('path', inversePath) + return inverse } /** @@ -91,11 +84,10 @@ function invertOperation(op) { if (type == 'set_node') { const { properties, node } = op - return { - ...op, - node: node.merge(properties), - properties: pick(node, Object.keys(properties)), - } + const inverseNode = node.merge(properties) + const inverseProperties = pick(node, Object.keys(properties)) + const inverse = op.set('node', inverseNode).set('properties', inverseProperties) + return inverse } /** @@ -103,10 +95,8 @@ function invertOperation(op) { */ if (type == 'insert_text') { - return { - ...op, - type: 'remove_text', - } + const inverse = op.set('type', 'remove_text') + return inverse } /** @@ -114,10 +104,8 @@ function invertOperation(op) { */ if (type == 'remove_text') { - return { - ...op, - type: 'insert_text', - } + const inverse = op.set('type', 'insert_text') + return inverse } /** @@ -125,10 +113,8 @@ function invertOperation(op) { */ if (type == 'add_mark') { - return { - ...op, - type: 'remove_mark', - } + const inverse = op.set('type', 'remove_mark') + return inverse } /** @@ -136,10 +122,8 @@ function invertOperation(op) { */ if (type == 'remove_mark') { - return { - ...op, - type: 'add_mark', - } + const inverse = op.set('type', 'add_mark') + return inverse } /** @@ -148,11 +132,10 @@ function invertOperation(op) { if (type == 'set_mark') { const { properties, mark } = op - return { - ...op, - mark: mark.merge(properties), - properties: pick(mark, Object.keys(properties)), - } + const inverseMark = mark.merge(properties) + const inverseProperties = pick(mark, Object.keys(properties)) + const inverse = op.set('mark', inverseMark).set('properties', inverseProperties) + return inverse } /** @@ -160,13 +143,40 @@ function invertOperation(op) { */ if (type == 'set_selection') { - const { properties, selection } = op - const inverse = { - ...op, - selection: { ...selection, ...properties }, - properties: pick(selection, Object.keys(properties)), + const { properties, selection, value } = op + const { anchorPath, focusPath, ...props } = properties + const { document } = value + + if (anchorPath !== undefined) { + props.anchorKey = anchorPath === null + ? null + : document.assertPath(anchorPath).key } + if (focusPath !== undefined) { + props.focusKey = focusPath === null + ? null + : document.assertPath(focusPath).key + } + + const inverseSelection = selection.merge(props) + const inverseProps = pick(selection, Object.keys(props)) + + if (anchorPath !== undefined) { + inverseProps.anchorPath = inverseProps.anchorKey === null + ? null + : document.getPath(inverseProps.anchorKey) + delete inverseProps.anchorKey + } + + if (focusPath !== undefined) { + inverseProps.focusPath = inverseProps.focusKey === null + ? null + : document.getPath(inverseProps.focusKey) + delete inverseProps.focusKey + } + + const inverse = op.set('selection', inverseSelection).set('properties', inverseProps) return inverse } @@ -176,18 +186,11 @@ function invertOperation(op) { if (type == 'set_value') { const { properties, value } = op - return { - ...op, - value: value.merge(properties), - properties: pick(value, Object.keys(properties)), - } + const inverseValue = value.merge(properties) + const inverseProperties = pick(value, Object.keys(properties)) + const inverse = op.set('value', inverseValue).set('properties', inverseProperties) + return inverse } - - /** - * Unknown. - */ - - throw new Error(`Unknown op type: "${type}".`) } /**