1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-22 23:12:52 +02:00

lots of work on refactoring for history

This commit is contained in:
Ian Storm Taylor
2016-08-30 14:36:31 -07:00
parent 7e333a067c
commit c35630d6ba
9 changed files with 778 additions and 601 deletions

View File

@@ -60,7 +60,6 @@ class Links extends React.Component {
onChange = (state) => {
this.setState({ state })
console.log(state.document.toJS())
}
/**

View File

@@ -1,7 +1,5 @@
import Operations from '../transforms/operations'
import Transforms from '../transforms'
import { List, Record } from 'immutable'
/**
* Transform.
@@ -18,10 +16,13 @@ class Transform {
*/
constructor(properties) {
const { state } = properties
this.initialState = state
const { state, operations = [] } = properties
this.state = state
this.operations = []
operations.forEach(op => {
this.applyOperation(op)
})
}
/**
@@ -34,26 +35,6 @@ class Transform {
return 'transform'
}
/**
* Add an `operation` to the transform that resulted in `state`.
*
* @param {Object} operation
* @return {Transform}
*/
operate(operation) {
const { type } = operation
const fn = Operations[type]
if (!fn) {
throw new Error(`Unknown operation type: "${type}".`)
}
this.state = fn(this.state, operation)
this.operations.push(operation)
return this
}
/**
* Apply the transform and return the new state.
*
@@ -64,38 +45,38 @@ class Transform {
*/
apply(options = {}) {
let { initialState, state, operations } = this
let { history, selection } = state
let { marks } = selection
let { state, operations } = this
let { history } = state
let { undos, redos } = history
// If there are no operations, abort early.
if (!operations.length) return state
// The `isNative` flag allows for natively-handled changes to skip
// rerendering the editor for improved performance.
const isNative = !!options.isNative
// Determine whether we need to create a new snapshot.
const shouldSnapshot = options.snapshot == null
? this.shouldSnapshot()
: options.snapshot
// If we should, save a snapshot into the history before transforming.
// Either create a new snapshot, or push the operations into the previous.
if (shouldSnapshot) {
const snapshot = this.snapshot()
const snapshot = { operations }
undos = undos.push(snapshot)
if (undos.size > 100) undos = undos.take(100)
redos = redos.clear()
history = history.merge({ undos, redos })
state = state.merge({ history })
} else {
const snapshot = undos.peek()
snapshot.operations = snapshot.operations.concat(operations)
}
// Check whether to remove the cursor marks.
// if (options.snapshot !== false && initialState.selection.marks) {
// selection = selection.merge({ marks: null })
// state = state.merge({ selection })
// }
// Apply the "isNative" flag, which is used to allow for natively-handled
// content changes to skip rerendering the editor for performance.
state = state.merge({
isNative: !!options.isNative
})
// Clear the redo stack and constrain the undos stack.
if (undos.size > 100) undos = undos.take(100)
redos = redos.clear()
// Update the state.
history = history.merge({ undos, redos })
state = state.merge({ history, isNative })
return state
}
@@ -114,69 +95,32 @@ class Transform {
// If there isn't a previous state, snapshot.
if (!previous) return true
// If the only operations applied are selection operations, don't snapshot.
const onlySelections = operations.every(op => op.type == 'set_selection')
if (onlySelections) return false
const types = operations.map(op => op.type)
const prevTypes = previous.operations.map(op => op.type)
const edits = types.filter(type => type != 'set_selection')
const prevEdits = prevTypes.filter(type => type != 'set_selection')
// If the current operations aren't one of the "combinable" types, snapshot.
const onlyInsert = operations.every(op => op.type == 'insert_text')
const onlyRemove = operations.every(op => op.type == 'remove_text')
const onlyPrevInsert = previous.operations.every(op => op.type == 'insert_text')
const onlyPrevRemove = previous.operations.every(op => op.type == 'remove_text')
if (onlyInsert && onlyPrevInsert) return false
if (onlyRemove && onlyPrevRemove) return false
const onlySelections = types.every(type => type == 'set_selection')
const onlyInsert = edits.every(type => type == 'insert_text')
const onlyRemove = edits.every(type => type == 'remove_text')
const prevOnlySelections = prevTypes.every(type => type == 'set_selection')
const prevOnlyInsert = prevEdits.every(type => type == 'insert_text')
const prevOnlyRemove = prevEdits.every(type => type == 'remove_text')
// If the only operations applied are selection operations, or if the
// current operations are all text editing, don't snapshot.
if (
(onlySelections) ||
(!prevOnlySelections && onlyInsert && prevOnlyInsert) ||
(!prevOnlySelections && onlyRemove && prevOnlyRemove)
) {
return false
}
// Otherwise, snapshot.
return true
}
/**
* Create a history-ready snapshot of the current state.
*
* @return {Object}
*/
snapshot() {
let { initialState, operations } = this
let { document, selection } = initialState
return { document, selection, operations }
}
/**
* Undo to the previous state in the history.
*
* @return {State} state
*/
undo() {
let { state } = this
let { history } = state
let { undos, redos } = history
// If there's no previous snapshot, return the current state.
let previous = undos.peek()
if (!previous) return state
// Remove the previous snapshot from the undo stack.
undos = undos.pop()
// Snapshot the current state, and move it into the redos stack.
let snapshot = this.snapshot()
redos = redos.push(snapshot)
// Return the previous state, with the updated history.
let { document, selection } = previous
history = history.merge({ undos, redos })
state = state.merge({
document,
selection,
history,
isNative: false
})
return state
}
/**
* Redo to the next state in the history.
*
@@ -192,21 +136,58 @@ class Transform {
let next = redos.peek()
if (!next) return state
// Remove the next history from the redo stack.
// Shift the next state into the undo stack.
redos = redos.pop()
undos = undos.push(next)
// Snapshot the current state, and move it into the undos stack.
let snapshot = this.snapshot()
undos = undos.push(snapshot)
// Replay the next operations.
const { operations } = next
operations.forEach(op => {
this.applyOperation(op)
})
// Return the next state, with the updated history.
let { document, selection } = next
// Update the state's history and force `isNative` to false.
history = history.merge({ undos, redos })
state = state.merge({
document,
selection,
state = this.state.merge({
history,
isNative: false
isNative: false,
})
return state
}
/**
* Undo the previous operations in the history.
*
* @return {State} state
*/
undo() {
let { state } = this
let { history } = state
let { undos, redos } = history
// If there's no previous snapshot, return the current state.
let previous = undos.peek()
if (!previous) return state
// Shift the previous operations into the redo stack.
undos = undos.pop()
redos = redos.push(previous)
// Replay the inverse of the previous operations.
const operations = previous.operations.slice().reverse()
operations.forEach(op => {
op.inverse.forEach(inv => {
this.applyOperation(inv)
})
})
// Update the state's history and force `isNative` to false.
history = history.merge({ undos, redos })
state = this.state.merge({
history,
isNative: false,
})
return state

View File

@@ -568,7 +568,6 @@ function Plugin(options = {}) {
return state
.transform()
.moveTo(selection)
.focus()
.apply()
}

View File

@@ -0,0 +1,297 @@
import uid from '../utils/uid'
/**
* Operations.
*
* @type {Object}
*/
const OPERATIONS = {
// Text operations.
insert_text: insertText,
remove_text: removeText,
// Mark operations.
add_mark: addMark,
remove_mark: removeMark,
set_mark: setMark,
// Node operations.
insert_node: insertNode,
move_node: moveNode,
remove_node: removeNode,
set_node: setNode,
split_node: splitNode,
// Selection operations.
set_selection: setSelection,
}
/**
* Apply an `operation` to the current state.
*
* @param {Transform} transform
* @param {Object} operation
* @return {Transform}
*/
export function applyOperation(transform, operation) {
let { state, operations } = transform
const { type } = operation
const fn = OPERATIONS[type]
if (!fn) {
throw new Error(`Unknown operation type: "${type}".`)
}
transform.state = fn(state, operation)
transform.operations = operations.concat([operation])
return transform
}
/**
* Add mark to text at `offset` and `length` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function addMark(state, operation) {
const { path, offset, length, mark } = operation
let { document } = state
let node = document.assertPath(path)
node = node.addMark(offset, length, mark)
document = document.updateDescendant(node)
state = state.merge({ document })
return state
}
/**
* Insert a `node` at `index` in a node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function insertNode(state, operation) {
const { path, index, node } = operation
let { document } = state
let parent = document.assertPath(path)
const isParent = document == parent
const nodes = parent.nodes.splice(index, 0, node)
parent = parent.merge({ nodes })
document = isParent ? parent : document.updateDescendant(parent)
state = state.merge({ document })
return state
}
/**
* Insert `text` at `offset` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function insertText(state, operation) {
const { path, offset, text, marks } = operation
let { document } = state
let node = document.assertPath(path)
node = node.insertText(offset, text, marks)
document = document.updateDescendant(node)
state = state.merge({ document })
return state
}
/**
* Move a node by `path` to a new parent by `path` and `index`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function moveNode(state, operation) {
const { path, newPath, newIndex } = operation
let { document } = state
const node = document.assertPath(path)
let parent = document.getParent(node)
const isParent = document == parent
const index = parent.nodes.indexOf(node)
parent = parent.removeNode(index)
document = isParent ? parent : document.updateDescendant(parent)
let target = document.assertPath(newPath)
const isTarget = document == target
target = target.insertNode(newIndex, node)
document = isTarget ? target : document.updateDescendant(target)
state = state.merge({ document })
return state
}
/**
* Remove mark from text at `offset` and `length` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function removeMark(state, operation) {
const { path, offset, length, mark } = operation
let { document } = state
let node = document.assertPath(path)
node = node.removeMark(offset, length, mark)
document = document.updateDescendant(node)
state = state.merge({ document })
return state
}
/**
* Remove a node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function removeNode(state, operation) {
const { path } = operation
let { document } = state
const node = document.assertPath(path)
let parent = document.getParent(node)
const index = parent.nodes.indexOf(node)
const isParent = document == parent
parent = parent.removeNode(index)
document = isParent ? parent : document.updateDescendant(parent)
document = document.normalize()
state = state.merge({ document })
return state
}
/**
* Remove text at `offset` and `length` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function removeText(state, operation) {
const { path, offset, length } = operation
let { document } = state
let node = document.assertPath(path)
node = node.removeText(offset, length)
document = document.updateDescendant(node)
document = document.normalize()
state = state.merge({ document })
return state
}
/**
* Set `properties` on mark on text at `offset` and `length` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function setMark(state, operation) {
const { path, offset, length, mark, properties } = operation
let { document } = state
let node = document.assertPath(path)
node = node.updateMark(offset, length, mark, properties)
document = document.updateDescendant(node)
state = state.merge({ document })
return state
}
/**
* Set `properties` on a node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function setNode(state, operation) {
const { path, properties } = operation
let { document } = state
let node = document.assertPath(path)
node = node.merge(properties)
document = document.updateDescendant(node)
document = document.normalize()
state = state.merge({ document })
return state
}
/**
* Set `properties` on the selection.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function setSelection(state, operation) {
let { properties } = operation
let { selection } = state
selection = selection.merge(properties)
state = state.merge({ selection })
return state
}
/**
* Split a node by `path` at `offset`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
function splitNode(state, operation) {
const { path, offset } = operation
let { document } = state
let node = document.assertPath(path)
let parent = document.getParent(node)
const isParent = document == parent
const index = parent.nodes.indexOf(node)
let child = node
let one
let two
if (node.kind != 'text') {
child = node.getTextAtOffset(offset)
}
while (child && child != parent) {
if (child.kind == 'text') {
const i = node.kind == 'text' ? offset : offset - node.getOffset(child)
const { characters } = child
const oneChars = characters.take(i)
const twoChars = characters.skip(i)
one = child.merge({ characters: oneChars })
two = child.merge({ characters: twoChars, key: uid() })
}
else {
const { nodes } = child
const oneNodes = nodes.takeUntil(n => n.key == one.key).push(one)
const twoNodes = nodes.skipUntil(n => n.key == one.key).rest().unshift(two)
one = child.merge({ nodes: oneNodes })
two = child.merge({ nodes: twoNodes, key: uid() })
}
child = document.getParent(child)
}
parent = parent.removeNode(index)
parent = parent.insertNode(index, two)
parent = parent.insertNode(index, one)
document = isParent ? parent : document.updateDescendant(parent)
state = state.merge({ document })
return state
}

View File

@@ -22,13 +22,13 @@ export function addMark(transform, mark) {
else if (selection.marks) {
const marks = selection.marks.add(mark)
const sel = selection.merge({ marks })
return transform.setSelection(sel)
return transform.moveTo(sel)
}
else {
const marks = document.getMarksAtRange(selection).add(mark)
const sel = selection.merge({ marks })
return transform.setSelection(sel)
return transform.moveTo(sel)
}
}
@@ -81,7 +81,7 @@ export function _delete(transform) {
return transform
.deleteAtRange(selection)
.setSelection(after)
.moveTo(after)
}
/**
@@ -155,7 +155,7 @@ export function deleteBackward(transform, n = 1) {
return transform
.deleteBackwardAtRange(selection, n)
.setSelection(after)
.moveTo(after)
}
/**
@@ -214,7 +214,7 @@ export function deleteForward(transform, n = 1) {
return transform
.deleteForwardAtRange(selection, n)
.setSelection(after)
.moveTo(after)
}
/**
@@ -237,7 +237,7 @@ export function insertBlock(transform, block) {
const text = document.getTexts().find(n => !keys.includes(n.key))
const after = selection.collapseToEndOf(text)
return transform.setSelection(after)
return transform.moveTo(after)
}
/**
@@ -287,7 +287,7 @@ export function insertFragment(transform, fragment) {
.moveForward(lastText.length)
}
return transform.setSelection(after)
return transform.moveTo(after)
}
/**
@@ -319,7 +319,7 @@ export function insertInline(transform, inline) {
after = selection.collapseToEndOf(text)
}
return transform.setSelection(after)
return transform.moveTo(after)
}
/**
@@ -354,7 +354,7 @@ export function insertText(transform, text, marks) {
return transform
.insertTextAtRange(selection, text, marks)
.setSelection(after)
.moveTo(after)
}
/**
@@ -407,7 +407,7 @@ export function splitBlock(transform, depth = 1) {
const nextNode = document.getNextText(startNode)
const after = selection.collapseToStartOf(nextNode)
return transform.setSelection(after)
return transform.moveTo(after)
}
/**
@@ -436,7 +436,7 @@ export function splitInline(transform, depth = Infinity) {
after = selection.collapseToStartOf(nextNode)
}
return transform.setSelection(after)
return transform.moveTo(after)
}
/**
@@ -460,13 +460,13 @@ export function removeMark(transform, mark) {
else if (selection.marks) {
const marks = selection.marks.remove(mark)
const sel = selection.merge({ marks })
return transform.setSelection(sel)
return transform.moveTo(sel)
}
else {
const marks = document.getMarksAtRange(selection).remove(mark)
const sel = selection.merge({ marks })
return transform.setSelection(sel)
return transform.moveTo(sel)
}
}
@@ -582,7 +582,7 @@ export function wrapInline(transform, properties) {
}
after = after.normalize(document)
return transform.setSelection(after)
return transform.moveTo(after)
}
/**
@@ -613,5 +613,5 @@ export function wrapText(transform, prefix, suffix = prefix) {
return transform
.wrapTextAtRange(selection, prefix, suffix)
.setSelection(after)
.moveTo(after)
}

View File

@@ -15,29 +15,10 @@ import uid from '../utils/uid'
export function addMarkByKey(transform, key, offset, length, mark) {
mark = Normalize.mark(mark)
const { state } = transform
const { document } = state
const path = document.getPath(key)
const inverse = {
type: 'remove_mark',
path,
offset,
length,
mark,
}
const operation = {
type: 'add_mark',
path,
offset,
length,
mark,
inverse,
}
return transform.operate(operation)
return transform.addMarkOperation(path, offset, length, mark)
}
/**
@@ -55,21 +36,7 @@ export function insertNodeByKey(transform, key, index, node) {
const { document } = state
const path = document.getPath(key)
const newPath = path.slice().push(index)
const inverse = {
type: 'remove_node',
path: newPath,
}
const operation = {
type: 'insert_node',
path,
index,
node,
inverse,
}
return transform.operate(operation)
return transform.insertNodeOperation(path, index, node)
}
/**
@@ -87,23 +54,7 @@ export function insertTextByKey(transform, key, offset, text, marks) {
const { state } = transform
const { document } = state
const path = document.getPath(key)
const inverse = {
type: 'remove_text',
path,
offset,
length: text.length,
}
const operation = {
type: 'insert_text',
path,
offset,
text,
marks,
}
return transform.operate(operation)
return transform.insertTextOperation(path, offset, text, marks)
}
/**
@@ -119,30 +70,9 @@ export function insertTextByKey(transform, key, offset, text, marks) {
export function moveNodeByKey(transform, key, newKey, newIndex) {
const { state } = transform
const { document } = state
const node = document.assertDescendant(key)
const path = document.getPath(key)
const parent = document.getParent(key)
const parentPath = path.slice(0, -1)
const parentIndex = path[path.length - 1]
const newPath = document.getPath(newKey)
const nodePath = newPath.slice().concat([newIndex])
const inverse = {
type: 'move_node',
path: nodePath,
newPath: parentPath,
newIndex: parentIndex,
}
const operation = {
type: 'move_node',
path,
newPath,
newIndex,
inverse,
}
return transform.operate(operation)
return transform.moveNodeOperation(path, newPath, newIndex)
}
/**
@@ -158,29 +88,10 @@ export function moveNodeByKey(transform, key, newKey, newIndex) {
export function removeMarkByKey(transform, key, offset, length, mark) {
mark = Normalize.mark(mark)
const { state } = transform
const { document } = state
const path = document.getPath(key)
const inverse = {
type: 'add_mark',
path,
offset,
length,
mark,
}
const operation = {
type: 'remove_mark',
path,
offset,
length,
mark,
inverse,
}
return transform.operate(operation)
return transform.removeMarkOperation(path, offset, length, mark)
}
/**
@@ -194,25 +105,8 @@ export function removeMarkByKey(transform, key, offset, length, mark) {
export function removeNodeByKey(transform, key) {
const { state } = transform
const { document } = state
const node = document.assertDescendant(key)
const path = document.getPath(key)
const parentPath = path.slice(0, -1)
const parentIndex = path.slice(-1)
const inverse = {
type: 'insert_node',
path: parentPath,
index: parentIndex,
node,
}
const operation = {
type: 'remove_node',
path,
inverse,
}
return transform.operate(operation)
return transform.removeNodeOperation(path)
}
/**
@@ -229,19 +123,7 @@ export function removeTextByKey(transform, key, offset, length) {
const { state } = transform
const { document } = state
const path = document.getPath(key)
// TODO!
const inverse = {}
const operation = {
type: 'remove_text',
path,
offset,
length,
inverse,
}
return transform.operate(operation)
return transform.removeTextOperation(path, offset, length)
}
/**
@@ -258,36 +140,10 @@ export function removeTextByKey(transform, key, offset, length) {
export function setMarkByKey(transform, key, offset, length, mark, properties) {
mark = Normalize.mark(mark)
properties = Normalize.markProperties(properties)
const { state } = transform
const { document } = state
const path = document.getPath(key)
const prevProps = {}
for (const k in properties) {
prevProps[k] = mark[k]
}
const inverse = {
type: 'set_mark',
path,
offset,
length,
mark,
properties: prevProps,
}
const operation = {
type: 'set_mark',
path,
offset,
length,
mark,
properties,
inverse,
}
return transform.operate(operation)
return transform.setMarkOperation(path, offset, length, mark, properties)
}
/**
@@ -301,31 +157,11 @@ export function setMarkByKey(transform, key, offset, length, mark, properties) {
export function setNodeByKey(transform, key, properties) {
properties = Normalize.nodeProperties(properties)
const { state } = transform
const { document } = state
const node = document.assertDescendant(key)
const path = document.getPath(key)
const prevProps = {}
for (const k in properties) {
prevProps[k] = node[k]
}
const inverse = {
type: 'set_node',
path,
properties: prevProps
}
const operation = {
type: 'set_node',
path,
properties,
inverse,
}
return transform.operate(operation)
return transform.setNodeOperation(path, properties)
}
/**
@@ -341,17 +177,5 @@ export function splitNodeByKey(transform, key, offset) {
const { state } = transform
const { document } = state
const path = document.getPath(key)
// TODO!
const inverse = {}
const operation = {
type: 'split_node',
path,
offset,
inverse,
}
return transform.operate(operation)
return transform.splitNodeOperation(path, offset)
}

View File

@@ -1,4 +1,30 @@
/**
* Apply operation.
*/
import {
applyOperation,
} from './apply-operation'
/**
* Operations.
*/
import {
addMarkOperation,
insertNodeOperation,
insertTextOperation,
moveNodeOperation,
removeMarkOperation,
removeNodeOperation,
removeTextOperation,
setMarkOperation,
setNodeOperation,
setSelectionOperation,
splitNodeOperation,
} from './operations'
/**
* At range.
*/
@@ -98,7 +124,6 @@ import {
moveTo,
moveToOffsets,
moveToRangeOf,
setSelection,
} from './on-selection'
/**
@@ -118,6 +143,28 @@ import {
export default {
/**
* Apply operation.
*/
applyOperation,
/**
* Operations.
*/
addMarkOperation,
insertNodeOperation,
insertTextOperation,
moveNodeOperation,
removeMarkOperation,
removeNodeOperation,
removeTextOperation,
setMarkOperation,
setNodeOperation,
setSelectionOperation,
splitNodeOperation,
/**
* At range.
*/
@@ -210,7 +257,6 @@ export default {
moveTo,
moveToOffsets,
moveToRangeOf,
setSelection,
/**
* Normalize.

View File

@@ -1,7 +1,4 @@
import Normalize from '../utils/normalize'
import Selection from '../models/selection'
/**
* Blur the selection.
*
@@ -13,7 +10,7 @@ export function blur(transform) {
const { state } = transform
const { selection } = state
const sel = selection.blur()
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -27,7 +24,7 @@ export function collapseToAnchor(transform) {
const { state } = transform
const { selection } = state
const sel = selection.collapseToAnchor()
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -41,7 +38,7 @@ export function collapseToEnd(transform) {
const { state } = transform
const { selection } = state
const sel = selection.collapseToEnd()
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -55,7 +52,7 @@ export function collapseToFocus(transform) {
const { state } = transform
const { selection } = state
const sel = selection.collapseToFocus()
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -69,7 +66,7 @@ export function collapseToStart(transform) {
const { state } = transform
const { selection } = state
const sel = selection.collapseToStart()
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -84,7 +81,7 @@ export function collapseToEndOf(transform, node) {
const { state } = transform
const { selection } = state
const sel = selection.collapseToEndOf(node)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -103,7 +100,7 @@ export function collapseToEndOfNextBlock(transform) {
if (!next) return transform
const sel = selection.collapseToEndOf(next)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -122,7 +119,7 @@ export function collapseToEndOfNextText(transform) {
if (!next) return transform
const sel = selection.collapseToEndOf(next)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -141,7 +138,7 @@ export function collapseToEndOfPreviousBlock(transform) {
if (!previous) return transform
const sel = selection.collapseToEndOf(previous)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -160,7 +157,7 @@ export function collapseToEndOfPreviousText(transform) {
if (!previous) return transform
const sel = selection.collapseToEndOf(previous)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -175,7 +172,7 @@ export function collapseToStartOf(transform, node) {
const { state } = transform
const { selection } = state
const sel = selection.collapseToStartOf(node)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -194,7 +191,7 @@ export function collapseToStartOfNextBlock(transform) {
if (!next) return transform
const sel = selection.collapseToStartOf(next)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -213,7 +210,7 @@ export function collapseToStartOfNextText(transform) {
if (!next) return transform
const sel = selection.collapseToStartOf(next)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -232,7 +229,7 @@ export function collapseToStartOfPreviousBlock(transform) {
if (!previous) return transform
const sel = selection.collapseToStartOf(previous)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -251,7 +248,7 @@ export function collapseToStartOfPreviousText(transform) {
if (!previous) return transform
const sel = selection.collapseToStartOf(previous)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -266,7 +263,7 @@ export function extendBackward(transform, n) {
const { state } = transform
const { document, selection } = state
const sel = selection.extendBackward(n).normalize(document)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -281,7 +278,7 @@ export function extendForward(transform, n) {
const { state } = transform
const { document, selection } = state
const sel = selection.extendForward(n).normalize(document)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -296,7 +293,7 @@ export function extendToEndOf(transform, node) {
const { state } = transform
const { document, selection } = state
const sel = selection.extendToEndOf(node).normalize(document)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -311,7 +308,7 @@ export function extendToStartOf(transform, node) {
const { state } = transform
const { document, selection } = state
const sel = selection.extendToStartOf(node).normalize(document)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -325,7 +322,7 @@ export function focus(transform) {
const { state } = transform
const { selection } = state
const sel = selection.focus()
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -340,7 +337,7 @@ export function moveBackward(transform, n) {
const { state } = transform
const { document, selection } = state
const sel = selection.moveBackward(n).normalize(document)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -355,7 +352,7 @@ export function moveForward(transform, n) {
const { state } = transform
const { document, selection } = state
const sel = selection.moveForward(n).normalize(document)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -367,11 +364,7 @@ export function moveForward(transform, n) {
*/
export function moveTo(transform, properties) {
properties = Normalize.selection(properties)
const { state } = transform
const { document, selection } = state
const sel = selection.merge(properties).normalize(document)
return transform.setSelection(sel)
return transform.setSelectionOperation(properties)
}
/**
@@ -387,7 +380,7 @@ export function moveToOffsets(transform, anchor, fokus) {
const { state } = transform
const { document, selection } = state
const sel = selection.moveToOffsets(anchor, fokus)
return transform.setSelection(sel)
return transform.setSelectionOperation(sel)
}
/**
@@ -403,42 +396,5 @@ export function moveToRangeOf(transform, start, end) {
const { state } = transform
const { document, selection } = state
const sel = selection.moveToRangeOf(start, end).normalize(document)
return transform.setSelection(sel)
}
/**
* Set the selection to a new `selection`.
*
* @param {Transform} transform
* @param {Mixed} selection
* @return {Transform}
*/
export function setSelection(transform, properties) {
properties = Normalize.selectionProperties(properties)
const { state } = transform
const { selection } = state
const prevProps = {}
if (properties.marks == selection.marks) {
properties.marks = null
}
for (const k in properties) {
prevProps[k] = selection[k]
}
const inverse = {
type: 'set_selection',
properties: prevProps
}
const operation = {
type: 'set_selection',
properties,
inverse,
}
return transform.operate(operation)
return transform.setSelectionOperation(sel)
}

View File

@@ -1,300 +1,375 @@
import Debug from 'debug'
import Normalize from '../utils/normalize'
import uid from '../utils/uid'
/**
* Debug.
*
* @type {Function}
*/
const debug = Debug('slate:operation')
/**
* Add mark to text at `offset` and `length` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Number} offset
* @param {Number} length
* @param {Mixed} mark
* @return {Transform}
*/
function addMark(state, operation) {
debug('add_mark', operation)
const { path, offset, length, mark } = operation
let { document } = state
let node = document.assertPath(path)
node = node.addMark(offset, length, mark)
document = document.updateDescendant(node)
state = state.merge({ document })
return state
export function addMarkOperation(transform, path, offset, length, mark) {
const inverse = [{
type: 'remove_mark',
path,
offset,
length,
mark,
}]
const operation = {
type: 'add_mark',
path,
offset,
length,
mark,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Insert a `node` at `index` in a node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Number} index
* @param {Node} node
* @return {Transform}
*/
function insertNode(state, operation) {
debug('insert_node', operation)
const { path, index, node } = operation
let { document } = state
let parent = document.assertPath(path)
const isParent = document == parent
const nodes = parent.nodes.splice(index, 0, node)
parent = parent.merge({ nodes })
document = isParent ? parent : document.updateDescendant(parent)
state = state.merge({ document })
return state
export function insertNodeOperation(transform, path, index, node) {
const inversePath = path.slice().concat([index])
const inverse = [{
type: 'remove_node',
path: inversePath,
}]
const operation = {
type: 'insert_node',
path,
index,
node,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Insert `text` at `offset` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Number} offset
* @param {String} text
* @param {Set} marks (optional)
* @return {Transform}
*/
function insertText(state, operation) {
debug('insert_text', operation)
const { path, offset, text, marks } = operation
let { document } = state
let node = document.assertPath(path)
node = node.insertText(offset, text, marks)
document = document.updateDescendant(node)
state = state.merge({ document })
return state
export function insertTextOperation(transform, path, offset, text, marks) {
const inverseLength = text.length
const inverse = [{
type: 'remove_text',
path,
offset,
length: inverseLength,
}]
const operation = {
type: 'insert_text',
path,
offset,
text,
marks,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Move a node by `path` to a new parent by `path` and `index`.
* Move a node by `path` to a `newPath` and `newIndex`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Array} newPath
* @param {Number} newIndex
* @return {Transform}
*/
function moveNode(state, operation) {
debug('move_node', operation)
const { path, newPath, newIndex } = operation
let { document } = state
const node = document.assertPath(path)
export function moveNodeOperation(transform, path, newPath, newIndex) {
const { state } = transform
const { document } = state
const parentPath = path.slice(0, -1)
const parentIndex = path[path.length - 1]
const inversePath = newPath.slice().concat([newIndex])
let parent = document.getParent(node)
const isParent = document == parent
const index = parent.nodes.indexOf(node)
parent = parent.removeNode(index)
document = isParent ? parent : document.updateDescendant(parent)
const inverse = [{
type: 'move_node',
path: inversePath,
newPath: parentPath,
newIndex: parentIndex,
}]
let target = document.assertPath(newPath)
const isTarget = document == target
target = target.insertNode(newIndex, node)
document = isTarget ? target : document.updateDescendant(target)
const operation = {
type: 'move_node',
path,
newPath,
newIndex,
inverse,
}
state = state.merge({ document })
return state
return transform.applyOperation(operation)
}
/**
* Remove mark from text at `offset` and `length` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Number} offset
* @param {Number} length
* @param {Mark} mark
* @return {Transform}
*/
function removeMark(state, operation) {
debug('remove_mark', operation)
const { path, offset, length, mark } = operation
let { document } = state
let node = document.assertPath(path)
node = node.removeMark(offset, length, mark)
document = document.updateDescendant(node)
state = state.merge({ document })
return state
export function removeMarkOperation(transform, path, offset, length, mark) {
const inverse = [{
type: 'add_mark',
path,
offset,
length,
mark,
}]
const operation = {
type: 'remove_mark',
path,
offset,
length,
mark,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Remove a node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @return {Transform}
*/
function removeNode(state, operation) {
debug('remove_node', operation)
const { path } = operation
let { document } = state
export function removeNodeOperation(transform, path) {
const { state } = transform
const { document } = state
const node = document.assertPath(path)
let parent = document.getParent(node)
const index = parent.nodes.indexOf(node)
const isParent = document == parent
parent = parent.removeNode(index)
document = isParent ? parent : document.updateDescendant(parent)
document = document.normalize()
state = state.merge({ document })
return state
const inversePath = path.slice(0, -1)
const inverseIndex = path.slice(-1)
const inverse = [{
type: 'insert_node',
path: inversePath,
index: inverseIndex,
node,
}]
const operation = {
type: 'remove_node',
path,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Remove text at `offset` and `length` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Number} offset
* @param {Number} length
* @return {Transform}
*/
function removeText(state, operation) {
debug('remove_text', operation)
const { path, offset, length } = operation
let { document } = state
let node = document.assertPath(path)
node = node.removeText(offset, length)
document = document.updateDescendant(node)
document = document.normalize()
state = state.merge({ document })
return state
export function removeTextOperation(transform, path, offset, length) {
const { state } = transform
const { document } = state
const node = document.assertPath(path)
const ranges = node.getRanges()
const inverse = []
ranges.reduce((start, range) => {
const { text, marks } = range
const end = start + text.length
if (start > offset + length) return
if (end <= offset) return
const endOffset = Math.min(end, offset + length)
const string = text.slice(offset, endOffset)
inverse.push({
type: 'insert_text',
path,
offset,
text: string,
marks,
})
return end
}, 0)
const operation = {
type: 'remove_text',
path,
offset,
length,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Set `properties` on mark on text at `offset` and `length` in node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Number} offset
* @param {Number} length
* @param {Mark} mark
* @return {Transform}
*/
function setMark(state, operation) {
debug('set_mark', operation)
const { path, offset, length, mark, properties } = operation
let { document } = state
let node = document.assertPath(path)
node = node.updateMark(offset, length, mark, properties)
document = document.updateDescendant(node)
state = state.merge({ document })
return state
export function setMarkOperation(transform, path, offset, length, mark, properties) {
const inverseProps = {}
for (const k in properties) {
inverseProps[k] = mark[k]
}
const inverse = [{
type: 'set_mark',
path,
offset,
length,
mark,
properties: inverseProps,
}]
const operation = {
type: 'set_mark',
path,
offset,
length,
mark,
properties,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Set `properties` on a node by `path`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Object || String} properties
* @return {Transform}
*/
function setNode(state, operation) {
debug('set_node', operation)
const { path, properties } = operation
let { document } = state
let node = document.assertPath(path)
node = node.merge(properties)
document = document.updateDescendant(node)
document = document.normalize()
state = state.merge({ document })
return state
export function setNodeOperation(transform, path, properties) {
const { state } = transform
const { document } = state
const node = document.assertPath(path)
const inverseProps = {}
for (const k in properties) {
inverseProps[k] = node[k]
}
const inverse = [{
type: 'set_node',
path,
properties: inverseProps
}]
const operation = {
type: 'set_node',
path,
properties,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Set `properties` on the selection.
* Set the selection to a new `selection`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Mixed} selection
* @return {Transform}
*/
function setSelection(state, operation) {
debug('set_selection', operation)
let { properties } = operation
let { selection } = state
selection = selection.merge(properties)
state = state.merge({ selection })
return state
export function setSelectionOperation(transform, properties) {
properties = Normalize.selectionProperties(properties)
const { state } = transform
const { selection } = state
const prevProps = {}
if (properties.marks == selection.marks) {
properties.marks = null
}
for (const k in properties) {
prevProps[k] = selection[k]
}
const inverse = [{
type: 'set_selection',
properties: prevProps
}]
const operation = {
type: 'set_selection',
properties,
inverse,
}
return transform.applyOperation(operation)
}
/**
* Split a node by `path` at `offset`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
* @param {Transform} transform
* @param {Array} path
* @param {Number} offset
* @return {Transform}
*/
function splitNode(state, operation) {
debug('split_node', operation)
const { path, offset } = operation
let { document } = state
let node = document.assertPath(path)
let parent = document.getParent(node)
const isParent = document == parent
const index = parent.nodes.indexOf(node)
export function splitNodeOperation(transform, path, offset) {
const inverse = []
let child = node
let one
let two
if (node.kind != 'text') {
child = node.getTextAtOffset(offset)
const operation = {
type: 'split_node',
path,
offset,
inverse,
}
while (child && child != parent) {
if (child.kind == 'text') {
const i = node.kind == 'text' ? offset : offset - node.getOffset(child)
const { characters } = child
const oneChars = characters.take(i)
const twoChars = characters.skip(i)
one = child.merge({ characters: oneChars })
two = child.merge({ characters: twoChars, key: uid() })
}
else {
const { nodes } = child
const oneNodes = nodes.takeUntil(n => n.key == one.key).push(one)
const twoNodes = nodes.skipUntil(n => n.key == one.key).rest().unshift(two)
one = child.merge({ nodes: oneNodes })
two = child.merge({ nodes: twoNodes, key: uid() })
}
child = document.getParent(child)
}
parent = parent.removeNode(index)
parent = parent.insertNode(index, two)
parent = parent.insertNode(index, one)
document = isParent ? parent : document.updateDescendant(parent)
state = state.merge({ document })
return state
}
/**
* Export.
*
* @type {Object}
*/
export default {
// Text operations.
insert_text: insertText,
remove_text: removeText,
// Mark operations.
add_mark: addMark,
remove_mark: removeMark,
set_mark: setMark,
// Node operations.
insert_node: insertNode,
move_node: moveNode,
remove_node: removeNode,
set_node: setNode,
split_node: splitNode,
// Selection operations.
set_selection: setSelection,
return transform.applyOperation(operation)
}