Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RichText: React tree instead of DOM tree #13838

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions packages/block-editor/src/components/rich-text/aria.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,12 @@
*/

import {
difference,
isEqual,
isNil,
keys,
pickBy,
startsWith,
} from 'lodash';

const isAriaPropName = ( name ) =>
startsWith( name, 'aria-' );

export const pickAriaProps = ( props ) =>
pickBy( props, ( value, key ) => isAriaPropName( key ) && ! isNil( value ) );

export const diffAriaProps = ( props, nextProps ) => {
const prevAriaKeys = keys( pickAriaProps( props ) );
const nextAriaKeys = keys( pickAriaProps( nextProps ) );
const removedKeys = difference( prevAriaKeys, nextAriaKeys );
const updatedKeys = nextAriaKeys.filter( ( key ) =>
! isEqual( props[ key ], nextProps[ key ] ) );
return { removedKeys, updatedKeys };
};
pickBy( props, ( value, key ) =>
startsWith( key, 'aria-' ) && ! isNil( value )
);
64 changes: 17 additions & 47 deletions packages/block-editor/src/components/rich-text/editable.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
/**
* External dependencies
*/
import { isEqual } from 'lodash';
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { Component, createElement } from '@wordpress/element';
import { BACKSPACE, DELETE } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import { diffAriaProps } from './aria';
import { toElement, applySelection } from '@wordpress/rich-text';

/**
* Browser dependencies
Expand Down Expand Up @@ -98,43 +93,6 @@ export default class Editable extends Component {
this.bindEditorNode = this.bindEditorNode.bind( this );
}

// We must prevent rerenders because the browser will modify the DOM. React
// will rerender the DOM fine, but we're losing selection and it would be
// more expensive to do so as it would just set the inner HTML through
// `dangerouslySetInnerHTML`. Instead RichText does it's own diffing and
// selection setting.
//
// Because we never update the component, we have to look through props and
// update the attributes on the wrapper nodes here. `componentDidUpdate`
// will never be called.
shouldComponentUpdate( nextProps ) {
this.configureIsPlaceholderVisible( nextProps.isPlaceholderVisible );

if ( ! isEqual( this.props.style, nextProps.style ) ) {
this.editorNode.setAttribute( 'style', '' );
Object.assign( this.editorNode.style, nextProps.style );
}

if ( ! isEqual( this.props.className, nextProps.className ) ) {
this.editorNode.className = classnames( nextProps.className, CLASS_NAME );
}

const { removedKeys, updatedKeys } = diffAriaProps( this.props, nextProps );
removedKeys.forEach( ( key ) =>
this.editorNode.removeAttribute( key ) );
updatedKeys.forEach( ( key ) =>
this.editorNode.setAttribute( key, nextProps[ key ] ) );

return false;
}

configureIsPlaceholderVisible( isPlaceholderVisible ) {
const isPlaceholderVisibleString = String( !! isPlaceholderVisible );
if ( this.editorNode.getAttribute( IS_PLACEHOLDER_VISIBLE_ATTR_NAME ) !== isPlaceholderVisibleString ) {
this.editorNode.setAttribute( IS_PLACEHOLDER_VISIBLE_ATTR_NAME, isPlaceholderVisibleString );
}
}

bindEditorNode( editorNode ) {
this.editorNode = editorNode;
this.props.setRef( editorNode );
Expand All @@ -150,19 +108,32 @@ export default class Editable extends Component {
}
}

componentDidUpdate() {
if ( this.selection && this.selection.startPath.length > 0 ) {
applySelection( this.selection, this.editorNode );
}
}

render() {
const {
tagName = 'div',
style,
record,
valueToEditableHTML,
value,
className,
isPlaceholderVisible,
...remainingProps
} = this.props;

delete remainingProps.setRef;

const { element, selection } = toElement( {
value,
multilineTag: this.props.multilineTag,
multilineWrapperTags: this.props.multilineWrapperTags,
} );

this.selection = selection;

return createElement( tagName, {
role: 'textbox',
'aria-multiline': true,
Expand All @@ -172,8 +143,7 @@ export default class Editable extends Component {
ref: this.bindEditorNode,
style,
suppressContentEditableWarning: true,
dangerouslySetInnerHTML: { __html: valueToEditableHTML( record ) },
...remainingProps,
} );
}, element );
}
}
109 changes: 8 additions & 101 deletions packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import classnames from 'classnames';
import {
find,
isNil,
isEqual,
omit,
pickBy,
get,
isPlainObject,
} from 'lodash';
import memize from 'memize';

Expand All @@ -27,7 +25,6 @@ import { isURL } from '@wordpress/url';
import {
isEmpty,
create,
apply,
applyFormat,
split,
toHTMLString,
Expand All @@ -36,7 +33,6 @@ import {
insertLineBreak,
insertLineSeparator,
isEmptyLine,
unstableToDom,
remove,
removeFormat,
isCollapsed,
Expand Down Expand Up @@ -118,11 +114,9 @@ export class RichText extends Component {
this.onSelectionChange = this.onSelectionChange.bind( this );
this.getRecord = this.getRecord.bind( this );
this.createRecord = this.createRecord.bind( this );
this.applyRecord = this.applyRecord.bind( this );
this.isEmpty = this.isEmpty.bind( this );
this.valueToFormat = this.valueToFormat.bind( this );
this.setRef = this.setRef.bind( this );
this.valueToEditableHTML = this.valueToEditableHTML.bind( this );
this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( this );
this.onPointerDown = this.onPointerDown.bind( this );

Expand Down Expand Up @@ -195,17 +189,6 @@ export class RichText extends Component {
} );
}

applyRecord( record, { domOnly } = {} ) {
apply( {
value: record,
current: this.editableRef,
multilineTag: this.multilineTag,
multilineWrapperTags: this.multilineWrapperTags,
prepareEditableTree: this.props.prepareEditableTree,
__unstableDomOnly: domOnly,
} );
}

isEmpty() {
return isEmpty( this.formatToValue( this.props.value ) );
}
Expand Down Expand Up @@ -359,6 +342,7 @@ export class RichText extends Component {
}

onBlur() {
this.setState( { start: undefined, end: undefined, selectedFormat: undefined } );
document.removeEventListener( 'selectionchange', this.onSelectionChange );
}

Expand Down Expand Up @@ -481,7 +465,6 @@ export class RichText extends Component {
}

this.setState( { start, end, selectedFormat } );
this.applyRecord( { ...value, selectedFormat }, { domOnly: true } );

delete this.formatPlaceholder;
}
Expand Down Expand Up @@ -509,8 +492,6 @@ export class RichText extends Component {
* created.
*/
onChange( record, { withoutHistory } = {} ) {
this.applyRecord( record );

const { start, end, formatPlaceholder, selectedFormat } = record;

this.formatPlaceholder = formatPlaceholder;
Expand Down Expand Up @@ -794,20 +775,13 @@ export class RichText extends Component {
}

if ( newSelectedFormat !== selectedFormat ) {
this.applyRecord( { ...value, selectedFormat: newSelectedFormat } );
this.setState( { selectedFormat: newSelectedFormat } );
return;
}

const newPos = value.start + ( isReverse ? -1 : 1 );

this.setState( { start: newPos, end: newPos } );
this.applyRecord( {
...value,
start: newPos,
end: newPos,
selectedFormat: isReverse ? formatsBefore.length : formatsAfter.length,
} );
}

/**
Expand Down Expand Up @@ -883,67 +857,6 @@ export class RichText extends Component {
selection.addRange( range );
}

componentDidUpdate( prevProps ) {
const { tagName, value, isSelected } = this.props;

if (
tagName === prevProps.tagName &&
value !== prevProps.value &&
value !== this.savedContent
) {
// Handle deprecated `children` and `node` sources.
// The old way of passing a value with the `node` matcher required
// the value to be mapped first, creating a new array each time, so
// a shallow check wouldn't work. We need to check deep equality.
// This is only executed for a deprecated API and will eventually be
// removed.
if ( Array.isArray( value ) && isEqual( value, this.savedContent ) ) {
return;
}

const record = this.formatToValue( value );

if ( isSelected ) {
const prevRecord = this.formatToValue( prevProps.value );
const length = getTextContent( prevRecord ).length;
record.start = length;
record.end = length;
}

this.applyRecord( record );
this.savedContent = value;
}

// If any format props update, reapply value.
const shouldReapply = Object.keys( this.props ).some( ( name ) => {
if ( name.indexOf( 'format_' ) !== 0 ) {
return false;
}

// Allow primitives and arrays:
if ( ! isPlainObject( this.props[ name ] ) ) {
return this.props[ name ] !== prevProps[ name ];
}

return Object.keys( this.props[ name ] ).some( ( subName ) => {
return this.props[ name ][ subName ] !== prevProps[ name ][ subName ];
} );
} );

if ( shouldReapply ) {
const record = this.formatToValue( value );

// Maintain the previous selection if the instance is currently
// selected.
if ( isSelected ) {
record.start = this.state.start;
record.end = this.state.end;
}

this.applyRecord( record );
}
}

/**
* Get props that are provided by formats to modify RichText.
*
Expand Down Expand Up @@ -986,14 +899,6 @@ export class RichText extends Component {
return value;
}

valueToEditableHTML( value ) {
return unstableToDom( {
value,
multilineTag: this.multilineTag,
prepareEditableTree: this.props.prepareEditableTree,
} ).body.innerHTML;
}

/**
* Removes editor only formats from the value.
*
Expand Down Expand Up @@ -1025,11 +930,14 @@ export class RichText extends Component {

// Handle deprecated `children` and `node` sources.
if ( this.usedDeprecatedChildrenSource ) {
return children.fromDOM( unstableToDom( {
const { body } = document.implementation.createHTMLDocument( '' );

body.innerHTML = toHTMLString( {
value,
multilineTag: this.multilineTag,
isEditableTree: false,
} ).body.childNodes );
} );

return children.fromDOM( body.childNodes );
}

if ( this.props.format === 'string' ) {
Expand Down Expand Up @@ -1100,8 +1008,7 @@ export class RichText extends Component {
<Editable
tagName={ Tagname }
style={ style }
record={ record }
valueToEditableHTML={ this.valueToEditableHTML }
value={ record }
isPlaceholderVisible={ isPlaceholderVisible }
aria-label={ placeholder }
aria-autocomplete="list"
Expand Down
Loading