diff --git a/packages/editor/src/components/rich-text/index.native.js b/packages/editor/src/components/rich-text/index.native.js index 843f93b984cc5..e6fadc60f34b4 100644 --- a/packages/editor/src/components/rich-text/index.native.js +++ b/packages/editor/src/components/rich-text/index.native.js @@ -125,9 +125,9 @@ export class RichText extends Component { onSplit( before, after ); } - valueToFormat( { formats, formatPlaceholder, text } ) { + valueToFormat( { formats, text } ) { const value = toHTMLString( { - value: { formats, formatPlaceholder, text }, + value: { formats, text }, multilineTag: this.multilineTag, } ); // remove the outer root tags @@ -226,10 +226,17 @@ export class RichText extends Component { // Let's fix that here so `rich-text/slice` can work properly const realStart = Math.min( start, end ); const realEnd = Math.max( start, end ); + const jump = this.state.start + 1 !== realStart; + // update format placeholder to continue writing in the current format + // or set it to null if user jumped to another part in the text + const formatPlaceholder = ! jump && this.state.formatPlaceholder ? { + ...this.state.formatPlaceholder, + index: realStart, + } : null; this.setState( { start: realStart, end: realEnd, - formatPlaceholder: null, + formatPlaceholder, lastValue: text, } ); } diff --git a/packages/rich-text/src/apply-format.native.js b/packages/rich-text/src/apply-format.native.js new file mode 100644 index 0000000000000..8bfb9cf3068e0 --- /dev/null +++ b/packages/rich-text/src/apply-format.native.js @@ -0,0 +1,71 @@ +/** + * External dependencies + */ + +import { find, without } from 'lodash'; + +/** + * Internal dependencies + */ + +import { normaliseFormats } from './normalise-formats'; + +/** + * Apply a format object to a Rich Text value from the given `startIndex` to the + * given `endIndex`. Indices are retrieved from the selection if none are + * provided. + * + * @param {Object} value Value to modify. + * @param {Object} format Format to apply. + * @param {number} startIndex Start index. + * @param {number} endIndex End index. + * + * @return {Object} A new value with the format applied. + */ +export function applyFormat( + { formats, formatPlaceholder, text, start, end }, + format, + startIndex = start, + endIndex = end +) { + const newFormats = formats.slice( 0 ); + const previousFormats = newFormats[ startIndex - 1 ] || []; + const placeholderFormats = formatPlaceholder && formatPlaceholder.index === start && formatPlaceholder.formats; + // Follow the same logic as in getActiveFormat: placeholderFormats has priority over previousFormats + const activeFormats = ( placeholderFormats ? placeholderFormats : previousFormats ) || []; + const hasType = find( activeFormats, { type: format.type } ); + + // The selection is collpased, insert a placeholder with the format so new input appears + // with the format applied. + if ( startIndex === endIndex ) { + return { + formats, + text, + start, + end, + formatPlaceholder: { + index: start, + formats: [ + ...without( activeFormats, hasType ), + ...! hasType && [ format ], + ], + }, + }; + } + + for ( let index = startIndex; index < endIndex; index++ ) { + applyFormats( newFormats, index, format ); + } + + return normaliseFormats( { formats: newFormats, text, start, end } ); +} + +function applyFormats( formats, index, format ) { + if ( formats[ index ] ) { + const newFormatsAtIndex = formats[ index ].filter( ( { type } ) => type !== format.type ); + newFormatsAtIndex.push( format ); + formats[ index ] = newFormatsAtIndex; + } else { + formats[ index ] = [ format ]; + } +} diff --git a/packages/rich-text/src/get-active-format.native.js b/packages/rich-text/src/get-active-format.native.js index 3f1c7afe3a75f..7ff34aa7f66e9 100644 --- a/packages/rich-text/src/get-active-format.native.js +++ b/packages/rich-text/src/get-active-format.native.js @@ -25,22 +25,17 @@ export function getActiveFormat( { formats, formatPlaceholder, start, end }, for return find( formats[ start ], { type: formatType } ); } - // otherwise get the previous character format (or the next one at the beginning of the text) - const previousLetterFormat = find( formats[ start > 0 ? start - 1 : start ], { type: formatType } ); + // if user picked (or unpicked) formats but didn't write anything in those formats yet return this format + if ( formatPlaceholder && formatPlaceholder.index === start ) { + return find( formatPlaceholder.formats, { type: formatType } ); + } + + // otherwise get the previous character format + const previousLetterFormat = find( formats[ start - 1 ], { type: formatType } ); if ( previousLetterFormat ) { return previousLetterFormat; } - // if user picked a format but didn't write anything in this format yet return this format - if ( - formatPlaceholder && - formatPlaceholder.format && - formatPlaceholder.index === start && - formatPlaceholder.format.type === formatType - ) { - return formatPlaceholder.format; - } - return undefined; } diff --git a/packages/rich-text/src/normalise-formats.native.js b/packages/rich-text/src/normalise-formats.native.js new file mode 100644 index 0000000000000..2a75e343a2c12 --- /dev/null +++ b/packages/rich-text/src/normalise-formats.native.js @@ -0,0 +1,36 @@ +/** + * Internal dependencies + */ + +import { isFormatEqual } from './is-format-equal'; + +/** + * Normalises formats: ensures subsequent equal formats have the same reference. + * + * @param {Object} value Value to normalise formats of. + * + * @return {Object} New value with normalised formats. + */ +export function normaliseFormats( { formats, formatPlaceholder, text, start, end } ) { + const newFormats = formats.slice( 0 ); + + newFormats.forEach( ( formatsAtIndex, index ) => { + const lastFormatsAtIndex = newFormats[ index - 1 ]; + + if ( lastFormatsAtIndex ) { + const newFormatsAtIndex = formatsAtIndex.slice( 0 ); + + newFormatsAtIndex.forEach( ( format, formatIndex ) => { + const lastFormat = lastFormatsAtIndex[ formatIndex ]; + + if ( isFormatEqual( format, lastFormat ) ) { + newFormatsAtIndex[ formatIndex ] = lastFormat; + } + } ); + + newFormats[ index ] = newFormatsAtIndex; + } + } ); + + return { formats: newFormats, formatPlaceholder, text, start, end }; +} diff --git a/packages/rich-text/src/remove-format.native.js b/packages/rich-text/src/remove-format.native.js new file mode 100644 index 0000000000000..d4ebd0356c18b --- /dev/null +++ b/packages/rich-text/src/remove-format.native.js @@ -0,0 +1,59 @@ +/** + * External dependencies + */ + +import { find, without } from 'lodash'; + +/** + * Internal dependencies + */ + +import { normaliseFormats } from './normalise-formats'; + +/** + * Remove any format object from a Rich Text value by type from the given + * `startIndex` to the given `endIndex`. Indices are retrieved from the + * selection if none are provided. + * + * @param {Object} value Value to modify. + * @param {string} formatType Format type to remove. + * @param {number} startIndex Start index. + * @param {number} endIndex End index. + * + * @return {Object} A new value with the format applied. + */ +export function removeFormat( + { formats, formatPlaceholder, text, start, end }, + formatType, + startIndex = start, + endIndex = end +) { + const newFormats = formats.slice( 0 ); + let newFormatPlaceholder = null; + + if ( start === end && formatPlaceholder && formatPlaceholder.index === start ) { + newFormatPlaceholder = { + ...formatPlaceholder, + formats: without( formatPlaceholder.formats || [], find( formatPlaceholder.formats || [], { type: formatType } ) ), + }; + } + + // Do not remove format if selection is empty + for ( let i = startIndex; i < endIndex; i++ ) { + if ( newFormats[ i ] ) { + filterFormats( newFormats, i, formatType ); + } + } + + return normaliseFormats( { formats: newFormats, formatPlaceholder: newFormatPlaceholder, text, start, end } ); +} + +function filterFormats( formats, index, formatType ) { + const newFormats = formats[ index ].filter( ( { type } ) => type !== formatType ); + + if ( newFormats.length ) { + formats[ index ] = newFormats; + } else { + delete formats[ index ]; + } +}