import { normaliseFormats } from './normalise-formats'; import { RichTextValue, RichTextFormat } from './types'; function replace( array: T[], index: number, value: T ) { array = array.slice(); array[ index ] = value; return array; } /** * 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 {RichTextValue} value Value to modify. * @param {RichTextFormat} format Format to apply. * @param {number} [startIndex] Start index. * @param {number} [endIndex] End index. * * @return {RichTextValue} A new value with the format applied. */ export function applyFormat( value: RichTextValue, format: RichTextFormat, startIndex: number = value.start || 0, endIndex: number = value.end || 0, ): RichTextValue { const { formats, activeFormats } = value; const newFormats = formats.slice(); // The selection is collapsed. if ( startIndex === endIndex ) { const startFormat = newFormats[ startIndex ]?.find( ( { type } ) => type === format.type ); // If the caret is at a format of the same type, expand start and end to // the edges of the format. This is useful to apply new attributes. if ( startFormat ) { const index = newFormats[ startIndex ].indexOf( startFormat ); while ( newFormats[ startIndex ] && newFormats[ startIndex ][ index ] === startFormat ) { newFormats[ startIndex ] = replace( newFormats[ startIndex ], index, format ); startIndex--; } endIndex++; while ( newFormats[ endIndex ] && newFormats[ endIndex ][ index ] === startFormat ) { newFormats[ endIndex ] = replace( newFormats[ endIndex ], index, format ); endIndex++; } } } else { // Determine the highest position the new format can be inserted at. let position = +Infinity; for ( let index = startIndex; index < endIndex; index++ ) { if ( newFormats[ index ] ) { newFormats[ index ] = newFormats[ index ].filter( ( { type } ) => type !== format.type ); const length = newFormats[ index ].length; if ( length < position ) { position = length; } } else { newFormats[ index ] = []; position = 0; } } for ( let index = startIndex; index < endIndex; index++ ) { newFormats[ index ].splice( position, 0, format ); } } return normaliseFormats( { ...value, formats: newFormats, // Always revise active formats. This serves as a placeholder for new // inputs with the format so new input appears with the format applied, // and ensures a format of the same type uses the latest values. activeFormats: [ ...( activeFormats?.filter( ( { type } ) => type !== format.type ) || [] ), format, ], } ); }