1
0
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:
Ian Storm Taylor
2017-10-14 15:36:27 -07:00
committed by GitHub
parent d4a58543b5
commit 6c42f6c9c3
95 changed files with 1938 additions and 1868 deletions

View File

@@ -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

View File

@@ -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 })

View File

@@ -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

View File

@@ -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}

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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.
*

View 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

View File

@@ -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')

View File

@@ -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

View File

@@ -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 })
}