From 99218df727d2cadadd03e461e412360331a469fe Mon Sep 17 00:00:00 2001 From: etoledom Date: Mon, 29 Apr 2019 17:33:55 +0200 Subject: [PATCH 01/28] Adding `onSelectionChange` from store to RichText --- .../src/components/rich-text/index.native.js | 18 +++++++++++++++++- .../block-library/src/heading/edit.native.js | 1 + .../block-library/src/paragraph/edit.native.js | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index aca01b3897556..6c3d467d9bf24 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -12,7 +12,7 @@ import { View, Platform } from 'react-native'; import { Component, RawHTML } from '@wordpress/element'; import { withInstanceId, compose } from '@wordpress/compose'; import { BlockFormatControls } from '@wordpress/block-editor'; -import { withSelect } from '@wordpress/data'; +import { withSelect, withDispatch } from '@wordpress/data'; import { applyFormat, getActiveFormat, @@ -494,6 +494,7 @@ export class RichText extends Component { this.firedAfterTextChanged = true; // Selection change event always fires after the fact this.props.onChange( this.lastContent ); } + this.props.onSelectionChange(realStart, realEnd); } isEmpty() { @@ -802,6 +803,21 @@ const RichTextContainer = compose( [ onCaretVerticalPositionChange: context.onCaretVerticalPositionChange, }; } ), + withDispatch( ( dispatch, { + clientId, + instanceId, + identifier = instanceId, + } ) => { + const { + selectionChange, + } = dispatch( 'core/block-editor' ); + + return { + onSelectionChange( start, end ) { + selectionChange( clientId, identifier, start, end ); + }, + }; + } ), ] )( RichText ); RichTextContainer.Content = ( { value, format, tagName: Tag, multiline, ...props } ) => { diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index 59cc25d8bd4c3..c48523a216190 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -121,6 +121,7 @@ class HeadingEdit extends Component { setAttributes( { level: newLevel } ) } /> Date: Thu, 2 May 2019 13:59:17 +0200 Subject: [PATCH 02/28] First experiment fixing selection change handling in rich text --- .../src/components/rich-text/index.native.js | 245 ++++++++++-------- .../block-library/src/heading/edit.native.js | 67 +---- packages/rich-text/src/apply-format.native.js | 83 ------ .../rich-text/src/get-active-format.native.js | 44 ---- .../rich-text/src/remove-format.native.js | 70 ----- 5 files changed, 144 insertions(+), 365 deletions(-) delete mode 100644 packages/rich-text/src/apply-format.native.js delete mode 100644 packages/rich-text/src/get-active-format.native.js delete mode 100644 packages/rich-text/src/remove-format.native.js diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 6c3d467d9bf24..690d32474dd5e 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -5,6 +5,7 @@ */ import RCTAztecView from 'react-native-aztec'; import { View, Platform } from 'react-native'; +import memize from 'memize'; /** * WordPress dependencies @@ -16,6 +17,7 @@ import { withSelect, withDispatch } from '@wordpress/data'; import { applyFormat, getActiveFormat, + __unstableGetActiveFormats as getActiveFormats, isEmpty, create, split, @@ -77,7 +79,7 @@ const gutenbergFormatNamesToAztec = { }; export class RichText extends Component { - constructor( { multiline } ) { + constructor( { value, multiline, selectionStart, selectionEnd } ) { super( ...arguments ); this.isMultiline = false; @@ -103,22 +105,33 @@ export class RichText extends Component { this.onPaste = this.onPaste.bind( this ); this.onFocus = this.onFocus.bind( this ); this.onBlur = this.onBlur.bind( this ); + this.onTextUpdate = this.onTextUpdate.bind( this ); this.onContentSizeChange = this.onContentSizeChange.bind( this ); this.onFormatChangeForceChild = this.onFormatChangeForceChild.bind( this ); this.onFormatChange = this.onFormatChange.bind( this ); + this.formatToValue = memize( + this.formatToValue.bind( this ), + { maxSize: 1 } + ); + // This prevents a bug in Aztec which triggers onSelectionChange twice on format change this.onSelectionChange = this.onSelectionChange.bind( this ); + this.onSelectionChangeFromAztec = this.onSelectionChangeFromAztec.bind( this ); this.valueToFormat = this.valueToFormat.bind( this ); this.willTrimSpaces = this.willTrimSpaces.bind( this ); this.state = { - start: 0, - end: 0, - formatPlaceholder: null, + activeFormats: [], + selectedFormat: null, height: 0, }; this.needsSelectionUpdate = false; this.savedContent = ''; this.isTouched = false; + + // Internal values that are update synchronously, unlike props. + this.value = value; + this.selectionStart = selectionStart; + this.selectionEnd = selectionEnd; } /** @@ -127,9 +140,8 @@ export class RichText extends Component { * @return {Object} The current record (value and selection). */ getRecord() { - const { formatPlaceholder, start, end } = this.state; - - let value = this.props.value === undefined ? null : this.props.value; + const { selectionStart: start, selectionEnd: end } = this.props; + let { value } = this.props; // Since we get the text selection from Aztec we need to be in sync with the HTML `value` // Removing leading white spaces using `trim()` should make sure this is the case. @@ -138,8 +150,34 @@ export class RichText extends Component { } const { formats, replacements, text } = this.formatToValue( value ); + const { activeFormats } = this.state; + + return { formats, replacements, text, start, end, activeFormats }; + } - return { formats, replacements, formatPlaceholder, text, start, end }; + /** + * Creates a RichText value "record" from native content and selection + * information + * + * @param {string} currentContent The content (usually an HTML string) from + * the native component. + * @param {number} selectionStart The start of the selection. + * @param {number} selectionEnd The end of the selection (same as start if + * cursor instead of selection). + * + * @return {Object} A RichText value with formats and selection. + */ + createRecord() { + return { + start: this.selectionStart, + end: this.selectionEnd, + ...create( { + html: this.value, + range: null, + multilineTag: this.multilineTag, + multilineWrapperTags: this.multilineWrapperTags, + } ), + }; } /* @@ -226,7 +264,7 @@ export class RichText extends Component { console.log( error ); } this.setState( { - formatPlaceholder: record.formatPlaceholder, + activeFormats: record.activeFormats, } ); if ( newContent && newContent !== this.props.value ) { this.props.onChange( newContent ); @@ -272,17 +310,25 @@ export class RichText extends Component { */ onChange( event ) { this.lastEventCount = event.nativeEvent.eventCount; + this.onTextUpdate( event ); + } + + onTextUpdate( event, refresh = false ) { const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); - this.lastContent = contentWithoutRootTag; + + if ( ! refresh ) { + this.value = contentWithoutRootTag; + } + this.comesFromAztec = true; this.firedAfterTextChanged = true; // the onChange event always fires after the fact - this.props.onChange( this.lastContent ); + + this.props.onChange( contentWithoutRootTag ); } - /** + /* * Handles any case where the content of the AztecRN instance has changed in size */ - onContentSizeChange( contentSize ) { const contentHeight = contentSize.height; this.setState( { height: contentHeight } ); @@ -294,19 +340,15 @@ export class RichText extends Component { this.comesFromAztec = true; this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged; - const currentRecord = this.createRecord( { - ...event.nativeEvent, - currentContent: unescapeSpaces( event.nativeEvent.text ), - } ); + this.onTextUpdate( event ); + const currentRecord = this.createRecord(); if ( this.multilineTag ) { if ( event.shiftKey ) { const insertedLineBreak = { needsSelectionUpdate: true, ...insert( currentRecord, '\n' ) }; this.onFormatChangeForceChild( insertedLineBreak ); } else if ( this.onSplit && isEmptyLine( currentRecord ) ) { - this.setState( { - needsSelectionUpdate: false, - } ); + this.needsSelectionUpdate = false; this.splitContent( currentRecord ); } else { const insertedLineSeparator = { needsSelectionUpdate: true, ...insertLineSeparator( currentRecord ) }; @@ -330,6 +372,11 @@ export class RichText extends Component { const keyCode = BACKSPACE; // TODO : should we differentiate BACKSPACE and DELETE? const isReverse = keyCode === BACKSPACE; + // Only process delete if the key press occurs at uncollapsed edge. + if ( ! isCollapsed( this.createRecord() ) ) { + return; + } + const empty = this.isEmpty(); if ( onMerge ) { @@ -343,6 +390,8 @@ export class RichText extends Component { if ( onRemove && empty && isReverse ) { onRemove( ! isReverse ); } + + event.preventDefault(); } /** @@ -355,7 +404,8 @@ export class RichText extends Component { const { onSplit } = this.props; const { pastedText, pastedHtml, files } = event.nativeEvent; - const currentRecord = this.createRecord( event.nativeEvent ); + this.onTextUpdate( event ); + const currentRecord = this.createRecord(); event.preventDefault(); @@ -396,8 +446,9 @@ export class RichText extends Component { href: decodeEntities( trimmedText ), }, } ); - this.lastContent = this.valueToFormat( linkedRecord ); - this.props.onChange( this.lastContent ); + this.value = this.valueToFormat( linkedRecord ); + //this.lastEventCount = undefined; + this.props.onChange( this.value ); // Allows us to ask for this information when we get a report. window.console.log( 'Created link:\n\n', trimmedText ); @@ -428,12 +479,14 @@ export class RichText extends Component { const recordToInsert = create( { html: pastedContent } ); const insertedContent = insert( currentRecord, recordToInsert ); const newContent = this.valueToFormat( insertedContent ); - this.lastContent = newContent; + + this.lastEventCount = undefined; + this.value = newContent; // explicitly set selection after inline paste this.forceSelectionUpdate( insertedContent.start, insertedContent.end ); - this.props.onChange( this.lastContent ); + this.props.onChange( this.value ); } else if ( onSplit ) { if ( ! pastedContent.length ) { return; @@ -463,75 +516,37 @@ export class RichText extends Component { } } - onSelectionChange( start, end, text, event ) { + onSelectionChange( start, end ) { + this.selectionStart = start; + this.selectionEnd = end; + + const value = this.createRecord(); + const activeFormats = getActiveFormats( value ); + this.setState( { activeFormats } ); + + this.props.onSelectionChange( start, end ); + } + + onSelectionChangeFromAztec( start, end, text, event ) { + // update text before updating selection + // Make sure there are changes made to the content before upgrading it upward + this.onTextUpdate( event, true ); + // `end` can be less than `start` on iOS // Let's fix that here so `rich-text/slice` can work properly const realStart = Math.min( start, end ); const realEnd = Math.max( start, end ); - const noChange = this.state.start === start && this.state.end === end; - const isTyping = this.state.start + 1 === realStart; - const shouldKeepFormats = noChange || isTyping; - // update format placeholder to continue writing in the current format - // or set it to null if user jumped to another part in the text - const formatPlaceholder = shouldKeepFormats && this.state.formatPlaceholder ? { - ...this.state.formatPlaceholder, - index: realStart, - } : null; - this.setState( { - start: realStart, - end: realEnd, - formatPlaceholder, - } ); - this.lastEventCount = event.nativeEvent.eventCount; - // Make sure there are changes made to the content before upgrading it upward - const newContent = this.removeRootTagsProduceByAztec( unescapeSpaces( text ) ); - if ( this.lastContent !== newContent ) { - // we don't want to refresh aztec native as no content can have changed from this event - // let's update lastContent to prevent that in shouldComponentUpdate - this.lastContent = newContent; - this.comesFromAztec = true; - this.firedAfterTextChanged = true; // Selection change event always fires after the fact - this.props.onChange( this.lastContent ); - } - this.props.onSelectionChange(realStart, realEnd); + this.onSelectionChange( realStart, realEnd ); + + // Update lastEventCount to prevent Aztec from re-rendering the content it just sent + this.lastEventCount = event.nativeEvent.eventCount; } isEmpty() { return isEmpty( this.formatToValue( this.props.value ) ); } - /** - * Creates a RichText value "record" from native content and selection - * information - * - * @param {string} currentContent The content (usually an HTML string) from - * the native component. - * @param {number} selectionStart The start of the selection. - * @param {number} selectionEnd The end of the selection (same as start if - * cursor instead of selection). - * - * @return {Object} A RichText value with formats and selection. - */ - createRecord( { currentContent, selectionStart, selectionEnd } ) { - // strip outer

tags - const innerContent = this.removeRootTagsProduceByAztec( currentContent ); - - // create record (with selection) from current contents - const currentRecord = { - start: selectionStart, - end: selectionEnd, - ...create( { - html: innerContent, - range: null, - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, - } ), - }; - - return currentRecord; - } - formatToValue( value ) { // Handle deprecated `children` and `node` sources. if ( Array.isArray( value ) ) { @@ -592,14 +607,15 @@ export class RichText extends Component { forceSelectionUpdate( start, end ) { if ( ! this.needsSelectionUpdate ) { this.needsSelectionUpdate = true; - this.setState( { start, end } ); + this.selectionStart = start; + this.selectionEnd = end; } } shouldComponentUpdate( nextProps ) { if ( nextProps.tagName !== this.props.tagName ) { this.lastEventCount = undefined; - this.lastContent = undefined; + this.value = undefined; return true; } @@ -609,7 +625,7 @@ export class RichText extends Component { // If the component is changed React side (undo/redo/merging/splitting/custom text actions) // we need to make sure the native is updated as well. - const previousValueToCheck = Platform.OS === 'android' ? this.props.value : this.lastContent; + const previousValueToCheck = Platform.OS === 'android' ? this.props.value : this.value; // Also, don't trust the "this.lastContent" as on Android, incomplete text events arrive // with only some of the text, while the virtual keyboard's suggestion system does its magic. @@ -634,6 +650,7 @@ export class RichText extends Component { componentDidMount() { if ( this.props.isSelected ) { this._editor.focus(); + this.onSelectionChange( this.props.selectionStart || 0, this.props.selectionEnd || 0 ); } } @@ -646,6 +663,9 @@ export class RichText extends Component { componentDidUpdate( prevProps ) { if ( this.props.isSelected && ! prevProps.isSelected ) { this._editor.focus(); + // Update selection props explicitly when component is selected as Aztec won't call onSelectionChange + // if its internal value hasn't change. When created, default value is 0, 0 + this.onSelectionChange( this.props.selectionStart || 0, this.props.selectionEnd || 0 ); } else if ( ! this.props.isSelected && prevProps.isSelected && this.isIOS ) { this._editor.blur(); } @@ -698,7 +718,7 @@ export class RichText extends Component { // So, skip forcing it, let Aztec just do its best and just log the fact. console.warn( 'RichText value will be trimmed for spaces! Avoiding setting the caret position manually.' ); } else { - selection = { start: this.state.start, end: this.state.end }; + selection = { start: this.selectionStart, end: this.selectionEnd }; } } @@ -747,7 +767,7 @@ export class RichText extends Component { activeFormats={ this.getActiveFormatNames( record ) } onContentSizeChange={ this.onContentSizeChange } onCaretVerticalPositionChange={ this.props.onCaretVerticalPositionChange } - onSelectionChange={ this.onSelectionChange } + onSelectionChange={ this.onSelectionChangeFromAztec } isSelected={ isSelected } blockType={ { tag: tagName } } color={ 'black' } @@ -772,35 +792,34 @@ RichText.defaultProps = { const RichTextContainer = compose( [ withInstanceId, - withSelect( ( select ) => { + withBlockEditContext( ( { clientId } ) => ( { clientId } ) ), + withSelect( ( select, { + clientId, + instanceId, + identifier = instanceId, + isSelected, + } ) => { const { getFormatTypes } = select( 'core/rich-text' ); + const { + getSelectionStart, + getSelectionEnd, + } = select( 'core/block-editor' ); + + const selectionStart = getSelectionStart(); + const selectionEnd = getSelectionEnd(); + + if ( isSelected === undefined ) { + isSelected = ( + selectionStart.clientId === clientId && + selectionStart.attributeKey === identifier + ); + } return { formatTypes: getFormatTypes(), - }; - } ), - withBlockEditContext( ( context, ownProps ) => { - // When explicitly set as not selected, do nothing. - if ( ownProps.isSelected === false ) { - return { - clientId: context.clientId, - }; - } - // When explicitly set as selected, use the value stored in the context instead. - if ( ownProps.isSelected === true ) { - return { - isSelected: context.isSelected, - clientId: context.clientId, - onCaretVerticalPositionChange: context.onCaretVerticalPositionChange, - }; - } - - // Ensures that only one RichText component can be focused. - return { - clientId: context.clientId, - isSelected: context.isSelected, - onFocus: context.onFocus || ownProps.onFocus, - onCaretVerticalPositionChange: context.onCaretVerticalPositionChange, + selectionStart: isSelected ? selectionStart.offset : undefined, + selectionEnd: isSelected ? selectionEnd.offset : undefined, + isSelected, }; } ), withDispatch( ( dispatch, { diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index c48523a216190..ded76af565b63 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -19,61 +19,7 @@ import { RichText, BlockControls } from '@wordpress/block-editor'; import { createBlock } from '@wordpress/blocks'; import { create } from '@wordpress/rich-text'; -const name = 'core/heading'; - class HeadingEdit extends Component { - constructor( props ) { - super( props ); - - this.splitBlock = this.splitBlock.bind( this ); - } - - /** - * Split handler for RichText value, namely when content is pasted or the - * user presses the Enter key. - * - * @param {?Array} before Optional before value, to be used as content - * in place of what exists currently for the - * block. If undefined, the block is deleted. - * @param {?Array} after Optional after value, to be appended in a new - * paragraph block to the set of blocks passed - * as spread. - * @param {...WPBlock} blocks Optional blocks inserted between the before - * and after value blocks. - */ - splitBlock( before, after, ...blocks ) { - const { - attributes, - insertBlocksAfter, - setAttributes, - onReplace, - } = this.props; - - if ( after ) { - // Append "After" content as a new heading block to the end of - // any other blocks being inserted after the current heading. - const newBlock = createBlock( name, { content: after } ); - blocks.push( newBlock ); - } else { - const newBlock = createBlock( 'core/paragraph', { content: after } ); - blocks.push( newBlock ); - } - - if ( blocks.length && insertBlocksAfter ) { - insertBlocksAfter( blocks ); - } - - const { content } = attributes; - if ( before === null ) { - onReplace( [] ); - } else if ( content !== before ) { - // Only update content if it has in-fact changed. In case that user - // has created a new paragraph at end of an existing one, the value - // of before will be strictly equal to the current content. - setAttributes( { content: before } ); - } - } - plainTextContent( html ) { const result = create( { html } ); if ( result ) { @@ -85,6 +31,7 @@ class HeadingEdit extends Component { render() { const { attributes, + insertBlocksAfter, setAttributes, mergeBlocks, style, @@ -133,7 +80,17 @@ class HeadingEdit extends Component { onBlur={ this.props.onBlur } // always assign onBlur as a props onChange={ ( value ) => setAttributes( { content: value } ) } onMerge={ mergeBlocks } - onSplit={ this.splitBlock } + unstableOnSplit={ + insertBlocksAfter ? + ( before, after, ...blocks ) => { + setAttributes( { content: before } ); + insertBlocksAfter( [ + ...blocks, + createBlock( 'core/paragraph', { content: after } ), + ] ); + } : + undefined + } placeholder={ placeholder || __( 'Write heading…' ) } /> diff --git a/packages/rich-text/src/apply-format.native.js b/packages/rich-text/src/apply-format.native.js deleted file mode 100644 index ef641cbaa224d..0000000000000 --- a/packages/rich-text/src/apply-format.native.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * External dependencies - */ - -import { cloneDeep } from 'lodash'; - -/** - * Internal dependencies - */ - -import { normaliseFormats } from './normalise-formats'; - -/** - * Apply a format object to a Rich Text value from the given `startIndex` to the - * given `endIndex`. Indices are retrieved from the selection if none are - * provided. - * - * @param {Object} value Value to modify. - * @param {Object} formats Formats to apply. - * @param {number} startIndex Start index. - * @param {number} endIndex End index. - * - * @return {Object} A new value with the format applied. - */ -export function applyFormat( - value, - formats, - startIndex = value.start, - endIndex = value.end -) { - const { formats: currentFormats, formatPlaceholder, start } = value; - - if ( ! Array.isArray( formats ) ) { - formats = [ formats ]; - } - - // The selection is collpased, insert a placeholder with the format so new input appears - // with the format applied. - if ( startIndex === endIndex ) { - const previousFormats = currentFormats[ startIndex - 1 ] || []; - const placeholderFormats = formatPlaceholder && formatPlaceholder.index === start && formatPlaceholder.formats; - // Follow the same logic as in getActiveFormat: placeholderFormats has priority over previousFormats - const activeFormats = ( placeholderFormats ? placeholderFormats : previousFormats ) || []; - return { - ...value, - formats: currentFormats, - formatPlaceholder: { - index: start, - formats: mergeFormats( activeFormats, formats ), - }, - }; - } - - const newFormats = currentFormats.slice( 0 ); - - for ( let index = startIndex; index < endIndex; index++ ) { - applyFormats( newFormats, index, formats ); - } - - return normaliseFormats( { ...value, formats: newFormats } ); -} - -function mergeFormats( formats1, formats2 ) { - const formatsOut = cloneDeep( formats1 ); - formats2.forEach( ( format2 ) => { - const format1In2 = formatsOut.find( ( format1 ) => format1.type === format2.type ); - // update properties while keeping the formats ordered - if ( format1In2 ) { - Object.assign( format1In2, format2 ); - } else { - formatsOut.push( cloneDeep( format2 ) ); - } - } ); - return formatsOut; -} - -function applyFormats( formats, index, newFormats ) { - if ( formats[ index ] ) { - formats[ index ] = mergeFormats( formats[ index ], newFormats ); - } else { - formats[ index ] = cloneDeep( newFormats ); - } -} diff --git a/packages/rich-text/src/get-active-format.native.js b/packages/rich-text/src/get-active-format.native.js deleted file mode 100644 index 152070fedf278..0000000000000 --- a/packages/rich-text/src/get-active-format.native.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * External dependencies - */ - -import { find } from 'lodash'; - -/** - * Gets the format object by type at the start of the selection. This can be - * used to get e.g. the URL of a link format at the current selection, but also - * to check if a format is active at the selection. Returns undefined if there - * is no format at the selection. - * - * @param {Object} value Value to inspect. - * @param {string} formatType Format type to look for. - * - * @return {?Object} Active format object of the specified type, or undefined. - */ -export function getActiveFormat( { formats, formatPlaceholder, start, end }, formatType ) { - if ( start === undefined ) { - return; - } - - // if selection is not empty, get the first character format - if ( start !== end ) { - return find( formats[ start ], { type: formatType } ); - } - - // if user picked (or unpicked) formats but didn't write anything in those formats yet return this format - if ( formatPlaceholder && formatPlaceholder.index === start ) { - return find( formatPlaceholder.formats, { type: formatType } ); - } - - // if we're at the start of text, use the first char to pick up the formats - const startPos = start === 0 ? 0 : start - 1; - - // otherwise get the previous character format - const previousLetterFormat = find( formats[ startPos ], { type: formatType } ); - - if ( previousLetterFormat ) { - return previousLetterFormat; - } - - return undefined; -} diff --git a/packages/rich-text/src/remove-format.native.js b/packages/rich-text/src/remove-format.native.js deleted file mode 100644 index 5c20260728b71..0000000000000 --- a/packages/rich-text/src/remove-format.native.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * External dependencies - */ - -import { cloneDeep } from 'lodash'; - -/** - * Internal dependencies - */ - -import { normaliseFormats } from './normalise-formats'; - -/** - * Remove any format object from a Rich Text value by type from the given - * `startIndex` to the given `endIndex`. Indices are retrieved from the - * selection if none are provided. - * - * @param {Object} value Value to modify. - * @param {string} formatType Format type to remove. - * @param {number} startIndex Start index. - * @param {number} endIndex End index. - * - * @return {Object} A new value with the format applied. - */ -export function removeFormat( - value, - formatType, - startIndex = value.start, - endIndex = value.end -) { - const { formats, formatPlaceholder, start, end } = value; - const newFormats = formats.slice( 0 ); - let newFormatPlaceholder = null; - - if ( start === end ) { - if ( formatPlaceholder && formatPlaceholder.index === start ) { - const placeholderFormats = ( formatPlaceholder.formats || [] ).slice( 0 ); - newFormatPlaceholder = { - ...formatPlaceholder, - // make sure we do not reuse the formats reference in our placeholder `formats` array - formats: cloneDeep( placeholderFormats.filter( ( { type } ) => type !== formatType ) ), - }; - } else if ( ! formatPlaceholder ) { - const previousFormat = ( start > 0 ? formats[ start - 1 ] : formats[ 0 ] ) || []; - newFormatPlaceholder = { - index: start, - formats: cloneDeep( previousFormat.filter( ( { type } ) => type !== formatType ) ), - }; - } - } - - // Do not remove format if selection is empty - for ( let i = startIndex; i < endIndex; i++ ) { - if ( newFormats[ i ] ) { - filterFormats( newFormats, i, formatType ); - } - } - - return normaliseFormats( { ...value, formats: newFormats, formatPlaceholder: newFormatPlaceholder } ); -} - -function filterFormats( formats, index, formatType ) { - const newFormats = formats[ index ].filter( ( { type } ) => type !== formatType ); - - if ( newFormats.length ) { - formats[ index ] = newFormats; - } else { - delete formats[ index ]; - } -} From b0cf148389c9f0b32575dea61db341a0100101c0 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Thu, 2 May 2019 13:59:50 +0200 Subject: [PATCH 03/28] Update readme change postcommit --- packages/core-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-data/README.md b/packages/core-data/README.md index a2fb271017fcd..8b760483d950b 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -175,6 +175,7 @@ _Returns_ - `Object`: Updated record. + ## Selectors @@ -419,7 +420,6 @@ _Returns_ - `boolean`: Whether a request is in progress for an embed preview. -

Code is Poetry.

From 6951224301a3b8c52794748b3e317d08caf69d12 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Fri, 3 May 2019 20:51:21 +0200 Subject: [PATCH 04/28] Fix sequence of events on split and selection change --- .../src/components/rich-text/index.native.js | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 690d32474dd5e..efc13eed58278 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -310,20 +310,20 @@ export class RichText extends Component { */ onChange( event ) { this.lastEventCount = event.nativeEvent.eventCount; + this.firedAfterTextChanged = true; // the onChange event always fires after the fact this.onTextUpdate( event ); } onTextUpdate( event, refresh = false ) { const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); - if ( ! refresh ) { - this.value = contentWithoutRootTag; - } - + this.value = contentWithoutRootTag; this.comesFromAztec = true; - this.firedAfterTextChanged = true; // the onChange event always fires after the fact - this.props.onChange( contentWithoutRootTag ); + // we don't want to refresh if our goal is just to create a record + if ( refresh ) { + this.props.onChange( contentWithoutRootTag ); + } } /* @@ -340,7 +340,7 @@ export class RichText extends Component { this.comesFromAztec = true; this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged; - this.onTextUpdate( event ); + //this.onTextUpdate( event ); const currentRecord = this.createRecord(); if ( this.multilineTag ) { @@ -528,6 +528,10 @@ export class RichText extends Component { } onSelectionChangeFromAztec( start, end, text, event ) { + if ( ! this.props.isSelected ) { + return; + } + // update text before updating selection // Make sure there are changes made to the content before upgrading it upward this.onTextUpdate( event, true ); @@ -609,6 +613,7 @@ export class RichText extends Component { this.needsSelectionUpdate = true; this.selectionStart = start; this.selectionEnd = end; + this.forceUpdate(); } } @@ -619,7 +624,7 @@ export class RichText extends Component { return true; } - // TODO: Please re-introduce the check to avoid updating the content right after an `onChange` call. + // TODO: Please re-introduce the check to avoid updating the content right after an `onChange` call. // It was removed in https://github.com/WordPress/gutenberg/pull/12417 to fix undo/redo problem. // If the component is changed React side (undo/redo/merging/splitting/custom text actions) From 763a870be21e870419cbd8fb332e283d9691fcfa Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Fri, 3 May 2019 20:54:59 +0200 Subject: [PATCH 05/28] Remove commented onTextUpdate call on enter pressed --- packages/block-editor/src/components/rich-text/index.native.js | 1 - packages/core-data/README.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index efc13eed58278..028ab1c22c4e9 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -340,7 +340,6 @@ export class RichText extends Component { this.comesFromAztec = true; this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged; - //this.onTextUpdate( event ); const currentRecord = this.createRecord(); if ( this.multilineTag ) { diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 8b760483d950b..a2fb271017fcd 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -175,7 +175,6 @@ _Returns_ - `Object`: Updated record. - ## Selectors @@ -420,6 +419,7 @@ _Returns_ - `boolean`: Whether a request is in progress for an embed preview. +

Code is Poetry.

From ba8f4001081793220ce65646931a5f614fa582dd Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Mon, 6 May 2019 12:19:00 +0200 Subject: [PATCH 06/28] Do not reset activeFormats when onSelectionChange is emitted while typing --- .../src/components/rich-text/index.native.js | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 028ab1c22c4e9..2a89aac8e2ff7 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -516,12 +516,20 @@ export class RichText extends Component { } onSelectionChange( start, end ) { + // onSelectionChange should not be emitted when we're simply typing, but Aztec does emit it + // Let's try to detect typing with a simple hack: + const noChange = this.selectionStart === start && this.selectionEnd === end; + const isTyping = this.selectionStart + 1 === start; + const shouldKeepFormats = noChange || isTyping; + this.selectionStart = start; this.selectionEnd = end; - const value = this.createRecord(); - const activeFormats = getActiveFormats( value ); - this.setState( { activeFormats } ); + if ( ! shouldKeepFormats ) { + const value = this.createRecord(); + const activeFormats = getActiveFormats( value ); + this.setState( { activeFormats } ); + } this.props.onSelectionChange( start, end ); } @@ -531,6 +539,14 @@ export class RichText extends Component { return; } + // AztecEditor-Android may emit a selection change event with 0,0 + // when simply updating the active formats + // let's ignore this event in that case + const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); + if ( contentWithoutRootTag === this.value && start === 0 && end === 0 ) { + return; + } + // update text before updating selection // Make sure there are changes made to the content before upgrading it upward this.onTextUpdate( event, true ); From 0e1d233925d952258d179c26bd2e1b35097ef1a7 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Mon, 6 May 2019 15:17:15 +0200 Subject: [PATCH 07/28] Update value from props --- .../block-editor/src/components/rich-text/index.native.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 2a89aac8e2ff7..5f337a92072a3 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -681,6 +681,10 @@ export class RichText extends Component { } componentDidUpdate( prevProps ) { + if ( this.props.value !== this.value ) { + this.value = this.props.value; + } + if ( this.props.isSelected && ! prevProps.isSelected ) { this._editor.focus(); // Update selection props explicitly when component is selected as Aztec won't call onSelectionChange From 3bd6a25f0413ff821b1962481c342a478bd78405 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Mon, 6 May 2019 16:09:42 +0200 Subject: [PATCH 08/28] Pinpoint conditions for the case where onSelectionChange is emitted without changing selection --- .../src/components/rich-text/index.native.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 5f337a92072a3..28535bcfbf1e7 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -543,7 +543,7 @@ export class RichText extends Component { // when simply updating the active formats // let's ignore this event in that case const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); - if ( contentWithoutRootTag === this.value && start === 0 && end === 0 ) { + if ( this.lastEventCount === undefined && contentWithoutRootTag === this.value && start === 0 && end === 0 ) { return; } @@ -594,6 +594,11 @@ export class RichText extends Component { } componentWillReceiveProps( nextProps ) { + if ( nextProps.value !== this.value ) { + this.value = this.props.value; + this.lastEventCount = undefined; + } + this.moveCaretToTheEndIfNeeded( nextProps ); } @@ -681,10 +686,6 @@ export class RichText extends Component { } componentDidUpdate( prevProps ) { - if ( this.props.value !== this.value ) { - this.value = this.props.value; - } - if ( this.props.isSelected && ! prevProps.isSelected ) { this._editor.focus(); // Update selection props explicitly when component is selected as Aztec won't call onSelectionChange From b898e7bf2975f168962c98f433421b994f2c2f1d Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Tue, 7 May 2019 11:47:15 +0200 Subject: [PATCH 09/28] Improve isTyping detection --- packages/block-editor/src/components/rich-text/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 28535bcfbf1e7..75a6c3ccf66e0 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -519,7 +519,7 @@ export class RichText extends Component { // onSelectionChange should not be emitted when we're simply typing, but Aztec does emit it // Let's try to detect typing with a simple hack: const noChange = this.selectionStart === start && this.selectionEnd === end; - const isTyping = this.selectionStart + 1 === start; + const isTyping = this.selectionStart + 1 === start && this.value !== this.props.value; const shouldKeepFormats = noChange || isTyping; this.selectionStart = start; From d4651cb5981226487993433b5e410e27a4a71cc8 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Tue, 7 May 2019 14:19:46 +0200 Subject: [PATCH 10/28] Fix applying link format --- packages/format-library/src/link/modal.native.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/format-library/src/link/modal.native.js b/packages/format-library/src/link/modal.native.js index b11929c821b8b..294d3e9c371d3 100644 --- a/packages/format-library/src/link/modal.native.js +++ b/packages/format-library/src/link/modal.native.js @@ -82,18 +82,17 @@ class ModalLinkUI extends Component { opensInNewWindow, text: linkText, } ); - const placeholderFormats = ( value.formatPlaceholder && value.formatPlaceholder.formats ) || []; if ( isCollapsed( value ) && ! isActive ) { // insert link - const toInsert = applyFormat( create( { text: linkText } ), [ ...placeholderFormats, format ], 0, linkText.length ); + const toInsert = applyFormat( create( { text: linkText } ), format, 0, linkText.length ); const newAttributes = insert( value, toInsert ); onChange( { ...newAttributes, needsSelectionUpdate: true } ); } else if ( text !== getTextContent( slice( value ) ) ) { // edit text in selected link - const toInsert = applyFormat( create( { text } ), [ ...placeholderFormats, format ], 0, text.length ); + const toInsert = applyFormat( create( { text } ), format, 0, text.length ); const newAttributes = insert( value, toInsert, value.start, value.end ); onChange( { ...newAttributes, needsSelectionUpdate: true } ); } else { // transform selected text into link - const newAttributes = applyFormat( value, [ ...placeholderFormats, format ] ); + const newAttributes = applyFormat( value, format ); onChange( { ...newAttributes, needsSelectionUpdate: true } ); } From 1ad6132d0ea548ba959b74078b849573f55c96cc Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Wed, 8 May 2019 12:09:00 +0200 Subject: [PATCH 11/28] Do not try to update text from the paste handler --- .../block-editor/src/components/rich-text/index.native.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 75a6c3ccf66e0..e0e10eeced0f5 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -403,7 +403,6 @@ export class RichText extends Component { const { onSplit } = this.props; const { pastedText, pastedHtml, files } = event.nativeEvent; - this.onTextUpdate( event ); const currentRecord = this.createRecord(); event.preventDefault(); @@ -446,7 +445,6 @@ export class RichText extends Component { }, } ); this.value = this.valueToFormat( linkedRecord ); - //this.lastEventCount = undefined; this.props.onChange( this.value ); // Allows us to ask for this information when we get a report. @@ -644,7 +642,7 @@ export class RichText extends Component { return true; } - // TODO: Please re-introduce the check to avoid updating the content right after an `onChange` call. + // TODO: Please re-introduce the check to avoid updating the content right after an `onChange` call. // It was removed in https://github.com/WordPress/gutenberg/pull/12417 to fix undo/redo problem. // If the component is changed React side (undo/redo/merging/splitting/custom text actions) From 9b91cf13974a7748789d89c0e2ced51a80b9abda Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Thu, 9 May 2019 13:09:41 +0300 Subject: [PATCH 12/28] onChange should update the state --- packages/block-editor/src/components/rich-text/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index e0e10eeced0f5..22dcc5bd1a45f 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -311,7 +311,7 @@ export class RichText extends Component { onChange( event ) { this.lastEventCount = event.nativeEvent.eventCount; this.firedAfterTextChanged = true; // the onChange event always fires after the fact - this.onTextUpdate( event ); + this.onTextUpdate( event, true ); } onTextUpdate( event, refresh = false ) { From 77bb21979be5eae83e3ceddcea9be48e6b9c0158 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Thu, 9 May 2019 13:28:03 +0300 Subject: [PATCH 13/28] Selection change events always fire after text already changed --- packages/block-editor/src/components/rich-text/index.native.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 22dcc5bd1a45f..6732b3bd57705 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -545,6 +545,8 @@ export class RichText extends Component { return; } + this.firedAfterTextChanged = true; // Selection change event always fires after the fact + // update text before updating selection // Make sure there are changes made to the content before upgrading it upward this.onTextUpdate( event, true ); From cc12da722df30ae60a244c560fbf354fb017a88c Mon Sep 17 00:00:00 2001 From: etoledom Date: Thu, 9 May 2019 12:36:09 +0200 Subject: [PATCH 14/28] Fix issue where the list block was not being selected on focus. --- .../block-editor/src/components/rich-text/index.native.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 6732b3bd57705..e10e91e2c6a05 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -817,7 +817,12 @@ RichText.defaultProps = { const RichTextContainer = compose( [ withInstanceId, - withBlockEditContext( ( { clientId } ) => ( { clientId } ) ), + withBlockEditContext( ( { clientId, onFocus }, ownProps ) => { + return { + clientId: clientId, + onFocus: onFocus || ownProps.onFocus + } + }), withSelect( ( select, { clientId, instanceId, From 1421cddf7ea159053795430a6a0a9e6459e91d52 Mon Sep 17 00:00:00 2001 From: etoledom Date: Thu, 9 May 2019 13:44:16 +0200 Subject: [PATCH 15/28] Add isSelected prop from context to RichText --- packages/block-editor/src/components/rich-text/index.native.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index e10e91e2c6a05..0ecb6b11df2d0 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -817,9 +817,10 @@ RichText.defaultProps = { const RichTextContainer = compose( [ withInstanceId, - withBlockEditContext( ( { clientId, onFocus }, ownProps ) => { + withBlockEditContext( ( { clientId, onFocus, isSelected }, ownProps ) => { return { clientId: clientId, + isSelected: isSelected, onFocus: onFocus || ownProps.onFocus } }), From 86894eaded3c4b283033c7fa438dbaa4a6c6863b Mon Sep 17 00:00:00 2001 From: etoledom Date: Thu, 9 May 2019 22:18:35 +0200 Subject: [PATCH 16/28] Fix move caret to the end when merging. This avoid moving the caret to the end when two blocks, both with content, are merged. --- packages/block-editor/src/components/rich-text/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 0ecb6b11df2d0..fec41904c05d6 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -617,7 +617,7 @@ export class RichText extends Component { // This logic will handle the selection when two blocks are merged or when block is split // into two blocks - if ( nextTextContent.startsWith( this.savedContent ) && this.savedContent && this.savedContent.length > 0 ) { + if ( nextTextContent === this.savedContent && this.savedContent && this.savedContent.length > 0 ) { let length = this.savedContent.length; if ( length === 0 && nextTextContent !== this.props.value ) { length = this.props.value.length; From 2f67bebaf3ff2796b8dfe93109fd949afd2699bd Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Fri, 10 May 2019 15:04:08 +0300 Subject: [PATCH 17/28] Caret position from props, don't force to end --- .../src/components/rich-text/index.native.js | 38 +++---------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index fec41904c05d6..4fdc0acd14da7 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -347,7 +347,6 @@ export class RichText extends Component { const insertedLineBreak = { needsSelectionUpdate: true, ...insert( currentRecord, '\n' ) }; this.onFormatChangeForceChild( insertedLineBreak ); } else if ( this.onSplit && isEmptyLine( currentRecord ) ) { - this.needsSelectionUpdate = false; this.splitContent( currentRecord ); } else { const insertedLineSeparator = { needsSelectionUpdate: true, ...insertLineSeparator( currentRecord ) }; @@ -598,34 +597,6 @@ export class RichText extends Component { this.value = this.props.value; this.lastEventCount = undefined; } - - this.moveCaretToTheEndIfNeeded( nextProps ); - } - - moveCaretToTheEndIfNeeded( nextProps ) { - const nextRecord = this.formatToValue( nextProps.value ); - const nextTextContent = getTextContent( nextRecord ); - - if ( this.isTouched || ! nextProps.isSelected ) { - this.savedContent = nextTextContent; - return; - } - - if ( nextTextContent === '' && this.savedContent === '' ) { - return; - } - - // This logic will handle the selection when two blocks are merged or when block is split - // into two blocks - if ( nextTextContent === this.savedContent && this.savedContent && this.savedContent.length > 0 ) { - let length = this.savedContent.length; - if ( length === 0 && nextTextContent !== this.props.value ) { - length = this.props.value.length; - } - - this.forceSelectionUpdate( length, length ); - this.savedContent = nextTextContent; - } } forceSelectionUpdate( start, end ) { @@ -663,8 +634,11 @@ export class RichText extends Component { this.lastEventCount = undefined; // force a refresh on the native side } - if ( Platform.OS === 'android' && this.comesFromAztec === false ) { - if ( this.needsSelectionUpdate ) { + if ( ! this.comesFromAztec ) { + if ( nextProps.selectionStart !== this.props.selectionStart && + nextProps.selectionStart !== this.selectionStart && + nextProps.isSelected) { + this.needsSelectionUpdate = true; this.lastEventCount = undefined; // force a refresh on the native side } } @@ -743,7 +717,7 @@ export class RichText extends Component { // So, skip forcing it, let Aztec just do its best and just log the fact. console.warn( 'RichText value will be trimmed for spaces! Avoiding setting the caret position manually.' ); } else { - selection = { start: this.selectionStart, end: this.selectionEnd }; + selection = { start: this.props.selectionStart, end: this.props.selectionEnd }; } } From f7d0169b9d5b3b3107c517138a483c0b6418f5e1 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Sun, 12 May 2019 03:28:06 +0300 Subject: [PATCH 18/28] Fix lint errors --- .../src/components/rich-text/index.native.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 4fdc0acd14da7..2412c7f0f6e7c 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -26,7 +26,6 @@ import { __unstableInsertLineSeparator as insertLineSeparator, __unstableIsEmptyLine as isEmptyLine, isCollapsed, - getTextContent, } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { BACKSPACE } from '@wordpress/keycodes'; @@ -637,7 +636,7 @@ export class RichText extends Component { if ( ! this.comesFromAztec ) { if ( nextProps.selectionStart !== this.props.selectionStart && nextProps.selectionStart !== this.selectionStart && - nextProps.isSelected) { + nextProps.isSelected ) { this.needsSelectionUpdate = true; this.lastEventCount = undefined; // force a refresh on the native side } @@ -792,12 +791,12 @@ RichText.defaultProps = { const RichTextContainer = compose( [ withInstanceId, withBlockEditContext( ( { clientId, onFocus, isSelected }, ownProps ) => { - return { - clientId: clientId, - isSelected: isSelected, - onFocus: onFocus || ownProps.onFocus - } - }), + return { + clientId, + isSelected, + onFocus: onFocus || ownProps.onFocus, + }; + } ), withSelect( ( select, { clientId, instanceId, From e2b77fb477a9dbd41c0690dff4dcb3a5cd4351a9 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Sun, 12 May 2019 05:38:55 +0300 Subject: [PATCH 19/28] WIP: more fixes --- .../src/components/rich-text/index.native.js | 125 +++++++++++------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 2412c7f0f6e7c..892a9d250c10d 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -5,6 +5,9 @@ */ import RCTAztecView from 'react-native-aztec'; import { View, Platform } from 'react-native'; +import { + pickBy, +} from 'lodash'; import memize from 'memize'; /** @@ -98,6 +101,7 @@ export class RichText extends Component { } this.isIOS = Platform.OS === 'ios'; + this.createRecord = this.createRecord.bind( this ); this.onChange = this.onChange.bind( this ); this.onEnter = this.onEnter.bind( this ); this.onBackspace = this.onBackspace.bind( this ); @@ -127,6 +131,8 @@ export class RichText extends Component { this.savedContent = ''; this.isTouched = false; + this.lastHistoryValue = value; + // Internal values that are update synchronously, unlike props. this.value = value; this.selectionStart = selectionStart; @@ -253,40 +259,55 @@ export class RichText extends Component { } onFormatChange( record, doUpdateChild ) { - let newContent; - // valueToFormat might throw when converting the record to a tree structure - // let's ignore the event for now and force a render update so we're still in sync - try { - newContent = this.valueToFormat( record ); - } catch ( error ) { - // eslint-disable-next-line no-console - console.log( error ); - } - this.setState( { - activeFormats: record.activeFormats, + const { start, end, activeFormats = [] } = record; + const changeHandlers = pickBy( this.props, ( v, key ) => + key.startsWith( 'format_on_change_functions_' ) + ); + + Object.values( changeHandlers ).forEach( ( changeHandler ) => { + changeHandler( record.formats, record.text ); } ); - if ( newContent && newContent !== this.props.value ) { - this.props.onChange( newContent ); - if ( record.needsSelectionUpdate && record.start && record.end && doUpdateChild ) { - this.forceSelectionUpdate( record.start, record.end ); - } - } else { - if ( doUpdateChild ) { - this.lastEventCount = undefined; - } else { - // make sure the component rerenders without refreshing the text on gutenberg - // (this can trigger other events that might update the active formats on aztec) - this.lastEventCount = 0; - } - this.forceUpdate(); + + this.value = this.valueToFormat( record ); + this.props.onChange( this.value ); + this.setState( { activeFormats } ); + // this.props.onSelectionChange( start, end ); + this.selectionStart = start; + this.selectionEnd = end; + + this.onCreateUndoLevel(); + + // //////////// + + // let newContent = this.value; + // if ( newContent && newContent !== this.props.value ) { + // /// + // } else { + // if ( doUpdateChild ) { + // this.lastEventCount = undefined; + // } else { + // // make sure the component rerenders without refreshing the text on gutenberg + // // (this can trigger other events that might update the active formats on aztec) + // this.lastEventCount = 0; + // } + // this.forceUpdate(); + // } + } + + onCreateUndoLevel() { + // If the content is the same, no level needs to be created. + if ( this.lastHistoryValue === this.value ) { + return; } + + this.props.onCreateUndoLevel(); + this.lastHistoryValue = this.value; } /* * Cleans up any root tags produced by aztec. * TODO: This should be removed on a later version when aztec doesn't return the top tag of the text being edited */ - removeRootTagsProduceByAztec( html ) { let result = this.removeRootTag( this.props.tagName, html ); // Temporary workaround for https://github.com/WordPress/gutenberg/pull/13763 @@ -309,15 +330,16 @@ export class RichText extends Component { */ onChange( event ) { this.lastEventCount = event.nativeEvent.eventCount; + this.comesFromAztec = true; this.firedAfterTextChanged = true; // the onChange event always fires after the fact this.onTextUpdate( event, true ); } - onTextUpdate( event, refresh = false ) { + onTextUpdate( event ) { const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); + const refresh = this.value != contentWithoutRootTag; this.value = contentWithoutRootTag; - this.comesFromAztec = true; // we don't want to refresh if our goal is just to create a record if ( refresh ) { @@ -369,10 +391,10 @@ export class RichText extends Component { const keyCode = BACKSPACE; // TODO : should we differentiate BACKSPACE and DELETE? const isReverse = keyCode === BACKSPACE; - // Only process delete if the key press occurs at uncollapsed edge. - if ( ! isCollapsed( this.createRecord() ) ) { - return; - } + // // Only process delete if the key press occurs at uncollapsed edge. + // if ( ! isCollapsed( this.createRecord() ) ) { + // return; + // } const empty = this.isEmpty(); @@ -499,8 +521,10 @@ export class RichText extends Component { this.isTouched = true; if ( this.props.onFocus ) { - this.props.onFocus( event ); + this.props.onFocus(); } + + // this.onSelectionChange(); } onBlur( event ) { @@ -527,22 +551,25 @@ export class RichText extends Component { this.setState( { activeFormats } ); } - this.props.onSelectionChange( start, end ); + if ( this.props.isSelected ) { + this.props.onSelectionChange( start, end ); + } } onSelectionChangeFromAztec( start, end, text, event ) { - if ( ! this.props.isSelected ) { - return; - } - - // AztecEditor-Android may emit a selection change event with 0,0 - // when simply updating the active formats - // let's ignore this event in that case - const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); - if ( this.lastEventCount === undefined && contentWithoutRootTag === this.value && start === 0 && end === 0 ) { - return; - } + // if ( ! this.props.isSelected ) { + // return; + // } + + // // AztecEditor-Android may emit a selection change event with 0,0 + // // when simply updating the active formats + // // let's ignore this event in that case + // const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); + // if ( this.lastEventCount === undefined && contentWithoutRootTag === this.value && start === 0 && end === 0 ) { + // return; + // } + this.comesFromAztec = true; this.firedAfterTextChanged = true; // Selection change event always fires after the fact // update text before updating selection @@ -647,6 +674,7 @@ export class RichText extends Component { componentDidMount() { if ( this.props.isSelected ) { + console.log( `Will focus block on mount: ${this.props.clientId}` ); this._editor.focus(); this.onSelectionChange( this.props.selectionStart || 0, this.props.selectionEnd || 0 ); } @@ -660,6 +688,7 @@ export class RichText extends Component { componentDidUpdate( prevProps ) { if ( this.props.isSelected && ! prevProps.isSelected ) { + console.log( `Will focus block on update: ${this.props.clientId}` ); this._editor.focus(); // Update selection props explicitly when component is selected as Aztec won't call onSelectionChange // if its internal value hasn't change. When created, default value is 0, 0 @@ -817,6 +846,9 @@ const RichTextContainer = compose( [ selectionStart.clientId === clientId && selectionStart.attributeKey === identifier ); + console.log( `iSelected is undefined and will set it to ${isSelected} for ${clientId}` ); + } else { + console.log( `iSelected: ${isSelected} for ${clientId}` ); } return { @@ -832,11 +864,14 @@ const RichTextContainer = compose( [ identifier = instanceId, } ) => { const { + __unstableMarkLastChangeAsPersistent, selectionChange, } = dispatch( 'core/block-editor' ); return { + onCreateUndoLevel: __unstableMarkLastChangeAsPersistent, onSelectionChange( start, end ) { + debugger; selectionChange( clientId, identifier, start, end ); }, }; From 680aada2b026a08a5a1ed8c59b087050b724451b Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Sun, 12 May 2019 06:02:42 +0300 Subject: [PATCH 20/28] WIP: Fix list external Enter key on Android --- .../src/components/rich-text/index.native.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 892a9d250c10d..866b5191b5d0d 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -271,7 +271,7 @@ export class RichText extends Component { this.value = this.valueToFormat( record ); this.props.onChange( this.value ); this.setState( { activeFormats } ); - // this.props.onSelectionChange( start, end ); + this.props.onSelectionChange( start, end ); this.selectionStart = start; this.selectionEnd = end; @@ -365,16 +365,19 @@ export class RichText extends Component { if ( this.multilineTag ) { if ( event.shiftKey ) { - const insertedLineBreak = { needsSelectionUpdate: true, ...insert( currentRecord, '\n' ) }; + this.needsSelectionUpdate = true; + const insertedLineBreak = { ...insert( currentRecord, '\n' ) }; this.onFormatChangeForceChild( insertedLineBreak ); } else if ( this.onSplit && isEmptyLine( currentRecord ) ) { this.splitContent( currentRecord ); } else { - const insertedLineSeparator = { needsSelectionUpdate: true, ...insertLineSeparator( currentRecord ) }; + this.needsSelectionUpdate = true; + const insertedLineSeparator = { ...insertLineSeparator( currentRecord ) }; this.onFormatChange( insertedLineSeparator, ! this.firedAfterTextChanged ); } } else if ( event.shiftKey || ! this.onSplit ) { - const insertedLineBreak = { needsSelectionUpdate: true, ...insert( currentRecord, '\n' ) }; + this.needsSelectionUpdate = true; + const insertedLineBreak = { ...insert( currentRecord, '\n' ) }; this.onFormatChangeForceChild( insertedLineBreak ); } else { this.splitContent( currentRecord ); @@ -871,7 +874,6 @@ const RichTextContainer = compose( [ return { onCreateUndoLevel: __unstableMarkLastChangeAsPersistent, onSelectionChange( start, end ) { - debugger; selectionChange( clientId, identifier, start, end ); }, }; From 586f076d901d8e6b21d2d105117c64156d1141e7 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Mon, 13 May 2019 02:05:55 +0300 Subject: [PATCH 21/28] No stray onSelectionChanged events from AztecAndroid --- .../src/components/rich-text/index.native.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 866b5191b5d0d..67083ca47cab1 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -554,9 +554,7 @@ export class RichText extends Component { this.setState( { activeFormats } ); } - if ( this.props.isSelected ) { - this.props.onSelectionChange( start, end ); - } + this.props.onSelectionChange( start, end ); } onSelectionChangeFromAztec( start, end, text, event ) { @@ -572,6 +570,17 @@ export class RichText extends Component { // return; // } + // `end` can be less than `start` on iOS + // Let's fix that here so `rich-text/slice` can work properly + const realStart = Math.min( start, end ); + const realEnd = Math.max( start, end ); + + // check and dicsard stray event, where the text and selection is equal to the ones already cached + const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); + if ( contentWithoutRootTag === this.value && realStart === this.selectionStart && realEnd === this.selectionEnd ) { + return; + } + this.comesFromAztec = true; this.firedAfterTextChanged = true; // Selection change event always fires after the fact @@ -579,11 +588,6 @@ export class RichText extends Component { // Make sure there are changes made to the content before upgrading it upward this.onTextUpdate( event, true ); - // `end` can be less than `start` on iOS - // Let's fix that here so `rich-text/slice` can work properly - const realStart = Math.min( start, end ); - const realEnd = Math.max( start, end ); - this.onSelectionChange( realStart, realEnd ); // Update lastEventCount to prevent Aztec from re-rendering the content it just sent From 380660e7d751834ac0de4eb7f207d0c0f89fa863 Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Mon, 13 May 2019 03:34:24 +0300 Subject: [PATCH 22/28] Only block html elements will be trimmed by AztecAndroid --- .../block-editor/src/components/rich-text/index.native.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 67083ca47cab1..1cb2f42450cca 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -706,9 +706,10 @@ export class RichText extends Component { } willTrimSpaces( html ) { - // regex for detecting spaces around html tags - const trailingSpaces = /(\s+)<.+?>|<.+?>(\s+)/g; - const matches = html.match( trailingSpaces ); + // regex for detecting spaces around block element html tags + const blockHtmlElements = '(div|br|blockquote|ul|ol|li|p|pre|h1|h2|h3|h4|h5|h6|iframe|hr)'; + const leadingOrTrailingSpaces = new RegExp(`(\\s+)<\/?${blockHtmlElements}>|<\/?${blockHtmlElements}>(\\s+)`, 'g'); + const matches = html.match( leadingOrTrailingSpaces ); if ( matches && matches.length > 0 ) { return true; } From 1b29e883a9593416622e23f102def3789a605ffc Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Mon, 13 May 2019 17:05:02 +0300 Subject: [PATCH 23/28] Mirror caret state on text change --- .../block-editor/src/components/rich-text/index.native.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 1cb2f42450cca..b317846483885 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -664,6 +664,13 @@ export class RichText extends Component { ( typeof this.props.value !== 'undefined' ) && ( Platform.OS === 'ios' || ( Platform.OS === 'android' && ( ! this.comesFromAztec || ! this.firedAfterTextChanged ) ) ) && nextProps.value !== previousValueToCheck ) { + + // Gutenberg seems to try to mirror the caret state even on events that only change the content so, + // let's force caret update if state has selection set. + if ( typeof nextProps.selectionStart !== 'undefined' && typeof nextProps.selectionEnd !== 'undefined' ) { + this.needsSelectionUpdate = true; + } + this.lastEventCount = undefined; // force a refresh on the native side } From 8e812c7ca148e8a24ba4b0619dc772affe3d5282 Mon Sep 17 00:00:00 2001 From: etoledom Date: Mon, 13 May 2019 15:23:34 +0200 Subject: [PATCH 24/28] Fix issue on iOS where the format buttons were not working properly. Pressing on the buttons didn't change the format of the selected text. --- .../block-editor/src/components/rich-text/index.native.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index b317846483885..d230fc7243cee 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -654,16 +654,14 @@ export class RichText extends Component { // If the component is changed React side (undo/redo/merging/splitting/custom text actions) // we need to make sure the native is updated as well. - const previousValueToCheck = Platform.OS === 'android' ? this.props.value : this.value; - // Also, don't trust the "this.lastContent" as on Android, incomplete text events arrive // with only some of the text, while the virtual keyboard's suggestion system does its magic. // ** compare with this.lastContent for optimizing performance by not forcing Aztec with text it already has // , but compare with props.value to not lose "half word" text because of Android virtual keyb autosuggestion behavior if ( ( typeof nextProps.value !== 'undefined' ) && ( typeof this.props.value !== 'undefined' ) && - ( Platform.OS === 'ios' || ( Platform.OS === 'android' && ( ! this.comesFromAztec || ! this.firedAfterTextChanged ) ) ) && - nextProps.value !== previousValueToCheck ) { + ( ! this.comesFromAztec || ! this.firedAfterTextChanged ) && + nextProps.value !== this.props.value ) { // Gutenberg seems to try to mirror the caret state even on events that only change the content so, // let's force caret update if state has selection set. From 404173da21d6b7abb08666fe56ba1f301a8afa2c Mon Sep 17 00:00:00 2001 From: etoledom Date: Mon, 13 May 2019 19:03:57 +0200 Subject: [PATCH 25/28] Restore iOS autoscroll while editing. --- packages/block-editor/src/components/rich-text/index.native.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index d230fc7243cee..a0f03d882c3c0 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -832,11 +832,12 @@ RichText.defaultProps = { const RichTextContainer = compose( [ withInstanceId, - withBlockEditContext( ( { clientId, onFocus, isSelected }, ownProps ) => { + withBlockEditContext( ( { clientId, onFocus, isSelected, onCaretVerticalPositionChange }, ownProps ) => { return { clientId, isSelected, onFocus: onFocus || ownProps.onFocus, + onCaretVerticalPositionChange, }; } ), withSelect( ( select, { From 716c775fd6cc2684b627ce7ed19827584bc1817e Mon Sep 17 00:00:00 2001 From: Stefanos Togkoulidis Date: Tue, 14 May 2019 12:25:47 +0300 Subject: [PATCH 26/28] Cleanup --- .../src/components/rich-text/index.native.js | 49 ++----------------- 1 file changed, 4 insertions(+), 45 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index a0f03d882c3c0..01d540ed7dbf2 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -258,7 +258,7 @@ export class RichText extends Component { this.onFormatChange( record, true ); } - onFormatChange( record, doUpdateChild ) { + onFormatChange( record ) { const { start, end, activeFormats = [] } = record; const changeHandlers = pickBy( this.props, ( v, key ) => key.startsWith( 'format_on_change_functions_' ) @@ -276,22 +276,6 @@ export class RichText extends Component { this.selectionEnd = end; this.onCreateUndoLevel(); - - // //////////// - - // let newContent = this.value; - // if ( newContent && newContent !== this.props.value ) { - // /// - // } else { - // if ( doUpdateChild ) { - // this.lastEventCount = undefined; - // } else { - // // make sure the component rerenders without refreshing the text on gutenberg - // // (this can trigger other events that might update the active formats on aztec) - // this.lastEventCount = 0; - // } - // this.forceUpdate(); - // } } onCreateUndoLevel() { @@ -338,7 +322,7 @@ export class RichText extends Component { onTextUpdate( event ) { const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); - const refresh = this.value != contentWithoutRootTag; + const refresh = this.value !== contentWithoutRootTag; this.value = contentWithoutRootTag; // we don't want to refresh if our goal is just to create a record @@ -394,11 +378,6 @@ export class RichText extends Component { const keyCode = BACKSPACE; // TODO : should we differentiate BACKSPACE and DELETE? const isReverse = keyCode === BACKSPACE; - // // Only process delete if the key press occurs at uncollapsed edge. - // if ( ! isCollapsed( this.createRecord() ) ) { - // return; - // } - const empty = this.isEmpty(); if ( onMerge ) { @@ -520,14 +499,12 @@ export class RichText extends Component { } } - onFocus( event ) { + onFocus() { this.isTouched = true; if ( this.props.onFocus ) { this.props.onFocus(); } - - // this.onSelectionChange(); } onBlur( event ) { @@ -558,18 +535,6 @@ export class RichText extends Component { } onSelectionChangeFromAztec( start, end, text, event ) { - // if ( ! this.props.isSelected ) { - // return; - // } - - // // AztecEditor-Android may emit a selection change event with 0,0 - // // when simply updating the active formats - // // let's ignore this event in that case - // const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); - // if ( this.lastEventCount === undefined && contentWithoutRootTag === this.value && start === 0 && end === 0 ) { - // return; - // } - // `end` can be less than `start` on iOS // Let's fix that here so `rich-text/slice` can work properly const realStart = Math.min( start, end ); @@ -662,7 +627,6 @@ export class RichText extends Component { ( typeof this.props.value !== 'undefined' ) && ( ! this.comesFromAztec || ! this.firedAfterTextChanged ) && nextProps.value !== this.props.value ) { - // Gutenberg seems to try to mirror the caret state even on events that only change the content so, // let's force caret update if state has selection set. if ( typeof nextProps.selectionStart !== 'undefined' && typeof nextProps.selectionEnd !== 'undefined' ) { @@ -686,7 +650,6 @@ export class RichText extends Component { componentDidMount() { if ( this.props.isSelected ) { - console.log( `Will focus block on mount: ${this.props.clientId}` ); this._editor.focus(); this.onSelectionChange( this.props.selectionStart || 0, this.props.selectionEnd || 0 ); } @@ -700,7 +663,6 @@ export class RichText extends Component { componentDidUpdate( prevProps ) { if ( this.props.isSelected && ! prevProps.isSelected ) { - console.log( `Will focus block on update: ${this.props.clientId}` ); this._editor.focus(); // Update selection props explicitly when component is selected as Aztec won't call onSelectionChange // if its internal value hasn't change. When created, default value is 0, 0 @@ -713,7 +675,7 @@ export class RichText extends Component { willTrimSpaces( html ) { // regex for detecting spaces around block element html tags const blockHtmlElements = '(div|br|blockquote|ul|ol|li|p|pre|h1|h2|h3|h4|h5|h6|iframe|hr)'; - const leadingOrTrailingSpaces = new RegExp(`(\\s+)<\/?${blockHtmlElements}>|<\/?${blockHtmlElements}>(\\s+)`, 'g'); + const leadingOrTrailingSpaces = new RegExp( `(\\s+)<\/?${ blockHtmlElements }>|<\/?${ blockHtmlElements }>(\\s+)`, 'g' ); const matches = html.match( leadingOrTrailingSpaces ); if ( matches && matches.length > 0 ) { return true; @@ -860,9 +822,6 @@ const RichTextContainer = compose( [ selectionStart.clientId === clientId && selectionStart.attributeKey === identifier ); - console.log( `iSelected is undefined and will set it to ${isSelected} for ${clientId}` ); - } else { - console.log( `iSelected: ${isSelected} for ${clientId}` ); } return { From 9df1bb3d97ffac13b576686d63c57675c12c0138 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Tue, 14 May 2019 17:32:34 +0200 Subject: [PATCH 27/28] Try a new approach to detect if the selection change event is manual or not --- .../src/components/rich-text/index.native.js | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 01d540ed7dbf2..702464c00f1c7 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -130,6 +130,7 @@ export class RichText extends Component { this.needsSelectionUpdate = false; this.savedContent = ''; this.isTouched = false; + this.lastAztecEventType = null; this.lastHistoryValue = value; @@ -276,6 +277,8 @@ export class RichText extends Component { this.selectionEnd = end; this.onCreateUndoLevel(); + + this.lastAztecEventType = 'format change'; } onCreateUndoLevel() { @@ -316,7 +319,8 @@ export class RichText extends Component { this.lastEventCount = event.nativeEvent.eventCount; this.comesFromAztec = true; this.firedAfterTextChanged = true; // the onChange event always fires after the fact - this.onTextUpdate( event, true ); + this.onTextUpdate( event ); + this.lastAztecEventType = 'input'; } onTextUpdate( event ) { @@ -337,6 +341,7 @@ export class RichText extends Component { onContentSizeChange( contentSize ) { const contentHeight = contentSize.height; this.setState( { height: contentHeight } ); + this.lastAztecEventType = 'content size change'; } // eslint-disable-next-line no-unused-vars @@ -366,6 +371,7 @@ export class RichText extends Component { } else { this.splitContent( currentRecord ); } + this.lastAztecEventType = 'input'; } // eslint-disable-next-line no-unused-vars @@ -393,6 +399,7 @@ export class RichText extends Component { } event.preventDefault(); + this.lastAztecEventType = 'input'; } /** @@ -505,6 +512,8 @@ export class RichText extends Component { if ( this.props.onFocus ) { this.props.onFocus(); } + + this.lastAztecEventType = 'focus'; } onBlur( event ) { @@ -513,19 +522,22 @@ export class RichText extends Component { if ( this.props.onBlur ) { this.props.onBlur( event ); } + + this.lastAztecEventType = 'blur'; } onSelectionChange( start, end ) { - // onSelectionChange should not be emitted when we're simply typing, but Aztec does emit it - // Let's try to detect typing with a simple hack: - const noChange = this.selectionStart === start && this.selectionEnd === end; - const isTyping = this.selectionStart + 1 === start && this.value !== this.props.value; - const shouldKeepFormats = noChange || isTyping; + const hasChanged = this.selectionStart !== start || this.selectionEnd !== end; this.selectionStart = start; this.selectionEnd = end; - if ( ! shouldKeepFormats ) { + // This is a manual selection change event if onChange was not triggered just before + // and we did not just trigger a text update + // `onChange` could be the last event and could have been triggered a long time ago so + // this approach is not perfectly reliable + const isManual = this.lastAztecEventType !== 'input' && this.props.value === this.value; + if ( hasChanged && isManual ) { const value = this.createRecord(); const activeFormats = getActiveFormats( value ); this.setState( { activeFormats } ); @@ -551,12 +563,14 @@ export class RichText extends Component { // update text before updating selection // Make sure there are changes made to the content before upgrading it upward - this.onTextUpdate( event, true ); + this.onTextUpdate( event ); this.onSelectionChange( realStart, realEnd ); // Update lastEventCount to prevent Aztec from re-rendering the content it just sent this.lastEventCount = event.nativeEvent.eventCount; + + this.lastAztecEventType = 'selection change'; } isEmpty() { From 50527d3120000b5fe874af6ac5fb1826edcaef62 Mon Sep 17 00:00:00 2001 From: etoledom Date: Wed, 15 May 2019 08:32:43 +0200 Subject: [PATCH 28/28] Fix iOS issue with formats, where pressing the format buttons sometimes won't change the format of the selected text --- .../block-editor/src/components/rich-text/index.native.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 702464c00f1c7..007a612c94826 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -316,6 +316,11 @@ export class RichText extends Component { * Handles any case where the content of the AztecRN instance has changed */ onChange( event ) { + const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); + // On iOS, onChange can be triggered after selection changes, even though there are no content changes. + if ( contentWithoutRootTag === this.value ) { + return; + } this.lastEventCount = event.nativeEvent.eventCount; this.comesFromAztec = true; this.firedAfterTextChanged = true; // the onChange event always fires after the fact