From 3b8159131dfcff7cd1bf22c625226588b5ad0fa3 Mon Sep 17 00:00:00 2001 From: Mapk Date: Thu, 7 Mar 2019 20:58:10 -0800 Subject: [PATCH 01/33] Text change for the block change tooltip --- packages/block-editor/src/components/block-switcher/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 25ef98edf8230..86d1bfcbcf922 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -95,7 +95,7 @@ export class BlockSwitcher extends Component { }; const label = ( 1 === blocks.length ? - __( 'Change block type' ) : + __( 'Change block type or style' ) : sprintf( _n( 'Change type of %d block', From 186b672ac8bba85ac113158019cf8722d821867c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Wed, 13 Mar 2019 12:55:28 +0100 Subject: [PATCH 02/33] RichText: change value to have separate keys for line and object formats (#13948) * Add objects and lineFormats * Update RichText * Fix image toolbar * Update format placeholder * lineFormat => lines * concatPair => mergePair * Update selectedFormat checks * Add some extra info to create docs * Move create docs inline * Merge lines and objects * Fix typos * Add getActiveObject unit tests * Update docs * Rebase * Adjust unstableToDom arguments * Remove normaliseFormats from list functions * Update native files * Update native file --- .../src/components/rich-text/format-edit.js | 13 +- .../src/components/rich-text/index.js | 24 +-- .../src/components/rich-text/index.native.js | 15 +- packages/format-library/src/image/index.js | 21 ++- packages/rich-text/README.md | 65 ++++++-- packages/rich-text/src/apply-format.js | 18 +-- packages/rich-text/src/apply-format.native.js | 14 +- packages/rich-text/src/change-list-type.js | 25 ++-- packages/rich-text/src/concat.js | 23 ++- packages/rich-text/src/create.js | 140 ++++++++---------- packages/rich-text/src/get-active-object.js | 20 +++ .../rich-text/src/get-last-child-index.js | 6 +- .../rich-text/src/get-parent-line-index.js | 6 +- packages/rich-text/src/indent-list-items.js | 29 ++-- packages/rich-text/src/index.js | 1 + .../rich-text/src/insert-line-separator.js | 9 +- packages/rich-text/src/insert-object.js | 6 +- packages/rich-text/src/insert.js | 9 +- packages/rich-text/src/join.js | 5 +- packages/rich-text/src/normalise-formats.js | 8 +- .../rich-text/src/normalise-formats.native.js | 36 ----- packages/rich-text/src/outdent-list-items.js | 19 +-- packages/rich-text/src/remove-format.js | 38 +++-- .../rich-text/src/remove-format.native.js | 9 +- packages/rich-text/src/replace.js | 8 +- packages/rich-text/src/slice.js | 11 +- packages/rich-text/src/split.js | 7 +- .../src/test/__snapshots__/to-dom.js.snap | 11 +- packages/rich-text/src/test/apply-format.js | 2 +- .../rich-text/src/test/change-list-type.js | 24 ++- packages/rich-text/src/test/concat.js | 3 + packages/rich-text/src/test/create.js | 1 + .../rich-text/src/test/get-active-object.js | 41 +++++ .../src/test/get-last-child-index.js | 12 +- .../src/test/get-parent-line-index.js | 8 +- packages/rich-text/src/test/helpers/index.js | 61 ++++++-- .../rich-text/src/test/indent-list-items.js | 74 ++++----- .../src/test/insert-line-separator.js | 20 ++- packages/rich-text/src/test/insert-object.js | 7 +- packages/rich-text/src/test/insert.js | 6 + packages/rich-text/src/test/join.js | 6 +- .../rich-text/src/test/outdent-list-items.js | 64 ++++---- packages/rich-text/src/test/replace.js | 7 + packages/rich-text/src/test/slice.js | 4 + packages/rich-text/src/test/split.js | 22 +++ packages/rich-text/src/test/to-dom.js | 2 - packages/rich-text/src/to-dom.js | 4 - packages/rich-text/src/to-html-string.js | 5 +- packages/rich-text/src/to-tree.js | 54 ++++--- packages/rich-text/src/toggle-format.js | 5 +- 50 files changed, 583 insertions(+), 445 deletions(-) create mode 100644 packages/rich-text/src/get-active-object.js delete mode 100644 packages/rich-text/src/normalise-formats.native.js create mode 100644 packages/rich-text/src/test/get-active-object.js diff --git a/packages/block-editor/src/components/rich-text/format-edit.js b/packages/block-editor/src/components/rich-text/format-edit.js index 7505ed1f81a3c..29911206aba83 100644 --- a/packages/block-editor/src/components/rich-text/format-edit.js +++ b/packages/block-editor/src/components/rich-text/format-edit.js @@ -3,7 +3,7 @@ */ import { withSelect } from '@wordpress/data'; import { Fragment } from '@wordpress/element'; -import { getActiveFormat } from '@wordpress/rich-text'; +import { getActiveFormat, getActiveObject } from '@wordpress/rich-text'; const FormatEdit = ( { formatTypes, onChange, value } ) => { return ( @@ -15,13 +15,20 @@ const FormatEdit = ( { formatTypes, onChange, value } ) => { const activeFormat = getActiveFormat( value, name ); const isActive = activeFormat !== undefined; - const activeAttributes = isActive ? activeFormat.attributes || {} : {}; + const activeObject = getActiveObject( value ); + const isObjectActive = activeObject !== undefined; return ( diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 5b91674b9a673..1d714ba9844d3 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -177,10 +177,10 @@ export class RichText extends Component { * @return {Object} The current record (value and selection). */ getRecord() { - const { formats, text } = this.formatToValue( this.props.value ); + const { formats, replacements, text } = this.formatToValue( this.props.value ); const { start, end, selectedFormat } = this.state; - return { formats, text, start, end, selectedFormat }; + return { formats, replacements, text, start, end, selectedFormat }; } createRecord() { @@ -394,13 +394,17 @@ export class RichText extends Component { } let { selectedFormat } = this.state; - const { formats, text, start, end } = this.createRecord(); + const { formats, replacements, text, start, end } = this.createRecord(); if ( this.formatPlaceholder ) { - formats[ this.state.start ] = formats[ this.state.start ] || []; - formats[ this.state.start ].push( this.formatPlaceholder ); - selectedFormat = formats[ this.state.start ].length; - } else if ( selectedFormat ) { + selectedFormat = this.formatPlaceholder.length; + + if ( selectedFormat > 0 ) { + formats[ this.state.start ] = this.formatPlaceholder; + } else { + delete formats[ this.state.start ]; + } + } else if ( selectedFormat > 0 ) { const formatsBefore = formats[ start - 1 ] || []; const formatsAfter = formats[ start ] || []; @@ -411,12 +415,13 @@ export class RichText extends Component { } source = source.slice( 0, selectedFormat ); + formats[ this.state.start ] = source; } else { delete formats[ this.state.start ]; } - const change = { formats, text, start, end, selectedFormat }; + const change = { formats, replacements, text, start, end, selectedFormat }; this.onChange( change, { withoutHistory: true, @@ -936,7 +941,6 @@ export class RichText extends Component { return unstableToDom( { value, multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, prepareEditableTree: this.props.prepareEditableTree, } ).body.innerHTML; } @@ -975,7 +979,6 @@ export class RichText extends Component { return children.fromDOM( unstableToDom( { value, multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, isEditableTree: false, } ).body.childNodes ); } @@ -984,7 +987,6 @@ export class RichText extends Component { return toHTMLString( { value, multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, } ); } 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 55bf4580e5e70..db750a18d41f2 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -98,9 +98,9 @@ export class RichText extends Component { const { formatPlaceholder, start, end } = this.state; // 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. - const { formats, text } = this.formatToValue( this.props.value === undefined ? undefined : this.props.value.trimLeft() ); + const { formats, replacements, text } = this.formatToValue( this.props.value === undefined ? undefined : this.props.value.trimLeft() ); - return { formats, formatPlaceholder, text, start, end }; + return { formats, replacements, formatPlaceholder, text, start, end }; } /* @@ -156,13 +156,12 @@ export class RichText extends Component { onSplit( before, after, ...blocks ); } - valueToFormat( { formats, text } ) { - const value = toHTMLString( { - value: { formats, text }, - multilineTag: this.multilineTag, - } ); + valueToFormat( value ) { // remove the outer root tags - return this.removeRootTagsProduceByAztec( value ); + return this.removeRootTagsProduceByAztec( toHTMLString( { + value, + multilineTag: this.multilineTag, + } ) ); } getActiveFormatNames( record ) { diff --git a/packages/format-library/src/image/index.js b/packages/format-library/src/image/index.js index 2dca1c41506b6..ce62a8b6c5a75 100644 --- a/packages/format-library/src/image/index.js +++ b/packages/format-library/src/image/index.js @@ -40,7 +40,7 @@ export const image = { } static getDerivedStateFromProps( props, state ) { - const { activeAttributes: { style } } = props; + const { activeObjectAttributes: { style } } = props; if ( style === state.previousStyle ) { return null; @@ -79,8 +79,8 @@ export const image = { } render() { - const { value, onChange, isActive, activeAttributes } = this.props; - const { style } = activeAttributes; + const { value, onChange, isObjectActive, activeObjectAttributes } = this.props; + const { style } = activeObjectAttributes; // Rerender PositionedAtSelection when the selection changes or when // the width changes. const key = value.start + style; @@ -91,7 +91,7 @@ export const image = { icon={ } title={ __( 'Inline Image' ) } onClick={ this.openModal } - isActive={ isActive } + isActive={ isObjectActive } /> { this.state.modal && } - { isActive && + { isObjectActive && { - const newFormats = value.formats.slice( 0 ); + const newReplacements = value.replacements.slice(); - newFormats[ value.start ] = [ { + newReplacements[ value.start ] = { type: name, - object: true, attributes: { - ...activeAttributes, + ...activeObjectAttributes, style: `width: ${ this.state.width }px;`, }, - } ]; + }; onChange( { ...value, - formats: newFormats, + replacements: newReplacements, } ); event.preventDefault(); diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 0ac85d85b640d..90fe67367ddc8 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -61,6 +61,26 @@ called without any input, an empty value will be created. If `multilineTag` will be separated by two newlines. The optional functions can be used to filter out content. +A value will have the following shape, which you are strongly encouraged not +to modify without the use of helper functions: + +```js +{ + text: string, + formats: Array, + replacements: Array, + ?start: number, + ?end: number, +} +``` + +As you can see, text and formatting are separated. `text` holds the text, +including any replacement characters for objects and lines. `formats`, +`objects` and `lines` are all sparse arrays of the same length as `text`. It +holds information about the formatting at the relevant text indices. Finally +`start` and `end` state which text indices are selected. They are only +provided if a `Range` was given. + **Parameters** - **$1** `[Object]`: Optional named arguments. @@ -93,9 +113,23 @@ is no format at the selection. `(Object|undefined)`: Active format object of the specified type, or undefined. +### getActiveObject + +[src/index.js#L11-L11](src/index.js#L11-L11) + +Gets the active object, if there is any. + +**Parameters** + +- **value** `Object`: Value to inspect. + +**Returns** + +`?Object`: Active object, or undefined. + ### getTextContent -[src/index.js#L13-L13](src/index.js#L13-L13) +[src/index.js#L14-L14](src/index.js#L14-L14) Get the textual content of a Rich Text value. This is similar to `Element.textContent`. @@ -110,7 +144,7 @@ Get the textual content of a Rich Text value. This is similar to ### insert -[src/index.js#L21-L21](src/index.js#L21-L21) +[src/index.js#L22-L22](src/index.js#L22-L22) Insert a Rich Text value, an HTML string, or a plain text string, into a Rich Text value at the given `startIndex`. Any content between `startIndex` @@ -130,7 +164,7 @@ none are provided. ### insertObject -[src/index.js#L24-L24](src/index.js#L24-L24) +[src/index.js#L25-L25](src/index.js#L25-L25) Insert a format as an object into a Rich Text value at the given `startIndex`. Any content between `startIndex` and `endIndex` will be @@ -149,7 +183,7 @@ removed. Indices are retrieved from the selection if none are provided. ### isCollapsed -[src/index.js#L14-L14](src/index.js#L14-L14) +[src/index.js#L15-L15](src/index.js#L15-L15) Check if the selection of a Rich Text value is collapsed or not. Collapsed means that no characters are selected, but there is a caret present. If there @@ -166,7 +200,7 @@ is no selection, `undefined` will be returned. This is similar to ### isEmpty -[src/index.js#L15-L15](src/index.js#L15-L15) +[src/index.js#L16-L16](src/index.js#L16-L16) Check if a Rich Text value is Empty, meaning it contains no text or any objects (such as images). @@ -181,7 +215,7 @@ objects (such as images). ### join -[src/index.js#L16-L16](src/index.js#L16-L16) +[src/index.js#L17-L17](src/index.js#L17-L17) Combine an array of Rich Text values into one, optionally separated by `separator`, which can be a Rich Text value, HTML string, or plain text @@ -198,7 +232,7 @@ string. This is similar to `Array.prototype.join`. ### registerFormatType -[src/index.js#L17-L17](src/index.js#L17-L17) +[src/index.js#L18-L18](src/index.js#L18-L18) Registers a new format provided a unique name and an object defining its behavior. @@ -218,7 +252,7 @@ behavior. ### remove -[src/index.js#L19-L19](src/index.js#L19-L19) +[src/index.js#L20-L20](src/index.js#L20-L20) Remove content from a Rich Text value between the given `startIndex` and `endIndex`. Indices are retrieved from the selection if none are provided. @@ -235,7 +269,7 @@ Remove content from a Rich Text value between the given `startIndex` and ### removeFormat -[src/index.js#L18-L18](src/index.js#L18-L18) +[src/index.js#L19-L19](src/index.js#L19-L19) Remove any format object from a Rich Text value by type from the given `startIndex` to the given `endIndex`. Indices are retrieved from the @@ -254,7 +288,7 @@ selection if none are provided. ### replace -[src/index.js#L20-L20](src/index.js#L20-L20) +[src/index.js#L21-L21](src/index.js#L21-L21) Search a Rich Text value and replace the match(es) with `replacement`. This is similar to `String.prototype.replace`. @@ -271,7 +305,7 @@ is similar to `String.prototype.replace`. ### slice -[src/index.js#L25-L25](src/index.js#L25-L25) +[src/index.js#L26-L26](src/index.js#L26-L26) Slice a Rich Text value from `startIndex` to `endIndex`. Indices are retrieved from the selection if none are provided. This is similar to @@ -289,7 +323,7 @@ retrieved from the selection if none are provided. This is similar to ### split -[src/index.js#L26-L26](src/index.js#L26-L26) +[src/index.js#L27-L27](src/index.js#L27-L27) Split a Rich Text value in two at the given `startIndex` and `endIndex`, or split at the given separator. This is similar to `String.prototype.split`. @@ -307,7 +341,7 @@ Indices are retrieved from the selection if none are provided. ### toggleFormat -[src/index.js#L29-L29](src/index.js#L29-L29) +[src/index.js#L30-L30](src/index.js#L30-L30) Toggles a format object to a Rich Text value at the current selection. @@ -322,7 +356,7 @@ Toggles a format object to a Rich Text value at the current selection. ### toHTMLString -[src/index.js#L28-L28](src/index.js#L28-L28) +[src/index.js#L29-L29](src/index.js#L29-L29) Create an HTML string from a Rich Text value. If a `multilineTag` is provided, text separated by a line separator will be wrapped in it. @@ -332,7 +366,6 @@ provided, text separated by a line separator will be wrapped in it. - **$1** `Object`: Named argements. - **$1.value** `Object`: Rich text value. - **$1.multilineTag** `[string]`: Multiline tag. -- **$1.multilineWrapperTags** `[Array]`: Tags where lines can be found if nesting is possible. **Returns** @@ -340,7 +373,7 @@ provided, text separated by a line separator will be wrapped in it. ### unregisterFormatType -[src/index.js#L31-L31](src/index.js#L31-L31) +[src/index.js#L32-L32](src/index.js#L32-L32) Unregisters a format. diff --git a/packages/rich-text/src/apply-format.js b/packages/rich-text/src/apply-format.js index 8134cd8d742a9..c9a59b96e6a65 100644 --- a/packages/rich-text/src/apply-format.js +++ b/packages/rich-text/src/apply-format.js @@ -23,12 +23,12 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the format applied. */ export function applyFormat( - { formats, text, start, end }, + value, format, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { - const newFormats = formats.slice( 0 ); + const newFormats = value.formats.slice( 0 ); // The selection is collapsed. if ( startIndex === endIndex ) { @@ -52,14 +52,10 @@ export function applyFormat( // with the format applied. } else { const previousFormat = newFormats[ startIndex - 1 ] || []; - const hasType = find( previousFormat, { type: format.type } ); return { - formats, - text, - start, - end, - formatPlaceholder: hasType ? undefined : format, + ...value, + formatPlaceholder: [ ...previousFormat, format ], }; } } else { @@ -68,7 +64,7 @@ export function applyFormat( } } - return normaliseFormats( { formats: newFormats, text, start, end } ); + return normaliseFormats( { ...value, formats: newFormats } ); } function applyFormats( formats, index, format ) { diff --git a/packages/rich-text/src/apply-format.native.js b/packages/rich-text/src/apply-format.native.js index 9c5a4749ae928..ef641cbaa224d 100644 --- a/packages/rich-text/src/apply-format.native.js +++ b/packages/rich-text/src/apply-format.native.js @@ -23,11 +23,13 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the format applied. */ export function applyFormat( - { formats: currentFormats, formatPlaceholder, text, start, end }, + value, formats, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { + const { formats: currentFormats, formatPlaceholder, start } = value; + if ( ! Array.isArray( formats ) ) { formats = [ formats ]; } @@ -40,10 +42,8 @@ export function applyFormat( // Follow the same logic as in getActiveFormat: placeholderFormats has priority over previousFormats const activeFormats = ( placeholderFormats ? placeholderFormats : previousFormats ) || []; return { + ...value, formats: currentFormats, - text, - start, - end, formatPlaceholder: { index: start, formats: mergeFormats( activeFormats, formats ), @@ -57,7 +57,7 @@ export function applyFormat( applyFormats( newFormats, index, formats ); } - return normaliseFormats( { formats: newFormats, text, start, end } ); + return normaliseFormats( { ...value, formats: newFormats } ); } function mergeFormats( formats1, formats2 ) { diff --git a/packages/rich-text/src/change-list-type.js b/packages/rich-text/src/change-list-type.js index 1dfc040657363..e429b91c6cbca 100644 --- a/packages/rich-text/src/change-list-type.js +++ b/packages/rich-text/src/change-list-type.js @@ -3,7 +3,6 @@ */ import { LINE_SEPARATOR } from './special-characters'; -import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; import { getParentLineIndex } from './get-parent-line-index'; @@ -20,12 +19,12 @@ import { getParentLineIndex } from './get-parent-line-index'; * @return {Object} The changed value. */ export function changeListType( value, newFormat ) { - const { text, formats, start, end } = value; + const { text, replacements, start, end } = value; const startingLineIndex = getLineIndex( value, start ); - const startLineFormats = formats[ startingLineIndex ] || []; - const endLineFormats = formats[ getLineIndex( value, end ) ] || []; + const startLineFormats = replacements[ startingLineIndex ] || []; + const endLineFormats = replacements[ getLineIndex( value, end ) ] || []; const startIndex = getParentLineIndex( value, startingLineIndex ); - const newFormats = formats.slice( 0 ); + const newReplacements = replacements.slice(); const startCount = startLineFormats.length - 1; const endCount = endLineFormats.length - 1; @@ -36,16 +35,16 @@ export function changeListType( value, newFormat ) { continue; } - if ( ( newFormats[ index ] || [] ).length <= startCount ) { + if ( ( newReplacements[ index ] || [] ).length <= startCount ) { break; } - if ( ! newFormats[ index ] ) { + if ( ! newReplacements[ index ] ) { continue; } changed = true; - newFormats[ index ] = newFormats[ index ].map( ( format, i ) => { + newReplacements[ index ] = newReplacements[ index ].map( ( format, i ) => { return i < startCount || i > endCount ? format : newFormat; } ); } @@ -54,10 +53,8 @@ export function changeListType( value, newFormat ) { return value; } - return normaliseFormats( { - text, - formats: newFormats, - start, - end, - } ); + return { + ...value, + replacements: newReplacements, + }; } diff --git a/packages/rich-text/src/concat.js b/packages/rich-text/src/concat.js index 9f14091f9b1c8..07be12140db91 100644 --- a/packages/rich-text/src/concat.js +++ b/packages/rich-text/src/concat.js @@ -3,6 +3,24 @@ */ import { normaliseFormats } from './normalise-formats'; +import { create } from './create'; + +/** + * Concats a pair of rich text values. Not that this mutates `a` and does NOT + * normalise formats! + * + * @param {Object} a Value to mutate. + * @param {Object} b Value to add read from. + * + * @return {Object} `a`, mutated. + */ +export function mergePair( a, b ) { + a.formats = a.formats.concat( b.formats ); + a.replacements = a.replacements.concat( b.replacements ); + a.text += b.text; + + return a; +} /** * Combine all Rich Text values into one. This is similar to @@ -13,8 +31,5 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value combining all given records. */ export function concat( ...values ) { - return normaliseFormats( values.reduce( ( accumlator, { formats, text } ) => ( { - text: accumlator.text + text, - formats: accumlator.formats.concat( formats ), - } ) ) ); + return normaliseFormats( values.reduce( mergePair, create() ) ); } diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index d1c1e47d4df06..37bd1189193f2 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -7,9 +7,9 @@ import { select } from '@wordpress/data'; * Internal dependencies */ -import { isEmpty } from './is-empty'; import { isFormatEqual } from './is-format-equal'; import { createElement } from './create-element'; +import { mergePair } from './concat'; import { LINE_SEPARATOR, OBJECT_REPLACEMENT_CHARACTER, @@ -22,7 +22,11 @@ import { const { TEXT_NODE, ELEMENT_NODE } = window.Node; function createEmptyValue() { - return { formats: [], text: '' }; + return { + formats: [], + replacements: [], + text: '', + }; } function simpleFindKey( object, value ) { @@ -96,6 +100,26 @@ function toFormat( { type, attributes } ) { * `multilineTag` will be separated by two newlines. The optional functions can * be used to filter out content. * + * A value will have the following shape, which you are strongly encouraged not + * to modify without the use of helper functions: + * + * ```js + * { + * text: string, + * formats: Array, + * replacements: Array, + * ?start: number, + * ?end: number, + * } + * ``` + * + * As you can see, text and formatting are separated. `text` holds the text, + * including any replacement characters for objects and lines. `formats`, + * `objects` and `lines` are all sparse arrays of the same length as `text`. It + * holds information about the formatting at the relevant text indices. Finally + * `start` and `end` state which text indices are selected. They are only + * provided if a `Range` was given. + * * @param {Object} [$1] Optional named arguments. * @param {Element} [$1.element] Element to create value from. * @param {string} [$1.text] Text to create value from. @@ -120,6 +144,7 @@ export function create( { if ( typeof text === 'string' && text.length > 0 ) { return { formats: Array( text.length ), + replacements: Array( text.length ), text, }; } @@ -291,10 +316,11 @@ function createFromElement( { const text = filterString( node.nodeValue ); range = filterRange( node, range, filterString ); accumulateSelection( accumulator, node, range, { text } ); - accumulator.text += text; // Create a sparse array of the same length as `text`, in which // formats can be added. accumulator.formats.length += text.length; + accumulator.replacements.length += text.length; + accumulator.text += text; continue; } @@ -312,8 +338,7 @@ function createFromElement( { if ( type === 'br' ) { accumulateSelection( accumulator, node, range, createEmptyValue() ); - accumulator.text += '\n'; - accumulator.formats.length += 1; + mergePair( accumulator, create( { text: '\n' } ) ); continue; } @@ -323,22 +348,10 @@ function createFromElement( { type, attributes: getAttributes( { element: node } ), } ); - - let format; - - if ( newFormat ) { - // Reuse the last format if it's equal. - if ( isFormatEqual( newFormat, lastFormat ) ) { - format = lastFormat; - } else { - format = newFormat; - } - } - - let value; + const format = isFormatEqual( newFormat, lastFormat ) ? lastFormat : newFormat; if ( multilineWrapperTags && multilineWrapperTags.indexOf( type ) !== -1 ) { - value = createFromMultilineElement( { + const value = createFromMultilineElement( { element: node, range, multilineTag, @@ -346,64 +359,39 @@ function createFromElement( { currentWrapperTags: [ ...currentWrapperTags, format ], isEditableTree, } ); - format = undefined; - } else { - value = createFromElement( { - element: node, - range, - multilineTag, - multilineWrapperTags, - isEditableTree, - } ); - } - - const text = value.text; - const start = accumulator.text.length; - accumulateSelection( accumulator, node, range, value ); - - // Don't apply the element as formatting if it has no content. - if ( isEmpty( value ) && format && ! format.attributes ) { + accumulateSelection( accumulator, node, range, value ); + mergePair( accumulator, value ); continue; } - const { formats } = accumulator; + const value = createFromElement( { + element: node, + range, + multilineTag, + multilineWrapperTags, + isEditableTree, + } ); - if ( format && format.attributes && text.length === 0 ) { - format.object = true; - accumulator.text += OBJECT_REPLACEMENT_CHARACTER; + accumulateSelection( accumulator, node, range, value ); - if ( formats[ start ] ) { - formats[ start ].unshift( format ); - } else { - formats[ start ] = [ format ]; + if ( ! format ) { + mergePair( accumulator, value ); + } else if ( value.text.length === 0 ) { + if ( format.attributes ) { + mergePair( accumulator, { + formats: [ , ], + replacements: [ format ], + text: OBJECT_REPLACEMENT_CHARACTER, + } ); } } else { - accumulator.text += text; - accumulator.formats.length += text.length; - - let i = value.formats.length; - - // Optimise for speed. - while ( i-- ) { - const formatIndex = start + i; - - if ( format ) { - if ( formats[ formatIndex ] ) { - formats[ formatIndex ].push( format ); - } else { - formats[ formatIndex ] = [ format ]; - } - } - - if ( value.formats[ i ] ) { - if ( formats[ formatIndex ] ) { - formats[ formatIndex ].push( ...value.formats[ i ] ); - } else { - formats[ formatIndex ] = value.formats[ i ]; - } - } - } + mergePair( accumulator, { + ...value, + formats: Array.from( value.formats, ( formats ) => + formats ? [ format, ...formats ] : [ format ] + ), + } ); } } @@ -459,17 +447,17 @@ function createFromMultilineElement( { isEditableTree, } ); - // Multiline value text should be separated by a double line break. + // Multiline value text should be separated by a line separator. if ( index !== 0 || currentWrapperTags.length > 0 ) { - const formats = currentWrapperTags.length > 0 ? [ currentWrapperTags ] : [ , ]; - accumulator.formats = accumulator.formats.concat( formats ); - accumulator.text += LINE_SEPARATOR; + mergePair( accumulator, { + formats: [ , ], + replacements: currentWrapperTags.length > 0 ? [ currentWrapperTags ] : [ , ], + text: LINE_SEPARATOR, + } ); } accumulateSelection( accumulator, node, range, value ); - - accumulator.formats = accumulator.formats.concat( value.formats ); - accumulator.text += value.text; + mergePair( accumulator, value ); } return accumulator; diff --git a/packages/rich-text/src/get-active-object.js b/packages/rich-text/src/get-active-object.js new file mode 100644 index 0000000000000..e324521284597 --- /dev/null +++ b/packages/rich-text/src/get-active-object.js @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ + +import { OBJECT_REPLACEMENT_CHARACTER } from './special-characters'; + +/** + * Gets the active object, if there is any. + * + * @param {Object} value Value to inspect. + * + * @return {?Object} Active object, or undefined. + */ +export function getActiveObject( { start, end, replacements, text } ) { + if ( start + 1 !== end || text[ start ] !== OBJECT_REPLACEMENT_CHARACTER ) { + return; + } + + return replacements[ start ]; +} diff --git a/packages/rich-text/src/get-last-child-index.js b/packages/rich-text/src/get-last-child-index.js index 976051b10c0a3..0fc4aeb32ff3e 100644 --- a/packages/rich-text/src/get-last-child-index.js +++ b/packages/rich-text/src/get-last-child-index.js @@ -12,8 +12,8 @@ import { LINE_SEPARATOR } from './special-characters'; * * @return {Array} The index of the last child. */ -export function getLastChildIndex( { text, formats }, lineIndex ) { - const lineFormats = formats[ lineIndex ] || []; +export function getLastChildIndex( { text, replacements }, lineIndex ) { + const lineFormats = replacements[ lineIndex ] || []; // Use the given line index in case there are no next children. let childIndex = lineIndex; @@ -24,7 +24,7 @@ export function getLastChildIndex( { text, formats }, lineIndex ) { continue; } - const formatsAtIndex = formats[ index ] || []; + const formatsAtIndex = replacements[ index ] || []; // If the amout of formats is equal or more, store it, then return the // last one if the amount of formats is less. diff --git a/packages/rich-text/src/get-parent-line-index.js b/packages/rich-text/src/get-parent-line-index.js index bd3f72de96519..1b0a0ecb5cf48 100644 --- a/packages/rich-text/src/get-parent-line-index.js +++ b/packages/rich-text/src/get-parent-line-index.js @@ -14,8 +14,8 @@ import { LINE_SEPARATOR } from './special-characters'; * * @return {Array} The parent list line index. */ -export function getParentLineIndex( { text, formats }, lineIndex ) { - const startFormats = formats[ lineIndex ] || []; +export function getParentLineIndex( { text, replacements }, lineIndex ) { + const startFormats = replacements[ lineIndex ] || []; let index = lineIndex; @@ -24,7 +24,7 @@ export function getParentLineIndex( { text, formats }, lineIndex ) { continue; } - const formatsAtIndex = formats[ index ] || []; + const formatsAtIndex = replacements[ index ] || []; if ( formatsAtIndex.length === startFormats.length - 1 ) { return index; diff --git a/packages/rich-text/src/indent-list-items.js b/packages/rich-text/src/indent-list-items.js index 1a61bd7db1536..985d1062c76ed 100644 --- a/packages/rich-text/src/indent-list-items.js +++ b/packages/rich-text/src/indent-list-items.js @@ -3,7 +3,6 @@ */ import { LINE_SEPARATOR } from './special-characters'; -import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; /** @@ -14,8 +13,8 @@ import { getLineIndex } from './get-line-index'; * * @return {boolean} The line index. */ -function getTargetLevelLineIndex( { text, formats }, lineIndex ) { - const startFormats = formats[ lineIndex ] || []; +function getTargetLevelLineIndex( { text, replacements }, lineIndex ) { + const startFormats = replacements[ lineIndex ] || []; let index = lineIndex; @@ -24,7 +23,7 @@ function getTargetLevelLineIndex( { text, formats }, lineIndex ) { continue; } - const formatsAtIndex = formats[ index ] || []; + const formatsAtIndex = replacements[ index ] || []; // Return the first line index that is one level higher. If the level is // lower or equal, there is no result. @@ -52,10 +51,10 @@ export function indentListItems( value, rootFormat ) { return value; } - const { text, formats, start, end } = value; + const { text, replacements, end } = value; const previousLineIndex = getLineIndex( value, lineIndex ); - const formatsAtLineIndex = formats[ lineIndex ] || []; - const formatsAtPreviousLineIndex = formats[ previousLineIndex ] || []; + const formatsAtLineIndex = replacements[ lineIndex ] || []; + const formatsAtPreviousLineIndex = replacements[ previousLineIndex ] || []; // The the indentation of the current line is greater than previous line, // then the line cannot be furter indented. @@ -63,7 +62,7 @@ export function indentListItems( value, rootFormat ) { return value; } - const newFormats = formats.slice(); + const newFormats = replacements.slice(); const targetLevelLineIndex = getTargetLevelLineIndex( value, lineIndex ); for ( let index = lineIndex; index < end; index++ ) { @@ -74,12 +73,12 @@ export function indentListItems( value, rootFormat ) { // Get the previous list, and if there's a child list, take over the // formats. If not, duplicate the last level and create a new level. if ( targetLevelLineIndex ) { - const targetFormats = formats[ targetLevelLineIndex ] || []; + const targetFormats = replacements[ targetLevelLineIndex ] || []; newFormats[ index ] = targetFormats.concat( ( newFormats[ index ] || [] ).slice( targetFormats.length - 1 ) ); } else { - const targetFormats = formats[ previousLineIndex ] || []; + const targetFormats = replacements[ previousLineIndex ] || []; const lastformat = targetFormats[ targetFormats.length - 1 ] || rootFormat; newFormats[ index ] = targetFormats.concat( @@ -89,10 +88,8 @@ export function indentListItems( value, rootFormat ) { } } - return normaliseFormats( { - text, - formats: newFormats, - start, - end, - } ); + return { + ...value, + replacements: newFormats, + }; } diff --git a/packages/rich-text/src/index.js b/packages/rich-text/src/index.js index 423046079132b..9d9f462021287 100644 --- a/packages/rich-text/src/index.js +++ b/packages/rich-text/src/index.js @@ -8,6 +8,7 @@ export { charAt } from './char-at'; export { concat } from './concat'; export { create } from './create'; export { getActiveFormat } from './get-active-format'; +export { getActiveObject } from './get-active-object'; export { getSelectionEnd } from './get-selection-end'; export { getSelectionStart } from './get-selection-start'; export { getTextContent } from './get-text-content'; diff --git a/packages/rich-text/src/insert-line-separator.js b/packages/rich-text/src/insert-line-separator.js index e273c9eea9ac4..77434a25ac012 100644 --- a/packages/rich-text/src/insert-line-separator.js +++ b/packages/rich-text/src/insert-line-separator.js @@ -24,15 +24,16 @@ export function insertLineSeparator( ) { const beforeText = getTextContent( value ).slice( 0, startIndex ); const previousLineSeparatorIndex = beforeText.lastIndexOf( LINE_SEPARATOR ); - const previousLineSeparatorFormats = value.formats[ previousLineSeparatorIndex ]; - let formats = [ , ]; + const previousLineSeparatorFormats = value.replacements[ previousLineSeparatorIndex ]; + let replacements = [ , ]; if ( previousLineSeparatorFormats ) { - formats = [ previousLineSeparatorFormats ]; + replacements = [ previousLineSeparatorFormats ]; } const valueToInsert = { - formats, + formats: [ , ], + replacements, text: LINE_SEPARATOR, }; diff --git a/packages/rich-text/src/insert-object.js b/packages/rich-text/src/insert-object.js index fcdfc6f897c2d..7495d6082cbcb 100644 --- a/packages/rich-text/src/insert-object.js +++ b/packages/rich-text/src/insert-object.js @@ -25,11 +25,9 @@ export function insertObject( endIndex ) { const valueToInsert = { + formats: [ , ], + replacements: [ formatToInsert ], text: OBJECT_REPLACEMENT_CHARACTER, - formats: [ [ { - ...formatToInsert, - object: true, - } ] ], }; return insert( value, valueToInsert, startIndex, endIndex ); diff --git a/packages/rich-text/src/insert.js b/packages/rich-text/src/insert.js index cf46305240fb7..4d2353eaba4cb 100644 --- a/packages/rich-text/src/insert.js +++ b/packages/rich-text/src/insert.js @@ -19,11 +19,13 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the value inserted. */ export function insert( - { formats, text, start, end }, + value, valueToInsert, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { + const { formats, replacements, text } = value; + if ( typeof valueToInsert === 'string' ) { valueToInsert = create( { text: valueToInsert } ); } @@ -32,6 +34,7 @@ export function insert( return normaliseFormats( { formats: formats.slice( 0, startIndex ).concat( valueToInsert.formats, formats.slice( endIndex ) ), + replacements: replacements.slice( 0, startIndex ).concat( valueToInsert.replacements, replacements.slice( endIndex ) ), text: text.slice( 0, startIndex ) + valueToInsert.text + text.slice( endIndex ), start: index, end: index, diff --git a/packages/rich-text/src/join.js b/packages/rich-text/src/join.js index 7784b5962ca53..cabfc82756016 100644 --- a/packages/rich-text/src/join.js +++ b/packages/rich-text/src/join.js @@ -20,8 +20,9 @@ export function join( values, separator = '' ) { separator = create( { text: separator } ); } - return normaliseFormats( values.reduce( ( accumlator, { formats, text } ) => ( { - text: accumlator.text + separator.text + text, + return normaliseFormats( values.reduce( ( accumlator, { formats, replacements, text } ) => ( { formats: accumlator.formats.concat( separator.formats, formats ), + replacements: accumlator.replacements.concat( separator.replacements, replacements ), + text: accumlator.text + separator.text + text, } ) ) ); } diff --git a/packages/rich-text/src/normalise-formats.js b/packages/rich-text/src/normalise-formats.js index 533df66933886..72c04f818f9a4 100644 --- a/packages/rich-text/src/normalise-formats.js +++ b/packages/rich-text/src/normalise-formats.js @@ -13,13 +13,13 @@ import { isFormatEqual } from './is-format-equal'; /** * Normalises formats: ensures subsequent equal formats have the same reference. * - * @param {Object} value Value to normalise formats of. + * @param {Object} value Value to normalise formats of. * * @return {Object} New value with normalised formats. */ -export function normaliseFormats( { formats, text, start, end } ) { +export function normaliseFormats( value ) { const refs = []; - const newFormats = formats.map( ( formatsAtIndex ) => + const newFormats = value.formats.map( ( formatsAtIndex ) => formatsAtIndex.map( ( format ) => { const equalRef = find( refs, ( ref ) => isFormatEqual( ref, format ) @@ -35,5 +35,5 @@ export function normaliseFormats( { formats, text, start, end } ) { } ) ); - return { formats: newFormats, text, start, end }; + return { ...value, formats: newFormats }; } diff --git a/packages/rich-text/src/normalise-formats.native.js b/packages/rich-text/src/normalise-formats.native.js deleted file mode 100644 index 2a75e343a2c12..0000000000000 --- a/packages/rich-text/src/normalise-formats.native.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Internal dependencies - */ - -import { isFormatEqual } from './is-format-equal'; - -/** - * Normalises formats: ensures subsequent equal formats have the same reference. - * - * @param {Object} value Value to normalise formats of. - * - * @return {Object} New value with normalised formats. - */ -export function normaliseFormats( { formats, formatPlaceholder, text, start, end } ) { - const newFormats = formats.slice( 0 ); - - newFormats.forEach( ( formatsAtIndex, index ) => { - const lastFormatsAtIndex = newFormats[ index - 1 ]; - - if ( lastFormatsAtIndex ) { - const newFormatsAtIndex = formatsAtIndex.slice( 0 ); - - newFormatsAtIndex.forEach( ( format, formatIndex ) => { - const lastFormat = lastFormatsAtIndex[ formatIndex ]; - - if ( isFormatEqual( format, lastFormat ) ) { - newFormatsAtIndex[ formatIndex ] = lastFormat; - } - } ); - - newFormats[ index ] = newFormatsAtIndex; - } - } ); - - return { formats: newFormats, formatPlaceholder, text, start, end }; -} diff --git a/packages/rich-text/src/outdent-list-items.js b/packages/rich-text/src/outdent-list-items.js index 3a493caa9b03a..19fac90515dfb 100644 --- a/packages/rich-text/src/outdent-list-items.js +++ b/packages/rich-text/src/outdent-list-items.js @@ -3,7 +3,6 @@ */ import { LINE_SEPARATOR } from './special-characters'; -import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; import { getParentLineIndex } from './get-parent-line-index'; import { getLastChildIndex } from './get-last-child-index'; @@ -16,16 +15,16 @@ import { getLastChildIndex } from './get-last-child-index'; * @return {Object} The changed value. */ export function outdentListItems( value ) { - const { text, formats, start, end } = value; + const { text, replacements, start, end } = value; const startingLineIndex = getLineIndex( value, start ); // Return early if the starting line index cannot be further outdented. - if ( formats[ startingLineIndex ] === undefined ) { + if ( replacements[ startingLineIndex ] === undefined ) { return value; } - const newFormats = formats.slice( 0 ); - const parentFormats = formats[ getParentLineIndex( value, startingLineIndex ) ] || []; + const newFormats = replacements.slice( 0 ); + const parentFormats = replacements[ getParentLineIndex( value, startingLineIndex ) ] || []; const endingLineIndex = getLineIndex( value, end ); const lastChildIndex = getLastChildIndex( value, endingLineIndex ); @@ -51,10 +50,8 @@ export function outdentListItems( value ) { } } - return normaliseFormats( { - text, - formats: newFormats, - start, - end, - } ); + return { + ...value, + replacements: newFormats, + }; } diff --git a/packages/rich-text/src/remove-format.js b/packages/rich-text/src/remove-format.js index be1c92ace7fd4..405cb6265e1e0 100644 --- a/packages/rich-text/src/remove-format.js +++ b/packages/rich-text/src/remove-format.js @@ -2,7 +2,7 @@ * External dependencies */ -import { find } from 'lodash'; +import { find, reject } from 'lodash'; /** * Internal dependencies @@ -23,28 +23,38 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the format applied. */ export function removeFormat( - { formats, text, start, end }, + value, formatType, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { - const newFormats = formats.slice( 0 ); + const newFormats = value.formats.slice( 0 ); // If the selection is collapsed, expand start and end to the edges of the // format. if ( startIndex === endIndex ) { const format = find( newFormats[ startIndex ], { type: formatType } ); - while ( find( newFormats[ startIndex ], format ) ) { - filterFormats( newFormats, startIndex, formatType ); - startIndex--; - } - - endIndex++; + if ( format ) { + while ( find( newFormats[ startIndex ], format ) ) { + filterFormats( newFormats, startIndex, formatType ); + startIndex--; + } - while ( find( newFormats[ endIndex ], format ) ) { - filterFormats( newFormats, endIndex, formatType ); endIndex++; + + while ( find( newFormats[ endIndex ], format ) ) { + filterFormats( newFormats, endIndex, formatType ); + endIndex++; + } + } else { + return { + ...value, + formatPlaceholder: reject( + newFormats[ startIndex - 1 ] || [], + { type: formatType } + ), + }; } } else { for ( let i = startIndex; i < endIndex; i++ ) { @@ -54,7 +64,7 @@ export function removeFormat( } } - return normaliseFormats( { formats: newFormats, text, start, end } ); + return normaliseFormats( { ...value, formats: newFormats } ); } function filterFormats( formats, index, formatType ) { diff --git a/packages/rich-text/src/remove-format.native.js b/packages/rich-text/src/remove-format.native.js index b6213f813bb63..5c20260728b71 100644 --- a/packages/rich-text/src/remove-format.native.js +++ b/packages/rich-text/src/remove-format.native.js @@ -23,11 +23,12 @@ import { normaliseFormats } from './normalise-formats'; * @return {Object} A new value with the format applied. */ export function removeFormat( - { formats, formatPlaceholder, text, start, end }, + value, formatType, - startIndex = start, - endIndex = end + startIndex = value.start, + endIndex = value.end ) { + const { formats, formatPlaceholder, start, end } = value; const newFormats = formats.slice( 0 ); let newFormatPlaceholder = null; @@ -55,7 +56,7 @@ export function removeFormat( } } - return normaliseFormats( { formats: newFormats, formatPlaceholder: newFormatPlaceholder, text, start, end } ); + return normaliseFormats( { ...value, formats: newFormats, formatPlaceholder: newFormatPlaceholder } ); } function filterFormats( formats, index, formatType ) { diff --git a/packages/rich-text/src/replace.js b/packages/rich-text/src/replace.js index 110fc186bd638..0cb26cb7431bd 100644 --- a/packages/rich-text/src/replace.js +++ b/packages/rich-text/src/replace.js @@ -20,11 +20,12 @@ import { normaliseFormats } from './normalise-formats'; * * @return {Object} A new value with replacements applied. */ -export function replace( { formats, text, start, end }, pattern, replacement ) { +export function replace( { formats, replacements, text, start, end }, pattern, replacement ) { text = text.replace( pattern, ( match, ...rest ) => { const offset = rest[ rest.length - 2 ]; let newText = replacement; let newFormats; + let newReplacements; if ( typeof newText === 'function' ) { newText = replacement( match, ...rest ); @@ -32,9 +33,11 @@ export function replace( { formats, text, start, end }, pattern, replacement ) { if ( typeof newText === 'object' ) { newFormats = newText.formats; + newReplacements = newText.replacements; newText = newText.text; } else { newFormats = Array( newText.length ); + newReplacements = Array( newText.length ); if ( formats[ offset ] ) { newFormats = newFormats.fill( formats[ offset ] ); @@ -42,6 +45,7 @@ export function replace( { formats, text, start, end }, pattern, replacement ) { } formats = formats.slice( 0, offset ).concat( newFormats, formats.slice( offset + match.length ) ); + replacements = replacements.slice( 0, offset ).concat( newReplacements, replacements.slice( offset + match.length ) ); if ( start ) { start = end = offset + newText.length; @@ -50,5 +54,5 @@ export function replace( { formats, text, start, end }, pattern, replacement ) { return newText; } ); - return normaliseFormats( { formats, text, start, end } ); + return normaliseFormats( { formats, replacements, text, start, end } ); } diff --git a/packages/rich-text/src/slice.js b/packages/rich-text/src/slice.js index bb4313dd61309..b535a445d45f5 100644 --- a/packages/rich-text/src/slice.js +++ b/packages/rich-text/src/slice.js @@ -10,16 +10,19 @@ * @return {Object} A new extracted value. */ export function slice( - { formats, text, start, end }, - startIndex = start, - endIndex = end + value, + startIndex = value.start, + endIndex = value.end ) { + const { formats, replacements, text } = value; + if ( startIndex === undefined || endIndex === undefined ) { - return { formats, text }; + return { ...value }; } return { formats: formats.slice( startIndex, endIndex ), + replacements: replacements.slice( startIndex, endIndex ), text: text.slice( startIndex, endIndex ), }; } diff --git a/packages/rich-text/src/split.js b/packages/rich-text/src/split.js index a300c3ccbcca4..f79a675b1e1f4 100644 --- a/packages/rich-text/src/split.js +++ b/packages/rich-text/src/split.js @@ -15,7 +15,7 @@ import { replace } from './replace'; * * @return {Array} An array of new values. */ -export function split( { formats, text, start, end }, string ) { +export function split( { formats, replacements, text, start, end }, string ) { if ( typeof string !== 'string' ) { return splitAtSelection( ...arguments ); } @@ -26,6 +26,7 @@ export function split( { formats, text, start, end }, string ) { const startIndex = nextStart; const value = { formats: formats.slice( startIndex, startIndex + substring.length ), + replacements: replacements.slice( startIndex, startIndex + substring.length ), text: substring, }; @@ -50,16 +51,18 @@ export function split( { formats, text, start, end }, string ) { } function splitAtSelection( - { formats, text, start, end }, + { formats, replacements, text, start, end }, startIndex = start, endIndex = end ) { const before = { formats: formats.slice( 0, startIndex ), + replacements: replacements.slice( 0, startIndex ), text: text.slice( 0, startIndex ), }; const after = { formats: formats.slice( endIndex ), + replacements: replacements.slice( endIndex ), text: text.slice( endIndex ), start: 0, end: 0, diff --git a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap index a68cdab1c911e..ae6c0db2d5575 100644 --- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap +++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap @@ -36,6 +36,7 @@ exports[`recordToDom should create a value with formatting with attributes 1`] = exports[`recordToDom should create a value with image object 1`] = ` + @@ -45,7 +46,10 @@ exports[`recordToDom should create a value with image object 1`] = ` exports[`recordToDom should create a value with image object and formatting 1`] = ` - + + @@ -57,7 +61,10 @@ exports[`recordToDom should create a value with image object and formatting 1`] exports[`recordToDom should create a value with image object and text after 1`] = ` - + + diff --git a/packages/rich-text/src/test/apply-format.js b/packages/rich-text/src/test/apply-format.js index de4f7b69e5f5e..b8cff3bc47620 100644 --- a/packages/rich-text/src/test/apply-format.js +++ b/packages/rich-text/src/test/apply-format.js @@ -61,7 +61,7 @@ describe( 'applyFormat', () => { }; const expected = { ...record, - formatPlaceholder: a2, + formatPlaceholder: [ a2 ], }; const result = applyFormat( deepFreeze( record ), a2 ); diff --git a/packages/rich-text/src/test/change-list-type.js b/packages/rich-text/src/test/change-list-type.js index 3817c66ba7977..6294368c6427d 100644 --- a/packages/rich-text/src/test/change-list-type.js +++ b/packages/rich-text/src/test/change-list-type.js @@ -17,7 +17,7 @@ describe( 'changeListType', () => { it( 'should only change list type if list item is indented', () => { const record = { - formats: [ , ], + replacements: [ , ], text: '1', start: 1, end: 1, @@ -26,27 +26,25 @@ describe( 'changeListType', () => { expect( result ).toEqual( record ); expect( result ).toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should change list type', () => { const record = { - formats: [ , [ ul ] ], + replacements: [ , [ ul ] ], text: `1${ LINE_SEPARATOR }`, start: 2, end: 2, }; const expected = { - formats: [ , [ ol ] ], - text: `1${ LINE_SEPARATOR }`, - start: 2, - end: 2, + ...record, + replacements: [ , [ ol ] ], }; const result = changeListType( deepFreeze( record ), ol ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); it( 'should outdent with multiple lines selected', () => { @@ -54,21 +52,19 @@ describe( 'changeListType', () => { const text = `a${ LINE_SEPARATOR }1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }i${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4${ LINE_SEPARATOR }b`; const record = { - formats: [ , [ ul ], , [ ul ], , [ ul, ul ], , [ ul ], , [ ul ], , , , [ ul ], , ], + replacements: [ , [ ul ], , [ ul ], , [ ul, ul ], , [ ul ], , [ ul ], , , , [ ul ], , ], text, start: 4, end: 9, }; const expected = { - formats: [ , [ ol ], , [ ol ], , [ ol, ul ], , [ ol ], , [ ol ], , , , [ ul ], , ], - text, - start: 4, - end: 9, + ...record, + replacements: [ , [ ol ], , [ ol ], , [ ol, ul ], , [ ol ], , [ ol ], , , , [ ul ], , ], }; const result = changeListType( deepFreeze( record ), ol ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 6 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 6 ); } ); } ); diff --git a/packages/rich-text/src/test/concat.js b/packages/rich-text/src/test/concat.js index 9ac2aa2dc7556..ae8253e1a5461 100644 --- a/packages/rich-text/src/test/concat.js +++ b/packages/rich-text/src/test/concat.js @@ -16,14 +16,17 @@ describe( 'concat', () => { it( 'should merge records', () => { const one = { formats: [ , , [ em ] ], + replacements: [ , , , ], text: 'one', }; const two = { formats: [ [ em ], , , ], + replacements: [ , , , ], text: 'two', }; const three = { formats: [ , , [ em ], [ em ], , , ], + replacements: [ , , , , , , ], text: 'onetwo', }; diff --git a/packages/rich-text/src/test/create.js b/packages/rich-text/src/test/create.js index 5128c55273d78..b585d72ec6614 100644 --- a/packages/rich-text/src/test/create.js +++ b/packages/rich-text/src/test/create.js @@ -81,6 +81,7 @@ describe( 'create', () => { expect( value ).toEqual( { formats: [ [ em ], [ em ], [ em, strong ], [ em, strong ] ], + replacements: [ , , , , ], text: 'test', } ); diff --git a/packages/rich-text/src/test/get-active-object.js b/packages/rich-text/src/test/get-active-object.js new file mode 100644 index 0000000000000..35d0f7637c7bc --- /dev/null +++ b/packages/rich-text/src/test/get-active-object.js @@ -0,0 +1,41 @@ +/** + * Internal dependencies + */ + +import { getActiveObject } from '../get-active-object'; +import { OBJECT_REPLACEMENT_CHARACTER } from '../special-characters'; + +describe( 'getActiveObject', () => { + it( 'should return object if selected', () => { + const record = { + replacements: [ { type: 'img' } ], + text: OBJECT_REPLACEMENT_CHARACTER, + start: 0, + end: 1, + }; + + expect( getActiveObject( record ) ).toEqual( { type: 'img' } ); + } ); + + it( 'should return nothing if nothing is selected', () => { + const record = { + replacements: [ { type: 'img' } ], + text: OBJECT_REPLACEMENT_CHARACTER, + start: 0, + end: 0, + }; + + expect( getActiveObject( record ) ).toBe( undefined ); + } ); + + it( 'should return nothing if te selection is not an object', () => { + const record = { + replacements: [ { type: 'em' } ], + text: 'a', + start: 0, + end: 1, + }; + + expect( getActiveObject( record ) ).toBe( undefined ); + } ); +} ); diff --git a/packages/rich-text/src/test/get-last-child-index.js b/packages/rich-text/src/test/get-last-child-index.js index 55c881d356555..f59cec9eeeab1 100644 --- a/packages/rich-text/src/test/get-last-child-index.js +++ b/packages/rich-text/src/test/get-last-child-index.js @@ -10,40 +10,40 @@ import deepFreeze from 'deep-freeze'; import { getLastChildIndex } from '../get-last-child-index'; import { LINE_SEPARATOR } from '../special-characters'; -describe( 'outdentListItems', () => { +describe( 'getLastChildIndex', () => { const ul = { type: 'ul' }; it( 'should return undefined if there is only one line', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , ], + replacements: [ , ], text: '1', } ), undefined ) ).toBe( undefined ); } ); it( 'should return the last line if no line is indented', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , ], + replacements: [ , ], text: `1${ LINE_SEPARATOR }`, } ), undefined ) ).toBe( 1 ); } ); it( 'should return the last child index', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , [ ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, } ), undefined ) ).toBe( 3 ); } ); it( 'should return the last child index by sibling', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , [ ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, } ), 1 ) ).toBe( 3 ); } ); it( 'should return the last child index (with further lower indented items)', () => { expect( getLastChildIndex( deepFreeze( { - formats: [ , [ ul ], , , , ], + replacements: [ , [ ul ], , , , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, } ), 1 ) ).toBe( 1 ); } ); diff --git a/packages/rich-text/src/test/get-parent-line-index.js b/packages/rich-text/src/test/get-parent-line-index.js index 4e6a75ffd0a6e..832ee4412dacc 100644 --- a/packages/rich-text/src/test/get-parent-line-index.js +++ b/packages/rich-text/src/test/get-parent-line-index.js @@ -15,28 +15,28 @@ describe( 'getParentLineIndex', () => { it( 'should return undefined if there is only one line', () => { expect( getParentLineIndex( deepFreeze( { - formats: [ , ], + replacements: [ , ], text: '1', } ), undefined ) ).toBe( undefined ); } ); it( 'should return undefined if the list is part of the first root list child', () => { expect( getParentLineIndex( deepFreeze( { - formats: [ , ], + replacements: [ , ], text: `1${ LINE_SEPARATOR }2`, } ), 2 ) ).toBe( undefined ); } ); it( 'should return the line index of the parent list (1)', () => { expect( getParentLineIndex( deepFreeze( { - formats: [ , , , [ ul ], , ], + replacements: [ , , , [ ul ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, } ), 3 ) ).toBe( 1 ); } ); it( 'should return the line index of the parent list (2)', () => { expect( getParentLineIndex( deepFreeze( { - formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`, } ), 5 ) ).toBe( undefined ); } ); diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js index 859e7e3acefc2..16b09bb19b832 100644 --- a/packages/rich-text/src/test/helpers/index.js +++ b/packages/rich-text/src/test/helpers/index.js @@ -4,7 +4,7 @@ export function getSparseArrayLength( array ) { const em = { type: 'em' }; const strong = { type: 'strong' }; -const img = { type: 'img', attributes: { src: '' }, object: true }; +const img = { type: 'img', attributes: { src: '' } }; const a = { type: 'a', attributes: { href: '#' } }; const ul = { type: 'ul' }; const ol = { type: 'ol' }; @@ -25,6 +25,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -43,6 +44,7 @@ export const spec = [ start: 0, end: 1, formats: [ , ], + replacements: [ , ], text: ' ', }, }, @@ -61,6 +63,7 @@ export const spec = [ start: 5, end: 5, formats: [ , , , , , , , , , , ], + replacements: [ , , , , , , , , , , ], text: 'test\u00a0 test', }, }, @@ -79,6 +82,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -97,6 +101,7 @@ export const spec = [ start: 0, end: 4, formats: [ , , , , ], + replacements: [ , , , , ], text: 'test', }, }, @@ -115,6 +120,7 @@ export const spec = [ start: 0, end: 2, formats: [ , , ], + replacements: [ , , ], text: '🍒', }, }, @@ -133,6 +139,7 @@ export const spec = [ start: 0, end: 2, formats: [ [ em ], [ em ] ], + replacements: [ , , ], text: '🍒', }, }, @@ -151,6 +158,7 @@ export const spec = [ start: 0, end: 4, formats: [ [ em ], [ em ], [ em ], [ em ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -169,6 +177,7 @@ export const spec = [ start: 0, end: 4, formats: [ [ em, strong ], [ em, strong ], [ em, strong ], [ em, strong ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -187,6 +196,7 @@ export const spec = [ start: 0, end: 2, formats: [ [ em ], [ em ], [ em ], [ em ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -205,6 +215,7 @@ export const spec = [ start: 0, end: 4, formats: [ [ a ], [ a ], [ a ], [ a ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -217,12 +228,13 @@ export const spec = [ endOffset: 1, endContainer: element, } ), - startPath: [ 1, 0 ], - endPath: [ 1, 0 ], + startPath: [ 0, 0 ], + endPath: [ 0, 0 ], record: { start: 0, end: 0, - formats: [ [ img ] ], + formats: [ , ], + replacements: [ img ], text: '\ufffc', }, }, @@ -235,12 +247,13 @@ export const spec = [ endOffset: 1, endContainer: element.querySelector( 'img' ), } ), - startPath: [ 0, 1, 0 ], - endPath: [ 0, 1, 0 ], + startPath: [ 0, 0, 0 ], + endPath: [ 0, 2, 0 ], record: { start: 0, end: 1, - formats: [ [ em, img ] ], + formats: [ [ em ] ], + replacements: [ img ], text: '\ufffc', }, }, @@ -258,7 +271,8 @@ export const spec = [ record: { start: 0, end: 5, - formats: [ , , [ em ], [ em ], [ em, img ] ], + formats: [ , , [ em ], [ em ], [ em ] ], + replacements: [ , , , , img ], text: 'test\ufffc', }, }, @@ -271,12 +285,13 @@ export const spec = [ endOffset: 2, endContainer: element, } ), - startPath: [ 0, 1, 0 ], + startPath: [ 0, 0, 0 ], endPath: [ 1, 2 ], record: { start: 0, end: 5, - formats: [ [ em, img ], [ em ], [ em ], , , ], + formats: [ [ em ], [ em ], [ em ], , , ], + replacements: [ img, , , , , ], text: '\ufffctest', }, }, @@ -295,6 +310,7 @@ export const spec = [ start: 0, end: 0, formats: [ , ], + replacements: [ , ], text: '\n', }, }, @@ -313,6 +329,7 @@ export const spec = [ start: 2, end: 3, formats: [ , , , , , ], + replacements: [ , , , , , ], text: 'te\nst', }, }, @@ -331,6 +348,7 @@ export const spec = [ start: 0, end: 1, formats: [ [ em ] ], + replacements: [ , ], text: '\n', }, }, @@ -347,6 +365,7 @@ export const spec = [ endPath: [ 4, 0 ], record: { formats: [ , , , , ], + replacements: [ , , , , ], text: 'a\n\nb', start: 2, end: 3, @@ -365,6 +384,7 @@ export const spec = [ endPath: [ 2, 0 ], record: { formats: [ , , , , ], + replacements: [ , , , , ], text: 'a\n\nb', start: 2, end: 2, @@ -386,6 +406,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -405,6 +426,7 @@ export const spec = [ start: 1, end: 4, formats: [ , , , , , , , ], + replacements: [ , , , , , , , ], text: 'one\u2028two', }, }, @@ -424,7 +446,8 @@ export const spec = [ record: { start: 0, end: 9, - formats: [ , , , [ ul ], , [ ul ], , [ ul, ol ], , [ ul, ol ], , , , , , , , ], + formats: [ , , , , , , , , , , , , , , , , , ], + replacements: [ , , , [ ul ], , [ ul ], , [ ul, ol ], , [ ul, ol ], , , , , , , , ], text: 'one\u2028a\u2028b\u20281\u20282\u2028three', }, }, @@ -445,6 +468,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -464,7 +488,8 @@ export const spec = [ record: { start: 1, end: 1, - formats: [ [ ul ] ], + formats: [ , ], + replacements: [ [ ul ] ], text: '\u2028', }, }, @@ -485,6 +510,7 @@ export const spec = [ start: 1, end: 1, formats: [ , , ], + replacements: [ , , ], text: '\u2028\u2028', }, }, @@ -504,6 +530,7 @@ export const spec = [ start: 4, end: 4, formats: [ , , , , ], + replacements: [ , , , , ], text: 'one\u2028', }, }, @@ -524,6 +551,7 @@ export const spec = [ start: 3, end: 3, formats: [ , , , ], + replacements: [ , , , ], text: 'one', }, }, @@ -534,6 +562,7 @@ export const spec = [ endPath: [], record: { formats: [ [ em ], [ em ], [ em ], [ em ], [ em ], [ em ], [ em ] ], + replacements: [ , , , , , , , ], text: 'one\u2028two', }, }, @@ -552,6 +581,7 @@ export const spec = [ start: 0, end: 0, formats: [], + replacements: [], text: '', }, }, @@ -570,6 +600,7 @@ export const spec = [ start: 0, end: 4, formats: [ [ strong ], [ strong ], [ strong ], [ strong ] ], + replacements: [ , , , , ], text: 'test', }, }, @@ -592,6 +623,7 @@ export const specWithRegistration = [ attributes: {}, unregisteredAttributes: {}, } ] ], + replacements: [ , ], text: 'a', }, }, @@ -613,6 +645,7 @@ export const specWithRegistration = [ class: 'test', }, } ] ], + replacements: [ , ], text: 'a', }, }, @@ -634,6 +667,7 @@ export const specWithRegistration = [ class: 'custom-format', }, } ] ], + replacements: [ , ], text: 'a', }, }, @@ -647,6 +681,7 @@ export const specWithRegistration = [ class: 'custom-format', }, } ] ], + replacements: [ , ], text: 'a', }, }, @@ -663,6 +698,7 @@ export const specWithRegistration = [ html: 'a', value: { formats: [ , ], + replacements: [ , ], text: 'a', }, noToHTMLString: true, @@ -685,6 +721,7 @@ export const specWithRegistration = [ attributes: {}, unregisteredAttributes: {}, } ] ], + replacements: [ , ], text: 'a', }, }, diff --git a/packages/rich-text/src/test/indent-list-items.js b/packages/rich-text/src/test/indent-list-items.js index e7f631e5fa8f9..349174247cc42 100644 --- a/packages/rich-text/src/test/indent-list-items.js +++ b/packages/rich-text/src/test/indent-list-items.js @@ -17,7 +17,7 @@ describe( 'indentListItems', () => { it( 'should not indent only item', () => { const record = { - formats: [ , ], + replacements: [ , ], text: '1', start: 1, end: 1, @@ -26,34 +26,32 @@ describe( 'indentListItems', () => { expect( result ).toEqual( record ); expect( result ).toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should indent', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }`; const record = { - formats: [ , , ], + replacements: [ , , ], text, start: 2, end: 2, }; const expected = { - formats: [ , [ ul ] ], - text, - start: 2, - end: 2, + ...record, + replacements: [ , [ ul ] ], }; const result = indentListItems( deepFreeze( record ), ul ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); it( 'should not indent without target list', () => { const record = { - formats: [ , [ ul ] ], + replacements: [ , [ ul ] ], text: `1${ LINE_SEPARATOR }`, start: 2, end: 2, @@ -62,80 +60,74 @@ describe( 'indentListItems', () => { expect( result ).toEqual( record ); expect( result ).toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); it( 'should indent and merge with previous list', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }${ LINE_SEPARATOR }`; const record = { - formats: [ , [ ol ], , ], + replacements: [ , [ ol ], , ], text, start: 3, end: 3, }; const expected = { - formats: [ , [ ol ], [ ol ] ], - text, - start: 3, - end: 3, + ...record, + replacements: [ , [ ol ], [ ol ] ], }; const result = indentListItems( deepFreeze( record ), ul ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should indent already indented item', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , [ ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul ], , ], text, start: 5, end: 5, }; const expected = { - formats: [ , [ ul ], , [ ul, ul ], , ], - text, - start: 5, - end: 5, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , ], }; const result = indentListItems( deepFreeze( record ), ul ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should indent with multiple lines selected', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , , , [ ul ], , ], + replacements: [ , , , [ ul ], , ], text, start: 2, end: 5, }; const expected = { - formats: [ , [ ul ], , [ ul, ul ], , ], - text, - start: 2, - end: 5, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , ], }; const result = indentListItems( deepFreeze( record ), ul ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should indent one level at a time', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , , , ], + replacements: [ , [ ul ], , [ ul, ul ], , , , ], text, start: 6, end: 6, @@ -144,34 +136,28 @@ describe( 'indentListItems', () => { const result1 = indentListItems( deepFreeze( record ), ul ); expect( result1 ).not.toBe( record ); - expect( getSparseArrayLength( result1.formats ) ).toBe( 3 ); + expect( getSparseArrayLength( result1.replacements ) ).toBe( 3 ); expect( result1 ).toEqual( { - formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], - text, - start: 6, - end: 6, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], } ); const result2 = indentListItems( deepFreeze( result1 ), ul ); expect( result2 ).not.toBe( result1 ); - expect( getSparseArrayLength( result2.formats ) ).toBe( 3 ); + expect( getSparseArrayLength( result2.replacements ) ).toBe( 3 ); expect( result2 ).toEqual( { - formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], - text, - start: 6, - end: 6, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], } ); const result3 = indentListItems( deepFreeze( result2 ), ul ); expect( result3 ).not.toBe( result2 ); - expect( getSparseArrayLength( result3.formats ) ).toBe( 3 ); + expect( getSparseArrayLength( result3.replacements ) ).toBe( 3 ); expect( result3 ).toEqual( { - formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul, ul ], , ], - text, - start: 6, - end: 6, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , [ ul, ul, ul ], , ], } ); } ); } ); diff --git a/packages/rich-text/src/test/insert-line-separator.js b/packages/rich-text/src/test/insert-line-separator.js index 398b0a2b8834f..497555cc4a01a 100644 --- a/packages/rich-text/src/test/insert-line-separator.js +++ b/packages/rich-text/src/test/insert-line-separator.js @@ -17,12 +17,14 @@ describe( 'insertLineSeparator', () => { it( 'should insert line separator at end', () => { const value = { formats: [ , ], + replacements: [ , ], text: '1', start: 1, end: 1, }; const expected = { formats: [ , , ], + replacements: [ , , ], text: `1${ LINE_SEPARATOR }`, start: 2, end: 2, @@ -31,18 +33,20 @@ describe( 'insertLineSeparator', () => { expect( result ).not.toBe( value ); expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should insert line separator at start', () => { const value = { formats: [ , ], + replacements: [ , ], text: '1', start: 0, end: 0, }; const expected = { formats: [ , , ], + replacements: [ , , ], text: `${ LINE_SEPARATOR }1`, start: 1, end: 1, @@ -51,18 +55,20 @@ describe( 'insertLineSeparator', () => { expect( result ).not.toBe( value ); expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should insert line separator with previous line separator formats', () => { const value = { - formats: [ , , , [ ol ], , ], + formats: [ , , , , , ], + replacements: [ , , , [ ol ], , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a`, start: 5, end: 5, }; const expected = { - formats: [ , , , [ ol ], , [ ol ] ], + formats: [ , , , , , , ], + replacements: [ , , , [ ol ], , [ ol ] ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a${ LINE_SEPARATOR }`, start: 6, end: 6, @@ -71,18 +77,20 @@ describe( 'insertLineSeparator', () => { expect( result ).not.toBe( value ); expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should insert line separator without formats if previous line separator did not have any', () => { const value = { formats: [ , , , , , ], + replacements: [ , , , , , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a`, start: 5, end: 5, }; const expected = { formats: [ , , , , , , ], + replacements: [ , , , , , , ], text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a${ LINE_SEPARATOR }`, start: 6, end: 6, @@ -91,6 +99,6 @@ describe( 'insertLineSeparator', () => { expect( result ).not.toBe( value ); expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); } ); diff --git a/packages/rich-text/src/test/insert-object.js b/packages/rich-text/src/test/insert-object.js index 15680372986a9..7c2ba57806ad0 100644 --- a/packages/rich-text/src/test/insert-object.js +++ b/packages/rich-text/src/test/insert-object.js @@ -17,12 +17,14 @@ describe( 'insert', () => { it( 'should delete and insert', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, }; const expected = { - formats: [ , , [ { ...obj, object: true } ], [ em ], , , , , , , ], + formats: [ , , , [ em ], , , , , , , ], + replacements: [ , , obj, , , , , , , , ], text: `on${ OBJECT_REPLACEMENT_CHARACTER }o three`, start: 3, end: 3, @@ -31,6 +33,7 @@ describe( 'insert', () => { expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); } ); diff --git a/packages/rich-text/src/test/insert.js b/packages/rich-text/src/test/insert.js index 64de9f44828a2..cae4cc5b85d5f 100644 --- a/packages/rich-text/src/test/insert.js +++ b/packages/rich-text/src/test/insert.js @@ -17,16 +17,19 @@ describe( 'insert', () => { it( 'should delete and insert', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [], text: 'one two three', start: 6, end: 6, }; const toInsert = { formats: [ [ strong ] ], + replacements: [], text: 'a', }; const expected = { formats: [ , , [ strong ], [ em ], , , , , , , ], + replacements: [], text: 'onao three', start: 3, end: 3, @@ -41,16 +44,19 @@ describe( 'insert', () => { it( 'should insert line break with selection', () => { const record = { formats: [ , , ], + replacements: [], text: 'tt', start: 1, end: 1, }; const toInsert = { formats: [ , ], + replacements: [], text: '\n', }; const expected = { formats: [ , , , ], + replacements: [], text: 't\nt', start: 2, end: 2, diff --git a/packages/rich-text/src/test/join.js b/packages/rich-text/src/test/join.js index fb2f20b1b2784..84801125cc2b0 100644 --- a/packages/rich-text/src/test/join.js +++ b/packages/rich-text/src/test/join.js @@ -15,8 +15,9 @@ describe( 'join', () => { const separators = [ ' ', { - text: ' ', formats: [ , ], + replacements: [ , ], + text: ' ', }, ]; @@ -24,14 +25,17 @@ describe( 'join', () => { it( 'should join records with string separator', () => { const one = { formats: [ , , [ em ] ], + replacements: [ , , , ], text: 'one', }; const two = { formats: [ [ em ], , , ], + replacements: [ , , , ], text: 'two', }; const three = { formats: [ , , [ em ], , [ em ], , , ], + replacements: [ , , , , , , , ], text: 'one two', }; const result = join( [ deepFreeze( one ), deepFreeze( two ) ], separator ); diff --git a/packages/rich-text/src/test/outdent-list-items.js b/packages/rich-text/src/test/outdent-list-items.js index c2bf8b30e4766..e4325dfb61c90 100644 --- a/packages/rich-text/src/test/outdent-list-items.js +++ b/packages/rich-text/src/test/outdent-list-items.js @@ -16,7 +16,7 @@ describe( 'outdentListItems', () => { it( 'should not outdent only item', () => { const record = { - formats: [ , ], + replacements: [ , ], text: '1', start: 1, end: 1, @@ -25,138 +25,126 @@ describe( 'outdentListItems', () => { expect( result ).toEqual( record ); expect( result ).toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should indent', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }`; const record = { - formats: [ , [ ul ] ], + replacements: [ , [ ul ] ], text, start: 2, end: 2, }; const expected = { - formats: [ , , ], - text, - start: 2, - end: 2, + ...record, + replacements: [ , , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); it( 'should outdent two levels deep', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , ], text, start: 5, end: 5, }; const expected = { - formats: [ , [ ul ], , [ ul ], , ], - text, - start: 5, - end: 5, + ...record, + replacements: [ , [ ul ], , [ ul ], , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should outdent with multiple lines selected', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , ], text, start: 2, end: 5, }; const expected = { - formats: [ , , , [ ul ], , ], - text, - start: 2, - end: 5, + ...record, + replacements: [ , , , [ ul ], , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 1 ); } ); it( 'should outdent list item with children', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], text, start: 2, end: 2, }; const expected = { - formats: [ , , , [ ul ], , [ ul ], , ], - text, - start: 2, - end: 2, + ...record, + replacements: [ , , , [ ul ], , [ ul ], , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should outdent list based on parent list', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; const record = { - formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], + replacements: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], text, start: 6, end: 6, }; const expected = { - formats: [ , [ ul ], , [ ul, ul ], , , , ], - text, - start: 6, - end: 6, + ...record, + replacements: [ , [ ul ], , [ ul, ul ], , , , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); } ); it( 'should outdent when a selected item is at level 0', () => { // As we're testing list formats, the text should remain the same. const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; const record = { - formats: [ , [ ul ], , , , ], + replacements: [ , [ ul ], , , , ], text, start: 2, end: 5, }; const expected = { - formats: [ , , , , , ], - text, - start: 2, - end: 5, + ...record, + replacements: [ , , , , , ], }; const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); - expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); } ); } ); diff --git a/packages/rich-text/src/test/replace.js b/packages/rich-text/src/test/replace.js index f3c7d9aa923e9..6cfd4dcc9e48b 100644 --- a/packages/rich-text/src/test/replace.js +++ b/packages/rich-text/src/test/replace.js @@ -16,12 +16,14 @@ describe( 'replace', () => { it( 'should replace string to string', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, }; const expected = { formats: [ , , , , [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , ], text: 'one 2 three', start: 5, end: 5, @@ -36,16 +38,19 @@ describe( 'replace', () => { it( 'should replace string to record', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, }; const replacement = { formats: [ , ], + replacements: [ , ], text: '2', }; const expected = { formats: [ , , , , , , , , , , , ], + replacements: [ , , , , , , , , , , , ], text: 'one 2 three', start: 5, end: 5, @@ -60,12 +65,14 @@ describe( 'replace', () => { it( 'should replace string to function', () => { const record = { formats: [ , , , , , , , , , , , , ], + replacements: [ , , , , , , , , , , , , ], text: 'abc12345#$*%', start: 6, end: 6, }; const expected = { formats: [ , , , , , , , , , , , , , , , , , , ], + replacements: [ , , , , , , , , , , , , , , , , , , ], text: 'abc - 12345 - #$*%', start: 18, end: 18, diff --git a/packages/rich-text/src/test/slice.js b/packages/rich-text/src/test/slice.js index 9d40f7a6de037..b181e5e9be817 100644 --- a/packages/rich-text/src/test/slice.js +++ b/packages/rich-text/src/test/slice.js @@ -16,10 +16,12 @@ describe( 'slice', () => { it( 'should slice', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', }; const expected = { formats: [ , [ em ], [ em ] ], + replacements: [ , , , ], text: ' tw', }; const result = slice( deepFreeze( record ), 3, 6 ); @@ -32,12 +34,14 @@ describe( 'slice', () => { it( 'should slice record', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 3, end: 6, }; const expected = { formats: [ , [ em ], [ em ] ], + replacements: [ , , , ], text: ' tw', }; const result = slice( deepFreeze( record ) ); diff --git a/packages/rich-text/src/test/split.js b/packages/rich-text/src/test/split.js index 1cdfbae9630d2..8eef998e488d6 100644 --- a/packages/rich-text/src/test/split.js +++ b/packages/rich-text/src/test/split.js @@ -18,17 +18,20 @@ describe( 'split', () => { start: 5, end: 10, formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', }; const expected = [ { formats: [ , , , , [ em ], [ em ] ], + replacements: [ , , , , , , ], text: 'one tw', }, { start: 0, end: 0, formats: [ [ em ], , , , , , , ], + replacements: [ , , , , , , , ], text: 'o three', }, ]; @@ -45,6 +48,7 @@ describe( 'split', () => { it( 'should split with selection', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, @@ -52,10 +56,12 @@ describe( 'split', () => { const expected = [ { formats: [ , , , , [ em ], [ em ] ], + replacements: [ , , , , , , ], text: 'one tw', }, { formats: [ [ em ], , , , , , , ], + replacements: [ , , , , , , , ], text: 'o three', start: 0, end: 0, @@ -74,6 +80,7 @@ describe( 'split', () => { it( 'should split empty', () => { const record = { formats: [], + replacements: [], text: '', start: 0, end: 0, @@ -81,10 +88,12 @@ describe( 'split', () => { const expected = [ { formats: [], + replacements: [], text: '', }, { formats: [], + replacements: [], text: '', start: 0, end: 0, @@ -103,6 +112,7 @@ describe( 'split', () => { it( 'should split multiline', () => { const record = { formats: [ , , , , , , , , , , ], + replacements: [ , , , , , , , , , , ], text: 'test\u2028\u2028test', start: 5, end: 5, @@ -110,10 +120,12 @@ describe( 'split', () => { const expected = [ { formats: [ , , , , ], + replacements: [ , , , , ], text: 'test', }, { formats: [ , , , , ], + replacements: [ , , , , ], text: 'test', start: 0, end: 0, @@ -134,33 +146,39 @@ describe( 'split', () => { start: 6, end: 16, formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , , , , , , , , , , , ], + replacements: [ , , , , , , , , , , , , , , , , , , , , , , , ], text: 'one two three four five', }; const expected = [ { formats: [ , , , ], + replacements: [ , , , ], text: 'one', }, { start: 2, end: 3, formats: [ [ em ], [ em ], [ em ] ], + replacements: [ , , , ], text: 'two', }, { start: 0, end: 5, formats: [ , , , , , ], + replacements: [ , , , , , ], text: 'three', }, { start: 0, end: 2, formats: [ , , , , ], + replacements: [ , , , , ], text: 'four', }, { formats: [ , , , , ], + replacements: [ , , , , ], text: 'five', }, ]; @@ -179,21 +197,25 @@ describe( 'split', () => { start: 5, end: 6, formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + replacements: [ , , , , , , , , , , , , , ], text: 'one two three', }; const expected = [ { formats: [ , , , ], + replacements: [ , , , ], text: 'one', }, { start: 1, end: 2, formats: [ [ em ], [ em ], [ em ] ], + replacements: [ , , , ], text: 'two', }, { formats: [ , , , , , ], + replacements: [ , , , , , ], text: 'three', }, ]; diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.js index 1df1e8e25b229..8db8a52e1b443 100644 --- a/packages/rich-text/src/test/to-dom.js +++ b/packages/rich-text/src/test/to-dom.js @@ -24,7 +24,6 @@ describe( 'recordToDom', () => { spec.forEach( ( { description, multilineTag, - multilineWrapperTags, record, startPath, endPath, @@ -33,7 +32,6 @@ describe( 'recordToDom', () => { const { body, selection } = toDom( { value: record, multilineTag, - multilineWrapperTags, } ); expect( body ).toMatchSnapshot(); expect( selection ).toEqual( { startPath, endPath } ); diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index b81c518468444..9ebe8365a25ba 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -122,7 +122,6 @@ function prepareFormats( prepareEditableTree = [], value ) { export function toDom( { value, multilineTag, - multilineWrapperTags, prepareEditableTree, isEditableTree = true, } ) { @@ -135,7 +134,6 @@ export function toDom( { formats: prepareFormats( prepareEditableTree, value ), }, multilineTag, - multilineWrapperTags, createEmpty, append, getLastChild, @@ -174,7 +172,6 @@ export function apply( { value, current, multilineTag, - multilineWrapperTags, prepareEditableTree, __unstableDomOnly, } ) { @@ -182,7 +179,6 @@ export function apply( { const { body, selection } = toDom( { value, multilineTag, - multilineWrapperTags, prepareEditableTree, } ); diff --git a/packages/rich-text/src/to-html-string.js b/packages/rich-text/src/to-html-string.js index 0ba36e62510a3..bf6c60fda0442 100644 --- a/packages/rich-text/src/to-html-string.js +++ b/packages/rich-text/src/to-html-string.js @@ -21,16 +21,13 @@ import { toTree } from './to-tree'; * @param {Object} $1 Named argements. * @param {Object} $1.value Rich text value. * @param {string} [$1.multilineTag] Multiline tag. - * @param {Array} [$1.multilineWrapperTags] Tags where lines can be found if - * nesting is possible. * * @return {string} HTML string. */ -export function toHTMLString( { value, multilineTag, multilineWrapperTags } ) { +export function toHTMLString( { value, multilineTag } ) { const tree = toTree( { value, multilineTag, - multilineWrapperTags, createEmpty, append, getLastChild, diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index c89bdc94a2d06..ef15fe7524b6e 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -91,7 +91,6 @@ const padding = { export function toTree( { value, multilineTag, - multilineWrapperTags = [], createEmpty, append, getLastChild, @@ -104,7 +103,7 @@ export function toTree( { onEndIndex, isEditableTree, } ) { - const { formats, text, start, end } = value; + const { formats, replacements, text, start, end } = value; const formatsLength = formats.length + 1; const tree = createEmpty(); const multilineFormat = { type: multilineTag }; @@ -138,12 +137,8 @@ export function toTree( { // Set multiline tags in queue for building the tree. if ( multilineTag ) { if ( character === LINE_SEPARATOR ) { - characterFormats = lastSeparatorFormats = ( characterFormats || [] ).reduce( ( accumulator, format ) => { - if ( character === LINE_SEPARATOR && multilineWrapperTags.indexOf( format.type ) !== -1 ) { - accumulator.push( format ); - accumulator.push( multilineFormat ); - } - + characterFormats = lastSeparatorFormats = ( replacements[ i ] || [] ).reduce( ( accumulator, format ) => { + accumulator.push( format, multilineFormat ); return accumulator; }, [ multilineFormat ] ); } else { @@ -196,11 +191,10 @@ export function toTree( { return; } - const { type, attributes, unregisteredAttributes, object } = format; + const { type, attributes, unregisteredAttributes } = format; const boundaryClass = ( isEditableTree && - ! object && character !== LINE_SEPARATOR && format === deepestActiveFormat ); @@ -210,7 +204,6 @@ export function toTree( { type, attributes, unregisteredAttributes, - object, boundaryClass, } ) ); @@ -218,7 +211,7 @@ export function toTree( { remove( pointer ); } - pointer = append( format.object ? parent : newNode, '' ); + pointer = append( newNode, '' ); } ); } @@ -240,22 +233,27 @@ export function toTree( { } } - if ( character !== OBJECT_REPLACEMENT_CHARACTER ) { - if ( character === '\n' ) { - pointer = append( getParent( pointer ), { - type: 'br', - attributes: isEditableTree ? { - 'data-rich-text-line-break': 'true', - } : undefined, - object: true, - } ); - // Ensure pointer is text node. - pointer = append( getParent( pointer ), '' ); - } else if ( ! isText( pointer ) ) { - pointer = append( getParent( pointer ), character ); - } else { - appendText( pointer, character ); - } + if ( character === OBJECT_REPLACEMENT_CHARACTER ) { + pointer = append( getParent( pointer ), fromFormat( { + ...replacements[ i ], + object: true, + } ) ); + // Ensure pointer is text node. + pointer = append( getParent( pointer ), '' ); + } else if ( character === '\n' ) { + pointer = append( getParent( pointer ), { + type: 'br', + attributes: isEditableTree ? { + 'data-rich-text-line-break': 'true', + } : undefined, + object: true, + } ); + // Ensure pointer is text node. + pointer = append( getParent( pointer ), '' ); + } else if ( ! isText( pointer ) ) { + pointer = append( getParent( pointer ), character ); + } else { + appendText( pointer, character ); } if ( onStartIndex && start === i + 1 ) { diff --git a/packages/rich-text/src/toggle-format.js b/packages/rich-text/src/toggle-format.js index 6e7854dcaa662..7545f5d30c9d6 100644 --- a/packages/rich-text/src/toggle-format.js +++ b/packages/rich-text/src/toggle-format.js @@ -14,10 +14,7 @@ import { applyFormat } from './apply-format'; * * @return {Object} A new value with the format applied or removed. */ -export function toggleFormat( - value, - format -) { +export function toggleFormat( value, format ) { if ( getActiveFormat( value, format.type ) ) { return removeFormat( value, format.type ); } From 08da64b407e95683946b34cf6ebca5d52ac28227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Wed, 13 Mar 2019 13:31:27 +0100 Subject: [PATCH 03/33] RichText: try alternative list shortcuts (to tab) (#14343) * RichText: try alternative list shortcuts * Try tooltips * Change tooltips to use text * Add inline comments * Add e2e test * Rebase --- .../src/components/rich-text/index.js | 79 +++++++++++++++---- .../src/components/rich-text/list-edit.js | 4 +- .../blocks/__snapshots__/list.test.js.snap | 38 +++++++++ packages/e2e-tests/specs/blocks/list.test.js | 42 ++++++++++ 4 files changed, 145 insertions(+), 18 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 1d714ba9844d3..92afb3799eb90 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -19,7 +19,7 @@ import memize from 'memize'; import { Component, Fragment, RawHTML } from '@wordpress/element'; import { isHorizontalEdge } from '@wordpress/dom'; import { createBlobURL } from '@wordpress/blob'; -import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT } from '@wordpress/keycodes'; +import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT, SPACE } from '@wordpress/keycodes'; import { withDispatch, withSelect } from '@wordpress/data'; import { pasteHandler, children, getBlockTransforms, findTransform } from '@wordpress/blocks'; import { withInstanceId, withSafeTimeout, compose } from '@wordpress/compose'; @@ -37,13 +37,11 @@ import { insertLineSeparator, isEmptyLine, unstableToDom, - getSelectionStart, - getSelectionEnd, remove, removeFormat, isCollapsed, LINE_SEPARATOR, - charAt, + indentListItems, } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { withFilters, IsolatedEventContainer } from '@wordpress/components'; @@ -599,10 +597,26 @@ export class RichText extends Component { this.handleHorizontalNavigation( event ); } + // Use the space key in list items (at the start of an item) to indent + // the list item. + if ( keyCode === SPACE && this.multilineTag === 'li' ) { + const value = this.createRecord(); + + if ( isCollapsed( value ) ) { + const { text, start } = value; + const characterBefore = text[ start - 1 ]; + + // The caret must be at the start of a line. + if ( ! characterBefore || characterBefore === LINE_SEPARATOR ) { + this.onChange( indentListItems( value, { type: this.props.tagName } ) ); + event.preventDefault(); + } + } + } + if ( keyCode === DELETE || keyCode === BACKSPACE ) { const value = this.createRecord(); - const start = getSelectionStart( value ); - const end = getSelectionEnd( value ); + const { replacements, text, start, end } = value; // Always handle full content deletion ourselves. if ( start === 0 && end !== 0 && end === value.text.length ) { @@ -615,22 +629,53 @@ export class RichText extends Component { let newValue; if ( keyCode === BACKSPACE ) { - if ( charAt( value, start - 1 ) === LINE_SEPARATOR ) { + const index = start - 1; + + if ( text[ index ] === LINE_SEPARATOR ) { + const collapsed = isCollapsed( value ); + + // If the line separator that is about te be removed + // contains wrappers, remove the wrappers first. + if ( collapsed && replacements[ index ] && replacements[ index ].length ) { + const newReplacements = replacements.slice(); + + newReplacements[ index ] = replacements[ index ].slice( 0, -1 ); + newValue = { + ...value, + replacements: newReplacements, + }; + } else { + newValue = remove( + value, + // Only remove the line if the selection is + // collapsed, otherwise remove the selection. + collapsed ? start - 1 : start, + end + ); + } + } + } else if ( text[ end ] === LINE_SEPARATOR ) { + const collapsed = isCollapsed( value ); + + // If the line separator that is about te be removed + // contains wrappers, remove the wrappers first. + if ( collapsed && replacements[ end ] && replacements[ end ].length ) { + const newReplacements = replacements.slice(); + + newReplacements[ end ] = replacements[ end ].slice( 0, -1 ); + newValue = { + ...value, + replacements: newReplacements, + }; + } else { newValue = remove( value, + start, // Only remove the line if the selection is - // collapsed. - isCollapsed( value ) ? start - 1 : start, - end + // collapsed, otherwise remove the selection. + collapsed ? end + 1 : end, ); } - } else if ( charAt( value, end ) === LINE_SEPARATOR ) { - newValue = remove( - value, - start, - // Only remove the line if the selection is collapsed. - isCollapsed( value ) ? end + 1 : end, - ); } if ( newValue ) { diff --git a/packages/block-editor/src/components/rich-text/list-edit.js b/packages/block-editor/src/components/rich-text/list-edit.js index 81de868f62719..aee85983258df 100644 --- a/packages/block-editor/src/components/rich-text/list-edit.js +++ b/packages/block-editor/src/components/rich-text/list-edit.js @@ -3,7 +3,7 @@ */ import { Toolbar } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { Fragment } from '@wordpress/element'; import { indentListItems, @@ -149,6 +149,7 @@ export const ListEdit = ( { { icon: 'editor-outdent', title: __( 'Outdent list item' ), + shortcut: _x( 'Backspace', 'keyboard key' ), onClick: () => { onChange( outdentListItems( value ) ); }, @@ -156,6 +157,7 @@ export const ListEdit = ( { { icon: 'editor-indent', title: __( 'Indent list item' ), + shortcut: _x( 'Space', 'keyboard key' ), onClick: () => { onChange( indentListItems( value, { type: tagName } ) ); }, diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap index b2e8ec4875aac..4d473f282a17a 100644 --- a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap +++ b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap @@ -98,6 +98,44 @@ exports[`List should change the indented list type 1`] = ` " `; +exports[`List should create and remove indented list with keyboard only 1`] = ` +" +
  • 1
    • a
      • i
+" +`; + +exports[`List should create and remove indented list with keyboard only 2`] = ` +" +
  • 1
    • a
+" +`; + +exports[`List should create and remove indented list with keyboard only 3`] = ` +" +
  • 1
    • a
+" +`; + +exports[`List should create and remove indented list with keyboard only 4`] = ` +" +
  • 1
    • a
+" +`; + +exports[`List should create and remove indented list with keyboard only 5`] = ` +" +
  • 1
+" +`; + +exports[`List should create and remove indented list with keyboard only 6`] = ` +" +
  • 1
+" +`; + +exports[`List should create and remove indented list with keyboard only 7`] = `""`; + exports[`List should create paragraph on split at end and merge back with content 1`] = ` "
  • one
diff --git a/packages/e2e-tests/specs/blocks/list.test.js b/packages/e2e-tests/specs/blocks/list.test.js index 23d02e0f10cb7..2bbc038627885 100644 --- a/packages/e2e-tests/specs/blocks/list.test.js +++ b/packages/e2e-tests/specs/blocks/list.test.js @@ -304,4 +304,46 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should create and remove indented list with keyboard only', async () => { + await clickBlockAppender(); + + await page.keyboard.type( '* 1' ); // Should be at level 0. + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ' a' ); // Should be at level 1. + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ' i' ); // Should be at level 2. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); // Should be at level 1. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); // Should be at level 0. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); // Should be at level 1. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); // Should be at level 0. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); // Should be at level 0. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); // Should remove list. + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + // That's 9 key presses to create the list, and 9 key presses to remove + // the list. ;) + } ); } ); From d7a4dfa1d45aa417281e65cf4455316e59921143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Wed, 13 Mar 2019 15:32:50 +0100 Subject: [PATCH 04/33] Fix source map paths published to npm (#14409) --- bin/packages/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/packages/build.js b/bin/packages/build.js index 02009c1e4a4d3..8a13f8f10b048 100755 --- a/bin/packages/build.js +++ b/bin/packages/build.js @@ -166,7 +166,7 @@ function buildJsFileFor( file, silent, environment ) { const destPath = getBuildPath( file, buildDir ); const babelOptions = getBabelConfig( environment ); babelOptions.sourceMaps = true; - babelOptions.sourceFileName = file; + babelOptions.sourceFileName = file.replace( PACKAGES_DIR, '@wordpress' ); mkdirp.sync( path.dirname( destPath ) ); const transformed = babel.transformFileSync( file, babelOptions ); From 1e541fb4ceacbabc3ca8aa6d7a3c95532891d09b Mon Sep 17 00:00:00 2001 From: Dave Whitley Date: Wed, 13 Mar 2019 10:05:16 -0500 Subject: [PATCH 05/33] Components: update Button readme to add design guidelines (#14194) These changes add design documentation. Co-Authored-By: kjellr --- packages/components/src/button/README.md | 123 ++++++++++++++++++++--- 1 file changed, 110 insertions(+), 13 deletions(-) diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md index 7f7667004b7de..53b857f00c434 100644 --- a/packages/components/src/button/README.md +++ b/packages/components/src/button/README.md @@ -1,29 +1,126 @@ # Button +Buttons let users take actions and make choices with a single click or tap. -Buttons express what action will occur when the user clicks or taps it. Buttons are used to trigger an action, and they can be used for any type of action, including navigation. +![Button components](https://make.wordpress.org/design/files/2019/03/button.png) -The presence of a `href` prop determines whether an `anchor` element is rendered instead of a `button`. +## Table of contents + +1. [Design guidelines](#design-guidelines) +2. [Development guidelines](#development-guidelines) +3. [Related components](#related-components) + +## Design guidelines + +### Usage + +Buttons tell users what actions they can take and give them a way to interact with the interface. You’ll find them throughout a UI, particularly in places like: + +- Modals +- Forms +- Toolbars + +### Best practices + +Buttons should: + +- **Be clearly and accurately labeled.** +- **Clearly communicate that clicking or tapping will trigger an action.** +- **Use established colors appropriately.** For example, only use red buttons for actions that are difficult or impossible to undo. +- **Prioritize the most important actions.** This helps users focus. Too many calls to action on one screen can be confusing, making users unsure what to do next. +- **Have consistent locations in the interface.** + +### Content guidelines + +Buttons should be clear and predictable—users should be able to anticipate what will happen when they click a button. Never deceive a user by mislabeling a button. + +Buttons text should lead with a strong verb that encourages action, and add a noun that clarifies what will actually change. The only exceptions are common actions like Save, Close, Cancel, or OK. Otherwise, use the {verb}+{noun} format to ensure that your button gives the user enough information. + +Button text should also be quickly scannable — avoid unnecessary words and articles like the, an, or a. + +### Types + +#### Link button + +Link buttons have low emphasis. They don’t stand out much on the page, so they’re used for less-important actions. What’s less important can vary based on context, but it’s usually a supplementary action to the main action we want someone to take. Link buttons are also useful when you don’t want to distract from the content. + +![Link button](https://make.wordpress.org/design/files/2019/03/link-button.png) + +#### Default button + +Default buttons have medium emphasis. The button appearance helps differentiate them from the page background, so they’re useful when you want more emphasis than a link button offers. + +![Default button](https://make.wordpress.org/design/files/2019/03/default-button.png) + +#### Primary button + +Primary buttons have high emphasis. Their color fill and shadow means they pop off the background. + +Since a high-emphasis button commands the most attention, a layout should contain a single primary button. This makes it clear that other buttons have less importance and helps users understand when an action requires their attention. -Note that this component may sometimes be confused with the Button block, which has semantically different use cases and functionality. +![Primary button](https://make.wordpress.org/design/files/2019/03/primary-button.png) -## Usage +#### Text label + +All button types use text labels to describe the action that happens when a user taps a button. If there’s no text label, there should be an icon to signify what the button does (e.g. an IconButton component). + +![](https://make.wordpress.org/design/files/2019/03/do-link-button.png) + +**Do** +Use color to distinguish link button labels from other text. + +![](https://make.wordpress.org/design/files/2019/03/dont-wrap-button-text.png) + +**Don’t** +Don’t wrap button text. For maximum legibility, keep text labels on a single line. + +### Hierarchy + +![A layout with a single prominent button](https://make.wordpress.org/design/files/2019/03/button.png) + +A layout should contain a single prominently-located button. If multiple buttons are required, a single high-emphasis button can be joined by medium- and low-emphasis buttons mapped to less-important actions. When using multiple buttons, make sure the available state of one button doesn’t look like the disabled state of another. + +![A diagram showing high emphasis at the top, medium emphasis in the middle, and low emphasis at the bottom](https://make.wordpress.org/design/files/2019/03/button-hierarchy.png) + +A button’s level of emphasis helps determine its appearance, typography, and placement. + +#### Placement + +Use button types to express different emphasis levels for all the actions a user can perform. + +![A link, default, and primary button](https://make.wordpress.org/design/files/2019/03/button-layout.png) + +This screen layout uses: + +1. A primary button for high emphasis. +2. A default button for medium emphasis. +3. A link button for low emphasis. + +Placement best practices: + +- **Do**: When using multiple buttons in a row, show users which action is more important by placing it next to a button with a lower emphasis (e.g. a primary button next to a default button, or a default button next to a link button). +- **Don’t**: Don’t place two primary buttons next to one another — they compete for focus. Only use one primary button per view. +- **Don’t**: Don’t place a button below another button if there is space to place them side by side. +- **Caution**: Avoid using too many buttons on a single page. When designing pages in the app or website, think about the most important actions for users to take. Too many calls to action can cause confusion and make users unsure what to do next — we always want users to feel confident and capable. + +## Development guidelines + +### Usage Renders a button with default style. -{% codetabs %} -{% ESNext %} ```jsx import { Button } from "@wordpress/components"; const MyButton = () => ( - + +Click me! + ); ``` -{% end %} -## Props +### Props + +The presence of a `href` prop determines whether an `anchor` element is rendered instead of a `button`. Name | Type | Default | Description --- | --- | --- | --- @@ -39,5 +136,5 @@ Name | Type | Default | Description ## Related components -* To group buttons together, use the `ButtonGroup` component. -* To display an icon inside the button, use the `IconButton` component. +- To group buttons together, use the `ButtonGroup` component. +- To display an icon inside the button, use the `IconButton` component. From c7d14ca3b988e690b141aeefe1da5592f75a735e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 13 Mar 2019 16:26:06 +0100 Subject: [PATCH 06/33] Upgrade React to 16.8.4: Welcome React Hooks (#14400) --- lib/client-assets.php | 19 +- package-lock.json | 209 +++--------------- package.json | 5 +- packages/element/README.md | 60 +++++ packages/element/package.json | 4 +- packages/element/src/react.js | 26 +++ phpunit/class-vendor-script-filename-test.php | 8 +- 7 files changed, 142 insertions(+), 189 deletions(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 801aefe8c9177..3b0f4ae6d0137 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -380,10 +380,21 @@ function gutenberg_register_scripts_and_styles() { * @since 0.1.0 */ function gutenberg_register_vendor_scripts() { - /* - * This function is kept as an empty stub, in case Gutenberg should need to - * explicitly provide a version newer than that provided by core. - */ + $suffix = SCRIPT_DEBUG ? '' : '.min'; + + // Vendor Scripts. + $react_suffix = ( SCRIPT_DEBUG ? '.development' : '.production' ) . $suffix; + + gutenberg_register_vendor_script( + 'react', + 'https://unpkg.com/react@16.8.4/umd/react' . $react_suffix . '.js', + array( 'wp-polyfill' ) + ); + gutenberg_register_vendor_script( + 'react-dom', + 'https://unpkg.com/react-dom@16.8.4/umd/react-dom' . $react_suffix . '.js', + array( 'react' ) + ); } /** diff --git a/package-lock.json b/package-lock.json index d01d93ab49028..f1897ceec693d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2842,8 +2842,8 @@ "@babel/runtime": "^7.3.1", "@wordpress/escape-html": "file:packages/escape-html", "lodash": "^4.17.11", - "react": "^16.6.3", - "react-dom": "^16.6.3" + "react": "^16.8.4", + "react-dom": "^16.8.4" } }, "@wordpress/escape-html": { @@ -3203,17 +3203,6 @@ "object.entries": "^1.0.4", "prop-types": "^15.6.1", "prop-types-exact": "^1.1.2" - }, - "dependencies": { - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } } }, "ajv": { @@ -7414,23 +7403,6 @@ "function-bind": "^1.1.1", "has": "^1.0.3" } - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "react-is": { - "version": "16.8.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz", - "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==", - "dev": true } } }, @@ -7447,23 +7419,6 @@ "semver": "^5.6.0" }, "dependencies": { - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "react-is": { - "version": "16.8.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.2.tgz", - "integrity": "sha512-D+NxhSR2HUCjYky1q1DwpNUD44cDpUXzSmmFyC3ug1bClcU/iDNy0YNn1iwme28fn+NFhpA13IndOd42CrFb+Q==", - "dev": true - }, "semver": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", @@ -7778,23 +7733,6 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "react-is": { - "version": "16.8.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz", - "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==", - "dev": true - }, "resolve": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", @@ -10027,6 +9965,11 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, "homedir-polyfill": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", @@ -17770,12 +17713,13 @@ } }, "prop-types": { - "version": "15.5.10", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", - "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.3.1" + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" } }, "prop-types-exact": { @@ -18012,25 +17956,14 @@ "integrity": "sha512-pLJkPbZCe+3ml+9Q15z+R69qYZDsluj0KwrdFb8kSNaqDzYAveDUblf7voHH9hNTdKIiIvP8iIdGFFKSgffVaQ==" }, "react": { - "version": "16.6.3", - "resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz", - "integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==", + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.4.tgz", + "integrity": "sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.11.2" - }, - "dependencies": { - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } + "scheduler": "^0.13.4" } }, "react-addons-shallow-compare": { @@ -18085,46 +18018,23 @@ "react-portal": "^4.1.5", "react-with-styles": "^3.2.0", "react-with-styles-interface-css": "^4.0.2" - }, - "dependencies": { - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } } }, "react-dom": { - "version": "16.6.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.3.tgz", - "integrity": "sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==", + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.4.tgz", + "integrity": "sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.11.2" - }, - "dependencies": { - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } + "scheduler": "^0.13.4" } }, "react-is": { - "version": "16.6.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz", - "integrity": "sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA==", - "dev": true + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.4.tgz", + "integrity": "sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==" }, "react-moment-proptypes": { "version": "1.6.0", @@ -18143,17 +18053,6 @@ "consolidated-events": "^1.1.1 || ^2.0.0", "object.values": "^1.0.4", "prop-types": "^15.6.1" - }, - "dependencies": { - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } } }, "react-portal": { @@ -18165,27 +18064,15 @@ } }, "react-test-renderer": { - "version": "16.6.3", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.6.3.tgz", - "integrity": "sha512-B5bCer+qymrQz/wN03lT0LppbZUDRq6AMfzMKrovzkGzfO81a9T+PWQW6MzkWknbwODQH/qpJno/yFQLX5IWrQ==", + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.4.tgz", + "integrity": "sha512-jQ9Tf/ilIGSr55Cz23AZ/7H3ABEdo9oy2zF9nDHZyhLHDSLKuoILxw2ifpBfuuwQvj4LCoqdru9iZf7gwFH28A==", "dev": true, "requires": { "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "react-is": "^16.6.3", - "scheduler": "^0.11.2" - }, - "dependencies": { - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "dev": true, - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } + "react-is": "^16.8.4", + "scheduler": "^0.13.4" } }, "react-with-direction": { @@ -18201,22 +18088,6 @@ "object.assign": "^4.1.0", "object.values": "^1.0.4", "prop-types": "^15.6.0" - }, - "dependencies": { - "hoist-non-react-statics": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", - "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" - }, - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } } }, "react-with-styles": { @@ -18228,22 +18099,6 @@ "hoist-non-react-statics": "^2.5.0", "prop-types": "^15.6.1", "react-with-direction": "^1.3.0" - }, - "dependencies": { - "hoist-non-react-statics": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", - "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" - }, - "prop-types": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } } }, "react-with-styles-interface-css": { @@ -19333,9 +19188,9 @@ "dev": true }, "scheduler": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz", - "integrity": "sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==", + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.4.tgz", + "integrity": "sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" diff --git a/package.json b/package.json index 1f0e798ff37f1..6e071182be1a5 100644 --- a/package.json +++ b/package.json @@ -106,8 +106,9 @@ "pegjs": "0.10.0", "phpegjs": "1.0.0-beta7", "postcss": "7.0.13", - "react-dom": "16.6.3", - "react-test-renderer": "16.6.3", + "react": "16.8.4", + "react-dom": "16.8.4", + "react-test-renderer": "16.8.4", "redux": "4.0.0", "rimraf": "2.6.2", "rtlcss": "2.4.0", diff --git a/packages/element/README.md b/packages/element/README.md index ae28a29cf6ec2..c39eb933ec8b9 100755 --- a/packages/element/README.md +++ b/packages/element/README.md @@ -314,6 +314,66 @@ Removes any mounted element from the target DOM node. - **target** `Element`: DOM node in which element is to be removed +### useCallback + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useContext + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useDebugValue + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useEffect + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useImperativeHandle + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useLayoutEffect + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useMemo + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useReducer + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useRef + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + +### useState + +[src/index.js#L1-L1](src/index.js#L1-L1) + +Make React Hooks available + diff --git a/packages/element/package.json b/packages/element/package.json index 3702a0b559a4d..3df21197c4278 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -25,8 +25,8 @@ "@babel/runtime": "^7.3.1", "@wordpress/escape-html": "file:../escape-html", "lodash": "^4.17.11", - "react": "^16.6.3", - "react-dom": "^16.6.3" + "react": "^16.8.4", + "react-dom": "^16.8.4" }, "publishConfig": { "access": "public" diff --git a/packages/element/src/react.js b/packages/element/src/react.js index 703a0455d0a16..a2fbd1cda3671 100644 --- a/packages/element/src/react.js +++ b/packages/element/src/react.js @@ -12,6 +12,16 @@ import { Fragment, isValidElement, StrictMode, + useState, + useEffect, + useContext, + useReducer, + useCallback, + useMemo, + useRef, + useImperativeHandle, + useLayoutEffect, + useDebugValue, } from 'react'; import { isString } from 'lodash'; @@ -99,6 +109,22 @@ export { isValidElement }; */ export { StrictMode }; +/** + * Make React Hooks available + */ +export { + useCallback, + useContext, + useDebugValue, + useEffect, + useImperativeHandle, + useLayoutEffect, + useMemo, + useReducer, + useRef, + useState, +}; + /** * Concatenate two or more React children objects. * diff --git a/phpunit/class-vendor-script-filename-test.php b/phpunit/class-vendor-script-filename-test.php index 0ee7a7b78b83d..e3febd3557f26 100644 --- a/phpunit/class-vendor-script-filename-test.php +++ b/phpunit/class-vendor-script-filename-test.php @@ -11,23 +11,23 @@ function vendor_script_filename_cases() { // Development mode scripts. array( 'react-handle', - 'https://unpkg.com/react@16.6.3/umd/react.development.js', + 'https://unpkg.com/react@16.8.4/umd/react.development.js', 'react-handle.HASH.js', ), array( 'react-dom-handle', - 'https://unpkg.com/react-dom@16.6.3/umd/react-dom.development.js', + 'https://unpkg.com/react-dom@16.8.4/umd/react-dom.development.js', 'react-dom-handle.HASH.js', ), // Production mode scripts. array( 'react-handle', - 'https://unpkg.com/react@16.6.3/umd/react.production.min.js', + 'https://unpkg.com/react@16.8.4/umd/react.production.min.js', 'react-handle.min.HASH.js', ), array( 'react-dom-handle', - 'https://unpkg.com/react-dom@16.6.3/umd/react-dom.production.min.js', + 'https://unpkg.com/react-dom@16.8.4/umd/react-dom.production.min.js', 'react-dom-handle.min.HASH.js', ), // Other cases. From a06f9312633dabaa31f4559e63003a4ce00d0807 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Wed, 13 Mar 2019 10:29:56 -0600 Subject: [PATCH 07/33] Document outline: Use links not buttons (#10815) * Adjust document outline to use an a tag vs button * target href links directly to page anchors, remove onClick handler * update test snapshot * update snapshot * better titleNode targeting * update snapshot * update snapshot * Close the table of contents panel when a link is clicked * add deterministic block id to tests * remove redundant screen reader text * Adjust map to avoid mutating original object * update snapshot * Update packages/editor/src/components/document-outline/index.js Co-Authored-By: adamsilverstein * Update packages/editor/src/components/document-outline/test/index.js remove leading _ Co-Authored-By: adamsilverstein * Update packages/editor/src/components/document-outline/index.js Co-Authored-By: adamsilverstein * update snapshot * update snapshots * update snapshot * update snapshots * fix up e2e tests * Fix snapshots by removing single quotes from outline links * Update packages/editor/src/components/document-outline/index.js Co-Authored-By: adamsilverstein * change target to href * rename close -> closeOutline * update snapshots after property name changes * rename close/onClose -> onRequestClose for TOC, on * restore onSelect * complete renaming * Block links are only valid for the current session - remove hash after following * update snapshot * cleanup; move block id hash removal functionality up to document outline; now includes title * update snapshot * use replaceState vs pushState * use defer and import at top of file * removeURLHash as helper * remove passing event in select handler * improve doc block * Skip title in outline when title node not found * remove removeURLHash --- .../src/components/document-outline/index.js | 29 +++++-------------- .../src/components/document-outline/item.js | 14 ++++----- .../components/document-outline/style.scss | 4 +++ .../test/__snapshots__/index.js.snap | 8 ++--- .../components/document-outline/test/index.js | 15 ++++++++-- .../src/components/table-of-contents/index.js | 2 +- .../src/components/table-of-contents/panel.js | 4 +-- 7 files changed, 37 insertions(+), 39 deletions(-) diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js index b73af4a92d087..5a61503909a88 100644 --- a/packages/editor/src/components/document-outline/index.js +++ b/packages/editor/src/components/document-outline/index.js @@ -8,7 +8,7 @@ import { countBy, flatMap, get } from 'lodash'; */ import { __ } from '@wordpress/i18n'; import { compose } from '@wordpress/compose'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { withSelect } from '@wordpress/data'; import { create, getTextContent, @@ -73,18 +73,9 @@ export const DocumentOutline = ( { blocks = [], title, onSelect, isTitleSupporte let prevHeadingLevel = 1; - // Select the corresponding block in the main editor - // when clicking on a heading item from the list. - const onSelectHeading = ( clientId ) => onSelect( clientId ); - const focusTitle = () => { - // Not great but it's the simplest way to focus the title right now. - const titleNode = document.querySelector( '.editor-post-title__input' ); - if ( titleNode ) { - titleNode.focus(); - } - }; - - const hasTitle = isTitleSupported && title; + // Not great but it's the simplest way to locate the title right now. + const titleNode = document.querySelector( '.editor-post-title__input' ); + const hasTitle = isTitleSupported && title && titleNode; const countByLevel = countBy( headings, 'level' ); const hasMultipleH1 = countByLevel[ 1 ] > 1; @@ -95,7 +86,8 @@ export const DocumentOutline = ( { blocks = [], title, onSelect, isTitleSupporte { title } @@ -119,9 +111,10 @@ export const DocumentOutline = ( { blocks = [], title, onSelect, isTitleSupporte key={ index } level={ `H${ item.level }` } isValid={ isValid } - onClick={ () => onSelectHeading( item.clientId ) } path={ item.path } isDisabled={ hasOutlineItemsDisabled } + href={ `#block-${ item.clientId }` } + onSelect={ onSelect } > { item.isEmpty ? emptyHeadingContent : @@ -152,11 +145,5 @@ export default compose( blocks: getBlocks(), isTitleSupported: get( postType, [ 'supports', 'title' ], false ), }; - } ), - withDispatch( ( dispatch ) => { - const { selectBlock } = dispatch( 'core/block-editor' ); - return { - onSelect: selectBlock, - }; } ) )( DocumentOutline ); diff --git a/packages/editor/src/components/document-outline/item.js b/packages/editor/src/components/document-outline/item.js index c0638100d25f1..1a92e7dbafc4e 100644 --- a/packages/editor/src/components/document-outline/item.js +++ b/packages/editor/src/components/document-outline/item.js @@ -6,16 +6,15 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; import { BlockTitle } from '@wordpress/block-editor'; const TableOfContentsItem = ( { children, isValid, level, - onClick, - isDisabled, path = [], + href, + onSelect, } ) => (
  • - +
  • ); diff --git a/packages/editor/src/components/document-outline/style.scss b/packages/editor/src/components/document-outline/style.scss index 53008b91e6b3d..c02f815eb6f09 100644 --- a/packages/editor/src/components/document-outline/style.scss +++ b/packages/editor/src/components/document-outline/style.scss @@ -11,6 +11,10 @@ display: flex; margin: 4px 0; + a { + text-decoration: none; + } + .document-outline__emdash::before { color: $light-gray-500; margin-right: 4px; diff --git a/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap b/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap index 89d0ebb04d658..8f79943b2f5d9 100644 --- a/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap @@ -6,19 +6,19 @@ exports[`DocumentOutline header blocks present should match snapshot 1`] = ` >