1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-31 10:51:44 +02:00

add immutable operation model, with serialization (#1409)

* add immutable operation model, with serialization

* fix split node operations, and deserializing operations
This commit is contained in:
Ian Storm Taylor
2017-11-16 11:32:13 -08:00
committed by GitHub
parent 1abc7e74b8
commit f1f07da5e5
12 changed files with 542 additions and 157 deletions

View File

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

View File

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

View File

@@ -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.

View File

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

View File

@@ -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__@@',

View File

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

View File

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

View File

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

View File

@@ -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<Operation|Object>|List<Operation|Object>} elements
* @return {List<Operation>}
*/
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

View File

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

View File

@@ -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]

View File

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