schlechtenburg/packages/rich-text/lib/apply-format.ts

108 lines
2.8 KiB
TypeScript
Raw Normal View History

2024-10-08 07:15:26 +00:00
import { normaliseFormats } from './normalise-formats';
import { RichTextValue, RichTextFormat } from './types';
function replace<T>( 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,
],
} );
}