From 5b754ec45073a14bff09f4bbab7f7b5761d1502c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 3 May 2017 12:29:30 -0400 Subject: [PATCH 1/3] Avoid focus reverting to first of two split blocks --- blocks/components/editable/index.js | 11 ++++------- editor/modes/visual-editor/block.js | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/blocks/components/editable/index.js b/blocks/components/editable/index.js index 1018bfac28975..5ff816bca15d9 100644 --- a/blocks/components/editable/index.js +++ b/blocks/components/editable/index.js @@ -211,13 +211,10 @@ export default class Editable extends wp.element.Component { // Splitting into two blocks this.setContent( this.props.value ); - // The setTimeout fixes the focus jump to the original block - setTimeout( () => { - this.props.onSplit( - nodeListToReact( before, createElement ), - nodeListToReact( after, createElement ) - ); - } ); + this.props.onSplit( + nodeListToReact( before, createElement ), + nodeListToReact( after, createElement ) + ); } onNodeChange( { element, parents } ) { diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index ba97c8ef68d0e..d3cc4f52b0405 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -20,6 +20,7 @@ class VisualEditorBlock extends wp.element.Component { this.setAttributes = this.setAttributes.bind( this ); this.maybeDeselect = this.maybeDeselect.bind( this ); this.maybeHover = this.maybeHover.bind( this ); + this.maybeStartTyping = this.maybeStartTyping.bind( this ); this.mergeWithPrevious = this.mergeWithPrevious.bind( this ); this.previousOffset = null; } @@ -63,6 +64,18 @@ class VisualEditorBlock extends wp.element.Component { } } + maybeStartTyping() { + // We do not want to dispatch start typing if... + // - State value already reflects that we're typing (dispatch noise) + // - The current block is not selected (e.g. after a split occurs, + // we'll still receive the keyDown event, but the focus has since + // shifted to the newly created block) + const { isTyping, isSelected, onStartTyping } = this.props; + if ( ! isTyping && isSelected ) { + onStartTyping(); + } + } + mergeWithPrevious() { const { block, previousBlock, onFocus, replaceBlocks } = this.props; @@ -137,7 +150,7 @@ class VisualEditorBlock extends wp.element.Component { 'is-hovered': isHovered } ); - const { onSelect, onStartTyping, onHover, onMouseLeave, onFocus, onInsertAfter } = this.props; + const { onSelect, onHover, onMouseLeave, onFocus, onInsertAfter } = this.props; // Determine whether the block has props to apply to the wrapper let wrapperProps; @@ -177,7 +190,7 @@ class VisualEditorBlock extends wp.element.Component { } -
+
Date: Wed, 3 May 2017 13:58:16 -0400 Subject: [PATCH 2/3] Assign content as children of root Editable element --- blocks/components/editable/index.js | 31 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/blocks/components/editable/index.js b/blocks/components/editable/index.js index 5ff816bca15d9..79255b872cccc 100644 --- a/blocks/components/editable/index.js +++ b/blocks/components/editable/index.js @@ -61,7 +61,6 @@ export default class Editable extends wp.element.Component { constructor() { super( ...arguments ); - this.onInit = this.onInit.bind( this ); this.onSetup = this.onSetup.bind( this ); this.onChange = this.onChange.bind( this ); this.onNewBlock = this.onNewBlock.bind( this ); @@ -79,6 +78,7 @@ export default class Editable extends wp.element.Component { componentDidMount() { this.initialize(); + this.focus(); } initialize() { @@ -101,7 +101,6 @@ export default class Editable extends wp.element.Component { onSetup( editor ) { this.editor = editor; - editor.on( 'init', this.onInit ); editor.on( 'focusout', this.onChange ); editor.on( 'NewBlock', this.onNewBlock ); editor.on( 'focusin', this.onFocus ); @@ -109,11 +108,6 @@ export default class Editable extends wp.element.Component { editor.on( 'keydown', this.onKeyDown ); } - onInit() { - this.setContent( this.props.value ); - this.focus(); - } - onFocus() { if ( ! this.props.onFocus ) { return; @@ -363,16 +357,23 @@ export default class Editable extends wp.element.Component { } render() { - const { tagName: Tag = 'div', style, focus, className, showAlignments = false, formattingControls } = this.props; + const { + tagName = 'div', + value, + style, + focus, + className, + showAlignments = false, + formattingControls + } = this.props; const classes = classnames( 'blocks-editable', className ); - let element = ( - - ); + let element = wp.element.createElement( tagName, { + ref: this.bindEditorNode, + style: style, + className: classes, + key: 'editor' + }, ...wp.element.Children.toArray( value ) ); if ( focus ) { element = [ From 9dc274196eaf92b794fd5370cc8f7538088aa974 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 3 May 2017 17:13:14 -0400 Subject: [PATCH 3/3] Move TinyMCE into separate component and prevent rerender --- blocks/components/editable/index.js | 64 ++++++++------------------- blocks/components/editable/tinymce.js | 49 ++++++++++++++++++++ 2 files changed, 68 insertions(+), 45 deletions(-) create mode 100644 blocks/components/editable/tinymce.js diff --git a/blocks/components/editable/index.js b/blocks/components/editable/index.js index 79255b872cccc..91aec065bfa2c 100644 --- a/blocks/components/editable/index.js +++ b/blocks/components/editable/index.js @@ -12,6 +12,7 @@ import 'element-closest'; */ import './style.scss'; import FormatToolbar from './format-toolbar'; +import TinyMCE from './tinymce'; // TODO: We mustn't import by relative path traversing from blocks to editor // as we're doing here; instead, we should consider a common components path. import Toolbar from '../../../editor/components/toolbar'; @@ -61,10 +62,10 @@ export default class Editable extends wp.element.Component { constructor() { super( ...arguments ); + this.onInit = this.onInit.bind( this ); this.onSetup = this.onSetup.bind( this ); this.onChange = this.onChange.bind( this ); this.onNewBlock = this.onNewBlock.bind( this ); - this.bindEditorNode = this.bindEditorNode.bind( this ); this.onFocus = this.onFocus.bind( this ); this.onNodeChange = this.onNodeChange.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); @@ -76,31 +77,9 @@ export default class Editable extends wp.element.Component { }; } - componentDidMount() { - this.initialize(); - this.focus(); - } - - initialize() { - const config = { - target: this.editorNode, - theme: false, - inline: true, - toolbar: false, - browser_spellcheck: true, - entity_encoding: 'raw', - convert_urls: false, - setup: this.onSetup, - formats: { - strikethrough: { inline: 'del' } - } - }; - - tinymce.init( config ); - } - onSetup( editor ) { this.editor = editor; + editor.on( 'init', this.onInit ); editor.on( 'focusout', this.onChange ); editor.on( 'NewBlock', this.onNewBlock ); editor.on( 'focusin', this.onFocus ); @@ -108,6 +87,10 @@ export default class Editable extends wp.element.Component { editor.on( 'keydown', this.onKeyDown ); } + onInit() { + this.focus(); + } + onFocus() { if ( ! this.props.onFocus ) { return; @@ -128,7 +111,7 @@ export default class Editable extends wp.element.Component { } getRelativePosition( node ) { - const editorPosition = this.editorNode.closest( '.editor-visual-editor__block' ).getBoundingClientRect(); + const editorPosition = this.editor.getBody().closest( '.editor-visual-editor__block' ).getBoundingClientRect(); const position = node.getBoundingClientRect(); return { top: position.top - editorPosition.top + 40 + ( position.height ), @@ -233,10 +216,6 @@ export default class Editable extends wp.element.Component { this.setState( { alignment, bookmark, formats, focusPosition } ); } - bindEditorNode( ref ) { - this.editorNode = ref; - } - updateContent() { const bookmark = this.editor.selection.getBookmark( 2, true ); this.savedContent = this.props.value; @@ -258,7 +237,7 @@ export default class Editable extends wp.element.Component { } getContent() { - return nodeListToReact( this.editorNode.childNodes || [], createElement ); + return nodeListToReact( this.editor.getBody().childNodes || [], createElement ); } focus() { @@ -357,23 +336,18 @@ export default class Editable extends wp.element.Component { } render() { - const { - tagName = 'div', - value, - style, - focus, - className, - showAlignments = false, - formattingControls - } = this.props; + const { tagName, style, value, focus, className, showAlignments = false, formattingControls } = this.props; const classes = classnames( 'blocks-editable', className ); - let element = wp.element.createElement( tagName, { - ref: this.bindEditorNode, - style: style, - className: classes, - key: 'editor' - }, ...wp.element.Children.toArray( value ) ); + let element = ( + + ); if ( focus ) { element = [ diff --git a/blocks/components/editable/tinymce.js b/blocks/components/editable/tinymce.js new file mode 100644 index 0000000000000..b20039428cb04 --- /dev/null +++ b/blocks/components/editable/tinymce.js @@ -0,0 +1,49 @@ +export default class TinyMCE extends wp.element.Component { + componentDidMount() { + tinymce.init( { + target: this.editorNode, + theme: false, + inline: true, + toolbar: false, + browser_spellcheck: true, + entity_encoding: 'raw', + convert_urls: false, + setup: this.props.onSetup, + formats: { + strikethrough: { inline: 'del' } + } + } ); + + if ( this.props.focus ) { + this.editorNode.focus(); + } + } + + shouldComponentUpdate() { + // We must prevent rerenders because TinyMCE will modify the DOM, thus + // breaking React's ability to reconcile changes. + // + // See: https://github.com/facebook/react/issues/6802 + return false; + } + + render() { + const { tagName = 'div', style, className, defaultValue } = this.props; + + // If a default value is provided, render it into the DOM even before + // TinyMCE finishes initializing. This avoids a short delay by allowing + // us to show and focus the content before it's truly ready to edit. + let children; + if ( defaultValue ) { + children = wp.element.Children.toArray( defaultValue ); + } + + return wp.element.createElement( tagName, { + ref: ( node ) => this.editorNode = node, + contentEditable: true, + suppressContentEditableWarning: true, + style, + className + }, children ); + } +}