mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-17 04:34:00 +02:00
Rename Range
to Leaf
, and Selection
to Range
(#1231)
* rename Range to Leaf * rename Selection to Range * add findDOMRange, findNode, findRange helpers * refactor to remove findDropPoint util * revert findDOMNode to throwing errors * export new helpers, fix linter * update docs * update examples
This commit is contained in:
@@ -39,7 +39,7 @@ A `Placeholder` component is just a convenience for rendering placeholders on to
|
||||
|
||||
#### Text
|
||||
|
||||
A `Text` component is rendered for each [`Text`](../models#text) model in the document tree. This component handles grouping the characters of the text node into ranges that have the same set of [`Marks`](../models#mark), and then delegates rendering each range to...
|
||||
A `Text` component is rendered for each [`Text`](../models#text) model in the document tree. This component handles grouping the characters of the text node into leaves that have the same set of [`Marks`](../models#mark), and then delegates rendering each leaf to...
|
||||
|
||||
|
||||
#### Void
|
||||
|
@@ -6,15 +6,15 @@ import SlateTypes from 'slate-prop-types'
|
||||
import Types from 'prop-types'
|
||||
import getWindow from 'get-window'
|
||||
import keycode from 'keycode'
|
||||
import { Selection } from 'slate'
|
||||
import logger from 'slate-dev-logger'
|
||||
|
||||
import TRANSFER_TYPES from '../constants/transfer-types'
|
||||
import Node from './node'
|
||||
import extendSelection from '../utils/extend-selection'
|
||||
import findClosestNode from '../utils/find-closest-node'
|
||||
import findDropPoint from '../utils/find-drop-point'
|
||||
import findNativePoint from '../utils/find-native-point'
|
||||
import findDOMNode from '../utils/find-dom-node'
|
||||
import findDOMRange from '../utils/find-dom-range'
|
||||
import findPoint from '../utils/find-point'
|
||||
import findRange from '../utils/find-range'
|
||||
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
|
||||
import getTransferData from '../utils/get-transfer-data'
|
||||
import scrollToSelection from '../utils/scroll-to-selection'
|
||||
@@ -143,16 +143,20 @@ class Content extends React.Component {
|
||||
if (selection.isUnset) return
|
||||
|
||||
// Otherwise, figure out which DOM nodes should be selected...
|
||||
const { anchorKey, anchorOffset, focusKey, focusOffset, isCollapsed } = selection
|
||||
const anchor = findNativePoint(anchorKey, anchorOffset)
|
||||
const focus = isCollapsed ? anchor : findNativePoint(focusKey, focusOffset)
|
||||
const current = native.getRangeAt(0)
|
||||
const range = findDOMRange(selection)
|
||||
|
||||
// If they are already selected, do nothing.
|
||||
if (!range) {
|
||||
logger.error('Unable to find a native DOM range from the current selection.', { selection })
|
||||
return
|
||||
}
|
||||
|
||||
// If the new range matches the current selection, do nothing.
|
||||
if (
|
||||
anchor.node == native.anchorNode &&
|
||||
anchor.offset == native.anchorOffset &&
|
||||
focus.node == native.focusNode &&
|
||||
focus.offset == native.focusOffset
|
||||
range.startContainer == current.startContainer &&
|
||||
range.startOffset == current.startOffset &&
|
||||
range.endContainer == current.endContainer &&
|
||||
range.endOffset == current.endOffset
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -160,11 +164,7 @@ class Content extends React.Component {
|
||||
// Otherwise, set the `isSelecting` flag and update the selection.
|
||||
this.tmp.isSelecting = true
|
||||
native.removeAllRanges()
|
||||
const range = window.document.createRange()
|
||||
range.setStart(anchor.node, anchor.offset)
|
||||
native.addRange(range)
|
||||
if (!isCollapsed) extendSelection(native, focus.node, focus.offset)
|
||||
|
||||
scrollToSelection(native)
|
||||
|
||||
// Then unset the `isSelecting` flag after a delay.
|
||||
@@ -432,19 +432,48 @@ class Content extends React.Component {
|
||||
|
||||
const { state } = this.props
|
||||
const { nativeEvent } = event
|
||||
const { dataTransfer } = nativeEvent
|
||||
const { dataTransfer, x, y } = nativeEvent
|
||||
const data = getTransferData(dataTransfer)
|
||||
const point = findDropPoint(event, state)
|
||||
if (!point) return
|
||||
|
||||
// Resolve a range from the caret position where the drop occured.
|
||||
const window = getWindow(event.target)
|
||||
let range
|
||||
|
||||
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
|
||||
if (window.document.caretRangeFromPoint) {
|
||||
range = window.document.caretRangeFromPoint(x, y)
|
||||
} else {
|
||||
const position = window.document.caretPositionFromPoint(x, y)
|
||||
range = window.document.createRange()
|
||||
range.setStart(position.offsetNode, position.offset)
|
||||
range.setEnd(position.offsetNode, position.offset)
|
||||
}
|
||||
|
||||
// Resolve a Slate range from the DOM range.
|
||||
let selection = findRange(range, state)
|
||||
if (!selection) return
|
||||
|
||||
const { document } = state
|
||||
const node = document.getNode(selection.anchorKey)
|
||||
const parent = document.getParent(node.key)
|
||||
const el = findDOMNode(parent)
|
||||
|
||||
// If the drop target is inside a void node, move it into either the next or
|
||||
// previous node, depending on which side the `x` and `y` coordinates are
|
||||
// closest to.
|
||||
if (parent.isVoid) {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const isPrevious = parent.kind == 'inline'
|
||||
? x - rect.left < rect.left + rect.width - x
|
||||
: y - rect.top < rect.top + rect.height - y
|
||||
|
||||
selection = isPrevious
|
||||
? selection.moveToEndOf(document.getPreviousText(node.key))
|
||||
: selection.moveToStartOf(document.getNextText(node.key))
|
||||
}
|
||||
|
||||
// Add drop-specific information to the data.
|
||||
data.target = Selection.create({
|
||||
anchorKey: point.key,
|
||||
anchorOffset: point.offset,
|
||||
focusKey: point.key,
|
||||
focusOffset: point.offset,
|
||||
isFocused: true
|
||||
})
|
||||
data.target = selection
|
||||
|
||||
// COMPAT: Edge throws "Permission denied" errors when
|
||||
// accessing `dropEffect` or `effectAllowed` (2017/7/12)
|
||||
@@ -484,33 +513,33 @@ class Content extends React.Component {
|
||||
const point = findPoint(anchorNode, anchorOffset, state)
|
||||
if (!point) return
|
||||
|
||||
// Get the text node and range in question.
|
||||
// Get the text node and leaf in question.
|
||||
const { document, selection } = state
|
||||
const node = document.getDescendant(point.key)
|
||||
const ranges = node.getRanges()
|
||||
const leaves = node.getLeaves()
|
||||
let start = 0
|
||||
let end = 0
|
||||
|
||||
const range = ranges.find((r) => {
|
||||
const leaf = leaves.find((r) => {
|
||||
end += r.text.length
|
||||
if (end >= point.offset) return true
|
||||
start = end
|
||||
})
|
||||
|
||||
// Get the text information.
|
||||
const { text } = range
|
||||
const { text } = leaf
|
||||
let { textContent } = anchorNode
|
||||
const block = document.getClosestBlock(node.key)
|
||||
const lastText = block.getLastText()
|
||||
const lastRange = ranges.last()
|
||||
const lastLeaf = leaves.last()
|
||||
const lastChar = textContent.charAt(textContent.length - 1)
|
||||
const isLastText = node == lastText
|
||||
const isLastRange = range == lastRange
|
||||
const isLastLeaf = leaf == lastLeaf
|
||||
|
||||
// COMPAT: If this is the last range, and the DOM text ends in a new line,
|
||||
// COMPAT: If this is the last leaf, and the DOM text ends in a new line,
|
||||
// we will have added another new line in <Leaf>'s render method to account
|
||||
// for browsers collapsing a single trailing new lines, so remove it.
|
||||
if (isLastText && isLastRange && lastChar == '\n') {
|
||||
if (isLastText && isLastLeaf && lastChar == '\n') {
|
||||
textContent = textContent.slice(0, -1)
|
||||
}
|
||||
|
||||
@@ -522,12 +551,12 @@ class Content extends React.Component {
|
||||
const corrected = selection.collapseToEnd().move(delta)
|
||||
const entire = selection.moveAnchorTo(point.key, start).moveFocusTo(point.key, end)
|
||||
|
||||
// Change the current state to have the range's text replaced.
|
||||
// Change the current state to have the leaf's text replaced.
|
||||
editor.change((change) => {
|
||||
change
|
||||
.select(entire)
|
||||
.delete()
|
||||
.insertText(textContent, range.marks)
|
||||
.insertText(textContent, leaf.marks)
|
||||
.select(corrected)
|
||||
})
|
||||
}
|
||||
@@ -687,42 +716,26 @@ class Content extends React.Component {
|
||||
|
||||
// Otherwise, determine the Slate selection from the native one.
|
||||
else {
|
||||
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
|
||||
const anchor = findPoint(anchorNode, anchorOffset, state)
|
||||
const focus = findPoint(focusNode, focusOffset, state)
|
||||
if (!anchor || !focus) return
|
||||
let range = findRange(native, state)
|
||||
if (!range) return
|
||||
|
||||
// There are situations where a select event will fire with a new native
|
||||
// selection that resolves to the same internal position. In those cases
|
||||
// we don't need to trigger any changes, since our internal model is
|
||||
// already up to date, but we do want to update the native selection again
|
||||
// to make sure it is in sync.
|
||||
if (
|
||||
anchor.key == selection.anchorKey &&
|
||||
anchor.offset == selection.anchorOffset &&
|
||||
focus.key == selection.focusKey &&
|
||||
focus.offset == selection.focusOffset &&
|
||||
selection.isFocused
|
||||
) {
|
||||
if (range.equals(selection)) {
|
||||
this.updateSelection()
|
||||
return
|
||||
}
|
||||
|
||||
const properties = {
|
||||
anchorKey: anchor.key,
|
||||
anchorOffset: anchor.offset,
|
||||
focusKey: focus.key,
|
||||
focusOffset: focus.offset,
|
||||
isFocused: true,
|
||||
isBackward: null
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
// 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
|
||||
@@ -734,12 +747,12 @@ class Content extends React.Component {
|
||||
if (
|
||||
anchorBlock &&
|
||||
!anchorBlock.isVoid &&
|
||||
anchor.offset == 0 &&
|
||||
anchorOffset == 0 &&
|
||||
focusBlock &&
|
||||
focusBlock.isVoid &&
|
||||
focus.offset != 0
|
||||
focusOffset != 0
|
||||
) {
|
||||
properties.focusOffset = 0
|
||||
range = range.set('focusOffset', 0)
|
||||
}
|
||||
|
||||
// COMPAT: If the selection is at the end of a non-void inline node, and
|
||||
@@ -748,32 +761,25 @@ class Content extends React.Component {
|
||||
if (
|
||||
anchorInline &&
|
||||
!anchorInline.isVoid &&
|
||||
anchor.offset == anchorText.text.length
|
||||
anchorOffset == anchorText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(anchor.key)
|
||||
const next = block.getNextText(anchor.key)
|
||||
if (next) {
|
||||
properties.anchorKey = next.key
|
||||
properties.anchorOffset = 0
|
||||
}
|
||||
const block = document.getClosestBlock(anchorKey)
|
||||
const next = block.getNextText(anchorKey)
|
||||
if (next) range = range.moveAnchorTo(next.key, 0)
|
||||
}
|
||||
|
||||
if (
|
||||
focusInline &&
|
||||
!focusInline.isVoid &&
|
||||
focus.offset == focusText.text.length
|
||||
focusOffset == focusText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(focus.key)
|
||||
const next = block.getNextText(focus.key)
|
||||
if (next) {
|
||||
properties.focusKey = next.key
|
||||
properties.focusOffset = 0
|
||||
}
|
||||
const block = document.getClosestBlock(focusKey)
|
||||
const next = block.getNextText(focusKey)
|
||||
if (next) range = range.moveFocusTo(next.key, 0)
|
||||
}
|
||||
|
||||
data.selection = selection
|
||||
.merge(properties)
|
||||
.normalize(document)
|
||||
range = range.normalize(document)
|
||||
data.selection = range
|
||||
}
|
||||
|
||||
debug('onSelect', { event, data })
|
||||
|
@@ -13,7 +13,7 @@ import { IS_FIREFOX } from '../constants/environment'
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:leaf')
|
||||
const debug = Debug('slate:leaves')
|
||||
|
||||
/**
|
||||
* Leaf.
|
||||
@@ -33,11 +33,11 @@ class Leaf extends React.Component {
|
||||
block: SlateTypes.block.isRequired,
|
||||
editor: Types.object.isRequired,
|
||||
index: Types.number.isRequired,
|
||||
leaves: SlateTypes.leaves.isRequired,
|
||||
marks: SlateTypes.marks.isRequired,
|
||||
node: SlateTypes.node.isRequired,
|
||||
offset: Types.number.isRequired,
|
||||
parent: SlateTypes.node.isRequired,
|
||||
ranges: SlateTypes.ranges.isRequired,
|
||||
schema: SlateTypes.schema.isRequired,
|
||||
state: SlateTypes.state.isRequired,
|
||||
text: Types.string.isRequired,
|
||||
@@ -139,7 +139,7 @@ class Leaf extends React.Component {
|
||||
*/
|
||||
|
||||
renderText(props) {
|
||||
const { block, node, parent, text, index, ranges } = props
|
||||
const { block, node, parent, text, index, leaves } = props
|
||||
|
||||
// COMPAT: If the text is empty and it's the only child, we need to render a
|
||||
// <br/> to get the block to have the proper height.
|
||||
@@ -160,8 +160,8 @@ class Leaf extends React.Component {
|
||||
const lastText = block.getLastText()
|
||||
const lastChar = text.charAt(text.length - 1)
|
||||
const isLastText = node == lastText
|
||||
const isLastRange = index == ranges.size - 1
|
||||
if (isLastText && isLastRange && lastChar == '\n') return `${text}\n`
|
||||
const isLastLeaf = index == leaves.size - 1
|
||||
if (isLastText && isLastLeaf && lastChar == '\n') return `${text}\n`
|
||||
|
||||
// Otherwise, just return the text.
|
||||
return text
|
||||
|
@@ -119,35 +119,35 @@ class Text extends React.Component {
|
||||
return startsBefore && endsAfter
|
||||
})
|
||||
|
||||
const ranges = node.getRanges(decs)
|
||||
const leaves = node.getLeaves(decs)
|
||||
let offset = 0
|
||||
|
||||
const leaves = ranges.map((range, i) => {
|
||||
const leaf = this.renderLeaf(ranges, range, i, offset)
|
||||
offset += range.text.length
|
||||
return leaf
|
||||
const children = leaves.map((leaf, i) => {
|
||||
const child = this.renderLeaf(leaves, leaf, i, offset)
|
||||
offset += leaf.text.length
|
||||
return child
|
||||
})
|
||||
|
||||
return (
|
||||
<span data-key={key} style={style}>
|
||||
{leaves}
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single leaf node given a `range` and `offset`.
|
||||
* Render a single leaf given a `leaf` and `offset`.
|
||||
*
|
||||
* @param {List<Range>} ranges
|
||||
* @param {Range} range
|
||||
* @param {List<Leaf>} leaves
|
||||
* @param {Leaf} leaf
|
||||
* @param {Number} index
|
||||
* @param {Number} offset
|
||||
* @return {Element} leaf
|
||||
*/
|
||||
|
||||
renderLeaf = (ranges, range, index, offset) => {
|
||||
renderLeaf = (leaves, leaf, index, offset) => {
|
||||
const { block, node, parent, schema, state, editor } = this.props
|
||||
const { text, marks } = range
|
||||
const { text, marks } = leaf
|
||||
|
||||
return (
|
||||
<Leaf
|
||||
@@ -159,7 +159,7 @@ class Text extends React.Component {
|
||||
node={node}
|
||||
offset={offset}
|
||||
parent={parent}
|
||||
ranges={ranges}
|
||||
leaves={leaves}
|
||||
schema={schema}
|
||||
state={state}
|
||||
text={text}
|
||||
|
@@ -1,16 +1,10 @@
|
||||
|
||||
/**
|
||||
* Components.
|
||||
*/
|
||||
|
||||
import Editor from './components/editor'
|
||||
import Placeholder from './components/placeholder'
|
||||
|
||||
/**
|
||||
* Utils.
|
||||
*/
|
||||
|
||||
import findDOMNode from './utils/find-dom-node'
|
||||
import findDOMRange from './utils/find-dom-range'
|
||||
import findNode from './utils/find-node'
|
||||
import findRange from './utils/find-range'
|
||||
|
||||
/**
|
||||
* Export.
|
||||
@@ -22,10 +16,16 @@ export {
|
||||
Editor,
|
||||
Placeholder,
|
||||
findDOMNode,
|
||||
findDOMRange,
|
||||
findNode,
|
||||
findRange,
|
||||
}
|
||||
|
||||
export default {
|
||||
Editor,
|
||||
Placeholder,
|
||||
findDOMNode,
|
||||
findDOMRange,
|
||||
findNode,
|
||||
findRange,
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ import { Block, Inline, coreSchema } from 'slate'
|
||||
import Content from '../components/content'
|
||||
import Placeholder from '../components/placeholder'
|
||||
import findDOMNode from '../utils/find-dom-node'
|
||||
import findPoint from '../utils/find-point'
|
||||
import findRange from '../utils/find-range'
|
||||
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
|
||||
|
||||
/**
|
||||
@@ -72,7 +72,6 @@ function Plugin(options = {}) {
|
||||
|
||||
const { state } = change
|
||||
const { selection } = state
|
||||
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
||||
|
||||
// COMPAT: In iOS, when using predictive text suggestions, the native
|
||||
// selection will be changed to span the existing word, so that the word is
|
||||
@@ -81,22 +80,11 @@ function Plugin(options = {}) {
|
||||
// the selection has gotten out of sync, and adjust it if so. (03/18/2017)
|
||||
const window = getWindow(e.target)
|
||||
const native = window.getSelection()
|
||||
const a = findPoint(native.anchorNode, native.anchorOffset, state)
|
||||
const f = findPoint(native.focusNode, native.focusOffset, state)
|
||||
const hasMismatch = a && f && (
|
||||
anchorKey != a.key ||
|
||||
anchorOffset != a.offset ||
|
||||
focusKey != f.key ||
|
||||
focusOffset != f.offset
|
||||
)
|
||||
const range = findRange(native, state)
|
||||
const hasMismatch = range && !range.equals(selection)
|
||||
|
||||
if (hasMismatch) {
|
||||
change.select({
|
||||
anchorKey: a.key,
|
||||
anchorOffset: a.offset,
|
||||
focusKey: f.key,
|
||||
focusOffset: f.offset
|
||||
})
|
||||
change.select(range)
|
||||
}
|
||||
|
||||
change.insertText(e.data)
|
||||
@@ -180,7 +168,8 @@ function Plugin(options = {}) {
|
||||
// the void node's spacer span, to the end of the void node's content.
|
||||
if (isVoid) {
|
||||
const r = range.cloneRange()
|
||||
const node = findDOMNode(isVoidBlock ? endBlock : endInline)
|
||||
const n = isVoidBlock ? endBlock : endInline
|
||||
const node = findDOMNode(n)
|
||||
r.setEndAfter(node)
|
||||
contents = r.cloneContents()
|
||||
attach = contents.childNodes[contents.childNodes.length - 1].firstChild
|
||||
@@ -315,6 +304,7 @@ function Plugin(options = {}) {
|
||||
if (Block.isBlock(node)) {
|
||||
change
|
||||
.select(target)
|
||||
.focus()
|
||||
.insertBlock(node)
|
||||
.removeNodeByKey(node.key)
|
||||
}
|
||||
@@ -322,6 +312,7 @@ function Plugin(options = {}) {
|
||||
if (Inline.isInline(node)) {
|
||||
change
|
||||
.select(target)
|
||||
.focus()
|
||||
.insertInline(node)
|
||||
.removeNodeByKey(node.key)
|
||||
}
|
||||
@@ -360,6 +351,7 @@ function Plugin(options = {}) {
|
||||
|
||||
change
|
||||
.select(target)
|
||||
.focus()
|
||||
.insertFragment(fragment)
|
||||
}
|
||||
|
||||
@@ -379,7 +371,7 @@ function Plugin(options = {}) {
|
||||
const { text, target } = data
|
||||
const { anchorKey } = target
|
||||
|
||||
change.select(target)
|
||||
change.select(target).focus()
|
||||
|
||||
let hasVoidParent = document.hasVoidParent(anchorKey)
|
||||
|
||||
|
@@ -1,53 +0,0 @@
|
||||
|
||||
/**
|
||||
* Extends a DOM `selection` to a given `el` and `offset`.
|
||||
*
|
||||
* COMPAT: In IE11, `selection.extend` doesn't exist natively, so we have to
|
||||
* polyfill it with this. (2017/09/06)
|
||||
*
|
||||
* https://gist.github.com/tyler-johnson/0a3e8818de3f115b2a2dc47468ac0099
|
||||
*
|
||||
* @param {Selection} selection
|
||||
* @param {Element} el
|
||||
* @param {Number} offset
|
||||
* @return {Selection}
|
||||
*/
|
||||
|
||||
function extendSelection(selection, el, offset) {
|
||||
// Use native method whenever possible.
|
||||
if (typeof selection.extend === 'function') {
|
||||
return selection.extend(el, offset)
|
||||
}
|
||||
|
||||
const range = document.createRange()
|
||||
const anchor = document.createRange()
|
||||
const focus = document.createRange()
|
||||
anchor.setStart(selection.anchorNode, selection.anchorOffset)
|
||||
focus.setStart(el, offset)
|
||||
|
||||
const v = focus.compareBoundaryPoints(window.Range.START_TO_START, anchor)
|
||||
|
||||
// If the focus is after the anchor...
|
||||
if (v >= 0) {
|
||||
range.setStart(selection.anchorNode, selection.anchorOffset)
|
||||
range.setEnd(el, offset)
|
||||
}
|
||||
|
||||
// Otherwise, if the anchor if after the focus...
|
||||
else {
|
||||
range.setStart(el, offset)
|
||||
range.setEnd(selection.anchorNode, selection.anchorOffset)
|
||||
}
|
||||
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
return selection
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default extendSelection
|
@@ -16,7 +16,7 @@ function findDOMNode(key) {
|
||||
const el = window.document.querySelector(`[data-key="${key}"]`)
|
||||
|
||||
if (!el) {
|
||||
throw new Error(`Unable to find a DOM node for "${key}". This is often because of forgetting to add \`props.attributes\` to a component returned from \`renderNode\`.`)
|
||||
throw new Error(`Unable to find a DOM node for "${key}". This is often because of forgetting to add \`props.attributes\` to a custom component.`)
|
||||
}
|
||||
|
||||
return el
|
||||
|
@@ -12,10 +12,8 @@ import findDOMNode from './find-dom-node'
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function findNativePoint(key, offset) {
|
||||
function findDOMPoint(key, offset) {
|
||||
const el = findDOMNode(key)
|
||||
if (!el) return null
|
||||
|
||||
const window = getWindow(el)
|
||||
const iterator = window.document.createNodeIterator(el, NodeFilter.SHOW_TEXT)
|
||||
let start = 0
|
||||
@@ -52,4 +50,4 @@ function findNativePoint(key, offset) {
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default findNativePoint
|
||||
export default findDOMPoint
|
34
packages/slate-react/src/utils/find-dom-range.js
Normal file
34
packages/slate-react/src/utils/find-dom-range.js
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import getWindow from 'get-window'
|
||||
|
||||
import findDOMPoint from './find-dom-point'
|
||||
|
||||
/**
|
||||
* Find a native DOM range Slate `range`.
|
||||
*
|
||||
* @param {Range} range
|
||||
* @return {Object|Null}
|
||||
*/
|
||||
|
||||
function findDOMRange(range) {
|
||||
const { anchorKey, anchorOffset, focusKey, focusOffset, isBackward, isCollapsed } = range
|
||||
const anchor = findDOMPoint(anchorKey, anchorOffset)
|
||||
const focus = isCollapsed ? anchor : findDOMPoint(focusKey, focusOffset)
|
||||
if (!anchor || !focus) return null
|
||||
|
||||
const window = getWindow(anchor.node)
|
||||
const r = window.document.createRange()
|
||||
const start = isBackward ? focus : anchor
|
||||
const end = isBackward ? anchor : focus
|
||||
r.setStart(start.node, start.offset)
|
||||
r.setEnd(end.node, end.offset)
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default findDOMRange
|
@@ -1,85 +0,0 @@
|
||||
|
||||
import getWindow from 'get-window'
|
||||
|
||||
import findClosestNode from './find-closest-node'
|
||||
import findPoint from './find-point'
|
||||
|
||||
/**
|
||||
* Find the target point for a drop `event`.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {State} state
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function findDropPoint(event, state) {
|
||||
const { document } = state
|
||||
const { nativeEvent, target } = event
|
||||
const { x, y } = nativeEvent
|
||||
|
||||
// Resolve the caret position where the drop occured.
|
||||
const window = getWindow(target)
|
||||
let n, o
|
||||
|
||||
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
|
||||
if (window.document.caretRangeFromPoint) {
|
||||
const range = window.document.caretRangeFromPoint(x, y)
|
||||
n = range.startContainer
|
||||
o = range.startOffset
|
||||
} else {
|
||||
const position = window.document.caretPositionFromPoint(x, y)
|
||||
n = position.offsetNode
|
||||
o = position.offset
|
||||
}
|
||||
|
||||
const nodeEl = findClosestNode(n, '[data-key]')
|
||||
const nodeKey = nodeEl.getAttribute('data-key')
|
||||
const node = document.key === nodeKey ? document : document.getDescendant(nodeKey)
|
||||
|
||||
// If the drop DOM target is inside an inline void node use last position of
|
||||
// the previous sibling text node or first position of the next sibling text
|
||||
// node as Slate target.
|
||||
if (node.isVoid && node.kind === 'inline') {
|
||||
const rect = nodeEl.getBoundingClientRect()
|
||||
const previous = x - rect.left < rect.left + rect.width - x
|
||||
const text = previous ?
|
||||
document.getPreviousSibling(nodeKey) :
|
||||
document.getNextSibling(nodeKey)
|
||||
const key = text.key
|
||||
const offset = previous ? text.characters.size : 0
|
||||
return { key, offset }
|
||||
}
|
||||
|
||||
// If the drop DOM target is inside a block void node use last position of
|
||||
// the previous sibling block node or first position of the next sibling block
|
||||
// node as Slate target.
|
||||
if (node.isVoid) {
|
||||
const rect = nodeEl.getBoundingClientRect()
|
||||
const previous = y - rect.top < rect.top + rect.height - y
|
||||
|
||||
if (previous) {
|
||||
const block = document.getPreviousBlock(nodeKey)
|
||||
const text = block.getLastText()
|
||||
const { key } = text
|
||||
const offset = text.characters.size
|
||||
return { key, offset }
|
||||
}
|
||||
|
||||
const block = document.getNextBlock(nodeKey)
|
||||
const text = block.getLastText()
|
||||
const { key } = text
|
||||
const offset = 0
|
||||
return { key, offset }
|
||||
}
|
||||
|
||||
const point = findPoint(n, o, state)
|
||||
return point
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default findDropPoint
|
28
packages/slate-react/src/utils/find-node.js
Normal file
28
packages/slate-react/src/utils/find-node.js
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
import findClosestNode from './find-closest-node'
|
||||
|
||||
/**
|
||||
* Find a Slate node from a DOM `element`.
|
||||
*
|
||||
* @param {Element} element
|
||||
* @return {Node|Null}
|
||||
*/
|
||||
|
||||
function findNode(element, state) {
|
||||
const closest = findClosestNode(element, '[data-key]')
|
||||
if (!closest) return null
|
||||
|
||||
const key = closest.getAttribute('data-key')
|
||||
if (!key) return null
|
||||
|
||||
const node = state.document.getNode(key)
|
||||
return node || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default findNode
|
@@ -2,7 +2,6 @@
|
||||
import getWindow from 'get-window'
|
||||
|
||||
import OffsetKey from './offset-key'
|
||||
import normalizeNodeAndOffset from './normalize-node-and-offset'
|
||||
import findClosestNode from './find-closest-node'
|
||||
|
||||
/**
|
||||
@@ -75,6 +74,87 @@ function findPoint(nativeNode, nativeOffset, state) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* From a DOM selection's `node` and `offset`, normalize so that it always
|
||||
* refers to a text node.
|
||||
*
|
||||
* @param {Element} node
|
||||
* @param {Number} offset
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function normalizeNodeAndOffset(node, offset) {
|
||||
// If it's an element node, its offset refers to the index of its children
|
||||
// including comment nodes, so try to find the right text child node.
|
||||
if (node.nodeType == 1 && node.childNodes.length) {
|
||||
const isLast = offset == node.childNodes.length
|
||||
const direction = isLast ? 'backward' : 'forward'
|
||||
const index = isLast ? offset - 1 : offset
|
||||
node = getEditableChild(node, index, direction)
|
||||
|
||||
// If the node has children, traverse until we have a leaf node. Leaf nodes
|
||||
// can be either text nodes, or other void DOM nodes.
|
||||
while (node.nodeType == 1 && node.childNodes.length) {
|
||||
const i = isLast ? node.childNodes.length - 1 : 0
|
||||
node = getEditableChild(node, i, direction)
|
||||
}
|
||||
|
||||
// Determine the new offset inside the text node.
|
||||
offset = isLast ? node.textContent.length : 0
|
||||
}
|
||||
|
||||
// Return the node and offset.
|
||||
return { node, offset }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nearest editable child at `index` in a `parent`, preferring
|
||||
* `direction`.
|
||||
*
|
||||
* @param {Element} parent
|
||||
* @param {Number} index
|
||||
* @param {String} direction ('forward' or 'backward')
|
||||
* @return {Element|Null}
|
||||
*/
|
||||
|
||||
function getEditableChild(parent, index, direction) {
|
||||
const { childNodes } = parent
|
||||
let child = childNodes[index]
|
||||
let i = index
|
||||
let triedForward = false
|
||||
let triedBackward = false
|
||||
|
||||
// While the child is a comment node, or an element node with no children,
|
||||
// keep iterating to find a sibling non-void, non-comment node.
|
||||
while (
|
||||
(child.nodeType == 8) ||
|
||||
(child.nodeType == 1 && child.childNodes.length == 0) ||
|
||||
(child.nodeType == 1 && child.getAttribute('contenteditable') == 'false')
|
||||
) {
|
||||
if (triedForward && triedBackward) break
|
||||
|
||||
if (i >= childNodes.length) {
|
||||
triedForward = true
|
||||
i = index - 1
|
||||
direction = 'backward'
|
||||
continue
|
||||
}
|
||||
|
||||
if (i < 0) {
|
||||
triedBackward = true
|
||||
i = index + 1
|
||||
direction = 'forward'
|
||||
continue
|
||||
}
|
||||
|
||||
child = childNodes[i]
|
||||
if (direction == 'forward') i++
|
||||
if (direction == 'backward') i--
|
||||
}
|
||||
|
||||
return child || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
|
54
packages/slate-react/src/utils/find-range.js
Normal file
54
packages/slate-react/src/utils/find-range.js
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
import getWindow from 'get-window'
|
||||
import isBackward from 'selection-is-backward'
|
||||
import { Range } from 'slate'
|
||||
|
||||
import findPoint from './find-point'
|
||||
|
||||
/**
|
||||
* Find a Slate range from a DOM `native` selection.
|
||||
*
|
||||
* @param {Selection} native
|
||||
* @param {State} state
|
||||
* @return {Range}
|
||||
*/
|
||||
|
||||
function findRange(native, state) {
|
||||
const el = native.anchorNode || native.startContainer
|
||||
const window = getWindow(el)
|
||||
|
||||
// If the `native` object is a DOM `Range` object, change it into something
|
||||
// that looks like a DOM `Selection` instead.
|
||||
if (native instanceof window.Range) {
|
||||
native = {
|
||||
anchorNode: native.startContainer,
|
||||
anchorOffset: native.startOffset,
|
||||
focusNode: native.endContainer,
|
||||
focusOffset: native.endOffset,
|
||||
}
|
||||
}
|
||||
|
||||
const { anchorNode, anchorOffset, focusNode, focusOffset, isCollapsed } = native
|
||||
const anchor = findPoint(anchorNode, anchorOffset, state)
|
||||
const focus = isCollapsed ? anchor : findPoint(focusNode, focusOffset, state)
|
||||
if (!anchor || !focus) return null
|
||||
|
||||
const range = Range.create({
|
||||
anchorKey: anchor.key,
|
||||
anchorOffset: anchor.offset,
|
||||
focusKey: focus.key,
|
||||
focusOffset: focus.offset,
|
||||
isBackward: isCollapsed ? false : isBackward(native),
|
||||
isFocused: true,
|
||||
})
|
||||
|
||||
return range
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default findRange
|
@@ -13,9 +13,8 @@ import { findDOMNode } from 'react-dom'
|
||||
*/
|
||||
|
||||
function getHtmlFromNativePaste(component, callback) {
|
||||
const el = findDOMNode(component)
|
||||
|
||||
// Create an off-screen clone of the element and give it focus.
|
||||
const el = findDOMNode(component)
|
||||
const clone = el.cloneNode()
|
||||
clone.setAttribute('class', '')
|
||||
clone.setAttribute('style', 'position: fixed; left: -9999px')
|
||||
|
@@ -1,89 +0,0 @@
|
||||
|
||||
/**
|
||||
* From a DOM selection's `node` and `offset`, normalize so that it always
|
||||
* refers to a text node.
|
||||
*
|
||||
* @param {Element} node
|
||||
* @param {Number} offset
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function normalizeNodeAndOffset(node, offset) {
|
||||
// If it's an element node, its offset refers to the index of its children
|
||||
// including comment nodes, so try to find the right text child node.
|
||||
if (node.nodeType == 1 && node.childNodes.length) {
|
||||
const isLast = offset == node.childNodes.length
|
||||
const direction = isLast ? 'backward' : 'forward'
|
||||
const index = isLast ? offset - 1 : offset
|
||||
node = getEditableChild(node, index, direction)
|
||||
|
||||
// If the node has children, traverse until we have a leaf node. Leaf nodes
|
||||
// can be either text nodes, or other void DOM nodes.
|
||||
while (node.nodeType == 1 && node.childNodes.length) {
|
||||
const i = isLast ? node.childNodes.length - 1 : 0
|
||||
node = getEditableChild(node, i, direction)
|
||||
}
|
||||
|
||||
// Determine the new offset inside the text node.
|
||||
offset = isLast ? node.textContent.length : 0
|
||||
}
|
||||
|
||||
// Return the node and offset.
|
||||
return { node, offset }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nearest editable child at `index` in a `parent`, preferring
|
||||
* `direction`.
|
||||
*
|
||||
* @param {Element} parent
|
||||
* @param {Number} index
|
||||
* @param {String} direction ('forward' or 'backward')
|
||||
* @return {Element|Null}
|
||||
*/
|
||||
|
||||
function getEditableChild(parent, index, direction) {
|
||||
const { childNodes } = parent
|
||||
let child = childNodes[index]
|
||||
let i = index
|
||||
let triedForward = false
|
||||
let triedBackward = false
|
||||
|
||||
// While the child is a comment node, or an element node with no children,
|
||||
// keep iterating to find a sibling non-void, non-comment node.
|
||||
while (
|
||||
(child.nodeType == 8) ||
|
||||
(child.nodeType == 1 && child.childNodes.length == 0) ||
|
||||
(child.nodeType == 1 && child.getAttribute('contenteditable') == 'false')
|
||||
) {
|
||||
if (triedForward && triedBackward) break
|
||||
|
||||
if (i >= childNodes.length) {
|
||||
triedForward = true
|
||||
i = index - 1
|
||||
direction = 'backward'
|
||||
continue
|
||||
}
|
||||
|
||||
if (i < 0) {
|
||||
triedBackward = true
|
||||
i = index + 1
|
||||
direction = 'forward'
|
||||
continue
|
||||
}
|
||||
|
||||
child = childNodes[i]
|
||||
if (direction == 'forward') i++
|
||||
if (direction == 'backward') i--
|
||||
}
|
||||
|
||||
return child || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default normalizeNodeAndOffset
|
@@ -1,12 +1,12 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from '../../../helpers/h'
|
||||
import { Selection } from 'slate'
|
||||
import { Range } from 'slate'
|
||||
|
||||
export default function (simulator) {
|
||||
const { state } = simulator
|
||||
const text = state.document.getTexts().first()
|
||||
const selection = Selection.create().collapseToStartOf(text).move(1).focus()
|
||||
const selection = Range.create().collapseToStartOf(text).move(1).focus()
|
||||
simulator.select(null, { selection })
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user