1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-02-25 01:33:37 +01:00
slate/lib/models/state.js

895 lines
19 KiB
JavaScript
Raw Normal View History

2016-06-15 12:07:12 -07:00
2016-07-06 20:19:19 -07:00
import Document from './document'
2016-06-17 18:52:23 -07:00
import Selection from './selection'
import Transform from './transform'
2016-06-27 14:08:30 -07:00
import uid from '../utils/uid'
import { Record, Stack } from 'immutable'
2016-06-15 12:07:12 -07:00
/**
* History.
2016-06-15 12:07:12 -07:00
*/
const History = new Record({
undos: new Stack(),
redos: new Stack()
2016-06-15 12:07:12 -07:00
})
2016-06-17 00:09:54 -07:00
/**
* Default properties.
2016-06-17 00:09:54 -07:00
*/
2016-06-20 12:57:31 -07:00
const DEFAULTS = {
document: new Document(),
selection: new Selection(),
history: new History(),
isNative: false
}
2016-06-17 00:09:54 -07:00
2016-06-15 12:07:12 -07:00
/**
* State.
*/
2016-07-06 20:19:19 -07:00
class State extends new Record(DEFAULTS) {
2016-06-15 12:07:12 -07:00
/**
2016-06-17 18:20:26 -07:00
* Create a new `State` with `properties`.
2016-06-15 12:07:12 -07:00
*
2016-06-20 12:57:31 -07:00
* @param {Object} properties
2016-06-15 12:07:12 -07:00
* @return {State} state
*/
2016-06-17 18:20:26 -07:00
static create(properties = {}) {
2016-06-23 10:43:36 -07:00
if (properties instanceof State) return properties
properties.document = Document.create(properties.document)
properties.selection = Selection.create(properties.selection)
2016-06-17 18:20:26 -07:00
return new State(properties)
2016-06-15 12:07:12 -07:00
}
2016-06-28 15:47:29 -07:00
/**
* Is the current selection blurred?
*
* @return {Boolean} isBlurred
*/
get isBlurred() {
return this.selection.isBlurred
}
/**
* Is the current selection focused?
*
* @return {Boolean} isFocused
*/
get isFocused() {
return this.selection.isFocused
}
/**
* Is the current selection collapsed?
*
* @return {Boolean} isCollapsed
*/
2016-06-23 15:39:44 -07:00
get isCollapsed() {
return this.selection.isCollapsed
}
/**
* Is the current selection expanded?
*
* @return {Boolean} isExpanded
*/
2016-06-23 15:39:44 -07:00
get isExpanded() {
return this.selection.isExpanded
}
2016-06-22 18:59:19 -07:00
/**
* Is the current selection backward?
*
* @return {Boolean} isBackward
*/
2016-06-23 15:39:44 -07:00
get isBackward() {
2016-06-22 18:59:19 -07:00
return this.selection.isBackward
}
/**
* Is the current selection forward?
*
* @return {Boolean} isForward
*/
2016-06-23 15:39:44 -07:00
get isForward() {
2016-06-22 18:59:19 -07:00
return this.selection.isForward
}
2016-06-21 10:43:04 -07:00
/**
* Get the current start key.
*
* @return {String} startKey
*/
2016-06-23 15:39:44 -07:00
get startKey() {
2016-06-21 10:43:04 -07:00
return this.selection.startKey
}
/**
* Get the current end key.
*
* @return {String} endKey
*/
2016-06-23 15:39:44 -07:00
get endKey() {
2016-06-21 10:43:04 -07:00
return this.selection.endKey
}
/**
* Get the current start offset.
*
* @return {String} startOffset
*/
2016-06-23 15:39:44 -07:00
get startOffset() {
2016-06-21 10:43:04 -07:00
return this.selection.startOffset
}
/**
* Get the current end offset.
*
* @return {String} endOffset
*/
2016-06-23 15:39:44 -07:00
get endOffset() {
2016-06-21 10:43:04 -07:00
return this.selection.endOffset
}
2016-06-22 18:59:19 -07:00
/**
* Get the current anchor key.
*
* @return {String} anchorKey
*/
2016-06-23 15:39:44 -07:00
get anchorKey() {
2016-06-22 18:59:19 -07:00
return this.selection.anchorKey
}
/**
* Get the current focus key.
*
* @return {String} focusKey
*/
2016-06-23 15:39:44 -07:00
get focusKey() {
2016-06-22 18:59:19 -07:00
return this.selection.focusKey
}
/**
* Get the current anchor offset.
*
* @return {String} anchorOffset
*/
2016-06-23 15:39:44 -07:00
get anchorOffset() {
2016-06-22 18:59:19 -07:00
return this.selection.anchorOffset
}
/**
* Get the current focus offset.
*
* @return {String} focusOffset
*/
2016-06-23 15:39:44 -07:00
get focusOffset() {
2016-06-22 18:59:19 -07:00
return this.selection.focusOffset
}
2016-06-23 15:39:44 -07:00
/**
* Get the current start text node.
*
* @return {Text} text
*/
get startText() {
return this.document.getDescendant(this.selection.startKey)
}
/**
* Get the current end node.
*
* @return {Text} text
*/
get endText() {
return this.document.getDescendant(this.selection.endKey)
}
/**
* Get the current anchor node.
*
* @return {Text} text
*/
get anchorText() {
return this.document.getDescendant(this.selection.anchorKey)
}
/**
* Get the current focus node.
*
* @return {Text} text
*/
get focusText() {
return this.document.getDescendant(this.selection.focusKey)
}
/**
* Get the current start text node's closest block parent.
*
* @return {Block} block
*/
get startBlock() {
return this.document.getClosestBlock(this.selection.startKey)
}
/**
* Get the current end text node's closest block parent.
*
* @return {Block} block
*/
get endBlock() {
return this.document.getClosestBlock(this.selection.endKey)
}
/**
* Get the current anchor text node's closest block parent.
*
* @return {Block} block
*/
get anchorBlock() {
return this.document.getClosestBlock(this.selection.anchorKey)
}
/**
* Get the current focus text node's closest block parent.
*
* @return {Block} block
*/
get focusBlock() {
return this.document.getClosestBlock(this.selection.focusKey)
}
2016-06-20 12:57:31 -07:00
/**
* Get the characters in the current selection.
*
* @return {List} characters
*/
2016-06-23 15:39:44 -07:00
get characters() {
return this.document.getCharactersAtRange(this.selection)
2016-06-20 12:57:31 -07:00
}
/**
* Get the marks of the current selection.
*
* @return {Set} marks
*/
2016-06-23 15:39:44 -07:00
get marks() {
return this.document.getMarksAtRange(this.selection)
2016-06-20 12:57:31 -07:00
}
2016-06-20 17:38:56 -07:00
/**
* Get the block nodes in the current selection.
2016-06-20 17:38:56 -07:00
*
* @return {List} nodes
2016-06-20 17:38:56 -07:00
*/
2016-06-23 15:39:44 -07:00
get blocks() {
2016-06-22 18:42:49 -07:00
return this.document.getBlocksAtRange(this.selection)
}
/**
* Get the fragment of the current selection.
*
* @return {List} nodes
*/
get fragment() {
return this.document.getFragmentAtRange(this.selection)
}
2016-06-22 18:42:49 -07:00
/**
* Get the inline nodes in the current selection.
*
* @return {List} nodes
2016-06-22 18:42:49 -07:00
*/
2016-06-23 15:39:44 -07:00
get inlines() {
2016-06-22 18:42:49 -07:00
return this.document.getInlinesAtRange(this.selection)
2016-06-20 17:38:56 -07:00
}
2016-06-20 12:57:31 -07:00
/**
* Get the text nodes in the current selection.
*
* @return {List} nodes
2016-06-20 12:57:31 -07:00
*/
2016-06-23 15:39:44 -07:00
get texts() {
2016-06-22 18:59:19 -07:00
return this.document.getTextsAtRange(this.selection)
2016-06-20 12:57:31 -07:00
}
/**
* Return a new `Transform` with the current state as a starting point.
*
* @return {Transform} transform
*/
transform() {
2016-06-20 12:57:31 -07:00
const state = this
return new Transform({ state })
}
/**
2016-06-20 12:57:31 -07:00
* Delete at the current selection.
2016-06-17 13:34:29 -07:00
*
* @return {State} state
*/
delete() {
let state = this
let { document, selection } = state
2016-06-17 13:34:29 -07:00
// When collapsed, there's nothing to do.
if (selection.isCollapsed) return state
2016-06-17 13:34:29 -07:00
// Otherwise, delete and update the selection.
document = document.deleteAtRange(selection)
selection = selection.moveToStart()
state = state.merge({ document, selection })
2016-06-17 13:34:29 -07:00
return state
}
/**
* Delete backward `n` characters at the current selection.
*
* @param {Number} n (optional)
* @return {State} state
*/
2016-06-17 00:09:54 -07:00
2016-06-17 13:34:29 -07:00
deleteBackward(n = 1) {
let state = this
let { document, selection } = state
let after = selection
2016-06-17 13:34:29 -07:00
// Determine what the selection should be after deleting.
const { startKey } = selection
2016-06-23 12:34:47 -07:00
const startNode = document.getDescendant(startKey)
2016-06-17 13:34:29 -07:00
2016-06-21 17:08:15 -07:00
if (selection.isExpanded) {
after = selection.moveToStart()
2016-06-20 13:21:24 -07:00
}
2016-06-21 17:08:15 -07:00
else if (selection.isAtStartOf(document)) {
after = selection
2016-06-17 18:52:23 -07:00
}
else if (selection.isAtStartOf(startNode)) {
2016-06-22 18:42:49 -07:00
const parent = document.getParent(startNode)
2016-06-23 15:39:44 -07:00
const previous = document.getPreviousSibling(parent).nodes.first()
after = selection.moveToEndOf(previous)
2016-06-17 13:34:29 -07:00
}
2016-06-17 00:34:27 -07:00
2016-06-20 17:38:56 -07:00
else {
after = selection.moveBackward(n)
2016-06-17 00:09:54 -07:00
}
2016-06-17 13:34:29 -07:00
// Delete backward and then update the selection.
document = document.deleteBackwardAtRange(selection)
selection = after
state = state.merge({ document, selection })
2016-06-17 13:34:29 -07:00
return state
}
/**
* Delete forward `n` characters at the current selection.
*
* @param {Number} n (optional)
* @return {State} state
*/
2016-06-15 12:07:12 -07:00
2016-06-17 13:34:29 -07:00
deleteForward(n = 1) {
let state = this
let { document, selection } = state
2016-06-30 18:42:10 -07:00
let { startKey } = selection
let after = selection
2016-06-17 18:52:23 -07:00
// Determine what the selection should be after deleting.
2016-06-30 18:42:10 -07:00
const block = document.getClosestBlock(startKey)
const inline = document.getClosestInline(startKey)
if (selection.isExpanded) {
after = selection.moveToStart()
2016-06-17 18:52:23 -07:00
}
2016-06-30 18:42:10 -07:00
else if ((block && block.isVoid) || (inline && inline.isVoid)) {
const next = document.getNextText(startKey)
const previous = document.getPreviousText(startKey)
after = next
? selection.moveToStartOf(next)
: selection.moveToEndOf(previous)
}
2016-06-17 18:52:23 -07:00
// Delete forward and then update the selection.
document = document.deleteForwardAtRange(selection)
selection = after
state = state.merge({ document, selection })
2016-06-17 12:00:15 -07:00
return state
}
2016-06-24 17:22:08 -07:00
/**
* Insert a `fragment` at the current selection.
*
* @param {List} fragment
* @return {State} state
*/
insertFragment(fragment) {
let state = this
let { document, selection } = state
2016-06-27 14:08:30 -07:00
let after = selection
2016-06-24 17:22:08 -07:00
2016-06-24 18:01:34 -07:00
// If there's nothing in the fragment, do nothing.
if (!fragment.length) return state
2016-06-27 14:08:30 -07:00
// Lookup some nodes for determining the selection next.
const texts = fragment.getTextNodes()
const lastText = texts.last()
const lastInline = fragment.getClosestInline(lastText)
const startText = document.getDescendant(selection.startKey)
const startBlock = document.getClosestBlock(startText)
const startInline = document.getClosestInline(startText)
const nextText = document.getNextText(startText)
const nextBlock = nextText ? document.getClosestBlock(nextText) : null
const nextNextText = nextText ? document.getNextText(nextText) : null
2016-06-27 15:15:11 -07:00
const docTexts = document.getTextNodes()
2016-06-24 18:01:34 -07:00
// Insert the fragment.
document = document.insertFragmentAtRange(selection, fragment)
// Determine what the selection should be after inserting.
2016-06-27 15:15:11 -07:00
const keys = docTexts.map(text => text.key)
2016-07-06 20:19:19 -07:00
const text = document.getTextNodes().findLast(n => !keys.includes(n.key))
2016-06-27 15:15:11 -07:00
after = text
? selection.moveToStartOf(text).moveForward(lastText.length)
: selection.moveToStart().moveForward(lastText.length)
2016-06-27 14:08:30 -07:00
// Update the document and selection.
selection = after
2016-06-24 18:01:34 -07:00
state = state.merge({ document, selection })
return state
2016-06-24 17:22:08 -07:00
}
2016-06-17 12:00:15 -07:00
/**
2016-06-20 12:57:31 -07:00
* Insert a `text` string at the current selection.
2016-06-17 12:00:15 -07:00
*
* @param {String} text
2016-06-17 13:34:29 -07:00
* @return {State} state
*/
insertText(text) {
2016-06-17 13:34:29 -07:00
let state = this
let { document, selection } = state
2016-06-24 14:07:11 -07:00
let after = selection
// Determine what the selection should be after inserting.
if (selection.isExpanded) {
after = selection.moveToStart()
}
2016-06-17 13:34:29 -07:00
// Insert the text and update the selection.
document = document.insertTextAtRange(selection, text)
2016-06-24 14:07:11 -07:00
selection = after
selection = selection.moveForward(text.length)
state = state.merge({ document, selection })
2016-06-17 13:34:29 -07:00
return state
}
/**
2016-06-20 12:57:31 -07:00
* Add a `mark` to the characters in the current selection.
*
* @param {Mark} mark
* @return {State} state
*/
mark(mark) {
let state = this
let { document, selection } = state
document = document.markAtRange(selection, mark)
state = state.merge({ document })
return state
}
2016-07-07 19:37:34 -07:00
/**
* 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
// Pass in properties, and force `isBackward` to be re-resolved.
selection = selection.merge({
2016-07-12 12:24:13 -07:00
...properties,
2016-07-07 19:37:34 -07:00
isBackward: null
})
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
2016-06-28 18:26:56 -07:00
/**
* Move the selection to the start of the previous block.
*
* @return {State} state
*/
moveToStartOfPreviousBlock() {
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.moveToStartOf(previous)
2016-06-30 14:37:29 -07:00
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the previous block.
*
* @return {State} state
*/
moveToEndOfPreviousBlock() {
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.moveToEndOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the next block.
*
* @return {State} state
*/
moveToStartOfNextBlock() {
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.moveToStartOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the next block.
*
* @return {State} state
*/
moveToEndOfNextBlock() {
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.moveToEndOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the previous text.
*
* @return {State} state
*/
moveToStartOfPreviousText() {
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.moveToStartOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the previous text.
*
* @return {State} state
*/
moveToEndOfPreviousText() {
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.moveToEndOf(previous)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the next text.
*
* @return {State} state
*/
moveToStartOfNextText() {
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.moveToStartOf(next)
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the end of the next text.
*
* @return {State} state
*/
moveToEndOfNextText() {
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.moveToEndOf(next)
selection = selection.normalize(document)
2016-06-28 18:26:56 -07:00
state = state.merge({ selection })
return state
}
2016-06-20 17:38:56 -07:00
/**
2016-06-30 11:13:56 -07:00
* Set `properties` of the block nodes in the current selection.
2016-06-22 18:42:49 -07:00
*
2016-06-30 11:13:56 -07:00
* @param {Object} properties
2016-06-22 18:42:49 -07:00
* @return {State} state
*/
2016-06-30 11:13:56 -07:00
setBlock(properties) {
2016-06-22 18:42:49 -07:00
let state = this
let { document, selection } = state
2016-06-30 11:13:56 -07:00
document = document.setBlockAtRange(selection, properties)
2016-06-22 18:42:49 -07:00
state = state.merge({ document })
return state
}
/**
2016-06-30 11:13:56 -07:00
* Set `properties` of the inline nodes in the current selection.
2016-06-20 17:38:56 -07:00
*
2016-06-30 11:13:56 -07:00
* @param {Object} properties
2016-06-20 17:38:56 -07:00
* @return {State} state
*/
2016-06-30 11:13:56 -07:00
setInline(properties) {
2016-06-20 17:38:56 -07:00
let state = this
let { document, selection } = state
2016-06-30 11:13:56 -07:00
document = document.setInlineAtRange(selection, properties)
2016-06-20 17:38:56 -07:00
state = state.merge({ document })
return state
}
2016-06-20 12:57:31 -07:00
/**
2016-06-23 15:39:44 -07:00
* Split the block node at the current selection.
2016-06-16 16:43:02 -07:00
*
* @return {State} state
*/
2016-06-23 15:39:44 -07:00
splitBlock() {
2016-06-17 00:09:54 -07:00
let state = this
let { document, selection } = state
2016-06-17 00:34:27 -07:00
// Split the document.
2016-06-23 15:39:44 -07:00
document = document.splitBlockAtRange(selection)
2016-06-17 13:34:29 -07:00
// Determine what the selection should be after splitting.
const { startKey } = selection
2016-06-23 12:34:47 -07:00
const startNode = document.getDescendant(startKey)
2016-06-23 15:39:44 -07:00
const nextNode = document.getNextText(startNode)
selection = selection.moveToStartOf(nextNode)
state = state.merge({ document, selection })
return state
}
/**
* Split the inline nodes at the current selection.
*
* @return {State} state
*/
splitInline() {
let state = this
let { document, selection } = state
// Split the document.
document = document.splitInlineAtRange(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.moveToStartOf(nextNode)
}
2016-06-17 16:10:44 -07:00
state = state.merge({ document, selection })
2016-06-17 16:10:44 -07:00
return state
}
2016-06-20 12:57:31 -07:00
/**
* Remove a `mark` to the characters in the current selection.
*
* @param {Mark} mark
* @return {State} state
*/
unmark(mark) {
let state = this
let { document, selection } = state
document = document.unmarkAtRange(selection, mark)
state = state.merge({ document })
return state
}
2016-06-21 18:00:18 -07:00
/**
* Wrap the block nodes in the current selection in new nodes of `type`.
*
* @param {String} type
2016-06-23 23:39:08 -07:00
* @param {Data} data (optional)
2016-06-21 18:00:18 -07:00
* @return {State} state
*/
2016-06-23 23:39:08 -07:00
wrapBlock(type, data) {
2016-06-21 18:00:18 -07:00
let state = this
let { document, selection } = state
2016-06-23 23:39:08 -07:00
document = document.wrapBlockAtRange(selection, type, data)
2016-06-21 18:00:18 -07:00
state = state.merge({ document })
return state
}
2016-06-21 19:02:39 -07:00
/**
2016-06-23 23:39:08 -07:00
* Unwrap the current selection from a block parent of `type`.
2016-06-21 19:02:39 -07:00
*
2016-06-23 23:39:08 -07:00
* @param {String} type (optional)
* @param {Data} data (optional)
2016-06-21 19:02:39 -07:00
* @return {State} state
*/
2016-06-23 23:39:08 -07:00
unwrapBlock(type, data) {
2016-06-22 18:42:49 -07:00
let state = this
let { document, selection } = state
2016-06-23 23:39:08 -07:00
document = document.unwrapBlockAtRange(selection, type, data)
2016-06-22 18:42:49 -07:00
state = state.merge({ document, selection })
return state
}
/**
* Wrap the current selection in new inline nodes of `type`.
*
* @param {String} type
2016-06-23 23:39:08 -07:00
* @param {Data} data (optional)
2016-06-22 18:42:49 -07:00
* @return {State} state
*/
wrapInline(type, data) {
let state = this
let { document, selection } = state
document = document.wrapInlineAtRange(selection, type, data)
2016-06-23 23:39:08 -07:00
// Determine what the selection should be after wrapping.
2016-07-06 20:19:19 -07:00
if (selection.isExpanded) {
const start = document.getNextText(selection.startKey)
const end = document.getPreviousText(selection.endKey)
2016-06-23 23:39:08 -07:00
selection = selection.moveToRangeOf(start, end)
selection = selection.normalize(document)
}
state = state.merge({ document, selection })
2016-06-22 18:42:49 -07:00
return state
}
/**
2016-06-23 23:39:08 -07:00
* Unwrap the current selection from an inline parent of `type`.
2016-06-22 18:42:49 -07:00
*
2016-06-23 23:39:08 -07:00
* @param {String} type (optional)
* @param {Data} data (optional)
2016-06-22 18:42:49 -07:00
* @return {State} state
*/
2016-06-23 23:39:08 -07:00
unwrapInline(type, data) {
2016-06-21 19:02:39 -07:00
let state = this
let { document, selection } = state
2016-06-23 23:39:08 -07:00
document = document.unwrapInlineAtRange(selection, type, data)
2016-06-21 19:02:39 -07:00
state = state.merge({ document, selection })
return state
}
2016-06-15 12:07:12 -07:00
}
/**
* Export.
*/
export default State