From d9bb0dd72c9f16ba8ac347abc217ddf023031e10 Mon Sep 17 00:00:00 2001 From: "Aeneas Rekkas (arekkas)" Date: Wed, 16 Nov 2016 13:02:58 +0100 Subject: [PATCH] move blur check to content component - closes #297 --- src/components/content.js | 58 ++++++++++++++++++++++++++++++++++++--- src/components/leaf.js | 58 +++++++-------------------------------- 2 files changed, 64 insertions(+), 52 deletions(-) diff --git a/src/components/content.js b/src/components/content.js index f81326af4..7957c5a60 100644 --- a/src/components/content.js +++ b/src/components/content.js @@ -4,6 +4,7 @@ import Debug from 'debug' import Node from './node' import OffsetKey from '../utils/offset-key' import React from 'react' +import ReactDOM from 'react-dom' import Selection from '../models/selection' import Transfer from '../utils/transfer' import TYPES from '../constants/types' @@ -28,6 +29,37 @@ const debug = Debug('slate:content') function noop() {} +/** + * Find the deepest descendant of a DOM `element`. + * + * @param {Element} node + * @return {Element} + */ + +function findDeepestNode(element) { + return element.firstChild + ? findDeepestNode(element.firstChild) + : element +} + +/** + * Tests if the child is a descendant of parent. + * + * @param parent + * @param child + * @returns {boolean} + */ + +function isDescendant(parent, child) { + if (!child || !parent) return false + let node = child.parentNode + while (node != null) { + if (node == parent) return true + node = node.parentNode + } + return false +} + /** * Content. * @@ -122,16 +154,34 @@ class Content extends React.Component { } /** - * When finished rendering, move the `isRendering` flag on next tick. + * When finished rendering, move the `isRendering` flag on next tick and clean up the DOM's activeElement + * if neccessary. * - * @param {Object} props - * @param {Object} state + * @param {Object} prevProps + * @param {Object} prevState */ - componentDidUpdate = (props, state) => { + componentDidUpdate = (prevProps, prevState) => { setTimeout(() => { this.tmp.isRendering = false }, 1) + + // If this component was focused last render, but is not now we might need to clean up the activeElement. + if (this.props.state.isBlurred && prevProps.state.isFocused) { + // Get the current selection + const ref = ReactDOM.findDOMNode(this) + const el = findDeepestNode(ref) + const window = getWindow(el) + const native = window.getSelection() + + // We need to make sure that the selection from our state is up-to-date with the native selection. + // We can do this by checking if native.anchorNode is a descendant of our this node. + if (isDescendant(ref, native.anchorNode)) { + // The selection was blurred, but the DOM selection state has not changed so we need to do this manually: + native.removeAllRanges() + if (ref) ref.blur() + } + } } /** diff --git a/src/components/leaf.js b/src/components/leaf.js index 96352b009..947296302 100644 --- a/src/components/leaf.js +++ b/src/components/leaf.js @@ -5,24 +5,6 @@ import React from 'react' import ReactDOM from 'react-dom' import getWindow from 'get-window' -/** - * Tests if the child is a descendant of parent. - * - * @param parent - * @param child - * @returns {boolean} - */ - -function isDescendant(parent, child) { - if (!child || !parent) return false - let node = child.parentNode - while (node != null) { - if (node == parent) return true - node = node.parentNode - } - return false -} - /** * Debugger. * @@ -154,6 +136,9 @@ class Leaf extends React.Component { const { state, ranges, isVoid } = this.props const { selection } = state + // If the selection is blurred we have nothing to do. + if (selection.isBlurred) return + let { anchorOffset, focusOffset } = selection const { node, index } = this.props const { start, end } = OffsetKey.findBounds(index, ranges) @@ -163,36 +148,6 @@ class Leaf extends React.Component { 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) - - // If no selection is defined we're probably in a node environment and we can skip the selection magic. - if (!window || !window.getSelection) return - - const native = window.getSelection() - const parent = ref.closest('[contenteditable]') - - // If the selection is blurred but the DOM selection still focuses this leaf, - // we need to clean up the ranges and blur the contenteditable. - if (selection.isBlurred && (hasAnchor || hasFocus)) { - // We need to make sure that the selection from our state is up-to-date with the native selection. - // We can do this by checking if native.anchorNode is a descendant of our contenteditable parent, which is this - // slate instance. If it's not, the selection is no longer under this instance's responsibility and not further - // action is required. - if (!isDescendant(parent, native.anchorNode)) return - - // Apparently our selection is blurred, but the DOM still has ranges in the nodes we manage. To fix this, - // we blur the contenteditable and remove all ranges. - native.removeAllRanges() - if (parent) parent.blur() - return - } - - // If the selection is blurred we have nothing to do. - if (selection.isBlurred) return - // If the leaf is a void leaf, ensure that it has no width. This is due to // void nodes always rendering an empty leaf, for browser compatibility. if (isVoid) { @@ -200,6 +155,13 @@ class Leaf extends React.Component { focusOffset = 0 } + // 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]') + // In firefox it is not enough to create a range, you also need to focus the contenteditable element. function focus() { if (parent) setTimeout(() => parent.focus(), 0)