mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-31 10:51:44 +02:00
refactor transforms to be modularized
This commit is contained in:
10
lib/index.js
10
lib/index.js
@@ -29,6 +29,12 @@ import Html from './serializers/html'
|
|||||||
import Plain from './serializers/plain'
|
import Plain from './serializers/plain'
|
||||||
import Raw from './serializers/raw'
|
import Raw from './serializers/raw'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Transforms from './transforms'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utils.
|
* Utils.
|
||||||
*/
|
*/
|
||||||
@@ -37,6 +43,8 @@ import findDOMNode from './utils/find-dom-node'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Export.
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -55,6 +63,7 @@ export {
|
|||||||
Selection,
|
Selection,
|
||||||
State,
|
State,
|
||||||
Text,
|
Text,
|
||||||
|
Transforms,
|
||||||
findDOMNode
|
findDOMNode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,5 +83,6 @@ export default {
|
|||||||
Selection,
|
Selection,
|
||||||
State,
|
State,
|
||||||
Text,
|
Text,
|
||||||
|
Transforms,
|
||||||
findDOMNode
|
findDOMNode
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,9 @@ import Document from './document'
|
|||||||
import Inline from './inline'
|
import Inline from './inline'
|
||||||
import Mark from './mark'
|
import Mark from './mark'
|
||||||
import Selection from './selection'
|
import Selection from './selection'
|
||||||
import Transforms from './transforms'
|
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
import direction from 'direction'
|
import direction from 'direction'
|
||||||
|
import isInRange from '../utils/is-in-range'
|
||||||
import includes from 'lodash/includes'
|
import includes from 'lodash/includes'
|
||||||
import memoize from '../utils/memoize'
|
import memoize from '../utils/memoize'
|
||||||
import uid from '../utils/uid'
|
import uid from '../utils/uid'
|
||||||
@@ -1198,38 +1198,6 @@ function normalizeKey(key) {
|
|||||||
return key.key
|
return key.key
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an `index` of a `text` node is in a `range`.
|
|
||||||
*
|
|
||||||
* @param {Number} index
|
|
||||||
* @param {Text} text
|
|
||||||
* @param {Selection} range
|
|
||||||
* @return {Set} characters
|
|
||||||
*/
|
|
||||||
|
|
||||||
function isInRange(index, text, range) {
|
|
||||||
const { startKey, startOffset, endKey, endOffset } = range
|
|
||||||
let matcher
|
|
||||||
|
|
||||||
if (text.key == startKey && text.key == endKey) {
|
|
||||||
return startOffset <= index && index < endOffset
|
|
||||||
} else if (text.key == startKey) {
|
|
||||||
return startOffset <= index
|
|
||||||
} else if (text.key == endKey) {
|
|
||||||
return index < endOffset
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms.
|
|
||||||
*/
|
|
||||||
|
|
||||||
for (const key in Transforms) {
|
|
||||||
Node[key] = Transforms[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export.
|
* Export.
|
||||||
*/
|
*/
|
||||||
|
@@ -399,837 +399,6 @@ class State extends new Record(DEFAULTS) {
|
|||||||
return new Transform({ state })
|
return new Transform({ state })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a `mark` to the characters in the current selection.
|
|
||||||
*
|
|
||||||
* @param {Mark} mark
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
addMark(mark) {
|
|
||||||
mark = normalizeMark(mark)
|
|
||||||
let state = this
|
|
||||||
let { cursorMarks, document, selection } = state
|
|
||||||
|
|
||||||
// If the selection is collapsed, add the mark to the cursor instead.
|
|
||||||
if (selection.isCollapsed) {
|
|
||||||
const marks = document.getMarksAtRange(selection)
|
|
||||||
state = state.merge({ cursorMarks: marks.add(mark) })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
document = document.addMarkAtRange(selection, mark)
|
|
||||||
state = state.merge({ document })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to the start of the previous block.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
collapseToStartOfPreviousBlock() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let blocks = document.getBlocksAtRange(selection)
|
|
||||||
let block = blocks.first()
|
|
||||||
if (!block) return state
|
|
||||||
|
|
||||||
let previous = document.getPreviousBlock(block)
|
|
||||||
if (!previous) return state
|
|
||||||
|
|
||||||
selection = selection.collapseToStartOf(previous)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to the end of the previous block.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
collapseToEndOfPreviousBlock() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let blocks = document.getBlocksAtRange(selection)
|
|
||||||
let block = blocks.first()
|
|
||||||
if (!block) return state
|
|
||||||
|
|
||||||
let previous = document.getPreviousBlock(block)
|
|
||||||
if (!previous) return state
|
|
||||||
|
|
||||||
selection = selection.collapseToEndOf(previous)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to the start of the next block.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
collapseToStartOfNextBlock() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let blocks = document.getBlocksAtRange(selection)
|
|
||||||
let block = blocks.last()
|
|
||||||
if (!block) return state
|
|
||||||
|
|
||||||
let next = document.getNextBlock(block)
|
|
||||||
if (!next) return state
|
|
||||||
|
|
||||||
selection = selection.collapseToStartOf(next)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to the end of the next block.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
collapseToEndOfNextBlock() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let blocks = document.getBlocksAtRange(selection)
|
|
||||||
let block = blocks.last()
|
|
||||||
if (!block) return state
|
|
||||||
|
|
||||||
let next = document.getNextBlock(block)
|
|
||||||
if (!next) return state
|
|
||||||
|
|
||||||
selection = selection.collapseToEndOf(next)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to the start of the previous text.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
collapseToStartOfPreviousText() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let texts = document.getTextsAtRange(selection)
|
|
||||||
let text = texts.first()
|
|
||||||
if (!text) return state
|
|
||||||
|
|
||||||
let previous = document.getPreviousText(text)
|
|
||||||
if (!previous) return state
|
|
||||||
|
|
||||||
selection = selection.collapseToStartOf(previous)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to the end of the previous text.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
collapseToEndOfPreviousText() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let texts = document.getTextsAtRange(selection)
|
|
||||||
let text = texts.first()
|
|
||||||
if (!text) return state
|
|
||||||
|
|
||||||
let previous = document.getPreviousText(text)
|
|
||||||
if (!previous) return state
|
|
||||||
|
|
||||||
selection = selection.collapseToEndOf(previous)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to the start of the next text.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
collapseToStartOfNextText() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let texts = document.getTextsAtRange(selection)
|
|
||||||
let text = texts.last()
|
|
||||||
if (!text) return state
|
|
||||||
|
|
||||||
let next = document.getNextText(text)
|
|
||||||
if (!next) return state
|
|
||||||
|
|
||||||
selection = selection.collapseToStartOf(next)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to the end of the next text.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
collapseToEndOfNextText() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let texts = document.getTextsAtRange(selection)
|
|
||||||
let text = texts.last()
|
|
||||||
if (!text) return state
|
|
||||||
|
|
||||||
let next = document.getNextText(text)
|
|
||||||
if (!next) return state
|
|
||||||
|
|
||||||
selection = selection.collapseToEndOf(next)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete at the current selection.
|
|
||||||
*
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
delete() {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let after
|
|
||||||
|
|
||||||
// When collapsed, there's nothing to do.
|
|
||||||
if (selection.isCollapsed) return state
|
|
||||||
|
|
||||||
// Determine what the selection will be after deleting.
|
|
||||||
const { startText } = this
|
|
||||||
const { startKey, startOffset, endKey, endOffset } = selection
|
|
||||||
const block = document.getClosestBlock(startText)
|
|
||||||
const highest = block.getHighestChild(startText)
|
|
||||||
const previous = block.getPreviousSibling(highest)
|
|
||||||
const next = block.getNextSibling(highest)
|
|
||||||
|
|
||||||
if (
|
|
||||||
previous &&
|
|
||||||
startOffset == 0 &&
|
|
||||||
(endKey != startKey || endOffset == startText.length)
|
|
||||||
) {
|
|
||||||
if (previous.kind == 'text') {
|
|
||||||
if (next && next.kind == 'text') {
|
|
||||||
after = selection.merge({
|
|
||||||
anchorKey: previous.key,
|
|
||||||
anchorOffset: previous.length,
|
|
||||||
focusKey: previous.key,
|
|
||||||
focusOffset: previous.length
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
after = selection.collapseToEndOf(previous)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const last = previous.getTexts().last()
|
|
||||||
after = selection.collapseToEndOf(last)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
after = selection.collapseToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete and update the selection.
|
|
||||||
document = document.deleteAtRange(selection)
|
|
||||||
selection = after
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete backward `n` characters at the current selection.
|
|
||||||
*
|
|
||||||
* @param {Number} n (optional)
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
deleteBackward(n = 1) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let after = selection
|
|
||||||
|
|
||||||
// Determine what the selection should be after deleting.
|
|
||||||
const { startKey } = selection
|
|
||||||
const startNode = document.getDescendant(startKey)
|
|
||||||
|
|
||||||
if (selection.isExpanded) {
|
|
||||||
after = selection.collapseToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (selection.isAtStartOf(document)) {
|
|
||||||
after = selection
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (selection.isAtStartOf(startNode)) {
|
|
||||||
const previous = document.getPreviousText(startNode)
|
|
||||||
const prevBlock = document.getClosestBlock(previous)
|
|
||||||
const prevInline = document.getClosestInline(previous)
|
|
||||||
|
|
||||||
if (prevBlock && prevBlock.isVoid) {
|
|
||||||
after = selection
|
|
||||||
} else if (prevInline && prevInline.isVoid) {
|
|
||||||
after = selection
|
|
||||||
} else {
|
|
||||||
after = selection.collapseToEndOf(previous)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (selection.isAtEndOf(startNode) && startNode.length == 1) {
|
|
||||||
const block = document.getClosestBlock(startKey)
|
|
||||||
const highest = block.getHighestChild(startKey)
|
|
||||||
const previous = block.getPreviousSibling(highest)
|
|
||||||
const next = block.getNextSibling(highest)
|
|
||||||
|
|
||||||
if (previous) {
|
|
||||||
if (previous.kind == 'text') {
|
|
||||||
if (next && next.kind == 'text') {
|
|
||||||
after = selection.merge({
|
|
||||||
anchorKey: previous.key,
|
|
||||||
anchorOffset: previous.length,
|
|
||||||
focusKey: previous.key,
|
|
||||||
focusOffset: previous.length
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
after = selection.collapseToEndOf(previous)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const last = previous.getTexts().last()
|
|
||||||
after = selection.collapseToEndOf(last)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
after = selection.moveBackward(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
after = selection.moveBackward(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete backward and then update the selection.
|
|
||||||
document = document.deleteBackwardAtRange(selection, n)
|
|
||||||
selection = after
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete forward `n` characters at the current selection.
|
|
||||||
*
|
|
||||||
* @param {Number} n (optional)
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
deleteForward(n = 1) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection, startText } = state
|
|
||||||
let { startKey, startOffset } = selection
|
|
||||||
let after = selection
|
|
||||||
|
|
||||||
// Determine what the selection should be after deleting.
|
|
||||||
const block = document.getClosestBlock(startKey)
|
|
||||||
const inline = document.getClosestInline(startKey)
|
|
||||||
const highest = block.getHighestChild(startKey)
|
|
||||||
const previous = block.getPreviousSibling(highest)
|
|
||||||
const next = block.getNextSibling(highest)
|
|
||||||
|
|
||||||
if (selection.isExpanded) {
|
|
||||||
after = selection.collapseToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ((block && block.isVoid) || (inline && inline.isVoid)) {
|
|
||||||
const nextText = document.getNextText(startKey)
|
|
||||||
const prevText = document.getPreviousText(startKey)
|
|
||||||
after = next
|
|
||||||
? selection.collapseToStartOf(nextText)
|
|
||||||
: selection.collapseToEndOf(prevText)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (previous && startOffset == 0 && startText.length == 1) {
|
|
||||||
if (previous.kind == 'text') {
|
|
||||||
if (next && next.kind == 'text') {
|
|
||||||
after = selection.merge({
|
|
||||||
anchorKey: previous.key,
|
|
||||||
anchorOffset: previous.length,
|
|
||||||
focusKey: previous.key,
|
|
||||||
focusOffset: previous.length
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
after = selection.collapseToEndOf(previous)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const last = previous.getTexts().last()
|
|
||||||
after = selection.collapseToEndOf(last)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete forward and then update the selection.
|
|
||||||
document = document.deleteForwardAtRange(selection, n)
|
|
||||||
selection = after
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a `block` at the current selection.
|
|
||||||
*
|
|
||||||
* @param {String || Object || Block} block
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
insertBlock(block) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let after = selection
|
|
||||||
|
|
||||||
// Insert the block
|
|
||||||
document = document.insertBlockAtRange(selection, block)
|
|
||||||
|
|
||||||
// Determine what the selection should be after inserting.
|
|
||||||
const keys = state.document.getTexts().map(text => text.key)
|
|
||||||
const text = document.getTexts().find(n => !keys.includes(n.key))
|
|
||||||
selection = selection.collapseToEndOf(text)
|
|
||||||
|
|
||||||
// Update the document and selection.
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a `fragment` at the current selection.
|
|
||||||
*
|
|
||||||
* @param {Document} fragment
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
insertFragment(fragment) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let after = selection
|
|
||||||
|
|
||||||
// If there's nothing in the fragment, do nothing.
|
|
||||||
if (!fragment.length) return state
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
document = document.insertFragmentAtRange(selection, fragment)
|
|
||||||
|
|
||||||
// Determine what the selection should be after inserting.
|
|
||||||
const keys = beforeTexts.map(text => text.key)
|
|
||||||
const text = document.getTexts().findLast(n => !keys.includes(n.key))
|
|
||||||
const previousText = text ? document.getPreviousText(text) : null
|
|
||||||
|
|
||||||
if (text && lastInline && previousText) {
|
|
||||||
after = selection.collapseToEndOf(previousText)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (text && lastInline) {
|
|
||||||
after = selection.collapseToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (text) {
|
|
||||||
after = selection
|
|
||||||
.collapseToStartOf(text)
|
|
||||||
.moveForward(lastText.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
after = selection
|
|
||||||
.collapseToStart()
|
|
||||||
.moveForward(lastText.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the document and selection.
|
|
||||||
selection = after
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a `inline` at the current selection.
|
|
||||||
*
|
|
||||||
* @param {String || Object || Block} inline
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
insertInline(inline) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection, startText } = state
|
|
||||||
let after = selection
|
|
||||||
const hasVoid = document.hasVoidParent(startText)
|
|
||||||
|
|
||||||
// Insert the inline
|
|
||||||
document = document.insertInlineAtRange(selection, inline)
|
|
||||||
|
|
||||||
// Determine what the selection should be after inserting.
|
|
||||||
if (hasVoid) {
|
|
||||||
selection = selection
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const keys = state.document.getTexts().map(text => text.key)
|
|
||||||
const text = document.getTexts().find(n => !keys.includes(n.key))
|
|
||||||
selection = selection.collapseToEndOf(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the document and selection.
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a `text` string at the current selection.
|
|
||||||
*
|
|
||||||
* @param {String} text
|
|
||||||
* @param {Set} marks (optional)
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
insertText(text, marks) {
|
|
||||||
let state = this
|
|
||||||
let { cursorMarks, document, selection } = state
|
|
||||||
let after
|
|
||||||
const isVoid = document.hasVoidParent(state.startText)
|
|
||||||
|
|
||||||
// Determine what the selection should be after inserting.
|
|
||||||
if (isVoid) {
|
|
||||||
after = selection
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (selection.isExpanded) {
|
|
||||||
after = selection.collapseToStart().moveForward(text.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
after = selection.moveForward(text.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert the text and update the selection.
|
|
||||||
document = document.insertTextAtRange(selection, text, marks || cursorMarks)
|
|
||||||
selection = after
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selection to a specific anchor and focus point.
|
|
||||||
*
|
|
||||||
* @param {Object} properties
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
moveTo(properties) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
|
|
||||||
// Allow for passing a `Selection` object.
|
|
||||||
if (properties instanceof Selection) {
|
|
||||||
properties = {
|
|
||||||
anchorKey: properties.anchorKey,
|
|
||||||
anchorOffset: properties.anchorOffset,
|
|
||||||
focusKey: properties.focusKey,
|
|
||||||
focusOffset: properties.focusOffset,
|
|
||||||
isFocused: properties.isFocused
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass in properties, and force `isBackward` to be re-resolved.
|
|
||||||
selection = selection.merge({
|
|
||||||
...properties,
|
|
||||||
isBackward: null
|
|
||||||
})
|
|
||||||
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set `properties` of the block nodes in the current selection.
|
|
||||||
*
|
|
||||||
* @param {Object} properties
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
setBlock(properties) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
document = document.setBlockAtRange(selection, properties)
|
|
||||||
state = state.merge({ document })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set `properties` of the inline nodes in the current selection.
|
|
||||||
*
|
|
||||||
* @param {Object} properties
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
setInline(properties) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
document = document.setInlineAtRange(selection, properties)
|
|
||||||
state = state.merge({ document })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split the block node at the current selection, to optional `depth`.
|
|
||||||
*
|
|
||||||
* @param {Number} depth (optional)
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
splitBlock(depth = 1) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
|
|
||||||
// Split the document.
|
|
||||||
document = document.splitBlockAtRange(selection, depth)
|
|
||||||
|
|
||||||
// Determine what the selection should be after splitting.
|
|
||||||
const { startKey } = selection
|
|
||||||
const startNode = document.getDescendant(startKey)
|
|
||||||
const nextNode = document.getNextText(startNode)
|
|
||||||
selection = selection.collapseToStartOf(nextNode)
|
|
||||||
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split the inline nodes at the current selection, to optional `depth`.
|
|
||||||
*
|
|
||||||
* @param {Number} depth (optional)
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
splitInline(depth = Infinity) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
|
|
||||||
// Split the document.
|
|
||||||
document = document.splitInlineAtRange(selection, depth)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a `mark` from the characters in the current selection.
|
|
||||||
*
|
|
||||||
* @param {Mark} mark
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
removeMark(mark) {
|
|
||||||
mark = normalizeMark(mark)
|
|
||||||
let state = this
|
|
||||||
let { cursorMarks, document, selection } = state
|
|
||||||
|
|
||||||
// If the selection is collapsed, remove the mark from the cursor instead.
|
|
||||||
if (selection.isCollapsed) {
|
|
||||||
const marks = document.getMarksAtRange(selection)
|
|
||||||
state = state.merge({ cursorMarks: marks.remove(mark) })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
document = document.removeMarkAtRange(selection, mark)
|
|
||||||
state = state.merge({ document })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or remove a `mark` from the characters in the current selection,
|
|
||||||
* depending on whether it's already there.
|
|
||||||
*
|
|
||||||
* @param {Mark} mark
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
toggleMark(mark) {
|
|
||||||
mark = normalizeMark(mark)
|
|
||||||
let state = this
|
|
||||||
let { marks, document, selection } = state
|
|
||||||
const exists = marks.some(m => m.equals(mark))
|
|
||||||
return exists
|
|
||||||
? state.removeMark(mark)
|
|
||||||
: state.addMark(mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap the block nodes in the current selection with a new block node with
|
|
||||||
* `properties`.
|
|
||||||
*
|
|
||||||
* @param {Object or String} properties
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
wrapBlock(properties) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
document = document.wrapBlockAtRange(selection, properties)
|
|
||||||
state = state.merge({ document })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unwrap the current selection from a block parent with `properties`.
|
|
||||||
*
|
|
||||||
* @param {Object or String} properties
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
unwrapBlock(properties) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
document = document.unwrapBlockAtRange(selection, properties)
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap the current selection in new inline nodes with `properties`.
|
|
||||||
*
|
|
||||||
* @param {Object or String} properties
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
wrapInline(properties) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
const { startKey } = selection
|
|
||||||
const previous = document.getPreviousText(startKey)
|
|
||||||
|
|
||||||
document = document.wrapInlineAtRange(selection, properties)
|
|
||||||
|
|
||||||
// Determine what the selection should be after wrapping.
|
|
||||||
if (selection.isCollapsed) {
|
|
||||||
selection = selection
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (selection.startOffset == 0) {
|
|
||||||
const text = previous
|
|
||||||
? document.getNextText(previous)
|
|
||||||
: document.getTexts().first()
|
|
||||||
selection = selection.moveToRangeOf(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (selection.startKey == selection.endKey) {
|
|
||||||
const text = document.getNextText(selection.startKey)
|
|
||||||
selection = selection.moveToRangeOf(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const anchor = document.getNextText(selection.anchorKey)
|
|
||||||
const focus = document.getDescendant(selection.focusKey)
|
|
||||||
selection = selection.merge({
|
|
||||||
anchorKey: anchor.key,
|
|
||||||
anchorOffset: 0,
|
|
||||||
focusKey: focus.key,
|
|
||||||
focusOffset: selection.focusOffset
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap the current selection with prefix/suffix.
|
|
||||||
*
|
|
||||||
* @param {String} prefix
|
|
||||||
* @param {String} suffix
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
wrapText(prefix, suffix = prefix) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
let { anchorOffset, anchorKey, focusOffset, focusKey, isBackward } = selection
|
|
||||||
let after
|
|
||||||
|
|
||||||
// Determine what the selection should be after wrapping.
|
|
||||||
if (anchorKey == focusKey) {
|
|
||||||
after = selection.moveForward(prefix.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
after = selection.merge({
|
|
||||||
anchorOffset: isBackward ? anchorOffset : anchorOffset + prefix.length,
|
|
||||||
focusOffset: isBackward ? focusOffset + prefix.length : focusOffset
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap the text and update the state.
|
|
||||||
document = document.wrapTextAtRange(selection, prefix, suffix)
|
|
||||||
selection = after
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unwrap the current selection from an inline parent with `properties`.
|
|
||||||
*
|
|
||||||
* @param {Object or String} properties
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
unwrapInline(properties) {
|
|
||||||
let state = this
|
|
||||||
let { document, selection } = state
|
|
||||||
document = document.unwrapInlineAtRange(selection, properties)
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a `mark` argument, which can be a string or plain object too.
|
|
||||||
*
|
|
||||||
* @param {Mark or String or Object} mark
|
|
||||||
* @return {Mark}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function normalizeMark(mark) {
|
|
||||||
if (typeof mark == 'string') {
|
|
||||||
return Mark.create({ type: mark })
|
|
||||||
} else {
|
|
||||||
return Mark.create(mark)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
|
import Transforms from '../transforms'
|
||||||
import includes from 'lodash/includes'
|
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'
|
||||||
@@ -23,39 +24,13 @@ const Step = new Record({
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Document range transforms.
|
* Defaults.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DOCUMENT_RANGE_TRANSFORMS = [
|
const DEFAULT_PROPERTIES = {
|
||||||
'deleteAtRange',
|
state: null,
|
||||||
'deleteBackwardAtRange',
|
steps: new List()
|
||||||
'deleteForwardAtRange',
|
}
|
||||||
'insertBlockAtRange',
|
|
||||||
'insertFragmentAtRange',
|
|
||||||
'insertInlineAtRange',
|
|
||||||
'insertTextAtRange',
|
|
||||||
'addMarkAtRange',
|
|
||||||
'setBlockAtRange',
|
|
||||||
'setInlineAtRange',
|
|
||||||
'splitBlockAtRange',
|
|
||||||
'splitInlineAtRange',
|
|
||||||
'removeMarkAtRange',
|
|
||||||
'toggleMarkAtRange',
|
|
||||||
'unwrapBlockAtRange',
|
|
||||||
'unwrapInlineAtRange',
|
|
||||||
'wrapBlockAtRange',
|
|
||||||
'wrapInlineAtRange',
|
|
||||||
'wrapTextAtRange'
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Document node transforms.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const DOCUMENT_NODE_TRANSFORMS = [
|
|
||||||
'removeNodeByKey',
|
|
||||||
'setNodeByKey',
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selection transforms.
|
* Selection transforms.
|
||||||
@@ -77,40 +52,7 @@ const SELECTION_TRANSFORMS = [
|
|||||||
'moveBackward',
|
'moveBackward',
|
||||||
'moveForward',
|
'moveForward',
|
||||||
'moveToOffsets',
|
'moveToOffsets',
|
||||||
'moveToRangeOf'
|
'moveToRangeOf',
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State-level document transforms.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const STATE_DOCUMENT_TRANSFORMS = [
|
|
||||||
'delete',
|
|
||||||
'deleteBackward',
|
|
||||||
'deleteForward',
|
|
||||||
'insertBlock',
|
|
||||||
'insertFragment',
|
|
||||||
'insertInline',
|
|
||||||
'insertText',
|
|
||||||
'addMark',
|
|
||||||
'setBlock',
|
|
||||||
'setInline',
|
|
||||||
'splitBlock',
|
|
||||||
'splitInline',
|
|
||||||
'removeMark',
|
|
||||||
'toggleMark',
|
|
||||||
'unwrapBlock',
|
|
||||||
'unwrapInline',
|
|
||||||
'wrapBlock',
|
|
||||||
'wrapInline',
|
|
||||||
'wrapText'
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State selection transforms.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const STATE_SELECTION_TRANSFORMS = [
|
|
||||||
'collapseToEndOfNextBlock',
|
'collapseToEndOfNextBlock',
|
||||||
'collapseToEndOfNextText',
|
'collapseToEndOfNextText',
|
||||||
'collapseToEndOfPreviousBlock',
|
'collapseToEndOfPreviousBlock',
|
||||||
@@ -122,33 +64,6 @@ const STATE_SELECTION_TRANSFORMS = [
|
|||||||
'moveTo',
|
'moveTo',
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
|
||||||
* All state-level transforms.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const STATE_TRANSFORMS = []
|
|
||||||
.concat(STATE_DOCUMENT_TRANSFORMS)
|
|
||||||
.concat(STATE_SELECTION_TRANSFORMS)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All transforms.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const TRANSFORMS = []
|
|
||||||
.concat(DOCUMENT_RANGE_TRANSFORMS)
|
|
||||||
.concat(DOCUMENT_NODE_TRANSFORMS)
|
|
||||||
.concat(SELECTION_TRANSFORMS)
|
|
||||||
.concat(STATE_TRANSFORMS)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defaults.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const DEFAULT_PROPERTIES = {
|
|
||||||
state: null,
|
|
||||||
steps: new List()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform.
|
* Transform.
|
||||||
*/
|
*/
|
||||||
@@ -224,37 +139,14 @@ class Transform extends new Record(DEFAULT_PROPERTIES) {
|
|||||||
|
|
||||||
applyStep(state, step) {
|
applyStep(state, step) {
|
||||||
const { type, args } = step
|
const { type, args } = step
|
||||||
|
const transform = Transforms[type]
|
||||||
|
|
||||||
if (includes(DOCUMENT_RANGE_TRANSFORMS, type)) {
|
if (!transform) {
|
||||||
let { document, selection } = state
|
throw new Error(`Unknown transform type: "${type}".`)
|
||||||
let [ range, ...rest ] = args
|
|
||||||
range = range.normalize(document)
|
|
||||||
document = document[type](range, ...rest)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (includes(DOCUMENT_NODE_TRANSFORMS, type)) {
|
state = transform(state, ...args)
|
||||||
let { document, selection } = state
|
return state
|
||||||
document = document[type](...args)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ document, selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (includes(SELECTION_TRANSFORMS, type)) {
|
|
||||||
let { document, selection } = state
|
|
||||||
selection = selection[type](...args)
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (includes(STATE_TRANSFORMS, type)) {
|
|
||||||
state = state[type](...args)
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -271,13 +163,7 @@ class Transform extends new Record(DEFAULT_PROPERTIES) {
|
|||||||
const previous = undos.peek()
|
const previous = undos.peek()
|
||||||
|
|
||||||
// If the only steps applied are selection transforms, don't snapshot.
|
// If the only steps applied are selection transforms, don't snapshot.
|
||||||
const onlySelections = steps.every((step) => {
|
const onlySelections = steps.every(step => includes(SELECTION_TRANSFORMS, step.type))
|
||||||
return (
|
|
||||||
includes(SELECTION_TRANSFORMS, step.type) ||
|
|
||||||
includes(STATE_SELECTION_TRANSFORMS, step.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.
|
||||||
@@ -392,7 +278,7 @@ class Transform extends new Record(DEFAULT_PROPERTIES) {
|
|||||||
* Add a step-creating method for each of the transforms.
|
* Add a step-creating method for each of the transforms.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
TRANSFORMS.forEach((type) => {
|
Object.keys(Transforms).forEach((type) => {
|
||||||
Transform.prototype[type] = function (...args) {
|
Transform.prototype[type] = function (...args) {
|
||||||
let transform = this
|
let transform = this
|
||||||
let { steps } = transform
|
let { steps } = transform
|
||||||
|
File diff suppressed because it is too large
Load Diff
612
lib/transforms/at-current-range.js
Normal file
612
lib/transforms/at-current-range.js
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
|
||||||
|
import normalizeMark from '../utils/normalize-mark'
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Mark} mark
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function addMark(state, mark) {
|
||||||
|
mark = normalizeMark(mark)
|
||||||
|
let { cursorMarks, document, selection } = state
|
||||||
|
|
||||||
|
// If the selection is collapsed, add the mark to the cursor instead.
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
const marks = document.getMarksAtRange(selection)
|
||||||
|
state = state.merge({ cursorMarks: marks.add(mark) })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
return addMarkAtRange(state, selection, mark)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete at the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function _delete(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let after
|
||||||
|
|
||||||
|
// When collapsed, there's nothing to do.
|
||||||
|
if (selection.isCollapsed) return state
|
||||||
|
|
||||||
|
// Determine what the selection will be after deleting.
|
||||||
|
const { startText } = state
|
||||||
|
const { startKey, startOffset, endKey, endOffset } = selection
|
||||||
|
const block = document.getClosestBlock(startText)
|
||||||
|
const highest = block.getHighestChild(startText)
|
||||||
|
const previous = block.getPreviousSibling(highest)
|
||||||
|
const next = block.getNextSibling(highest)
|
||||||
|
|
||||||
|
if (
|
||||||
|
previous &&
|
||||||
|
startOffset == 0 &&
|
||||||
|
(endKey != startKey || endOffset == startText.length)
|
||||||
|
) {
|
||||||
|
if (previous.kind == 'text') {
|
||||||
|
if (next && next.kind == 'text') {
|
||||||
|
after = selection.merge({
|
||||||
|
anchorKey: previous.key,
|
||||||
|
anchorOffset: previous.length,
|
||||||
|
focusKey: previous.key,
|
||||||
|
focusOffset: previous.length
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
after = selection.collapseToEndOf(previous)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const last = previous.getTexts().last()
|
||||||
|
after = selection.collapseToEndOf(last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
after = selection.collapseToStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete and update the selection.
|
||||||
|
state = deleteAtRange(state, selection)
|
||||||
|
state = state.merge({ selection: after })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete backward `n` characters at the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Number} n (optional)
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function deleteBackward(state, n = 1) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let after = selection
|
||||||
|
|
||||||
|
// Determine what the selection should be after deleting.
|
||||||
|
const { startKey } = selection
|
||||||
|
const startNode = document.getDescendant(startKey)
|
||||||
|
|
||||||
|
if (selection.isExpanded) {
|
||||||
|
after = selection.collapseToStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (selection.isAtStartOf(document)) {
|
||||||
|
after = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (selection.isAtStartOf(startNode)) {
|
||||||
|
const previous = document.getPreviousText(startNode)
|
||||||
|
const prevBlock = document.getClosestBlock(previous)
|
||||||
|
const prevInline = document.getClosestInline(previous)
|
||||||
|
|
||||||
|
if (prevBlock && prevBlock.isVoid) {
|
||||||
|
after = selection
|
||||||
|
} else if (prevInline && prevInline.isVoid) {
|
||||||
|
after = selection
|
||||||
|
} else {
|
||||||
|
after = selection.collapseToEndOf(previous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (selection.isAtEndOf(startNode) && startNode.length == 1) {
|
||||||
|
const block = document.getClosestBlock(startKey)
|
||||||
|
const highest = block.getHighestChild(startKey)
|
||||||
|
const previous = block.getPreviousSibling(highest)
|
||||||
|
const next = block.getNextSibling(highest)
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
if (previous.kind == 'text') {
|
||||||
|
if (next && next.kind == 'text') {
|
||||||
|
after = selection.merge({
|
||||||
|
anchorKey: previous.key,
|
||||||
|
anchorOffset: previous.length,
|
||||||
|
focusKey: previous.key,
|
||||||
|
focusOffset: previous.length
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
after = selection.collapseToEndOf(previous)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const last = previous.getTexts().last()
|
||||||
|
after = selection.collapseToEndOf(last)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
after = selection.moveBackward(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
after = selection.moveBackward(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete backward and then update the selection.
|
||||||
|
state = deleteBackwardAtRange(state, selection, n)
|
||||||
|
state = state.merge({ selection: after })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete forward `n` characters at the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Number} n (optional)
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function deleteForward(state, n = 1) {
|
||||||
|
let { document, selection, startText } = state
|
||||||
|
let { startKey, startOffset } = selection
|
||||||
|
let after = selection
|
||||||
|
|
||||||
|
// Determine what the selection should be after deleting.
|
||||||
|
const block = document.getClosestBlock(startKey)
|
||||||
|
const inline = document.getClosestInline(startKey)
|
||||||
|
const highest = block.getHighestChild(startKey)
|
||||||
|
const previous = block.getPreviousSibling(highest)
|
||||||
|
const next = block.getNextSibling(highest)
|
||||||
|
|
||||||
|
if (selection.isExpanded) {
|
||||||
|
after = selection.collapseToStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
else if ((block && block.isVoid) || (inline && inline.isVoid)) {
|
||||||
|
const nextText = document.getNextText(startKey)
|
||||||
|
const prevText = document.getPreviousText(startKey)
|
||||||
|
after = next
|
||||||
|
? selection.collapseToStartOf(nextText)
|
||||||
|
: selection.collapseToEndOf(prevText)
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (previous && startOffset == 0 && startText.length == 1) {
|
||||||
|
if (previous.kind == 'text') {
|
||||||
|
if (next && next.kind == 'text') {
|
||||||
|
after = selection.merge({
|
||||||
|
anchorKey: previous.key,
|
||||||
|
anchorOffset: previous.length,
|
||||||
|
focusKey: previous.key,
|
||||||
|
focusOffset: previous.length
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
after = selection.collapseToEndOf(previous)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const last = previous.getTexts().last()
|
||||||
|
after = selection.collapseToEndOf(last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete forward and then update the selection.
|
||||||
|
state = deleteForwardAtRange(state, selection, n)
|
||||||
|
state = state.merge({ selection: after })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a `block` at the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {String || Object || Block} block
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function insertBlock(state, block) {
|
||||||
|
let { document, selection } = state
|
||||||
|
const keys = document.getTexts().map(text => text.key)
|
||||||
|
|
||||||
|
// Insert the block
|
||||||
|
state = insertBlockAtRange(state, selection, block)
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Update the document and selection.
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a `fragment` at the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Document} fragment
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function insertFragment(state, fragment) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let after = selection
|
||||||
|
|
||||||
|
// If there's nothing in the fragment, do nothing.
|
||||||
|
if (!fragment.length) return state
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
state = insertFragmentAtRange(state, selection, fragment)
|
||||||
|
document = state.document
|
||||||
|
selection = state.selection
|
||||||
|
|
||||||
|
// Determine what the selection should be after inserting.
|
||||||
|
const keys = beforeTexts.map(text => text.key)
|
||||||
|
const text = document.getTexts().findLast(n => !keys.includes(n.key))
|
||||||
|
const previousText = text ? document.getPreviousText(text) : null
|
||||||
|
|
||||||
|
if (text && lastInline && previousText) {
|
||||||
|
after = selection.collapseToEndOf(previousText)
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (text && lastInline) {
|
||||||
|
after = selection.collapseToStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (text) {
|
||||||
|
after = selection
|
||||||
|
.collapseToStartOf(text)
|
||||||
|
.moveForward(lastText.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
after = selection
|
||||||
|
.collapseToStart()
|
||||||
|
.moveForward(lastText.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the document and selection.
|
||||||
|
selection = after
|
||||||
|
state = state.merge({ document, selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a `inline` at the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {String || Object || Block} inline
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function insertInline(state, inline) {
|
||||||
|
let { document, selection, startText } = state
|
||||||
|
const hasVoid = document.hasVoidParent(startText)
|
||||||
|
const keys = document.getTexts().map(text => text.key)
|
||||||
|
|
||||||
|
// Insert the inline
|
||||||
|
state = insertInlineAtRange(state, selection, inline)
|
||||||
|
document = state.document
|
||||||
|
selection = state.selection
|
||||||
|
|
||||||
|
// Determine what the selection should be after inserting.
|
||||||
|
if (hasVoid) {
|
||||||
|
selection = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
const text = document.getTexts().find(n => !keys.includes(n.key))
|
||||||
|
selection = selection.collapseToEndOf(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the document and selection.
|
||||||
|
state = state.merge({ document, selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a `text` string at the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {String} text
|
||||||
|
* @param {Set} marks (optional)
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function insertText(state, text, marks) {
|
||||||
|
let { cursorMarks, document, selection } = state
|
||||||
|
let after
|
||||||
|
const isVoid = document.hasVoidParent(state.startText)
|
||||||
|
|
||||||
|
// Determine what the selection should be after inserting.
|
||||||
|
if (isVoid) {
|
||||||
|
after = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (selection.isExpanded) {
|
||||||
|
after = selection.collapseToStart().moveForward(text.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
after = selection.moveForward(text.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the text and update the selection.
|
||||||
|
state = insertTextAtRange(state, selection, text, marks || cursorMarks)
|
||||||
|
state = state.merge({ selection: after })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set `properties` of the block nodes in the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Object} properties
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setBlock(state, properties) {
|
||||||
|
return setBlockAtRange(state, state.selection, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set `properties` of the inline nodes in the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Object} properties
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setInline(state, properties) {
|
||||||
|
return setInlineAtRange(state, state.selection, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split the block node at the current selection, to optional `depth`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Number} depth (optional)
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function splitBlock(state, depth = 1) {
|
||||||
|
state = splitBlockAtRange(state, state.selection, depth)
|
||||||
|
let { document, selection } = state
|
||||||
|
|
||||||
|
// Determine what the selection should be after splitting.
|
||||||
|
const { startKey } = selection
|
||||||
|
const startNode = document.getDescendant(startKey)
|
||||||
|
const nextNode = document.getNextText(startNode)
|
||||||
|
selection = selection.collapseToStartOf(nextNode)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split the inline nodes at the current selection, to optional `depth`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Number} depth (optional)
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function splitInline(state, depth = Infinity) {
|
||||||
|
let { document, selection } = state
|
||||||
|
|
||||||
|
// Split the document.
|
||||||
|
state = splitInlineAtRange(state, selection, depth)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.merge({ document, selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a `mark` from the characters in the current selection.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Mark} mark
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function removeMark(state, mark) {
|
||||||
|
mark = normalizeMark(mark)
|
||||||
|
let { cursorMarks, document, selection } = state
|
||||||
|
|
||||||
|
// If the selection is collapsed, remove the mark from the cursor instead.
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
const marks = document.getMarksAtRange(selection)
|
||||||
|
state = state.merge({ cursorMarks: marks.remove(mark) })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
return removeMarkAtRange(state, state.selection, mark)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or remove a `mark` from the characters in the current selection,
|
||||||
|
* depending on whether it's already there.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Mark} mark
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function toggleMark(state, mark) {
|
||||||
|
mark = normalizeMark(mark)
|
||||||
|
const exists = state.marks.some(m => m.equals(mark))
|
||||||
|
return exists
|
||||||
|
? removeMark(state, mark)
|
||||||
|
: addMark(state, mark)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unwrap the current selection from a block parent with `properties`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Object or String} properties
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function unwrapBlock(state, properties) {
|
||||||
|
return unwrapBlockAtRange(state, state.selection, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unwrap the current selection from an inline parent with `properties`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Object or String} properties
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function unwrapInline(state, properties) {
|
||||||
|
return unwrapInlineAtRange(state, state.selection, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the block nodes in the current selection with a new block node with
|
||||||
|
* `properties`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Object or String} properties
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function wrapBlock(state, properties) {
|
||||||
|
return wrapBlockAtRange(state, state.selection, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the current selection in new inline nodes with `properties`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Object or String} properties
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function wrapInline(state, properties) {
|
||||||
|
let { document, selection } = state
|
||||||
|
const { startKey } = selection
|
||||||
|
const previous = document.getPreviousText(startKey)
|
||||||
|
|
||||||
|
state = wrapInlineAtRange(state, selection, properties)
|
||||||
|
document = state.document
|
||||||
|
selection = state.selection
|
||||||
|
|
||||||
|
// Determine what the selection should be after wrapping.
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
selection = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (selection.startOffset == 0) {
|
||||||
|
const text = previous
|
||||||
|
? document.getNextText(previous)
|
||||||
|
: document.getTexts().first()
|
||||||
|
selection = selection.moveToRangeOf(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (selection.startKey == selection.endKey) {
|
||||||
|
const text = document.getNextText(selection.startKey)
|
||||||
|
selection = selection.moveToRangeOf(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
const anchor = document.getNextText(selection.anchorKey)
|
||||||
|
const focus = document.getDescendant(selection.focusKey)
|
||||||
|
selection = selection.merge({
|
||||||
|
anchorKey: anchor.key,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusKey: focus.key,
|
||||||
|
focusOffset: selection.focusOffset
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the current selection with prefix/suffix.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {String} prefix
|
||||||
|
* @param {String} suffix
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function wrapText(state, prefix, suffix = prefix) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let { anchorOffset, anchorKey, focusOffset, focusKey, isBackward } = selection
|
||||||
|
let after
|
||||||
|
|
||||||
|
// Determine what the selection should be after wrapping.
|
||||||
|
if (anchorKey == focusKey) {
|
||||||
|
after = selection.moveForward(prefix.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
after = selection.merge({
|
||||||
|
anchorOffset: isBackward ? anchorOffset : anchorOffset + prefix.length,
|
||||||
|
focusOffset: isBackward ? focusOffset + prefix.length : focusOffset
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the text and update the state.
|
||||||
|
state = wrapTextAtRange(state, selection, prefix, suffix)
|
||||||
|
state = state.merge({ selection: after })
|
||||||
|
return state
|
||||||
|
}
|
1117
lib/transforms/at-range.js
Normal file
1117
lib/transforms/at-range.js
Normal file
File diff suppressed because it is too large
Load Diff
0
lib/transforms/by-current-keys.js
Normal file
0
lib/transforms/by-current-keys.js
Normal file
37
lib/transforms/by-key.js
Normal file
37
lib/transforms/by-key.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
import normalizeProperties from '../utils/normalize-node-or-mark-properties'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a node by `key`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {String} key
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function removeNodeByKey(state, key) {
|
||||||
|
let { document } = state
|
||||||
|
document = document.removeDescendant(key)
|
||||||
|
document = document.normalize()
|
||||||
|
state = state.merge({ document })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set `properties` on a node by `key`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {String} key
|
||||||
|
* @param {Object or String} properties
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function setNodeByKey(state, key, properties) {
|
||||||
|
properties = normalizeProperties(properties)
|
||||||
|
let { document } = state
|
||||||
|
let descendant = document.assertDescendant(key)
|
||||||
|
descendant = descendant.merge(properties)
|
||||||
|
document = document.updateDescendant(descendant)
|
||||||
|
state = state.merge({ document })
|
||||||
|
return state
|
||||||
|
}
|
188
lib/transforms/index.js
Normal file
188
lib/transforms/index.js
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* At range.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteAtRange,
|
||||||
|
deleteBackwardAtRange,
|
||||||
|
deleteForwardAtRange,
|
||||||
|
insertBlockAtRange,
|
||||||
|
insertFragmentAtRange,
|
||||||
|
insertInlineAtRange,
|
||||||
|
insertTextAtRange,
|
||||||
|
addMarkAtRange,
|
||||||
|
setBlockAtRange,
|
||||||
|
setInlineAtRange,
|
||||||
|
splitBlockAtRange,
|
||||||
|
splitInlineAtRange,
|
||||||
|
removeMarkAtRange,
|
||||||
|
toggleMarkAtRange,
|
||||||
|
unwrapBlockAtRange,
|
||||||
|
unwrapInlineAtRange,
|
||||||
|
wrapBlockAtRange,
|
||||||
|
wrapInlineAtRange,
|
||||||
|
wrapTextAtRange,
|
||||||
|
} from './at-range'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At current range.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
_delete,
|
||||||
|
deleteBackward,
|
||||||
|
deleteForward,
|
||||||
|
insertBlock,
|
||||||
|
insertFragment,
|
||||||
|
insertInline,
|
||||||
|
insertText,
|
||||||
|
addMark,
|
||||||
|
setBlock,
|
||||||
|
setInline,
|
||||||
|
splitBlock,
|
||||||
|
splitInline,
|
||||||
|
removeMark,
|
||||||
|
toggleMark,
|
||||||
|
unwrapBlock,
|
||||||
|
unwrapInline,
|
||||||
|
wrapBlock,
|
||||||
|
wrapInline,
|
||||||
|
wrapText,
|
||||||
|
} from './at-current-range'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By key.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
removeNodeByKey,
|
||||||
|
setNodeByKey,
|
||||||
|
} from './by-key'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On selection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
blur,
|
||||||
|
collapseToAnchor,
|
||||||
|
collapseToEnd,
|
||||||
|
collapseToEndOf,
|
||||||
|
collapseToEndOfNextBlock,
|
||||||
|
collapseToEndOfNextText,
|
||||||
|
collapseToEndOfPreviousBlock,
|
||||||
|
collapseToEndOfPreviousText,
|
||||||
|
collapseToFocus,
|
||||||
|
collapseToStart,
|
||||||
|
collapseToStartOf,
|
||||||
|
collapseToStartOfNextBlock,
|
||||||
|
collapseToStartOfNextText,
|
||||||
|
collapseToStartOfPreviousBlock,
|
||||||
|
collapseToStartOfPreviousText,
|
||||||
|
extendBackward,
|
||||||
|
extendForward,
|
||||||
|
extendToEndOf,
|
||||||
|
extendToStartOf,
|
||||||
|
focus,
|
||||||
|
moveBackward,
|
||||||
|
moveForward,
|
||||||
|
moveTo,
|
||||||
|
moveToOffsets,
|
||||||
|
moveToRangeOf,
|
||||||
|
} from './on-selection'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At range.
|
||||||
|
*/
|
||||||
|
|
||||||
|
deleteAtRange,
|
||||||
|
deleteBackwardAtRange,
|
||||||
|
deleteForwardAtRange,
|
||||||
|
insertBlockAtRange,
|
||||||
|
insertFragmentAtRange,
|
||||||
|
insertInlineAtRange,
|
||||||
|
insertTextAtRange,
|
||||||
|
addMarkAtRange,
|
||||||
|
setBlockAtRange,
|
||||||
|
setInlineAtRange,
|
||||||
|
splitBlockAtRange,
|
||||||
|
splitInlineAtRange,
|
||||||
|
removeMarkAtRange,
|
||||||
|
toggleMarkAtRange,
|
||||||
|
unwrapBlockAtRange,
|
||||||
|
unwrapInlineAtRange,
|
||||||
|
wrapBlockAtRange,
|
||||||
|
wrapInlineAtRange,
|
||||||
|
wrapTextAtRange,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At current range.
|
||||||
|
*/
|
||||||
|
|
||||||
|
delete: _delete,
|
||||||
|
deleteBackward,
|
||||||
|
deleteForward,
|
||||||
|
insertBlock,
|
||||||
|
insertFragment,
|
||||||
|
insertInline,
|
||||||
|
insertText,
|
||||||
|
addMark,
|
||||||
|
setBlock,
|
||||||
|
setInline,
|
||||||
|
splitBlock,
|
||||||
|
splitInline,
|
||||||
|
removeMark,
|
||||||
|
toggleMark,
|
||||||
|
unwrapBlock,
|
||||||
|
unwrapInline,
|
||||||
|
wrapBlock,
|
||||||
|
wrapInline,
|
||||||
|
wrapText,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By key.
|
||||||
|
*/
|
||||||
|
|
||||||
|
removeNodeByKey,
|
||||||
|
setNodeByKey,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On selection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
blur,
|
||||||
|
collapseToAnchor,
|
||||||
|
collapseToEnd,
|
||||||
|
collapseToEndOf,
|
||||||
|
collapseToEndOfNextBlock,
|
||||||
|
collapseToEndOfNextText,
|
||||||
|
collapseToEndOfPreviousBlock,
|
||||||
|
collapseToEndOfPreviousText,
|
||||||
|
collapseToFocus,
|
||||||
|
collapseToStart,
|
||||||
|
collapseToStartOf,
|
||||||
|
collapseToStartOfNextBlock,
|
||||||
|
collapseToStartOfNextText,
|
||||||
|
collapseToStartOfPreviousBlock,
|
||||||
|
collapseToStartOfPreviousText,
|
||||||
|
extendBackward,
|
||||||
|
extendForward,
|
||||||
|
extendToEndOf,
|
||||||
|
extendToStartOf,
|
||||||
|
focus,
|
||||||
|
moveBackward,
|
||||||
|
moveForward,
|
||||||
|
moveTo,
|
||||||
|
moveToOffsets,
|
||||||
|
moveToRangeOf,
|
||||||
|
|
||||||
|
}
|
211
lib/transforms/on-selection.js
Normal file
211
lib/transforms/on-selection.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
|
||||||
|
import Selection from '../models/selection'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to the end of the previous block.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function collapseToEndOfPreviousBlock(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let blocks = document.getBlocksAtRange(selection)
|
||||||
|
let block = blocks.first()
|
||||||
|
if (!block) return state
|
||||||
|
|
||||||
|
let previous = document.getPreviousBlock(block)
|
||||||
|
if (!previous) return state
|
||||||
|
|
||||||
|
selection = selection.collapseToEndOf(previous)
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to the start of the previous block.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function collapseToStartOfPreviousBlock(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let blocks = document.getBlocksAtRange(selection)
|
||||||
|
let block = blocks.first()
|
||||||
|
if (!block) return state
|
||||||
|
|
||||||
|
let previous = document.getPreviousBlock(block)
|
||||||
|
if (!previous) return state
|
||||||
|
|
||||||
|
selection = selection.collapseToStartOf(previous)
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to the start of the next block.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function collapseToStartOfNextBlock(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let blocks = document.getBlocksAtRange(selection)
|
||||||
|
let block = blocks.last()
|
||||||
|
if (!block) return state
|
||||||
|
|
||||||
|
let next = document.getNextBlock(block)
|
||||||
|
if (!next) return state
|
||||||
|
|
||||||
|
selection = selection.collapseToStartOf(next)
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to the end of the next block.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function collapseToEndOfNextBlock(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let blocks = document.getBlocksAtRange(selection)
|
||||||
|
let block = blocks.last()
|
||||||
|
if (!block) return state
|
||||||
|
|
||||||
|
let next = document.getNextBlock(block)
|
||||||
|
if (!next) return state
|
||||||
|
|
||||||
|
selection = selection.collapseToEndOf(next)
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to the start of the previous text.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function collapseToStartOfPreviousText(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let texts = document.getTextsAtRange(selection)
|
||||||
|
let text = texts.first()
|
||||||
|
if (!text) return state
|
||||||
|
|
||||||
|
let previous = document.getPreviousText(text)
|
||||||
|
if (!previous) return state
|
||||||
|
|
||||||
|
selection = selection.collapseToStartOf(previous)
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to the end of the previous text.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function collapseToEndOfPreviousText(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let texts = document.getTextsAtRange(selection)
|
||||||
|
let text = texts.first()
|
||||||
|
if (!text) return state
|
||||||
|
|
||||||
|
let previous = document.getPreviousText(text)
|
||||||
|
if (!previous) return state
|
||||||
|
|
||||||
|
selection = selection.collapseToEndOf(previous)
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to the start of the next text.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function collapseToStartOfNextText(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let texts = document.getTextsAtRange(selection)
|
||||||
|
let text = texts.last()
|
||||||
|
if (!text) return state
|
||||||
|
|
||||||
|
let next = document.getNextText(text)
|
||||||
|
if (!next) return state
|
||||||
|
|
||||||
|
selection = selection.collapseToStartOf(next)
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to the end of the next text.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function collapseToEndOfNextText(state) {
|
||||||
|
let { document, selection } = state
|
||||||
|
let texts = document.getTextsAtRange(selection)
|
||||||
|
let text = texts.last()
|
||||||
|
if (!text) return state
|
||||||
|
|
||||||
|
let next = document.getNextText(text)
|
||||||
|
if (!next) return state
|
||||||
|
|
||||||
|
selection = selection.collapseToEndOf(next)
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to a specific anchor and focus point.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Object} properties
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function moveTo(state, properties) {
|
||||||
|
let { document, selection } = state
|
||||||
|
|
||||||
|
// Allow for passing a `Selection` object.
|
||||||
|
if (properties instanceof Selection) {
|
||||||
|
properties = {
|
||||||
|
anchorKey: properties.anchorKey,
|
||||||
|
anchorOffset: properties.anchorOffset,
|
||||||
|
focusKey: properties.focusKey,
|
||||||
|
focusOffset: properties.focusOffset,
|
||||||
|
isFocused: properties.isFocused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass in properties, and force `isBackward` to be re-resolved.
|
||||||
|
selection = selection.merge({
|
||||||
|
...properties,
|
||||||
|
isBackward: null
|
||||||
|
})
|
||||||
|
|
||||||
|
selection = selection.normalize(document)
|
||||||
|
state = state.merge({ selection })
|
||||||
|
return state
|
||||||
|
}
|
13
lib/transforms/options.js
Normal file
13
lib/transforms/options.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the transforms
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the transform as not being "distinct", in that it by itself should not
|
||||||
|
* create a new save boundary.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param
|
||||||
|
*/
|
32
lib/utils/is-in-range.js
Normal file
32
lib/utils/is-in-range.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an `index` of a `text` node is in a `range`.
|
||||||
|
*
|
||||||
|
* @param {Number} index
|
||||||
|
* @param {Text} text
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {Set} characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isInRange(index, text, range) {
|
||||||
|
const { startKey, startOffset, endKey, endOffset } = range
|
||||||
|
let matcher
|
||||||
|
|
||||||
|
if (text.key == startKey && text.key == endKey) {
|
||||||
|
return startOffset <= index && index < endOffset
|
||||||
|
} else if (text.key == startKey) {
|
||||||
|
return startOffset <= index
|
||||||
|
} else if (text.key == endKey) {
|
||||||
|
return index < endOffset
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default isInRange
|
35
lib/utils/normalize-block.js
Normal file
35
lib/utils/normalize-block.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
import Block from '../models/block'
|
||||||
|
import normalizeProperties from './normalize-node-or-mark-properties'
|
||||||
|
import typeOf from 'type-of'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a `block` argument, which can be a string or plain object too.
|
||||||
|
*
|
||||||
|
* @param {Block or String or Object} block
|
||||||
|
* @return {Block}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizeBlock(block) {
|
||||||
|
if (block instanceof Block) return block
|
||||||
|
|
||||||
|
const type = typeOf(block)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
case 'object': {
|
||||||
|
return Block.create(normalizeProperties(block))
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`A \`block\` argument must be a block, an object or a string, but you passed: "${type}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default normalizeBlock
|
35
lib/utils/normalize-inline.js
Normal file
35
lib/utils/normalize-inline.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
import Inline from '../models/inline'
|
||||||
|
import normalizeProperties from './normalize-node-or-mark-properties'
|
||||||
|
import typeOf from 'type-of'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize an `inline` argument, which can be a string or plain object too.
|
||||||
|
*
|
||||||
|
* @param {Inline or String or Object} inline
|
||||||
|
* @return {Inline}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizeInline(inline) {
|
||||||
|
if (inline instanceof Inline) return inline
|
||||||
|
|
||||||
|
const type = typeOf(inline)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
case 'object': {
|
||||||
|
return Inline.create(normalizeProperties(inline))
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`An \`inline\` argument must be an inline, an object or a string, but you passed: "${type}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default normalizeInline
|
35
lib/utils/normalize-mark.js
Normal file
35
lib/utils/normalize-mark.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
import Mark from '../models/mark'
|
||||||
|
import typeOf from 'type-of'
|
||||||
|
import normalizeProperties from './normalize-node-or-mark-properties'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a `mark` argument, which can be a string or plain object too.
|
||||||
|
*
|
||||||
|
* @param {Mark or String or Object} mark
|
||||||
|
* @return {Mark}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizeMark(mark) {
|
||||||
|
if (mark instanceof Mark) return mark
|
||||||
|
|
||||||
|
const type = typeOf(mark)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
case 'object': {
|
||||||
|
return Mark.create(normalizeProperties(mark))
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`A \`mark\` argument must be a mark, an object or a string, but you passed: "${type}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default normalizeMark
|
47
lib/utils/normalize-node-or-mark-properties.js
Normal file
47
lib/utils/normalize-node-or-mark-properties.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
import Data from '../models/data'
|
||||||
|
import typeOf from 'type-of'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize the `properties` of a node or mark, which can be either a type
|
||||||
|
* string or a dictionary of properties. If it's a dictionary, `data` is
|
||||||
|
* optional and shouldn't be set if null or undefined.
|
||||||
|
*
|
||||||
|
* @param {String or Object} properties
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizeNodeOrMarkProperties(properties = {}) {
|
||||||
|
const ret = {}
|
||||||
|
const type = typeOf(properties)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'string': {
|
||||||
|
ret.type = properties
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'object': {
|
||||||
|
for (const key in properties) {
|
||||||
|
if (key == 'data') {
|
||||||
|
if (properties[key] != null) ret[key] = Data.create(properties[key])
|
||||||
|
} else {
|
||||||
|
ret[key] = properties[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`A \`properties\` argument must be an object or a string, but you passed: "${type}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default normalizeNodeOrMarkProperties
|
Reference in New Issue
Block a user