diff --git a/core/src/utils/input-masking/mask-controller.ts b/core/src/utils/input-masking/mask-controller.ts index b0298601c1..65a1cf7942 100644 --- a/core/src/utils/input-masking/mask-controller.ts +++ b/core/src/utils/input-masking/mask-controller.ts @@ -2,7 +2,7 @@ import { MaskHistory, MaskModel } from './classes'; import { MASK_DEFAULT_OPTIONS } from './constants'; import { isBeforeInputEventSupported, isEventProducingCharacter, EventListener } from './dom'; import type { ElementState, MaskOptions, SelectionRange, TypedInputEvent } from './types/mask-interface'; -import { getNotEmptySelection } from './utils'; +import { areElementValuesEqual, getLineSelection, getNotEmptySelection, getWordSelection } from './utils'; import { maskTransform } from './utils/transform'; /** @@ -33,7 +33,37 @@ export class MaskController extends MaskHistory { if (isBeforeInputEventSupported(element)) { this.eventListener.listen('beforeinput', (event) => { - switch (event.type) { + const isForward = event.inputType.includes('Forward'); + + this.updateHistory(this.elementState); + + switch (event.inputType) { + case 'deleteByCut': + case 'deleteContentBackward': + case 'deleteContentForward': + return this.handleDelete({ + event, + isForward, + selection: getNotEmptySelection(this.elementState, isForward), + }); + case 'deleteWordForward': + case 'deleteWordBackward': + return this.handleDelete({ + event, + isForward, + selection: getWordSelection(this.elementState, isForward), + force: true, + }); + case 'deleteSoftLineBackward': + case 'deleteSoftLineForward': + case 'deleteHardLineBackward': + case 'deleteHardLineForward': + return this.handleDelete({ + event, + isForward, + selection: getLineSelection(this.elementState, isForward), + force: true, + }); case 'insertText': default: return this.handleInsert(event, event.data || ''); @@ -124,13 +154,49 @@ export class MaskController extends MaskHistory { isForward: boolean; force?: boolean; }): void { - // TODO implementation - console.debug('handleDelete', { - event, + const initialState: ElementState = { + value: this.elementState.value, selection, - isForward, - force, + }; + const [initialFrom, initialTo] = initialState.selection; + const { elementState } = this.options.preprocessor( + { + elementState: initialState, + data: '', + }, + isForward ? 'deleteForward' : 'deleteBackward', + ); + const maskModel = new MaskModel(elementState, this.options); + const [from, to] = elementState.selection; + + maskModel.deleteCharacters([from, to]); + + const newElementState = this.options.postprocessor(maskModel, initialState); + const newPossibleValue = + initialState.value.slice(0, initialFrom) + + initialState.value.slice(initialTo); + + if (newPossibleValue === newElementState.value && !force) { + return; + } + + event.preventDefault(); + + if (areElementValuesEqual(initialState, elementState, maskModel, newElementState)) { + // User presses Backspace/Delete for the fixed value + return this.updateSelectionRange(isForward ? [to, to] : [from, from]); + } + + // TODO: drop it when `event: Event | TypedInputEvent` => `event: TypedInputEvent` + const inputTypeFallback = isForward + ? 'deleteContentForward' + : 'deleteContentBackward'; + + this.updateElementState(newElementState, { + inputType: 'inputType' in event ? event.inputType : inputTypeFallback, + data: null, }); + this.updateHistory(newElementState); } private handleInsert(event: Event | TypedInputEvent, data: string): void { diff --git a/core/src/utils/input-masking/utils/get-line-selection.ts b/core/src/utils/input-masking/utils/get-line-selection.ts new file mode 100644 index 0000000000..9c3757c85c --- /dev/null +++ b/core/src/utils/input-masking/utils/get-line-selection.ts @@ -0,0 +1,21 @@ +import type { ElementState, SelectionRange } from "../types/mask-interface"; + +export function getLineSelection( + { value, selection }: ElementState, + isForward: boolean, +): SelectionRange { + const [from, to] = selection; + + if (from !== to) { + return [from, to]; + } + + const nearestBreak = isForward + ? value.slice(from).indexOf('\n') + 1 || value.length + : value.slice(0, to).lastIndexOf('\n') + 1; + + const selectFrom = isForward ? from : nearestBreak; + const selectTo = isForward ? nearestBreak : to; + + return [selectFrom, selectTo]; +} diff --git a/core/src/utils/input-masking/utils/get-word-selection.ts b/core/src/utils/input-masking/utils/get-word-selection.ts new file mode 100644 index 0000000000..131bced090 --- /dev/null +++ b/core/src/utils/input-masking/utils/get-word-selection.ts @@ -0,0 +1,46 @@ +import type { ElementState, SelectionRange } from "../types/mask-interface"; + +const TRAILING_SPACES_REG = /\s+$/g; +const LEADING_SPACES_REG = /^\s+/g; +const SPACE_REG = /\s/; + +export function getWordSelection( + { value, selection }: ElementState, + isForward: boolean, +): SelectionRange { + const [from, to] = selection; + + if (from !== to) { + return [from, to]; + } + + if (isForward) { + const valueAfterSelectionStart = value.slice(from); + const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [ + '', + ]; + const nearestWordEndIndex = valueAfterSelectionStart + .trimStart() + .search(SPACE_REG); + + return [ + from, + nearestWordEndIndex !== -1 + ? from + leadingSpaces.length + nearestWordEndIndex + : value.length, + ]; + } + + const valueBeforeSelectionEnd = value.slice(0, to); + const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || ['']; + const selectedWordLength = valueBeforeSelectionEnd + .trimEnd() + .split('') + .reverse() + .findIndex(char => char.match(SPACE_REG)); + + return [ + selectedWordLength !== -1 ? to - trailingSpaces.length - selectedWordLength : 0, + to, + ]; +} diff --git a/core/src/utils/input-masking/utils/index.ts b/core/src/utils/input-masking/utils/index.ts index ba6a3252b8..e8a5c1cbe0 100644 --- a/core/src/utils/input-masking/utils/index.ts +++ b/core/src/utils/input-masking/utils/index.ts @@ -2,3 +2,5 @@ export * from './get-not-empty-selection'; export * from './identity'; export * from './element-states-equality'; export * from './format-mask'; +export * from './get-word-selection'; +export * from './get-line-selection';