diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 033a4f2c747abe..2c16a030415a72 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -130,8 +130,8 @@ _Parameters_ - _value_ `RichTextValue`: Value to modify. - _format_ `RichTextFormat`: Format to apply. -- _startIndex_ `[number]`: Start index. -- _endIndex_ `[number]`: End index. +- _startIndex_ `number | undefined`: Start index. +- _endIndex_ `number | undefined`: End index. _Returns_ @@ -172,7 +172,7 @@ _Parameters_ - _$1_ `[Object]`: Optional named arguments. - _$1.element_ `[Element]`: Element to create value from. - _$1.text_ `[string]`: Text to create value from. -- _$1.html_ `[string]`: HTML to create value from. +- _$1.html_ `[string | RichTextData]`: HTML string or RichTextData to create value from. - _$1.range_ `[Range]`: Range to create value from. - _$1.\_\_unstableIsEditableTree_ `[boolean]`: @@ -191,11 +191,11 @@ _Parameters_ _Returns_ -- `RichTextFormat|undefined`: Active format object of the specified type, or undefined. +- `RichTextFormat | undefined`: Active format object of the specified type, or undefined. ### getActiveFormats -Gets the all format objects at the start of the selection. +Gets all format objects at the start of the selection. _Parameters_ @@ -216,7 +216,7 @@ _Parameters_ _Returns_ -- `RichTextFormat|void`: Active object, or undefined. +- `RichTextFormat | undefined`: Active object, or undefined. ### getTextContent @@ -266,9 +266,9 @@ Check if the selection of a Rich Text value is collapsed or not. Collapsed means _Parameters_ -- _props_ `RichTextValue`: The rich text value to check. -- _props.start_ `RichTextValue[ 'start' ]`: -- _props.end_ `RichTextValue[ 'end' ]`: +- _props_ `{ start?: number; end?: number; }`: The rich text value to check. +- _props.start_ `number`: +- _props.end_ `number`: _Returns_ @@ -276,7 +276,7 @@ _Returns_ ### isEmpty -Check if a Rich Text value is Empty, meaning it contains no text or any objects (such as images). +Check if a Rich Text value is empty, meaning it contains no text or any objects (such as images). _Parameters_ @@ -292,8 +292,8 @@ Combine an array of Rich Text values into one, optionally separated by `separato _Parameters_ -- _values_ `Array`: An array of values to join. -- _separator_ `[string|RichTextValue]`: Separator string or value. +- _values_ `Array< RichTextValue >`: An array of values to join. +- _separator_ `string | RichTextValue`: Separator string or value. _Returns_ @@ -305,12 +305,12 @@ Registers a new format provided a unique name and an object defining its behavio _Parameters_ -- _name_ `string`: Format name. -- _settings_ `WPFormat`: Format settings. +- _name_ `string`: - Format name. +- _settings_ `WPFormat`: - Format settings. _Returns_ -- `WPFormat|undefined`: The format, if it has been successfully registered; otherwise `undefined`. +- `WPFormat | undefined`: The format, if it has been successfully registered; otherwise `undefined`. ### remove @@ -319,8 +319,8 @@ Remove content from a Rich Text value between the given `startIndex` and `endInd _Parameters_ - _value_ `RichTextValue`: Value to modify. -- _startIndex_ `[number]`: Start index. -- _endIndex_ `[number]`: End index. +- _startIndex_ `number`: Start index. +- _endIndex_ `number`: End index. _Returns_ @@ -347,9 +347,9 @@ Search a Rich Text value and replace the match(es) with `replacement`. This is s _Parameters_ -- _value_ `RichTextValue`: The value to modify. -- _pattern_ `RegExp|string`: A RegExp object or literal. Can also be a string. It is treated as a verbatim string and is not interpreted as a regular expression. Only the first occurrence will be replaced. -- _replacement_ `Function|string`: The match or matches are replaced with the specified or the value returned by the specified function. +- _value_ `RichTextValue`: - The value to modify. +- _pattern_ `RegExp|string`: - A RegExp object or literal. Can also be a string. It is treated as a verbatim string and is not interpreted as a regular expression. Only the first occurrence will be replaced. +- _replacement_ `Function|string|RichTextValue`: - The match or matches are replaced with the specified or the value returned by the specified function. _Returns_ @@ -379,8 +379,8 @@ Slice a Rich Text value from `startIndex` to `endIndex`. Indices are retrieved f _Parameters_ - _value_ `RichTextValue`: Value to modify. -- _startIndex_ `[number]`: Start index. -- _endIndex_ `[number]`: End index. +- _startIndex_ `number | undefined`: Start index. +- _endIndex_ `number | undefined`: End index. _Returns_ @@ -392,12 +392,17 @@ Split a Rich Text value in two at the given `startIndex` and `endIndex`, or spli _Parameters_ -- _value_ `RichTextValue`: -- _string_ `[number|string]`: Start index, or string at which to split. +- _value_ `RichTextValue`: The rich text value to split. +- _value.formats_ `RichTextValue[ 'formats' ]`: +- _value.replacements_ `RichTextValue[ 'replacements' ]`: +- _value.text_ `RichTextValue[ 'text' ]`: +- _value.start_ `RichTextValue[ 'start' ]`: +- _value.end_ `RichTextValue[ 'end' ]`: +- _string_ `number | string`: Start index, or string at which to split. _Returns_ -- `Array|undefined`: An array of new values. +- `Array< RichTextValue > | undefined`: An array of new values. ### store @@ -417,8 +422,8 @@ Toggles a format object to a Rich Text value at the current selection. _Parameters_ -- _value_ `RichTextValue`: Value to modify. -- _format_ `RichTextFormat`: Format to apply or remove. +- _value_ `RichTextValue`: - Value to modify. +- _format_ `RichTextFormat`: - Format to apply or remove. _Returns_ @@ -430,9 +435,9 @@ Create an HTML string from a Rich Text value. _Parameters_ -- _$1_ `Object`: Named arguments. -- _$1.value_ `RichTextValue`: Rich text value. -- _$1.preserveWhiteSpace_ `[boolean]`: Preserves newlines if true. +- _options_ `{ value: RichTextValue; preserveWhiteSpace?: boolean; }`: - Named arguments. +- _options.value_ `RichTextValue`: - Rich text value. +- _options.preserveWhiteSpace_ `boolean`: - Preserves newlines if true. _Returns_ @@ -448,7 +453,7 @@ _Parameters_ _Returns_ -- `WPFormat|undefined`: The previous format value, if it has been successfully unregistered; otherwise `undefined`. +- `WPFormat | undefined`: The previous format value, if it has been successfully unregistered; otherwise `undefined`. ### useAnchor diff --git a/packages/rich-text/src/apply-format.js b/packages/rich-text/src/apply-format.ts similarity index 75% rename from packages/rich-text/src/apply-format.js rename to packages/rich-text/src/apply-format.ts index c10f09bec852e4..9d7262922acef1 100644 --- a/packages/rich-text/src/apply-format.js +++ b/packages/rich-text/src/apply-format.ts @@ -3,11 +3,9 @@ */ import { normaliseFormats } from './normalise-formats'; +import type { RichTextValue, RichTextFormat } from './types'; -/** @typedef {import('./types').RichTextValue} RichTextValue */ -/** @typedef {import('./types').RichTextFormat} RichTextFormat */ - -function replace( array, index, value ) { +function replace< T >( array: T[], index: number, value: T ): T[] { array = array.slice(); array[ index ] = value; return array; @@ -18,26 +16,31 @@ function replace( array, index, value ) { * given `endIndex`. Indices are retrieved from the selection if none are * provided. * - * @param {RichTextValue} value Value to modify. - * @param {RichTextFormat} format Format to apply. - * @param {number} [startIndex] Start index. - * @param {number} [endIndex] End index. + * @param value Value to modify. + * @param format Format to apply. + * @param startIndex Start index. + * @param endIndex End index. * - * @return {RichTextValue} A new value with the format applied. + * @return A new value with the format applied. */ export function applyFormat( - value, - format, - startIndex = value.start, - endIndex = value.end -) { + value: RichTextValue, + format: RichTextFormat, + startIndex: number | undefined = value.start, + endIndex: number | undefined = value.end +): RichTextValue { + // TODO: This is copied from other files, doing the same check. + if ( startIndex === undefined || endIndex === undefined ) { + return { ...value }; + } + const { formats, activeFormats } = value; const newFormats = formats.slice(); // The selection is collapsed. if ( startIndex === endIndex ) { const startFormat = newFormats[ startIndex ]?.find( - ( { type } ) => type === format.type + ( newFormat ) => newFormat?.type === format.type ); // If the caret is at a format of the same type, expand start and end to @@ -78,7 +81,7 @@ export function applyFormat( for ( let index = startIndex; index < endIndex; index++ ) { if ( newFormats[ index ] ) { newFormats[ index ] = newFormats[ index ].filter( - ( { type } ) => type !== format.type + ( newFormat ) => newFormat?.type !== format.type ); const length = newFormats[ index ].length; diff --git a/packages/rich-text/src/concat.js b/packages/rich-text/src/concat.ts similarity index 58% rename from packages/rich-text/src/concat.js rename to packages/rich-text/src/concat.ts index 499015166dfa6b..85d328fe094a68 100644 --- a/packages/rich-text/src/concat.js +++ b/packages/rich-text/src/concat.ts @@ -4,19 +4,18 @@ import { normaliseFormats } from './normalise-formats'; import { create } from './create'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextValue } from './types'; /** - * Concats a pair of rich text values. Not that this mutates `a` and does NOT + * Concatenates a pair of rich text values. Note that this mutates `a` and does NOT * normalise formats! * - * @param {Object} a Value to mutate. - * @param {Object} b Value to add read from. + * @param {RichTextValue} a Value to mutate. + * @param {RichTextValue} b Value to add read from. * - * @return {Object} `a`, mutated. + * @return {RichTextValue} `a`, mutated. */ -export function mergePair( a, b ) { +export function mergePair( a: RichTextValue, b: RichTextValue ): RichTextValue { a.formats = a.formats.concat( b.formats ); a.replacements = a.replacements.concat( b.replacements ); a.text += b.text; @@ -32,6 +31,6 @@ export function mergePair( a, b ) { * * @return {RichTextValue} A new value combining all given records. */ -export function concat( ...values ) { +export function concat( ...values: RichTextValue[] ): RichTextValue { return normaliseFormats( values.reduce( mergePair, create() ) ); } diff --git a/packages/rich-text/src/create-element.js b/packages/rich-text/src/create-element.js deleted file mode 100644 index eebbe450075e80..00000000000000 --- a/packages/rich-text/src/create-element.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Parse the given HTML into a body element. - * - * Note: The current implementation will return a shared reference, reset on - * each call to `createElement`. Therefore, you should not hold a reference to - * the value to operate upon asynchronously, as it may have unexpected results. - * - * @param {HTMLDocument} document The HTML document to use to parse. - * @param {string} html The HTML to parse. - * - * @return {HTMLBodyElement} Body element with parsed HTML. - */ -export function createElement( { implementation }, html ) { - // Because `createHTMLDocument` is an expensive operation, and with this - // function being internal to `rich-text` (full control in avoiding a risk - // of asynchronous operations on the shared reference), a single document - // is reused and reset for each call to the function. - if ( ! createElement.body ) { - createElement.body = implementation.createHTMLDocument( '' ).body; - } - - createElement.body.innerHTML = html; - - return createElement.body; -} diff --git a/packages/rich-text/src/create-element.ts b/packages/rich-text/src/create-element.ts new file mode 100644 index 00000000000000..a2386794d33ed7 --- /dev/null +++ b/packages/rich-text/src/create-element.ts @@ -0,0 +1,35 @@ +/** + * Parse the given HTML into a body element. + * + * Note: The current implementation will return a shared reference, reset on + * each call to `createElement`. Therefore, you should not hold a reference to + * the value to operate upon asynchronously, as it may have unexpected results. + * + * @param options Options object. + * @param options.implementation The HTML implementation to use. + * @param options.implementation.createHTMLDocument + * @param html The HTML to parse. + * + * @return Body element with parsed HTML. + */ +export function createElement( + { + implementation, + }: { + implementation: { createHTMLDocument: ( title: string ) => Document }; + }, + html: string +): HTMLElement { + // Because `createHTMLDocument` is an expensive operation, and with this + // function being internal to `rich-text` (full control in avoiding a risk + // of asynchronous operations on the shared reference), a single document + // is reused and reset for each call to the function. + if ( ! ( createElement as any ).body ) { + ( createElement as any ).body = + implementation.createHTMLDocument( '' ).body; + } + + ( createElement as any ).body.innerHTML = html; + + return ( createElement as any ).body; +} diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.ts similarity index 76% rename from packages/rich-text/src/create.js rename to packages/rich-text/src/create.ts index a8fc4b92c49600..35cf9b54bd3370 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.ts @@ -13,9 +13,18 @@ import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from './special-characters'; import { toHTMLString } from './to-html-string'; import { getTextContent } from './get-text-content'; -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -function createEmptyValue() { +import type { RichTextValue } from './types'; + +// interface CreateEmptyValue { +// formats: any[]; +// replacements: any[]; +// text: string; +// } +// TODO: There are a few instances where a type of RichTextValue is used without the start and end properties. +// type CreateEmptyValue = Omit< RichTextValue, 'start' | 'end' >; +type CreateEmptyValue = RichTextValue; + +function createEmptyValue(): CreateEmptyValue { return { formats: [], replacements: [], @@ -23,10 +32,31 @@ function createEmptyValue() { }; } -function toFormat( { tagName, attributes } ) { +interface ToFormatAttributes { + class?: string; + [ key: string ]: any; +} + +// TODO: This is very similar to RichTextFormat but has a formatType property. +interface ToFormatResult { + type: string; + formatType?: any; + tagName?: string; + attributes?: Record< string, any >; + unregisteredAttributes?: Record< string, any >; +} + +function toFormat( { + tagName, + attributes, +}: { + tagName: string; + attributes?: ToFormatAttributes; +} ): ToFormatResult | null { let formatType; if ( attributes && attributes.class ) { + // @ts-expect-error Stores are not typed formatType = select( richTextStore ).getFormatTypeForClassName( attributes.class ); @@ -45,6 +75,7 @@ function toFormat( { tagName, attributes } ) { if ( ! formatType ) { formatType = + // @ts-expect-error Stores are not typed select( richTextStore ).getFormatTypeForBareElement( tagName ); } @@ -63,8 +94,8 @@ function toFormat( { tagName, attributes } ) { return { formatType, type: formatType.name, tagName }; } - const registeredAttributes = {}; - const unregisteredAttributes = {}; + const registeredAttributes: Record< string, any > = {}; + const unregisteredAttributes: Record< string, any > = {}; const _attributes = { ...attributes }; for ( const key in formatType.attributes ) { @@ -114,25 +145,29 @@ function toFormat( { tagName, attributes } ) { * @todo Add methods to manipulate the data, such as applyFormat, slice etc. */ export class RichTextData { - #value; + private value: CreateEmptyValue; - static empty() { + static empty(): RichTextData { return new RichTextData(); } - static fromPlainText( text ) { + static fromPlainText( text: string ): RichTextData { return new RichTextData( create( { text } ) ); } - static fromHTMLString( html ) { + static fromHTMLString( html: string ): RichTextData { return new RichTextData( create( { html } ) ); } /** * Create a RichTextData instance from an HTML element. * - * @param {HTMLElement} htmlElement The HTML element to create the instance from. - * @param {{preserveWhiteSpace?: boolean}} options Options. + * @param {HTMLElement} htmlElement The HTML element to create the instance from. + * @param {{preserveWhiteSpace?: boolean}} options Options. + * @param options.preserveWhiteSpace * @return {RichTextData} The RichTextData instance. */ - static fromHTMLElement( htmlElement, options = {} ) { + static fromHTMLElement( + htmlElement: HTMLElement, + options: { preserveWhiteSpace?: boolean } = {} + ): RichTextData { const { preserveWhiteSpace = false } = options; const element = preserveWhiteSpace ? htmlElement @@ -143,11 +178,11 @@ export class RichTextData { } ); return richTextData; } - constructor( init = createEmptyValue() ) { - this.#value = init; + constructor( init: CreateEmptyValue = createEmptyValue() ) { + this.value = init; } - toPlainText() { - return getTextContent( this.#value ); + toPlainText(): string { + return getTextContent( this.value ); } // We could expose `toHTMLElement` at some point as well, but we'd only use // it internally. @@ -157,32 +192,34 @@ export class RichTextData { * @param {{preserveWhiteSpace?: boolean}} options Options. * @return {string} The HTML string. */ - toHTMLString( { preserveWhiteSpace } = {} ) { + toHTMLString( { + preserveWhiteSpace, + }: { preserveWhiteSpace?: boolean } = {} ): string { return ( - this.originalHTML || - toHTMLString( { value: this.#value, preserveWhiteSpace } ) + ( this as any ).originalHTML || + toHTMLString( { value: this.value, preserveWhiteSpace } ) ); } - valueOf() { + valueOf(): string { return this.toHTMLString(); } - toString() { + toString(): string { return this.toHTMLString(); } - toJSON() { + toJSON(): string { return this.toHTMLString(); } - get length() { + get length(): number { return this.text.length; } - get formats() { - return this.#value.formats; + get formats(): any[] { + return this.value.formats; } - get replacements() { - return this.#value.replacements; + get replacements(): any[] { + return this.value.replacements; } - get text() { - return this.#value.text; + get text(): string { + return this.value.text; } } @@ -192,7 +229,7 @@ for ( const name of Object.getOwnPropertyNames( String.prototype ) ) { } Object.defineProperty( RichTextData.prototype, name, { - value( ...args ) { + value( ...args: any[] ) { // Should we convert back to RichTextData? return this.toHTMLString()[ name ]( ...args ); }, @@ -225,12 +262,12 @@ for ( const name of Object.getOwnPropertyNames( String.prototype ) ) { * `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. - * @param {string} [$1.html] HTML to create value from. - * @param {Range} [$1.range] Range to create value from. - * @param {boolean} [$1.__unstableIsEditableTree] + * @param {Object} [$1] Optional named arguments. + * @param {Element} [$1.element] Element to create value from. + * @param {string} [$1.text] Text to create value from. + * @param {string | RichTextData} [$1.html] HTML string or RichTextData to create value from. + * @param {Range} [$1.range] Range to create value from. + * @param {boolean} [$1.__unstableIsEditableTree] * @return {RichTextValue} A rich text value. */ export function create( { @@ -239,7 +276,13 @@ export function create( { html, range, __unstableIsEditableTree: isEditableTree, -} = {} ) { +}: { + element?: Element; + text?: string; + html?: string | RichTextData; + range?: Range; + __unstableIsEditableTree?: boolean; +} = {} ): RichTextValue { if ( html instanceof RichTextData ) { return { text: html.text, @@ -282,7 +325,12 @@ export function create( { * @param {Range} range Range to create value with. * @param {Object} value Value that is being accumulated. */ -function accumulateSelection( accumulator, node, range, value ) { +function accumulateSelection( + accumulator: any, + node: Node, + range: Range | null | undefined, + value: any +) { if ( ! range ) { return; } @@ -345,10 +393,15 @@ function accumulateSelection( accumulator, node, range, value ) { * @param {Range} range The range to filter. * @param {Function} filter Function to use to filter the text. * - * @return {Object|void} Object containing range properties. + * @return {Object|undefined} Object containing range properties. */ -function filterRange( node, range, filter ) { - if ( ! range ) { +function filterRange( + node: Node, + range: Range | null | undefined, + filter: ( text: string ) => string +) { + // TODO: Added null check + if ( ! range || node.nodeValue === null ) { return; } @@ -363,7 +416,12 @@ function filterRange( node, range, filter ) { endOffset = filter( node.nodeValue.slice( 0, endOffset ) ).length; } - return { startContainer, startOffset, endContainer, endOffset }; + // TODO: Return a actual range object + const filteredRange = range.cloneRange(); + filteredRange.setStart( node, startOffset ); + filteredRange.setEnd( node, endOffset ); + // return { startContainer, startOffset, endContainer, endOffset }; + return filteredRange; } /** @@ -383,11 +441,15 @@ function filterRange( node, range, filter ) { * * @return {HTMLElement} New element with collapsed whitespace. */ -function collapseWhiteSpace( element, isRoot = true ) { - const clone = element.cloneNode( true ); +function collapseWhiteSpace( + element: HTMLElement, + isRoot: boolean = true +): HTMLElement { + const clone = element.cloneNode( true ) as HTMLElement; clone.normalize(); Array.from( clone.childNodes ).forEach( ( node, i, nodes ) => { - if ( node.nodeType === node.TEXT_NODE ) { + // TODO: Added null check + if ( node.nodeType === node.TEXT_NODE && node.nodeValue !== null ) { let newNodeValue = node.nodeValue; if ( /[\n\t\r\f]/.test( newNodeValue ) ) { @@ -410,7 +472,7 @@ function collapseWhiteSpace( element, isRoot = true ) { node.nodeValue = newNodeValue; } else if ( node.nodeType === node.ELEMENT_NODE ) { - collapseWhiteSpace( node, false ); + collapseWhiteSpace( node as HTMLElement, false ); } } ); return clone; @@ -429,7 +491,7 @@ const CARRIAGE_RETURN = '\r'; * * @param {string} string */ -export function removeReservedCharacters( string ) { +export function removeReservedCharacters( string: string ): string { // with the global flag, note that we should create a new regex each time OR // reset lastIndex state. return string.replace( @@ -451,7 +513,15 @@ export function removeReservedCharacters( string ) { * * @return {RichTextValue} A rich text value. */ -function createFromElement( { element, range, isEditableTree } ) { +function createFromElement( { + element, + range, + isEditableTree, +}: { + element: Element; + range?: Range; + isEditableTree?: boolean; +} ): RichTextValue { const accumulator = createEmptyValue(); if ( ! element ) { @@ -467,13 +537,19 @@ function createFromElement( { element, range, isEditableTree } ) { // Optimise for speed. for ( let index = 0; index < length; index++ ) { - const node = element.childNodes[ index ]; + const node = element.childNodes[ index ] as HTMLElement; // TODO: This type isn't valid + // const node = element.children[ index ]; const tagName = node.nodeName.toLowerCase(); - if ( node.nodeType === node.TEXT_NODE ) { + if ( node.nodeType === node.TEXT_NODE && node.nodeValue !== null ) { const text = removeReservedCharacters( node.nodeValue ); - range = filterRange( node, range, removeReservedCharacters ); - accumulateSelection( accumulator, node, range, { text } ); + // TODO: Is this supposed to be shadowing the range parameter? + const filteredRange = filterRange( + node, + range, + removeReservedCharacters + ); + accumulateSelection( accumulator, node, filteredRange, { text } ); // Create a sparse array of the same length as `text`, in which // formats can be added. accumulator.formats.length += text.length; @@ -488,8 +564,8 @@ function createFromElement( { element, range, isEditableTree } ) { node.tagName === 'SPAN' && node.hasAttribute( 'data-rich-text-comment' ) ) ) { - const value = { - formats: [ , ], + const value: RichTextValue = { + formats: [ , ], // TODO: Is this correct? replacements: [ { type: '#comment', @@ -525,8 +601,8 @@ function createFromElement( { element, range, isEditableTree } ) { } if ( tagName === 'script' ) { - const value = { - formats: [ , ], + const value: RichTextValue = { + formats: [ , ], // TODO: Is this correct? replacements: [ { type: tagName, @@ -561,7 +637,7 @@ function createFromElement( { element, range, isEditableTree } ) { delete format.formatType; accumulateSelection( accumulator, node, range, createEmptyValue() ); mergePair( accumulator, { - formats: [ , ], + formats: [ , ], // TODO: Is this correct? replacements: [ { ...format, @@ -592,7 +668,7 @@ function createFromElement( { element, range, isEditableTree } ) { } else if ( value.text.length === 0 ) { if ( format.attributes ) { mergePair( accumulator, { - formats: [ , ], + formats: [], replacements: [ format ], text: OBJECT_REPLACEMENT_CHARACTER, } ); @@ -600,7 +676,7 @@ function createFromElement( { element, range, isEditableTree } ) { } else { // Indices should share a reference to the same formats array. // Only create a new reference if `formats` changes. - function mergeFormats( formats ) { + const mergeFormats = ( formats: any[] ) => { if ( mergeFormats.formats === formats ) { return mergeFormats.newFormats; } @@ -613,7 +689,7 @@ function createFromElement( { element, range, isEditableTree } ) { mergeFormats.newFormats = newFormats; return newFormats; - } + }; // Since the formats parameter can be `undefined`, preset // `mergeFormats` with a new reference. @@ -638,13 +714,17 @@ function createFromElement( { element, range, isEditableTree } ) { * @return {Object|void} Attribute object or `undefined` if the element has no * attributes. */ -function getAttributes( { element } ) { +function getAttributes( { + element, +}: { + element: Element; +} ): Record< string, any > | undefined { if ( ! element.hasAttributes() ) { return; } const length = element.attributes.length; - let accumulator; + let accumulator: Record< string, any > | undefined; // Optimise for speed. for ( let i = 0; i < length; i++ ) { diff --git a/packages/rich-text/src/get-active-format.js b/packages/rich-text/src/get-active-format.ts similarity index 64% rename from packages/rich-text/src/get-active-format.js rename to packages/rich-text/src/get-active-format.ts index 80c6f08ec88d9b..693674dcdfc7f2 100644 --- a/packages/rich-text/src/get-active-format.js +++ b/packages/rich-text/src/get-active-format.ts @@ -2,9 +2,7 @@ * Internal dependencies */ import { getActiveFormats } from './get-active-formats'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ -/** @typedef {import('./types').RichTextFormat} RichTextFormat */ +import type { RichTextValue, RichTextFormat } from './types'; /** * Gets the format object by type at the start of the selection. This can be @@ -15,10 +13,13 @@ import { getActiveFormats } from './get-active-formats'; * @param {RichTextValue} value Value to inspect. * @param {string} formatType Format type to look for. * - * @return {RichTextFormat|undefined} Active format object of the specified - * type, or undefined. + * @return {RichTextFormat | undefined} Active format object of the specified + * type, or undefined. */ -export function getActiveFormat( value, formatType ) { +export function getActiveFormat( + value: RichTextValue, + formatType: string +): RichTextFormat | undefined { return getActiveFormats( value ).find( ( { type } ) => type === formatType ); diff --git a/packages/rich-text/src/get-active-formats.js b/packages/rich-text/src/get-active-formats.ts similarity index 87% rename from packages/rich-text/src/get-active-formats.js rename to packages/rich-text/src/get-active-formats.ts index e3bc7d8415de58..8af3e34db29189 100644 --- a/packages/rich-text/src/get-active-formats.js +++ b/packages/rich-text/src/get-active-formats.ts @@ -1,13 +1,11 @@ -/** @typedef {import('./types').RichTextValue} RichTextValue */ -/** @typedef {import('./types').RichTextFormatList} RichTextFormatList */ - /** * Internal dependencies */ +import type { RichTextValue, RichTextFormatList } from './types'; import { isFormatEqual } from './is-format-equal'; /** - * Gets the all format objects at the start of the selection. + * Gets all format objects at the start of the selection. * * @param {RichTextValue} value Value to inspect. * @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no @@ -15,7 +13,10 @@ import { isFormatEqual } from './is-format-equal'; * * @return {RichTextFormatList} Active format objects. */ -export function getActiveFormats( value, EMPTY_ACTIVE_FORMATS = [] ) { +export function getActiveFormats( + value: RichTextValue, + EMPTY_ACTIVE_FORMATS: Array< any > = [] +): RichTextFormatList { const { formats, start, end, activeFormats } = value; if ( start === undefined ) { return EMPTY_ACTIVE_FORMATS; @@ -52,7 +53,7 @@ export function getActiveFormats( value, EMPTY_ACTIVE_FORMATS = [] ) { let i = selectedFormats.length; // For performance reasons, start from the end where it's much quicker to - // realise that there are no active formats. + // realize that there are no active formats. while ( i-- ) { const formatsAtIndex = selectedFormats[ i ]; diff --git a/packages/rich-text/src/get-active-object.js b/packages/rich-text/src/get-active-object.ts similarity index 50% rename from packages/rich-text/src/get-active-object.js rename to packages/rich-text/src/get-active-object.ts index a1e5623b65b73f..962f696c3c94c8 100644 --- a/packages/rich-text/src/get-active-object.js +++ b/packages/rich-text/src/get-active-object.ts @@ -3,18 +3,23 @@ */ import { OBJECT_REPLACEMENT_CHARACTER } from './special-characters'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ -/** @typedef {import('./types').RichTextFormat} RichTextFormat */ +import type { RichTextValue, RichTextFormat } from './types'; /** * Gets the active object, if there is any. * * @param {RichTextValue} value Value to inspect. * - * @return {RichTextFormat|void} Active object, or undefined. + * @return {RichTextFormat | undefined} Active object, or undefined. */ -export function getActiveObject( { start, end, replacements, text } ) { +export function getActiveObject( + value: RichTextValue +): RichTextFormat | undefined { + const { start, end, replacements, text } = value; + if ( start === undefined || end === undefined ) { + return; + } + if ( start + 1 !== end || text[ start ] !== OBJECT_REPLACEMENT_CHARACTER ) { return; } diff --git a/packages/rich-text/src/get-format-type.js b/packages/rich-text/src/get-format-type.ts similarity index 53% rename from packages/rich-text/src/get-format-type.js rename to packages/rich-text/src/get-format-type.ts index 48e41d41b9c795..6cf7caf2d563fb 100644 --- a/packages/rich-text/src/get-format-type.js +++ b/packages/rich-text/src/get-format-type.ts @@ -6,16 +6,16 @@ import { select } from '@wordpress/data'; * Internal dependencies */ import { store as richTextStore } from './store'; - -/** @typedef {import('./register-format-type').RichTextFormatType} RichTextFormatType */ +import type { WPFormat } from './types'; /** * Returns a registered format type. * - * @param {string} name Format name. + * @param name - Format name. * - * @return {RichTextFormatType|undefined} Format type. + * @return RichTextFormat | undefined - Format type. */ -export function getFormatType( name ) { +export function getFormatType( name: string ): WPFormat | undefined { + // @ts-expect-error Stores are not typed return select( richTextStore ).getFormatType( name ); } diff --git a/packages/rich-text/src/get-format-types.js b/packages/rich-text/src/get-format-types.ts similarity index 57% rename from packages/rich-text/src/get-format-types.js rename to packages/rich-text/src/get-format-types.ts index 74bec3a355aa62..9f53901ef7ed78 100644 --- a/packages/rich-text/src/get-format-types.js +++ b/packages/rich-text/src/get-format-types.ts @@ -6,14 +6,14 @@ import { select } from '@wordpress/data'; * Internal dependencies */ import { store as richTextStore } from './store'; - -/** @typedef {import('./register-format-type').RichTextFormatType} RichTextFormatType */ +import type { WPFormat } from './types'; /** * Returns all registered formats. * - * @return {Array} Format settings. + * @return Array Format settings. */ -export function getFormatTypes() { +export function getFormatTypes(): Array< WPFormat > { + // @ts-expect-error Stores are not typed return select( richTextStore ).getFormatTypes(); } diff --git a/packages/rich-text/src/get-text-content.js b/packages/rich-text/src/get-text-content.js deleted file mode 100644 index 71fc9e7a749f0b..00000000000000 --- a/packages/rich-text/src/get-text-content.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Internal dependencies - */ -import { OBJECT_REPLACEMENT_CHARACTER } from './special-characters'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -/** - * Get the textual content of a Rich Text value. This is similar to - * `Element.textContent`. - * - * @param {RichTextValue} value Value to use. - * - * @return {string} The text content. - */ -export function getTextContent( { text } ) { - return text.replace( OBJECT_REPLACEMENT_CHARACTER, '' ); -} diff --git a/packages/rich-text/src/get-text-content.ts b/packages/rich-text/src/get-text-content.ts new file mode 100644 index 00000000000000..60f55e44f7b28b --- /dev/null +++ b/packages/rich-text/src/get-text-content.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { OBJECT_REPLACEMENT_CHARACTER } from './special-characters'; +import type { RichTextValue } from './types'; + +/** + * Get the textual content of a Rich Text value. This is similar to + * `Element.textContent`. + * + * @param value Value to use. + * + * @return The text content. + */ +export function getTextContent( value: RichTextValue ): string { + return value.text.replace( OBJECT_REPLACEMENT_CHARACTER, '' ); +} diff --git a/packages/rich-text/src/insert-object.js b/packages/rich-text/src/insert-object.ts similarity index 77% rename from packages/rich-text/src/insert-object.js rename to packages/rich-text/src/insert-object.ts index ac8afb25b2010b..f6483d4bfd5664 100644 --- a/packages/rich-text/src/insert-object.js +++ b/packages/rich-text/src/insert-object.ts @@ -1,12 +1,9 @@ /** * Internal dependencies */ - import { insert } from './insert'; import { OBJECT_REPLACEMENT_CHARACTER } from './special-characters'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ -/** @typedef {import('./types').RichTextFormat} RichTextFormat */ +import type { RichTextValue, RichTextFormat } from './types'; /** * Insert a format as an object into a Rich Text value at the given @@ -20,8 +17,13 @@ import { OBJECT_REPLACEMENT_CHARACTER } from './special-characters'; * * @return {RichTextValue} A new value with the object inserted. */ -export function insertObject( value, formatToInsert, startIndex, endIndex ) { - const valueToInsert = { +export function insertObject( + value: RichTextValue, + formatToInsert: RichTextFormat, + startIndex?: number, + endIndex?: number +): RichTextValue { + const valueToInsert: RichTextValue = { formats: [ , ], replacements: [ formatToInsert ], text: OBJECT_REPLACEMENT_CHARACTER, diff --git a/packages/rich-text/src/insert.js b/packages/rich-text/src/insert.ts similarity index 81% rename from packages/rich-text/src/insert.js rename to packages/rich-text/src/insert.ts index e908c676505d3d..4bd5d6a2a6e432 100644 --- a/packages/rich-text/src/insert.js +++ b/packages/rich-text/src/insert.ts @@ -4,8 +4,7 @@ import { create } from './create'; import { normaliseFormats } from './normalise-formats'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextValue } from './types'; /** * Insert a Rich Text value, an HTML string, or a plain text string, into a @@ -21,13 +20,17 @@ import { normaliseFormats } from './normalise-formats'; * @return {RichTextValue} A new value with the value inserted. */ export function insert( - value, - valueToInsert, - startIndex = value.start, - endIndex = value.end -) { + value: RichTextValue, + valueToInsert: RichTextValue | string, + startIndex: number | undefined = value.start, + endIndex: number | undefined = value.end +): RichTextValue { const { formats, replacements, text } = value; + if ( startIndex === undefined || endIndex === undefined ) { + return { ...value }; + } + if ( typeof valueToInsert === 'string' ) { valueToInsert = create( { text: valueToInsert } ); } diff --git a/packages/rich-text/src/is-collapsed.ts b/packages/rich-text/src/is-collapsed.ts index fcde8014799206..8a5fb6da1c72f7 100644 --- a/packages/rich-text/src/is-collapsed.ts +++ b/packages/rich-text/src/is-collapsed.ts @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -import type { RichTextValue } from './types'; - /** * 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 @@ -17,7 +12,10 @@ import type { RichTextValue } from './types'; export function isCollapsed( { start, end, -}: RichTextValue ): boolean | undefined { +}: { + start?: number; + end?: number; +} ): boolean | undefined { if ( start === undefined || end === undefined ) { return; } diff --git a/packages/rich-text/src/is-empty.js b/packages/rich-text/src/is-empty.js deleted file mode 100644 index 86bc64bb36b565..00000000000000 --- a/packages/rich-text/src/is-empty.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -/** - * Check if a Rich Text value is Empty, meaning it contains no text or any - * objects (such as images). - * - * @param {RichTextValue} value Value to use. - * - * @return {boolean} True if the value is empty, false if not. - */ -export function isEmpty( { text } ) { - return text.length === 0; -} diff --git a/packages/rich-text/src/is-empty.ts b/packages/rich-text/src/is-empty.ts new file mode 100644 index 00000000000000..71107de3735cab --- /dev/null +++ b/packages/rich-text/src/is-empty.ts @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import type { RichTextValue } from './types'; + +/** + * Check if a Rich Text value is empty, meaning it contains no text or any + * objects (such as images). + * + * @param value Value to use. + * + * @return True if the value is empty, false if not. + */ +export function isEmpty( value: RichTextValue ): boolean { + return value.text.length === 0; +} diff --git a/packages/rich-text/src/is-format-equal.js b/packages/rich-text/src/is-format-equal.ts similarity index 73% rename from packages/rich-text/src/is-format-equal.js rename to packages/rich-text/src/is-format-equal.ts index dc6d9653770b76..8557e31bdfe1f5 100644 --- a/packages/rich-text/src/is-format-equal.js +++ b/packages/rich-text/src/is-format-equal.ts @@ -1,14 +1,20 @@ -/** @typedef {import('./types').RichTextFormat} RichTextFormat */ +/** + * Internal dependencies + */ +import type { RichTextFormat } from './types'; /** * Optimised equality check for format objects. * - * @param {?RichTextFormat} format1 Format to compare. - * @param {?RichTextFormat} format2 Format to compare. + * @param format1 Format to compare. + * @param format2 Format to compare. * - * @return {boolean} True if formats are equal, false if not. + * @return True if formats are equal, false if not. */ -export function isFormatEqual( format1, format2 ) { +export function isFormatEqual( + format1: RichTextFormat | null, + format2: RichTextFormat | null +): boolean { // Both not defined. if ( format1 === format2 ) { return true; diff --git a/packages/rich-text/src/is-range-equal.js b/packages/rich-text/src/is-range-equal.ts similarity index 65% rename from packages/rich-text/src/is-range-equal.js rename to packages/rich-text/src/is-range-equal.ts index 70a70aa6af6d7d..c9a05a1e605872 100644 --- a/packages/rich-text/src/is-range-equal.js +++ b/packages/rich-text/src/is-range-equal.ts @@ -3,16 +3,16 @@ * considered equal if their start and end occur in the same container and * offset. * - * @param {Range|null} a First range object to test. - * @param {Range|null} b First range object to test. + * @param {Range | null} a First range object to test. + * @param {Range | null} b First range object to test. * * @return {boolean} Whether the two ranges are equal. */ -export function isRangeEqual( a, b ) { +export function isRangeEqual( a: Range | null, b: Range | null ): boolean { return ( a === b || - ( a && - b && + ( a !== null && + b !== null && a.startContainer === b.startContainer && a.startOffset === b.startOffset && a.endContainer === b.endContainer && diff --git a/packages/rich-text/src/join.js b/packages/rich-text/src/join.ts similarity index 71% rename from packages/rich-text/src/join.js rename to packages/rich-text/src/join.ts index 6d91fbec1e7a3f..5da2c08b795bf2 100644 --- a/packages/rich-text/src/join.js +++ b/packages/rich-text/src/join.ts @@ -4,20 +4,21 @@ import { create } from './create'; import { normaliseFormats } from './normalise-formats'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextValue } from './types'; /** * 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 * string. This is similar to `Array.prototype.join`. * - * @param {Array} values An array of values to join. - * @param {string|RichTextValue} [separator] Separator string or value. - * - * @return {RichTextValue} A new combined value. + * @param values An array of values to join. + * @param separator Separator string or value. + * @return A new combined value. */ -export function join( values, separator = '' ) { +export function join( + values: Array< RichTextValue >, + separator: string | RichTextValue = '' +): RichTextValue { if ( typeof separator === 'string' ) { separator = create( { text: separator } ); } diff --git a/packages/rich-text/src/normalise-formats.js b/packages/rich-text/src/normalise-formats.ts similarity index 73% rename from packages/rich-text/src/normalise-formats.js rename to packages/rich-text/src/normalise-formats.ts index e73314151849e8..edd2fd581fb911 100644 --- a/packages/rich-text/src/normalise-formats.js +++ b/packages/rich-text/src/normalise-formats.ts @@ -3,24 +3,22 @@ */ import { isFormatEqual } from './is-format-equal'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextValue } from './types'; /** * Normalises formats: ensures subsequent adjacent equal formats have the same * reference. * - * @param {RichTextValue} value Value to normalise formats of. - * - * @return {RichTextValue} New value with normalised formats. + * @param value Value to normalise formats of. + * @return New value with normalised formats. */ -export function normaliseFormats( value ) { +export function normaliseFormats( value: RichTextValue ): RichTextValue { const newFormats = value.formats.slice(); newFormats.forEach( ( formatsAtIndex, index ) => { const formatsAtPreviousIndex = newFormats[ index - 1 ]; - if ( formatsAtPreviousIndex ) { + if ( formatsAtPreviousIndex && formatsAtIndex !== undefined ) { const newFormatsAtIndex = formatsAtIndex.slice(); newFormatsAtIndex.forEach( ( format, formatIndex ) => { diff --git a/packages/rich-text/src/register-format-type.js b/packages/rich-text/src/register-format-type.ts similarity index 69% rename from packages/rich-text/src/register-format-type.js rename to packages/rich-text/src/register-format-type.ts index c8aa45f022154c..37b1ee584c141f 100644 --- a/packages/rich-text/src/register-format-type.js +++ b/packages/rich-text/src/register-format-type.ts @@ -6,33 +6,27 @@ import { select, dispatch } from '@wordpress/data'; * Internal dependencies */ import { store as richTextStore } from './store'; -/** - * @typedef {Object} WPFormat - * - * @property {string} name A string identifying the format. Must be - * unique across all registered formats. - * @property {string} tagName The HTML tag this format will wrap the - * selection with. - * @property {boolean} interactive Whether format makes content interactive or not. - * @property {string | null} [className] A class to match the format. - * @property {string} title Name of the format. - * @property {Function} edit Should return a component for the user to - * interact with the new registered format. - */ +import type { WPFormat } from './types'; /** - * Registers a new format provided a unique name and an object defining its - * behavior. + * Registers a new format provided a unique name and an object defining its behavior. * - * @param {string} name Format name. - * @param {WPFormat} settings Format settings. + * @param name - Format name. + * @param settings - Format settings. * - * @return {WPFormat|undefined} The format, if it has been successfully - * registered; otherwise `undefined`. + * @return The format, if it has been successfully registered; otherwise `undefined`. */ -export function registerFormatType( name, settings ) { +export function registerFormatType( + name: string, + settings: WPFormat | undefined +): WPFormat | undefined { + if ( settings === undefined ) { + return undefined; + } + settings = { - name, + // @ts-ignore Overriding is expected + name, // TODO: Is this hiding a bug? ...settings, }; @@ -48,6 +42,7 @@ export function registerFormatType( name, settings ) { return; } + // @ts-expect-error Stores are not typed if ( select( richTextStore ).getFormatType( settings.name ) ) { window.console.error( 'Format "' + settings.name + '" is already registered.' @@ -71,7 +66,10 @@ export function registerFormatType( name, settings ) { return; } - if ( ! /^[_a-zA-Z]+[a-zA-Z0-9_-]*$/.test( settings.className ) ) { + if ( + settings.className !== null && + ! /^[_a-zA-Z]+[a-zA-Z0-9_-]*$/.test( settings.className ) + ) { window.console.error( 'A class name must begin with a letter, followed by any number of hyphens, underscores, letters, or numbers.' ); @@ -80,6 +78,7 @@ export function registerFormatType( name, settings ) { if ( settings.className === null ) { const formatTypeForBareElement = select( + // @ts-expect-error Stores are not typed richTextStore ).getFormatTypeForBareElement( settings.tagName ); @@ -94,6 +93,7 @@ export function registerFormatType( name, settings ) { } } else { const formatTypeForClassName = select( + // @ts-expect-error Stores are not typed richTextStore ).getFormatTypeForClassName( settings.className ); @@ -112,7 +112,11 @@ export function registerFormatType( name, settings ) { return; } - if ( 'keywords' in settings && settings.keywords.length > 3 ) { + if ( + 'keywords' in settings && + settings.keywords && // TODO: Is this correct? + settings.keywords.length > 3 + ) { window.console.error( 'The format "' + settings.name + @@ -126,6 +130,7 @@ export function registerFormatType( name, settings ) { return; } + // @ts-expect-error Stores are not typed dispatch( richTextStore ).addFormatTypes( settings ); return settings; diff --git a/packages/rich-text/src/remove-format.js b/packages/rich-text/src/remove-format.ts similarity index 64% rename from packages/rich-text/src/remove-format.js rename to packages/rich-text/src/remove-format.ts index 831b72e279faa9..a55ac55911fc95 100644 --- a/packages/rich-text/src/remove-format.js +++ b/packages/rich-text/src/remove-format.ts @@ -3,8 +3,7 @@ */ import { normaliseFormats } from './normalise-formats'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextValue, RichTextFormat } from './types'; /** * Remove any format object from a Rich Text value by type from the given @@ -19,11 +18,15 @@ import { normaliseFormats } from './normalise-formats'; * @return {RichTextValue} A new value with the format applied. */ export function removeFormat( - value, - formatType, - startIndex = value.start, - endIndex = value.end -) { + value: RichTextValue, + formatType: string, + startIndex: number | undefined = value.start, + endIndex: number | undefined = value.end +): RichTextValue { + // TODO: This is copied from other files, doing the same check. + if ( startIndex === undefined || endIndex === undefined ) { + return { ...value }; + } const { formats, activeFormats } = value; const newFormats = formats.slice(); @@ -31,13 +34,13 @@ export function removeFormat( // format. if ( startIndex === endIndex ) { const format = newFormats[ startIndex ]?.find( - ( { type } ) => type === formatType + ( { type }: RichTextFormat ) => type === formatType ); if ( format ) { while ( newFormats[ startIndex ]?.find( - ( newFormat ) => newFormat === format + ( newFormat: RichTextFormat ) => newFormat === format ) ) { filterFormats( newFormats, startIndex, formatType ); @@ -48,7 +51,7 @@ export function removeFormat( while ( newFormats[ endIndex ]?.find( - ( newFormat ) => newFormat === format + ( newFormat: RichTextFormat ) => newFormat === format ) ) { filterFormats( newFormats, endIndex, formatType ); @@ -67,13 +70,23 @@ export function removeFormat( ...value, formats: newFormats, activeFormats: - activeFormats?.filter( ( { type } ) => type !== formatType ) || [], + activeFormats?.filter( + ( { type }: RichTextFormat ) => type !== formatType + ) || [], } ); } -function filterFormats( formats, index, formatType ) { +function filterFormats( + formats: Array< RichTextFormat[] | undefined >, + index: number, + formatType: string +): void { + if ( ! formats[ index ] ) { + return; + } + const newFormats = formats[ index ].filter( - ( { type } ) => type !== formatType + ( { type }: RichTextFormat ) => type !== formatType ); if ( newFormats.length ) { diff --git a/packages/rich-text/src/remove.js b/packages/rich-text/src/remove.js deleted file mode 100644 index c8db71949b35c2..00000000000000 --- a/packages/rich-text/src/remove.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Internal dependencies - */ - -import { insert } from './insert'; -import { create } from './create'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -/** - * Remove content from a Rich Text value between the given `startIndex` and - * `endIndex`. Indices are retrieved from the selection if none are provided. - * - * @param {RichTextValue} value Value to modify. - * @param {number} [startIndex] Start index. - * @param {number} [endIndex] End index. - * - * @return {RichTextValue} A new value with the content removed. - */ -export function remove( value, startIndex, endIndex ) { - return insert( value, create(), startIndex, endIndex ); -} diff --git a/packages/rich-text/src/remove.ts b/packages/rich-text/src/remove.ts new file mode 100644 index 00000000000000..484eaec229c8e8 --- /dev/null +++ b/packages/rich-text/src/remove.ts @@ -0,0 +1,25 @@ +/** + * Internal dependencies + */ + +import { insert } from './insert'; +import { create } from './create'; +import type { RichTextValue } from './types'; + +/** + * Remove content from a Rich Text value between the given `startIndex` and + * `endIndex`. Indices are retrieved from the selection if none are provided. + * + * @param value Value to modify. + * @param startIndex Start index. + * @param endIndex End index. + * + * @return A new value with the content removed. + */ +export function remove( + value: RichTextValue, + startIndex?: number, + endIndex?: number +): RichTextValue { + return insert( value, create(), startIndex, endIndex ); +} diff --git a/packages/rich-text/src/replace.js b/packages/rich-text/src/replace.js deleted file mode 100644 index f95b37e925b462..00000000000000 --- a/packages/rich-text/src/replace.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Internal dependencies - */ - -import { normaliseFormats } from './normalise-formats'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -/** - * Search a Rich Text value and replace the match(es) with `replacement`. This - * is similar to `String.prototype.replace`. - * - * @param {RichTextValue} value The value to modify. - * @param {RegExp|string} pattern A RegExp object or literal. Can also be - * a string. It is treated as a verbatim - * string and is not interpreted as a - * regular expression. Only the first - * occurrence will be replaced. - * @param {Function|string} replacement The match or matches are replaced with - * the specified or the value returned by - * the specified function. - * - * @return {RichTextValue} A new value with replacements applied. - */ -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 ); - } - - 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 ] ); - } - } - - 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; - } - - return newText; - } ); - - return normaliseFormats( { formats, replacements, text, start, end } ); -} diff --git a/packages/rich-text/src/replace.ts b/packages/rich-text/src/replace.ts new file mode 100644 index 00000000000000..b8679d77e7fc19 --- /dev/null +++ b/packages/rich-text/src/replace.ts @@ -0,0 +1,76 @@ +/** + * Internal dependencies + */ + +import { normaliseFormats } from './normalise-formats'; +import type { RichTextValue } from './types'; + +/** + * Search a Rich Text value and replace the match(es) with `replacement`. This + * is similar to `String.prototype.replace`. + * + * @param {RichTextValue} value - The value to modify. + * @param {RegExp|string} pattern - A RegExp object or literal. Can also be + * a string. It is treated as a verbatim + * string and is not interpreted as a + * regular expression. Only the first + * occurrence will be replaced. + * @param {Function|string|RichTextValue} replacement - The match or matches are replaced with + * the specified or the value returned by + * the specified function. + * + * @return {RichTextValue} A new value with replacements applied. + */ +export function replace( + { formats, replacements, text, start, end }: RichTextValue, + pattern: RegExp | string, + replacement: + | ( ( match: string, ...args: any[] ) => string | RichTextValue ) + | string + | RichTextValue +): RichTextValue { + text = text.replace( pattern, ( match, ...rest ) => { + const offset = rest[ rest.length - 2 ]; + let newText: string | RichTextValue; + let newFormats: Array< RichTextValue[ 'formats' ][ 0 ] >; + let newReplacements: Array< RichTextValue[ 'replacements' ][ 0 ] >; + + if ( typeof replacement === 'function' ) { + newText = replacement( match, ...rest ); + } else { + newText = replacement; + } + + if ( typeof newText === 'object' ) { + const richTextValue = newText as RichTextValue; + newFormats = richTextValue.formats; + newReplacements = richTextValue.replacements; + newText = richTextValue.text; + } else { + newFormats = Array( newText.length ); + newReplacements = Array( newText.length ); + + if ( formats[ offset ] ) { + newFormats = newFormats.fill( formats[ offset ] ); + } + } + + 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; + } + + return newText; + } ); + + return normaliseFormats( { formats, replacements, text, start, end } ); +} diff --git a/packages/rich-text/src/slice.js b/packages/rich-text/src/slice.ts similarity index 57% rename from packages/rich-text/src/slice.js rename to packages/rich-text/src/slice.ts index 94b48cbb47c491..7b0d0cf76a517e 100644 --- a/packages/rich-text/src/slice.js +++ b/packages/rich-text/src/slice.ts @@ -1,17 +1,24 @@ -/** @typedef {import('./types').RichTextValue} RichTextValue */ +/** + * Internal dependencies + */ +import type { RichTextValue } from './types'; /** * Slice a Rich Text value from `startIndex` to `endIndex`. Indices are * retrieved from the selection if none are provided. This is similar to * `String.prototype.slice`. * - * @param {RichTextValue} value Value to modify. - * @param {number} [startIndex] Start index. - * @param {number} [endIndex] End index. + * @param value Value to modify. + * @param startIndex Start index. + * @param endIndex End index. * - * @return {RichTextValue} A new extracted value. + * @return A new extracted value. */ -export function slice( value, startIndex = value.start, endIndex = value.end ) { +export function slice( + value: RichTextValue, + startIndex: number | undefined = value.start, + endIndex: number | undefined = value.end +): RichTextValue { const { formats, replacements, text } = value; if ( startIndex === undefined || endIndex === undefined ) { diff --git a/packages/rich-text/src/special-characters.js b/packages/rich-text/src/special-characters.ts similarity index 100% rename from packages/rich-text/src/special-characters.js rename to packages/rich-text/src/special-characters.ts diff --git a/packages/rich-text/src/split.js b/packages/rich-text/src/split.ts similarity index 61% rename from packages/rich-text/src/split.js rename to packages/rich-text/src/split.ts index 1853fb7bce585d..ecbc1dda1ed617 100644 --- a/packages/rich-text/src/split.js +++ b/packages/rich-text/src/split.ts @@ -1,29 +1,38 @@ /** * Internal dependencies */ - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextValue } from './types'; /** * 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`. * Indices are retrieved from the selection if none are provided. * - * @param {RichTextValue} value - * @param {number|string} [string] Start index, or string at which to split. - * - * @return {Array|undefined} An array of new values. + * @param value The rich text value to split. + * @param value.formats + * @param value.replacements + * @param value.text + * @param value.start + * @param value.end + * @param string Start index, or string at which to split. + * @return An array of new values. */ -export function split( { formats, replacements, text, start, end }, string ) { +export function split( + { formats, replacements, text, start, end }: RichTextValue, + string: number | string +): Array< RichTextValue > | undefined { if ( typeof string !== 'string' ) { - return splitAtSelection( ...arguments ); + return splitAtSelection( + { formats, replacements, text, start, end }, + string + ); } let nextStart = 0; - return text.split( string ).map( ( substring ) => { + return text.split( string ).map( ( substring: string ) => { const startIndex = nextStart; - const value = { + const value: RichTextValue = { formats: formats.slice( startIndex, startIndex + substring.length ), replacements: replacements.slice( startIndex, @@ -53,20 +62,20 @@ export function split( { formats, replacements, text, start, end }, string ) { } function splitAtSelection( - { formats, replacements, text, start, end }, - startIndex = start, - endIndex = end -) { + { formats, replacements, text, start, end }: RichTextValue, + startIndex: number | undefined = start, + endIndex: number | undefined = end +): Array< RichTextValue > | undefined { if ( start === undefined || end === undefined ) { return; } - const before = { + const before: RichTextValue = { formats: formats.slice( 0, startIndex ), replacements: replacements.slice( 0, startIndex ), text: text.slice( 0, startIndex ), }; - const after = { + const after: RichTextValue = { formats: formats.slice( endIndex ), replacements: replacements.slice( endIndex ), text: text.slice( endIndex ), diff --git a/packages/rich-text/src/test/apply-format.js b/packages/rich-text/src/test/apply-format.ts similarity index 100% rename from packages/rich-text/src/test/apply-format.js rename to packages/rich-text/src/test/apply-format.ts diff --git a/packages/rich-text/src/test/concat.js b/packages/rich-text/src/test/concat.ts similarity index 73% rename from packages/rich-text/src/test/concat.js rename to packages/rich-text/src/test/concat.ts index ae8253e1a5461e..a32e355c919af1 100644 --- a/packages/rich-text/src/test/concat.js +++ b/packages/rich-text/src/test/concat.ts @@ -9,28 +9,32 @@ import deepFreeze from 'deep-freeze'; import { concat } from '../concat'; import { getSparseArrayLength } from './helpers'; +import type { RichTextValue } from '../types'; describe( 'concat', () => { const em = { type: 'em' }; it( 'should merge records', () => { - const one = { + const one: RichTextValue = { formats: [ , , [ em ] ], replacements: [ , , , ], text: 'one', }; - const two = { + const two: RichTextValue = { formats: [ [ em ], , , ], replacements: [ , , , ], text: 'two', }; - const three = { + const three: RichTextValue = { formats: [ , , [ em ], [ em ], , , ], replacements: [ , , , , , , ], text: 'onetwo', }; - const merged = concat( deepFreeze( one ), deepFreeze( two ) ); + const merged = concat( + deepFreeze( one ) as RichTextValue, + deepFreeze( two ) as RichTextValue + ); expect( merged ).not.toBe( one ); expect( merged ).toEqual( three ); diff --git a/packages/rich-text/src/test/create.js b/packages/rich-text/src/test/create.ts similarity index 100% rename from packages/rich-text/src/test/create.js rename to packages/rich-text/src/test/create.ts diff --git a/packages/rich-text/src/test/get-active-format.js b/packages/rich-text/src/test/get-active-format.ts similarity index 100% rename from packages/rich-text/src/test/get-active-format.js rename to packages/rich-text/src/test/get-active-format.ts diff --git a/packages/rich-text/src/test/get-active-object.js b/packages/rich-text/src/test/get-active-object.ts similarity index 74% rename from packages/rich-text/src/test/get-active-object.js rename to packages/rich-text/src/test/get-active-object.ts index 35d0f7637c7bc2..d3a71f97ff1383 100644 --- a/packages/rich-text/src/test/get-active-object.js +++ b/packages/rich-text/src/test/get-active-object.ts @@ -4,10 +4,12 @@ import { getActiveObject } from '../get-active-object'; import { OBJECT_REPLACEMENT_CHARACTER } from '../special-characters'; +import type { RichTextValue } from '../types'; describe( 'getActiveObject', () => { it( 'should return object if selected', () => { - const record = { + const record: RichTextValue = { + formats: [ , ], replacements: [ { type: 'img' } ], text: OBJECT_REPLACEMENT_CHARACTER, start: 0, @@ -18,7 +20,8 @@ describe( 'getActiveObject', () => { } ); it( 'should return nothing if nothing is selected', () => { - const record = { + const record: RichTextValue = { + formats: [ , ], replacements: [ { type: 'img' } ], text: OBJECT_REPLACEMENT_CHARACTER, start: 0, @@ -28,8 +31,9 @@ describe( 'getActiveObject', () => { expect( getActiveObject( record ) ).toBe( undefined ); } ); - it( 'should return nothing if te selection is not an object', () => { - const record = { + it( 'should return nothing if the selection is not an object', () => { + const record: RichTextValue = { + formats: [ , ], replacements: [ { type: 'em' } ], text: 'a', start: 0, diff --git a/packages/rich-text/src/test/get-format-type.js b/packages/rich-text/src/test/get-format-type.ts similarity index 100% rename from packages/rich-text/src/test/get-format-type.js rename to packages/rich-text/src/test/get-format-type.ts diff --git a/packages/rich-text/src/test/get-format-types.js b/packages/rich-text/src/test/get-format-types.ts similarity index 100% rename from packages/rich-text/src/test/get-format-types.js rename to packages/rich-text/src/test/get-format-types.ts diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.tsx similarity index 67% rename from packages/rich-text/src/test/helpers/index.js rename to packages/rich-text/src/test/helpers/index.tsx index 7658ede7e37737..ae151d88743bde 100644 --- a/packages/rich-text/src/test/helpers/index.js +++ b/packages/rich-text/src/test/helpers/index.tsx @@ -2,6 +2,7 @@ * Internal dependencies */ import { ZWNBSP, OBJECT_REPLACEMENT_CHARACTER } from '../../special-characters'; +import type { RichTextValue, WPFormat } from '../../types'; export function getSparseArrayLength( array ) { return array.reduce( ( accumulator ) => accumulator + 1, 0 ); @@ -12,16 +13,25 @@ const strong = { type: 'strong' }; const img = { type: 'img', attributes: { src: '' } }; const a = { type: 'a', attributes: { href: '#' } }; -export const spec = [ +type Spec = { + description: string; + html: string; + createRange: ( element: HTMLElement ) => Range; + record: RichTextValue; + startPath: Array< number >; + endPath: Array< number >; +}; + +export const spec: Spec[] = [ { description: 'should create an empty value', html: '', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 0, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 0 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 0 ], record: { @@ -36,12 +46,12 @@ export const spec = [ description: 'should ignore manually added object replacement character', html: `test${ OBJECT_REPLACEMENT_CHARACTER }`, - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 4 ], record: { @@ -56,12 +66,12 @@ export const spec = [ description: 'should ignore manually added object replacement character with formatting', html: `h${ OBJECT_REPLACEMENT_CHARACTER }i`, - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 0, 0, 2 ], record: { @@ -75,12 +85,12 @@ export const spec = [ { description: 'should preserve non breaking space', html: 'test\u00a0 test', - createRange: ( element ) => ( { - startOffset: 5, - startContainer: element.firstChild, - endOffset: 5, - endContainer: element.firstChild, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 5 ); + range.setEnd( element, 5 ); + return range; + }, startPath: [ 0, 5 ], endPath: [ 0, 5 ], record: { @@ -94,12 +104,12 @@ export const spec = [ { description: 'should create an empty value from empty tags', html: '', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 0 ], record: { @@ -113,12 +123,12 @@ export const spec = [ { description: 'should create a value without formatting', html: 'test', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.firstChild, - endOffset: 4, - endContainer: element.firstChild, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 4 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 4 ], record: { @@ -132,12 +142,12 @@ export const spec = [ { description: 'should preserve emoji', html: '🍒', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 2 ], record: { @@ -151,12 +161,12 @@ export const spec = [ { description: 'should preserve emoji in formatting', html: '🍒', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 0, 0, 2 ], record: { @@ -170,12 +180,12 @@ export const spec = [ { description: 'should create a value with formatting', html: 'test', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.firstChild, - endOffset: 1, - endContainer: element.firstChild, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element.getRootNode(), 0 ); + range.setEnd( element.getRootNode(), 4 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 0, 0, 4 ], record: { @@ -189,12 +199,12 @@ export const spec = [ { description: 'should create a value with nested formatting', html: 'test', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0, 0, 0 ], endPath: [ 0, 0, 0, 4 ], record: { @@ -213,12 +223,14 @@ export const spec = [ { description: 'should create a value with formatting for split tags', html: 'test', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.querySelector( 'em' ), - endOffset: 1, - endContainer: element.querySelector( 'em' ), - } ), + createRange: ( element ) => { + const range = document.createRange(); + // @ts-ignore Node will exist in test + range.setStart( element.querySelector( 'em' ).getRootNode(), 0 ); + // @ts-ignore Node will exist in test + range.setEnd( element.querySelector( 'em' ).getRootNode(), 1 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 0, 0, 2 ], record: { @@ -232,12 +244,12 @@ export const spec = [ { description: 'should create a value with formatting with attributes', html: 'test', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 0, 0, 4 ], record: { @@ -251,12 +263,12 @@ export const spec = [ { description: 'should create a value with image object', html: '', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 0 ], record: { @@ -270,12 +282,14 @@ export const spec = [ { description: 'should create a value with image object and formatting', html: '', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.querySelector( 'img' ), - endOffset: 1, - endContainer: element.querySelector( 'img' ), - } ), + createRange: ( element ) => { + const range = document.createRange(); + // @ts-ignore Node will exist in test + range.setStart( element.querySelector( 'img' )?.getRootNode(), 0 ); + // @ts-ignore Node will exist in test + range.setEnd( element.querySelector( 'img' )?.getRootNode(), 1 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 0, 2, 0 ], record: { @@ -289,12 +303,12 @@ export const spec = [ { description: 'should create a value with image object and text before', html: 'test', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 2, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 2 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 1, 2, 0 ], record: { @@ -308,12 +322,12 @@ export const spec = [ { description: 'should create a value with image object and text after', html: 'test', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 2, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 2 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 1, 2 ], record: { @@ -327,12 +341,12 @@ export const spec = [ { description: 'should handle br', html: '
', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 0 ], record: { @@ -346,12 +360,12 @@ export const spec = [ { description: 'should handle br with text', html: 'te
st', - createRange: ( element ) => ( { - startOffset: 1, - startContainer: element, - endOffset: 2, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 1 ); + range.setEnd( element, 2 ); + return range; + }, startPath: [ 0, 2 ], endPath: [ 2, 0 ], record: { @@ -365,12 +379,12 @@ export const spec = [ { description: 'should handle br with formatting', html: '
', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 0, 2, 0 ], record: { @@ -384,12 +398,12 @@ export const spec = [ { description: 'should handle double br', html: 'a

b', - createRange: ( element ) => ( { - startOffset: 2, - startContainer: element, - endOffset: 3, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 2 ); + range.setEnd( element, 3 ); + return range; + }, startPath: [ 2, 0 ], endPath: [ 4, 0 ], record: { @@ -403,12 +417,12 @@ export const spec = [ { description: 'should handle selection before br', html: 'a

b', - createRange: ( element ) => ( { - startOffset: 2, - startContainer: element, - endOffset: 2, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 2 ); + range.setEnd( element, 2 ); + return range; + }, startPath: [ 2, 0 ], endPath: [ 2, 0 ], record: { @@ -422,12 +436,12 @@ export const spec = [ { description: 'should remove padding', html: ZWNBSP, - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 0 ], record: { @@ -441,12 +455,12 @@ export const spec = [ { description: 'should filter format boundary attributes', html: 'test', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0, 0 ], endPath: [ 0, 0, 4 ], record: { @@ -460,12 +474,12 @@ export const spec = [ { description: 'should not error with overlapping formats (1)', html: '12', - createRange: ( element ) => ( { - startOffset: 1, - startContainer: element.firstChild, - endOffset: 1, - endContainer: element.firstChild, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element.children[ 0 ], 1 ); + range.setEnd( element.children[ 0 ], 1 ); + return range; + }, startPath: [ 0, 0, 0, 1 ], endPath: [ 0, 0, 0, 1 ], record: { @@ -482,12 +496,13 @@ export const spec = [ { description: 'should not error with overlapping formats (2)', html: '12', - createRange: ( element ) => ( { - startOffset: 1, - startContainer: element.firstChild, - endOffset: 1, - endContainer: element.firstChild, - } ), + createRange: ( element ) => { + const range = document.createRange(); + // TODO:: Unsure if these are the right element/node to pick + range.setStart( element.children[ 0 ], 1 ); + range.setEnd( element.children[ 0 ], 1 ); + return range; + }, startPath: [ 0, 0, 0, 1 ], endPath: [ 0, 0, 0, 1 ], record: { @@ -504,12 +519,12 @@ export const spec = [ { description: 'should disarm script', html: '', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 0, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element.children[ 0 ], 0 ); + range.setEnd( element.children[ 0 ], 0 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 0 ], record: { @@ -528,12 +543,12 @@ export const spec = [ { description: 'should disarm on* attribute', html: '', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 0, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element.children[ 0 ], 0 ); + range.setEnd( element.children[ 0 ], 0 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 0, 0 ], record: { @@ -554,12 +569,12 @@ export const spec = [ { description: 'should preserve comments', html: '', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 2, 0 ], record: { @@ -580,12 +595,12 @@ export const spec = [ { description: 'should preserve funky comments', html: '', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element, - } ), + createRange: ( element ) => { + const range = document.createRange(); + range.setStart( element, 0 ); + range.setEnd( element, 1 ); + return range; + }, startPath: [ 0, 0 ], endPath: [ 2, 0 ], record: { @@ -605,7 +620,16 @@ export const spec = [ }, ]; -export const specWithRegistration = [ +type SpecWithRegistration = { + description: string; + formatName?: string; + formatType?: WPFormat; + html: string; + value: RichTextValue; + noToHTMLString?: boolean; +}; + +export const specWithRegistration: SpecWithRegistration[] = [ { description: 'should create format by matching the class', formatName: 'my-plugin/link', diff --git a/packages/rich-text/src/test/insert-object.js b/packages/rich-text/src/test/insert-object.ts similarity index 81% rename from packages/rich-text/src/test/insert-object.js rename to packages/rich-text/src/test/insert-object.ts index 7c2ba57806ad03..3870d1e55fdc68 100644 --- a/packages/rich-text/src/test/insert-object.js +++ b/packages/rich-text/src/test/insert-object.ts @@ -9,27 +9,33 @@ import deepFreeze from 'deep-freeze'; import { insertObject } from '../insert-object'; import { getSparseArrayLength } from './helpers'; import { OBJECT_REPLACEMENT_CHARACTER } from '../special-characters'; +import type { RichTextValue } from '../types'; describe( 'insert', () => { const obj = { type: 'obj' }; const em = { type: 'em' }; it( 'should delete and insert', () => { - const record = { + const record: RichTextValue = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 6, end: 6, }; - const expected = { + const expected: RichTextValue = { formats: [ , , , [ em ], , , , , , , ], replacements: [ , , obj, , , , , , , , ], text: `on${ OBJECT_REPLACEMENT_CHARACTER }o three`, start: 3, end: 3, }; - const result = insertObject( deepFreeze( record ), obj, 2, 6 ); + const result = insertObject( + deepFreeze( record ) as RichTextValue, + obj, + 2, + 6 + ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); diff --git a/packages/rich-text/src/test/insert.js b/packages/rich-text/src/test/insert.ts similarity index 84% rename from packages/rich-text/src/test/insert.js rename to packages/rich-text/src/test/insert.ts index cae4cc5b85d5f5..68c014a24e2b18 100644 --- a/packages/rich-text/src/test/insert.js +++ b/packages/rich-text/src/test/insert.ts @@ -9,25 +9,26 @@ import deepFreeze from 'deep-freeze'; import { insert } from '../insert'; import { getSparseArrayLength } from './helpers'; +import type { RichTextValue } from '../types'; describe( 'insert', () => { const em = { type: 'em' }; const strong = { type: 'strong' }; it( 'should delete and insert', () => { - const record = { + const record: RichTextValue = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], replacements: [], text: 'one two three', start: 6, end: 6, }; - const toInsert = { + const toInsert: RichTextValue = { formats: [ [ strong ] ], replacements: [], text: 'a', }; - const expected = { + const expected: RichTextValue = { formats: [ , , [ strong ], [ em ], , , , , , , ], replacements: [], text: 'onao three', @@ -42,19 +43,19 @@ describe( 'insert', () => { } ); it( 'should insert line break with selection', () => { - const record = { + const record: RichTextValue = { formats: [ , , ], replacements: [], text: 'tt', start: 1, end: 1, }; - const toInsert = { + const toInsert: RichTextValue = { formats: [ , ], replacements: [], text: '\n', }; - const expected = { + const expected: RichTextValue = { formats: [ , , , ], replacements: [], text: 't\nt', diff --git a/packages/rich-text/src/test/is-collapsed.js b/packages/rich-text/src/test/is-collapsed.ts similarity index 100% rename from packages/rich-text/src/test/is-collapsed.js rename to packages/rich-text/src/test/is-collapsed.ts diff --git a/packages/rich-text/src/test/is-empty.js b/packages/rich-text/src/test/is-empty.ts similarity index 69% rename from packages/rich-text/src/test/is-empty.js rename to packages/rich-text/src/test/is-empty.ts index 6cd1d0b6d08275..3cb183538e4282 100644 --- a/packages/rich-text/src/test/is-empty.js +++ b/packages/rich-text/src/test/is-empty.ts @@ -3,21 +3,24 @@ */ import { isEmpty } from '../is-empty'; +import type { RichTextValue } from '../types'; describe( 'isEmpty', () => { it( 'should return true', () => { - const one = { + const one: RichTextValue = { formats: [], text: '', + replacements: [], }; expect( isEmpty( one ) ).toBe( true ); } ); it( 'should return false', () => { - const one = { + const one: RichTextValue = { formats: [], text: 'test', + replacements: [], }; expect( isEmpty( one ) ).toBe( false ); diff --git a/packages/rich-text/src/test/is-format-equal.js b/packages/rich-text/src/test/is-format-equal.ts similarity index 100% rename from packages/rich-text/src/test/is-format-equal.js rename to packages/rich-text/src/test/is-format-equal.ts diff --git a/packages/rich-text/src/test/join.js b/packages/rich-text/src/test/join.ts similarity index 100% rename from packages/rich-text/src/test/join.js rename to packages/rich-text/src/test/join.ts diff --git a/packages/rich-text/src/test/normalise-formats.js b/packages/rich-text/src/test/normalise-formats.ts similarity index 100% rename from packages/rich-text/src/test/normalise-formats.js rename to packages/rich-text/src/test/normalise-formats.ts diff --git a/packages/rich-text/src/test/register-format-type.js b/packages/rich-text/src/test/register-format-type.ts similarity index 100% rename from packages/rich-text/src/test/register-format-type.js rename to packages/rich-text/src/test/register-format-type.ts diff --git a/packages/rich-text/src/test/remove-format.js b/packages/rich-text/src/test/remove-format.ts similarity index 100% rename from packages/rich-text/src/test/remove-format.js rename to packages/rich-text/src/test/remove-format.ts diff --git a/packages/rich-text/src/test/replace.js b/packages/rich-text/src/test/replace.ts similarity index 100% rename from packages/rich-text/src/test/replace.js rename to packages/rich-text/src/test/replace.ts diff --git a/packages/rich-text/src/test/slice.js b/packages/rich-text/src/test/slice.ts similarity index 86% rename from packages/rich-text/src/test/slice.js rename to packages/rich-text/src/test/slice.ts index b181e5e9be8171..0285669e1ded7f 100644 --- a/packages/rich-text/src/test/slice.js +++ b/packages/rich-text/src/test/slice.ts @@ -9,17 +9,18 @@ import deepFreeze from 'deep-freeze'; import { slice } from '../slice'; import { getSparseArrayLength } from './helpers'; +import type { RichTextValue } from '../types'; describe( 'slice', () => { const em = { type: 'em' }; it( 'should slice', () => { - const record = { + const record: RichTextValue = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], replacements: [ , , , , , , , , , , , , , ], text: 'one two three', }; - const expected = { + const expected: RichTextValue = { formats: [ , [ em ], [ em ] ], replacements: [ , , , ], text: ' tw', @@ -32,14 +33,14 @@ describe( 'slice', () => { } ); it( 'should slice record', () => { - const record = { + const record: RichTextValue = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], replacements: [ , , , , , , , , , , , , , ], text: 'one two three', start: 3, end: 6, }; - const expected = { + const expected: RichTextValue = { formats: [ , [ em ], [ em ] ], replacements: [ , , , ], text: ' tw', diff --git a/packages/rich-text/src/test/split.js b/packages/rich-text/src/test/split.ts similarity index 87% rename from packages/rich-text/src/test/split.js rename to packages/rich-text/src/test/split.ts index 05a76889ee6864..8b91d8c5de08b0 100644 --- a/packages/rich-text/src/test/split.js +++ b/packages/rich-text/src/test/split.ts @@ -9,19 +9,20 @@ import deepFreeze from 'deep-freeze'; import { split } from '../split'; import { getSparseArrayLength } from './helpers'; +import type { RichTextValue } from '../types'; describe( 'split', () => { const em = { type: 'em' }; it( 'should split', () => { - const record = { + const record: RichTextValue = { start: 5, end: 10, formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], replacements: [ , , , , , , , , , , , , , ], text: 'one two three', }; - const expected = [ + const expected: RichTextValue[] = [ { formats: [ , , , , [ em ], [ em ] ], replacements: [ , , , , , , ], @@ -35,10 +36,12 @@ describe( 'split', () => { text: 'o three', }, ]; - const result = split( deepFreeze( record ), 6, 6 ); + // TODO: These were taking too many parameters + // const result = split( deepFreeze( record ), 6, 6 ); + const result = split( deepFreeze( record ), 6 ); expect( result ).toEqual( expected ); - result.forEach( ( item, index ) => { + result?.forEach( ( item, index ) => { expect( item ).not.toBe( record ); expect( getSparseArrayLength( item.formats ) ).toBe( getSparseArrayLength( expected[ index ].formats ) @@ -68,10 +71,11 @@ describe( 'split', () => { end: 0, }, ]; + // TODO: This wasn't taking any parameters const result = split( deepFreeze( record ) ); expect( result ).toEqual( expected ); - result.forEach( ( item, index ) => { + result?.forEach( ( item, index ) => { expect( item ).not.toBe( record ); expect( getSparseArrayLength( item.formats ) ).toBe( getSparseArrayLength( expected[ index ].formats ) @@ -101,10 +105,11 @@ describe( 'split', () => { end: 0, }, ]; + // TODO: This wasn't taking any parameters const result = split( deepFreeze( record ) ); expect( result ).toEqual( expected ); - result.forEach( ( item, index ) => { + result?.forEach( ( item, index ) => { expect( item ).not.toBe( record ); expect( getSparseArrayLength( item.formats ) ).toBe( getSparseArrayLength( expected[ index ].formats ) @@ -180,7 +185,7 @@ describe( 'split', () => { const result = split( deepFreeze( record ), ' ' ); expect( result ).toEqual( expected ); - result.forEach( ( item, index ) => { + result?.forEach( ( item, index ) => { expect( item ).not.toBe( record ); expect( getSparseArrayLength( item.formats ) ).toBe( getSparseArrayLength( expected[ index ].formats ) @@ -218,7 +223,7 @@ describe( 'split', () => { const result = split( deepFreeze( record ), ' ' ); expect( result ).toEqual( expected ); - result.forEach( ( item, index ) => { + result?.forEach( ( item, index ) => { expect( item ).not.toBe( record ); expect( getSparseArrayLength( item.formats ) ).toBe( getSparseArrayLength( expected[ index ].formats ) @@ -232,6 +237,7 @@ describe( 'split', () => { replacements: [], text: '', }; + // TODO: This wasn't taking any parameters expect( split( deepFreeze( record ) ) ).toBe( undefined ); } ); } ); diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.ts similarity index 95% rename from packages/rich-text/src/test/to-dom.js rename to packages/rich-text/src/test/to-dom.ts index 4f9b2df86cad35..e5c073102e2c40 100644 --- a/packages/rich-text/src/test/to-dom.js +++ b/packages/rich-text/src/test/to-dom.ts @@ -84,10 +84,12 @@ describe( 'applyValue', () => { cases.forEach( ( { current, future, description, movedCount } ) => { // eslint-disable-next-line jest/valid-title it( description, () => { - const body = createElement( document, current ).cloneNode( true ); + const body = createElement( document, current ).cloneNode( + true + ) as HTMLElement; const futureBody = createElement( document, future ).cloneNode( true - ); + ) as HTMLElement; const childNodes = Array.from( futureBody.childNodes ); applyValue( futureBody, body ); const count = childNodes.reduce( ( acc, { parentNode } ) => { diff --git a/packages/rich-text/src/test/to-html-string.js b/packages/rich-text/src/test/to-html-string.ts similarity index 88% rename from packages/rich-text/src/test/to-html-string.js rename to packages/rich-text/src/test/to-html-string.ts index 32360e6805d69f..5023fc2da43f49 100644 --- a/packages/rich-text/src/test/to-html-string.js +++ b/packages/rich-text/src/test/to-html-string.ts @@ -7,12 +7,19 @@ import { registerFormatType } from '../register-format-type'; import { unregisterFormatType } from '../unregister-format-type'; import { specWithRegistration } from './helpers'; -function createNode( HTML ) { +// TODO: This is a temporary function to create an HTMLElement from a string. +function createNode( HTML: string ): HTMLElement { const doc = document.implementation.createHTMLDocument( '' ); doc.body.innerHTML = HTML; - return doc.body.firstChild; + return doc.body.firstChild as HTMLElement; } +// function createElement( htmlString: string ): HTMLElement { +// const template = document.createElement( 'div' ); +// template.innerHTML = htmlString; +// return template.firstElementChild as HTMLElement; +// } + describe( 'toHTMLString', () => { beforeAll( () => { // Initialize the rich-text store. diff --git a/packages/rich-text/src/test/toggle-format.js b/packages/rich-text/src/test/toggle-format.ts similarity index 100% rename from packages/rich-text/src/test/toggle-format.js rename to packages/rich-text/src/test/toggle-format.ts diff --git a/packages/rich-text/src/test/unregister-format-type.js b/packages/rich-text/src/test/unregister-format-type.ts similarity index 100% rename from packages/rich-text/src/test/unregister-format-type.js rename to packages/rich-text/src/test/unregister-format-type.ts diff --git a/packages/rich-text/src/test/update-formats.js b/packages/rich-text/src/test/update-formats.ts similarity index 100% rename from packages/rich-text/src/test/update-formats.js rename to packages/rich-text/src/test/update-formats.ts diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.ts similarity index 55% rename from packages/rich-text/src/to-dom.js rename to packages/rich-text/src/to-dom.ts index ac8308c7274b58..174d461c23e10a 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.ts @@ -5,31 +5,34 @@ import { toTree } from './to-tree'; import { createElement } from './create-element'; import { isRangeEqual } from './is-range-equal'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextValue } from './types'; /** * Creates a path as an array of indices from the given root node to the given * node. * - * @param {Node} node Node to find the path of. - * @param {HTMLElement} rootNode Root node to find the path from. - * @param {Array} path Initial path to build on. + * @param node Node to find the path of. + * @param rootNode Root node to find the path from. + * @param path Initial path to build on. * - * @return {Array} The path from the root node to the node. + * @return The path from the root node to the node. */ -function createPathToNode( node, rootNode, path ) { +function createPathToNode( + node: Node, + rootNode: HTMLElement, + path: number[] +): number[] { const parentNode = node.parentNode; let i = 0; - while ( ( node = node.previousSibling ) ) { + while ( ( node = node.previousSibling as Node ) ) { i++; } path = [ i, ...path ]; if ( parentNode !== rootNode ) { - path = createPathToNode( parentNode, rootNode, path ); + path = createPathToNode( parentNode as Node, rootNode, path ); } return path; @@ -38,16 +41,19 @@ function createPathToNode( node, rootNode, path ) { /** * Gets a node given a path (array of indices) from the given node. * - * @param {HTMLElement} node Root node to find the wanted node in. - * @param {Array} path Path (indices) to the wanted node. + * @param node Root node to find the wanted node in. + * @param path Path (indices) to the wanted node. * - * @return {Object} Object with the found node and the remaining offset (if any). + * @return Object with the found node and the remaining offset (if any). */ -function getNodeByPath( node, path ) { +function getNodeByPath( + node: HTMLElement, + path: number[] +): { node: Node; offset: number } { path = [ ...path ]; while ( node && path.length > 1 ) { - node = node.childNodes[ path.shift() ]; + node = node.childNodes[ path.shift() as number ] as HTMLElement; } return { @@ -56,56 +62,88 @@ function getNodeByPath( node, path ) { }; } -function append( element, child ) { - if ( child.html !== undefined ) { - return ( element.innerHTML += child.html ); +interface Child { + html?: string; + type?: string; + attributes?: Record< string, string >; + [ key: string ]: any; +} + +function append( element: HTMLElement, child: Child | string ): Node { + if ( typeof child === 'object' && child.html !== undefined ) { + element.innerHTML += child.html; + return element.lastChild as Node; } if ( typeof child === 'string' ) { child = element.ownerDocument.createTextNode( child ); + return element.appendChild( child as Node ); } const { type, attributes } = child; if ( type ) { + let node: Node; if ( type === '#comment' ) { - child = element.ownerDocument.createComment( - attributes[ 'data-rich-text-comment' ] + node = element.ownerDocument.createComment( + attributes?.[ 'data-rich-text-comment' ] || '' ); } else { - child = element.ownerDocument.createElement( type ); - - for ( const key in attributes ) { - child.setAttribute( key, attributes[ key ] ); + node = element.ownerDocument.createElement( type ); + + if ( attributes ) { + for ( const key in attributes ) { + ( node as HTMLElement ).setAttribute( + key, + attributes[ key ] + ); + } } } + return element.appendChild( node ); } - return element.appendChild( child ); + return element.appendChild( child as Node ); } -function appendText( node, text ) { +function appendText( node: Text, text: string ): void { node.appendData( text ); } -function getLastChild( { lastChild } ) { +function getLastChild( { lastChild }: { lastChild: Node } ): Node | null { return lastChild; } -function getParent( { parentNode } ) { +function getParent( { parentNode }: { parentNode: Node } ): Node | null { return parentNode; } -function isText( node ) { +function isText( node: Node ): boolean { return node.nodeType === node.TEXT_NODE; } -function getText( { nodeValue } ) { +function getText( { nodeValue }: { nodeValue: string } ): string { return nodeValue; } -function remove( node ) { - return node.parentNode.removeChild( node ); +function remove( node: Node ): Node { + return node.parentNode?.removeChild( node ) as Node; +} + +interface ToDomOptions { + value: RichTextValue; + prepareEditableTree?: ( value: RichTextValue ) => any[]; + isEditableTree?: boolean; + placeholder?: string; + doc?: Document; +} + +interface ToDomResult { + body: HTMLElement; + selection: { + startPath: number[]; + endPath: number[]; + }; } export function toDom( { @@ -114,9 +152,9 @@ export function toDom( { isEditableTree = true, placeholder, doc = document, -} ) { - let startPath = []; - let endPath = []; +}: ToDomOptions ): ToDomResult { + let startPath: number[] = []; + let endPath: number[] = []; if ( prepareEditableTree ) { value = { @@ -133,9 +171,9 @@ export function toDom( { * each call to `createEmpty`. Therefore, you should not hold a reference to * the value to operate upon asynchronously, as it may have unexpected results. * - * @return {Object} RichText tree. + * @return RichText tree. */ - const createEmpty = () => createElement( doc, '' ); + const createEmpty = (): HTMLElement => createElement( doc, '' ); const tree = toTree( { value, @@ -147,14 +185,14 @@ export function toDom( { getText, remove, appendText, - onStartIndex( body, pointer ) { + onStartIndex( body: HTMLElement, pointer: Text ) { startPath = createPathToNode( pointer, body, [ - pointer.nodeValue.length, + pointer.nodeValue?.length || 0, ] ); }, - onEndIndex( body, pointer ) { + onEndIndex( body: HTMLElement, pointer: Text ) { endPath = createPathToNode( pointer, body, [ - pointer.nodeValue.length, + pointer.nodeValue?.length || 0, ] ); }, isEditableTree, @@ -167,16 +205,24 @@ export function toDom( { }; } +interface ApplyOptions { + value: RichTextValue; + current: HTMLElement; + prepareEditableTree?: ( value: RichTextValue ) => any[]; + __unstableDomOnly?: boolean; + placeholder?: string; +} + /** * Create an `Element` tree from a Rich Text value and applies the difference to * the `Element` tree contained by `current`. * - * @param {Object} $1 Named arguments. - * @param {RichTextValue} $1.value Value to apply. - * @param {HTMLElement} $1.current The live root node to apply the element tree to. - * @param {Function} [$1.prepareEditableTree] Function to filter editorable formats. - * @param {boolean} [$1.__unstableDomOnly] Only apply elements, no selection. - * @param {string} [$1.placeholder] Placeholder text. + * @param options Named arguments. + * @param options.value + * @param options.current + * @param options.prepareEditableTree + * @param options.__unstableDomOnly + * @param options.placeholder */ export function apply( { value, @@ -184,7 +230,7 @@ export function apply( { prepareEditableTree, __unstableDomOnly, placeholder, -} ) { +}: ApplyOptions ): void { // Construct a new element tree in memory. const { body, selection } = toDom( { value, @@ -200,9 +246,9 @@ export function apply( { } } -export function applyValue( future, current ) { +export function applyValue( future: HTMLElement, current: HTMLElement ): void { let i = 0; - let futureChild; + let futureChild: Node | null; while ( ( futureChild = future.firstChild ) ) { const currentChild = current.childNodes[ i ]; @@ -213,12 +259,14 @@ export function applyValue( future, current ) { if ( currentChild.nodeName !== futureChild.nodeName || ( currentChild.nodeType === currentChild.TEXT_NODE && - currentChild.data !== futureChild.data ) + ( currentChild as Text ).data !== + ( futureChild as Text ).data ) ) { current.replaceChild( futureChild, currentChild ); } else { - const currentAttributes = currentChild.attributes; - const futureAttributes = futureChild.attributes; + const currentAttributes = ( currentChild as Element ) + .attributes; + const futureAttributes = ( futureChild as Element ).attributes; if ( currentAttributes ) { let ii = currentAttributes.length; @@ -228,8 +276,10 @@ export function applyValue( future, current ) { while ( ii-- ) { const { name } = currentAttributes[ ii ]; - if ( ! futureChild.getAttribute( name ) ) { - currentChild.removeAttribute( name ); + if ( + ! ( futureChild as Element ).getAttribute( name ) + ) { + ( currentChild as Element ).removeAttribute( name ); } } } @@ -238,13 +288,22 @@ export function applyValue( future, current ) { for ( let ii = 0; ii < futureAttributes.length; ii++ ) { const { name, value } = futureAttributes[ ii ]; - if ( currentChild.getAttribute( name ) !== value ) { - currentChild.setAttribute( name, value ); + if ( + ( currentChild as Element ).getAttribute( name ) !== + value + ) { + ( currentChild as Element ).setAttribute( + name, + value + ); } } } - applyValue( futureChild, currentChild ); + applyValue( + futureChild as HTMLElement, + currentChild as HTMLElement + ); future.removeChild( futureChild ); } } else { @@ -259,7 +318,10 @@ export function applyValue( future, current ) { } } -export function applySelection( { startPath, endPath }, current ) { +export function applySelection( + { startPath, endPath }: { startPath: number[]; endPath: number[] }, + current: HTMLElement +): void { const { node: startContainer, offset: startOffset } = getNodeByPath( current, startPath @@ -270,7 +332,17 @@ export function applySelection( { startPath, endPath }, current ) { ); const { ownerDocument } = current; const { defaultView } = ownerDocument; + + if ( ! defaultView ) { + return; + } + const selection = defaultView.getSelection(); + + if ( ! selection ) { + return; + } + const range = ownerDocument.createRange(); range.setStart( startContainer, startOffset ); diff --git a/packages/rich-text/src/to-html-string.js b/packages/rich-text/src/to-html-string.ts similarity index 61% rename from packages/rich-text/src/to-html-string.js rename to packages/rich-text/src/to-html-string.ts index a4c12b4c47f00d..82623191a15f02 100644 --- a/packages/rich-text/src/to-html-string.js +++ b/packages/rich-text/src/to-html-string.ts @@ -13,19 +13,24 @@ import { */ import { toTree } from './to-tree'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextValue } from './types'; /** * Create an HTML string from a Rich Text value. * - * @param {Object} $1 Named arguments. - * @param {RichTextValue} $1.value Rich text value. - * @param {boolean} [$1.preserveWhiteSpace] Preserves newlines if true. + * @param options - Named arguments. + * @param options.value - Rich text value. + * @param options.preserveWhiteSpace - Preserves newlines if true. * - * @return {string} HTML string. + * @return HTML string. */ -export function toHTMLString( { value, preserveWhiteSpace } ) { +export function toHTMLString( { + value, + preserveWhiteSpace, +}: { + value: RichTextValue; + preserveWhiteSpace?: boolean; +} ): string { const tree = toTree( { value, preserveWhiteSpace, @@ -37,20 +42,24 @@ export function toHTMLString( { value, preserveWhiteSpace } ) { getText, remove, appendText, + onStartIndex: () => {}, + onEndIndex: () => {}, + isEditableTree: false, + placeholder: '', } ); return createChildrenHTML( tree.children ); } -function createEmpty() { +function createEmpty(): Record< string, any > { return {}; } -function getLastChild( { children } ) { +function getLastChild( { children }: { children?: any[] } ): any { return children && children[ children.length - 1 ]; } -function append( parent, object ) { +function append( parent: any, object: any ): any { if ( typeof object === 'string' ) { object = { text: object }; } @@ -61,23 +70,23 @@ function append( parent, object ) { return object; } -function appendText( object, text ) { +function appendText( object: any, text: string ): void { object.text += text; } -function getParent( { parent } ) { +function getParent( { parent }: { parent: any } ): any { return parent; } -function isText( { text } ) { +function isText( { text }: { text: any } ): boolean { return typeof text === 'string'; } -function getText( { text } ) { +function getText( { text }: { text: string } ): string { return text; } -function remove( object ) { +function remove( object: any ): any { const index = object.parent.children.indexOf( object ); if ( index !== -1 ) { @@ -87,7 +96,19 @@ function remove( object ) { return object; } -function createElementHTML( { type, attributes, object, children } ) { +// interface ElementAttributes { +// type: string; +// attributes: Record< string, string >; +// object?: any; +// children?: any[]; +// } + +function createElementHTML( { + type, + attributes, + object, + children, +}: Child ): string { if ( type === '#comment' ) { // We can't restore the original comment delimiters, because once parsed // into DOM nodes, we don't have the information. But in the future we @@ -118,7 +139,13 @@ function createElementHTML( { type, attributes, object, children } ) { ) }`; } -function createChildrenHTML( children = [] ) { +interface Child { + html?: string; + text?: string; + [ key: string ]: any; +} + +function createChildrenHTML( children: Child[] = [] ): string { return children .map( ( child ) => { if ( child.html !== undefined ) { diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.ts similarity index 75% rename from packages/rich-text/src/to-tree.js rename to packages/rich-text/src/to-tree.ts index 0e3caad4f70c83..d1514185a8c23d 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.ts @@ -5,13 +5,17 @@ import { getActiveFormats } from './get-active-formats'; import { getFormatType } from './get-format-type'; import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from './special-characters'; +import type { RichTextValue, RichTextFormat } from './types'; -function restoreOnAttributes( attributes, isEditableTree ) { +function restoreOnAttributes( + attributes: Record< string, string >, + isEditableTree: boolean +): Record< string, string > { if ( isEditableTree ) { return attributes; } - const newAttributes = {}; + const newAttributes: Record< string, string > = {}; for ( const key in attributes ) { let newKey = key; @@ -25,36 +29,48 @@ function restoreOnAttributes( attributes, isEditableTree ) { return newAttributes; } +interface FromFormatOptions { + type: string; + tagName?: string; + attributes?: Record< string, string >; + unregisteredAttributes?: Record< string, string >; + object?: boolean; + boundaryClass?: boolean; + isEditableTree: boolean; +} + +interface FormatElementInfo { + type: string; + object?: boolean; + attributes: Record< string, string >; +} + /** * Converts a format object to information that can be used to create an element * from (type, attributes and object). * - * @param {Object} $1 Named parameters. - * @param {string} $1.type The format type. - * @param {string} $1.tagName The tag name. - * @param {Object} $1.attributes The format attributes. - * @param {Object} $1.unregisteredAttributes The unregistered format - * attributes. - * @param {boolean} $1.object Whether or not it is an object - * format. - * @param {boolean} $1.boundaryClass Whether or not to apply a boundary - * class. - * @param {boolean} $1.isEditableTree - * - * @return {Object} Information to be used for element creation. + * @param options Named parameters. + * @param options.type + * @param options.tagName + * @param options.attributes + * @param options.unregisteredAttributes + * @param options.object + * @param options.boundaryClass + * @param options.isEditableTree + * @return Information to be used for element creation. */ function fromFormat( { type, tagName, - attributes, - unregisteredAttributes, + attributes = {}, + unregisteredAttributes = {}, object, boundaryClass, isEditableTree, -} ) { +}: FromFormatOptions ): FormatElementInfo { const formatType = getFormatType( type ); - let elementAttributes = {}; + const elementAttributes: Record< string, string > = {}; if ( boundaryClass && isEditableTree ) { elementAttributes[ 'data-rich-text-format-boundary' ] = 'true'; @@ -62,7 +78,7 @@ function fromFormat( { if ( ! formatType ) { if ( attributes ) { - elementAttributes = { ...attributes, ...elementAttributes }; + Object.assign( elementAttributes, attributes ); } return { @@ -75,7 +91,7 @@ function fromFormat( { }; } - elementAttributes = { ...unregisteredAttributes, ...elementAttributes }; + Object.assign( elementAttributes, unregisteredAttributes ); for ( const name in attributes ) { const key = formatType.attributes @@ -113,11 +129,15 @@ function fromFormat( { /** * Checks if both arrays of formats up until a certain index are equal. * - * @param {Array} a Array of formats to compare. - * @param {Array} b Array of formats to compare. - * @param {number} index Index to check until. + * @param a Array of formats to compare. + * @param b Array of formats to compare. + * @param index Index to check until. */ -function isEqualUntil( a, b, index ) { +function isEqualUntil( + a: RichTextFormat[], + b: RichTextFormat[], + index: number +): boolean { do { if ( a[ index ] !== b[ index ] ) { return false; @@ -127,6 +147,23 @@ function isEqualUntil( a, b, index ) { return true; } +interface ToTreeOptions { + value: RichTextValue; + preserveWhiteSpace?: boolean; + createEmpty: () => any; + append: ( parent: any, child: any ) => any; + getLastChild: ( node: any ) => any; + getParent: ( node: any ) => any; + isText: ( node: any ) => boolean; + getText: ( node: any ) => string; + remove: ( node: any ) => any; + appendText: ( node: any, text: string ) => void; + onStartIndex?: ( tree: any, pointer: any ) => void; + onEndIndex?: ( tree: any, pointer: any ) => void; + isEditableTree: boolean; + placeholder?: string; +} + export function toTree( { value, preserveWhiteSpace, @@ -142,15 +179,15 @@ export function toTree( { onEndIndex, isEditableTree, placeholder, -} ) { +}: ToTreeOptions ): any { const { formats, replacements, text, start, end } = value; const formatsLength = formats.length + 1; const tree = createEmpty(); const activeFormats = getActiveFormats( value ); const deepestActiveFormat = activeFormats[ activeFormats.length - 1 ]; - let lastCharacterFormats; - let lastCharacter; + let lastCharacterFormats: RichTextFormat[] | undefined; + let lastCharacter: string | undefined; append( tree, '' ); @@ -197,6 +234,7 @@ export function toTree( { tagName, attributes, unregisteredAttributes, + object: format.object, boundaryClass, isEditableTree, } ) @@ -235,12 +273,12 @@ export function toTree( { attributes: { contenteditable: 'false', 'data-rich-text-comment': - attributes[ 'data-rich-text-comment' ], + attributes?.[ 'data-rich-text-comment' ], }, } ); append( append( pointer, { type: 'span' } ), - attributes[ 'data-rich-text-comment' ].trim() + attributes?.[ 'data-rich-text-comment' ].trim() ); } else if ( ! isEditableTree && type === 'script' ) { pointer = append( @@ -252,7 +290,7 @@ export function toTree( { ); append( pointer, { html: decodeURIComponent( - attributes[ 'data-rich-text-script' ] + attributes?.[ 'data-rich-text-script' ] ), } ); } else if ( formatType?.contentEditable === false ) { diff --git a/packages/rich-text/src/toggle-format.js b/packages/rich-text/src/toggle-format.ts similarity index 78% rename from packages/rich-text/src/toggle-format.js rename to packages/rich-text/src/toggle-format.ts index 11e61a9d3976f8..af076383cfe0f1 100644 --- a/packages/rich-text/src/toggle-format.js +++ b/packages/rich-text/src/toggle-format.ts @@ -13,18 +13,20 @@ import { getActiveFormat } from './get-active-format'; import { removeFormat } from './remove-format'; import { applyFormat } from './apply-format'; -/** @typedef {import('./types').RichTextValue} RichTextValue */ -/** @typedef {import('./types').RichTextFormat} RichTextFormat */ +import type { RichTextValue, RichTextFormat } from './types'; /** * Toggles a format object to a Rich Text value at the current selection. * - * @param {RichTextValue} value Value to modify. - * @param {RichTextFormat} format Format to apply or remove. + * @param {RichTextValue} value - Value to modify. + * @param {RichTextFormat} format - Format to apply or remove. * * @return {RichTextValue} A new value with the format applied or removed. */ -export function toggleFormat( value, format ) { +export function toggleFormat( + value: RichTextValue, + format: RichTextFormat +): RichTextValue { if ( getActiveFormat( value, format.type ) ) { // For screen readers, will announce if formatting control is disabled. if ( format.title ) { diff --git a/packages/rich-text/src/types.ts b/packages/rich-text/src/types.ts index 5ef595fcdb7615..7c344fc95d2959 100644 --- a/packages/rich-text/src/types.ts +++ b/packages/rich-text/src/types.ts @@ -9,6 +9,12 @@ export type RichTextFormat = { | 'core/strikethrough' | 'core/image' | string; + title?: string; + attributes?: Record< string, any >; + unregisteredAttributes?: Record< string, any >; + tagName?: string; // TODO: Is this correct, possibly should be required? + innerHTML?: string; // TODO: Is this correct + object?: boolean; // TODO: Is this correct }; /** @@ -24,8 +30,35 @@ export type RichTextFormatList = Array< RichTextFormat >; */ export type RichTextValue = { text: string; - formats: Array< RichTextFormatList >; - replacements: Array< RichTextFormat >; - start: number; - end: number; + formats: Array< RichTextFormatList | any >; // TODO: using any to cover undefined case, which fixes types in create.ts, Array.from + replacements: Array< RichTextFormat | undefined >; + // TODO: Should these really be optional? + start?: number; + end?: number; + activeFormats?: Array< RichTextFormat >; + // TODO: Is this correct? + object?: boolean; // Whether the format is an object. +}; + +/** + * Represents a format in WordPress. + */ +export type WPFormat = { + tagName: string; // The HTML tag this format will wrap the selection with. + title: string; // Name of the format. + edit: ( () => JSX.Element ) | ( () => void ); // Should return a component for the user to interact with the new registered format. + + name?: string; // A string identifying the format. Must be unique across all registered formats. + interactive?: boolean; // Whether format makes content interactive or not. + className?: string | null; // A class to match the format. + keywords?: string[]; // Keywords to match the format. // TODO: Is this correct? + contentEditable?: boolean; // Whether the format is editable. + + // Experimental + __experimentalCreatePrepareEditableTree?: () => void; // A function that prepares the editable tree for the format. + __experimentalCreateOnChangeEditableValue?: () => void; // A function that is called when the editable value changes. + + // TODO: Is these correct? + attributes?: Record< string, string >; // Attributes to match the format. + object?: boolean; // Whether the format is an object. }; diff --git a/packages/rich-text/src/unregister-format-type.js b/packages/rich-text/src/unregister-format-type.ts similarity index 55% rename from packages/rich-text/src/unregister-format-type.js rename to packages/rich-text/src/unregister-format-type.ts index a1616568f148ec..c4cb3b8a7dccc1 100644 --- a/packages/rich-text/src/unregister-format-type.js +++ b/packages/rich-text/src/unregister-format-type.ts @@ -7,19 +7,19 @@ import { select, dispatch } from '@wordpress/data'; * Internal dependencies */ import { store as richTextStore } from './store'; - -/** @typedef {import('./register-format-type').WPFormat} WPFormat */ +import type { WPFormat } from './types'; /** * Unregisters a format. * - * @param {string} name Format name. + * @param name Format name. * - * @return {WPFormat|undefined} The previous format value, if it has - * been successfully unregistered; - * otherwise `undefined`. + * @return The previous format value, if it has + * been successfully unregistered; + * otherwise `undefined`. */ -export function unregisterFormatType( name ) { +export function unregisterFormatType( name: string ): WPFormat | undefined { + // @ts-expect-error No typing on the store const oldFormat = select( richTextStore ).getFormatType( name ); if ( ! oldFormat ) { @@ -27,6 +27,7 @@ export function unregisterFormatType( name ) { return; } + // @ts-expect-error No typing on the store dispatch( richTextStore ).removeFormatTypes( name ); return oldFormat; diff --git a/packages/rich-text/src/update-formats.js b/packages/rich-text/src/update-formats.ts similarity index 66% rename from packages/rich-text/src/update-formats.js rename to packages/rich-text/src/update-formats.ts index 668e1a73db8dff..4075b67c3664b0 100644 --- a/packages/rich-text/src/update-formats.js +++ b/packages/rich-text/src/update-formats.ts @@ -1,24 +1,32 @@ /** * Internal dependencies */ - import { isFormatEqual } from './is-format-equal'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ +import type { RichTextFormat, RichTextValue } from './types'; /** * Efficiently updates all the formats from `start` (including) until `end` * (excluding) with the active formats. Mutates `value`. * - * @param {Object} $1 Named paramentes. - * @param {RichTextValue} $1.value Value te update. - * @param {number} $1.start Index to update from. - * @param {number} $1.end Index to update until. - * @param {Array} $1.formats Replacement formats. + * @param {Object} params Named parameters. + * @param {RichTextValue} params.value Value to update. + * @param {number} params.start Index to update from. + * @param {number} params.end Index to update until. + * @param {Array} params.formats Replacement formats. * * @return {RichTextValue} Mutated value. */ -export function updateFormats( { value, start, end, formats } ) { +export function updateFormats( { + value, + start, + end, + formats, +}: { + value: RichTextValue; + start: number; + end: number; + formats: Array< RichTextFormat >; +} ): RichTextValue { // Start and end may be switched in case of delete. const min = Math.min( start, end ); const max = Math.max( start, end );