From 79a2d215713ee55f4f758eaecaa1a8189f804a26 Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Fri, 17 May 2019 15:26:48 -0700 Subject: [PATCH] Reconcile dom node (#2801) * Add reconcileDOMNode command fixes set-text-from-dom-node * Fix linting and cleanup unused files * Removed unnecessary console log --- .../src/plugins/android/dom-snapshot.js | 3 +- .../slate-react/src/plugins/android/index.js | 5 +- packages/slate-react/src/plugins/dom/after.js | 3 +- .../slate-react/src/plugins/react/commands.js | 60 +++++++++++++++++ .../slate-react/src/plugins/react/index.js | 3 + .../src/utils/set-text-from-dom-node.js | 67 ------------------- 6 files changed, 67 insertions(+), 74 deletions(-) create mode 100644 packages/slate-react/src/plugins/react/commands.js delete mode 100644 packages/slate-react/src/utils/set-text-from-dom-node.js diff --git a/packages/slate-react/src/plugins/android/dom-snapshot.js b/packages/slate-react/src/plugins/android/dom-snapshot.js index 34d731630..c56f522ff 100644 --- a/packages/slate-react/src/plugins/android/dom-snapshot.js +++ b/packages/slate-react/src/plugins/android/dom-snapshot.js @@ -1,4 +1,3 @@ -import getSelectionFromDom from '../../utils/get-selection-from-dom' import ElementSnapshot from './element-snapshot' import SELECTORS from '../../constants/selectors' @@ -51,7 +50,7 @@ export default class DomSnapshot { } this.snapshot = new ElementSnapshot(elements) - this.selection = getSelectionFromDom(window, editor, domSelection) + this.selection = editor.findSelection(domSelection) } /** diff --git a/packages/slate-react/src/plugins/android/index.js b/packages/slate-react/src/plugins/android/index.js index 0ef1e0e96..8372e6ed7 100644 --- a/packages/slate-react/src/plugins/android/index.js +++ b/packages/slate-react/src/plugins/android/index.js @@ -5,7 +5,6 @@ import pick from 'lodash/pick' import { ANDROID_API_VERSION } from 'slate-dev-environment' import fixSelectionInZeroWidthBlock from './fix-selection-in-zero-width-block' import getSelectionFromDom from '../../utils/get-selection-from-dom' -import setTextFromDomNode from '../../utils/set-text-from-dom-node' import isInputDataEnter from './is-input-data-enter' import isInputDataLastChar from './is-input-data-last-char' import DomSnapshot from './dom-snapshot' @@ -133,10 +132,10 @@ function AndroidPlugin() { function reconcile(window, editor, { from }) { debug.reconcile({ from }) const domSelection = window.getSelection() - const selection = getSelectionFromDom(window, editor, domSelection) + const selection = editor.findSelection(domSelection) nodes.forEach(node => { - setTextFromDomNode(window, editor, node) + editor.reconcileDOMNode(node) }) editor.select(selection) diff --git a/packages/slate-react/src/plugins/dom/after.js b/packages/slate-react/src/plugins/dom/after.js index 973683bc2..479fd879a 100644 --- a/packages/slate-react/src/plugins/dom/after.js +++ b/packages/slate-react/src/plugins/dom/after.js @@ -8,7 +8,6 @@ import { IS_IOS, IS_IE, IS_EDGE } from 'slate-dev-environment' import cloneFragment from '../../utils/clone-fragment' import getEventTransfer from '../../utils/get-event-transfer' import setEventTransfer from '../../utils/set-event-transfer' -import setTextFromDomNode from '../../utils/set-text-from-dom-node' /** * Debug. @@ -432,7 +431,7 @@ function AfterPlugin(options = {}) { } const { anchorNode } = domSelection - setTextFromDomNode(window, editor, anchorNode) + editor.reconcileDOMNode(anchorNode) next() } diff --git a/packages/slate-react/src/plugins/react/commands.js b/packages/slate-react/src/plugins/react/commands.js new file mode 100644 index 000000000..2df38dbef --- /dev/null +++ b/packages/slate-react/src/plugins/react/commands.js @@ -0,0 +1,60 @@ +/** + * A set of commands for the React plugin. + * + * @return {Object} + */ + +function CommandsPlugin() { + /** + * reconcileDOMNode takes text from inside the `domNode` and uses it to set + * the text inside the matching `node` in Slate. + * + * @param {Window} window + * @param {Editor} editor + * @param {Node} domNode + */ + + function reconcileDOMNode(editor, domNode) { + const { value } = editor + const { document, selection } = value + const domElement = domNode.parentElement.closest('[data-key]') + const point = editor.findPoint(domElement, 0) + const node = document.getDescendant(point.path) + const block = document.getClosestBlock(point.path) + + // Get text information + const { text } = node + let { textContent: domText } = domElement + + const isLastNode = block.nodes.last() === node + const lastChar = domText.charAt(domText.length - 1) + + // COMPAT: If this is the last leaf, and the DOM text ends in a new line, + // we will have added another new line in 's render method to account + // for browsers collapsing a single trailing new lines, so remove it. + if (isLastNode && lastChar === '\n') { + domText = domText.slice(0, -1) + } + + // If the text is no different, abort. + if (text === domText) return + + let entire = selection + .moveAnchorTo(point.path, 0) + .moveFocusTo(point.path, text.length) + + entire = document.resolveRange(entire) + + // Change the current value to have the leaf's text replaced. + editor.insertTextAtRange(entire, domText, node.marks) + return + } + + return { + commands: { + reconcileDOMNode, + }, + } +} + +export default CommandsPlugin diff --git a/packages/slate-react/src/plugins/react/index.js b/packages/slate-react/src/plugins/react/index.js index 24bc7eb79..b70bdc9ec 100644 --- a/packages/slate-react/src/plugins/react/index.js +++ b/packages/slate-react/src/plugins/react/index.js @@ -2,6 +2,7 @@ import PlaceholderPlugin from 'slate-react-placeholder' import EditorPropsPlugin from './editor-props' import RenderingPlugin from './rendering' +import CommandsPlugin from './commands' import QueriesPlugin from './queries' import DOMPlugin from '../dom' import RestoreDOMPlugin from './restore-dom' @@ -16,6 +17,7 @@ import RestoreDOMPlugin from './restore-dom' function ReactPlugin(options = {}) { const { placeholder = '', plugins = [] } = options const renderingPlugin = RenderingPlugin(options) + const commandsPlugin = CommandsPlugin(options) const queriesPlugin = QueriesPlugin(options) const editorPropsPlugin = EditorPropsPlugin(options) const domPlugin = DOMPlugin({ @@ -36,6 +38,7 @@ function ReactPlugin(options = {}) { restoreDomPlugin, placeholderPlugin, renderingPlugin, + commandsPlugin, queriesPlugin, ] } diff --git a/packages/slate-react/src/utils/set-text-from-dom-node.js b/packages/slate-react/src/utils/set-text-from-dom-node.js deleted file mode 100644 index de1e6ecd2..000000000 --- a/packages/slate-react/src/utils/set-text-from-dom-node.js +++ /dev/null @@ -1,67 +0,0 @@ -import findPoint from './find-point' - -/** - * setTextFromDomNode lets us take a domNode and reconcile the text in the - * editor's Document such that it reflects the text in the DOM. This is the - * opposite of what the Editor usually does which takes the Editor's Document - * and React modifies the DOM to match. The purpose of this method is for - * composition changes where we don't know what changes the user made by - * looking at events. Instead we wait until the DOM is in a safe state, we - * read from it, and update the Editor's Document. - * - * @param {Window} window - * @param {Editor} editor - * @param {Node} domNode - */ - -export default function setTextFromDomNode(window, editor, domNode) { - const point = findPoint(domNode, 0, editor) - if (!point) return - - // Get the text node and leaf in question. - const { value } = editor - const { document, selection } = value - const node = document.getDescendant(point.path) - const block = document.getClosestBlock(point.path) - const leaves = node.getLeaves() - const lastText = block.getLastText() - const lastLeaf = leaves.last() - let start = 0 - let end = 0 - - const leaf = - leaves.find(r => { - start = end - end += r.text.length - if (end > point.offset) return true - }) || lastLeaf - - // Get the text information. - const { text } = leaf - let { textContent } = domNode - const isLastText = node === lastText - const isLastLeaf = leaf === lastLeaf - const lastChar = textContent.charAt(textContent.length - 1) - - // COMPAT: If this is the last leaf, and the DOM text ends in a new line, - // we will have added another new line in 's render method to account - // for browsers collapsing a single trailing new lines, so remove it. - if (isLastText && isLastLeaf && lastChar === '\n') { - textContent = textContent.slice(0, -1) - } - - // If the text is no different, abort. - if (textContent === text) return - - // Determine what the selection should be after changing the text. - // const delta = textContent.length - text.length - // const corrected = selection.moveToEnd().moveForward(delta) - let entire = selection - .moveAnchorTo(point.path, start) - .moveFocusTo(point.path, end) - - entire = document.resolveRange(entire) - - // Change the current value to have the leaf's text replaced. - editor.insertTextAtRange(entire, textContent, leaf.marks) -}