mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-16 20:24:01 +02:00
Add a Point
model, and standardize Range/Point logic (#2035)
* add `Node.createRange` for range resolution * fix lint * fix range offset defaults to be `null` * change `isBackward` to be computed from paths * remove debuggers * add point model, update range with deprecations, update hyperscript * got all tests passing * get tests passing, convert changes * fix lint * fix examples * update deprecations * update docs * update slate-react point utils * fix document.normalizeRange * fix lint
This commit is contained in:
@@ -376,7 +376,7 @@ class Content extends React.Component {
|
||||
|
||||
editor.change(change => {
|
||||
if (change.value.isInVoid) {
|
||||
change.collapseToStartOfNextText()
|
||||
change.moveToStartOfNextText()
|
||||
} else {
|
||||
change.splitBlockAtRange(range)
|
||||
}
|
||||
|
@@ -109,20 +109,20 @@ class Text extends React.Component {
|
||||
const { key } = node
|
||||
|
||||
const decs = decorations.filter(d => {
|
||||
const { startKey, endKey, startPath, endPath } = d
|
||||
const { start, end } = d
|
||||
|
||||
// If either of the decoration's keys match, include it.
|
||||
if (startKey === key || endKey === key) return true
|
||||
if (start.key === key || end.key === key) return true
|
||||
|
||||
// Otherwise, if the decoration is in a single node, it's not ours.
|
||||
if (startKey === endKey) return false
|
||||
if (start.key === end.key) return false
|
||||
|
||||
// If the node's path is before the start path, ignore it.
|
||||
const path = document.assertPathByKey(key)
|
||||
if (PathUtils.compare(path, startPath) === -1) return false
|
||||
if (PathUtils.compare(path, start.path) === -1) return false
|
||||
|
||||
// If the node's path is after the end path, ignore it.
|
||||
if (PathUtils.compare(path, endPath) === 1) return false
|
||||
if (PathUtils.compare(path, end.path) === 1) return false
|
||||
|
||||
// Otherwise, include it.
|
||||
return true
|
||||
|
@@ -85,7 +85,7 @@ function AfterPlugin() {
|
||||
// an inline node will be automatically replaced to be at the last offset
|
||||
// of a previous inline node, which screws us up, so we always want to set
|
||||
// it to the end of the node. (2016/11/29)
|
||||
change.focus().collapseToEndOf(node)
|
||||
change.focus().moveToEndOfNode(node)
|
||||
}
|
||||
|
||||
debug('onClick', { event })
|
||||
@@ -125,7 +125,8 @@ function AfterPlugin() {
|
||||
// If user cuts a void block node or a void inline node,
|
||||
// manually removes it since selection is collapsed in this case.
|
||||
const { value } = change
|
||||
const { endBlock, endInline, isCollapsed } = value
|
||||
const { endBlock, endInline, selection } = value
|
||||
const { isCollapsed } = selection
|
||||
const isVoidBlock = endBlock && endBlock.isVoid && isCollapsed
|
||||
const isVoidInline = endInline && endInline.isVoid && isCollapsed
|
||||
|
||||
@@ -219,13 +220,13 @@ function AfterPlugin() {
|
||||
// needs to account for the selection's content being deleted.
|
||||
if (
|
||||
isDraggingInternally &&
|
||||
selection.endKey == target.endKey &&
|
||||
selection.endOffset < target.endOffset
|
||||
selection.end.key == target.end.key &&
|
||||
selection.end.offset < target.end.offset
|
||||
) {
|
||||
target = target.move(
|
||||
selection.startKey == selection.endKey
|
||||
? 0 - selection.endOffset + selection.startOffset
|
||||
: 0 - selection.endOffset
|
||||
selection.start.key == selection.end.key
|
||||
? 0 - selection.end.offset + selection.start.offset
|
||||
: 0 - selection.end.offset
|
||||
)
|
||||
}
|
||||
|
||||
@@ -236,11 +237,11 @@ function AfterPlugin() {
|
||||
change.select(target)
|
||||
|
||||
if (type == 'text' || type == 'html') {
|
||||
const { anchorKey } = target
|
||||
let hasVoidParent = document.hasVoidParent(anchorKey)
|
||||
const { anchor } = target
|
||||
let hasVoidParent = document.hasVoidParent(anchor.key)
|
||||
|
||||
if (hasVoidParent) {
|
||||
let n = document.getNode(anchorKey)
|
||||
let n = document.getNode(anchor.key)
|
||||
|
||||
while (hasVoidParent) {
|
||||
n = document.getNextText(n.key)
|
||||
@@ -248,7 +249,7 @@ function AfterPlugin() {
|
||||
hasVoidParent = document.hasVoidParent(n.key)
|
||||
}
|
||||
|
||||
if (n) change.collapseToStartOf(n)
|
||||
if (n) change.moveToStartOfNode(n)
|
||||
}
|
||||
|
||||
if (text) {
|
||||
@@ -275,7 +276,7 @@ function AfterPlugin() {
|
||||
// has fired in a node: https://github.com/facebook/react/issues/11379.
|
||||
// Until this is fixed in React, we dispatch a mouseup event on that
|
||||
// DOM node, since that will make it go back to normal.
|
||||
const focusNode = document.getNode(target.focusKey)
|
||||
const focusNode = document.getNode(target.focus.key)
|
||||
const el = findDOMNode(focusNode, window)
|
||||
if (!el) return
|
||||
|
||||
@@ -343,12 +344,12 @@ function AfterPlugin() {
|
||||
|
||||
// Determine what the selection should be after changing the text.
|
||||
const delta = textContent.length - text.length
|
||||
const corrected = selection.collapseToEnd().move(delta)
|
||||
const corrected = selection.moveToEnd().moveForward(delta)
|
||||
let entire = selection
|
||||
.moveAnchorTo(point.key, start)
|
||||
.moveFocusTo(point.key, end)
|
||||
|
||||
entire = document.normalizeRange(entire)
|
||||
entire = document.resolveRange(entire)
|
||||
|
||||
// Change the current value to have the leaf's text replaced.
|
||||
change.insertTextAtRange(entire, textContent, leaf.marks).select(corrected)
|
||||
@@ -372,7 +373,7 @@ function AfterPlugin() {
|
||||
// preserve native autocorrect behavior, so they shouldn't be handled here.
|
||||
if (Hotkeys.isSplitBlock(event) && !IS_IOS) {
|
||||
return value.isInVoid
|
||||
? change.collapseToStartOfNextText()
|
||||
? change.moveToStartOfNextText()
|
||||
: change.splitBlock()
|
||||
}
|
||||
|
||||
@@ -413,22 +414,22 @@ function AfterPlugin() {
|
||||
// selection isn't properly collapsed. (2017/10/17)
|
||||
if (Hotkeys.isCollapseLineBackward(event)) {
|
||||
event.preventDefault()
|
||||
return change.collapseLineBackward()
|
||||
return change.moveToStartOfBlock()
|
||||
}
|
||||
|
||||
if (Hotkeys.isCollapseLineForward(event)) {
|
||||
event.preventDefault()
|
||||
return change.collapseLineForward()
|
||||
return change.moveToEndOfBlock()
|
||||
}
|
||||
|
||||
if (Hotkeys.isExtendLineBackward(event)) {
|
||||
event.preventDefault()
|
||||
return change.extendLineBackward()
|
||||
return change.moveFocusToStartOfBlock()
|
||||
}
|
||||
|
||||
if (Hotkeys.isExtendLineForward(event)) {
|
||||
event.preventDefault()
|
||||
return change.extendLineForward()
|
||||
return change.moveFocusToEndOfBlock()
|
||||
}
|
||||
|
||||
// COMPAT: If a void node is selected, or a zero-width text node adjacent to
|
||||
@@ -441,7 +442,7 @@ function AfterPlugin() {
|
||||
|
||||
if (isInVoid || isPreviousInVoid || startText.text == '') {
|
||||
event.preventDefault()
|
||||
return change.collapseCharBackward()
|
||||
return change.moveBackward()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +452,7 @@ function AfterPlugin() {
|
||||
|
||||
if (isInVoid || isNextInVoid || startText.text == '') {
|
||||
event.preventDefault()
|
||||
return change.collapseCharForward()
|
||||
return change.moveForward()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +463,7 @@ function AfterPlugin() {
|
||||
|
||||
if (isInVoid || isPreviousInVoid || startText.text == '') {
|
||||
event.preventDefault()
|
||||
return change.extendCharBackward()
|
||||
return change.moveFocusBackward()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,7 +473,7 @@ function AfterPlugin() {
|
||||
|
||||
if (isInVoid || isNextInVoid || startText.text == '') {
|
||||
event.preventDefault()
|
||||
return change.extendCharForward()
|
||||
return change.moveFocusForward()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,13 +536,13 @@ function AfterPlugin() {
|
||||
let range = findRange(native, value)
|
||||
if (!range) return
|
||||
|
||||
const { anchorKey, anchorOffset, focusKey, focusOffset } = range
|
||||
const anchorText = document.getNode(anchorKey)
|
||||
const focusText = document.getNode(focusKey)
|
||||
const anchorInline = document.getClosestInline(anchorKey)
|
||||
const focusInline = document.getClosestInline(focusKey)
|
||||
const focusBlock = document.getClosestBlock(focusKey)
|
||||
const anchorBlock = document.getClosestBlock(anchorKey)
|
||||
const { anchor, focus } = range
|
||||
const anchorText = document.getNode(anchor.key)
|
||||
const focusText = document.getNode(focus.key)
|
||||
const anchorInline = document.getClosestInline(anchor.key)
|
||||
const focusInline = document.getClosestInline(focus.key)
|
||||
const focusBlock = document.getClosestBlock(focus.key)
|
||||
const anchorBlock = document.getClosestBlock(anchor.key)
|
||||
|
||||
// COMPAT: If the anchor point is at the start of a non-void, and the
|
||||
// focus point is inside a void node with an offset that isn't `0`, set
|
||||
@@ -553,12 +554,12 @@ function AfterPlugin() {
|
||||
if (
|
||||
anchorBlock &&
|
||||
!anchorBlock.isVoid &&
|
||||
anchorOffset == 0 &&
|
||||
anchor.offset == 0 &&
|
||||
focusBlock &&
|
||||
focusBlock.isVoid &&
|
||||
focusOffset != 0
|
||||
focus.offset != 0
|
||||
) {
|
||||
range = range.set('focusOffset', 0)
|
||||
range = range.setFocus(focus.setOffset(0))
|
||||
}
|
||||
|
||||
// COMPAT: If the selection is at the end of a non-void inline node, and
|
||||
@@ -567,24 +568,24 @@ function AfterPlugin() {
|
||||
if (
|
||||
anchorInline &&
|
||||
!anchorInline.isVoid &&
|
||||
anchorOffset == anchorText.text.length
|
||||
anchor.offset == anchorText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(anchorKey)
|
||||
const next = block.getNextText(anchorKey)
|
||||
const block = document.getClosestBlock(anchor.key)
|
||||
const next = block.getNextText(anchor.key)
|
||||
if (next) range = range.moveAnchorTo(next.key, 0)
|
||||
}
|
||||
|
||||
if (
|
||||
focusInline &&
|
||||
!focusInline.isVoid &&
|
||||
focusOffset == focusText.text.length
|
||||
focus.offset == focusText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(focusKey)
|
||||
const next = block.getNextText(focusKey)
|
||||
const block = document.getClosestBlock(focus.key)
|
||||
const next = block.getNextText(focus.key)
|
||||
if (next) range = range.moveFocusTo(next.key, 0)
|
||||
}
|
||||
|
||||
range = document.normalizeRange(range)
|
||||
range = document.resolveRange(range)
|
||||
change.select(range)
|
||||
}
|
||||
|
||||
|
@@ -19,9 +19,9 @@ const { FRAGMENT, HTML, TEXT } = TRANSFER_TYPES
|
||||
function cloneFragment(event, value, fragment = value.fragment) {
|
||||
const window = getWindow(event.target)
|
||||
const native = window.getSelection()
|
||||
const { startKey, endKey } = value
|
||||
const startVoid = value.document.getClosestVoid(startKey)
|
||||
const endVoid = value.document.getClosestVoid(endKey)
|
||||
const { start, end } = value.selection
|
||||
const startVoid = value.document.getClosestVoid(start.key)
|
||||
const endVoid = value.document.getClosestVoid(end.key)
|
||||
|
||||
// If the selection is collapsed, and it isn't inside a void node, abort.
|
||||
if (native.isCollapsed && !startVoid) return
|
||||
|
@@ -1,16 +1,15 @@
|
||||
import findDOMNode from './find-dom-node'
|
||||
|
||||
/**
|
||||
* Find a native DOM selection point from a Slate `key` and `offset`.
|
||||
* Find a native DOM selection point from a Slate `point`.
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {Number} offset
|
||||
* @param {Point} point
|
||||
* @param {Window} win (optional)
|
||||
* @return {Object|Null}
|
||||
*/
|
||||
|
||||
function findDOMPoint(key, offset, win = window) {
|
||||
const el = findDOMNode(key, win)
|
||||
function findDOMPoint(point, win = window) {
|
||||
const el = findDOMNode(point.key, win)
|
||||
let start = 0
|
||||
let n
|
||||
|
||||
@@ -27,8 +26,8 @@ function findDOMPoint(key, offset, win = window) {
|
||||
const { length } = n.textContent
|
||||
const end = start + length
|
||||
|
||||
if (offset <= end) {
|
||||
const o = offset - start
|
||||
if (point.offset <= end) {
|
||||
const o = point.offset - start
|
||||
return { node: n, offset: o >= 0 ? o : 0 }
|
||||
}
|
||||
|
||||
|
@@ -9,21 +9,15 @@ import findDOMPoint from './find-dom-point'
|
||||
*/
|
||||
|
||||
function findDOMRange(range, win = window) {
|
||||
const {
|
||||
anchorKey,
|
||||
anchorOffset,
|
||||
focusKey,
|
||||
focusOffset,
|
||||
isBackward,
|
||||
isCollapsed,
|
||||
} = range
|
||||
const anchor = findDOMPoint(anchorKey, anchorOffset, win)
|
||||
const focus = isCollapsed ? anchor : findDOMPoint(focusKey, focusOffset, win)
|
||||
if (!anchor || !focus) return null
|
||||
const { anchor, focus, isBackward, isCollapsed } = range
|
||||
const domAnchor = findDOMPoint(anchor, win)
|
||||
const domFocus = isCollapsed ? domAnchor : findDOMPoint(focus, win)
|
||||
|
||||
if (!domAnchor || !domFocus) return null
|
||||
|
||||
const r = win.document.createRange()
|
||||
const start = isBackward ? focus : anchor
|
||||
const end = isBackward ? anchor : focus
|
||||
const start = isBackward ? domFocus : domAnchor
|
||||
const end = isBackward ? domAnchor : domFocus
|
||||
r.setStart(start.node, start.offset)
|
||||
r.setEnd(end.node, end.offset)
|
||||
return r
|
||||
|
@@ -21,7 +21,7 @@ const VOID_SELECTOR = '[data-slate-void]'
|
||||
* @param {Element} nativeNode
|
||||
* @param {Number} nativeOffset
|
||||
* @param {Value} value
|
||||
* @return {Object}
|
||||
* @return {Point}
|
||||
*/
|
||||
|
||||
function findPoint(nativeNode, nativeOffset, value) {
|
||||
@@ -78,10 +78,8 @@ function findPoint(nativeNode, nativeOffset, value) {
|
||||
// then afterwards for the correct `element`. (2017/03/03)
|
||||
if (!value.document.hasDescendant(key)) return null
|
||||
|
||||
return {
|
||||
key,
|
||||
offset,
|
||||
}
|
||||
const point = value.document.createPoint({ key, offset })
|
||||
return point
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -48,8 +48,8 @@ function findRange(native, value) {
|
||||
// last word of a span, it sets the endContainer to the containing span.
|
||||
// `selection-is-backward` doesn't handle this case.
|
||||
if (IS_IE || IS_EDGE) {
|
||||
const domAnchor = findDOMPoint(anchor.key, anchor.offset)
|
||||
const domFocus = findDOMPoint(focus.key, focus.offset)
|
||||
const domAnchor = findDOMPoint(anchor)
|
||||
const domFocus = findDOMPoint(focus)
|
||||
|
||||
native = {
|
||||
anchorNode: domAnchor.node,
|
||||
@@ -61,10 +61,8 @@ function findRange(native, value) {
|
||||
|
||||
const { document } = value
|
||||
const range = document.createRange({
|
||||
anchorKey: anchor.key,
|
||||
anchorOffset: anchor.offset,
|
||||
focusKey: focus.key,
|
||||
focusOffset: focus.offset,
|
||||
anchor,
|
||||
focus,
|
||||
isBackward: isCollapsed ? false : isBackward(native),
|
||||
isFocused: true,
|
||||
})
|
||||
|
@@ -80,7 +80,7 @@ function orderChildDecorations(node, decorations) {
|
||||
// Range start.
|
||||
// A rangeStart should be before the child containing its startKey, in order
|
||||
// to consider it active before going down the child.
|
||||
const startKeyOrder = keyOrders[decoration.startKey]
|
||||
const startKeyOrder = keyOrders[decoration.start.key]
|
||||
const containingChildOrder =
|
||||
startKeyOrder === undefined
|
||||
? 0
|
||||
@@ -93,7 +93,7 @@ function orderChildDecorations(node, decorations) {
|
||||
})
|
||||
|
||||
// Range end.
|
||||
const endKeyOrder = (keyOrders[decoration.endKey] || globalOrder) + 0.5
|
||||
const endKeyOrder = (keyOrders[decoration.end.key] || globalOrder) + 0.5
|
||||
|
||||
endPoints.push({
|
||||
isRangeEnd: true,
|
||||
|
@@ -7,10 +7,14 @@ function decorateNode(block) {
|
||||
const text = block.getFirstText()
|
||||
return [
|
||||
{
|
||||
anchorKey: text.key,
|
||||
anchorOffset: 1,
|
||||
focusKey: text.key,
|
||||
focusOffset: 2,
|
||||
anchor: {
|
||||
key: text.key,
|
||||
offset: 1,
|
||||
},
|
||||
focus: {
|
||||
key: text.key,
|
||||
offset: 2,
|
||||
},
|
||||
marks: [{ type: 'bold' }],
|
||||
},
|
||||
]
|
||||
|
Reference in New Issue
Block a user