1
0
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:
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 { 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) => {

View File

@ -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)
}

View File

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

View File

@ -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,
})
}

View File

@ -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'