1
0
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:
Ian Storm Taylor 2017-03-30 00:41:06 -04:00
parent fccca74b8f
commit 6198708086
6 changed files with 137 additions and 232 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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