mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-14 11:14:04 +02:00
add insert fragment, remove normalize ranges
This commit is contained in:
@@ -18,6 +18,8 @@ class Content extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
onBeforeInput: React.PropTypes.func,
|
onBeforeInput: React.PropTypes.func,
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func,
|
||||||
|
onCopy: React.PropTypes.func,
|
||||||
|
onCut: React.PropTypes.func,
|
||||||
onKeyDown: React.PropTypes.func,
|
onKeyDown: React.PropTypes.func,
|
||||||
onPaste: React.PropTypes.func,
|
onPaste: React.PropTypes.func,
|
||||||
onSelect: React.PropTypes.func,
|
onSelect: React.PropTypes.func,
|
||||||
@@ -167,6 +169,8 @@ class Content extends React.Component {
|
|||||||
style={style}
|
style={style}
|
||||||
onSelect={e => this.onSelect(e)}
|
onSelect={e => this.onSelect(e)}
|
||||||
onPaste={e => this.onPaste(e)}
|
onPaste={e => this.onPaste(e)}
|
||||||
|
onCopy={e => this.onEvent('onCopy', e)}
|
||||||
|
onCut={e => this.onEvent('onCut', e)}
|
||||||
onKeyDown={e => this.onEvent('onKeyDown', e)}
|
onKeyDown={e => this.onEvent('onKeyDown', e)}
|
||||||
onBeforeInput={e => this.onEvent('onBeforeInput', e)}
|
onBeforeInput={e => this.onEvent('onBeforeInput', e)}
|
||||||
>
|
>
|
||||||
|
@@ -106,6 +106,8 @@ class Editor extends React.Component {
|
|||||||
onChange={state => this.onChange(state)}
|
onChange={state => this.onChange(state)}
|
||||||
renderMark={mark => this.renderMark(mark)}
|
renderMark={mark => this.renderMark(mark)}
|
||||||
renderNode={node => this.renderNode(node)}
|
renderNode={node => this.renderNode(node)}
|
||||||
|
onCopy={(e) => this.onEvent('onCopy', e)}
|
||||||
|
onCut={(e) => this.onEvent('onCut', e)}
|
||||||
onPaste={(e, paste) => this.onEvent('onPaste', e, paste)}
|
onPaste={(e, paste) => this.onEvent('onPaste', e, paste)}
|
||||||
onBeforeInput={e => this.onEvent('onBeforeInput', e)}
|
onBeforeInput={e => this.onEvent('onBeforeInput', e)}
|
||||||
onKeyDown={e => this.onEvent('onKeyDown', e)}
|
onKeyDown={e => this.onEvent('onKeyDown', e)}
|
||||||
|
@@ -2,9 +2,11 @@
|
|||||||
import Block from './block'
|
import Block from './block'
|
||||||
import Character from './character'
|
import Character from './character'
|
||||||
import Data from './data'
|
import Data from './data'
|
||||||
|
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 { List, Map, Set } from 'immutable'
|
import { List, Map, Set } from 'immutable'
|
||||||
|
|
||||||
@@ -43,145 +45,6 @@ const Node = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete everything in a `range`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
deleteAtRange(range) {
|
|
||||||
let node = this
|
|
||||||
range = range.normalize(node)
|
|
||||||
|
|
||||||
// If the range is collapsed, there's nothing to do.
|
|
||||||
if (range.isCollapsed) return node
|
|
||||||
|
|
||||||
// Make sure the children exist.
|
|
||||||
const { startKey, startOffset, endKey, endOffset } = range
|
|
||||||
node.assertHasDescendant(startKey)
|
|
||||||
node.assertHasDescendant(endKey)
|
|
||||||
|
|
||||||
// If the start and end nodes are the same, just remove characters.
|
|
||||||
if (startKey == endKey) {
|
|
||||||
let text = node.getDescendant(startKey)
|
|
||||||
text = text.removeCharacters(startOffset, endOffset)
|
|
||||||
node = node.updateDescendant(text)
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the blocks and determine the edge boundaries.
|
|
||||||
const start = range.moveToStart()
|
|
||||||
const end = range.moveToEnd()
|
|
||||||
node = node.splitBlockAtRange(start, Infinity)
|
|
||||||
node = node.splitBlockAtRange(end, Infinity)
|
|
||||||
|
|
||||||
const startText = node.getDescendant(startKey)
|
|
||||||
const startEdgeText = node.getNextText(startKey)
|
|
||||||
|
|
||||||
const endText = node.getNextText(endKey)
|
|
||||||
const endEdgeText = node.getDescendant(endKey)
|
|
||||||
|
|
||||||
// Remove the new blocks inside the edges.
|
|
||||||
const startEdgeBlock = node.getFurthestBlock(startEdgeText)
|
|
||||||
const endEdgeBlock = node.getFurthestBlock(endEdgeText)
|
|
||||||
|
|
||||||
const nodes = node.nodes
|
|
||||||
.takeUntil(n => n == startEdgeBlock)
|
|
||||||
.concat(node.nodes.skipUntil(n => n == endEdgeBlock).rest())
|
|
||||||
|
|
||||||
node = node.merge({ nodes })
|
|
||||||
|
|
||||||
// Take the end edge's split text and move it to the start edge.
|
|
||||||
let startBlock = node.getFurthestBlock(startText)
|
|
||||||
let endChild = node.getFurthestInline(endText) || endText
|
|
||||||
|
|
||||||
const startNodes = startBlock.nodes.push(endChild)
|
|
||||||
startBlock = startBlock.merge({ nodes: startNodes })
|
|
||||||
node = node.updateDescendant(startBlock)
|
|
||||||
|
|
||||||
// While the end child is an only child, remove the block it's in.
|
|
||||||
let endParent = node.getClosestBlock(endChild)
|
|
||||||
|
|
||||||
while (endParent && endParent.nodes.size == 1) {
|
|
||||||
endChild = endParent
|
|
||||||
endParent = node.getClosestBlock(endParent)
|
|
||||||
}
|
|
||||||
|
|
||||||
node = node.removeDescendant(endChild)
|
|
||||||
|
|
||||||
// Normalize the adjacent text nodes.
|
|
||||||
return node.normalize()
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete backward `n` characters at a `range`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {Number} n (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
deleteBackwardAtRange(range, n = 1) {
|
|
||||||
let node = this
|
|
||||||
range = range.normalize(node)
|
|
||||||
|
|
||||||
// When collapsed at the start of the node, there's nothing to do.
|
|
||||||
if (range.isCollapsed && range.isAtStartOf(node)) return node
|
|
||||||
|
|
||||||
// When the range is still expanded, just do a regular delete.
|
|
||||||
if (range.isExpanded) return node.deleteAtRange(range)
|
|
||||||
|
|
||||||
// When at start of a text node, merge forwards into the next text node.
|
|
||||||
const { startKey } = range
|
|
||||||
const startNode = node.getDescendant(startKey)
|
|
||||||
|
|
||||||
if (range.isAtStartOf(startNode)) {
|
|
||||||
const previous = node.getPreviousText(startNode)
|
|
||||||
range = range.extendToEndOf(previous)
|
|
||||||
range = range.normalize(node)
|
|
||||||
return node.deleteAtRange(range)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, remove `n` characters behind of the cursor.
|
|
||||||
range = range.extendBackward(n)
|
|
||||||
return node.deleteAtRange(range)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete forward `n` characters at a `range`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {Number} n (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
deleteForwardAtRange(range, n = 1) {
|
|
||||||
let node = this
|
|
||||||
range = range.normalize(node)
|
|
||||||
|
|
||||||
// When collapsed at the end of the node, there's nothing to do.
|
|
||||||
if (range.isCollapsed && range.isAtEndOf(node)) return node
|
|
||||||
|
|
||||||
// When the range is still expanded, just do a regular delete.
|
|
||||||
if (range.isExpanded) return node.deleteAtRange(range)
|
|
||||||
|
|
||||||
// When at end of a text node, merge forwards into the next text node.
|
|
||||||
const { startKey } = range
|
|
||||||
const startNode = node.getDescendant(startKey)
|
|
||||||
|
|
||||||
if (range.isAtEndOf(startNode)) {
|
|
||||||
const next = node.getNextText(startNode)
|
|
||||||
range = range.extendToStartOf(next)
|
|
||||||
range = range.normalize(node)
|
|
||||||
return node.deleteAtRange(range)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, remove `n` characters ahead of the cursor.
|
|
||||||
range = range.extendForward(n)
|
|
||||||
return node.deleteAtRange(range)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively find all ancestor nodes by `iterator`.
|
* Recursively find all ancestor nodes by `iterator`.
|
||||||
*
|
*
|
||||||
@@ -297,6 +160,44 @@ const Node = {
|
|||||||
return this.nodes.find(node => node.key == key)
|
return this.nodes.find(node => node.key == key)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a fragment of the node at a `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {List} nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
getFragmentAtRange(range) {
|
||||||
|
let node = this
|
||||||
|
let nodes = Block.createList()
|
||||||
|
|
||||||
|
// If the range is collapsed, there's nothing to do.
|
||||||
|
if (range.isCollapsed) return Document.create({ nodes })
|
||||||
|
|
||||||
|
// Make sure the children exist.
|
||||||
|
const { startKey, endKey } = range
|
||||||
|
node.assertHasDescendant(startKey)
|
||||||
|
node.assertHasDescendant(endKey)
|
||||||
|
|
||||||
|
// Split at the start and end.
|
||||||
|
const start = range.moveToStart()
|
||||||
|
const end = range.moveToEnd()
|
||||||
|
node = node.splitBlockAtRange(start, Infinity)
|
||||||
|
node = node.splitBlockAtRange(end, Infinity)
|
||||||
|
|
||||||
|
// Get the start and end nodes.
|
||||||
|
const startNode = node.getHighestChild(startKey)
|
||||||
|
const endNode = node.getHighestChild(endKey)
|
||||||
|
|
||||||
|
nodes = node.nodes
|
||||||
|
.skipUntil(node => node == startNode)
|
||||||
|
.rest()
|
||||||
|
.takeUntil(node => node == endNode)
|
||||||
|
.push(endNode)
|
||||||
|
|
||||||
|
return Document.create({ nodes })
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the highest child ancestor of a node by `key`.
|
* Get the highest child ancestor of a node by `key`.
|
||||||
*
|
*
|
||||||
@@ -486,6 +387,28 @@ const Node = {
|
|||||||
: offset + child.getOffset(key)
|
: offset + child.getOffset(key)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the offset from a `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {Number} offset
|
||||||
|
*/
|
||||||
|
|
||||||
|
getOffsetAtRange(range) {
|
||||||
|
if (range.isExpanded) {
|
||||||
|
throw new Error('The range must be collapsed to calculcate its offset.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startKey, startOffset } = range
|
||||||
|
const startNode = this.getDescendant(startKey)
|
||||||
|
|
||||||
|
if (!startNode) {
|
||||||
|
throw new Error(`Could not find a node by startKey "${startKey}".`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getOffset(startKey) + startOffset
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the parent of a child node by `key`.
|
* Get the parent of a child node by `key`.
|
||||||
*
|
*
|
||||||
@@ -621,74 +544,19 @@ const Node = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert text `string` at a `range`.
|
* Check if the inline nodes are split at a `range`.
|
||||||
*
|
*
|
||||||
* @param {Selection} range
|
* @param {Selection} range
|
||||||
* @param {String} string
|
* @return {Boolean} isSplit
|
||||||
* @return {Node} node
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
insertTextAtRange(range, string) {
|
isInlineSplitAtRange(range) {
|
||||||
let node = this
|
range = range.normalize(this)
|
||||||
range = range.normalize(node)
|
if (range.isExpanded) throw new Error()
|
||||||
|
|
||||||
// When still expanded, remove the current range first.
|
const { startKey } = range
|
||||||
if (range.isExpanded) {
|
const start = this.getFurthestInline(startKey) || this.getDescendant(startKey)
|
||||||
node = node.deleteAtRange(range)
|
return range.isAtStartOf(start) || range.isAtEndOf(start)
|
||||||
range = range.moveToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert text at the range's offset.
|
|
||||||
const { startKey, startOffset } = range
|
|
||||||
let text = node.getDescendant(startKey)
|
|
||||||
text = text.insertText(string, startOffset)
|
|
||||||
node = node.updateDescendant(text)
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new `mark` to the characters at `range`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {Mark or String} mark
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
markAtRange(range, mark) {
|
|
||||||
let node = this
|
|
||||||
range = range.normalize(node)
|
|
||||||
|
|
||||||
// Allow for just passing a type for convenience.
|
|
||||||
if (typeof mark == 'string') {
|
|
||||||
mark = new Mark({ type: mark })
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the range is collapsed, do nothing.
|
|
||||||
if (range.isCollapsed) return node
|
|
||||||
|
|
||||||
// Otherwise, find each of the text nodes within the range.
|
|
||||||
const { startKey, startOffset, endKey, endOffset } = range
|
|
||||||
let texts = node.getTextsAtRange(range)
|
|
||||||
|
|
||||||
// Apply the mark to each of the text nodes's matching characters.
|
|
||||||
texts = texts.map((text) => {
|
|
||||||
let characters = text.characters.map((char, i) => {
|
|
||||||
if (!isInRange(i, text, range)) return char
|
|
||||||
let { marks } = char
|
|
||||||
marks = marks.add(mark)
|
|
||||||
return char.merge({ marks })
|
|
||||||
})
|
|
||||||
|
|
||||||
return text.merge({ characters })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update each of the text nodes.
|
|
||||||
texts.forEach((text) => {
|
|
||||||
node = node.updateDescendant(text)
|
|
||||||
})
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -767,383 +635,6 @@ const Node = {
|
|||||||
return this.merge({ nodes })
|
return this.merge({ nodes })
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the block nodes in a range to `type`, with optional `data`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {String} type (optional)
|
|
||||||
* @param {Data} data (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
setBlockAtRange(range, type, data) {
|
|
||||||
let node = this
|
|
||||||
range = range.normalize(node)
|
|
||||||
|
|
||||||
// Allow for passing data only.
|
|
||||||
if (typeof type == 'object') {
|
|
||||||
data = type
|
|
||||||
type = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update each of the blocks.
|
|
||||||
const blocks = node.getBlocksAtRange(range)
|
|
||||||
blocks.forEach((block) => {
|
|
||||||
const obj = {}
|
|
||||||
if (type) obj.type = type
|
|
||||||
if (data) obj.data = Data.create(data)
|
|
||||||
block = block.merge(obj)
|
|
||||||
node = node.updateDescendant(block)
|
|
||||||
})
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the inline nodes in a range to `type`, with optional `data`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {String} type (optional)
|
|
||||||
* @param {Data} data (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
setInlineAtRange(range, type, data) {
|
|
||||||
let node = this
|
|
||||||
range = range.normalize(node)
|
|
||||||
|
|
||||||
// Allow for passing data only.
|
|
||||||
if (typeof type == 'object') {
|
|
||||||
data = type
|
|
||||||
type = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update each of the inlines.
|
|
||||||
const inlines = node.getInlinesAtRange(range)
|
|
||||||
inlines.forEach((inline) => {
|
|
||||||
const obj = {}
|
|
||||||
if (type) obj.type = type
|
|
||||||
if (data) obj.data = Data.create(data)
|
|
||||||
inline = inline.merge(obj)
|
|
||||||
node = node.updateDescendant(inline)
|
|
||||||
})
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split the block nodes at a `range`, to optional `depth`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {Number} depth (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
splitBlockAtRange(range, depth = 1) {
|
|
||||||
let node = this
|
|
||||||
range = range.normalize(node)
|
|
||||||
|
|
||||||
// If the range is expanded, remove it first.
|
|
||||||
if (range.isExpanded) {
|
|
||||||
node = node.deleteAtRange(range)
|
|
||||||
range = range.moveToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the inline nodes at the range.
|
|
||||||
node = node.splitInlineAtRange(range)
|
|
||||||
|
|
||||||
// Find the highest inline elements that were split.
|
|
||||||
const { startKey } = range
|
|
||||||
const firstText = node.getDescendant(startKey)
|
|
||||||
const secondText = node.getNextText(startKey)
|
|
||||||
const firstChild = node.getFurthestInline(firstText) || firstText
|
|
||||||
const secondChild = node.getFurthestInline(secondText) || secondText
|
|
||||||
let parent = node.getClosestBlock(firstChild)
|
|
||||||
let firstChildren = parent.nodes.takeUntil(n => n == firstChild).push(firstChild)
|
|
||||||
let secondChildren = parent.nodes.skipUntil(n => n == secondChild)
|
|
||||||
let d = 0
|
|
||||||
|
|
||||||
// While the parent is a block, split the block nodes.
|
|
||||||
while (parent && d < depth) {
|
|
||||||
const firstChild = parent.merge({ nodes: firstChildren })
|
|
||||||
const secondChild = Block.create({
|
|
||||||
nodes: secondChildren,
|
|
||||||
type: parent.type,
|
|
||||||
data: parent.data
|
|
||||||
})
|
|
||||||
|
|
||||||
firstChildren = Block.createList([firstChild])
|
|
||||||
secondChildren = Block.createList([secondChild])
|
|
||||||
|
|
||||||
// Add the new children.
|
|
||||||
const grandparent = node.getParent(parent)
|
|
||||||
const nodes = grandparent.nodes
|
|
||||||
.takeUntil(n => n.key == firstChild.key)
|
|
||||||
.push(firstChild)
|
|
||||||
.push(secondChild)
|
|
||||||
.concat(grandparent.nodes.skipUntil(n => n.key == firstChild.key).rest())
|
|
||||||
|
|
||||||
// Update the grandparent.
|
|
||||||
node = grandparent == node
|
|
||||||
? node.merge({ nodes })
|
|
||||||
: node.updateDescendant(grandparent.merge({ nodes }))
|
|
||||||
|
|
||||||
d++
|
|
||||||
parent = node.getClosestBlock(firstChild)
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split the inline nodes at a `range`, to optional `depth`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {Number} depth (optiona)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
splitInlineAtRange(range, depth = Infinity) {
|
|
||||||
range = range.normalize(this)
|
|
||||||
let node = this
|
|
||||||
|
|
||||||
// If the range is expanded, remove it first.
|
|
||||||
if (range.isExpanded) {
|
|
||||||
node = node.deleteAtRange(range)
|
|
||||||
range = range.moveToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
// First split the text nodes.
|
|
||||||
node = node.splitTextAtRange(range)
|
|
||||||
|
|
||||||
// Find the children that were split.
|
|
||||||
const { startKey } = range
|
|
||||||
let firstChild = node.getDescendant(startKey)
|
|
||||||
let secondChild = node.getNextText(firstChild)
|
|
||||||
let parent = node.getClosestInline(firstChild)
|
|
||||||
let d = 0
|
|
||||||
|
|
||||||
// While the parent is an inline parent, split the inline nodes.
|
|
||||||
while (parent && d < depth) {
|
|
||||||
firstChild = parent.merge({ nodes: Inline.createList([firstChild]) })
|
|
||||||
secondChild = Inline.create({
|
|
||||||
nodes: [secondChild],
|
|
||||||
type: parent.type,
|
|
||||||
data: parent.data
|
|
||||||
})
|
|
||||||
|
|
||||||
// Split the children.
|
|
||||||
const grandparent = node.getParent(parent)
|
|
||||||
const nodes = grandparent.nodes
|
|
||||||
.takeUntil(n => n.key == firstChild.key)
|
|
||||||
.push(firstChild)
|
|
||||||
.push(secondChild)
|
|
||||||
.concat(grandparent.nodes.skipUntil(n => n.key == firstChild.key).rest())
|
|
||||||
|
|
||||||
// Update the grandparent.
|
|
||||||
node = grandparent == node
|
|
||||||
? node.merge({ nodes })
|
|
||||||
: node.updateDescendant(grandparent.merge({ nodes }))
|
|
||||||
|
|
||||||
d++
|
|
||||||
parent = node.getClosestInline(firstChild)
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split the text nodes at a `range`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
splitTextAtRange(range) {
|
|
||||||
range = range.normalize(this)
|
|
||||||
let node = this
|
|
||||||
|
|
||||||
// If the range is expanded, remove it first.
|
|
||||||
if (range.isExpanded) {
|
|
||||||
node = node.deleteAtRange(range)
|
|
||||||
range = range.moveToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the text node's characters.
|
|
||||||
const { startKey, startOffset } = range
|
|
||||||
const text = node.getDescendant(startKey)
|
|
||||||
const { characters } = text
|
|
||||||
const firstChars = characters.take(startOffset)
|
|
||||||
const secondChars = characters.skip(startOffset)
|
|
||||||
let firstChild = text.merge({ characters: firstChars })
|
|
||||||
let secondChild = Text.create({ characters: secondChars })
|
|
||||||
|
|
||||||
// Split the text nodes.
|
|
||||||
let parent = node.getParent(text)
|
|
||||||
const nodes = parent.nodes
|
|
||||||
.takeUntil(c => c.key == firstChild.key)
|
|
||||||
.push(firstChild)
|
|
||||||
.push(secondChild)
|
|
||||||
.concat(parent.nodes.skipUntil(n => n.key == firstChild.key).rest())
|
|
||||||
|
|
||||||
// Update the nodes.
|
|
||||||
parent = parent.merge({ nodes })
|
|
||||||
node = node.updateDescendant(parent)
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an existing `mark` to the characters at `range`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {Mark or String} mark
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
unmarkAtRange(range, mark) {
|
|
||||||
let node = this
|
|
||||||
range = range.normalize(node)
|
|
||||||
|
|
||||||
// Allow for just passing a type for convenience.
|
|
||||||
if (typeof mark == 'string') {
|
|
||||||
mark = new Mark({ type: mark })
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the range is collapsed, do nothing.
|
|
||||||
if (range.isCollapsed) return node
|
|
||||||
|
|
||||||
// Otherwise, find each of the text nodes within the range.
|
|
||||||
let texts = node.getTextsAtRange(range)
|
|
||||||
|
|
||||||
// Apply the mark to each of the text nodes's matching characters.
|
|
||||||
texts = texts.map((text) => {
|
|
||||||
let characters = text.characters.map((char, i) => {
|
|
||||||
if (!isInRange(i, text, range)) return char
|
|
||||||
let { marks } = char
|
|
||||||
marks = marks.remove(mark)
|
|
||||||
return char.merge({ marks })
|
|
||||||
})
|
|
||||||
|
|
||||||
return text.merge({ characters })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update each of the text nodes.
|
|
||||||
texts.forEach((text) => {
|
|
||||||
node = node.updateDescendant(text)
|
|
||||||
})
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unwrap all of the block nodes in a `range` from a block node of `type.`
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {String} type (optional)
|
|
||||||
* @param {Data or Object} data (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
unwrapBlockAtRange(range, type, data) {
|
|
||||||
range = range.normalize(this)
|
|
||||||
let node = this
|
|
||||||
|
|
||||||
// Allow for only data.
|
|
||||||
if (typeof type == 'object') {
|
|
||||||
data = type
|
|
||||||
type = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that data is immutable.
|
|
||||||
if (data) data = Data.create(data)
|
|
||||||
|
|
||||||
// Find the closest wrapping blocks of each text node.
|
|
||||||
const texts = node.getBlocksAtRange(range)
|
|
||||||
const wrappers = texts.reduce((wrappers, text) => {
|
|
||||||
const match = node.getClosest(text, (parent) => {
|
|
||||||
if (parent.kind != 'block') return false
|
|
||||||
if (type && parent.type != type) return false
|
|
||||||
if (data && !parent.data.isSuperset(data)) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (match) wrappers = wrappers.add(match)
|
|
||||||
return wrappers
|
|
||||||
}, new Set())
|
|
||||||
|
|
||||||
// Replace each of the wrappers with their child nodes.
|
|
||||||
wrappers.forEach((wrapper) => {
|
|
||||||
const parent = node.getParent(wrapper)
|
|
||||||
|
|
||||||
// Replace the wrapper in the parent's nodes with the block.
|
|
||||||
const nodes = parent.nodes.takeUntil(n => n == wrapper)
|
|
||||||
.concat(wrapper.nodes)
|
|
||||||
.concat(parent.nodes.skipUntil(n => n == wrapper).rest())
|
|
||||||
|
|
||||||
// Update the parent.
|
|
||||||
node = parent == node
|
|
||||||
? node.merge({ nodes })
|
|
||||||
: node.updateDescendant(parent.merge({ nodes }))
|
|
||||||
})
|
|
||||||
|
|
||||||
return node.normalize()
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unwrap the inline nodes in a `range` from an parent inline with `type`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {String} type (optional)
|
|
||||||
* @param {Data} data (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
unwrapInlineAtRange(range, type, data) {
|
|
||||||
range = range.normalize(this)
|
|
||||||
let node = this
|
|
||||||
let blocks = node.getInlinesAtRange(range)
|
|
||||||
|
|
||||||
// Allow for no type.
|
|
||||||
if (typeof type == 'object') {
|
|
||||||
data = type
|
|
||||||
type = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that data is immutable.
|
|
||||||
if (data) data = Data.create(data)
|
|
||||||
|
|
||||||
// Find the closest matching inline wrappers of each text node.
|
|
||||||
const texts = this.getTextNodes()
|
|
||||||
const wrappers = texts.reduce((wrappers, text) => {
|
|
||||||
const match = node.getClosest(text, (parent) => {
|
|
||||||
if (parent.kind != 'inline') return false
|
|
||||||
if (type && parent.type != type) return false
|
|
||||||
if (data && !parent.data.isSuperset(data)) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (match) wrappers = wrappers.add(match)
|
|
||||||
return wrappers
|
|
||||||
}, new Set())
|
|
||||||
|
|
||||||
// Replace each of the wrappers with their child nodes.
|
|
||||||
wrappers.forEach((wrapper) => {
|
|
||||||
const parent = node.getParent(wrapper)
|
|
||||||
|
|
||||||
// Replace the wrapper in the parent's nodes with the block.
|
|
||||||
const nodes = parent.nodes.takeUntil(n => n == wrapper)
|
|
||||||
.concat(wrapper.nodes)
|
|
||||||
.concat(parent.nodes.skipUntil(n => n == wrapper).rest())
|
|
||||||
|
|
||||||
// Update the parent.
|
|
||||||
node = parent == node
|
|
||||||
? node.merge({ nodes })
|
|
||||||
: node.updateDescendant(parent.merge({ nodes }))
|
|
||||||
})
|
|
||||||
|
|
||||||
return node.normalize()
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new value for a child node by `key`.
|
* Set a new value for a child node by `key`.
|
||||||
*
|
*
|
||||||
@@ -1162,137 +653,6 @@ const Node = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return this.merge({ nodes })
|
return this.merge({ nodes })
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap all of the blocks in a `range` in a new block node of `type`.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {String} type
|
|
||||||
* @param {Data} data (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
wrapBlockAtRange(range, type, data) {
|
|
||||||
range = range.normalize(this)
|
|
||||||
data = Data.create(data)
|
|
||||||
let node = this
|
|
||||||
|
|
||||||
// Get the block nodes, sorted by depth.
|
|
||||||
const blocks = node.getBlocksAtRange(range)
|
|
||||||
const sorted = blocks.sort((a, b) => {
|
|
||||||
const da = node.getDepth(a)
|
|
||||||
const db = node.getDepth(b)
|
|
||||||
if (da == db) return 0
|
|
||||||
if (da > db) return -1
|
|
||||||
if (da < db) return 1
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get the lowest common siblings, relative to the highest block.
|
|
||||||
const highest = sorted.first()
|
|
||||||
const depth = node.getDepth(highest)
|
|
||||||
const siblings = blocks.reduce((siblings, block) => {
|
|
||||||
const sibling = node.getDepth(block) == depth
|
|
||||||
? block
|
|
||||||
: node.getClosest(block, (p) => node.getDepth(p) == depth)
|
|
||||||
siblings = siblings.push(sibling)
|
|
||||||
return siblings
|
|
||||||
}, Block.createList())
|
|
||||||
|
|
||||||
// Wrap the siblings in a new block.
|
|
||||||
const wrapper = Block.create({
|
|
||||||
nodes: siblings,
|
|
||||||
type,
|
|
||||||
data
|
|
||||||
})
|
|
||||||
|
|
||||||
// Replace the siblings with the wrapper.
|
|
||||||
const first = siblings.first()
|
|
||||||
const last = siblings.last()
|
|
||||||
const parent = node.getParent(highest)
|
|
||||||
const nodes = parent.nodes
|
|
||||||
.takeUntil(node => node == first)
|
|
||||||
.push(wrapper)
|
|
||||||
.concat(parent.nodes.skipUntil(node => node == last).rest())
|
|
||||||
|
|
||||||
// Update the parent.
|
|
||||||
node = parent == node
|
|
||||||
? node.merge({ nodes })
|
|
||||||
: node.updateDescendant(parent.merge({ nodes }))
|
|
||||||
|
|
||||||
return node
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap the text and inline nodes in a `range` with a new inline node.
|
|
||||||
*
|
|
||||||
* @param {Selection} range
|
|
||||||
* @param {String} type
|
|
||||||
* @param {Data} data (optional)
|
|
||||||
* @return {Node} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
wrapInlineAtRange(range, type, data) {
|
|
||||||
range = range.normalize(this)
|
|
||||||
data = Data.create(data)
|
|
||||||
let node = this
|
|
||||||
|
|
||||||
// If collapsed or unset, there's nothing to wrap.
|
|
||||||
if (range.isCollapsed || range.isUnset) return node
|
|
||||||
|
|
||||||
// Split at the start of the range.
|
|
||||||
const start = range.moveToStart()
|
|
||||||
node = node.splitInlineAtRange(start)
|
|
||||||
|
|
||||||
// Determine the new end of the range, and split there.
|
|
||||||
const { startKey, startOffset, endKey, endOffset } = range
|
|
||||||
const firstNode = node.getDescendant(startKey)
|
|
||||||
const nextNode = node.getNextText(startKey)
|
|
||||||
const end = startKey != endKey
|
|
||||||
? range.moveToEnd()
|
|
||||||
: Selection.create({
|
|
||||||
anchorKey: nextNode.key,
|
|
||||||
anchorOffset: endOffset - startOffset,
|
|
||||||
focusKey: nextNode.key,
|
|
||||||
focusOffset: endOffset - startOffset
|
|
||||||
})
|
|
||||||
|
|
||||||
node = node.splitInlineAtRange(end)
|
|
||||||
|
|
||||||
// Calculate the new range to wrap around.
|
|
||||||
const endNode = node.getDescendant(end.anchorKey)
|
|
||||||
range = Selection.create({
|
|
||||||
anchorKey: nextNode.key,
|
|
||||||
anchorOffset: 0,
|
|
||||||
focusKey: endNode.key,
|
|
||||||
focusOffset: endNode.length
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get the furthest inline nodes in the range.
|
|
||||||
const texts = node.getTextsAtRange(range)
|
|
||||||
const children = texts.map(text => node.getFurthestInline(text) || text)
|
|
||||||
|
|
||||||
// Iterate each of the child nodes, wrapping them.
|
|
||||||
children.forEach((child) => {
|
|
||||||
const obj = {}
|
|
||||||
obj.nodes = [child]
|
|
||||||
obj.type = type
|
|
||||||
if (data) obj.data = data
|
|
||||||
const wrapper = Inline.create(obj)
|
|
||||||
|
|
||||||
// Replace the child in it's parent with the wrapper.
|
|
||||||
const parent = node.getParent(child)
|
|
||||||
const nodes = parent.nodes.takeUntil(n => n == child)
|
|
||||||
.push(wrapper)
|
|
||||||
.concat(parent.nodes.skipUntil(n => n == child).rest())
|
|
||||||
|
|
||||||
// Update the parent.
|
|
||||||
node = parent == node
|
|
||||||
? node.merge({ nodes })
|
|
||||||
: node.updateDescendant(parent.merge({ nodes }))
|
|
||||||
})
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1333,6 +693,14 @@ function isInRange(index, text, range) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms.
|
||||||
|
*/
|
||||||
|
|
||||||
|
for (var key in Transforms) {
|
||||||
|
Node[key] = Transforms[key]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export.
|
* Export.
|
||||||
*/
|
*/
|
||||||
|
@@ -21,7 +21,8 @@ const DEFAULTS = {
|
|||||||
document: new Document(),
|
document: new Document(),
|
||||||
selection: new Selection(),
|
selection: new Selection(),
|
||||||
history: new History(),
|
history: new History(),
|
||||||
isNative: true
|
isNative: true,
|
||||||
|
copiedFragment: null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -267,17 +268,27 @@ class State extends Record(DEFAULTS) {
|
|||||||
/**
|
/**
|
||||||
* Get the block nodes in the current selection.
|
* Get the block nodes in the current selection.
|
||||||
*
|
*
|
||||||
* @return {OrderedMap} nodes
|
* @return {List} nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get blocks() {
|
get blocks() {
|
||||||
return this.document.getBlocksAtRange(this.selection)
|
return this.document.getBlocksAtRange(this.selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fragment of the current selection.
|
||||||
|
*
|
||||||
|
* @return {List} nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
get fragment() {
|
||||||
|
return this.document.getFragmentAtRange(this.selection)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the inline nodes in the current selection.
|
* Get the inline nodes in the current selection.
|
||||||
*
|
*
|
||||||
* @return {OrderedMap} nodes
|
* @return {List} nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get inlines() {
|
get inlines() {
|
||||||
@@ -287,7 +298,7 @@ class State extends Record(DEFAULTS) {
|
|||||||
/**
|
/**
|
||||||
* Get the text nodes in the current selection.
|
* Get the text nodes in the current selection.
|
||||||
*
|
*
|
||||||
* @return {OrderedMap} nodes
|
* @return {List} nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get texts() {
|
get texts() {
|
||||||
|
@@ -44,6 +44,19 @@ export default {
|
|||||||
.apply({ isNative: true })
|
.apply({ isNative: true })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The core `onCopy` handler.
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Editor} editor
|
||||||
|
* @return {State or Null}
|
||||||
|
*/
|
||||||
|
|
||||||
|
onCopy(e, state, editor) {
|
||||||
|
editor.fragment = state.fragment
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The core `onKeyDown` handler.
|
* The core `onKeyDown` handler.
|
||||||
*
|
*
|
||||||
@@ -138,16 +151,20 @@ export default {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onPaste(e, paste, state, editor) {
|
onPaste(e, paste, state, editor) {
|
||||||
|
const { fragment } = editor
|
||||||
|
|
||||||
// Don't handle files in core.
|
// Don't handle files in core.
|
||||||
if (paste.type == 'files') return
|
if (paste.type == 'files') return
|
||||||
|
|
||||||
// If the paste type is html...
|
// If pasting html and the text matches the current fragment, use that.
|
||||||
if (paste.type == 'html') {
|
if (paste.type == 'html' && paste.text == fragment.text) {
|
||||||
// First, check for a match in the clipboard.
|
return state
|
||||||
// Otherwise, check if we have a paste deserializer.
|
.transform()
|
||||||
|
.insertFragment(fragment)
|
||||||
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, just insert the plain text splitting at characters.
|
// Otherwise, just insert the plain text splitting at new lines.
|
||||||
let transform = state.transform()
|
let transform = state.transform()
|
||||||
|
|
||||||
paste.text
|
paste.text
|
||||||
|
Reference in New Issue
Block a user