1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-31 19:01:54 +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) => { 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) this.two.applyOperations(ops)
} }
@@ -255,7 +258,10 @@ class SyncingOperationsExample extends React.Component {
*/ */
onTwoChange = (change) => { 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) this.one.applyOperations(ops)
} }

View File

@@ -56,6 +56,7 @@ Changes.addMarkByKey = (change, key, offset, length, mark, options = {}) => {
operations.push({ operations.push({
type: 'add_mark', type: 'add_mark',
value,
path, path,
offset: start, offset: start,
length: end - start, length: end - start,
@@ -113,6 +114,7 @@ Changes.insertNodeByKey = (change, key, index, node, options = {}) => {
change.applyOperation({ change.applyOperation({
type: 'insert_node', type: 'insert_node',
value,
path: [...path, index], path: [...path, index],
node, node,
}) })
@@ -144,6 +146,7 @@ Changes.insertTextByKey = (change, key, offset, text, marks, options = {}) => {
change.applyOperation({ change.applyOperation({
type: 'insert_text', type: 'insert_text',
value,
path, path,
offset, offset,
text, text,
@@ -180,6 +183,7 @@ Changes.mergeNodeByKey = (change, key, options = {}) => {
change.applyOperation({ change.applyOperation({
type: 'merge_node', type: 'merge_node',
value,
path, path,
position, position,
}) })
@@ -211,6 +215,7 @@ Changes.moveNodeByKey = (change, key, newKey, newIndex, options = {}) => {
change.applyOperation({ change.applyOperation({
type: 'move_node', type: 'move_node',
value,
path, path,
newPath: [...newPath, newIndex], newPath: [...newPath, newIndex],
}) })
@@ -265,6 +270,7 @@ Changes.removeMarkByKey = (change, key, offset, length, mark, options = {}) => {
operations.push({ operations.push({
type: 'remove_mark', type: 'remove_mark',
value,
path, path,
offset: start, offset: start,
length: end - start, length: end - start,
@@ -320,6 +326,7 @@ Changes.removeNodeByKey = (change, key, options = {}) => {
change.applyOperation({ change.applyOperation({
type: 'remove_node', type: 'remove_node',
value,
path, path,
node, node,
}) })
@@ -371,6 +378,7 @@ Changes.removeTextByKey = (change, key, offset, length, options = {}) => {
removals.push({ removals.push({
type: 'remove_text', type: 'remove_text',
value,
path, path,
offset: start, offset: start,
text: string, text: string,
@@ -434,6 +442,7 @@ Changes.setMarkByKey = (change, key, offset, length, mark, properties, options =
change.applyOperation({ change.applyOperation({
type: 'set_mark', type: 'set_mark',
value,
path, path,
offset, offset,
length, length,
@@ -467,6 +476,7 @@ Changes.setNodeByKey = (change, key, properties, options = {}) => {
change.applyOperation({ change.applyOperation({
type: 'set_node', type: 'set_node',
value,
path, path,
node, node,
properties, properties,
@@ -495,6 +505,7 @@ Changes.splitNodeByKey = (change, key, position, options = {}) => {
change.applyOperation({ change.applyOperation({
type: 'split_node', type: 'split_node',
value,
path, path,
position, position,
target, target,

View File

@@ -31,13 +31,15 @@ Changes.redo = (change) => {
// Replay the next operations. // Replay the next operations.
next.forEach((op) => { next.forEach((op) => {
// When the operation mutates selection, omit its `isFocused` props to const { type, properties } = op
// prevent editor focus changing during continuously redoing.
let { type, properties } = op // When the operation mutates the selection, omit its `isFocused` value to
if (type === 'set_selection') { // prevent the editor focus from changing during redoing.
properties = omit(properties, 'isFocused') 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. // Update the history.
@@ -67,14 +69,16 @@ Changes.undo = (change) => {
redos = redos.push(previous) redos = redos.push(previous)
// Replay the inverse of the previous operations. // Replay the inverse of the previous operations.
previous.slice().reverse().map(invert).forEach((inverseOp) => { previous.slice().reverse().map(invert).forEach((inverse) => {
// When the operation mutates selection, omit its `isFocused` props to const { type, properties } = inverse
// prevent editor focus changing during continuously undoing.
let { type, properties } = inverseOp // When the operation mutates the selection, omit its `isFocused` value to
if (type === 'set_selection') { // prevent the editor focus from changing during undoing.
properties = omit(properties, 'isFocused') 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. // Update the history.

View File

@@ -38,29 +38,12 @@ Changes.select = (change, properties, options = {}) => {
props[k] = properties[k] 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 // If the selection moves, clear any marks, unless the new selection
// properties change the marks in some way. // properties change the marks in some way.
const moved = [ const moved = [
'anchorPath', 'anchorKey',
'anchorOffset', 'anchorOffset',
'focusPath', 'focusKey',
'focusOffset', 'focusOffset',
].some(p => props.hasOwnProperty(p)) ].some(p => props.hasOwnProperty(p))
@@ -76,6 +59,7 @@ Changes.select = (change, properties, options = {}) => {
// Apply the operation. // Apply the operation.
change.applyOperation({ change.applyOperation({
type: 'set_selection', type: 'set_selection',
value,
properties: props, properties: props,
selection: sel, selection: sel,
}, snapshot ? { skip: false, merge: false } : {}) }, snapshot ? { skip: false, merge: false } : {})

View File

@@ -14,6 +14,7 @@ const MODEL_TYPES = {
INLINE: '@@__SLATE_INLINE__@@', INLINE: '@@__SLATE_INLINE__@@',
LEAF: '@@__SLATE_LEAF__@@', LEAF: '@@__SLATE_LEAF__@@',
MARK: '@@__SLATE_MARK__@@', MARK: '@@__SLATE_MARK__@@',
OPERATION: '@@__SLATE_OPERATION__@@',
RANGE: '@@__SLATE_RANGE__@@', RANGE: '@@__SLATE_RANGE__@@',
SCHEMA: '@@__SLATE_SCHEMA__@@', SCHEMA: '@@__SLATE_SCHEMA__@@',
STACK: '@@__SLATE_STACK__@@', 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 Leaf from './models/leaf'
import Mark from './models/mark' import Mark from './models/mark'
import Node from './models/node' import Node from './models/node'
import Operation from './models/operation'
import Operations from './operations' import Operations from './operations'
import Range from './models/range' import Range from './models/range'
import Schema from './models/schema' import Schema from './models/schema'
@@ -34,6 +35,7 @@ export {
Leaf, Leaf,
Mark, Mark,
Node, Node,
Operation,
Operations, Operations,
Range, Range,
Schema, Schema,
@@ -55,6 +57,7 @@ export default {
Leaf, Leaf,
Mark, Mark,
Node, Node,
Operation,
Operations, Operations,
Range, Range,
Schema, Schema,

View File

@@ -1,9 +1,11 @@
import Debug from 'debug' import Debug from 'debug'
import isPlainObject from 'is-plain-object'
import pick from 'lodash/pick' import pick from 'lodash/pick'
import MODEL_TYPES from '../constants/model-types' import MODEL_TYPES from '../constants/model-types'
import Changes from '../changes' import Changes from '../changes'
import Operation from './operation'
import apply from '../operations/apply' import apply from '../operations/apply'
/** /**
@@ -71,6 +73,13 @@ class Change {
let { value } = this let { value } = this
let { history } = value 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 // Default options to the change-level flags, this allows for setting
// specific options for all of the operations of a given change. // specific options for all of the operations of a given change.
options = { ...flags, ...options } 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 = {} const props = {}
if ('anchorKey' in attrs) props.anchorKey = attrs.anchorKey if ('anchorKey' in attrs) props.anchorKey = attrs.anchorKey
if ('anchorOffset' in attrs) props.anchorOffset = attrs.anchorOffset 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 ('focusKey' in attrs) props.focusKey = attrs.focusKey
if ('focusOffset' in attrs) props.focusOffset = attrs.focusOffset 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 ('isBackward' in attrs) props.isBackward = attrs.isBackward
if ('isFocused' in attrs) props.isFocused = attrs.isFocused 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 return props
} }

View File

@@ -1,8 +1,7 @@
import Debug from 'debug' import Debug from 'debug'
import Node from '../models/node' import Operation from '../models/operation'
import Mark from '../models/mark'
/** /**
* Debug. * Debug.
@@ -24,13 +23,12 @@ const APPLIERS = {
* Add mark to text at `offset` and `length` in node by `path`. * Add mark to text at `offset` and `length` in node by `path`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
add_mark(value, operation) { add_mark(value, operation) {
const { path, offset, length } = operation const { path, offset, length, mark } = operation
const mark = Mark.create(operation.mark)
let { document } = value let { document } = value
let node = document.assertPath(path) let node = document.assertPath(path)
node = node.addMark(offset, length, mark) node = node.addMark(offset, length, mark)
@@ -43,13 +41,12 @@ const APPLIERS = {
* Insert a `node` at `index` in a node by `path`. * Insert a `node` at `index` in a node by `path`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
insert_node(value, operation) { insert_node(value, operation) {
const { path } = operation const { path, node } = operation
const node = Node.create(operation.node)
const index = path[path.length - 1] const index = path[path.length - 1]
const rest = path.slice(0, -1) const rest = path.slice(0, -1)
let { document } = value let { document } = value
@@ -64,16 +61,12 @@ const APPLIERS = {
* Insert `text` at `offset` in node by `path`. * Insert `text` at `offset` in node by `path`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
insert_text(value, operation) { insert_text(value, operation) {
const { path, offset, text } = operation const { path, offset, text, marks } = operation
let { marks } = operation
if (Array.isArray(marks)) marks = Mark.createSet(marks)
let { document, selection } = value let { document, selection } = value
const { anchorKey, focusKey, anchorOffset, focusOffset } = selection const { anchorKey, focusKey, anchorOffset, focusOffset } = selection
let node = document.assertPath(path) let node = document.assertPath(path)
@@ -98,7 +91,7 @@ const APPLIERS = {
* Merge a node at `path` with the previous node. * Merge a node at `path` with the previous node.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
@@ -146,7 +139,7 @@ const APPLIERS = {
* Move a node by `path` to `newPath`. * Move a node by `path` to `newPath`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
@@ -202,13 +195,12 @@ const APPLIERS = {
* Remove mark from text at `offset` and `length` in node by `path`. * Remove mark from text at `offset` and `length` in node by `path`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
remove_mark(value, operation) { remove_mark(value, operation) {
const { path, offset, length } = operation const { path, offset, length, mark } = operation
const mark = Mark.create(operation.mark)
let { document } = value let { document } = value
let node = document.assertPath(path) let node = document.assertPath(path)
node = node.removeMark(offset, length, mark) node = node.removeMark(offset, length, mark)
@@ -221,7 +213,7 @@ const APPLIERS = {
* Remove a node by `path`. * Remove a node by `path`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
@@ -230,6 +222,7 @@ const APPLIERS = {
let { document, selection } = value let { document, selection } = value
const { startKey, endKey } = selection const { startKey, endKey } = selection
const node = document.assertPath(path) const node = document.assertPath(path)
// If the selection is set, check to see if it needs to be updated. // If the selection is set, check to see if it needs to be updated.
if (selection.isSet) { if (selection.isSet) {
const hasStartNode = node.hasNode(startKey) const hasStartNode = node.hasNode(startKey)
@@ -282,7 +275,7 @@ const APPLIERS = {
* Remove `text` at `offset` in node by `path`. * Remove `text` at `offset` in node by `path`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
@@ -294,7 +287,6 @@ const APPLIERS = {
const { anchorKey, focusKey, anchorOffset, focusOffset } = selection const { anchorKey, focusKey, anchorOffset, focusOffset } = selection
let node = document.assertPath(path) let node = document.assertPath(path)
// Update the selection.
if (anchorKey == node.key && anchorOffset >= rangeOffset) { if (anchorKey == node.key && anchorOffset >= rangeOffset) {
selection = selection.moveAnchor(-length) selection = selection.moveAnchor(-length)
} }
@@ -313,13 +305,12 @@ const APPLIERS = {
* Set `properties` on mark on text at `offset` and `length` in node by `path`. * Set `properties` on mark on text at `offset` and `length` in node by `path`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
set_mark(value, operation) { set_mark(value, operation) {
const { path, offset, length, properties } = operation const { path, offset, length, mark, properties } = operation
const mark = Mark.create(operation.mark)
let { document } = value let { document } = value
let node = document.assertPath(path) let node = document.assertPath(path)
node = node.updateMark(offset, length, mark, properties) node = node.updateMark(offset, length, mark, properties)
@@ -332,7 +323,7 @@ const APPLIERS = {
* Set `properties` on a node by `path`. * Set `properties` on a node by `path`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
@@ -340,11 +331,6 @@ const APPLIERS = {
const { path, properties } = operation const { path, properties } = operation
let { document } = value let { document } = value
let node = document.assertPath(path) let node = document.assertPath(path)
// Delete properties that are not allowed to be updated.
delete properties.nodes
delete properties.key
node = node.merge(properties) node = node.merge(properties)
document = document.updateNode(node) document = document.updateNode(node)
value = value.set('document', document) value = value.set('document', document)
@@ -355,33 +341,24 @@ const APPLIERS = {
* Set `properties` on the selection. * Set `properties` on the selection.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
set_selection(value, operation) { set_selection(value, operation) {
const properties = { ...operation.properties } const { properties } = operation
const { anchorPath, focusPath, ...props } = properties
let { document, selection } = value let { document, selection } = value
if (properties.marks != null) { if (anchorPath !== undefined) {
properties.marks = Mark.createSet(properties.marks) props.anchorKey = anchorPath === null ? null : document.assertPath(anchorPath).key
} }
if (properties.anchorPath !== undefined) { if (focusPath !== undefined) {
properties.anchorKey = properties.anchorPath === null props.focusKey = focusPath === null ? null : document.assertPath(focusPath).key
? null
: document.assertPath(properties.anchorPath).key
delete properties.anchorPath
} }
if (properties.focusPath !== undefined) { selection = selection.merge(props)
properties.focusKey = properties.focusPath === null
? null
: document.assertPath(properties.focusPath).key
delete properties.focusPath
}
selection = selection.merge(properties)
selection = selection.normalize(document) selection = selection.normalize(document)
value = value.set('selection', selection) value = value.set('selection', selection)
return value return value
@@ -391,18 +368,12 @@ const APPLIERS = {
* Set `properties` on `value`. * Set `properties` on `value`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
set_value(value, operation) { set_value(value, operation) {
const { properties } = 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) value = value.merge(properties)
return value return value
}, },
@@ -411,7 +382,7 @@ const APPLIERS = {
* Split a node by `path` at `offset`. * Split a node by `path` at `offset`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Operation} operation
* @return {Value} * @return {Value}
*/ */
@@ -462,11 +433,12 @@ const APPLIERS = {
* Apply an `operation` to a `value`. * Apply an `operation` to a `value`.
* *
* @param {Value} value * @param {Value} value
* @param {Object} operation * @param {Object|Operation} operation
* @return {Value} value * @return {Value} value
*/ */
function applyOperation(value, operation) { function applyOperation(value, operation) {
operation = Operation.create(operation)
const { type } = operation const { type } = operation
const apply = APPLIERS[type] const apply = APPLIERS[type]

View File

@@ -2,6 +2,8 @@
import Debug from 'debug' import Debug from 'debug'
import pick from 'lodash/pick' import pick from 'lodash/pick'
import Operation from '../models/operation'
/** /**
* Debug. * Debug.
* *
@@ -18,6 +20,7 @@ const debug = Debug('slate:operation:invert')
*/ */
function invertOperation(op) { function invertOperation(op) {
op = Operation.create(op)
const { type } = op const { type } = op
debug(type, op) debug(type, op)
@@ -26,10 +29,8 @@ function invertOperation(op) {
*/ */
if (type == 'insert_node') { if (type == 'insert_node') {
return { const inverse = op.set('type', 'remove_node')
...op, return inverse
type: 'remove_node',
}
} }
/** /**
@@ -37,10 +38,8 @@ function invertOperation(op) {
*/ */
if (type == 'remove_node') { if (type == 'remove_node') {
return { const inverse = op.set('type', 'insert_node')
...op, return inverse
type: 'insert_node',
}
} }
/** /**
@@ -48,11 +47,9 @@ function invertOperation(op) {
*/ */
if (type == 'move_node') { if (type == 'move_node') {
return { const { newPath, path } = op
...op, const inverse = op.set('path', newPath).set('newPath', path)
path: op.newPath, return inverse
newPath: op.path,
}
} }
/** /**
@@ -63,11 +60,9 @@ function invertOperation(op) {
const { path } = op const { path } = op
const { length } = path const { length } = path
const last = length - 1 const last = length - 1
return { const inversePath = path.slice(0, last).concat([path[last] - 1])
...op, const inverse = op.set('type', 'split_node').set('path', inversePath)
type: 'split_node', return inverse
path: path.slice(0, last).concat([path[last] - 1]),
}
} }
/** /**
@@ -78,11 +73,9 @@ function invertOperation(op) {
const { path } = op const { path } = op
const { length } = path const { length } = path
const last = length - 1 const last = length - 1
return { const inversePath = path.slice(0, last).concat([path[last] + 1])
...op, const inverse = op.set('type', 'merge_node').set('path', inversePath)
type: 'merge_node', return inverse
path: path.slice(0, last).concat([path[last] + 1]),
}
} }
/** /**
@@ -91,11 +84,10 @@ function invertOperation(op) {
if (type == 'set_node') { if (type == 'set_node') {
const { properties, node } = op const { properties, node } = op
return { const inverseNode = node.merge(properties)
...op, const inverseProperties = pick(node, Object.keys(properties))
node: node.merge(properties), const inverse = op.set('node', inverseNode).set('properties', inverseProperties)
properties: pick(node, Object.keys(properties)), return inverse
}
} }
/** /**
@@ -103,10 +95,8 @@ function invertOperation(op) {
*/ */
if (type == 'insert_text') { if (type == 'insert_text') {
return { const inverse = op.set('type', 'remove_text')
...op, return inverse
type: 'remove_text',
}
} }
/** /**
@@ -114,10 +104,8 @@ function invertOperation(op) {
*/ */
if (type == 'remove_text') { if (type == 'remove_text') {
return { const inverse = op.set('type', 'insert_text')
...op, return inverse
type: 'insert_text',
}
} }
/** /**
@@ -125,10 +113,8 @@ function invertOperation(op) {
*/ */
if (type == 'add_mark') { if (type == 'add_mark') {
return { const inverse = op.set('type', 'remove_mark')
...op, return inverse
type: 'remove_mark',
}
} }
/** /**
@@ -136,10 +122,8 @@ function invertOperation(op) {
*/ */
if (type == 'remove_mark') { if (type == 'remove_mark') {
return { const inverse = op.set('type', 'add_mark')
...op, return inverse
type: 'add_mark',
}
} }
/** /**
@@ -148,11 +132,10 @@ function invertOperation(op) {
if (type == 'set_mark') { if (type == 'set_mark') {
const { properties, mark } = op const { properties, mark } = op
return { const inverseMark = mark.merge(properties)
...op, const inverseProperties = pick(mark, Object.keys(properties))
mark: mark.merge(properties), const inverse = op.set('mark', inverseMark).set('properties', inverseProperties)
properties: pick(mark, Object.keys(properties)), return inverse
}
} }
/** /**
@@ -160,13 +143,40 @@ function invertOperation(op) {
*/ */
if (type == 'set_selection') { if (type == 'set_selection') {
const { properties, selection } = op const { properties, selection, value } = op
const inverse = { const { anchorPath, focusPath, ...props } = properties
...op, const { document } = value
selection: { ...selection, ...properties },
properties: pick(selection, Object.keys(properties)), 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 return inverse
} }
@@ -176,18 +186,11 @@ function invertOperation(op) {
if (type == 'set_value') { if (type == 'set_value') {
const { properties, value } = op const { properties, value } = op
return { const inverseValue = value.merge(properties)
...op, const inverseProperties = pick(value, Object.keys(properties))
value: value.merge(properties), const inverse = op.set('value', inverseValue).set('properties', inverseProperties)
properties: pick(value, Object.keys(properties)), return inverse
}
} }
/**
* Unknown.
*/
throw new Error(`Unknown op type: "${type}".`)
} }
/** /**