UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

372 lines (371 loc) 13.8 kB
"use client"; import React, { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useMaskito } from '@maskito/react'; import { maskitoTransform, maskitoUpdateElement } from '@maskito/core'; import { maskitoCaretGuard, maskitoNumberOptionsGenerator } from '@maskito/kit'; import InputModeNumber from "./text-mask/InputModeNumber.js"; import { jsx as _jsx } from "react/jsx-runtime"; export default function TextMask(props) { const { inputElement, inputRef, mask: rawMask, value, showMask, optionsEnhancer, ghostPlaceholder, stripValue, allowOverflow, overwriteMode, ...rest } = props; const localRef = useRef(null); const [inputMode] = useState(() => new InputModeNumber()); useEffect(() => () => inputMode.remove(), [inputMode]); const maskParams = typeof rawMask === 'object' && rawMask !== null && 'maskParams' in rawMask ? rawMask.maskParams : undefined; const separatorTokens = useMemo(() => getSeparatorTokens(rawMask), [rawMask]); const options = useMemo(() => { if (typeof rawMask === 'object' && rawMask !== null && 'instanceOf' in rawMask && 'maskParams' in rawMask && rawMask.instanceOf === 'createNumberMask' && rawMask.maskParams) { const mp = rawMask.maskParams; return createMaskitoNumberOptions(mp); } const mask = normalizeMask(rawMask); if (!mask) { return null; } const overflowAwareMask = allowOverflow ? withOverflowSupport(mask) : mask; const preprocessors = Array.isArray(mask) ? [({ elementState, data }, actionType) => { if (actionType === 'deleteBackward') { const value = elementState.value; const [selectionStart, selectionEnd] = elementState.selection; const isSeparatorBeforeCaret = separatorTokens.includes(value[selectionStart - 1]); if (isSeparatorBeforeCaret) { const newCaretPosition = selectionStart - 1; const valueBeforeCaret = value.slice(0, selectionStart); const valueAfterCaret = value.slice(selectionEnd); return { elementState: { value: valueAfterCaret ? `${valueBeforeCaret}${valueAfterCaret}` : valueBeforeCaret.slice(0, -1), selection: [newCaretPosition, newCaretPosition] }, data }; } return { elementState, data }; } return { elementState, data }; }] : []; return { mask: overflowAwareMask, ...(preprocessors.length > 0 ? { preprocessors } : {}), ...(overwriteMode != null ? { overwriteMode } : {}) }; }, [rawMask, allowOverflow, overwriteMode, maskParams === null || maskParams === void 0 ? void 0 : maskParams.thousandsSeparatorSymbol, maskParams === null || maskParams === void 0 ? void 0 : maskParams.decimalSymbol, maskParams === null || maskParams === void 0 ? void 0 : maskParams.allowDecimal, maskParams === null || maskParams === void 0 ? void 0 : maskParams.decimalLimit]); const enhancedOptions = useMemo(() => { return typeof optionsEnhancer === 'function' ? optionsEnhancer(options) : options; }, [options, optionsEnhancer]); const maskitoRef = useMaskito({ options: enhancedOptions }); const setRefs = useCallback(el => { maskitoRef(el); if (inputRef) { if (typeof inputRef === 'object' && 'current' in inputRef) { inputRef.current = el; } else if (typeof inputRef === 'function') { inputRef(el); } } localRef.current = el; if (el) { inputMode.setElement(el); } }, [inputRef, maskitoRef, inputMode]); const params = useMemo(() => { const baseProps = { ...rest }; if (enhancedOptions && typeof baseProps.value === 'string' && baseProps.value != null) { const raw = String(baseProps.value); const sel = [raw.length, raw.length]; const { value: formatted } = maskitoTransform({ value: raw, selection: sel }, enhancedOptions); baseProps.value = formatted; } baseProps.onChange = e => { if (localRef.current && enhancedOptions) { var _element$selectionSta, _element$selectionEnd; const element = localRef.current; const selection = [(_element$selectionSta = element.selectionStart) !== null && _element$selectionSta !== void 0 ? _element$selectionSta : 0, (_element$selectionEnd = element.selectionEnd) !== null && _element$selectionEnd !== void 0 ? _element$selectionEnd : 0]; const elementState = { value: e.target.value, selection }; const validated = maskitoTransform(elementState, enhancedOptions); maskitoUpdateElement(element, validated); } if (typeof rest.onChange === 'function') { if (typeof stripValue === 'function') { const v = stripValue(e.target.value); rest.onChange({ target: { value: v } }); } else { rest.onChange(e); } } }; return baseProps; }, [rest, enhancedOptions, stripValue]); useEffect(() => { var _el$selectionStart, _el$selectionEnd; if (!localRef.current || !enhancedOptions) { return; } const el = localRef.current; const valueToTransform = value; if (valueToTransform === undefined || valueToTransform === null) { return; } const cleanValue = cleanNumericValue(String(valueToTransform), rawMask); const selection = [(_el$selectionStart = el.selectionStart) !== null && _el$selectionStart !== void 0 ? _el$selectionStart : cleanValue.length, (_el$selectionEnd = el.selectionEnd) !== null && _el$selectionEnd !== void 0 ? _el$selectionEnd : cleanValue.length]; const validated = maskitoTransform({ value: cleanValue, selection }, enhancedOptions); if (el.value !== validated.value) { maskitoUpdateElement(el, validated); } }, [enhancedOptions, value, rawMask]); const defaultValue = useMemo(() => { const raw = typeof value === 'string' ? value : undefined; if (!raw && showMask) { if (ghostPlaceholder) { return ghostPlaceholder; } if (typeof rawMask === 'object' && rawMask !== null && 'maskParams' in rawMask) { var _mp$prefix, _mp$suffix; const mp = rawMask.maskParams; const prefix = (_mp$prefix = mp.prefix) !== null && _mp$prefix !== void 0 ? _mp$prefix : ''; const suffix = (_mp$suffix = mp.suffix) !== null && _mp$suffix !== void 0 ? _mp$suffix : ''; if (prefix || suffix) { return prefix + suffix; } } return undefined; } if (!raw) { return undefined; } if (!options) { return raw; } const hasFormatting = Array.isArray(options.postprocessors) && options.postprocessors.length > 0; if (!hasFormatting) { return raw; } const cleanValue = cleanNumericValue(raw, rawMask); const { value: formatted } = maskitoTransform({ value: cleanValue, selection: [cleanValue.length, cleanValue.length] }, options); return formatted; }, [value, showMask, options, rawMask, ghostPlaceholder]); return inputElement ? (cloneElement(inputElement, { ...params, defaultValue, ref: setRefs })) : _jsx("input", { ref: setRefs, defaultValue: defaultValue, ...params }); } function withOverflowSupport(mask) { if (Array.isArray(mask)) { const baseMask = [...mask]; return state => { var _state$value; return appendOverflowTokens(baseMask, (_state$value = state === null || state === void 0 ? void 0 : state.value) !== null && _state$value !== void 0 ? _state$value : ''); }; } if (typeof mask === 'function') { return state => { var _state$value2; const resolved = mask(state); if (!Array.isArray(resolved)) { return resolved; } return appendOverflowTokens(resolved, (_state$value2 = state === null || state === void 0 ? void 0 : state.value) !== null && _state$value2 !== void 0 ? _state$value2 : ''); }; } return mask; } const ALL_CHARACTERS = /[\s\S]/; function appendOverflowTokens(maskExpression, currentValue) { if (!Array.isArray(maskExpression)) { return maskExpression; } const userSlots = countUserSlots(maskExpression); const userInputLength = countUserInputLength(currentValue, maskExpression); const overflow = Math.max(0, userInputLength - userSlots); if (overflow <= 0) { return maskExpression; } return maskExpression.concat(Array.from({ length: overflow }, () => ALL_CHARACTERS)); } function countUserSlots(maskExpression) { if (!Array.isArray(maskExpression)) { return 0; } return maskExpression.reduce((count, token) => { return count + (typeof token === 'string' ? 0 : 1); }, 0); } function getSeparatorTokens(maskExpression) { if (!Array.isArray(maskExpression)) { return []; } return maskExpression.filter(token => typeof token === 'string' && token.length > 0); } function countUserInputLength(value, maskExpression) { if (typeof value !== 'string') { return value == null ? 0 : String(value).length; } if (!Array.isArray(maskExpression) || !maskExpression.length) { return value.length; } let strippedValue = value; for (const token of maskExpression) { if (typeof token === 'string' && token.length > 0) { const escaped = escapeRegExp(token); strippedValue = strippedValue.replace(new RegExp(escaped, 'g'), ''); } } return strippedValue.length; } function escapeRegExp(input) { return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function normalizeMask(maskProp) { if (maskProp && typeof maskProp === 'object' && 'mask' in maskProp && !Array.isArray(maskProp)) { return normalizeMask(maskProp.mask); } if (maskProp === false) { return /^.*$/; } if (Array.isArray(maskProp)) { return maskProp.filter(t => t !== '[]'); } if (maskProp instanceof RegExp) { return maskProp; } return /^.*$/; } function stripAffixes(value, prefix, suffix) { let result = value; if (prefix && result.startsWith(prefix)) { result = result.slice(prefix.length); } if (suffix && result.endsWith(suffix)) { result = result.slice(0, -suffix.length); } else if (suffix && suffix.includes(' ')) { const suffixParts = suffix.split(' '); const lastPart = suffixParts[suffixParts.length - 1]; if (lastPart && result.endsWith(lastPart)) { result = result.slice(0, -lastPart.length).trim(); } } return result; } function cleanNumericValue(value, rawMask) { if (typeof rawMask === 'object' && rawMask !== null && 'maskParams' in rawMask) { var _mp$prefix2, _mp$suffix2; const mp = rawMask.maskParams; return stripAffixes(value, (_mp$prefix2 = mp.prefix) !== null && _mp$prefix2 !== void 0 ? _mp$prefix2 : '', (_mp$suffix2 = mp.suffix) !== null && _mp$suffix2 !== void 0 ? _mp$suffix2 : ''); } return value; } function createMaskitoNumberOptions(mp) { var _mp$decimalSymbol, _mp$thousandsSeparato, _mp$prefix3, _mp$suffix3, _mp$decimalLimit; const decimal = (_mp$decimalSymbol = mp.decimalSymbol) !== null && _mp$decimalSymbol !== void 0 ? _mp$decimalSymbol : ','; const thousand = (_mp$thousandsSeparato = mp.thousandsSeparatorSymbol) !== null && _mp$thousandsSeparato !== void 0 ? _mp$thousandsSeparato : ' '; const prefix = (_mp$prefix3 = mp.prefix) !== null && _mp$prefix3 !== void 0 ? _mp$prefix3 : ''; const suffix = (_mp$suffix3 = mp.suffix) !== null && _mp$suffix3 !== void 0 ? _mp$suffix3 : ''; const allowNegative = mp.allowNegative !== false; const min = allowNegative ? Number.MIN_SAFE_INTEGER - 1 : 0; const defaultDecimalLimit = mp.allowDecimal === true ? 2 : 0; const maximumFractionDigits = Math.max(0, Number((_mp$decimalLimit = mp.decimalLimit) !== null && _mp$decimalLimit !== void 0 ? _mp$decimalLimit : defaultDecimalLimit)); const suffixStartsWithComma = suffix && suffix.startsWith(','); const postfixToUse = suffixStartsWithComma ? suffix.slice(1) : suffix; const base = maskitoNumberOptionsGenerator({ min, max: Number.MAX_SAFE_INTEGER + 1, thousandSeparator: thousand, decimalSeparator: decimal, maximumFractionDigits, minimumFractionDigits: 0, prefix, postfix: postfixToUse, decimalPseudoSeparators: ['.', ',', '·'], minusSign: '-' }); const caretGuard = maskitoCaretGuard(value => { const left = prefix ? prefix.length : 0; const right = postfixToUse ? postfixToUse.length : 0; const max = Math.max(left, value.length - right); return [left, max]; }); const clearEmptyPostprocessor = elementState => { const value = elementState.value; const withoutAffixes = stripAffixes(value, prefix, postfixToUse).trim(); if (withoutAffixes === '' || withoutAffixes === decimal) { return { ...elementState, value: '', selection: [0, 0] }; } return elementState; }; const postprocessors = suffixStartsWithComma ? [...(base.postprocessors || []), elementState => { const prefixLen = prefix ? prefix.length : 0; const postfixLen = postfixToUse ? postfixToUse.length : 0; const valueWithoutAffixes = elementState.value.slice(prefixLen, elementState.value.length - postfixLen); const newValue = prefix + valueWithoutAffixes + ',' + postfixToUse; return { ...elementState, value: newValue }; }, clearEmptyPostprocessor] : [...(base.postprocessors || []), clearEmptyPostprocessor]; const plugins = [...(base.plugins || []), caretGuard]; return { ...base, plugins, postprocessors }; } //# sourceMappingURL=TextMask.js.map