1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-20 06:01:24 +02:00

refactor at-current-range transforms

This commit is contained in:
Ian Storm Taylor
2016-08-18 14:24:17 -07:00
parent bde43c597d
commit 99faf3153c
5 changed files with 112 additions and 183 deletions

View File

@@ -4,16 +4,6 @@ import includes from 'lodash/includes'
import xor from 'lodash/xor' import xor from 'lodash/xor'
import { List, Record } from 'immutable' import { List, Record } from 'immutable'
/**
* Snapshot, with a state-like shape.
*/
const Snapshot = new Record({
document: null,
selection: null,
operations: new List()
})
/** /**
* Selection transforms. * Selection transforms.
*/ */
@@ -143,29 +133,29 @@ class Transform {
*/ */
shouldSnapshot() { shouldSnapshot() {
const { state, operations } = this const { state, transforms } = this
const { history, selection } = state const { history, selection } = state
const { undos, redos } = history const { undos, redos } = history
const previous = undos.peek() const previous = undos.peek()
// If the only operations applied are selection transforms, don't snapshot. // If the only transforms applied are selection transforms, don't snapshot.
const onlySelections = operations.every(operation => includes(SELECTION_TRANSFORMS, operation.type)) const onlySelections = transforms.every(t => includes(SELECTION_TRANSFORMS, t.type))
if (onlySelections) return false if (onlySelections) return false
// If there isn't a previous state, snapshot. // If there isn't a previous state, snapshot.
if (!previous) return true if (!previous) return true
// If there is a previous state but the operations are different, snapshot. // If there is a previous state but the transforms are different, snapshot.
const types = operations.map(operation => operation.type) const types = transforms.map(t => t.type)
const prevTypes = previous.operations.map(operation => operation.type) const prevTypes = previous.transforms.map(t => t.type)
const diff = xor(types.toArray(), prevTypes.toArray()) const diff = xor(types, prevTypes)
if (diff.length) return true if (diff.length) return true
// If the current operations aren't one of the "combinable" types, snapshot. // If the current transforms aren't one of the "combinable" types, snapshot.
const allCombinable = ( const allCombinable = (
operations.every(operation => operation.type == 'insertText') || transforms.every(t => t.type == 'insertText') ||
operations.every(operation => operation.type == 'deleteForward') || transforms.every(t => t.type == 'deleteForward') ||
operations.every(operation => operation.type == 'deleteBackward') transforms.every(t => t.type == 'deleteBackward')
) )
if (!allCombinable) return true if (!allCombinable) return true
@@ -177,13 +167,13 @@ class Transform {
/** /**
* Create a history-ready snapshot of the current state. * Create a history-ready snapshot of the current state.
* *
* @return {Snapshot} snapshot * @return {Object}
*/ */
snapshot() { snapshot() {
let { state, operations } = this let { state, transforms, operations } = this
let { document, selection } = state let { document, selection } = state
return new Snapshot({ document, selection, operations }) return { document, selection, transforms, operations }
} }
/** /**
@@ -259,7 +249,7 @@ class Transform {
} }
/** /**
* Add a operation-creating method for each of the transforms. * Add a transform method for each of the transforms.
*/ */
Object.keys(Transforms).forEach((type) => { Object.keys(Transforms).forEach((type) => {

View File

@@ -1,29 +1,6 @@
import Normalize from '../utils/normalize' import Normalize from '../utils/normalize'
import {
addMarkAtRange,
deleteAtRange,
deleteBackwardAtRange,
deleteForwardAtRange,
insertBlockAtRange,
insertFragmentAtRange,
insertInlineAtRange,
insertTextAtRange,
removeMarkAtRange,
setBlockAtRange,
setInlineAtRange,
splitBlockAtRange,
splitInlineAtRange,
splitTextAtRange,
toggleMarkAtRange,
unwrapBlockAtRange,
unwrapInlineAtRange,
wrapBlockAtRange,
wrapInlineAtRange,
wrapTextAtRange,
} from './at-range'
/** /**
* Add a `mark` to the characters in the current selection. * Add a `mark` to the characters in the current selection.
* *
@@ -34,19 +11,17 @@ import {
export function addMark(transform, mark) { export function addMark(transform, mark) {
mark = Normalize.mark(mark) mark = Normalize.mark(mark)
let { state } = transform
let { document, selection } = state
// If the selection is collapsed, add the mark to the cursor instead. const { state } = transform
const { document, selection } = state
if (selection.isCollapsed) { if (selection.isCollapsed) {
const marks = document.getMarksAtRange(selection) const marks = document.getMarksAtRange(selection).add(mark)
selection = selection.merge({ marks: marks.add(mark) }) const sel = selection.merge({ marks })
state = state.merge({ selection }) return transform.updateSelection(sel)
transform.state = state
return transform
} }
return addMarkAtRange(transform, selection, mark) return transform.addMarkAtRange(selection, mark)
} }
/** /**
@@ -57,14 +32,12 @@ export function addMark(transform, mark) {
*/ */
export function _delete(transform) { export function _delete(transform) {
let { state } = transform const { state } = transform
let { document, selection } = state const { document, selection } = state
let after let after
// When collapsed, there's nothing to do.
if (selection.isCollapsed) return transform if (selection.isCollapsed) return transform
// Determine what the selection will be after deleting.
const { startText } = state const { startText } = state
const { startKey, startOffset, endKey, endOffset } = selection const { startKey, startOffset, endKey, endOffset } = selection
const block = document.getClosestBlock(startText) const block = document.getClosestBlock(startText)
@@ -98,12 +71,9 @@ export function _delete(transform) {
after = selection.collapseToStart() after = selection.collapseToStart()
} }
// Delete and update the selection.
transform = deleteAtRange(transform, selection)
state = transform.state
state = state.merge({ selection: after })
transform.state = state
return transform return transform
.deleteAtRange(selection)
.updateSelection(after)
} }
/** /**
@@ -115,11 +85,10 @@ export function _delete(transform) {
*/ */
export function deleteBackward(transform, n = 1) { export function deleteBackward(transform, n = 1) {
let { state } = transform const { state } = transform
let { document, selection } = state const { document, selection } = state
let after = selection let after
// Determine what the selection should be after deleting.
const { startKey } = selection const { startKey } = selection
const startNode = document.getDescendant(startKey) const startNode = document.getDescendant(startKey)
@@ -176,12 +145,9 @@ export function deleteBackward(transform, n = 1) {
after = selection.moveBackward(n) after = selection.moveBackward(n)
} }
// Delete backward and then update the selection.
transform = deleteBackwardAtRange(transform, selection, n)
state = transform.state
state = state.merge({ selection: after })
transform.state = state
return transform return transform
.deleteBackwardAtRange(selection, n)
.updateSelection(after)
} }
/** /**
@@ -193,12 +159,11 @@ export function deleteBackward(transform, n = 1) {
*/ */
export function deleteForward(transform, n = 1) { export function deleteForward(transform, n = 1) {
let { state } = transform const { state } = transform
let { document, selection, startText } = state const { document, selection, startText } = state
let { startKey, startOffset } = selection const { startKey, startOffset } = selection
let after = selection let after
// Determine what the selection should be after deleting.
const block = document.getClosestBlock(startKey) const block = document.getClosestBlock(startKey)
const inline = document.getClosestInline(startKey) const inline = document.getClosestInline(startKey)
const highest = block.getHighestChild(startKey) const highest = block.getHighestChild(startKey)
@@ -235,12 +200,13 @@ export function deleteForward(transform, n = 1) {
} }
} }
// Delete forward and then update the selection. else {
transform = deleteForwardAtRange(transform, selection, n) after = selection
state = transform.state }
state = state.merge({ selection: after })
transform.state = state
return transform return transform
.deleteForwardAtRange(selection, n)
.updateSelection(after)
} }
/** /**
@@ -256,20 +222,14 @@ export function insertBlock(transform, block) {
let { document, selection } = state let { document, selection } = state
const keys = document.getTexts().map(text => text.key) const keys = document.getTexts().map(text => text.key)
// Insert the block transform.insertBlockAtRange(selection, block)
transform = insertBlockAtRange(transform, selection, block)
state = transform.state state = transform.state
document = state.document document = state.document
selection = state.selection
// Determine what the selection should be after inserting.
const text = document.getTexts().find(n => !keys.includes(n.key)) const text = document.getTexts().find(n => !keys.includes(n.key))
selection = selection.collapseToEndOf(text) const after = selection.collapseToEndOf(text)
// Update the document and selection. return transform.updateSelection(after)
state = state.merge({ selection })
transform.state = state
return transform
} }
/** /**
@@ -283,26 +243,21 @@ export function insertBlock(transform, block) {
export function insertFragment(transform, fragment) { export function insertFragment(transform, fragment) {
let { state } = transform let { state } = transform
let { document, selection } = state let { document, selection } = state
let after = selection
// If there's nothing in the fragment, do nothing.
if (!fragment.length) return transform if (!fragment.length) return transform
// Lookup some nodes for determining the selection next.
const lastText = fragment.getTexts().last() const lastText = fragment.getTexts().last()
const lastInline = fragment.getClosestInline(lastText) const lastInline = fragment.getClosestInline(lastText)
const beforeTexts = document.getTexts() const beforeTexts = document.getTexts()
// Insert the fragment. transform.insertFragmentAtRange(selection, fragment)
transform = insertFragmentAtRange(transform, selection, fragment)
state = transform.state state = transform.state
document = state.document document = state.document
selection = state.selection
// Determine what the selection should be after inserting.
const keys = beforeTexts.map(text => text.key) const keys = beforeTexts.map(text => text.key)
const news = document.getTexts().filter(n => !keys.includes(n.key)) const news = document.getTexts().filter(n => !keys.includes(n.key))
const text = news.size ? news.takeLast(2).first() : null const text = news.size ? news.takeLast(2).first() : null
let after
if (text && lastInline) { if (text && lastInline) {
after = selection.collapseToEndOf(text) after = selection.collapseToEndOf(text)
@@ -324,11 +279,7 @@ export function insertFragment(transform, fragment) {
.moveForward(lastText.length) .moveForward(lastText.length)
} }
// Update the document and selection. return transform.updateSelection(after)
selection = after
state = state.merge({ document, selection })
transform.state = state
return transform
} }
/** /**
@@ -342,29 +293,25 @@ export function insertFragment(transform, fragment) {
export function insertInline(transform, inline) { export function insertInline(transform, inline) {
let { state } = transform let { state } = transform
let { document, selection, startText } = state let { document, selection, startText } = state
let after
const hasVoid = document.hasVoidParent(startText) const hasVoid = document.hasVoidParent(startText)
const keys = document.getTexts().map(text => text.key) const keys = document.getTexts().map(text => text.key)
// Insert the inline transform.insertInlineAtRange(selection, inline)
transform = insertInlineAtRange(transform, selection, inline)
state = transform.state state = transform.state
document = state.document document = state.document
selection = state.selection
// Determine what the selection should be after inserting.
if (hasVoid) { if (hasVoid) {
selection = selection after = selection
} }
else { else {
const text = document.getTexts().find(n => !keys.includes(n.key)) const text = document.getTexts().find(n => !keys.includes(n.key))
selection = selection.collapseToEndOf(text) after = selection.collapseToEndOf(text)
} }
// Update the document and selection. return transform.updateSelection(after)
state = state.merge({ document, selection })
transform.state = state
return transform
} }
/** /**
@@ -377,12 +324,12 @@ export function insertInline(transform, inline) {
*/ */
export function insertText(transform, text, marks) { export function insertText(transform, text, marks) {
let { state } = transform const { state } = transform
let { document, selection } = state const { document, selection } = state
const { startKey } = selection
const isVoid = document.hasVoidParent(startKey)
let after let after
const isVoid = document.hasVoidParent(state.startText)
// Determine what the selection should be after inserting.
if (isVoid) { if (isVoid) {
after = selection after = selection
} }
@@ -395,15 +342,12 @@ export function insertText(transform, text, marks) {
after = selection.moveForward(text.length) after = selection.moveForward(text.length)
} }
// Insert the text and update the selection.
marks = marks || selection.marks marks = marks || selection.marks
state = insertTextAtRange(transform, selection, text, marks)
state = transform.state
state = state.merge({ selection: after })
transform.state = state
return transform
}
return transform
.insertTextAtRange(selection, text, marks)
.updateSelection(after)
}
/** /**
* Set `properties` of the block nodes in the current selection. * Set `properties` of the block nodes in the current selection.
@@ -416,7 +360,7 @@ export function insertText(transform, text, marks) {
export function setBlock(transform, properties) { export function setBlock(transform, properties) {
const { state } = transform const { state } = transform
const { selection } = state const { selection } = state
return setBlockAtRange(transform, selection, properties) return transform.setBlockAtRange(selection, properties)
} }
/** /**
@@ -430,7 +374,7 @@ export function setBlock(transform, properties) {
export function setInline(transform, properties) { export function setInline(transform, properties) {
const { state } = transform const { state } = transform
const { selection } = state const { selection } = state
return setInlineAtRange(transform, selection, properties) return transform.setInlineAtRange(selection, properties)
} }
/** /**
@@ -443,18 +387,19 @@ export function setInline(transform, properties) {
export function splitBlock(transform, depth = 1) { export function splitBlock(transform, depth = 1) {
let { state } = transform let { state } = transform
transform = splitBlockAtRange(transform, state.selection, depth)
state = transform.state
let { document, selection } = state let { document, selection } = state
// Determine what the selection should be after splitting. transform.splitBlockAtRange(selection, depth)
state = transform.state
document = state.document
const { startKey } = selection const { startKey } = selection
const startNode = document.getDescendant(startKey) const startNode = document.getDescendant(startKey)
const nextNode = document.getNextText(startNode) const nextNode = document.getNextText(startNode)
selection = selection.collapseToStartOf(nextNode) const after = selection.collapseToStartOf(nextNode)
state = state.merge({ selection })
transform.state = state return transform.updateSelection(after)
return transform
} }
/** /**
@@ -468,26 +413,22 @@ export function splitBlock(transform, depth = 1) {
export function splitInline(transform, depth = Infinity) { export function splitInline(transform, depth = Infinity) {
let { state } = transform let { state } = transform
let { document, selection } = state let { document, selection } = state
let after = selection
// Split the document. transform.splitInlineAtRange(selection, depth)
transform = splitInlineAtRange(transform, selection, depth)
state = transform.state state = transform.state
document = state.document document = state.document
selection = state.selection
// Determine what the selection should be after splitting.
const { startKey } = selection const { startKey } = selection
const inlineParent = document.getClosestInline(startKey) const inlineParent = document.getClosestInline(startKey)
if (inlineParent) { if (inlineParent) {
const startNode = document.getDescendant(startKey) const startNode = document.getDescendant(startKey)
const nextNode = document.getNextText(startNode) const nextNode = document.getNextText(startNode)
selection = selection.collapseToStartOf(nextNode) after = selection.collapseToStartOf(nextNode)
} }
state = state.merge({ document, selection }) return transform.updateSelection(after)
transform.state = state
return transform
} }
/** /**
@@ -500,19 +441,17 @@ export function splitInline(transform, depth = Infinity) {
export function removeMark(transform, mark) { export function removeMark(transform, mark) {
mark = Normalize.mark(mark) mark = Normalize.mark(mark)
let { state } = transform
let { document, selection } = state
// If the selection is collapsed, remove the mark from the cursor instead. const { state } = transform
const { document, selection } = state
if (selection.isCollapsed) { if (selection.isCollapsed) {
const marks = document.getMarksAtRange(selection) const marks = document.getMarksAtRange(selection).remove(mark)
selection = selection.merge({ marks: marks.remove(mark) }) const sel = selection.merge({ marks })
state = state.merge({ selection }) return transform.updateSelection(sel)
transform.state = state
return transform
} }
return removeMarkAtRange(transform, state.selection, mark) return transform.removeMarkAtRange(selection, mark)
} }
/** /**
@@ -526,11 +465,15 @@ export function removeMark(transform, mark) {
export function toggleMark(transform, mark) { export function toggleMark(transform, mark) {
mark = Normalize.mark(mark) mark = Normalize.mark(mark)
const { state } = transform const { state } = transform
const exists = state.marks.some(m => m.equals(mark)) const exists = state.marks.some(m => m.equals(mark))
return exists
? removeMark(transform, mark) if (exists) {
: addMark(transform, mark) return transform.removeMark(mark)
} else {
return transform.addMark(mark)
}
} }
/** /**
@@ -544,7 +487,7 @@ export function toggleMark(transform, mark) {
export function unwrapBlock(transform, properties) { export function unwrapBlock(transform, properties) {
const { state } = transform const { state } = transform
const { selection } = state const { selection } = state
return unwrapBlockAtRange(transform, selection, properties) return transform.unwrapBlockAtRange(selection, properties)
} }
/** /**
@@ -558,7 +501,7 @@ export function unwrapBlock(transform, properties) {
export function unwrapInline(transform, properties) { export function unwrapInline(transform, properties) {
const { state } = transform const { state } = transform
const { selection } = state const { selection } = state
return unwrapInlineAtRange(transform, selection, properties) return transform.unwrapInlineAtRange(selection, properties)
} }
/** /**
@@ -573,7 +516,7 @@ export function unwrapInline(transform, properties) {
export function wrapBlock(transform, properties) { export function wrapBlock(transform, properties) {
const { state } = transform const { state } = transform
const { selection } = state const { selection } = state
return wrapBlockAtRange(transform, selection, properties) return transform.wrapBlockAtRange(selection, properties)
} }
/** /**
@@ -587,35 +530,34 @@ export function wrapBlock(transform, properties) {
export function wrapInline(transform, properties) { export function wrapInline(transform, properties) {
let { state } = transform let { state } = transform
let { document, selection } = state let { document, selection } = state
let after
const { startKey } = selection const { startKey } = selection
const previous = document.getPreviousText(startKey) const previous = document.getPreviousText(startKey)
transform = wrapInlineAtRange(transform, selection, properties) transform.wrapInlineAtRange(selection, properties)
state = transform.state state = transform.state
document = state.document document = state.document
selection = state.selection
// Determine what the selection should be after wrapping. // Determine what the selection should be after wrapping.
if (selection.isCollapsed) { if (selection.isCollapsed) {
selection = selection after = selection
} }
else if (selection.startOffset == 0) { else if (selection.startOffset == 0) {
const text = previous const text = previous ? document.getNextText(previous) : document.getTexts().first()
? document.getNextText(previous) after = selection.moveToRangeOf(text)
: document.getTexts().first()
selection = selection.moveToRangeOf(text)
} }
else if (selection.startKey == selection.endKey) { else if (selection.startKey == selection.endKey) {
const text = document.getNextText(selection.startKey) const text = document.getNextText(selection.startKey)
selection = selection.moveToRangeOf(text) after = selection.moveToRangeOf(text)
} }
else { else {
const anchor = document.getNextText(selection.anchorKey) const anchor = document.getNextText(selection.anchorKey)
const focus = document.getDescendant(selection.focusKey) const focus = document.getDescendant(selection.focusKey)
selection = selection.merge({ after = selection.merge({
anchorKey: anchor.key, anchorKey: anchor.key,
anchorOffset: 0, anchorOffset: 0,
focusKey: focus.key, focusKey: focus.key,
@@ -623,10 +565,8 @@ export function wrapInline(transform, properties) {
}) })
} }
selection = selection.normalize(document) after = after.normalize(document)
state = state.merge({ selection }) return transform.updateSelection(after)
transform.state = state
return transform
} }
/** /**
@@ -639,12 +579,11 @@ export function wrapInline(transform, properties) {
*/ */
export function wrapText(transform, prefix, suffix = prefix) { export function wrapText(transform, prefix, suffix = prefix) {
let { state } = transform const { state } = transform
let { document, selection } = state const { document, selection } = state
let { anchorOffset, anchorKey, focusOffset, focusKey, isBackward } = selection const { anchorOffset, anchorKey, focusOffset, focusKey, isBackward } = selection
let after let after
// Determine what the selection should be after wrapping.
if (anchorKey == focusKey) { if (anchorKey == focusKey) {
after = selection.moveForward(prefix.length) after = selection.moveForward(prefix.length)
} }
@@ -656,10 +595,7 @@ export function wrapText(transform, prefix, suffix = prefix) {
}) })
} }
// Wrap the text and update the state.
transform = wrapTextAtRange(transform, selection, prefix, suffix)
state = transform.state
state = state.merge({ selection: after })
transform.state = state
return transform return transform
.wrapTextAtRange(selection, prefix, suffix)
.updateSelection(after)
} }

View File

@@ -98,6 +98,7 @@ import {
moveTo, moveTo,
moveToOffsets, moveToOffsets,
moveToRangeOf, moveToRangeOf,
updateSelection,
} from './on-selection' } from './on-selection'
/** /**
@@ -209,6 +210,7 @@ export default {
moveTo, moveTo,
moveToOffsets, moveToOffsets,
moveToRangeOf, moveToRangeOf,
updateSelection,
/** /**
* Normalize. * Normalize.

View File

@@ -431,8 +431,8 @@ export function updateSelection(transform, sel) {
selection = selection.merge(sel) selection = selection.merge(sel)
state = state.merge({ selection }) state = state.merge({ selection })
transform.add(state, { return transform.add(state, {
type: 'update-selection', type: 'update-selection',
selection: sel selection,
}) })
} }

View File

@@ -4,6 +4,7 @@ import Document from '../models/document'
import Inline from '../models/inline' import Inline from '../models/inline'
import Data from '../models/data' import Data from '../models/data'
import Mark from '../models/mark' import Mark from '../models/mark'
import Selection from '../models/selection'
import Text from '../models/text' import Text from '../models/text'
import typeOf from 'type-of' import typeOf from 'type-of'