From 2512df98712ca954f44d8913e5145dff1751e120 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 11 May 2018 11:22:52 -0400 Subject: [PATCH] Writing Flow: Consider horizontal handled by stopPropagation in RichText --- editor/components/rich-text/index.js | 77 +++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js index cd2488cd919f47..634d187353b82a 100644 --- a/editor/components/rich-text/index.js +++ b/editor/components/rich-text/index.js @@ -45,7 +45,27 @@ import { EVENTS } from './constants'; import { withBlockEditContext } from '../block-edit/context'; import { domToFormat, valueToString } from './format'; -const { BACKSPACE, DELETE, ENTER, rawShortcut } = keycodes; +/** + * Browser dependencies + */ + +const { Node } = window; + +/** + * Module constants + */ + +const { LEFT, RIGHT, BACKSPACE, DELETE, ENTER, rawShortcut } = keycodes; + +/** + * Zero-width space character used by TinyMCE as a caret landing point for + * inline boundary nodes. + * + * @see tinymce/src/core/main/ts/text/Zwsp.ts + * + * @type {string} + */ +const TINYMCE_ZWSP = '\uFEFF'; /** * Returns true if the node is the inline node boundary. This is used in node @@ -61,7 +81,7 @@ const { BACKSPACE, DELETE, ENTER, rawShortcut } = keycodes; */ export function isEmptyInlineBoundary( node ) { const text = node.nodeName === 'A' ? node.innerText : node.textContent; - return text === '\uFEFF'; + return text === TINYMCE_ZWSP; } /** @@ -114,6 +134,7 @@ export class RichText extends Component { this.onChange = this.onChange.bind( this ); this.onNewBlock = this.onNewBlock.bind( this ); this.onNodeChange = this.onNodeChange.bind( this ); + this.onHorizontalNavigationKeyDown = this.onHorizontalNavigationKeyDown.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); this.onKeyUp = this.onKeyUp.bind( this ); this.changeFormats = this.changeFormats.bind( this ); @@ -438,19 +459,56 @@ export class RichText extends Component { left: position.left - containerPosition.left + ( position.width / 2 ) + toolbarOffset.left, }; } + /** + * Handles a horizontal navigation key down event to stop propagation if it + * can be inferred that it will be handled by TinyMCE (notably transitions + * out of an inline boundary node). + * + * @param {KeyboardEvent} event Keydown event. + */ + onHorizontalNavigationKeyDown( event ) { + const { focusNode, focusOffset } = window.getSelection(); + const { nodeType, nodeValue } = focusNode; + + if ( nodeType !== Node.TEXT_NODE ) { + return; + } + + const isReverse = event.keyCode === LEFT; + + let offset = focusOffset; + if ( isReverse ) { + offset--; + } + + // [WORKAROUND]: When in a new paragraph in a new inline boundary node, + // while typing the zero-width space occurs as the first child instead + // of at the end of the inline boundary where the caret is. This should + // only be exempt when focusNode is not _only_ the ZWSP, which occurs + // when caret is placed on the right outside edge of inline boundary. + if ( ! isReverse && focusOffset === nodeValue.length && + nodeValue.length > 1 && nodeValue[ 0 ] === TINYMCE_ZWSP ) { + offset = 0; + } + + if ( nodeValue[ offset ] === TINYMCE_ZWSP ) { + event.stopPropagation(); + } + } /** * Handles a keydown event from tinyMCE. * - * @param {KeydownEvent} event The keydow event as triggered by tinyMCE. + * @param {KeyboardEvent} event Keydown event. */ onKeyDown( event ) { const dom = this.editor.dom; const rootNode = this.editor.getBody(); + const { keyCode } = event; if ( - ( event.keyCode === BACKSPACE && isHorizontalEdge( rootNode, true ) ) || - ( event.keyCode === DELETE && isHorizontalEdge( rootNode, false ) ) + ( keyCode === BACKSPACE && isHorizontalEdge( rootNode, true ) ) || + ( keyCode === DELETE && isHorizontalEdge( rootNode, false ) ) ) { if ( ! this.props.onMerge && ! this.props.onRemove ) { return; @@ -458,7 +516,7 @@ export class RichText extends Component { this.onCreateUndoLevel(); - const forward = event.keyCode === DELETE; + const forward = keyCode === DELETE; if ( this.props.onMerge ) { this.props.onMerge( forward ); @@ -476,9 +534,14 @@ export class RichText extends Component { event.stopImmediatePropagation(); } + const isHorizontalNavigation = keyCode === LEFT || keyCode === RIGHT; + if ( isHorizontalNavigation ) { + this.onHorizontalNavigationKeyDown( event ); + } + // If we click shift+Enter on inline RichTexts, we avoid creating two contenteditables // We also split the content and call the onSplit prop if provided. - if ( event.keyCode === ENTER ) { + if ( keyCode === ENTER ) { if ( this.props.multiline ) { if ( ! this.props.onSplit ) { return;