From 9c81ed9c47289df3f896eba187d9fd441c4b9dee Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Fri, 2 Dec 2016 11:07:02 -0800 Subject: [PATCH] fix selection behavior around inline nodes --- src/components/content.js | 5 +- src/components/editor.js | 3 +- src/plugins/core.js | 97 ++++++++++++++++++++++++++++----------- 3 files changed, 75 insertions(+), 30 deletions(-) diff --git a/src/components/content.js b/src/components/content.js index 1f56abd7d..540299ac1 100644 --- a/src/components/content.js +++ b/src/components/content.js @@ -10,7 +10,6 @@ import Transfer from '../utils/transfer' import TYPES from '../constants/types' import getWindow from 'get-window' import keycode from 'keycode' -import noop from '../utils/noop' import { IS_FIREFOX, IS_MAC } from '../constants/environment' /** @@ -656,7 +655,7 @@ class Content extends React.Component { const anchorInline = document.getClosestInline(anchor.key) const focusInline = document.getClosestInline(focus.key) - if (anchorInline && anchor.offset == anchorText.length) { + if (anchorInline && !anchorInline.isVoid && anchor.offset == anchorText.length) { const block = document.getClosestBlock(anchor.key) const next = block.getNextText(anchor.key) if (next) { @@ -665,7 +664,7 @@ class Content extends React.Component { } } - if (focusInline && focus.offset == focusText.length) { + if (focusInline && !focusInline.isVoid && focus.offset == focusText.length) { const block = document.getClosestBlock(focus.key) const next = block.getNextText(focus.key) if (next) { diff --git a/src/components/editor.js b/src/components/editor.js index 05836746e..3a544d0aa 100644 --- a/src/components/editor.js +++ b/src/components/editor.js @@ -4,6 +4,7 @@ import CorePlugin from '../plugins/core' import Debug from 'debug' import React from 'react' import Schema from '../models/schema' +import State from '../models/state' import noop from '../utils/noop' /** @@ -58,7 +59,7 @@ class Editor extends React.Component { readOnly: React.PropTypes.bool, schema: React.PropTypes.object, spellCheck: React.PropTypes.bool, - state: React.PropTypes.object.isRequired, + state: React.PropTypes.instanceOf(State).isRequired, style: React.PropTypes.object }; diff --git a/src/plugins/core.js b/src/plugins/core.js index b2287d736..8e5e0163f 100644 --- a/src/plugins/core.js +++ b/src/plugins/core.js @@ -4,7 +4,6 @@ import Character from '../models/character' import Debug from 'debug' import Placeholder from '../components/placeholder' import React from 'react' -import String from '../utils/string' import getWindow from 'get-window' import { IS_MAC } from '../constants/environment' @@ -455,7 +454,10 @@ function Plugin(options = {}) { /** * On `left` key down, move backward. * - * COMPAT: This is required to solve for the case where an inline void node is + * COMPAT: This is required to make navigating with the left arrow work when + * a void node is selected. + * + * COMPAT: This is also required to solve for the case where an inline node is * surrounded by empty text nodes with zero-width spaces in them. Without this * the zero-width spaces will cause two arrow keys to jump to the next text. * @@ -473,17 +475,33 @@ function Plugin(options = {}) { const { document, startKey, startText } = state const hasVoidParent = document.hasVoidParent(startKey) - if ( - startText.text == '' || - hasVoidParent - ) { - const previousText = document.getPreviousText(startKey) - if (!previousText) return - + // If the current text node is empty, or we're inside a void parent, we're + // going to need to handle the selection behavior. + if (startText.text == '' || hasVoidParent) { e.preventDefault() + const previous = document.getPreviousText(startKey) + + // If there's no previous text node in the document, abort. + if (!previous) return + + // If the previous text is in the current block, and inside a non-void + // inline node, move one character into the inline node. + const { startBlock } = state + const previousBlock = document.getClosestBlock(previous.key) + const previousInline = document.getClosestInline(previous.key) + + if (previousBlock == startBlock && previousInline && !previousInline.isVoid) { + return state + .transform() + .collapseToEndOf(previous) + .moveBackward(1) + .apply() + } + + // Otherwise, move to the end of the previous node. return state .transform() - .collapseToEndOf(previousText) + .collapseToEndOf(previous) .apply() } } @@ -491,10 +509,18 @@ function Plugin(options = {}) { /** * On `right` key down, move forward. * - * COMPAT: This is required to solve for the case where an inline void node is + * COMPAT: This is required to make navigating with the right arrow work when + * a void node is selected. + * + * COMPAT: This is also required to solve for the case where an inline node is * surrounded by empty text nodes with zero-width spaces in them. Without this * the zero-width spaces will cause two arrow keys to jump to the next text. * + * COMPAT: In Chrome & Safari, selections that are at the zero offset of + * an inline node will be automatically replaced to be at the last offset + * of a previous inline node, which screws us up, so we never want to set the + * selection to the very start of an inline node here. (2016/11/29) + * * @param {Event} e * @param {Object} data * @param {State} state @@ -509,24 +535,43 @@ function Plugin(options = {}) { const { document, startKey, startText } = state const hasVoidParent = document.hasVoidParent(startKey) - if ( - startText.text == '' || - hasVoidParent - ) { - const nextText = document.getNextText(startKey) - if (!nextText) return state - - // COMPAT: In Chrome & Safari, selections that are at the zero offset of - // an inline node will be automatically replaced to be at the last offset - // of a previous inline node, which screws us up, so we always want to set - // it to the end of the node. (2016/11/29) - const hasNextVoidParent = document.hasVoidParent(nextText.key) - const method = hasNextVoidParent ? 'collapseToEndOf' : 'collapseToStartOf' - + // If the current text node is empty, or we're inside a void parent, we're + // going to need to handle the selection behavior. + if (startText.text == '' || hasVoidParent) { e.preventDefault() + const next = document.getNextText(startKey) + + // If there's no next text node in the document, abort. + if (!next) return state + + // If the next text is inside a void node, move to the end of it. + const isInVoid = document.hasVoidParent(next.key) + + if (isInVoid) { + return state + .transform() + .collapseToEndOf(next) + .apply() + } + + // If the next text is in the current block, and inside an inline node, + // move one character into the inline node. + const { startBlock } = state + const nextBlock = document.getClosestBlock(next.key) + const nextInline = document.getClosestInline(next.key) + + if (nextBlock == startBlock && nextInline) { + return state + .transform() + .collapseToStartOf(next) + .moveForward(1) + .apply() + } + + // Otherwise, move to the start of the next text node. return state .transform() - [method](nextText) + .collapseToStartOf(next) .apply() } }