1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-07-31 12:30:11 +02:00

big refactor and cleanup of transforms

This commit is contained in:
Ian Storm Taylor
2016-06-17 13:34:29 -07:00
parent 63449e9b04
commit 94fbe87232
3 changed files with 367 additions and 151 deletions

View File

@@ -253,7 +253,7 @@ class Selection extends SelectionRecord {
/** /**
* Move the selection forward `n` characters. * Move the selection forward `n` characters.
* *
* @param {Number} n * @param {Number} n (optional)
* @return {Selection} selection * @return {Selection} selection
*/ */
@@ -271,7 +271,7 @@ class Selection extends SelectionRecord {
/** /**
* Move the selection backward `n` characters. * Move the selection backward `n` characters.
* *
* @param {Number} n * @param {Number} n (optional)
* @return {Selection} selection * @return {Selection} selection
*/ */
@@ -286,6 +286,118 @@ class Selection extends SelectionRecord {
}) })
} }
/**
* Extend the focus point forward `n` characters.
*
* @param {Number} n (optional)
* @return {Selection} selection
*/
extendForward(n = 1) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({
focusOffset: this.focusOffset + n,
isBackward: false
})
}
/**
* Extend the focus point backward `n` characters.
*
* @param {Number} n (optional)
* @return {Selection} selection
*/
extendBackward(n = 1) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({
focusOffset: this.focusOffset - n,
isBackward: true
})
}
/**
* Extend the focus forward to the start of a `node`.
*
* @param {Node} node
* @return {Selection} selection
*/
extendForwardToStartOf(node) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({
focusKey: node.key,
focusOffset: 0,
isBackward: false
})
}
/**
* Extend the focus backward to the start of a `node`.
*
* @param {Node} node
* @return {Selection} selection
*/
extendBackwardToStartOf(node) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({
focusKey: node.key,
focusOffset: 0,
isBackward: true
})
}
/**
* Extend the focus forward to the end of a `node`.
*
* @param {Node} node
* @return {Selection} selection
*/
extendForwardToEndOf(node) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({
focusKey: node.key,
focusOffset: node.length,
isBackward: false
})
}
/**
* Extend the focus backward to the end of a `node`.
*
* @param {Node} node
* @return {Selection} selection
*/
extendBackwardToEndOf(node) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed before extending.')
}
return this.merge({
focusKey: node.key,
focusOffset: node.length,
isBackward: true
})
}
} }
/** /**

View File

@@ -46,7 +46,9 @@ const SELECTION_LIKE_METHODS = [
'moveToEndOf', 'moveToEndOf',
'moveToRangeOf', 'moveToRangeOf',
'moveForward', 'moveForward',
'moveBackward' 'moveBackward',
'extendForward',
'extendBackward'
] ]
/** /**
@@ -196,51 +198,6 @@ class State extends StateRecord {
return this.selection.isAtEndOf(node) return this.selection.isAtEndOf(node)
} }
/**
* Backspace.
*
* @return {State} state
*/
backspace() {
let state = this
// When not collapsed, remove the entire selection.
if (!state.isCollapsed) {
state = state.removeRange()
state = state.moveToStart()
return state
}
// When already at the start of the content, there's nothing to do.
if (state.isAtStartOf(state)) return state
// When at start of a node, merge backwards into the previous node.
const { startNode } = state
if (state.isAtStartOf(startNode)) {
const { selection, startOffset } = state
const parent = state.getParentNode(startNode)
const previous = state.getPreviousNode(parent).nodes.first()
const range = selection.merge({
anchorKey: previous.key,
anchorOffset: previous.length,
focusKey: startNode.key,
focusOffset: 0,
isBackward: false
})
state = state.removeRange(range)
return state
}
// Otherwise, remove one character behind of the cursor.
const { endOffset } = state
const startOffset = endOffset - 1
state = state.removeCharacters(startNode.key, startOffset, endOffset)
state = state.moveBackward()
return state
}
/** /**
* Delete a single character. * Delete a single character.
* *
@@ -250,73 +207,260 @@ class State extends StateRecord {
delete() { delete() {
let state = this let state = this
// When not collapsed, remove the entire selection range. // When collapsed, there's nothing to do.
if (!state.isCollapsed) { if (state.isCollapsed) return state
state = state.removeRange()
state = state.moveToStart()
return state
}
// When already at the end of the content, there's nothing to do. // Otherwise, delete and update the selection.
if (state.isAtEndOf(state)) return state state = state.deleteAtRange(state.selection)
state = state.moveToStart()
return state
}
// When at end of a node, merge forwards into the next node. /**
const { startNode } = state * Delete everything in a `range`.
if (state.isAtEndOf(startNode)) { *
const { selection, startOffset } = state * @param {Selection} range
const parent = state.getParentNode(startNode) * @return {State} state
const next = state.getNextNode(parent).nodes.first() */
const range = selection.merge({
anchorKey: startNode.key, deleteAtRange(range) {
anchorOffset: startNode.length, let state = this
focusKey: next.key,
focusOffset: 0, // If the range is collapsed, there's nothing to do.
isBackward: false if (range.isCollapsed) return state
const { startKey, startOffset, endKey, endOffset } = range
let startNode = state.getNode(startKey)
// If the start and end nodes are the same, remove the matching characters.
if (startKey == endKey) {
let { characters } = startNode
characters = characters.filterNot((char, i) => {
return startOffset <= i && i < endOffset
}) })
state = state.removeRange(range) startNode = startNode.merge({ characters })
state = state.updateNode(startNode)
return state return state
} }
// Otherwise, remove one character ahead of the cursor. // Otherwise, remove the text from the first and last nodes...
const { startOffset } = state const startRange = Selection.create({
const endOffset = startOffset + 1 anchorKey: startKey,
state = state.removeCharacters(startNode.key, startOffset, endOffset) anchorOffset: startOffset,
focusKey: startKey,
focusOffset: startNode.length
})
const endRange = Selection.create({
anchorKey: endKey,
anchorOffset: 0,
focusKey: endKey,
focusOffset: endOffset
})
state = state.deleteAtRange(startRange)
state = state.deleteAtRange(endRange)
// Then remove any nodes in between the top-most start and end nodes...
let startParent = state.getParentNode(startKey)
let endParent = state.getParentNode(endKey)
const startGrandestParent = state.nodes.find((node) => {
return node == startParent || node.hasNode(startParent)
})
const endGrandestParent = state.nodes.find((node) => {
return node == endParent || node.hasNode(endParent)
})
const nodes = state.nodes
.takeUntil(node => node == startGrandestParent)
.set(startGrandestParent.key, startGrandestParent)
.concat(state.nodes.skipUntil(node => node == endGrandestParent))
state = state.merge({ nodes })
// Then bring the end text node into the start node.
let endText = state.getNode(endKey)
startParent = startParent.pushNode(endText)
endParent = endParent.removeNode(endText)
state = state.updateNode(startParent)
state = state.updateNode(endParent)
return state
}
/**
* Delete backward `n` characters at the current selection.
*
* @param {Number} n (optional)
* @return {State} state
*/
deleteBackward(n = 1) {
let state = this
let selection = state.selection
// Determine what the selection should be after deleting.
const startNode = state.startNode
if (state.isCollapsed && state.isAtStartOf(startNode)) {
const parent = state.getParentNode(startNode)
const previous = state.getPreviousNode(parent).nodes.first()
selection = selection.moveToEndOf(previous)
}
else if (state.isCollapsed && !state.isAtEndOf(state)) {
selection = selection.moveBackward(n)
}
// Delete backward and then update the selection.
state = state.deleteBackwardAtRange(state.selection)
state = state.merge({ selection })
return state
}
/**
* Delete backward `n` characters at a `range`.
*
* @param {Selection} range
* @param {Number} n (optional)
* @return {State} state
*/
deleteBackwardAtRange(range, n = 1) {
let state = this
// When collapsed at the end of the document, there's nothing to do.
if (range.isCollapsed && range.isAtEndOf(state)) return state
// When the range is still expanded, just do a regular delete.
if (range.isExpanded) return state.deleteAtRange(range)
// When at start of a text node, merge forwards into the next text node.
const { startKey } = range
const startNode = state.getNode(startKey)
if (range.isAtStartOf(startNode)) {
const parent = state.getParentNode(startNode)
const previous = state.getPreviousNode(parent).nodes.first()
range = range.extendBackwardToEndOf(previous)
state = state.deleteAtRange(range)
return state
}
// Otherwise, remove `n` characters behind of the cursor.
range = range.extendBackward(n)
state = state.deleteAtRange(range)
return state
}
/**
* Delete forward `n` characters at the current selection.
*
* @param {Number} n (optional)
* @return {State} state
*/
deleteForward(n = 1) {
let state = this
state = state.deleteForwardAtRange(state.selection)
return state
}
/**
* Delete forward `n` characters at a `range`.
*
* @param {Selection} range
* @param {Number} n (optional)
* @return {State} state
*/
deleteForwardAtRange(range, n = 1) {
let state = this
// When collapsed at the end of the document, there's nothing to do.
if (range.isCollapsed && range.isAtEndOf(state)) return state
// When the range is still expanded, just do a regular delete.
if (range.isExpanded) return state.deleteAtRange(range)
// When at end of a text node, merge forwards into the next text node.
const { startKey } = range
const startNode = state.getNode(startKey)
if (range.isAtEndOf(startNode)) {
const parent = state.getParentNode(startNode)
const next = state.getNextNode(parent).nodes.first()
range = range.extendForwardToStartOf(next)
state = state.deleteAtRange(range)
return state
}
// Otherwise, remove `n` characters ahead of the cursor.
range = range.extendForward(n)
state = state.deleteAtRange(range)
return state return state
} }
/** /**
* Insert a `text` string at the current cursor position. * Insert a `text` string at the current cursor position.
* *
* @param {String} text * @param {String or Node or OrderedMap} data
* @return {State} state * @return {State} state
*/ */
insert(text) { insert(data) {
let state = this
state = state.insertAtRange(state.selection, data)
// When the data is a string of characters...
if (typeof data == 'string') {
state = state.moveForward(data.length)
}
return state
}
/**
* Insert `data` at a `range`.
*
* @param {Selection} range
* @param {String or Node or OrderedMap} data
* @return {State} state
*/
insertAtRange(range, data) {
let state = this let state = this
// When still expanded, remove the current range first. // When still expanded, remove the current range first.
if (state.isExpanded) { if (range.isExpanded) {
state = state.delete() state = state.deleteAtRange(range)
range = range.moveToStart()
} }
// Insert text at the current cursor. // When the data is a string of characters...
const ranges = [{ text }] if (typeof data == 'string') {
let { startNode, startOffset } = state
let { characters } = startNode
let newCharacters = convertRangesToCharacters(ranges)
const { size } = newCharacters
// Splice in the new characters. // Insert text at the current cursor.
characters = characters.slice(0, startOffset) const ranges = [{ text: data }]
.concat(newCharacters) let { startNode, startOffset } = state
.concat(characters.slice(startOffset + size - 1, Infinity)) let { characters } = startNode
let newCharacters = convertRangesToCharacters(ranges)
const { size } = newCharacters
// Splice in the new characters.
characters = characters.slice(0, startOffset)
.concat(newCharacters)
.concat(characters.slice(startOffset + size - 1, Infinity))
// Update the existing text node.
startNode = startNode.merge({ characters })
state = state.updateNode(startNode)
return state
}
// Update the existing text node and the selection.
startNode = startNode.merge({ characters })
state = state.updateNode(startNode)
state = state.moveForward(size)
return state return state
} }
@@ -360,40 +504,45 @@ class State extends StateRecord {
split() { split() {
let state = this let state = this
state = state.splitRange() state = state.splitAtRange(state.selection)
const parent = state.getParentNode(state.startKey) const parent = state.getParentNode(state.startNode)
const next = state.getNextNode(parent) const next = state.getNextNode(parent)
const text = next.nodes.first() const text = next.nodes.first()
state = state.moveToStartOf(text) state = state.moveToStartOf(text)
// const next = state.getNextTextNode(state.startNode)
// state = state.moveToStartOf(next)
return state return state
} }
/** /**
* Split the nodes at a `selection`. * Split the nodes at a `range`.
* *
* @param {Selection} selection (optional) * @param {Selection} range
* @return {State} state * @return {State} state
*/ */
splitRange(selection = this.selection) { splitAtRange(range) {
let state = this let state = this
// If there's an existing selection, remove it first. // If the range is expanded, remove it first.
if (!selection.isCollapsed) { if (range.isExpanded) {
state = state.removeRange(selection) state = state.deleteAtRange(range)
selection = selection.moveToStart() range = range.moveToStart()
} }
const { startKey, startOffset } = range
const startNode = state.getNode(startKey)
// Split the text node's characters. // Split the text node's characters.
const { startNode, startOffset } = state const { characters, length } = startNode
const parent = state.getParentNode(startNode)
const { characters , length } = startNode
const firstCharacters = characters.take(startOffset) const firstCharacters = characters.take(startOffset)
const secondCharacters = characters.takeLast(length - startOffset) const secondCharacters = characters.takeLast(length - startOffset)
// Create a new first node with only the first set of characters. // Create a new first node with only the first set of characters.
const parent = state.getParentNode(startNode)
const firstText = startNode.set('characters', firstCharacters) const firstText = startNode.set('characters', firstCharacters)
const firstNode = parent.updateNode(firstText) const firstNode = parent.updateNode(firstText)
@@ -416,6 +565,7 @@ class State extends StateRecord {
.set(secondNode.key, secondNode) .set(secondNode.key, secondNode)
.concat(afters) .concat(afters)
// If the state is the grandparent, just merge, otherwise deep merge.
if (grandparent == state) { if (grandparent == state) {
state = state.merge({ nodes }) state = state.merge({ nodes })
} else { } else {
@@ -426,52 +576,6 @@ class State extends StateRecord {
return state return state
} }
/**
* Merge the nodes between `selection`.
*
* @param {Selection} selection (optional)
* @return {State} state
*/
removeRange(selection = this.selection) {
let state = this
// If the selection is collapsed, there's nothing to do.
if (selection.isCollapsed) return state
// If the start and end nodes are the same, just remove the matching text.
const { startKey, startOffset, endKey, endOffset } = selection
if (startKey == endKey) {
return state.removeCharacters(startKey, startOffset, endOffset)
}
// Otherwise, remove the text from the first and last nodes...
let startText = state.getNode(startKey)
state = state.removeCharacters(startKey, startOffset, startText.length)
state = state.removeCharacters(endKey, 0, endOffset)
// Then remove any nodes in between the top-most start and end nodes...
let startNode = state.getParentNode(startKey)
let endNode = state.getParentNode(endKey)
const startParent = state.nodes.find(node => node == startNode || node.hasNode(startNode))
const endParent = state.nodes.find(node => node == endNode || node.hasNode(endNode))
const nodes = state.nodes
.takeUntil(node => node == startParent)
.set(startParent.key, startParent)
.concat(state.nodes.skipUntil(node => node == endParent))
state = state.merge({ nodes })
// Then bring the end text node into the start node.
let endText = state.getNode(endKey)
startNode = startNode.pushNode(endText)
endNode = endNode.removeNode(endText)
state = state.updateNode(startNode)
state = state.updateNode(endNode)
return state
}
} }
/** /**

View File

@@ -32,7 +32,7 @@ const CORE_PLUGIN = {
e.preventDefault() e.preventDefault()
return isWord(e) return isWord(e)
? state.backspaceWord() ? state.backspaceWord()
: state.backspace() : state.deleteBackward()
} }
case 'delete': { case 'delete': {
@@ -41,7 +41,7 @@ const CORE_PLUGIN = {
e.preventDefault() e.preventDefault()
return isWord(e) return isWord(e)
? state.deleteWord() ? state.deleteWord()
: state.delete() : state.deleteForward()
} }
case 'y': { case 'y': {