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:
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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.
|
||||
|
@@ -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 } : {})
|
||||
|
@@ -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__@@',
|
||||
|
94
packages/slate/src/constants/operation-attributes.js
Normal file
94
packages/slate/src/constants/operation-attributes.js
Normal 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
|
@@ -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,
|
||||
|
@@ -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 }
|
||||
|
296
packages/slate/src/models/operation.js
Normal file
296
packages/slate/src/models/operation.js
Normal 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
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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]
|
||||
|
||||
|
@@ -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}".`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user