mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-02-24 09:13:24 +01:00
refactor at-current-range transforms
This commit is contained in:
parent
bde43c597d
commit
99faf3153c
@ -4,16 +4,6 @@ import includes from 'lodash/includes'
|
||||
import xor from 'lodash/xor'
|
||||
import { List, Record } from 'immutable'
|
||||
|
||||
/**
|
||||
* Snapshot, with a state-like shape.
|
||||
*/
|
||||
|
||||
const Snapshot = new Record({
|
||||
document: null,
|
||||
selection: null,
|
||||
operations: new List()
|
||||
})
|
||||
|
||||
/**
|
||||
* Selection transforms.
|
||||
*/
|
||||
@ -143,29 +133,29 @@ class Transform {
|
||||
*/
|
||||
|
||||
shouldSnapshot() {
|
||||
const { state, operations } = this
|
||||
const { state, transforms } = this
|
||||
const { history, selection } = state
|
||||
const { undos, redos } = history
|
||||
const previous = undos.peek()
|
||||
|
||||
// If the only operations applied are selection transforms, don't snapshot.
|
||||
const onlySelections = operations.every(operation => includes(SELECTION_TRANSFORMS, operation.type))
|
||||
// If the only transforms applied are selection transforms, don't snapshot.
|
||||
const onlySelections = transforms.every(t => includes(SELECTION_TRANSFORMS, t.type))
|
||||
if (onlySelections) return false
|
||||
|
||||
// If there isn't a previous state, snapshot.
|
||||
if (!previous) return true
|
||||
|
||||
// If there is a previous state but the operations are different, snapshot.
|
||||
const types = operations.map(operation => operation.type)
|
||||
const prevTypes = previous.operations.map(operation => operation.type)
|
||||
const diff = xor(types.toArray(), prevTypes.toArray())
|
||||
// If there is a previous state but the transforms are different, snapshot.
|
||||
const types = transforms.map(t => t.type)
|
||||
const prevTypes = previous.transforms.map(t => t.type)
|
||||
const diff = xor(types, prevTypes)
|
||||
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 = (
|
||||
operations.every(operation => operation.type == 'insertText') ||
|
||||
operations.every(operation => operation.type == 'deleteForward') ||
|
||||
operations.every(operation => operation.type == 'deleteBackward')
|
||||
transforms.every(t => t.type == 'insertText') ||
|
||||
transforms.every(t => t.type == 'deleteForward') ||
|
||||
transforms.every(t => t.type == 'deleteBackward')
|
||||
)
|
||||
|
||||
if (!allCombinable) return true
|
||||
@ -177,13 +167,13 @@ class Transform {
|
||||
/**
|
||||
* Create a history-ready snapshot of the current state.
|
||||
*
|
||||
* @return {Snapshot} snapshot
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
snapshot() {
|
||||
let { state, operations } = this
|
||||
let { state, transforms, operations } = this
|
||||
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) => {
|
||||
|
@ -1,29 +1,6 @@
|
||||
|
||||
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.
|
||||
*
|
||||
@ -34,19 +11,17 @@ import {
|
||||
|
||||
export function addMark(transform, 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) {
|
||||
const marks = document.getMarksAtRange(selection)
|
||||
selection = selection.merge({ marks: marks.add(mark) })
|
||||
state = state.merge({ selection })
|
||||
transform.state = state
|
||||
return transform
|
||||
const marks = document.getMarksAtRange(selection).add(mark)
|
||||
const sel = selection.merge({ marks })
|
||||
return transform.updateSelection(sel)
|
||||
}
|
||||
|
||||
return addMarkAtRange(transform, selection, mark)
|
||||
return transform.addMarkAtRange(selection, mark)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,14 +32,12 @@ export function addMark(transform, mark) {
|
||||
*/
|
||||
|
||||
export function _delete(transform) {
|
||||
let { state } = transform
|
||||
let { document, selection } = state
|
||||
const { state } = transform
|
||||
const { document, selection } = state
|
||||
let after
|
||||
|
||||
// When collapsed, there's nothing to do.
|
||||
if (selection.isCollapsed) return transform
|
||||
|
||||
// Determine what the selection will be after deleting.
|
||||
const { startText } = state
|
||||
const { startKey, startOffset, endKey, endOffset } = selection
|
||||
const block = document.getClosestBlock(startText)
|
||||
@ -98,12 +71,9 @@ export function _delete(transform) {
|
||||
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
|
||||
.deleteAtRange(selection)
|
||||
.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -115,11 +85,10 @@ export function _delete(transform) {
|
||||
*/
|
||||
|
||||
export function deleteBackward(transform, n = 1) {
|
||||
let { state } = transform
|
||||
let { document, selection } = state
|
||||
let after = selection
|
||||
const { state } = transform
|
||||
const { document, selection } = state
|
||||
let after
|
||||
|
||||
// Determine what the selection should be after deleting.
|
||||
const { startKey } = selection
|
||||
const startNode = document.getDescendant(startKey)
|
||||
|
||||
@ -176,12 +145,9 @@ export function deleteBackward(transform, n = 1) {
|
||||
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
|
||||
.deleteBackwardAtRange(selection, n)
|
||||
.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -193,12 +159,11 @@ export function deleteBackward(transform, n = 1) {
|
||||
*/
|
||||
|
||||
export function deleteForward(transform, n = 1) {
|
||||
let { state } = transform
|
||||
let { document, selection, startText } = state
|
||||
let { startKey, startOffset } = selection
|
||||
let after = selection
|
||||
const { state } = transform
|
||||
const { document, selection, startText } = state
|
||||
const { startKey, startOffset } = selection
|
||||
let after
|
||||
|
||||
// Determine what the selection should be after deleting.
|
||||
const block = document.getClosestBlock(startKey)
|
||||
const inline = document.getClosestInline(startKey)
|
||||
const highest = block.getHighestChild(startKey)
|
||||
@ -235,12 +200,13 @@ export function deleteForward(transform, n = 1) {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete forward and then update the selection.
|
||||
transform = deleteForwardAtRange(transform, selection, n)
|
||||
state = transform.state
|
||||
state = state.merge({ selection: after })
|
||||
transform.state = state
|
||||
else {
|
||||
after = selection
|
||||
}
|
||||
|
||||
return transform
|
||||
.deleteForwardAtRange(selection, n)
|
||||
.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -256,20 +222,14 @@ export function insertBlock(transform, block) {
|
||||
let { document, selection } = state
|
||||
const keys = document.getTexts().map(text => text.key)
|
||||
|
||||
// Insert the block
|
||||
transform = insertBlockAtRange(transform, selection, block)
|
||||
transform.insertBlockAtRange(selection, block)
|
||||
state = transform.state
|
||||
document = state.document
|
||||
selection = state.selection
|
||||
|
||||
// Determine what the selection should be after inserting.
|
||||
const text = document.getTexts().find(n => !keys.includes(n.key))
|
||||
selection = selection.collapseToEndOf(text)
|
||||
const after = selection.collapseToEndOf(text)
|
||||
|
||||
// Update the document and selection.
|
||||
state = state.merge({ selection })
|
||||
transform.state = state
|
||||
return transform
|
||||
return transform.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -283,26 +243,21 @@ export function insertBlock(transform, block) {
|
||||
export function insertFragment(transform, fragment) {
|
||||
let { state } = transform
|
||||
let { document, selection } = state
|
||||
let after = selection
|
||||
|
||||
// If there's nothing in the fragment, do nothing.
|
||||
if (!fragment.length) return transform
|
||||
|
||||
// Lookup some nodes for determining the selection next.
|
||||
const lastText = fragment.getTexts().last()
|
||||
const lastInline = fragment.getClosestInline(lastText)
|
||||
const beforeTexts = document.getTexts()
|
||||
|
||||
// Insert the fragment.
|
||||
transform = insertFragmentAtRange(transform, selection, fragment)
|
||||
transform.insertFragmentAtRange(selection, fragment)
|
||||
state = transform.state
|
||||
document = state.document
|
||||
selection = state.selection
|
||||
|
||||
// Determine what the selection should be after inserting.
|
||||
const keys = beforeTexts.map(text => text.key)
|
||||
const news = document.getTexts().filter(n => !keys.includes(n.key))
|
||||
const text = news.size ? news.takeLast(2).first() : null
|
||||
let after
|
||||
|
||||
if (text && lastInline) {
|
||||
after = selection.collapseToEndOf(text)
|
||||
@ -324,11 +279,7 @@ export function insertFragment(transform, fragment) {
|
||||
.moveForward(lastText.length)
|
||||
}
|
||||
|
||||
// Update the document and selection.
|
||||
selection = after
|
||||
state = state.merge({ document, selection })
|
||||
transform.state = state
|
||||
return transform
|
||||
return transform.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -342,29 +293,25 @@ export function insertFragment(transform, fragment) {
|
||||
export function insertInline(transform, inline) {
|
||||
let { state } = transform
|
||||
let { document, selection, startText } = state
|
||||
let after
|
||||
|
||||
const hasVoid = document.hasVoidParent(startText)
|
||||
const keys = document.getTexts().map(text => text.key)
|
||||
|
||||
// Insert the inline
|
||||
transform = insertInlineAtRange(transform, selection, inline)
|
||||
transform.insertInlineAtRange(selection, inline)
|
||||
state = transform.state
|
||||
document = state.document
|
||||
selection = state.selection
|
||||
|
||||
// Determine what the selection should be after inserting.
|
||||
if (hasVoid) {
|
||||
selection = selection
|
||||
after = selection
|
||||
}
|
||||
|
||||
else {
|
||||
const text = document.getTexts().find(n => !keys.includes(n.key))
|
||||
selection = selection.collapseToEndOf(text)
|
||||
after = selection.collapseToEndOf(text)
|
||||
}
|
||||
|
||||
// Update the document and selection.
|
||||
state = state.merge({ document, selection })
|
||||
transform.state = state
|
||||
return transform
|
||||
return transform.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -377,12 +324,12 @@ export function insertInline(transform, inline) {
|
||||
*/
|
||||
|
||||
export function insertText(transform, text, marks) {
|
||||
let { state } = transform
|
||||
let { document, selection } = state
|
||||
const { state } = transform
|
||||
const { document, selection } = state
|
||||
const { startKey } = selection
|
||||
const isVoid = document.hasVoidParent(startKey)
|
||||
let after
|
||||
const isVoid = document.hasVoidParent(state.startText)
|
||||
|
||||
// Determine what the selection should be after inserting.
|
||||
if (isVoid) {
|
||||
after = selection
|
||||
}
|
||||
@ -395,15 +342,12 @@ export function insertText(transform, text, marks) {
|
||||
after = selection.moveForward(text.length)
|
||||
}
|
||||
|
||||
// Insert the text and update the selection.
|
||||
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.
|
||||
@ -416,7 +360,7 @@ export function insertText(transform, text, marks) {
|
||||
export function setBlock(transform, properties) {
|
||||
const { state } = transform
|
||||
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) {
|
||||
const { state } = transform
|
||||
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) {
|
||||
let { state } = transform
|
||||
transform = splitBlockAtRange(transform, state.selection, depth)
|
||||
state = transform.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 startNode = document.getDescendant(startKey)
|
||||
const nextNode = document.getNextText(startNode)
|
||||
selection = selection.collapseToStartOf(nextNode)
|
||||
state = state.merge({ selection })
|
||||
transform.state = state
|
||||
return transform
|
||||
const after = selection.collapseToStartOf(nextNode)
|
||||
|
||||
return transform.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -468,26 +413,22 @@ export function splitBlock(transform, depth = 1) {
|
||||
export function splitInline(transform, depth = Infinity) {
|
||||
let { state } = transform
|
||||
let { document, selection } = state
|
||||
let after = selection
|
||||
|
||||
// Split the document.
|
||||
transform = splitInlineAtRange(transform, selection, depth)
|
||||
transform.splitInlineAtRange(selection, depth)
|
||||
state = transform.state
|
||||
document = state.document
|
||||
selection = state.selection
|
||||
|
||||
// Determine what the selection should be after splitting.
|
||||
const { startKey } = selection
|
||||
const inlineParent = document.getClosestInline(startKey)
|
||||
|
||||
if (inlineParent) {
|
||||
const startNode = document.getDescendant(startKey)
|
||||
const nextNode = document.getNextText(startNode)
|
||||
selection = selection.collapseToStartOf(nextNode)
|
||||
after = selection.collapseToStartOf(nextNode)
|
||||
}
|
||||
|
||||
state = state.merge({ document, selection })
|
||||
transform.state = state
|
||||
return transform
|
||||
return transform.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -500,19 +441,17 @@ export function splitInline(transform, depth = Infinity) {
|
||||
|
||||
export function removeMark(transform, 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) {
|
||||
const marks = document.getMarksAtRange(selection)
|
||||
selection = selection.merge({ marks: marks.remove(mark) })
|
||||
state = state.merge({ selection })
|
||||
transform.state = state
|
||||
return transform
|
||||
const marks = document.getMarksAtRange(selection).remove(mark)
|
||||
const sel = selection.merge({ marks })
|
||||
return transform.updateSelection(sel)
|
||||
}
|
||||
|
||||
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) {
|
||||
mark = Normalize.mark(mark)
|
||||
|
||||
const { state } = transform
|
||||
const exists = state.marks.some(m => m.equals(mark))
|
||||
return exists
|
||||
? removeMark(transform, mark)
|
||||
: addMark(transform, mark)
|
||||
|
||||
if (exists) {
|
||||
return transform.removeMark(mark)
|
||||
} else {
|
||||
return transform.addMark(mark)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -544,7 +487,7 @@ export function toggleMark(transform, mark) {
|
||||
export function unwrapBlock(transform, properties) {
|
||||
const { state } = transform
|
||||
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) {
|
||||
const { state } = transform
|
||||
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) {
|
||||
const { state } = transform
|
||||
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) {
|
||||
let { state } = transform
|
||||
let { document, selection } = state
|
||||
let after
|
||||
|
||||
const { startKey } = selection
|
||||
const previous = document.getPreviousText(startKey)
|
||||
|
||||
transform = wrapInlineAtRange(transform, selection, properties)
|
||||
transform.wrapInlineAtRange(selection, properties)
|
||||
state = transform.state
|
||||
document = state.document
|
||||
selection = state.selection
|
||||
|
||||
// Determine what the selection should be after wrapping.
|
||||
if (selection.isCollapsed) {
|
||||
selection = selection
|
||||
after = selection
|
||||
}
|
||||
|
||||
else if (selection.startOffset == 0) {
|
||||
const text = previous
|
||||
? document.getNextText(previous)
|
||||
: document.getTexts().first()
|
||||
selection = selection.moveToRangeOf(text)
|
||||
const text = previous ? document.getNextText(previous) : document.getTexts().first()
|
||||
after = selection.moveToRangeOf(text)
|
||||
}
|
||||
|
||||
else if (selection.startKey == selection.endKey) {
|
||||
const text = document.getNextText(selection.startKey)
|
||||
selection = selection.moveToRangeOf(text)
|
||||
after = selection.moveToRangeOf(text)
|
||||
}
|
||||
|
||||
else {
|
||||
const anchor = document.getNextText(selection.anchorKey)
|
||||
const focus = document.getDescendant(selection.focusKey)
|
||||
selection = selection.merge({
|
||||
after = selection.merge({
|
||||
anchorKey: anchor.key,
|
||||
anchorOffset: 0,
|
||||
focusKey: focus.key,
|
||||
@ -623,10 +565,8 @@ export function wrapInline(transform, properties) {
|
||||
})
|
||||
}
|
||||
|
||||
selection = selection.normalize(document)
|
||||
state = state.merge({ selection })
|
||||
transform.state = state
|
||||
return transform
|
||||
after = after.normalize(document)
|
||||
return transform.updateSelection(after)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -639,12 +579,11 @@ export function wrapInline(transform, properties) {
|
||||
*/
|
||||
|
||||
export function wrapText(transform, prefix, suffix = prefix) {
|
||||
let { state } = transform
|
||||
let { document, selection } = state
|
||||
let { anchorOffset, anchorKey, focusOffset, focusKey, isBackward } = selection
|
||||
const { state } = transform
|
||||
const { document, selection } = state
|
||||
const { anchorOffset, anchorKey, focusOffset, focusKey, isBackward } = selection
|
||||
let after
|
||||
|
||||
// Determine what the selection should be after wrapping.
|
||||
if (anchorKey == focusKey) {
|
||||
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
|
||||
.wrapTextAtRange(selection, prefix, suffix)
|
||||
.updateSelection(after)
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ import {
|
||||
moveTo,
|
||||
moveToOffsets,
|
||||
moveToRangeOf,
|
||||
updateSelection,
|
||||
} from './on-selection'
|
||||
|
||||
/**
|
||||
@ -209,6 +210,7 @@ export default {
|
||||
moveTo,
|
||||
moveToOffsets,
|
||||
moveToRangeOf,
|
||||
updateSelection,
|
||||
|
||||
/**
|
||||
* Normalize.
|
||||
|
@ -431,8 +431,8 @@ export function updateSelection(transform, sel) {
|
||||
selection = selection.merge(sel)
|
||||
state = state.merge({ selection })
|
||||
|
||||
transform.add(state, {
|
||||
return transform.add(state, {
|
||||
type: 'update-selection',
|
||||
selection: sel
|
||||
selection,
|
||||
})
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import Document from '../models/document'
|
||||
import Inline from '../models/inline'
|
||||
import Data from '../models/data'
|
||||
import Mark from '../models/mark'
|
||||
import Selection from '../models/selection'
|
||||
import Text from '../models/text'
|
||||
import typeOf from 'type-of'
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user