mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-21 22:02:05 +02:00
change selection updating logic to happen at the top-level, closes #662
This commit is contained in:
parent
fccca74b8f
commit
6198708086
@ -41,13 +41,13 @@ class CheckListItem extends React.Component {
|
||||
const checked = node.data.get('checked')
|
||||
return (
|
||||
<div {...attributes} className="check-list-item">
|
||||
<div contentEditable={false}>
|
||||
<span contentEditable={false}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
@ -29,9 +29,9 @@ const schema = {
|
||||
nodes: {
|
||||
image: (props) => {
|
||||
const { node, state } = props
|
||||
const isFocused = state.selection.hasEdgeIn(node)
|
||||
const active = state.isFocused && state.selection.hasEdgeIn(node)
|
||||
const src = node.data.get('src')
|
||||
const className = isFocused ? 'active' : null
|
||||
const className = active ? 'active' : null
|
||||
return (
|
||||
<img src={src} className={className} {...props.attributes} />
|
||||
)
|
||||
|
@ -79,7 +79,7 @@ class Content extends React.Component {
|
||||
super(props)
|
||||
this.tmp = {}
|
||||
this.tmp.compositions = 0
|
||||
this.forces = 0
|
||||
this.tmp.forces = 0
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,10 +110,12 @@ class Content extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* On mount, if `autoFocus` is set, focus the editor.
|
||||
* On mount, update the selection, and focus the editor if `autoFocus` is set.
|
||||
*/
|
||||
|
||||
componentDidMount = () => {
|
||||
this.updateSelection()
|
||||
|
||||
if (this.props.autoFocus) {
|
||||
const el = ReactDOM.findDOMNode(this)
|
||||
el.focus()
|
||||
@ -121,22 +123,99 @@ class Content extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* On update, if the state is blurred now, but was focused before, and the
|
||||
* DOM still has a node inside the editor selected, we need to blur it.
|
||||
*
|
||||
* @param {Object} prevProps
|
||||
* @param {Object} prevState
|
||||
* On update, update the selection.
|
||||
*/
|
||||
|
||||
componentDidUpdate = (prevProps, prevState) => {
|
||||
if (this.props.state.isBlurred && prevProps.state.isFocused) {
|
||||
const el = ReactDOM.findDOMNode(this)
|
||||
const window = getWindow(el)
|
||||
const native = window.getSelection()
|
||||
componentDidUpdate = () => {
|
||||
this.updateSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the native DOM selection to reflect the internal model.
|
||||
*/
|
||||
|
||||
updateSelection = () => {
|
||||
const { state } = this.props
|
||||
const { selection } = state
|
||||
const el = ReactDOM.findDOMNode(this)
|
||||
const window = getWindow(el)
|
||||
const native = window.getSelection()
|
||||
|
||||
// If both selections are blurred, do nothing.
|
||||
if (!native.rangeCount && selection.isBlurred) return
|
||||
|
||||
// If the selection has been blurred, but hasn't been updated in the DOM,
|
||||
// blur the DOM selection.
|
||||
if (selection.isBlurred) {
|
||||
if (!el.contains(native.anchorNode)) return
|
||||
native.removeAllRanges()
|
||||
el.blur()
|
||||
debug('updateSelection', { selection, native })
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, figure out which DOM nodes should be selected...
|
||||
const { anchorText, focusText } = state
|
||||
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
||||
const anchorRanges = anchorText.getRanges()
|
||||
const focusRanges = focusText.getRanges()
|
||||
let a = 0
|
||||
let f = 0
|
||||
let anchorIndex
|
||||
let focusIndex
|
||||
let anchorOff
|
||||
let focusOff
|
||||
|
||||
anchorRanges.forEach((range, i, ranges) => {
|
||||
const { length } = range.text
|
||||
a += length
|
||||
if (a < anchorOffset) return
|
||||
anchorIndex = i
|
||||
anchorOff = anchorOffset - (a - length)
|
||||
return false
|
||||
})
|
||||
|
||||
focusRanges.forEach((range, i, ranges) => {
|
||||
const { length } = range.text
|
||||
f += length
|
||||
if (f < focusOffset) return
|
||||
focusIndex = i
|
||||
focusOff = focusOffset - (f - length)
|
||||
return false
|
||||
})
|
||||
|
||||
const anchorSpan = el.querySelector(`[data-offset-key="${anchorKey}-${anchorIndex}"]`)
|
||||
const focusSpan = el.querySelector(`[data-offset-key="${focusKey}-${focusIndex}"]`)
|
||||
const anchorEl = anchorSpan.firstChild
|
||||
const focusEl = focusSpan.firstChild
|
||||
|
||||
// If they are already selected, do nothing.
|
||||
if (
|
||||
anchorEl == native.anchorNode &&
|
||||
anchorOff == native.anchorOffset &&
|
||||
focusEl == native.focusNode &&
|
||||
focusOff == native.focusOffset
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, set the `isSelecting` flag and update the selection.
|
||||
this.tmp.isSelecting = true
|
||||
native.removeAllRanges()
|
||||
const range = window.document.createRange()
|
||||
range.setStart(anchorEl, anchorOff)
|
||||
native.addRange(range)
|
||||
native.extend(focusEl, focusOff)
|
||||
|
||||
// Then unset the `isSelecting` flag after a delay.
|
||||
setTimeout(() => {
|
||||
// COMPAT: In Firefox, it's not enough to create a range, you also need to
|
||||
// focus the contenteditable element too. (2016/11/16)
|
||||
if (IS_FIREFOX) el.focus()
|
||||
this.tmp.isSelecting = false
|
||||
})
|
||||
|
||||
debug('updateSelection', { selection, native })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -254,7 +333,7 @@ class Content extends React.Component {
|
||||
onCompositionEnd = (event) => {
|
||||
if (!this.isInContentEditable(event)) return
|
||||
|
||||
this.forces++
|
||||
this.tmp.forces++
|
||||
const count = this.tmp.compositions
|
||||
|
||||
// The `count` check here ensures that if another composition starts
|
||||
@ -619,6 +698,7 @@ class Content extends React.Component {
|
||||
if (this.props.readOnly) return
|
||||
if (this.tmp.isCopying) return
|
||||
if (this.tmp.isComposing) return
|
||||
if (this.tmp.isSelecting) return
|
||||
if (!this.isInContentEditable(event)) return
|
||||
|
||||
const window = getWindow(event.target)
|
||||
@ -640,10 +720,11 @@ class Content extends React.Component {
|
||||
const focus = getPoint(focusNode, focusOffset, state, editor)
|
||||
if (!anchor || !focus) return
|
||||
|
||||
// There are valid situations where a select event will fire when we're
|
||||
// already at that position (for example when entering a character), since
|
||||
// our `insertText` transform already updates the selection. In those
|
||||
// cases we can safely ignore the event.
|
||||
// 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 &&
|
||||
@ -651,6 +732,7 @@ class Content extends React.Component {
|
||||
focus.offset == selection.focusOffset &&
|
||||
selection.isFocused
|
||||
) {
|
||||
this.updateSelection()
|
||||
return
|
||||
}
|
||||
|
||||
@ -736,7 +818,7 @@ class Content extends React.Component {
|
||||
return (
|
||||
<div
|
||||
data-slate-editor
|
||||
key={this.forces}
|
||||
key={this.tmp.forces}
|
||||
ref={this.ref}
|
||||
contentEditable={!readOnly}
|
||||
suppressContentEditableWarning
|
||||
|
@ -3,7 +3,6 @@ import Debug from 'debug'
|
||||
import OffsetKey from '../utils/offset-key'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import getWindow from 'get-window'
|
||||
import { IS_FIREFOX } from '../constants/environment'
|
||||
|
||||
/**
|
||||
@ -88,123 +87,10 @@ class Leaf extends React.Component {
|
||||
const text = this.renderText(props)
|
||||
if (el.textContent != text) return true
|
||||
|
||||
// If the selection was previously focused, and now it isn't, re-render so
|
||||
// that the selection will be properly removed.
|
||||
if (this.props.state.isFocused && props.state.isBlurred) {
|
||||
const { index, node, ranges, state } = this.props
|
||||
const { start, end } = OffsetKey.findBounds(index, ranges)
|
||||
if (state.selection.hasEdgeBetween(node, start, end)) return true
|
||||
}
|
||||
|
||||
// If the selection will be focused, only re-render if this leaf contains
|
||||
// one or both of the selection's edges.
|
||||
if (props.state.isFocused) {
|
||||
const { index, node, ranges, state } = props
|
||||
const { start, end } = OffsetKey.findBounds(index, ranges)
|
||||
if (state.selection.hasEdgeBetween(node, start, end)) return true
|
||||
}
|
||||
|
||||
// Otherwise, don't update.
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* When the DOM updates, try updating the selection.
|
||||
*/
|
||||
|
||||
componentDidMount() {
|
||||
this.updateSelection()
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the DOM selection if it's inside the leaf.
|
||||
*/
|
||||
|
||||
updateSelection() {
|
||||
const { state, ranges } = this.props
|
||||
const { selection } = state
|
||||
|
||||
// If the selection is blurred we have nothing to do.
|
||||
if (selection.isBlurred) return
|
||||
|
||||
const { node, index } = this.props
|
||||
const { start, end } = OffsetKey.findBounds(index, ranges)
|
||||
const anchorOffset = selection.anchorOffset - start
|
||||
const focusOffset = selection.focusOffset - start
|
||||
|
||||
// If neither matches, the selection doesn't start or end here, so exit.
|
||||
const hasAnchor = selection.hasAnchorBetween(node, start, end)
|
||||
const hasFocus = selection.hasFocusBetween(node, start, end)
|
||||
if (!hasAnchor && !hasFocus) return
|
||||
|
||||
// We have a selection to render, so prepare a few things...
|
||||
const ref = ReactDOM.findDOMNode(this)
|
||||
const el = findDeepestNode(ref)
|
||||
const window = getWindow(el)
|
||||
const native = window.getSelection()
|
||||
const parent = ref.closest('[contenteditable]')
|
||||
|
||||
// COMPAT: In Firefox, it's not enough to create a range, you also need to
|
||||
// focus the contenteditable element. (2016/11/16)
|
||||
function focus() {
|
||||
if (!IS_FIREFOX) return
|
||||
if (parent) setTimeout(() => parent.focus())
|
||||
}
|
||||
|
||||
// If both the start and end are here, set the selection all at once.
|
||||
if (hasAnchor && hasFocus) {
|
||||
native.removeAllRanges()
|
||||
const range = window.document.createRange()
|
||||
range.setStart(el, anchorOffset)
|
||||
native.addRange(range)
|
||||
native.extend(el, focusOffset)
|
||||
focus()
|
||||
}
|
||||
|
||||
// Otherwise we need to set the selection across two different leaves.
|
||||
// If the selection is forward, we can set things in sequence. In the
|
||||
// first leaf to render, reset the selection and set the new start. And
|
||||
// then in the second leaf to render, extend to the new end.
|
||||
else if (selection.isForward) {
|
||||
if (hasAnchor) {
|
||||
native.removeAllRanges()
|
||||
const range = window.document.createRange()
|
||||
range.setStart(el, anchorOffset)
|
||||
native.addRange(range)
|
||||
} else if (hasFocus) {
|
||||
native.extend(el, focusOffset)
|
||||
focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, if the selection is backward, we need to hack the order a bit.
|
||||
// In the first leaf to render, set a phony start anchor to store the true
|
||||
// end position. And then in the second leaf to render, set the start and
|
||||
// extend the end to the stored value.
|
||||
else if (hasFocus) {
|
||||
native.removeAllRanges()
|
||||
const range = window.document.createRange()
|
||||
range.setStart(el, focusOffset)
|
||||
native.addRange(range)
|
||||
}
|
||||
else if (hasAnchor) {
|
||||
const endNode = native.focusNode
|
||||
const endOffset = native.focusOffset
|
||||
native.removeAllRanges()
|
||||
const range = window.document.createRange()
|
||||
range.setStart(el, anchorOffset)
|
||||
native.addRange(range)
|
||||
native.extend(endNode, endOffset)
|
||||
focus()
|
||||
}
|
||||
|
||||
this.debug('updateSelection', { selection })
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the leaf.
|
||||
*
|
||||
@ -225,7 +111,6 @@ class Leaf extends React.Component {
|
||||
// get out of sync, causing it to not realize the DOM needs updating.
|
||||
this.tmp.renders++
|
||||
|
||||
|
||||
this.debug('render', { props })
|
||||
|
||||
return (
|
||||
|
@ -89,80 +89,42 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
shouldComponentUpdate = (nextProps) => {
|
||||
const { props } = this
|
||||
const { Component } = this.state
|
||||
|
||||
// If the node is rendered with a `Component` that has enabled suppression
|
||||
// of update checking, always return true so that it can deal with update
|
||||
// checking itself.
|
||||
if (Component && Component.suppressShouldComponentUpdate) {
|
||||
return true
|
||||
// If the `Component` has enabled suppression of update checking, always
|
||||
// return true so that it can deal with update checking itself.
|
||||
if (Component && Component.suppressShouldComponentUpdate) return true
|
||||
|
||||
// If the `readOnly` status has changed, re-render in case there is any
|
||||
// user-land logic that depends on it, like nested editable contents.
|
||||
if (nextProps.readOnly != props.readOnly) return true
|
||||
|
||||
// If the node has changed, update. PERF: There are cases where it will have
|
||||
// changed, but it's properties will be exactly the same (eg. copy-paste)
|
||||
// which this won't catch. But that's rare and not a drag on performance, so
|
||||
// for simplicity we just let them through.
|
||||
if (nextProps.node != props.node) return true
|
||||
|
||||
// If the node is a block or inline, which can have custom renderers, we
|
||||
// include an extra check to re-render if the node's focus changes, to make
|
||||
// it simple for users to show a node's "selected" state.
|
||||
if (props.node.kind != 'text') {
|
||||
const hasEdgeIn = props.state.selection.hasEdgeIn(props.node)
|
||||
const nextHasEdgeIn = nextProps.state.selection.hasEdgeIn(nextProps.node)
|
||||
const hasFocus = props.state.isFocused || nextProps.state.isFocused
|
||||
const hasEdge = hasEdgeIn || nextHasEdgeIn
|
||||
if (hasFocus && hasEdge) return true
|
||||
}
|
||||
|
||||
// If the `readOnly` status has changed, we need to re-render in case there is
|
||||
// any user-land logic that depends on it, like nested editable contents.
|
||||
if (nextProps.readOnly !== this.props.readOnly) return true
|
||||
|
||||
// If the node has changed, update. PERF: There are certain cases where the
|
||||
// node instance will have changed, but it's properties will be exactly the
|
||||
// same (copy-paste, delete backwards, etc.) in which case this will not
|
||||
// catch a potentially avoidable re-render. But those cases are rare enough
|
||||
// that they aren't really a drag on performance, so for simplicity we just
|
||||
// let them through.
|
||||
if (nextProps.node != this.props.node) {
|
||||
return true
|
||||
}
|
||||
|
||||
const nextHasEdgeIn = nextProps.state.selection.hasEdgeIn(nextProps.node)
|
||||
|
||||
// If the selection is focused and is inside the node, we need to update so
|
||||
// that the selection will be set by one of the <Leaf> components.
|
||||
if (
|
||||
nextProps.state.isFocused &&
|
||||
nextHasEdgeIn
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
const hasEdgeIn = this.props.state.selection.hasEdgeIn(nextProps.node)
|
||||
// If the selection is blurred but was previously focused (or vice versa) inside the node,
|
||||
// we need to update to ensure the selection gets updated by re-rendering.
|
||||
if (
|
||||
this.props.state.isFocused != nextProps.state.isFocused &&
|
||||
(
|
||||
hasEdgeIn || nextHasEdgeIn
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// For block and inline nodes, which can have custom renderers, we need to
|
||||
// include another check for whether the previous selection had an edge in
|
||||
// the node, to allow for intuitive selection-based rendering.
|
||||
if (
|
||||
this.props.node.kind != 'text' &&
|
||||
hasEdgeIn != nextHasEdgeIn
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// For text nodes, which can have custom decorations, we need to check to
|
||||
// see if the block has changed, which has caused the decorations to change.
|
||||
// If the node is a text node, re-render if the current decorations have
|
||||
// changed, even if the content of the text node itself hasn't.
|
||||
if (nextProps.node.kind == 'text' && nextProps.schema.hasDecorators) {
|
||||
const { node, schema, state } = nextProps
|
||||
|
||||
const { document } = state
|
||||
const decorators = document.getDescendantDecorators(node.key, schema)
|
||||
const ranges = node.getRanges(decorators)
|
||||
|
||||
const prevNode = this.props.node
|
||||
const prevSchema = this.props.schema
|
||||
const prevDocument = this.props.state.document
|
||||
const prevDecorators = prevDocument.getDescendantDecorators(prevNode.key, prevSchema)
|
||||
const prevRanges = prevNode.getRanges(prevDecorators)
|
||||
|
||||
if (!ranges.equals(prevRanges)) {
|
||||
return true
|
||||
}
|
||||
const nextDecorators = nextProps.state.document.getDescendantDecorators(nextProps.node.key, nextProps.schema)
|
||||
const decorators = props.state.document.getDescendantDecorators(props.node.key, props.schema)
|
||||
const nextRanges = nextProps.node.getRanges(nextDecorators)
|
||||
const ranges = props.node.getRanges(decorators)
|
||||
if (!nextRanges.equals(ranges)) return true
|
||||
}
|
||||
|
||||
// Otherwise, don't update.
|
||||
|
@ -187,34 +187,11 @@ function Plugin(options = {}) {
|
||||
*/
|
||||
|
||||
function onBlur(e, data, state) {
|
||||
const isNative = true
|
||||
|
||||
debug('onBlur', { data, isNative })
|
||||
|
||||
debug('onBlur', { data })
|
||||
return state
|
||||
.transform()
|
||||
.blur()
|
||||
.apply({ isNative })
|
||||
}
|
||||
|
||||
/**
|
||||
* On focus.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Object} data
|
||||
* @param {State} state
|
||||
* @return {State}
|
||||
*/
|
||||
|
||||
function onFocus(e, data, state) {
|
||||
const isNative = true
|
||||
|
||||
debug('onFocus', { data, isNative })
|
||||
|
||||
return state
|
||||
.transform()
|
||||
.focus()
|
||||
.apply({ isNative })
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -975,7 +952,6 @@ function Plugin(options = {}) {
|
||||
onBeforeChange,
|
||||
onBeforeInput,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onCopy,
|
||||
onCut,
|
||||
onDrop,
|
||||
|
Loading…
x
Reference in New Issue
Block a user