UNPKG

ngx-mask

Version:

Input masking for modern Angular — Reactive, template-driven and Signal Forms, zoneless and SSR ready. Dates, numbers, separators, custom patterns.

4,141 lines 218 kB
import * as i0 from '@angular/core';
import { InjectionToken, EventEmitter, inject, Injectable, signal, ElementRef, Renderer2, makeEnvironmentProviders, input, model, booleanAttribute, output, ChangeDetectorRef, Injector, effect, untracked, HostListener, Directive, Pipe } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { NgControl, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';

var MaskExpression;
(function (MaskExpression) {
    MaskExpression["SEPARATOR"] = "separator";
    MaskExpression["PERCENT"] = "percent";
    MaskExpression["IP"] = "IP";
    MaskExpression["CPF_CNPJ"] = "CPF_CNPJ";
    MaskExpression["CPF_CNPJ_ALPHA"] = "CPF_CNPJ_ALPHA";
    MaskExpression["MONTH"] = "M";
    MaskExpression["MONTHS"] = "M0";
    MaskExpression["MINUTE"] = "m";
    MaskExpression["HOUR"] = "h";
    MaskExpression["HOURS"] = "H";
    MaskExpression["MINUTES"] = "m0";
    MaskExpression["HOURS_HOUR"] = "Hh";
    MaskExpression["SECONDS"] = "s0";
    MaskExpression["HOURS_MINUTES_SECONDS"] = "Hh:m0:s0";
    MaskExpression["EMAIL_MASK"] = "A*@A*.A*";
    MaskExpression["HOURS_MINUTES"] = "Hh:m0";
    MaskExpression["MINUTES_SECONDS"] = "m0:s0";
    MaskExpression["DAYS_MONTHS_YEARS"] = "d0/M0/0000";
    MaskExpression["DAYS_MONTHS"] = "d0/M0";
    MaskExpression["DAYS"] = "d0";
    MaskExpression["DAY"] = "d";
    MaskExpression["SECOND"] = "s";
    MaskExpression["LETTER_S"] = "S";
    MaskExpression["DOT"] = ".";
    MaskExpression["NUMPAD_DECIMAL"] = "Decimal";
    MaskExpression["NUMPAD_DECIMAL_CODE"] = "NumpadDecimal";
    MaskExpression["COMMA"] = ",";
    MaskExpression["CURLY_BRACKETS_LEFT"] = "{";
    MaskExpression["CURLY_BRACKETS_RIGHT"] = "}";
    MaskExpression["MINUS"] = "-";
    MaskExpression["OR"] = "||";
    MaskExpression["HASH"] = "#";
    MaskExpression["EMPTY_STRING"] = "";
    MaskExpression["SYMBOL_STAR"] = "*";
    MaskExpression["SYMBOL_QUESTION"] = "?";
    MaskExpression["SLASH"] = "/";
    MaskExpression["WHITE_SPACE"] = " ";
    MaskExpression["NUMBER_ZERO"] = "0";
    MaskExpression["NUMBER_NINE"] = "9";
    MaskExpression["BACKSPACE"] = "Backspace";
    MaskExpression["DELETE"] = "Delete";
    MaskExpression["ARROW_LEFT"] = "ArrowLeft";
    MaskExpression["ARROW_UP"] = "ArrowUp";
    MaskExpression["DOUBLE_ZERO"] = "00";
})(MaskExpression || (MaskExpression = {}));

const NGX_MASK_CONFIG = new InjectionToken('ngx-mask config');
const NEW_CONFIG = new InjectionToken('new ngx-mask config');
const INITIAL_CONFIG = new InjectionToken('initial ngx-mask config');
const initialConfig = {
    suffix: '',
    prefix: '',
    thousandSeparator: ' ',
    decimalMarker: ['.', ','],
    clearIfNotMatch: false,
    showMaskTyped: false,
    instantPrefix: false,
    placeHolderCharacter: '_',
    dropSpecialCharacters: true,
    hiddenInput: false,
    shownMaskExpression: '',
    separatorLimit: '',
    allowNegativeNumbers: false,
    validation: true,
    specialCharacters: ['-', '/', '(', ')', '.', ':', ' ', '+', ',', '@', '[', ']', '"', "'"],
    leadZeroDateTime: false,
    apm: false,
    leadZero: false,
    typeFromDecimals: false,
    keepCharacterPositions: false,
    triggerOnMaskChange: false,
    inputTransformFn: (value) => value,
    outputTransformFn: (value) => value,
    maskFilled: new EventEmitter(),
    maskAliases: {},
    defaultValueOnBlur: null,
    patterns: {
        '0': {
            pattern: new RegExp('\\d'),
        },
        '9': {
            pattern: new RegExp('\\d'),
            optional: true,
        },
        X: {
            pattern: new RegExp('\\d'),
            symbol: '*',
        },
        A: {
            pattern: new RegExp('[a-zA-Z0-9]'),
        },
        S: {
            pattern: new RegExp('[a-zA-Z]'),
        },
        U: {
            pattern: new RegExp('[A-Z]'),
        },
        L: {
            pattern: new RegExp('[a-z]'),
        },
        d: {
            pattern: new RegExp('\\d'),
            symbol: '*',
        },
        m: {
            pattern: new RegExp('\\d'),
        },
        M: {
            pattern: new RegExp('\\d'),
            symbol: '*',
        },
        H: {
            pattern: new RegExp('\\d'),
        },
        h: {
            pattern: new RegExp('\\d'),
        },
        s: {
            pattern: new RegExp('\\d'),
        },
    },
};
/**
 * Built-in mask tokens dispatched by exact equality (IP, CPF_CNPJ, CPF_CNPJ_ALPHA, email)
 * or by a reserved prefix (separator, percent) inside the mask pipeline. User-defined
 * aliases must not shadow them — substituting e.g. 'IP' early would break the built-in
 * exact-equality dispatch deep in the applier.
 */
const RESERVED_MASK_TOKENS = new Set([
    MaskExpression.IP,
    MaskExpression.CPF_CNPJ,
    MaskExpression.CPF_CNPJ_ALPHA,
    MaskExpression.EMAIL_MASK,
    MaskExpression.SEPARATOR,
    MaskExpression.PERCENT,
]);
/** Tracks alias keys already warned about, so the shadowing warning fires once per key. */
const warnedShadowedAliases = new Set();
/**
 * Resolves a user-defined mask alias to its mask expression. Returns the input expression
 * unchanged when no alias matches or when the alias key shadows a built-in token (in which
 * case a console warning is emitted once per key and the built-in wins).
 */
function resolveMaskAlias(maskExpression, maskAliases) {
    const expression = maskExpression ?? MaskExpression.EMPTY_STRING;
    if (!expression || !maskAliases) {
        return expression;
    }
    const aliased = maskAliases[expression];
    if (typeof aliased !== 'string') {
        return expression;
    }
    if (RESERVED_MASK_TOKENS.has(expression)) {
        if (!warnedShadowedAliases.has(expression)) {
            warnedShadowedAliases.add(expression);
            // eslint-disable-next-line no-console
            console.warn(`ngx-mask: mask alias "${expression}" shadows a built-in mask token and is ignored. Rename the alias.`);
        }
        return expression;
    }
    return aliased;
}
const timeMasks = [
    MaskExpression.HOURS_MINUTES_SECONDS,
    MaskExpression.HOURS_MINUTES,
    MaskExpression.MINUTES_SECONDS,
];
const withoutValidation = [
    MaskExpression.PERCENT,
    MaskExpression.HOURS_HOUR,
    MaskExpression.SECONDS,
    MaskExpression.MINUTES,
    MaskExpression.SEPARATOR,
    MaskExpression.DAYS_MONTHS_YEARS,
    MaskExpression.DAYS_MONTHS,
    MaskExpression.DAYS,
    MaskExpression.MONTHS,
];

/** CPF_CNPJ / CPF_CNPJ_ALPHA pre-processor (moved verbatim from applyMask). Sets
 *  `this.cpfCnpjError`, rewrites the mask, then falls through to the generic loop.
 *  Discriminated by exact equality BEFORE any startsWith handler — the 'H' of HOURS
 *  would otherwise catch CPF_CNPJ_ALPHA by substring (CODEBASE_NOTES #3/#4). */
const cpfCnpjHandler = function (state, params) {
    const isCpfCnpjAlpha = state.maskExpression === MaskExpression.CPF_CNPJ_ALPHA;
    this.cpfCnpjError = params.arr.length !== 11 && params.arr.length !== 14;
    const valueHasAnyLetter = /[a-zA-Z]/.test(state.processedValue);
    if (valueHasAnyLetter && isCpfCnpjAlpha) {
        state.maskExpression = 'AA.AAA.AAA/AAAA-00';
    }
    else if (params.arr.length > 11) {
        state.maskExpression = isCpfCnpjAlpha ? 'AA.AAA.AAA/AAAA-00' : '00.000.000/0000-00';
    }
    else {
        state.maskExpression = '000.000.000-00';
    }
    return state;
};

/**
 * Generic character-by-character pattern loop (moved verbatim from applyMask,
 * original lines 581-968). This is the default/fallback handler — always runs when
 * no exact/startsWith handler matched. IP and CPF_CNPJ pre-processors rewrite
 * `state.maskExpression` and then fall through into this loop.
 *
 * The inline HOURS/HOUR/MINUTE/SECOND/DAY/MONTH sub-branches (including the day-window
 * anchor flow-gating of #1611/#1513, CODEBASE_NOTES.md #20) are preserved unmodified —
 * they are explicitly out of scope for the phase-1 extraction boundary.
 *
 * Note: several branches read `this.maskExpression` (the instance field) rather than
 * the local `maskExpression` — this distinction is preserved exactly as in the
 * original source.
 */
const genericPatternHandler = function (state, params) {
    const { inputArray } = params;
    const maskExpression = state.maskExpression;
    const processedValue = state.processedValue;
    let { cursor, result, multi, processedPosition, stepBack } = state;
    for (
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    let i = 0, inputSymbol = inputArray[0]; i < inputArray.length; i++, inputSymbol = inputArray[i] ?? MaskExpression.EMPTY_STRING) {
        if (cursor === maskExpression.length) {
            break;
        }
        const symbolStarInPattern = MaskExpression.SYMBOL_STAR in this.patterns;
        if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) &&
            maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION) {
            result += inputSymbol;
            cursor += 2;
        }
        else if (maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
            multi &&
            this._checkSymbolMask(inputSymbol, maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)) {
            result += inputSymbol;
            cursor += 3;
            multi = false;
        }
        else if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) &&
            maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
            !symbolStarInPattern) {
            result += inputSymbol;
            multi = true;
        }
        else if (maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION &&
            this._checkSymbolMask(inputSymbol, maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)) {
            result += inputSymbol;
            cursor += 3;
        }
        else if (this._checkSymbolMask(inputSymbol, maskExpression[cursor] ?? MaskExpression.EMPTY_STRING)) {
            if (maskExpression[cursor] === MaskExpression.HOURS) {
                if (this.apm ? Number(inputSymbol) > 9 : Number(inputSymbol) > 2) {
                    processedPosition = !this.leadZeroDateTime
                        ? processedPosition + 1
                        : processedPosition;
                    cursor += 1;
                    this._shiftStep(cursor);
                    i--;
                    if (this.leadZeroDateTime) {
                        result += '0';
                    }
                    continue;
                }
            }
            if (maskExpression[cursor] === MaskExpression.HOUR) {
                if (this.apm
                    ? (result.length === 1 && Number(result) > 1) ||
                        (result === '1' && Number(inputSymbol) > 2) ||
                        (processedValue.slice(cursor - 1, cursor).length === 1 &&
                            Number(processedValue.slice(cursor - 1, cursor)) > 2) ||
                        (processedValue.slice(cursor - 1, cursor) === '1' &&
                            Number(inputSymbol) > 2)
                    : (result === '2' && Number(inputSymbol) > 3) ||
                        ((result.slice(cursor - 2, cursor) === '2' ||
                            result.slice(cursor - 3, cursor) === '2' ||
                            result.slice(cursor - 4, cursor) === '2' ||
                            result.slice(cursor - 1, cursor) === '2') &&
                            Number(inputSymbol) > 3 &&
                            cursor > 10)) {
                    processedPosition = processedPosition + 1;
                    cursor += 1;
                    i--;
                    continue;
                }
            }
            if (maskExpression[cursor] === MaskExpression.MINUTE ||
                maskExpression[cursor] === MaskExpression.SECOND) {
                if (Number(inputSymbol) > 5) {
                    processedPosition = !this.leadZeroDateTime
                        ? processedPosition + 1
                        : processedPosition;
                    cursor += 1;
                    this._shiftStep(cursor);
                    i--;
                    if (this.leadZeroDateTime) {
                        result += '0';
                    }
                    continue;
                }
            }
            const daysCount = 31;
            const inputValueCursor = processedValue[cursor];
            const inputValueCursorPlusOne = processedValue[cursor + 1];
            const inputValueCursorPlusTwo = processedValue[cursor + 2];
            const inputValueCursorMinusOne = processedValue[cursor - 1];
            const inputValueCursorMinusTwo = processedValue[cursor - 2];
            const inputValueSliceMinusThreeMinusOne = processedValue.slice(cursor - 3, cursor - 1);
            const inputValueSliceMinusOnePlusOne = processedValue.slice(cursor - 1, cursor + 1);
            const inputValueSliceCursorPlusTwo = processedValue.slice(cursor, cursor + 2);
            const inputValueSliceMinusTwoCursor = processedValue.slice(cursor - 2, cursor);
            // Issue #1523: when the date token directly abuts a plain digit
            // token (year-first masks without separators like 00M0d0 or
            // 0000M0d0), the backward-looking windows below read the previous
            // field's digits (year) as day/month digits and skip valid input.
            // Disable only those backward heuristics in that layout.
            const tokenAbutsDigitField = maskExpression[cursor - 1] === MaskExpression.NUMBER_ZERO;
            if (maskExpression[cursor] === MaskExpression.DAY) {
                const maskStartWithMonth = maskExpression.slice(0, 2) === MaskExpression.MONTHS;
                const startWithMonthInput = maskExpression.slice(0, 2) === MaskExpression.MONTHS &&
                    this.specialCharacters.includes(inputValueCursorMinusTwo);
                // Issue #1611: `cursor` indexes the mask, `i` indexes the input.
                // On paste/writeValue of a bare digit string the cursor runs
                // ahead of the input by every emitted separator, so cursor-based
                // slices read year digits instead of the day — anchor the day
                // window on the input index in those flows. Keystroke flows keep
                // the historical cursor anchor (with showMaskTyped the value also
                // carries placeholder chars, where the input anchor misreads).
                const dayWindowStart = params.justPasted || this.writingValue ? i : cursor;
                const dayWindowSlice = processedValue.slice(dayWindowStart, dayWindowStart + 2);
                const dayWindowNext = processedValue[dayWindowStart + 1];
                if ((Number(inputSymbol) > 3 && this.leadZeroDateTime) ||
                    (!maskStartWithMonth &&
                        (Number(inputValueSliceCursorPlusTwo) > daysCount ||
                            (!tokenAbutsDigitField &&
                                Number(inputValueSliceMinusOnePlusOne) > daysCount) ||
                            this.specialCharacters.includes(inputValueCursorPlusOne))) ||
                    (startWithMonthInput
                        ? Number(inputValueSliceMinusOnePlusOne) > daysCount ||
                            (!this.specialCharacters.includes(inputValueCursor) &&
                                this.specialCharacters.includes(inputValueCursorPlusTwo)) ||
                            this.specialCharacters.includes(inputValueCursor)
                        : Number(dayWindowSlice) > daysCount ||
                            (this.specialCharacters.includes(dayWindowNext) && !params.backspaced))) {
                    processedPosition = !this.leadZeroDateTime
                        ? processedPosition + 1
                        : processedPosition;
                    cursor += 1;
                    this._shiftStep(cursor);
                    i--;
                    if (this.leadZeroDateTime) {
                        result += '0';
                    }
                    continue;
                }
            }
            if (maskExpression[cursor] === MaskExpression.MONTH) {
                const monthsCount = 12;
                // Issue #1513: the backward-looking day/month windows below
                // assume the digits before the MONTH token belong to a DAY
                // field. In year-first masks with separators (e.g. 0000-M0-d0)
                // they read YEAR digits through the separator and shove a
                // spurious leading zero into the month. tokenAbutsDigitField
                // (#1523) only covers separator-less layouts, so derive field
                // ownership from the mask itself: locate the field (maximal
                // run of non-special tokens) immediately preceding this MONTH
                // token — a plain digit run of 3+ characters is a year, not a
                // day, and the day-based heuristics must not fire.
                let precedingFieldEnd = cursor - 1;
                while (precedingFieldEnd >= 0 &&
                    this.specialCharacters.includes(maskExpression[precedingFieldEnd])) {
                    precedingFieldEnd--;
                }
                let precedingFieldStart = precedingFieldEnd;
                while (precedingFieldStart >= 0 &&
                    !this.specialCharacters.includes(maskExpression[precedingFieldStart])) {
                    precedingFieldStart--;
                }
                const precedingField = maskExpression.slice(precedingFieldStart + 1, precedingFieldEnd + 1);
                const yearFieldPrecedesMonth = precedingField.length > 2 &&
                    precedingField
                        .split(MaskExpression.EMPTY_STRING)
                        .every((token) => token === MaskExpression.NUMBER_ZERO);
                // mask without day
                const withoutDays = cursor === 0 &&
                    (Number(inputSymbol) > 2 ||
                        Number(inputValueSliceCursorPlusTwo) > monthsCount ||
                        (this.specialCharacters.includes(inputValueCursorPlusOne) &&
                            !params.backspaced));
                // day<10 && month<12 for input
                const specialChart = maskExpression.slice(cursor + 2, cursor + 3);
                const day1monthInput = inputValueSliceMinusThreeMinusOne.includes(specialChart) &&
                    maskExpression.includes('d0') &&
                    ((this.specialCharacters.includes(inputValueCursorMinusTwo) &&
                        Number(inputValueSliceMinusOnePlusOne) > monthsCount &&
                        !this.specialCharacters.includes(inputValueCursor)) ||
                        this.specialCharacters.includes(inputValueCursor));
                //  month<12 && day<10 for input
                const day2monthInput = !tokenAbutsDigitField &&
                    !yearFieldPrecedesMonth &&
                    Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
                    !this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
                    this.specialCharacters.includes(inputValueCursorMinusOne) &&
                    (Number(inputValueSliceCursorPlusTwo) > monthsCount ||
                        this.specialCharacters.includes(inputValueCursorPlusOne));
                // cursor === 5 && without days
                const day2monthInputDot = (Number(inputValueSliceCursorPlusTwo) > monthsCount && cursor === 5) ||
                    (this.specialCharacters.includes(inputValueCursorPlusOne) && cursor === 5);
                // // day<10 && month<12 for paste whole data
                const day1monthPaste = !tokenAbutsDigitField &&
                    !yearFieldPrecedesMonth &&
                    Number(inputValueSliceMinusThreeMinusOne) > daysCount &&
                    !this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
                    !this.specialCharacters.includes(inputValueSliceMinusTwoCursor) &&
                    Number(inputValueSliceMinusTwoCursor) > monthsCount &&
                    maskExpression.includes('d0');
                // 10<day<31 && month<12 for paste whole data
                const day2monthPaste = !tokenAbutsDigitField &&
                    !yearFieldPrecedesMonth &&
                    Number(inputValueSliceMinusThreeMinusOne) <= daysCount &&
                    !this.specialCharacters.includes(inputValueSliceMinusThreeMinusOne) &&
                    !this.specialCharacters.includes(inputValueCursorMinusOne) &&
                    Number(inputValueSliceMinusOnePlusOne) > monthsCount;
                if ((Number(inputSymbol) > 1 && this.leadZeroDateTime) ||
                    withoutDays ||
                    day1monthInput ||
                    day2monthPaste ||
                    day1monthPaste ||
                    day2monthInput ||
                    (day2monthInputDot && !this.leadZeroDateTime)) {
                    processedPosition = !this.leadZeroDateTime
                        ? processedPosition + 1
                        : processedPosition;
                    cursor += 1;
                    this._shiftStep(cursor);
                    i--;
                    if (this.leadZeroDateTime) {
                        result += '0';
                    }
                    continue;
                }
            }
            result += inputSymbol;
            cursor++;
        }
        else if (this.specialCharacters.includes(inputSymbol) &&
            maskExpression[cursor] === inputSymbol) {
            result += inputSymbol;
            cursor++;
        }
        else if (this.specialCharacters.indexOf(maskExpression[cursor] ?? MaskExpression.EMPTY_STRING) !== -1) {
            result += maskExpression[cursor];
            cursor++;
            this._shiftStep(cursor);
            i--;
        }
        else if (maskExpression[cursor] === MaskExpression.NUMBER_NINE && this.showMaskTyped) {
            this._shiftStep(cursor);
        }
        else if (this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING] &&
            this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING]?.optional) {
            // If the input symbol is whitespace or doesn't match the pattern,
            // skip it without consuming the mask position
            if (inputSymbol.trim() === MaskExpression.EMPTY_STRING) {
                // Skip whitespace input, don't advance mask cursor
                continue;
            }
            if (!!inputArray[cursor] &&
                maskExpression !== '099.099.099.099' &&
                maskExpression !== '000.000.000-00' &&
                maskExpression !== '00.000.000/0000-00' &&
                !maskExpression.match(/^9+\.0+$/) &&
                !this.patterns[maskExpression[cursor] ?? MaskExpression.EMPTY_STRING]?.optional) {
                result += inputArray[cursor];
            }
            if (maskExpression.includes(MaskExpression.NUMBER_NINE + MaskExpression.SYMBOL_STAR) &&
                maskExpression.includes(MaskExpression.NUMBER_ZERO + MaskExpression.SYMBOL_STAR)) {
                cursor++;
            }
            cursor++;
            i--;
        }
        else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_STAR &&
            // A typed char that exactly matches the mask's literal char at cursor+2
            // (e.g. the '@' in 'A*@A*.A*') always terminates the 'A*' run, regardless
            // of whether that literal is registered in `specialCharacters` — an
            // explicitly empty `specialCharacters` list must not make mask literals
            // unmatchable (#1512).
            (this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)
                ? this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2]
                : inputSymbol === this.maskExpression[cursor + 2]) &&
            multi) {
            cursor += 3;
            result += inputSymbol;
        }
        else if (this.maskExpression[cursor + 1] === MaskExpression.SYMBOL_QUESTION &&
            (this._findSpecialChar(this.maskExpression[cursor + 2] ?? MaskExpression.EMPTY_STRING)
                ? this._findSpecialChar(inputSymbol) === this.maskExpression[cursor + 2]
                : inputSymbol === this.maskExpression[cursor + 2]) &&
            multi) {
            cursor += 3;
            result += inputSymbol;
        }
        else if (this.showMaskTyped &&
            this.specialCharacters.indexOf(inputSymbol) < 0 &&
            inputSymbol !== this.placeHolderCharacter &&
            this.placeHolderCharacter.length === 1) {
            stepBack = true;
        }
    }
    state.cursor = cursor;
    state.result = result;
    state.multi = multi;
    state.processedPosition = processedPosition;
    state.stepBack = stepBack;
    return state;
};

/** IP pre-processor (moved verbatim from applyMask). Sets `this.ipError`, rewrites
 *  the mask to `099.099.099.099`, then falls through to the generic loop. */
const ipHandler = function (state) {
    const valuesIP = state.processedValue.split(MaskExpression.DOT);
    this.ipError = this._validIP(valuesIP);
    state.maskExpression = '099.099.099.099';
    return state;
};

/** PERCENT handler (moved verbatim from applyMask). startsWith(PERCENT); terminal —
 *  produces the final `result`, no fallthrough. */
const percentHandler = function (state, params) {
    const { backspaced } = params;
    const { cursor } = state;
    let processedValue = state.processedValue;
    if (processedValue.match('[a-z]|[A-Z]') ||
        // eslint-disable-next-line no-useless-escape
        (processedValue.match(/[-!$%^&*()_+|~=`{}\[\]:";'<>?,\/.]/) && !backspaced)) {
        processedValue = this._stripToDecimal(processedValue);
        const precision = this.getPrecision(state.maskExpression);
        processedValue = this.checkInputPrecision(processedValue, precision);
    }
    const decimalMarker = this._activeDecimalMarker(processedValue);
    if (processedValue.indexOf(decimalMarker) > 0 &&
        !this.percentage(processedValue.substring(0, processedValue.indexOf(decimalMarker)))) {
        let base = processedValue.substring(0, processedValue.indexOf(decimalMarker) - 1);
        if (this.allowNegativeNumbers &&
            processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS &&
            !backspaced) {
            base = processedValue.substring(0, processedValue.indexOf(decimalMarker));
        }
        processedValue = `${base}${processedValue.substring(processedValue.indexOf(decimalMarker), processedValue.length)}`;
    }
    let value;
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    this.allowNegativeNumbers && processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS
        ? (value = `${MaskExpression.MINUS}${processedValue.slice(cursor + 1, cursor + processedValue.length)}`)
        : (value = processedValue);
    if (this.percentage(value)) {
        state.result = this._splitPercentZero(processedValue);
    }
    else {
        state.result = this._splitPercentZero(processedValue.substring(0, processedValue.length - 1));
    }
    state.processedValue = processedValue;
    return state;
};

/**
 * SEPARATOR handler (moved verbatim from applyMask, original lines 219-580).
 * Discriminator: `maskExpression.startsWith(MaskExpression.SEPARATOR)`. Produces the
 * final `result` for this call.
 *
 * Two sequential zero-handling stages are preserved together, in original relative
 * order (CODEBASE_NOTES.md #13): the `if (backspaced)` guard block and the
 * precision-0 leading-zero stripper.
 *
 * The `typeFromDecimals` sub-case (#733/#1414/#1315) early-returns the final string
 * via `state.earlyReturn` after calling `cb()` — this must bypass the shared
 * post-processing in `applyMask` exactly, or the caret-pin `cb()` contract breaks.
 */
const separatorHandler = function (state, params) {
    const { backspaced, justPasted, startsWithPrefix, prefixAlreadyRemovedByCaller, cb, inputValue, } = params;
    let { processedValue, processedPosition, backspaceShift, shift, stepBack } = state;
    let result;
    if (processedValue.match('[wа-яА-Я]') ||
        processedValue.match('[ЁёА-я]') ||
        processedValue.match('[a-z]|[A-Z]') ||
        processedValue.match(/[-@#!$%\\^&*()_£¬'+|~=`{}\]:";<>.?/]/) ||
        processedValue.match('[^A-Za-z0-9,]')) {
        processedValue = this._stripToDecimal(processedValue);
    }
    const precision = this.getPrecision(state.maskExpression);
    let decimalMarker = this.decimalMarker;
    if (Array.isArray(this.decimalMarker)) {
        decimalMarker = this._activeDecimalMarker(this.actualValue);
    }
    // Issues #733/#1414/#1315: opt-in "banking" typing mode — typed digits fill
    // the value from the decimal end, ATM/calculator style (5 -> 0.05 -> 0.57
    // -> 5.73); backspace shifts digits back to the right. Only keystroke and
    // backspace flows are reinterpreted; paste and writeValue keep the regular
    // separator formatting. The early return leaves every existing separator
    // path byte-identical when the option is off.
    if (this.typeFromDecimals &&
        Number.isFinite(precision) &&
        precision > 0 &&
        !justPasted &&
        !this.writingValue) {
        result = this._formatFromDecimals(processedValue, precision, decimalMarker);
        this._shift.clear();
        const res = result.includes(MaskExpression.MINUS) && this.prefix && this.allowNegativeNumbers
            ? `${MaskExpression.MINUS}${this.prefix}${result
                .split(MaskExpression.MINUS)
                .join(MaskExpression.EMPTY_STRING)}${this.suffix}`
            : result.length
                ? `${this.prefix}${result}${this.suffix}`
                : this.instantPrefix
                    ? this.prefix
                    : MaskExpression.EMPTY_STRING;
        // Pin the caret to the end of the value part: every keystroke reshapes
        // the whole string, so the in-place caret position is meaningless here.
        cb(res.length - this.suffix.length - processedPosition, true);
        state.earlyReturn = res;
        return state;
    }
    // Issue #1250: typing an additional decimal marker into a value that
    // already contains one must be a no-op — otherwise everything after the
    // new marker is re-parsed as a fresh decimal part and the value is
    // mangled (15.000,53 + ',' typed after the '1' -> 1,5). Remove the newly
    // typed marker (the char right before the caret) and step the caret back.
    // Paste keeps its own semantics (#1547 below: last marker wins), and
    // backspace/writeValue flows cannot introduce a new marker.
    if (!justPasted && !backspaced && !this.writingValue) {
        const isDecimalMarkerChar = (char) => !!char &&
            char !== this.thousandSeparator &&
            (Array.isArray(this.decimalMarker)
                ? this.decimalMarker.includes(char)
                : char === this.decimalMarker);
        const prefixOffset = startsWithPrefix && !prefixAlreadyRemovedByCaller ? this.prefix.length : 0;
        const typedMarkerIndex = processedPosition - prefixOffset - 1;
        if (typedMarkerIndex >= 0 && isDecimalMarkerChar(processedValue[typedMarkerIndex])) {
            let markerCount = 0;
            for (const char of processedValue) {
                if (isDecimalMarkerChar(char)) {
                    markerCount++;
                }
            }
            if (markerCount > 1) {
                processedValue =
                    processedValue.slice(0, typedMarkerIndex) +
                        processedValue.slice(typedMarkerIndex + 1);
                stepBack = true;
            }
        }
    }
    // Issue #1547: a pasted value may contain grouping separators that are
    // also configured decimal markers (default decimalMarker is ['.', ',']),
    // e.g. '1,234.56'. Only the last marker character can actually be the
    // decimal marker — treat the earlier ones as thousand separators and
    // strip them, otherwise formatting cuts the value off at the first one.
    if (justPasted && Array.isArray(this.decimalMarker)) {
        const markerPositions = [];
        for (let i = 0; i < processedValue.length; i++) {
            const char = processedValue[i];
            if (char !== this.thousandSeparator &&
                this.decimalMarker.includes(char)) {
                markerPositions.push(i);
            }
        }
        if (markerPositions.length > 1) {
            const lastMarkerPosition = markerPositions[markerPositions.length - 1];
            processedValue = processedValue
                .split(MaskExpression.EMPTY_STRING)
                .filter((_, index) => index === lastMarkerPosition || !markerPositions.includes(index))
                .join(MaskExpression.EMPTY_STRING);
        }
    }
    if (backspaced) {
        const { decimalMarkerIndex, nonZeroIndex } = this._findFirstNonZeroAndDecimalIndex(processedValue, decimalMarker);
        const zeroIndexMinus = processedValue[0] === MaskExpression.MINUS;
        const zeroIndexDecimalMarker = processedValue[0] === decimalMarker;
        const firstIndexDecimalMarker = processedValue[1] === decimalMarker;
        // Issues #1355/#1578: an all-zero remainder (e.g. '00' from '500', '00,000'
        // from '100,000') must keep its zeros, matching the ',000,000' case.
        // Only collapse when nothing but a bare decimal marker (or '-.') remains.
        if ((zeroIndexDecimalMarker && !nonZeroIndex) ||
            (zeroIndexMinus && firstIndexDecimalMarker && !nonZeroIndex)) {
            processedValue = MaskExpression.NUMBER_ZERO;
        }
        if (decimalMarkerIndex && nonZeroIndex && zeroIndexMinus && processedPosition === 1) {
            if (decimalMarkerIndex < nonZeroIndex || decimalMarkerIndex > nonZeroIndex) {
                processedValue = MaskExpression.MINUS + processedValue.slice(nonZeroIndex);
            }
        }
        // Issue #1516: decimalMarkerIndex is 0 when the remainder starts with
        // the decimal marker (',34' after deleting the integer part) — a truthy
        // check would treat it as "no decimal marker" and slice the marker off.
        if (decimalMarkerIndex === null && nonZeroIndex && processedValue.length > nonZeroIndex) {
            processedValue = zeroIndexMinus
                ? MaskExpression.MINUS + processedValue.slice(nonZeroIndex)
                : processedValue.slice(nonZeroIndex);
        }
        if (decimalMarkerIndex && nonZeroIndex && processedPosition === 0) {
            if (decimalMarkerIndex < nonZeroIndex) {
                processedValue = processedValue.slice(decimalMarkerIndex - 1);
            }
            if (decimalMarkerIndex > nonZeroIndex) {
                processedValue = processedValue.slice(nonZeroIndex);
            }
        }
    }
    // Leading-zero stripping is a typing-time rule ('05' -> '5'). When backspaced,
    // leading zeros before a non-zero digit were already removed above, and an
    // all-zero remainder must keep its zeros (issues #1355/#1578).
    if (precision === 0 && !backspaced) {
        processedValue = this.allowNegativeNumbers
            ? processedValue.length > 2 &&
                processedValue[0] === MaskExpression.MINUS &&
                processedValue[1] === MaskExpression.NUMBER_ZERO &&
                processedValue[2] !== this.thousandSeparator &&
                processedValue[2] !== MaskExpression.COMMA &&
                processedValue[2] !== MaskExpression.DOT
                ? '-' + processedValue.slice(2, processedValue.length)
                : processedValue[0] === MaskExpression.NUMBER_ZERO &&
                    processedValue.length > 1 &&
                    processedValue[1] !== this.thousandSeparator &&
                    processedValue[1] !== MaskExpression.COMMA &&
                    processedValue[1] !== MaskExpression.DOT
                    ? processedValue.slice(1, processedValue.length)
                    : processedValue
            : processedValue.length > 1 &&
                processedValue[0] === MaskExpression.NUMBER_ZERO &&
                processedValue[1] !== this.thousandSeparator &&
                processedValue[1] !== MaskExpression.COMMA &&
                processedValue[1] !== MaskExpression.DOT
                ? processedValue.slice(1, processedValue.length)
                : processedValue;
    }
    else {
        if (processedValue[0] === decimalMarker && processedValue.length > 1 && !backspaced) {
            processedValue =
                MaskExpression.NUMBER_ZERO + processedValue.slice(0, processedValue.length + 1);
            this.plusOnePosition = true;
        }
        if (processedValue[0] === MaskExpression.NUMBER_ZERO &&
            processedValue[1] !== decimalMarker &&
            processedValue[1] !== this.thousandSeparator &&
            !backspaced) {
            processedValue =
                processedValue.length > 1
                    ? processedValue.slice(0, 1) +
                        decimalMarker +
                        processedValue.slice(1, processedValue.length + 1)
                    : processedValue;
            this.plusOnePosition = true;
        }
        if (this.allowNegativeNumbers &&
            !backspaced &&
            processedValue[0] === MaskExpression.MINUS &&
            (processedValue[1] === decimalMarker ||
                processedValue[1] === MaskExpression.NUMBER_ZERO)) {
            processedValue =
                processedValue[1] === decimalMarker && processedValue.length > 2
                    ? processedValue.slice(0, 1) +
                        MaskExpression.NUMBER_ZERO +
                        processedValue.slice(1, processedValue.length)
                    : processedValue[1] === MaskExpression.NUMBER_ZERO &&
                        processedValue.length > 2 &&
                        processedValue[2] !== decimalMarker
                        ? processedValue.slice(0, 2) +
                            decimalMarker +
                            processedValue.slice(2, processedValue.length)
                        : processedValue;
            this.plusOnePosition = true;
        }
    }
    // Historical note: pre-refactor code used different allowed-character regexes
    // per config (plain separator: no COMMA; dot thousand-sep: no SPACE, COMMA OK;
    // comma thousand-sep: no SPACE, COMMA OK). The unified regex below removes
    // exactly the active thousandSeparator + decimalMarker(s) from the invalid set,
    // so each config already accepts its own chars and rejects the others —
    // verified by the "unified invalidChars regex" cases in separator.spec.ts.
    const thousandSeparatorCharEscaped = this._charToRegExpExpression(this.thousandSeparator);
    let invalidChars = '@#!$%^&*()_+|~=`{}\\[\\]:\\s,\\.";<>?\\/'.replace(thousandSeparatorCharEscaped, '');
    //.replace(decimalMarkerEscaped, '');
    if (Array.isArray(this.decimalMarker)) {
        for (const marker of this.decimalMarker) {
            invalidChars = invalidChars.replace(this._charToRegExpExpression(marker), MaskExpression.EMPTY_STRING);
        }
    }
    else {
        invalidChars = invalidChars.replace(this._charToRegExpExpression(this.decimalMarker), '');
    }
    const invalidCharRegexp = new RegExp('[' + invalidChars + ']');
    if (processedValue.match(invalidCharRegexp)) {
        processedValue = processedValue.substring(0, processedValue.length - 1);
    }
    processedValue = this.checkInputPrecision(processedValue, precision);
    const strForSep = processedValue.replace(new RegExp(thousandSeparatorCharEscaped, 'g'), '');
    result = this._formatWithSeparators(strForSep, this.thousandSeparator, this.decimalMarker, precision);
    const commaShift = result.indexOf(MaskExpression.COMMA) - processedValue.indexOf(MaskExpression.COMMA);
    const shiftStep = result.length - processedValue.length;
    const backspacedDecimalMarkerWithSeparatorLimit = backspaced && result.length < inputValue.length - this.suffix.length && this.separatorLimit;
    if ((result[processedPosition - 1] === this.thousandSeparator ||
        result[processedPosition - this.prefix.length]) &&
        this.prefix &&
        backspaced) {
        processedPosition = processedPosition - 1;
    }
    else if ((shiftStep > 0 && result[processedPosition] !== this.thousandSeparator) ||
        backspacedDecimalMarkerWithSeparatorLimit) {
        backspaceShift = true;
        let _shift = 0;
        do {
            this._shift.add(processedPosition + _shift);
            _shift++;
        } while (_shift < shiftStep);
    }
    else if (result[processedPosition - 1] === this.thousandSeparator ||
        shiftStep === -4 ||
        shiftStep === -3 ||
        result[processedPosition] === this.thousandSeparator) {
        this._shift.clear();
        this._shift.add(processedPosition - 1);
    }
    else if ((commaShift !== 0 &&
        processedPosition > 0 &&
        !(result.indexOf(MaskExpression.COMMA) >= processedPosition && processedPosition > 3)) ||
        (!(result.indexOf(MaskExpression.DOT) >= processedPosition && processedPosition > 3) &&
            shiftStep <= 0)) {
        this._shift.clear();
        backspaceShift = true;
        shift = shiftStep;
        processedPosition += shiftStep;
        this._shift.add(processedPosition);
    }
    else {
        this._shift.clear();
    }
    state.processedValue = processedValue;
    state.processedPosition = processedPosition;
    state.result = result;
    state.backspaceShift = backspaceShift;
    state.shift = shift;
    state.stepBack = stepBack;
    return state;
};

const MASK_HANDLERS = [
    {
        id: 'ip',
        terminal: false,
        match: (maskExpression) => maskExpression === MaskExpression.IP,
        handle: ipHandler,
    },
    {
        id: 'cpf-cnpj',
        terminal: false,
        match: (maskExpression) => maskExpression === MaskExpression.CPF_CNPJ ||
            maskExpression === MaskExpression.CPF_CNPJ_ALPHA,
        handle: cpfCnpjHandler,
    },
    {
        id: 'percent',
        terminal: true,
        match: (maskExpression) => maskExpression.startsWith(MaskExpression.PERCENT),
        handle: percentHandler,
    },
    {
        id: 'separator',
        terminal: true,
        match: (maskExpression) => maskExpression.startsWith(MaskExpression.SEPARATOR),
        handle: separatorHandler,
    },
];
/** Runs mask-type dispatch: first matching entry wins. IP/CPF_CNPJ pre-process then
 *  fall through to the generic loop; PERCENT/SEPARATOR resolve terminally; no match →
 *  generic loop directly (the original `else`). IP and CPF_CNPJ are mutually exclusive
 *  exact matches, so scanning the ordered table is behavior-identical to the original
 *  IP → CPF_CNPJ → PERCENT → SEPARATOR → else chain. */
function dispatchMaskHandler(self, state, params) {
    for (const entry of MASK_HANDLERS) {
        if (entry.match(state.maskExpression)) {
            const next = entry.handle.call(self, state, params);
            return entry.terminal ? next : genericPatternHandler.call(self, next, params);
        }
    }
    return genericPatternHandler.call(self, state, params);
}

class NgxMaskApplierService {
    _config = inject(NGX_MASK_CONFIG);
    dropSpecialCharacters = this._config.dropSpecialCharacters;
    hiddenInput = this._config.hiddenInput;
    clearIfNotMatch = this._config.clearIfNotMatch;
    specialCharacters = this._config.specialCharacters;
    patterns = this._config.patterns;
    prefix = this._config.prefix;
    suffix = this._config.suffix;
    thousandSeparator = this._config.thousandSeparator;
    decimalMarker = this._config.decimalMarker;
    customPattern;
    showMaskTyped = this._config.showMaskTyped;
    placeHolderCharacter = this._config.placeHolderCharacter;
    validation = this._config.validation;
    separatorLimit = this._config.separatorLimit;
    allowNegativeNumbers = this._config.allowNegativeNumbers;
    leadZeroDateTime = this._config.leadZeroDateTime;
    leadZero = this._config.leadZero;
    typeFromDecimals = this._config.typeFromDecimals;
    apm = this._config.apm;
    inputTransformFn = this._config.inputTransformFn;
    outputTransformFn = this._config.outputTransformFn;
    keepCharacterPositions = this._config.keepCharacterPositions;
    instantPrefix = this._config.instantPrefix;
    triggerOnMaskChange = this._config.triggerOnMaskChange;
    _shift = new Set();
    plusOnePosition = false;
    maskExpression = '';
    actualValue = '';
    showKeepCharacterExp = '';
    shownMaskExpression = this._config.shownMaskExpression;
    deletedSpecialCharacter = false;
    /**
     * Whether we are currently in writeValue function, in this case when applying the mask we don't want to trigger onChange function,
     * since writeValue should be a one way only process of writing the DOM value based on the Angular model value.
     */
    writingValue = false;
    ipError;
    cpfCnpjError;
    applyMask(inputValue, maskExpression, position = 0, justPasted = false, backspaced = false, 
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    cb = () => { }) {
        if (!maskExpression || typeof inputValue !== 'string') {
            return MaskExpression.EMPTY_STRING;
        }
        let cursor = 0;
        let result = '';
        const multi = false;
        let backspaceShift = false;
        let shift = 1;
        let stepBack = false;
        let processedValue = inputValue;
        let processedPosition = position;
        const startsWithPrefix = processedValue.slice(0, this.prefix.length) === this.prefix;
        // On paste with showMaskTyped, NgxMaskService.applyMask has already removed the
        // prefix via removeMask() before delegating here (unless the value consisted of
        // the prefix alone, in which case the raw value falls through). Stripping again
        // would eat leading characters that merely look like the prefix (#1551).
        const prefixAlreadyRemovedByCaller = justPasted &&
            this.showMaskTyped &&
            this.placeHolderCharacter.length === 1 &&
            !this.leadZeroDateTime &&
            processedValue !== this.prefix;
        if (startsWithPrefix && !prefixAlreadyRemovedByCaller) {
            processedValue = processedValue.slice(this.prefix.length);
        }
        if (!!this.suffix && processedValue.length > 0) {
            processedValue = this.checkAndRemoveSuffix(processedValue);
        }
        if (processedValue === '(' && this.prefix) {
            processedValue = '';
        }
        const inputArray = processedValue.toString().split(MaskExpression.EMPTY_STRING);
        if (this.allowNegativeNumbers &&
            processedValue.slice(cursor, cursor + 1) === MaskExpression.MINUS) {
            result += processedValue.slice(cursor, cursor + 1);
        }
        const arr = [];
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let i = 0; i < processedValue.length; i++) {
            if (processedValue[i]?.match('\\d')) {
                arr.push(processedValue[i] ?? MaskExpression.EMPTY_STRING);
            }
        }
        // Per-mask-type dispatch (IP/CPF_CNPJ pre-processors → generic loop; PERCENT and
        // SEPARATOR terminal handlers; generic pattern loop as fallback). See
        // ./mask-handlers/mask-handlers.registry.ts.
        const state = {
            processedValue,
            processedPosition,
            cursor,
            result,
            multi,
            backspaceShift,
            shift,
            stepBack,
            maskExpression,
        };
        const resolved = dispatchMaskHandler(this, state, {
            inputValue,
            position,
            justPasted,
            backspaced,
            cb,
            inputArray,
            arr,
            startsWithPrefix,
            prefixAlreadyRemovedByCaller,
        });
        if (typeof resolved.earlyReturn === 'string') {
            return resolved.earlyReturn;
        }
        processedValue = resolved.processedValue;
        processedPosition = resolved.processedPosition;
        cursor = resolved.cursor;
        result = resolved.result;
        backspaceShift = resolved.backspaceShift;
        shift = resolved.shift;
        stepBack = resolved.stepBack;
        // eslint-disable-next-line no-param-reassign
        maskExpression = resolved.maskExpression;
        if (result[processedPosition - 1] &&
            result.length + 1 === maskExpression.length &&
            this.specialCharacters.indexOf(maskExpression[maskExpression.length - 1] ?? MaskExpression.EMPTY_STRING) !== -1) {
            result += maskExpression[maskExpression.length - 1];
        }
        let newPosition = processedPosition + 1;
        while (this._shift.has(newPosition)) {
            shift++;
            newPosition++;
        }
        let actualShift = justPasted && !maskExpression.startsWith(MaskExpression.SEPARATOR)
            ? cursor
            : this._shift.has(processedPosition)
                ? shift
                : 0;
        if (stepBack) {
            actualShift--;
        }
        cb(actualShift, backspaceShift);
        if (shift < 0) {
            this._shift.clear();
        }
        let onlySpecial = false;
        if (backspaced) {
            onlySpecial = inputArray.every((char) => this.specialCharacters.includes(char));
        }
        let res = `${this.prefix}${onlySpecial ? MaskExpression.EMPTY_STRING : result}${this.showMaskTyped ? '' : this.suffix}`;
        if (result.length === 0) {
            res = this.instantPrefix ? `${this.prefix}${result}` : `${result}`;
        }
        const isSpecialCharacterMaskFirstSymbol = processedValue.length === 1 &&
            this.specialCharacters.includes(maskExpression[0]) &&
            processedValue !== maskExpression[0];
        if (isSpecialCharacterMaskFirstSymbol) {
            // The mask may start with several literal special characters in a row
            // (e.g. '+(000) 000-0000'). A single typed character must be checked
            // against the first PATTERN position of the mask, not literally against
            // index 1 — otherwise a valid digit is rejected and the leading literals
            // are never auto-filled (#1498).
            let firstPatternIndex = 1;
            while (firstPatternIndex < maskExpression.length &&
                this.specialCharacters.includes(maskExpression[firstPatternIndex])) {
                firstPatternIndex++;
            }
            if (!this._checkSymbolMask(processedValue, maskExpression[firstPatternIndex] ?? MaskExpression.EMPTY_STRING)) {
                return '';
            }
        }
        if (result.includes(MaskExpression.MINUS) && this.prefix && this.allowNegativeNumbers) {
            if (backspaced && result === MaskExpression.MINUS) {
                return '';
            }
            res = `${MaskExpression.MINUS}${this.prefix}${result
                .split(MaskExpression.MINUS)
                .join(MaskExpression.EMPTY_STRING)}${this.suffix}`;
        }
        return res;
    }
    _findDropSpecialChar(inputSymbol) {
        if (Array.isArray(this.dropSpecialCharacters)) {
            return this.dropSpecialCharacters.find((val) => val === inputSymbol);
        }
        return this._findSpecialChar(inputSymbol);
    }
    _findSpecialChar(inputSymbol) {
        return this.specialCharacters.find((val) => val === inputSymbol);
    }
    _checkSymbolMask(inputSymbol, maskSymbol) {
        this.patterns = this.customPattern ? this.customPattern : this.patterns;
        return ((this.patterns[maskSymbol]?.pattern &&
            this.patterns[maskSymbol]?.pattern.test(inputSymbol)) ??
            false);
    }
    _formatWithSeparators = (str, thousandSeparatorChar, decimalChars, precision) => {
        let x;
        let decimalChar;
        if (Array.isArray(decimalChars)) {
            const regExp = new RegExp(decimalChars.map((v) => ('[\\^$.|?*+()'.indexOf(v) >= 0 ? `\\${v}` : v)).join('|'));
            x = str.split(regExp);
            decimalChar = str.match(regExp)?.[0] ?? MaskExpression.EMPTY_STRING;
        }
        else {
            x = str.split(decimalChars);
            decimalChar = decimalChars;
        }
        const decimals = x.length > 1 ? `${decimalChar}${x[1]}` : MaskExpression.EMPTY_STRING;
        let res = x[0] ?? MaskExpression.EMPTY_STRING;
        const separatorLimit = this.separatorLimit.replace(/\s/g, MaskExpression.EMPTY_STRING);
        if (separatorLimit && +separatorLimit) {
            if (res[0] === MaskExpression.MINUS) {
                res = `-${res.slice(1, res.length).slice(0, separatorLimit.length)}`;
            }
            else {
                res = res.slice(0, separatorLimit.length);
            }
        }
        res = this._applyThousandGrouping(res, thousandSeparatorChar);
        if (typeof precision === 'undefined') {
            return res + decimals;
        }
        else if (precision === 0) {
            return res;
        }
        return res + decimals.substring(0, precision + 1);
    };
    /**
     * Formats a value for the `typeFromDecimals` mode: every digit of the raw value is
     * read as one integer that is then split `precision` digits from the right
     * (ATM/calculator style). Non-digit characters are ignored, so plain typing,
     * mid-string edits and backspace all reduce to "digits shifted through the
     * decimal marker".
     */
    _formatFromDecimals(value, precision, decimalMarker) {
        const negative = this.allowNegativeNumbers && value.startsWith(MaskExpression.MINUS);
        let digits = value.replace(/\D+/g, MaskExpression.EMPTY_STRING).replace(/^0+/, '');
        if (!digits) {
            return negative ? MaskExpression.MINUS : MaskExpression.EMPTY_STRING;
        }
        const separatorLimit = this.separatorLimit.replace(/\s/g, MaskExpression.EMPTY_STRING);
        if (separatorLimit && +separatorLimit) {
            // Dropping the overflow from the right rejects the most recently typed
            // digits once the integer part has reached the configured limit.
            digits = digits.slice(0, separatorLimit.length + precision);
        }
        digits = digits.padStart(precision + 1, MaskExpression.NUMBER_ZERO);
        const integerPart = this._applyThousandGrouping(digits.slice(0, digits.length - precision), this.thousandSeparator);
        const decimalPart = digits.slice(digits.length - precision);
        return `${negative ? MaskExpression.MINUS : MaskExpression.EMPTY_STRING}${integerPart}${decimalMarker}${decimalPart}`;
    }
    /** Inserts `separator` between every 3-digit group of an integer-digit string. */
    _applyThousandGrouping(digits, separator) {
        const rgx = /(\d+)(\d{3})/;
        let grouped = digits;
        while (separator && rgx.test(grouped)) {
            grouped = grouped.replace(rgx, '$1' + separator + '$2');
        }
        return grouped;
    }
    percentage = (str) => {
        const sanitizedStr = str.replace(',', '.');
        const value = Number(this.allowNegativeNumbers && str.includes(MaskExpression.MINUS)
            ? sanitizedStr.slice(1, str.length)
            : sanitizedStr);
        return !isNaN(value) && value >= 0 && value <= 100;
    };
    getPrecision = (maskExpression) => {
        const x = maskExpression.split(MaskExpression.DOT);
        if (x.length > 1) {
            return Number(x[x.length - 1]);
        }
        return Infinity;
    };
    checkAndRemoveSuffix = (inputValue) => {
        for (let i = this.suffix?.length - 1; i >= 0; i--) {
            const substr = this.suffix.substring(i, this.suffix?.length);
            if (inputValue.endsWith(substr) &&
                i !== this.suffix?.length - 1 &&
                // A partial suffix tail (i > 0) that makes up the WHOLE value is a
                // leftover of the displayed suffix only when the previous rendered
                // value ended with the suffix AND the edit shrank the value to (or
                // below) the old value-part length, i.e. it was a deletion of the
                // suffix head. Otherwise it is fresh user input that merely collides
                // with the suffix text and must be kept (#1495).
                (i === 0 ||
                    inputValue.length > substr.length ||
                    (this.actualValue.endsWith(this.suffix) &&
                        inputValue.length <= this.actualValue.length - this.suffix.length)) &&
                (i - 1 < 0 ||
                    !inputValue.endsWith(this.suffix.substring(i - 1, this.suffix?.length)))) {
                return inputValue.slice(0, inputValue.length - substr.length);
            }
        }
        return inputValue;
    };
    checkInputPrecision = (inputValue, precision) => {
        let processedInputValue = inputValue;
        if (precision < Infinity) {
            const decimalMarker = this._activeDecimalMarker(inputValue);
            const precisionRegEx = new RegExp(this._charToRegExpExpression(decimalMarker) + `\\d{${precision}}.*$`);
            const precisionMatch = processedInputValue.match(precisionRegEx);
            const precisionMatchLength = (precisionMatch && precisionMatch[0]?.length) ?? 0;
            if (precisionMatchLength - 1 > precision) {
                const diff = precisionMatchLength - 1 - precision;
                processedInputValue = processedInputValue.substring(0, processedInputValue.length - diff);
            }
            if (precision === 0 &&
                this._compareOrIncludes(processedInputValue[processedInputValue.length - 1], decimalMarker, this.thousandSeparator)) {
                processedInputValue = processedInputValue.substring(0, processedInputValue.length - 1);
            }
        }
        return processedInputValue;
    };
    _stripToDecimal(str) {
        const isDecimalMarkerChar = (char) => char === MaskExpression.DOT || char === MaskExpression.COMMA;
        return str
            .split(MaskExpression.EMPTY_STRING)
            .filter((i, idx) => {
            const isDecimalMarker = typeof this.decimalMarker === 'string'
                ? i === this.decimalMarker
                : isDecimalMarkerChar(i) && this.decimalMarker.includes(i);
            return (i.match('^-?\\d') ||
                i === this.thousandSeparator ||
                isDecimalMarker ||
                (i === MaskExpression.MINUS && idx === 0 && this.allowNegativeNumbers));
        })
            .join(MaskExpression.EMPTY_STRING);
    }
    _charToRegExpExpression(char) {
        // if (Array.isArray(char)) {
        // 	return char.map((v) => ('[\\^$.|?*+()'.indexOf(v) >= 0 ? `\\${v}` : v)).join('|');
        // }
        if (char) {
            const charsToEscape = '[\\^$.|?*+()';
            return char === ' ' ? '\\s' : charsToEscape.indexOf(char) >= 0 ? `\\${char}` : char;
        }
        return char;
    }
    _shiftStep(cursor) {
        this._shift.add(cursor + this.prefix.length || 0);
    }
    _compareOrIncludes(value, comparedValue, excludedValue) {
        return Array.isArray(comparedValue)
            ? comparedValue.filter((v) => v !== excludedValue).includes(value)
            : value === comparedValue;
    }
    _validIP(valuesIP) {
        return !(valuesIP.length === 4 &&
            !valuesIP.some((value, index) => {
                if (valuesIP.length !== index + 1) {
                    return value === MaskExpression.EMPTY_STRING || Number(value) > 255;
                }
                return value === MaskExpression.EMPTY_STRING || Number(value.substring(0, 3)) > 255;
            }));
    }
    /**
     * The single rule for "which decimal marker is active": the configured marker, or — for an
     * array — the first configured marker present in `value` that is not the thousandSeparator,
     * else the first configured marker that is not the thousandSeparator (#1653).
     */
    _activeDecimalMarker(value) {
        if (!Array.isArray(this.decimalMarker)) {
            return this.decimalMarker;
        }
        const candidates = this.decimalMarker.filter((dm) => dm !== this.thousandSeparator);
        return (candidates.find((dm) => value.includes(dm)) ?? candidates[0] ?? this.decimalMarker[0]);
    }
    _splitPercentZero(value) {
        if (value === MaskExpression.MINUS && this.allowNegativeNumbers) {
            return value;
        }
        const decimal = this._activeDecimalMarker(value);
        const decimalIndex = value.indexOf(decimal);
        const emptyOrMinus = this.allowNegativeNumbers && value.includes(MaskExpression.MINUS) ? '-' : '';
        if (decimalIndex === -1) {
            const parsedValue = parseInt(emptyOrMinus ? value.slice(1, value.length) : value, 10);
            return isNaN(parsedValue)
                ? MaskExpression.EMPTY_STRING
                : `${emptyOrMinus}${parsedValue}`;
        }
        else {
            const integerPart = parseInt(value.replace('-', '').substring(0, decimalIndex), 10);
            const decimalPart = value.substring(decimalIndex + 1);
            const integerString = isNaN(integerPart) ? '' : integerPart.toString();
            return integerString === MaskExpression.EMPTY_STRING
                ? MaskExpression.EMPTY_STRING
                : `${emptyOrMinus}${integerString}${decimal}${decimalPart}`;
        }
    }
    _findFirstNonZeroAndDecimalIndex(inputString, decimalMarker) {
        let decimalMarkerIndex = null;
        let nonZeroIndex = null;
        for (let i = 0; i < inputString.length; i++) {
            const char = inputString[i];
            if (char === decimalMarker && decimalMarkerIndex === null) {
                decimalMarkerIndex = i;
            }
            if (char && char >= '1' && char <= '9' && nonZeroIndex === null) {
                nonZeroIndex = i;
            }
            if (decimalMarkerIndex !== null && nonZeroIndex !== null) {
                break;
            }
        }
        return {
            decimalMarkerIndex,
            nonZeroIndex,
        };
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskApplierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
    static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskApplierService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskApplierService, decorators: [{
            type: Injectable
        }] });

class NgxMaskService extends NgxMaskApplierService {
    isNumberValue = false;
    maskIsShown = '';
    selStart = null;
    selEnd = null;
    maskChanged = false;
    maskExpressionArray = [];
    previousValue = '';
    currentValue = '';
    // `writingValue` is declared on NgxMaskApplierService: applyMask needs it to
    // distinguish the writeValue flow from keystroke flows (#1611).
    isInitialized = false;
    /**
     * Set by the directive's keepCharacterPositions handling for the current edit:
     * true — the directive fully resolved the resulting display value into actualValue,
     * so applyMask must short-circuit and render actualValue as-is;
     * false — the edit must flow through regular masking (no short-circuit);
     * null — the directive was not involved in this applyMask call (legacy behavior).
     * Consumed and reset by applyMask. This lets keepCharacterPositions work without
     * showMaskTyped (#1545, #1543).
     */
    keepCharacterPositionsHandled = null;
    _isFocused = signal(false, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_isFocused" }] : /* istanbul ignore next */ []));
    _emitValue = false;
    _start;
    _end;
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    onChange = (_) => { };
    _elementRef = inject(ElementRef, { optional: true });
    document = inject(DOCUMENT);
    _config = inject(NGX_MASK_CONFIG);
    _renderer = inject(Renderer2, { optional: true });
    /**
     * Applies the mask to the input value.
     * @param inputValue The input value to be masked.
     * @param maskExpression The mask expression to apply.
     * @param position The position in the input value.
     * @param justPasted Whether the value was just pasted.
     * @param backspaced Whether the value was backspaced.
     * @param cb Callback function.
     * @returns The masked value.
     */
    applyMask(inputValue, maskExpression, position = 0, justPasted = false, backspaced = false, 
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    cb = () => { }) {
        // Consume the directive's keepCharacterPositions verdict for this edit (see the
        // keepCharacterPositionsHandled doc). Reset immediately so it never leaks into
        // applyMask calls that do not originate from the directive's input handling.
        const kcpHandled = this.keepCharacterPositionsHandled;
        this.keepCharacterPositionsHandled = null;
        // If no mask expression, return the input value or the actual value
        if (!maskExpression) {
            // A RECONFIGURATION to an empty mask (mask was non-empty, now swapped to '') must
            // revert the display/model to the raw unmasked value, not the stale masked
            // `actualValue` from the mask that no longer applies (#1616: mask signal set to ''
            // on a populated control left the masked display/FormControl unchanged forever,
            // since this early return short-circuited before the `maskChanged`/emit logic
            // below ever ran). Only strip when a mask genuinely changed — a directive with no
            // mask ever configured must keep returning verbatim `actualValue`/`inputValue`.
            if (this.maskChanged) {
                const rawValue = this.removeMask(inputValue);
                this.previousValue = this.currentValue;
                this.currentValue = rawValue;
                this.actualValue = rawValue;
                this._emitValue = this.previousValue !== this.currentValue;
                if (this._emitValue && this.triggerOnMaskChange) {
                    this.formControlResult(rawValue);
                }
                if (!this.triggerOnMaskChange) {
                    this.maskChanged = false;
                }
                return rawValue;
            }
            return inputValue !== this.actualValue ? this.actualValue : inputValue;
        }
        // #1492: a numeric FormControl value like 0.0000007 or 1e21 stringifies to
        // exponential notation ('7e-7'); separator masking would strip the 'e'/'-' and
        // render garbage ('77'). Expand scientific notation to plain decimal form before
        // masking. Covers writeValue-driven flows and the pipe; a keystroke value can
        // never match, since separator masking never lets a letter through.
        if (maskExpression.startsWith(MaskExpression.SEPARATOR) &&
            inputValue &&
            /\d[eE][+-]?\d/.test(inputValue)) {
            const expandedNumber = Number(this._replaceDecimalMarkerToDot(inputValue));
            if (!Number.isNaN(expandedNumber)) {
                // eslint-disable-next-line no-param-reassign
                inputValue = this._toPlainDecimalString(expandedNumber);
                if (this.decimalMarker === MaskExpression.COMMA ||
                    (Array.isArray(this.decimalMarker) &&
                        this.thousandSeparator === MaskExpression.DOT)) {
                    // eslint-disable-next-line no-param-reassign
                    inputValue = inputValue.replace(MaskExpression.DOT, MaskExpression.COMMA);
                }
            }
        }
        // Show mask in input if required
        this.maskIsShown = this.showMaskTyped
            ? this.showMaskInInput()
            : MaskExpression.EMPTY_STRING;
        // Handle specific mask expressions
        if (this.maskExpression === MaskExpression.IP && this.showMaskTyped) {
            this.maskIsShown = this.showMaskInInput(inputValue || MaskExpression.HASH);
        }
        const isCpfCnpjMask = this.maskExpression === MaskExpression.CPF_CNPJ ||
            this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA;
        if (isCpfCnpjMask && this.showMaskTyped) {
            this.maskIsShown = this.showMaskInInput(inputValue || MaskExpression.HASH);
        }
        // Handle empty input value with mask typed
        if (!inputValue && this.showMaskTyped) {
            // #1590: only a USER-driven clear may propagate back to the model. When this
            // skeleton render originates from writeValue() (writingValue), emitting '' here
            // would be a view->model write inside a model->view sync — with nested CVA
            // wrappers the real initial value arrives in a LATER microtask than the initial
            // null write, and this echo overwrote it before it ever reached the view.
            if (!this.writingValue) {
                this.formControlResult(this.prefix);
            }
            return `${this.prefix}${this.maskIsShown}${this.suffix}`;
        }
        const getSymbol = !!inputValue && typeof this.selStart === 'number'
            ? (inputValue[this.selStart] ?? MaskExpression.EMPTY_STRING)
            : MaskExpression.EMPTY_STRING;
        let newInputValue = '';
        let newPosition = position;
        // Handle hidden input or input with asterisk symbol
        if ((this.hiddenInput ||
            (inputValue && inputValue.indexOf(MaskExpression.SYMBOL_STAR) >= 0)) &&
            !this.writingValue) {
            // Seed from the raw input only when the user typed the FIRST character (the
            // single char is the raw symbol, actualValue is empty/stale). On backspace a
            // length-1 value is the remaining masked display (e.g. '*'), so the hidden
            // characters must come from actualValue or the deletion destroys both (#1612).
            let actualResult = inputValue && inputValue.length === 1 && !backspaced
                ? inputValue.split(MaskExpression.EMPTY_STRING)
                : this.actualValue.split(MaskExpression.EMPTY_STRING);
            // Handle backspace
            if (backspaced) {
                actualResult = actualResult
                    .slice(0, position)
                    .concat(actualResult.slice(position + 1));
            }
            // Remove mask if showMaskTyped is true
            if (this.showMaskTyped) {
                // eslint-disable-next-line no-param-reassign
                inputValue = this.removeMask(inputValue);
                actualResult = this.removeMask(actualResult.join('')).split(MaskExpression.EMPTY_STRING);
            }
            // Handle selection start and end
            if (typeof this.selStart === 'object' && typeof this.selEnd === 'object') {
                this.selStart = Number(this.selStart);
                this.selEnd = Number(this.selEnd);
            }
            else {
                if (inputValue !== MaskExpression.EMPTY_STRING && actualResult.length) {
                    if (typeof this.selStart === 'number' && typeof this.selEnd === 'number') {
                        if (inputValue.length > actualResult.length) {
                            actualResult.splice(this.selStart, 0, getSymbol);
                        }
                        else if (inputValue.length < actualResult.length) {
                            if (actualResult.length - inputValue.length === 1) {
                                if (backspaced) {
                                    actualResult.splice(this.selStart - 1, 1);
                                }
                                else {
                                    actualResult.splice(inputValue.length - 1, 1);
                                }
                            }
                            else {
                                actualResult.splice(this.selStart, this.selEnd - this.selStart);
                            }
                        }
                    }
                }
                else {
                    actualResult = [];
                }
            }
            // Handle actual value length
            if (this.actualValue.length) {
                if (actualResult.length < inputValue.length) {
                    newInputValue = this.shiftTypedSymbols(actualResult.join(MaskExpression.EMPTY_STRING));
                }
                else if (actualResult.length === inputValue.length) {
                    newInputValue = actualResult.join(MaskExpression.EMPTY_STRING);
                }
                else {
                    newInputValue = inputValue;
                }
            }
            else {
                newInputValue = inputValue;
            }
        }
        // Handle just pasted input
        if (justPasted && (this.hiddenInput || !this.hiddenInput)) {
            newInputValue = inputValue;
        }
        // Handle backspace with special characters
        if (backspaced &&
            this.specialCharacters.indexOf(this.maskExpression[newPosition] ?? MaskExpression.EMPTY_STRING) !== -1 &&
            this.showMaskTyped &&
            !this.prefix) {
            newInputValue = this.currentValue;
        }
        // Handle deleted special character
        if (this.deletedSpecialCharacter && newPosition) {
            if (this.specialCharacters.includes(this.actualValue.slice(newPosition, newPosition + 1))) {
                newPosition = newPosition + 1;
            }
            else if (maskExpression.slice(newPosition - 1, newPosition + 1) !== MaskExpression.MONTHS) {
                newPosition = newPosition - 2;
            }
            this.deletedSpecialCharacter = false;
        }
        // Remove mask if showMaskTyped is true and placeHolderCharacter length is 1
        if (this.showMaskTyped &&
            this.placeHolderCharacter.length === 1 &&
            !this.leadZeroDateTime) {
            newInputValue = this.removeMask(newInputValue);
        }
        // Handle mask changed
        if (this.maskChanged) {
            newInputValue = inputValue;
        }
        else {
            newInputValue =
                Boolean(newInputValue) && newInputValue.length ? newInputValue : inputValue;
        }
        // Handle keepCharacterPositions: when the directive resolved the display for this
        // edit (kcpHandled === true), or legacily whenever showMaskTyped is on and the
        // directive gave no explicit verdict, render actualValue as-is.
        if ((kcpHandled ?? this.showMaskTyped) &&
            this.keepCharacterPositions &&
            this.actualValue &&
            !justPasted &&
            !this.writingValue) {
            const value = this.dropSpecialCharacters
                ? this.removeMask(this.actualValue)
                : this.actualValue;
            this.formControlResult(value);
            return this.actualValue
                ? this.actualValue
                : `${this.prefix}${this.maskIsShown}${this.suffix}`;
        }
        // Apply the mask using the parent class method
        const result = super.applyMask(newInputValue, maskExpression, newPosition, justPasted, backspaced, cb);
        // #1615: a value the mask cannot process AT ALL (masking leaves nothing of it) is
        // rendered verbatim instead of being destroyed, and must not reach formControlResult
        // — the empty remnant would clobber the model that holds the sentinel (e.g.
        // setValue('ONGOING') on a digits mask). Values that PARTIALLY match keep regular
        // masking. Two cases reach this:
        // 1. this.writingValue: the originating writeValue() call itself.
        // 2. currentValue === inputValue (and the mask didn't just change): a LATER re-render
        //    replaying the exact same already-verbatim value outside of writeValue (e.g.
        //    ngOnChanges re-running applyMask via the directive's _applyMask() when an
        //    UNRELATED input like `disabled` changes) — without this, that pass would fall
        //    through to regular masking, emit the empty result via onChange, and clobber the
        //    model even though the DOM would still show the sentinel correctly.
        if ((this.writingValue || (!this.maskChanged && inputValue === this.currentValue)) &&
            inputValue &&
            !result &&
            this.removeMask(inputValue)) {
            this.actualValue = inputValue;
            this.previousValue = this.currentValue;
            this.currentValue = inputValue;
            return inputValue;
        }
        this.actualValue = this.getActualValue(result);
        // handle some separator implications:
        // a.) adjust decimalMarker default (. -> ,) if thousandSeparator is a dot
        if (this.thousandSeparator === MaskExpression.DOT &&
            this.decimalMarker === MaskExpression.DOT) {
            this.decimalMarker = MaskExpression.COMMA;
        }
        // b) remove decimal marker from list of special characters to mask
        if (this.maskExpression.startsWith(MaskExpression.SEPARATOR) &&
            this.dropSpecialCharacters === true) {
            this.specialCharacters = this.specialCharacters.filter((item) => !this._compareOrIncludes(item, this.decimalMarker, this.thousandSeparator) //item !== this.decimalMarker, // !
            );
        }
        // Update previous and current values
        if (result || result === '') {
            this.previousValue = this.currentValue;
            this.currentValue = result;
            this._emitValue =
                this.previousValue !== this.currentValue ||
                    (this.previousValue === this.currentValue && justPasted);
        }
        // Propagate the input value back to the Angular model
        // Only emit if:
        // 1. _emitValue is true (value changed), AND
        // 2. Either mask didn't change, OR triggerOnMaskChange is true
        const shouldEmit = this._emitValue && (!this.maskChanged || this.triggerOnMaskChange);
        if (shouldEmit) {
            this.formControlResult(result);
        }
        // Reset maskChanged flag even if we didn't emit
        if (this.maskChanged && !this.triggerOnMaskChange) {
            this.maskChanged = false;
        }
        // Handle hidden input and showMaskTyped
        if (!this.showMaskTyped || (this.showMaskTyped && this.hiddenInput)) {
            if (this.hiddenInput) {
                return `${this.hideInput(result, this.maskExpression)}${this.maskIsShown.slice(result.length)}`;
            }
            return result;
        }
        const resLen = result.length;
        const prefNmask = `${this.prefix}${this.maskIsShown}${this.suffix}`;
        // Handle specific mask expressions
        // NOTE: IP/CPF_CNPJ/CPF_CNPJ_ALPHA are checked via exact equality BEFORE the HOURS
        // substring check below, because MaskExpression.HOURS is the single character 'H' and
        // 'CPF_CNPJ_ALPHA' contains 'H' (from "ALPHA"), which previously caused CPF_CNPJ_ALPHA
        // to be misrouted into the HOURS branch instead of its own branch.
        if (this.maskExpression === MaskExpression.IP ||
            this.maskExpression === MaskExpression.CPF_CNPJ ||
            this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA) {
            return `${result}${prefNmask}`;
        }
        else if (this.maskExpression.includes(MaskExpression.HOURS)) {
            const countSkipedSymbol = this._numberSkipedSymbols(result);
            return `${result}${prefNmask.slice(resLen + countSkipedSymbol)}`;
        }
        return `${result}${prefNmask.slice(resLen)}`;
    }
    // get the number of characters that were shifted
    _numberSkipedSymbols(value) {
        const regex = /(^|\D)(\d\D)/g;
        let match = regex.exec(value);
        let countSkipedSymbol = 0;
        while (match != null) {
            countSkipedSymbol += 1;
            match = regex.exec(value);
        }
        return countSkipedSymbol;
    }
    applyValueChanges(position, justPasted, backspaced, 
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    cb = () => { }) {
        const formElement = this._elementRef?.nativeElement;
        if (!formElement) {
            return;
        }
        formElement.value = this.applyMask(formElement.value, this.maskExpression, position, justPasted, backspaced, cb);
        if (formElement === this._getActiveElement()) {
            return;
        }
        this.clearIfNotMatchFn();
    }
    hideInput(inputValue, maskExpression) {
        return inputValue
            .split(MaskExpression.EMPTY_STRING)
            .map((curr, index) => {
            const maskChar = maskExpression[index] ?? MaskExpression.EMPTY_STRING;
            const pattern = this.patterns?.[maskChar];
            if (pattern?.symbol) {
                return pattern.symbol;
            }
            // #1574: a '0' immediately following a concealed DAY/MONTH token
            // is that field's second digit (d0/M0) — conceal it too so the
            // whole 2-digit field is hidden, not just its first character.
            // Display-only: does not touch maskExpression/processedValue, so
            // date validation/leadZero clamping in generic-pattern.handler.ts
            // is unaffected.
            const prevMaskChar = maskExpression[index - 1];
            const prevPattern = prevMaskChar ? this.patterns?.[prevMaskChar] : null;
            if (maskChar === MaskExpression.NUMBER_ZERO &&
                (prevMaskChar === MaskExpression.DAY ||
                    prevMaskChar === MaskExpression.MONTH) &&
                prevPattern?.symbol) {
                return prevPattern.symbol;
            }
            return curr;
        })
            .join(MaskExpression.EMPTY_STRING);
    }
    // this function is not necessary, it checks result against maskExpression
    getActualValue(res) {
        const compare = res
            .split(MaskExpression.EMPTY_STRING)
            .filter((symbol, i) => {
            const maskChar = this.maskExpression[i] ?? MaskExpression.EMPTY_STRING;
            return (this._checkSymbolMask(symbol, maskChar) ||
                (this.specialCharacters.includes(maskChar) && symbol === maskChar));
        });
        if (compare.join(MaskExpression.EMPTY_STRING) === res) {
            return compare.join(MaskExpression.EMPTY_STRING);
        }
        return res;
    }
    shiftTypedSymbols(inputValue) {
        let symbolToReplace = '';
        const newInputValue = (inputValue &&
            inputValue
                .split(MaskExpression.EMPTY_STRING)
                .map((currSymbol, index) => {
                if (this.specialCharacters.includes(inputValue[index + 1] ?? MaskExpression.EMPTY_STRING) &&
                    inputValue[index + 1] !== this.maskExpression[index + 1]) {
                    symbolToReplace = currSymbol;
                    return inputValue[index + 1];
                }
                if (symbolToReplace.length) {
                    const replaceSymbol = symbolToReplace;
                    symbolToReplace = MaskExpression.EMPTY_STRING;
                    return replaceSymbol;
                }
                return currSymbol;
            })) ||
            [];
        return newInputValue.join(MaskExpression.EMPTY_STRING);
    }
    /**
     * Convert number value to string
     * 3.1415 -> '3.1415'
     * 1e-7 -> '0.0000001'
     */
    numberToString(value) {
        if ((!value && value !== 0) ||
            (this.maskExpression.startsWith(MaskExpression.SEPARATOR) &&
                (this.leadZero || !this.dropSpecialCharacters)) ||
            (this.maskExpression.startsWith(MaskExpression.SEPARATOR) &&
                this.separatorLimit.length > 14 &&
                String(value).length > 14)) {
            return String(value);
        }
        // #1573: toLocaleString('fullwide', ...) is NOT locale-independent — 'fullwide' is
        // not a real locale tag, so Intl silently falls back to the runtime DEFAULT locale
        // (e.g. de-AT on Edge with Austrian regional format emits '0,5'). Expand exponential
        // notation with pure string math instead; the decimal marker is always '.'.
        return this._toPlainDecimalString(Number(value));
    }
    /**
     * Locale-independent replacement for toLocaleString('fullwide', { useGrouping: false,
     * maximumFractionDigits: 20 }) (#1573): expands exponential notation ('7e-7', '1e+21')
     * to plain decimal form using '.' as decimal marker, regardless of the runtime locale.
     */
    _toPlainDecimalString(value) {
        const stringValue = String(value);
        const match = /^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(stringValue);
        if (!match) {
            return stringValue;
        }
        const [, sign, integerPart, fractionPart = '', exponentPart] = match;
        const exponent = Number(exponentPart);
        const digits = `${integerPart}${fractionPart}`;
        const pointIndex = integerPart.length + exponent;
        if (pointIndex <= 0) {
            return `${sign}0.${'0'.repeat(-pointIndex)}${digits}`;
        }
        if (pointIndex >= digits.length) {
            return `${sign}${digits}${'0'.repeat(pointIndex - digits.length)}`;
        }
        return `${sign}${digits.slice(0, pointIndex)}.${digits.slice(pointIndex)}`;
    }
    showMaskInInput(inputVal) {
        if (this.showMaskTyped && !!this.shownMaskExpression) {
            if (this.maskExpression.length !== this.shownMaskExpression.length) {
                throw new Error('Mask expression must match mask placeholder length');
            }
            else {
                return this.shownMaskExpression;
            }
        }
        else if (this.showMaskTyped) {
            if (inputVal) {
                if (this.maskExpression === MaskExpression.IP) {
                    return this._checkForIp(inputVal);
                }
                if (this.maskExpression === MaskExpression.CPF_CNPJ ||
                    this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA) {
                    return this._checkForCpfCnpj(inputVal);
                }
            }
            if (this.placeHolderCharacter.length === this.maskExpression.length) {
                return this.placeHolderCharacter;
            }
            return this.maskExpression.replace(/\w/g, this.placeHolderCharacter);
        }
        return '';
    }
    clearIfNotMatchFn() {
        const formElement = this._elementRef?.nativeElement;
        if (!formElement) {
            return;
        }
        if (this.clearIfNotMatch &&
            this.prefix.length + this.maskExpression.length + this.suffix.length !==
                formElement.value.replace(this.placeHolderCharacter, MaskExpression.EMPTY_STRING)
                    .length) {
            this.formElementProperty = ['value', MaskExpression.EMPTY_STRING];
            this.applyMask('', this.maskExpression);
        }
    }
    set formElementProperty([name, value]) {
        if (!this._renderer || !this._elementRef) {
            return;
        }
        // Use queueMicrotask to defer DOM updates to next microtask.
        // This prevents ExpressionChangedAfterItHasBeenCheckedError
        // and ensures proper timing with Angular's change detection.
        queueMicrotask(() => {
            this._renderer?.setProperty(this._elementRef?.nativeElement, name, value);
        });
    }
    checkDropSpecialCharAmount(mask) {
        const chars = mask
            .split(MaskExpression.EMPTY_STRING)
            .filter((item) => this._findDropSpecialChar(item));
        return chars.length;
    }
    removeMask(inputValue) {
        return this._removeMask(this._removeSuffix(this._removePrefix(inputValue)), this.specialCharacters.concat('_').concat(this.placeHolderCharacter));
    }
    _checkForIp(inputVal) {
        if (inputVal === MaskExpression.HASH) {
            return `${this.placeHolderCharacter}.${this.placeHolderCharacter}.${this.placeHolderCharacter}.${this.placeHolderCharacter}`;
        }
        const arr = [];
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let i = 0; i < inputVal.length; i++) {
            const value = inputVal[i] ?? MaskExpression.EMPTY_STRING;
            if (!value) {
                continue;
            }
            if (value.match('\\d')) {
                arr.push(value);
            }
        }
        if (arr.length <= 3) {
            return `${this.placeHolderCharacter}.${this.placeHolderCharacter}.${this.placeHolderCharacter}`;
        }
        if (arr.length > 3 && arr.length <= 6) {
            return `${this.placeHolderCharacter}.${this.placeHolderCharacter}`;
        }
        if (arr.length > 6 && arr.length <= 9) {
            return this.placeHolderCharacter;
        }
        if (arr.length > 9 && arr.length <= 12) {
            return '';
        }
        return '';
    }
    _checkForCpfCnpj(inputVal) {
        const cpf = `${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
            `.${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
            `.${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
            `-${this.placeHolderCharacter}${this.placeHolderCharacter}`;
        const cnpj = `${this.placeHolderCharacter}${this.placeHolderCharacter}` +
            `.${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
            `.${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
            `/${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}${this.placeHolderCharacter}` +
            `-${this.placeHolderCharacter}${this.placeHolderCharacter}`;
        if (inputVal === MaskExpression.HASH) {
            return cpf;
        }
        const isCpfCnpjAlpha = this.maskExpression === MaskExpression.CPF_CNPJ_ALPHA;
        const hasAnyLetter = /[a-zA-Z]/.test(inputVal);
        const arr = this._countCpfCnpjTypedChars(inputVal, isCpfCnpjAlpha);
        if (isCpfCnpjAlpha && hasAnyLetter) {
            // CNPJ_ALPHA shape is "AA.AAA.AAA/AAAA-00": separators fall after the 2nd, 5th,
            // 8th and 12th typed character, so the placeholder slice offset must account for
            // however many separators `result` has already passed through, mirroring the
            // numeric CPF/CNPJ bucketed offsets below.
            if (arr.length <= 2) {
                return cnpj.slice(arr.length, cnpj.length);
            }
            if (arr.length > 2 && arr.length <= 5) {
                return cnpj.slice(arr.length + 1, cnpj.length);
            }
            if (arr.length > 5 && arr.length <= 8) {
                return cnpj.slice(arr.length + 2, cnpj.length);
            }
            if (arr.length > 8 && arr.length <= 12) {
                return cnpj.slice(arr.length + 3, cnpj.length);
            }
            return cnpj.slice(arr.length + 4, cnpj.length);
        }
        else {
            if (arr.length <= 3) {
                return cpf.slice(arr.length, cpf.length);
            }
            if (arr.length > 3 && arr.length <= 6) {
                return cpf.slice(arr.length + 1, cpf.length);
            }
            if (arr.length > 6 && arr.length <= 9) {
                return cpf.slice(arr.length + 2, cpf.length);
            }
            if (arr.length > 9 && arr.length < 11) {
                return cpf.slice(arr.length + 3, cpf.length);
            }
        }
        if (arr.length === 11) {
            return '';
        }
        if (arr.length === 12) {
            if (inputVal.length === 17) {
                return cnpj.slice(16, cnpj.length);
            }
            return cnpj.slice(15, cnpj.length);
        }
        if (arr.length > 12 && arr.length <= 14) {
            return cnpj.slice(arr.length + 4, cnpj.length);
        }
        return '';
    }
    /** Collects the characters counted as "typed" for CPF/CNPJ progress tracking: digits only
     *  for the numeric mask, alphanumerics for CPF_CNPJ_ALPHA. */
    _countCpfCnpjTypedChars(inputVal, isCpfCnpjAlpha) {
        const arr = [];
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let i = 0; i < inputVal.length; i++) {
            const value = inputVal[i] ?? MaskExpression.EMPTY_STRING;
            if (!value) {
                continue;
            }
            if (isCpfCnpjAlpha ? value.match('[a-zA-Z0-9]') : value.match('\\d')) {
                arr.push(value);
            }
        }
        return arr;
    }
    /**
     * Recursively determine the current active element by navigating the Shadow DOM until the Active Element is found.
     */
    _getActiveElement(document = this.document) {
        const shadowRootEl = document?.activeElement?.shadowRoot;
        if (!shadowRootEl?.activeElement) {
            return document.activeElement;
        }
        else {
            return this._getActiveElement(shadowRootEl);
        }
    }
    /**
     * Propogates the input value back to the Angular model by triggering the onChange function. It won't do this if writingValue
     * is true. If that is true it means we are currently in the writeValue function, which is supposed to only update the actual
     * DOM element based on the Angular model value. It should be a one way process, i.e. writeValue should not be modifying the Angular
     * model value too. Therefore, we don't trigger onChange in this scenario.
     * @param inputValue the current form input value
     */
    formControlResult(inputValue) {
        const outputTransformFn = this.outputTransformFn
            ? this.outputTransformFn
            : (v) => v;
        this.writingValue = false;
        this.maskChanged = false;
        if (!this.isInitialized && this._emitValue) {
            return;
        }
        // #1519: a non-special placeHolderCharacter (e.g. 'X') is never something the user
        // typed — it's the showMaskTyped fill character for unfilled slots — so it must never
        // reach the model, regardless of dropSpecialCharacters. The default '_' placeholder is
        // already covered by specialCharacters/removeMask; a custom single-char placeholder
        // that isn't a special character is not, since none of the branches below include it
        // in their removal set.
        if (this.showMaskTyped &&
            this.placeHolderCharacter.length === 1 &&
            this.specialCharacters.indexOf(this.placeHolderCharacter) === -1) {
            // eslint-disable-next-line no-param-reassign
            inputValue = inputValue
                .split(this.placeHolderCharacter)
                .join(MaskExpression.EMPTY_STRING);
        }
        if (Array.isArray(this.dropSpecialCharacters)) {
            this.onChange(outputTransformFn(this._toNumber(this._checkSymbols(this._removeMask(this._removeSuffix(this._removePrefix(inputValue)), this.dropSpecialCharacters)))));
        }
        else if (this.dropSpecialCharacters ||
            (!this.dropSpecialCharacters && this.prefix === inputValue)) {
            this.onChange(outputTransformFn(this._toNumber(this._checkSymbols(this._removeSuffix(this._removePrefix(inputValue))))));
        }
        else {
            this.onChange(outputTransformFn(this._toNumber(inputValue)));
        }
    }
    _toNumber(value) {
        if (!this.isNumberValue || value === MaskExpression.EMPTY_STRING) {
            return value;
        }
        if (this.maskExpression.startsWith(MaskExpression.SEPARATOR) &&
            (this.leadZero || !this.dropSpecialCharacters)) {
            return value;
        }
        if (String(value).length > 14 && this.maskExpression.startsWith(MaskExpression.SEPARATOR)) {
            return String(value);
        }
        const num = Number(value);
        if (this.maskExpression.startsWith(MaskExpression.SEPARATOR) && Number.isNaN(num)) {
            const val = String(value).replace(',', '.');
            return Number(val);
        }
        return Number.isNaN(num) ? value : num;
    }
    _removeMask(value, specialCharactersForRemove) {
        if (this.maskExpression.startsWith(MaskExpression.PERCENT) &&
            (value.includes(MaskExpression.DOT) ||
                (Array.isArray(this.decimalMarker) &&
                    value.includes(MaskExpression.COMMA) &&
                    this._activeDecimalMarker(value) === MaskExpression.COMMA))) {
            return value;
        }
        return value
            ? value.replace(this._regExpForRemove(specialCharactersForRemove), MaskExpression.EMPTY_STRING)
            : value;
    }
    _removePrefix(value) {
        if (!this.prefix) {
            return value;
        }
        return value ? value.replace(this.prefix, MaskExpression.EMPTY_STRING) : value;
    }
    _removeSuffix(value) {
        if (!this.suffix) {
            return value;
        }
        return value ? value.replace(this.suffix, MaskExpression.EMPTY_STRING) : value;
    }
    _retrieveSeparatorValue(result) {
        let specialCharacters = Array.isArray(this.dropSpecialCharacters)
            ? this.specialCharacters.filter((v) => {
                return this.dropSpecialCharacters.includes(v);
            })
            : this.specialCharacters;
        if (!this.deletedSpecialCharacter &&
            this._checkPatternForSpace() &&
            result.includes(MaskExpression.WHITE_SPACE) &&
            this.maskExpression.includes(MaskExpression.SYMBOL_STAR)) {
            specialCharacters = specialCharacters.filter((char) => char !== MaskExpression.WHITE_SPACE);
        }
        return this._removeMask(result, specialCharacters);
    }
    _regExpForRemove(specialCharactersForRemove) {
        return new RegExp(specialCharactersForRemove
            .map((item) => {
            // Only escape characters that have special meaning in regex
            if (/[.*+?^${}()|[\]\\/-]/.test(item)) {
                return `\\${item}`;
            }
            return item;
        })
            .join('|'), 'gi');
    }
    _replaceDecimalMarkerToDot(value) {
        const markers = Array.isArray(this.decimalMarker)
            ? this.decimalMarker
            : [this.decimalMarker];
        return value.replace(this._regExpForRemove(markers), MaskExpression.DOT);
    }
    _checkSymbols(result) {
        let processedResult = result;
        if (processedResult === MaskExpression.EMPTY_STRING) {
            return processedResult;
        }
        if (this.maskExpression.startsWith(MaskExpression.PERCENT) &&
            this._activeDecimalMarker(processedResult) === MaskExpression.COMMA) {
            processedResult = processedResult.replace(MaskExpression.COMMA, MaskExpression.DOT);
        }
        const separatorPrecision = this._retrieveSeparatorPrecision(this.maskExpression);
        const separatorValue = this.specialCharacters.length === 0
            ? this._retrieveSeparatorValue(processedResult)
            : this._replaceDecimalMarkerToDot(this._retrieveSeparatorValue(processedResult));
        if (!this.isNumberValue) {
            return separatorValue;
        }
        if (separatorPrecision) {
            if (processedResult === this.decimalMarker) {
                return null;
            }
            if (separatorValue.length > 14) {
                return String(separatorValue);
            }
            return this._checkPrecision(this.maskExpression, separatorValue);
        }
        else {
            return separatorValue;
        }
    }
    _checkPatternForSpace() {
        for (const key in this.patterns) {
            // eslint-disable-next-line no-prototype-builtins
            if (this.patterns[key] && this.patterns[key]?.hasOwnProperty('pattern')) {
                const patternString = this.patterns[key]?.pattern.toString();
                const pattern = this.patterns[key]?.pattern;
                if (patternString?.includes(MaskExpression.WHITE_SPACE) &&
                    pattern?.test(this.maskExpression)) {
                    return true;
                }
            }
        }
        return false;
    }
    _retrieveSeparatorPrecision(maskExpression) {
        const matcher = maskExpression.match(new RegExp(`^separator\\.([^d]*)`));
        return matcher ? Number(matcher[1]) : null;
    }
    _checkPrecision(separatorExpression, separatorValue) {
        const separatorPrecision = this.getPrecision(separatorExpression);
        let value = separatorValue;
        if ((separatorExpression.indexOf('2') > 0 && !this._isFocused()) ||
            (this.leadZero &&
                !this._isFocused() &&
                Number(separatorPrecision) > 0 &&
                Number.isFinite(separatorPrecision))) {
            if (this.decimalMarker === MaskExpression.COMMA && this.leadZero) {
                value = value.replace(',', '.');
            }
            const precision = this.leadZero ? Number(separatorPrecision) : 2;
            // #1567: Number(value).toFixed() corrupts values with more significant digits
            // than an IEEE-754 double can hold (e.g. '999999999999999.99' becomes
            // '1000000000000000.00'). Round such values textually instead; safe-range
            // values keep the original toFixed() semantics.
            if (this._exceedsDoublePrecision(value)) {
                return this._stringToFixed(value, precision);
            }
            return Number(value).toFixed(precision);
        }
        return this.numberToString(value);
    }
    /**
     * True when the plain decimal string carries more significant digits than an IEEE-754
     * double can represent exactly (15 is the guaranteed round-trip digit count), meaning a
     * Number() round-trip would corrupt it (#1567).
     */
    _exceedsDoublePrecision(value) {
        if (!/^-?\d+(\.\d+)?$/.test(value)) {
            return false;
        }
        const significantDigits = value.replace(/\D/g, '').replace(/^0+/, '');
        return significantDigits.length > 15;
    }
    /**
     * Exact string-based equivalent of Number.prototype.toFixed (round half away from zero)
     * for plain decimal strings beyond double precision (#1567).
     */
    _stringToFixed(value, precision) {
        const isNegative = value.startsWith(MaskExpression.MINUS);
        const absValue = isNegative ? value.slice(1) : value;
        const [integerPart = '0', fractionPart = ''] = absValue.split(MaskExpression.DOT);
        const paddedFraction = fractionPart.padEnd(precision + 1, '0');
        const keptFraction = paddedFraction.slice(0, precision);
        const shouldRoundUp = (paddedFraction.charCodeAt(precision) || 0) >= 53; // '5'
        let scaled = BigInt(integerPart + keptFraction);
        if (shouldRoundUp) {
            scaled += 1n;
        }
        const digits = scaled.toString().padStart(precision + 1, '0');
        const sign = isNegative && scaled > 0n ? MaskExpression.MINUS : '';
        return precision > 0
            ? `${sign}${digits.slice(0, -precision)}.${digits.slice(-precision)}`
            : `${sign}${digits}`;
    }
    _repeatPatternSymbols(maskExp) {
        return ((maskExp.match(/{[0-9]+}/) &&
            maskExp
                .split(MaskExpression.EMPTY_STRING)
                .reduce((accum, currVal, index) => {
                this._start =
                    currVal === MaskExpression.CURLY_BRACKETS_LEFT ? index : this._start;
                if (currVal !== MaskExpression.CURLY_BRACKETS_RIGHT) {
                    return this._findSpecialChar(currVal) ? accum + currVal : accum;
                }
                this._end = index;
                const repeatNumber = Number(maskExp.slice(this._start + 1, this._end));
                const replaceWith = new Array(repeatNumber + 1).join(maskExp[this._start - 1]);
                if (maskExp.slice(0, this._start).length > 1 &&
                    maskExp.includes(MaskExpression.LETTER_S)) {
                    const symbols = maskExp.slice(0, this._start - 1);
                    return symbols.includes(MaskExpression.CURLY_BRACKETS_LEFT)
                        ? accum + replaceWith
                        : symbols + accum + replaceWith;
                }
                else {
                    return accum + replaceWith;
                }
            }, '')) ||
            maskExp);
    }
    /**
     * Decimal marker of the value being normalized in writeValue/pipe flows.
     *
     * #1573: this used to return the RUNTIME default locale's decimal marker
     * ((1.1).toLocaleString().substring(1, 2)), which made mask behavior depend on the
     * OS/browser regional format: under a comma-decimal locale (e.g. Edge + Austrian
     * regional settings) a preformatted value like '10,000' (thousandSeparator ',')
     * had its ',' replaced by the configured '.' decimalMarker, corrupting the value
     * 1000x. JS number stringification (String(n)) always uses '.', and string values
     * are expected to use the configured markers — the runtime locale is never the
     * right source, so this is always '.'.
     */
    currentLocaleDecimalMarker() {
        return MaskExpression.DOT;
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
    static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskService, decorators: [{
            type: Injectable
        }] });

/**
 * @internal
 */
function _configFactory() {
    const initConfig = inject(INITIAL_CONFIG);
    const configValue = inject(NEW_CONFIG);
    return configValue instanceof Function
        ? { ...initConfig, ...configValue() }
        : { ...initConfig, ...configValue };
}
function provideNgxMask(configValue) {
    return [
        {
            provide: NEW_CONFIG,
            useValue: configValue,
        },
        {
            provide: INITIAL_CONFIG,
            useValue: initialConfig,
        },
        {
            provide: NGX_MASK_CONFIG,
            useFactory: _configFactory,
        },
        NgxMaskService,
    ];
}
function provideEnvironmentNgxMask(configValue) {
    return makeEnvironmentProviders(provideNgxMask(configValue));
}

class NgxMaskDirective {
    // ===== Mask Configuration Inputs =====
    mask = input('', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "mask" }] : /* istanbul ignore next */ []));
    specialCharacters = input([], /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "specialCharacters" }] : /* istanbul ignore next */ []));
    patterns = input({}, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "patterns" }] : /* istanbul ignore next */ []));
    prefix = input('', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "prefix" }] : /* istanbul ignore next */ []));
    suffix = input('', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "suffix" }] : /* istanbul ignore next */ []));
    thousandSeparator = input(' ', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "thousandSeparator" }] : /* istanbul ignore next */ []));
    decimalMarker = input('.', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "decimalMarker" }] : /* istanbul ignore next */ []));
    dropSpecialCharacters = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "dropSpecialCharacters" }] : /* istanbul ignore next */ []));
    hiddenInput = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "hiddenInput" }] : /* istanbul ignore next */ []));
    showMaskTyped = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "showMaskTyped" }] : /* istanbul ignore next */ []));
    placeHolderCharacter = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "placeHolderCharacter" }] : /* istanbul ignore next */ []));
    shownMaskExpression = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "shownMaskExpression" }] : /* istanbul ignore next */ []));
    clearIfNotMatch = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "clearIfNotMatch" }] : /* istanbul ignore next */ []));
    validation = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
    separatorLimit = input('', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "separatorLimit" }] : /* istanbul ignore next */ []));
    typeFromDecimals = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "typeFromDecimals" }] : /* istanbul ignore next */ []));
    allowNegativeNumbers = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "allowNegativeNumbers" }] : /* istanbul ignore next */ []));
    leadZeroDateTime = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "leadZeroDateTime" }] : /* istanbul ignore next */ []));
    leadZero = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "leadZero" }] : /* istanbul ignore next */ []));
    triggerOnMaskChange = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "triggerOnMaskChange" }] : /* istanbul ignore next */ []));
    apm = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "apm" }] : /* istanbul ignore next */ []));
    inputTransformFn = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "inputTransformFn" }] : /* istanbul ignore next */ []));
    outputTransformFn = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "outputTransformFn" }] : /* istanbul ignore next */ []));
    keepCharacterPositions = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "keepCharacterPositions" }] : /* istanbul ignore next */ []));
    instantPrefix = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "instantPrefix" }] : /* istanbul ignore next */ []));
    // Read directly at blur time (signal is always current) and falls back to the DI
    // config, so no ngOnChanges mirroring into the service is needed (#1435).
    defaultValueOnBlur = input(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "defaultValueOnBlur" }] : /* istanbul ignore next */ []));
    /**
     * Signal Forms `FormValueControl` surface. At runtime `FormField` never drives this model
     * when the directive is bound (it prefers the `NG_VALUE_ACCESSOR` provided above, see
     * `_isCvaMode`), but the template type-checker treats any directive owning a `value` model
     * as the field's custom control and checks the bound field's value type against it (#1640).
     * The model therefore accepts exactly what `writeValue()` accepts — `string | number | null |
     * undefined` — so numeric and nullable fields (`signal({ amount: 0 })`,
     * `signal<number | null>(null)`, `signal<string | null>(null)`) compile under
     * `strictTemplates`. Values pushed back into the model are always the unmasked string (see
     * `_propagateToValueModel`), i.e. `value()` / `valueChange` are declared wider than what is
     * emitted.
     */
    value = model('', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
    disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
    touched = model(false, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
    maskFilled = output();
    _maskValue = signal('', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_maskValue" }] : /* istanbul ignore next */ []));
    _inputValue = signal('', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_inputValue" }] : /* istanbul ignore next */ []));
    _position = signal(null, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_position" }] : /* istanbul ignore next */ []));
    _code = signal('', /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_code" }] : /* istanbul ignore next */ []));
    _maskExpressionArray = signal([], /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_maskExpressionArray" }] : /* istanbul ignore next */ []));
    _justPasted = signal(false, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_justPasted" }] : /* istanbul ignore next */ []));
    _isFocused = signal(false, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_isFocused" }] : /* istanbul ignore next */ []));
    /** For IME composition event */
    _isComposing = signal(false, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_isComposing" }] : /* istanbul ignore next */ []));
    /**
     * True once Angular has driven this directive through the `ControlValueAccessor` contract
     * (`registerOnChange`). NOTE: Signal Forms' `FormField` ALSO takes this path — it prefers a
     * host-provided `NG_VALUE_ACCESSOR` over the custom-control `value` model binding (see
     * `FormField.ɵngControlCreate`), so it calls `registerOnChange`/`writeValue` too and this
     * flag is `true` in both modes. That is fine: with the flag set, the `value`-model effect
     * below is a no-op and all rendering goes through `writeValue()`. The `value` model is only
     * driven directly (flag stays `false`) when the directive is used standalone with a
     * `[(value)]` binding and no forms integration.
     */
    _isCvaMode = signal(false, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_isCvaMode" }] : /* istanbul ignore next */ []));
    /** Guards against the value effect echoing back a value we just propagated ourselves. */
    _skipNextValueEffect = signal(false, /* @ts-ignore */
    ...(ngDevMode ? [{ debugName: "_skipNextValueEffect" }] : /* istanbul ignore next */ []));
    /**
     * The exact stringified value last pushed through `onChange` (view → model). Signal Forms'
     * `FormField` echoes every model update back through `writeValue()` — including updates that
     * originated from the view. Re-masking that unmasked echo is at best a redundant re-render
     * and at worst destructive for masks whose unmasked form is ambiguous (e.g. IP:
     * '192168178' cannot reconstruct the typed dots of '192.168.1.78'). `writeValue()` consumes
     * this marker to skip exactly that echo. `null` = no pending propagation.
     */
    _lastPropagatedValue = null;
    /**
     * True once the first `ngOnChanges` pass has applied the mask configuration to the service.
     * Signal Forms' `FormField` syncs its field value through the template `ɵɵcontrol` update
     * instruction, which runs BEFORE the sibling directives' first `ngOnChanges` on the same
     * element — so the very first `writeValue()` would otherwise see an unconfigured service
     * (empty `maskExpression`, default `leadZero`/`thousandSeparator`/...) and render the raw
     * value. Until this flag is set, `writeValue()` stashes the incoming value and `ngOnChanges`
     * replays it once the configuration is in place.
     */
    _configApplied = false;
    _pendingInitialValue;
    _hasPendingInitialValue = false;
    /**
     * True once the `disabled` input has ever delivered `true`. The disabled effect's very
     * first run fires with the input's default `false` even when nothing binds `[disabled]`.
     * Because effects run after Angular Forms' `setUpControl` (which calls
     * `setDisabledState(true)` for initially-disabled controls) and both DOM writes are
     * queueMicrotask-deferred in FIFO order, forwarding that default `false` would land last
     * and re-enable an initially-disabled control (#1607, #1614). An initial `false` write is
     * never needed — inputs are enabled by default — so `false` is only forwarded after the
     * input has explicitly driven the state to `true` at least once.
     */
    _disabledEverSet = false;
    /** Ensures the multi-character placeHolderCharacter warning (#1347) is emitted only once. */
    _warnedAboutMultiCharPlaceholder = false;
    _maskService = inject(NgxMaskService, { self: true });
    document = inject(DOCUMENT);
    _config = inject(NGX_MASK_CONFIG);
    /**
     * Under zoneless change detection, writing to the underlying FormControl programmatically
     * (e.g. `setValue`/`patchValue` from outside a signal/effect context, or from a callback
     * zone.js used to auto-flush like `requestAnimationFrame`/`queueMicrotask`) does not itself
     * trigger a CD run. Bindings that read `form.pristine`/`form.dirty`/`form.value` on the host
     * template need an explicit `markForCheck()` once the directive finishes reacting to a
     * programmatic value write, otherwise the view stays stale until something else schedules CD.
     */
    _changeDetectorRef = inject(ChangeDetectorRef);
    _elementRef = inject(ElementRef);
    _renderer = inject(Renderer2);
    // Injector is used to lazily resolve NgControl. NgControl cannot be injected directly:
    // this directive is registered as the control's NG_VALUE_ACCESSOR, so a direct
    // inject(NgControl) would form a circular dependency. Resolving it lazily on first use
    // (after the control graph is wired) sidesteps the cycle.
    _injector = inject(Injector);
    _ngControl;
    _resolveNgControl() {
        if (typeof this._ngControl === 'undefined') {
            this._ngControl = this._injector.get(NgControl, null);
        }
        return this._ngControl;
    }
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    onChange = (_) => { };
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    onTouch = () => { };
    constructor() {
        // Default onChange used in Signal Forms mode, where Angular never calls registerOnChange.
        // It pushes the unmasked value into the `value` model so the `valueChange` output fires.
        // registerOnChange() overrides this to additionally invoke Angular's CVA callback.
        this._maskService.onChange = this.onChange = (value) => {
            this._propagateToValueModel(value);
        };
        // Signal Forms drives the `value` model input directly (custom-control mode).
        // In classic CVA mode `writeValue` handles rendering instead, so this effect is a no-op
        // there. We skip the pass immediately following our own onChange propagation to avoid
        // clobbering the raw `_inputValue` with the masked value echoed back from the model.
        effect(() => {
            const signalValue = this.value();
            untracked(() => {
                if (this._isCvaMode()) {
                    return;
                }
                if (this._skipNextValueEffect()) {
                    this._skipNextValueEffect.set(false);
                    return;
                }
                if (String(signalValue) !== String(this._inputValue())) {
                    this.writeValue(signalValue);
                }
            });
        });
        effect(() => {
            const isDisabled = this.disabled();
            untracked(() => {
                // Never force-enable on the default `false`: it would clobber Angular Forms'
                // own setDisabledState(true) for initially-disabled controls (see
                // _disabledEverSet). `false` is forwarded only after an explicit `true`.
                if (!isDisabled && !this._disabledEverSet) {
                    return;
                }
                this._disabledEverSet = true;
                this.setDisabledState(isDisabled);
            });
        });
    }
    ngOnChanges(changes) {
        const { mask, specialCharacters, patterns, prefix, suffix, thousandSeparator, decimalMarker, dropSpecialCharacters, hiddenInput, showMaskTyped, placeHolderCharacter, shownMaskExpression, clearIfNotMatch, validation, separatorLimit, typeFromDecimals, allowNegativeNumbers, leadZeroDateTime, leadZero, triggerOnMaskChange, apm, inputTransformFn, outputTransformFn, keepCharacterPositions, instantPrefix, } = changes;
        if (mask) {
            if (mask.currentValue !== mask.previousValue && !mask.firstChange) {
                this._maskService.maskChanged = true;
            }
            // User-defined aliases (config maskAliases) expand BEFORE any other processing,
            // including the `||` multi-mask split — an alias may expand to a multi-mask.
            const resolvedMask = this._resolvedMaskInput();
            const maskParts = resolvedMask.split(MaskExpression.OR);
            if (maskParts.length > 1) {
                this._maskExpressionArray.set(maskParts.sort((a, b) => a.length - b.length));
                this._setMask();
            }
            else {
                this._maskExpressionArray.set([]);
                this._maskValue.set(resolvedMask);
                this._maskService.maskExpression = this._maskValue();
            }
        }
        if (specialCharacters) {
            if (Array.isArray(specialCharacters.currentValue)) {
                this._maskService.specialCharacters = specialCharacters.currentValue;
            }
            // A non-array value (e.g. null from an unset dynamic config) keeps the current
            // service value (config defaults) and must not abort the whole ngOnChanges pass,
            // otherwise sibling inputs changed in the same cycle would be silently ignored (#1512).
        }
        if (allowNegativeNumbers) {
            this._maskService.allowNegativeNumbers = allowNegativeNumbers.currentValue;
            if (this._maskService.allowNegativeNumbers) {
                this._maskService.specialCharacters = this._maskService.specialCharacters.filter((c) => c !== MaskExpression.MINUS);
            }
        }
        // Only overwrite the mask available patterns if a pattern has actually been passed in
        if (patterns && patterns.currentValue) {
            this._maskService.patterns = patterns.currentValue;
        }
        if (apm && apm.currentValue) {
            this._maskService.apm = apm.currentValue;
        }
        if (instantPrefix) {
            this._maskService.instantPrefix = instantPrefix.currentValue;
        }
        if (prefix) {
            this._maskService.prefix = prefix.currentValue;
        }
        if (suffix) {
            this._maskService.suffix = suffix.currentValue;
        }
        if (thousandSeparator) {
            this._maskService.thousandSeparator = thousandSeparator.currentValue;
            if (thousandSeparator.previousValue && thousandSeparator.currentValue) {
                const previousDecimalMarker = this._maskService.decimalMarker;
                if (thousandSeparator.currentValue === this._maskService.decimalMarker) {
                    this._maskService.decimalMarker =
                        thousandSeparator.currentValue === MaskExpression.COMMA
                            ? MaskExpression.DOT
                            : MaskExpression.COMMA;
                }
                if (this._maskService.dropSpecialCharacters === true) {
                    this._maskService.specialCharacters = this._config.specialCharacters;
                }
                if (typeof previousDecimalMarker === 'string' &&
                    typeof this._maskService.decimalMarker === 'string') {
                    this._inputValue.set(this._inputValue()
                        .split(thousandSeparator.previousValue)
                        .join('')
                        .replace(previousDecimalMarker, this._maskService.decimalMarker));
                    this._maskService.actualValue = this._inputValue();
                }
                this._maskService.writingValue = true;
            }
        }
        if (decimalMarker) {
            this._maskService.decimalMarker = decimalMarker.currentValue;
        }
        if (dropSpecialCharacters) {
            this._maskService.dropSpecialCharacters = dropSpecialCharacters.currentValue;
        }
        if (hiddenInput) {
            this._maskService.hiddenInput = hiddenInput.currentValue;
            if (hiddenInput.previousValue === true && hiddenInput.currentValue === false) {
                this._inputValue.set(this._maskService.actualValue);
            }
        }
        if (showMaskTyped) {
            this._maskService.showMaskTyped = showMaskTyped.currentValue;
            if (showMaskTyped.previousValue === false &&
                showMaskTyped.currentValue === true &&
                this._isFocused()) {
                requestAnimationFrame(() => {
                    this._maskService._elementRef?.nativeElement.click();
                });
            }
        }
        if (placeHolderCharacter) {
            // `null`/`undefined` means "no custom placeholder" (#1643): fall back to the default
            // instead of storing null, which broke every `.length` read on the service.
            this._maskService.placeHolderCharacter =
                placeHolderCharacter.currentValue ?? this._config.placeHolderCharacter;
            if (typeof placeHolderCharacter.currentValue === 'string' &&
                placeHolderCharacter.currentValue.length > 1 &&
                !this._warnedAboutMultiCharPlaceholder) {
                this._warnedAboutMultiCharPlaceholder = true;
                // eslint-disable-next-line no-console
                console.warn('Ngx-mask: placeHolderCharacter should be a single character; behavior with multi-character values is undefined (e.g. keepCharacterPositions will not work). Current value:', placeHolderCharacter.currentValue);
            }
        }
        if (shownMaskExpression) {
            this._maskService.shownMaskExpression = shownMaskExpression.currentValue;
        }
        if (clearIfNotMatch) {
            this._maskService.clearIfNotMatch = clearIfNotMatch.currentValue;
        }
        if (validation) {
            this._maskService.validation = validation.currentValue;
        }
        if (separatorLimit) {
            this._maskService.separatorLimit = separatorLimit.currentValue;
        }
        if (typeFromDecimals) {
            this._maskService.typeFromDecimals = typeFromDecimals.currentValue;
        }
        if (leadZeroDateTime) {
            this._maskService.leadZeroDateTime = leadZeroDateTime.currentValue;
        }
        if (leadZero) {
            this._maskService.leadZero = leadZero.currentValue;
        }
        if (triggerOnMaskChange) {
            this._maskService.triggerOnMaskChange = triggerOnMaskChange.currentValue;
        }
        if (inputTransformFn) {
            this._maskService.inputTransformFn = inputTransformFn.currentValue;
        }
        if (outputTransformFn) {
            this._maskService.outputTransformFn = outputTransformFn.currentValue;
        }
        if (keepCharacterPositions) {
            this._maskService.keepCharacterPositions = keepCharacterPositions.currentValue;
        }
        this._applyMask();
        if (!this._configApplied) {
            this._configApplied = true;
            if (this._hasPendingInitialValue) {
                // Replay the writeValue() call that arrived before this first ngOnChanges pass
                // (see _configApplied) now that the mask configuration is applied.
                this._hasPendingInitialValue = false;
                const pendingValue = this._pendingInitialValue;
                this._pendingInitialValue = null;
                this.writeValue(pendingValue);
            }
        }
    }
    validate({ value }) {
        const processedValue = typeof value === 'number' ? String(value) : value;
        const maskValue = this._maskValue();
        if (!this._maskService.validation || !maskValue) {
            return null;
        }
        if (this._maskService.ipError) {
            return this._createValidationError(processedValue);
        }
        if (this._maskService.cpfCnpjError) {
            return this._createValidationError(processedValue);
        }
        if (maskValue.startsWith(MaskExpression.SEPARATOR)) {
            return null;
        }
        if (withoutValidation.includes(maskValue)) {
            return null;
        }
        if (this._maskService.clearIfNotMatch) {
            return null;
        }
        if (timeMasks.includes(maskValue)) {
            return this._validateTime(processedValue);
        }
        if (maskValue === MaskExpression.EMAIL_MASK) {
            const emailPattern = /^[^@]+@[^@]+\.[^@]+$/;
            if (!emailPattern.test(processedValue) && processedValue) {
                return this._createValidationError(processedValue);
            }
            else {
                return null;
            }
        }
        if (processedValue && processedValue.length >= 1) {
            let counterOfOpt = 0;
            if (maskValue.includes(MaskExpression.CURLY_BRACKETS_LEFT) &&
                maskValue.includes(MaskExpression.CURLY_BRACKETS_RIGHT)) {
                const lengthInsideCurlyBrackets = maskValue.slice(maskValue.indexOf(MaskExpression.CURLY_BRACKETS_LEFT) + 1, maskValue.indexOf(MaskExpression.CURLY_BRACKETS_RIGHT));
                return lengthInsideCurlyBrackets === String(processedValue.length)
                    ? null
                    : this._createValidationError(processedValue);
            }
            if (maskValue.startsWith(MaskExpression.PERCENT)) {
                return null;
            }
            for (const key in this._maskService.patterns) {
                if (this._maskService.patterns[key]?.optional) {
                    if (maskValue.indexOf(key) !== maskValue.lastIndexOf(key)) {
                        const opt = maskValue
                            .split(MaskExpression.EMPTY_STRING)
                            .filter((i) => i === key)
                            .join(MaskExpression.EMPTY_STRING);
                        counterOfOpt += opt.length;
                    }
                    else if (maskValue.indexOf(key) !== -1) {
                        counterOfOpt++;
                    }
                    const firstOptionalIndex = maskValue.indexOf(key);
                    if (firstOptionalIndex !== -1 && processedValue.length >= firstOptionalIndex) {
                        // #1515: the early "everything before the first optional token is
                        // filled" return is only sound for trailing-optional layouts. When a
                        // mandatory token follows the first optional one (e.g. `999SSS`,
                        // indexOf === 0) it used to mark EVERY value as valid.
                        const hasMandatoryAfterOptional = maskValue
                            .slice(firstOptionalIndex)
                            .split(MaskExpression.EMPTY_STRING)
                            .some((symbol) => !!this._maskService.patterns[symbol] &&
                            !this._maskService.patterns[symbol]?.optional);
                        if (!hasMandatoryAfterOptional) {
                            return null;
                        }
                        if (!this.prefix() && !this.suffix() && this._isPlainTokenMask(maskValue)) {
                            // Position-aware check: match the value against the mask allowing
                            // optional tokens to be skipped; every mandatory token must be
                            // consumed (catches `12a` for `999SSS`, which a pure length
                            // check cannot).
                            return this._matchesMaskWithOptionalSkip(processedValue, maskValue)
                                ? null
                                : this._createValidationError(processedValue);
                        }
                        // Non-plain masks (prefix/suffix, exotic symbols) fall through to the
                        // legacy length check below (`maskValue.length - counterOfOpt`).
                    }
                    if (counterOfOpt === maskValue.length) {
                        return null;
                    }
                }
            }
            if ((maskValue.indexOf(MaskExpression.SYMBOL_STAR) > 1 &&
                processedValue.length < maskValue.indexOf(MaskExpression.SYMBOL_STAR)) ||
                (maskValue.indexOf(MaskExpression.SYMBOL_QUESTION) > 1 &&
                    processedValue.length < maskValue.indexOf(MaskExpression.SYMBOL_QUESTION))) {
                return this._createValidationError(processedValue);
            }
            if (maskValue.indexOf(MaskExpression.SYMBOL_STAR) === -1 ||
                maskValue.indexOf(MaskExpression.SYMBOL_QUESTION) === -1) {
                const array = maskValue.split('*');
                const length = this._maskService.dropSpecialCharacters
                    ? maskValue.length -
                        this._maskService.checkDropSpecialCharAmount(maskValue) -
                        counterOfOpt
                    : this.prefix()
                        ? maskValue.length + this.prefix().length - counterOfOpt
                        : maskValue.length - counterOfOpt;
                if (array.length === 1) {
                    if (processedValue.length < length) {
                        // #1583: for `||` multi-masks, a value shorter than the selected
                        // alternative can still be complete: it must stop exactly at a
                        // special-character boundary of the selected alternative and satisfy
                        // the length requirement of another (shorter) alternative.
                        if (this._isCompleteAlternativeBoundary(processedValue)) {
                            return null;
                        }
                        return this._createValidationError(processedValue);
                    }
                }
                if (array.length > 1) {
                    const lastIndexArray = array[array.length - 1];
                    if (lastIndexArray &&
                        this._maskService.specialCharacters.includes(lastIndexArray[0]) &&
                        String(processedValue).includes(lastIndexArray[0] ?? '') &&
                        !this.dropSpecialCharacters()) {
                        const special = value.split(lastIndexArray[0]);
                        return special[special.length - 1].length === lastIndexArray.length - 1
                            ? null
                            : this._createValidationError(processedValue);
                    }
                    else if (((lastIndexArray &&
                        !this._maskService.specialCharacters.includes(lastIndexArray[0])) ||
                        !lastIndexArray ||
                        this._maskService.dropSpecialCharacters) &&
                        processedValue.length >= length - 1) {
                        return null;
                    }
                    else {
                        return this._createValidationError(processedValue);
                    }
                }
            }
            if (maskValue.indexOf(MaskExpression.SYMBOL_STAR) === 1 ||
                maskValue.indexOf(MaskExpression.SYMBOL_QUESTION) === 1) {
                return null;
            }
        }
        if (value) {
            this.maskFilled.emit();
            return null;
        }
        return null;
    }
    onPaste() {
        this._justPasted.set(true);
    }
    onFocus() {
        this._isFocused.set(true);
        this._maskService._isFocused.set(true);
    }
    onModelChange(value) {
        // on form reset we need to update the actualValue
        if ((value === MaskExpression.EMPTY_STRING ||
            value === null ||
            typeof value === 'undefined') &&
            this._maskService.actualValue) {
            this._maskService.actualValue = this._maskService.getActualValue(MaskExpression.EMPTY_STRING);
        }
    }
    onInput(e) {
        this._maskService.isInitialized = true;
        // Android IMEs report every keydown as key 'Unidentified' / keyCode 229, so a
        // backspace is undetectable from keydown alone and all _code()-based deletion
        // branches misfire (cursor jumps, "stuck" backspace — #1497). The input event's
        // inputType is the reliable source: derive the deletion code from it.
        const inputType = e.inputType;
        if (inputType === 'deleteContentBackward') {
            this._code.set(MaskExpression.BACKSPACE);
        }
        else if (inputType === 'deleteContentForward') {
            this._code.set(MaskExpression.DELETE);
        }
        else if (inputType &&
            (this._code() === MaskExpression.BACKSPACE || this._code() === MaskExpression.DELETE)) {
            // A non-delete edit that produced no meaningful keydown (IME insertion,
            // context-menu paste): clear the stale deletion code so the edit is not
            // processed as a backspace.
            this._code.set(inputType);
        }
        // If IME is composing text, we wait for the composed text — but only when the mask
        // can actually accept letters. Purely numeric masks gain nothing from composition,
        // and some Android IMEs (Samsung Keyboard) deliver every keystroke as
        // insertCompositionText without firing compositionend until blur, which would leave
        // the FormControl stale for the whole edit (#1293).
        if (this._isComposing() && this._maskAcceptsLetterInput()) {
            return;
        }
        const el = e.target;
        const transformedValue = this._maskService.inputTransformFn
            ? this._maskService.inputTransformFn(el.value)
            : el.value;
        if (el.type !== 'number') {
            if (typeof transformedValue === 'string' || typeof transformedValue === 'number') {
                const transformedString = transformedValue.toString();
                // Only rewrite el.value when the transform actually changed it: any
                // programmatic `.value =` assignment clears the browser's "last changed
                // by a user edit" flag, which silently disables native constraint
                // validation (minlength -> validity.tooShort) even when the mask is a
                // no-op (#1379). With an empty mask the directive must stay a pure
                // passthrough for the native input.
                if (el.value !== transformedString) {
                    el.value = transformedString;
                }
                this._inputValue.set(el.value);
                this._setMask();
                if (!this._maskValue()) {
                    this.onChange(el.value);
                    return;
                }
                // On paste of a raw value that does not yet carry the prefix, the caret
                // position reported by the element is prefix.length short of where it must
                // land once applyMask prepends the prefix (#1571). Captured here because the
                // applyValueChanges callback below resets the _justPasted flag.
                const pastedValueWithoutPrefix = this._justPasted() &&
                    !!this._maskService.prefix &&
                    !el.value.startsWith(this._maskService.prefix);
                let position = el.selectionStart === 1
                    ? el.selectionStart + this._maskService.prefix.length
                    : el.selectionStart;
                if (this.keepCharacterPositions() &&
                    this._maskService.placeHolderCharacter.length === 1 &&
                    !this._justPasted()) {
                    const suffix = this.suffix();
                    const prefix = this.prefix();
                    const inputSymbol = el.value.slice(position - 1, position);
                    const prefixLength = prefix.length;
                    const showMaskTyped = this.showMaskTyped();
                    const maskExpression = this._maskService.maskExpression;
                    // Placeholder skeleton of the mask (special chars kept, fillable slots
                    // replaced by the placeholder character). With showMaskTyped the service
                    // renders it as maskIsShown; without showMaskTyped it is derived locally so
                    // keepCharacterPositions works on its own (#1545, #1543).
                    const maskSkeleton = this._maskService.maskIsShown.length
                        ? this._maskService.maskIsShown
                        : maskExpression.replace(/\w/g, this._maskService.placeHolderCharacter);
                    const hasSelection = this._maskService.selStart !== this._maskService.selEnd;
                    const selStartAbs = Number(this._maskService.selStart);
                    const selEndAbs = Number(this._maskService.selEnd);
                    const selStart = selStartAbs - prefixLength;
                    const selEnd = selEndAbs - prefixLength;
                    const backspaceOrDelete = this._code() === MaskExpression.BACKSPACE ||
                        this._code() === MaskExpression.DELETE;
                    // Whether this block fully resolved the resulting display value into
                    // this._maskService.actualValue. When true, applyMask short-circuits and
                    // renders actualValue as-is; when false the edit flows through regular
                    // masking (e.g. appending at the end, or clearing the whole value).
                    let kcpHandled = true;
                    if (backspaceOrDelete) {
                        if (hasSelection) {
                            const preEditLength = el.value.length + (selEndAbs - selStartAbs);
                            if (!showMaskTyped &&
                                selStartAbs <= prefixLength &&
                                selEndAbs >= preEditLength) {
                                // The whole value was selected and deleted: clear through the
                                // regular path instead of showing a placeholder skeleton.
                                kcpHandled = false;
                            }
                            else if (this._maskService.selStart === prefixLength) {
                                this._maskService.actualValue = `${prefix}${maskSkeleton.slice(0, selEnd)}${this._inputValue().split(prefix).join('')}`;
                            }
                            else if (this._maskService.selStart ===
                                maskSkeleton.length + prefixLength) {
                                this._maskService.actualValue = `${this._inputValue()}${maskSkeleton.slice(selStart, selEnd)}`;
                            }
                            else {
                                this._maskService.actualValue = `${prefix}${this._inputValue()
                                    .split(prefix)
                                    .join('')
                                    .slice(0, selStart)}${maskSkeleton.slice(selStart, selEnd)}${this._maskService.actualValue.slice(selEnd + prefixLength, maskSkeleton.length + prefixLength)}${suffix}`;
                            }
                        }
                        else if (!this._maskService.specialCharacters.includes(maskExpression.slice(position - prefixLength, position + 1 - prefixLength))) {
                            if (!showMaskTyped && position >= el.value.length) {
                                // Deleting the last character: no gap needs to be kept.
                                kcpHandled = false;
                            }
                            else if (selStart === 1 && prefix) {
                                this._maskService.actualValue = `${prefix}${this._maskService.placeHolderCharacter}${el.value
                                    .split(prefix)
                                    .join('')
                                    .split(suffix)
                                    .join('')}${suffix}`;
                                position = position - 1;
                            }
                            else {
                                const part1 = el.value.substring(0, position);
                                const part2 = el.value.substring(position);
                                this._maskService.actualValue = `${part1}${this._maskService.placeHolderCharacter}${part2}`;
                            }
                        }
                        position = this._code() === MaskExpression.DELETE ? position + 1 : position;
                    }
                    else if (hasSelection) {
                        // A selected range was replaced by the typed symbol. Keep the layout:
                        // blank the selection to the mask skeleton and put the typed symbol
                        // into the first fillable slot of the selection (#1527, #1489).
                        const preEditLength = el.value.length - 1 + (selEndAbs - selStartAbs);
                        if (!showMaskTyped &&
                            selStartAbs <= prefixLength &&
                            selEndAbs >= preEditLength) {
                            // The whole value was replaced: mask it through the regular path.
                            kcpHandled = false;
                        }
                        else {
                            let maskIdx = Math.max(selStart, 0);
                            while (maskIdx < maskExpression.length &&
                                this._maskService.specialCharacters.includes(maskExpression[maskIdx] ?? MaskExpression.EMPTY_STRING)) {
                                maskIdx += 1;
                            }
                            const blanked = `${el.value.slice(0, selStartAbs)}${maskSkeleton.slice(Math.max(selStart, 0), selEnd)}${el.value.slice(selStartAbs + 1)}`;
                            const targetAbs = maskIdx + prefixLength;
                            if (targetAbs < blanked.length - suffix.length &&
                                this._maskService._checkSymbolMask(inputSymbol, maskExpression[maskIdx] ?? MaskExpression.EMPTY_STRING)) {
                                this._maskService.actualValue = `${blanked.slice(0, targetAbs)}${inputSymbol}${blanked.slice(targetAbs + 1)}`;
                                position = targetAbs + 1;
                            }
                            else {
                                this._maskService.actualValue = blanked;
                                position = selStartAbs;
                            }
                        }
                    }
                    else {
                        // Single-caret insert (overwrite mode): the typed symbol lands in the
                        // first fillable mask slot at or after the caret, skipping any number
                        // of special characters (#1544, #1489).
                        const oldDisplay = `${el.value.slice(0, position - 1)}${el.value.slice(position)}`;
                        const oldDisplayNoSuffix = suffix
                            ? oldDisplay.split(suffix).join('')
                            : oldDisplay;
                        let maskIdx = position - 1 - prefixLength;
                        if (maskIdx < 0) {
                            // Typed inside the prefix: reject the symbol.
                            position = position - 1;
                        }
                        else {
                            while (maskIdx < maskExpression.length &&
                                this._maskService.specialCharacters.includes(maskExpression[maskIdx] ?? MaskExpression.EMPTY_STRING)) {
                                maskIdx += 1;
                            }
                            const targetAbs = maskIdx + prefixLength;
                            if (targetAbs >= oldDisplayNoSuffix.length) {
                                if (oldDisplayNoSuffix.length <= prefixLength || !showMaskTyped) {
                                    // First symbol, or appending at the end without
                                    // showMaskTyped: regular masking handles it (including
                                    // leadZeroDateTime insertions).
                                    kcpHandled = false;
                                }
                                else {
                                    // All slots of the rendered mask are already consumed.
                                    position = position - 1;
                                }
                            }
                            else if (this._maskService._checkSymbolMask(inputSymbol, maskExpression[maskIdx] ?? MaskExpression.EMPTY_STRING)) {
                                this._maskService.actualValue = `${oldDisplayNoSuffix.slice(0, targetAbs)}${inputSymbol}${oldDisplayNoSuffix.slice(targetAbs + 1)}${suffix}`;
                                position = targetAbs + 1;
                            }
                            else {
                                // Rejected symbol: keep the current display.
                                position = position - 1;
                            }
                        }
                    }
                    this._maskService.keepCharacterPositionsHandled = kcpHandled;
                }
                // A literal '*' typed into the VALUE (custom pattern allowing asterisks) makes
                // the service's applyMask take its hiddenInput shadow-value branch, whose
                // equal/shorter-length cases restore the stale pre-edit actualValue and discard
                // a selection replacement (#1504). Without hiddenInput there is no shadow state
                // to preserve, so drop it and let the edited value flow through regular masking.
                if (!this._maskService.hiddenInput &&
                    !this._maskService.showMaskTyped &&
                    !this.keepCharacterPositions() &&
                    this._maskService.selStart !== this._maskService.selEnd &&
                    el.value.includes(MaskExpression.SYMBOL_STAR)) {
                    this._maskService.actualValue = MaskExpression.EMPTY_STRING;
                }
                let caretShift = 0;
                let backspaceShift = false;
                if (this._code() === MaskExpression.DELETE && MaskExpression.SEPARATOR) {
                    this._maskService.deletedSpecialCharacter = true;
                }
                if (this._inputValue().length >= this._maskService.maskExpression.length - 1 &&
                    this._code() !== MaskExpression.BACKSPACE &&
                    this._maskService.maskExpression === MaskExpression.DAYS_MONTHS_YEARS &&
                    position < 10) {
                    const inputSymbol = this._inputValue().slice(position - 1, position);
                    el.value =
                        this._inputValue().slice(0, position - 1) +
                            inputSymbol +
                            this._inputValue().slice(position + 1);
                }
                // #1488: generalizes the single-mask (d0/M0/0000) caret-shift-past-separator
                // fix to any date/time field. Locates the date/time field containing
                // `position` by scanning maskExpression for a DAY/MONTH/HOURS/HOUR/MINUTE/
                // SECOND token followed by its digit run, then checks overflow against the
                // same per-field thresholds generic-pattern.handler.ts already establishes
                // (day 31, month 12, hour 23/12 w/ apm, minute/second 59). Read-only against
                // generic-pattern.handler.ts — mirrors its thresholds, does not import from it.
                if (this.leadZeroDateTime()) {
                    const field = this._findDateTimeFieldAt(this._maskService.maskExpression, position);
                    if (field &&
                        this._isFieldOverflowing(field, position, el.value, this._inputValue(), !!this.apm())) {
                        position = position + 2;
                    }
                }
                if (this._maskService.maskExpression === MaskExpression.HOURS_MINUTES_SECONDS &&
                    this.apm()) {
                    if (this._justPasted() && el.value.slice(0, 2) === MaskExpression.DOUBLE_ZERO) {
                        el.value = el.value.slice(1, 2) + el.value.slice(2, el.value.length);
                    }
                    el.value =
                        el.value === MaskExpression.DOUBLE_ZERO
                            ? MaskExpression.NUMBER_ZERO
                            : el.value;
                }
                this._maskService.applyValueChanges(position, this._justPasted(), this._code() === MaskExpression.BACKSPACE ||
                    this._code() === MaskExpression.DELETE, (shift, _backspaceShift) => {
                    this._justPasted.set(false);
                    caretShift = shift;
                    backspaceShift = _backspaceShift;
                });
                // only set the selection if the element is active
                if (this._getActiveElement() !== el) {
                    return;
                }
                if (this._maskService.plusOnePosition) {
                    position = position + 1;
                    this._maskService.plusOnePosition = false;
                }
                // update position after applyValueChanges to prevent cursor on wrong position when it has an array of maskExpression
                if (this._maskExpressionArray().length) {
                    if (this._code() === MaskExpression.BACKSPACE) {
                        const specialChartMinusOne = this.specialCharacters().includes(this._maskService.actualValue.slice(position - 1, position));
                        const allowFewMaskChangeMask = this._maskService.removeMask(this._inputValue())?.length ===
                            this._maskService.removeMask(this._maskService.maskExpression)?.length;
                        const specialChartPlusOne = this.specialCharacters().includes(this._maskService.actualValue.slice(position, position + 1));
                        if (allowFewMaskChangeMask && !specialChartPlusOne) {
                            position = el.selectionStart + 1;
                        }
                        else {
                            position = specialChartMinusOne ? position - 1 : position;
                        }
                    }
                    else {
                        position =
                            el.selectionStart === 1
                                ? el.selectionStart + this._maskService.prefix.length
                                : el.selectionStart;
                    }
                }
                this._position.set(this._position() === 1 && this._inputValue().length === 1
                    ? null
                    : this._position());
                let positionToApply = this._position()
                    ? this._inputValue().length + position + caretShift
                    : position +
                        (this._code() === MaskExpression.BACKSPACE && !backspaceShift
                            ? 0
                            : caretShift);
                // For separator masks the applier's caret shift is computed on the raw
                // (prefix-less) value, so account for the prefix the mask just added (#1571).
                if (pastedValueWithoutPrefix &&
                    this._maskValue().startsWith(MaskExpression.SEPARATOR)) {
                    positionToApply += this._maskService.prefix.length;
                }
                if (positionToApply > this._getActualInputLength()) {
                    // A bare decimal marker ('.' or, with a prefix, '$.') is not a number:
                    // formControlResult has just emitted null for it, which the ngModelChange
                    // reset handler treats as a form reset and clears actualValue. The plain
                    // length clamp would then land the caret BEFORE the marker — keep it right
                    // after the marker instead (#1572).
                    const decimalMarker = this._maskService.decimalMarker;
                    const prefix = this._maskService.prefix;
                    const valueWithoutPrefix = el.value.startsWith(prefix)
                        ? el.value.slice(prefix.length)
                        : el.value;
                    const isBareDecimalMarker = valueWithoutPrefix.length === 1 &&
                        (Array.isArray(decimalMarker)
                            ? decimalMarker.includes(valueWithoutPrefix)
                            : valueWithoutPrefix === decimalMarker);
                    positionToApply = isBareDecimalMarker
                        ? this._getActualInputLength() + 1
                        : this._getActualInputLength();
                }
                if (positionToApply < 0) {
                    positionToApply = 0;
                }
                el.setSelectionRange(positionToApply, positionToApply);
                this._position.set(null);
            }
            else {
                // eslint-disable-next-line no-console
                console.warn('Ngx-mask writeValue work with string | number, your current value:', typeof transformedValue);
            }
        }
        else {
            if (!this._maskValue()) {
                this.onChange(el.value);
                return;
            }
            this._maskService.applyValueChanges(el.value.length, this._justPasted(), this._code() === MaskExpression.BACKSPACE || this._code() === MaskExpression.DELETE);
        }
    }
    /**
     * #1488: locates the date/time field (DAY/MONTH/HOURS/HOUR/MINUTE/SECOND) that
     * `position` falls within, scanning `maskExpression` left-to-right. A field spans
     * its token plus its digit-pair companion — `0` for DAY/MONTH/MINUTE/SECOND (e.g.
     * `d0`, `M0`, `m0`, `s0`), or `HOUR` for the combined `Hh` hour field. Matches any
     * position from the field's first char up to and including its end (separator index),
     * so both a still-mid-typing caret and the just-completed field are covered — the
     * precise overflow decision (single first digit vs. completed 2-digit value) happens
     * in `_isFieldOverflowing`. Returns null for non-date/time masks, a no-op for the caller.
     */
    _findDateTimeFieldAt(maskExpression, position) {
        const dateTimeTokens = [
            MaskExpression.DAY,
            MaskExpression.MONTH,
            MaskExpression.HOURS,
            MaskExpression.HOUR,
            MaskExpression.MINUTE,
            MaskExpression.SECOND,
        ];
        let index = 0;
        while (index < maskExpression.length) {
            const token = maskExpression[index] ?? MaskExpression.EMPTY_STRING;
            if (dateTimeTokens.includes(token)) {
                const next = maskExpression[index + 1];
                const isCombinedHour = token === MaskExpression.HOURS && next === MaskExpression.HOUR;
                const end = next === MaskExpression.NUMBER_ZERO || isCombinedHour ? index + 2 : index + 1;
                if (position >= index && position <= end) {
                    return {
                        token: token,
                        start: index,
                        end,
                    };
                }
                index = end;
                continue;
            }
            index++;
        }
        return null;
    }
    /**
     * Per-token overflow thresholds, mirroring the ones `generic-pattern.handler.ts`
     * already establishes (day 31, month 12, hour 23/12 w/ apm, minute/second 59) — kept
     * as a small local table rather than importing from the handler (see design's
     * Rejected Alternatives: the directive and the handler stay on separate sides of the
     * display/parse boundary).
     *
     * Two overflow shapes are checked, mirroring how the handler itself decides mid-type:
     * - `firstDigitValue`: when `position` is exactly one char past the field's start (the
     *   just-typed char is the field's first digit), an out-of-range first digit alone
     *   (e.g. month digit `3`, day digit `4`) already triggers the handler's own leadZero
     *   pad within this same keystroke — `el.value` hasn't caught up yet at this point in
     *   the pipeline, so the raw typed digit (`inputValue`) must be read instead.
     * - `fieldValue`: once both digits of the field are present in `el.value`, an
     *   out-of-range 2-digit value (e.g. day `33`, month `13`) triggers the shift directly.
     */
    _isFieldOverflowing(field, position, elValue, inputValue, apm) {
        const fieldValue = Number(elValue.slice(field.start, field.end));
        const isFirstDigitOfField = position === field.start + 1;
        const firstDigitValue = isFirstDigitOfField
            ? Number(inputValue.slice(position - 1, position))
            : NaN;
        switch (field.token) {
            case MaskExpression.DAY:
                return (fieldValue > 31 && fieldValue < 40) || firstDigitValue > 3;
            case MaskExpression.MONTH:
                return fieldValue > 12 || firstDigitValue > 1;
            case MaskExpression.HOURS:
                return apm
                    ? fieldValue > 9 || firstDigitValue > 9
                    : fieldValue > 23 || firstDigitValue > 2;
            case MaskExpression.HOUR:
                return apm ? fieldValue > 12 : fieldValue > 23;
            case MaskExpression.MINUTE:
            case MaskExpression.SECOND:
                return fieldValue > 59 || firstDigitValue > 5;
            default:
                return false;
        }
    }
    /**
     * Whether any pattern slot of the current mask expression can accept a letter.
     * IME composition is only meaningful for such masks; for purely numeric masks
     * (digit patterns, separator, date/time, IP, CPF_CNPJ) waiting for compositionend
     * only delays the model sync — and Samsung Keyboard may never fire it until blur
     * (#1293). Unknown/letterless probe failures fall back to `false` (process live).
     */
    _maskAcceptsLetterInput() {
        const patterns = this._maskService.patterns;
        const maskExpression = this._maskService.maskExpression;
        for (const symbol of maskExpression) {
            const pattern = patterns[symbol]?.pattern;
            if (!pattern) {
                continue;
            }
            // Re-create without sticky/global flags: test() on those is stateful.
            const probe = new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ''));
            if (probe.test('a') || probe.test('A')) {
                return true;
            }
        }
        return false;
    }
    // IME starts
    onCompositionStart() {
        this._isComposing.set(true);
    }
    // IME completes
    onCompositionEnd(e) {
        this._isComposing.set(false);
        if (!this._maskAcceptsLetterInput()) {
            // For letterless masks every edit was already processed live in onInput
            // (#1293); reprocessing the same value through the paste path would
            // double-apply the mask.
            return;
        }
        this._justPasted.set(true);
        this.onInput(e);
    }
    onBlur(e) {
        if (this._maskValue()) {
            const el = e.target;
            const pristineBeforeDefault = this._applyDefaultValueOnBlur(el);
            if (this._maskService.leadZero &&
                el.value.length > 0 &&
                typeof this._maskService.decimalMarker === 'string') {
                const maskExpression = this._maskService.maskExpression;
                const decimalMarker = this._maskService.decimalMarker;
                const suffix = this._maskService.suffix;
                const precision = Number(this._maskService.maskExpression.slice(maskExpression.length - 1, maskExpression.length));
                if (precision > 0) {
                    el.value = suffix ? el.value.split(suffix).join('') : el.value;
                    const decimalPart = el.value.split(decimalMarker)[1];
                    el.value = el.value.includes(decimalMarker)
                        ? el.value +
                            MaskExpression.NUMBER_ZERO.repeat(precision - (decimalPart?.length || 0)) +
                            suffix
                        : el.value +
                            decimalMarker +
                            MaskExpression.NUMBER_ZERO.repeat(precision) +
                            suffix;
                    this._maskService.actualValue = el.value;
                    // #1634: propagate through the regular formControlResult pipeline (not a
                    // raw onChange) so outputTransformFn runs on this blur-time reformat, same
                    // as every typing-time emission. A raw onChange here sent the masked display
                    // string straight to the model, bypassing outputTransformFn entirely.
                    this._maskService.formControlResult(this._maskService.actualValue);
                }
            }
            this._maskService.clearIfNotMatchFn();
            if (pristineBeforeDefault !== null) {
                // The default-value emission (and a possible leadZero padding pass above)
                // ran through the view-change pipeline, which dirties the control. A
                // blur-time programmatic write must not change dirtiness, so restore the
                // pre-write pristine state. Touched is NOT restored — blur legitimately
                // touches the control (onTouch below).
                this._restoreControlStateAfterWrite(pristineBeforeDefault, false);
                this._changeDetectorRef.markForCheck();
            }
        }
        this._isFocused.set(false);
        this._maskService._isFocused.set(false);
        this.onTouch();
    }
    /**
     * Issue #1435 (defaultValueOnBlur): when configured — via the directive input or the
     * DI config — and the control's unmasked value is empty on blur (covers '', a bare
     * prefix/suffix and the showMaskTyped skeleton), writes the default through the
     * regular mask pipeline: applyMask renders the masked display and emits the usual
     * model output (dropSpecialCharacters/outputTransformFn applied) via formControlResult.
     * Returns the control's pre-write pristine state for the caller to restore once the
     * whole blur pass is done, or `null` when no default was applied.
     */
    _applyDefaultValueOnBlur(el) {
        const defaultValue = this.defaultValueOnBlur() ?? this._config.defaultValueOnBlur;
        if (!defaultValue || this._maskService.removeMask(el.value)) {
            return null;
        }
        const ngControl = this._resolveNgControl();
        const wasPristine = ngControl ? Boolean(ngControl.pristine) : true;
        const displayValue = this._maskService.applyMask(defaultValue, this._maskService.maskExpression);
        el.value = displayValue;
        this._inputValue.set(displayValue);
        return wasPristine;
    }
    onClick(e) {
        if (!this._maskValue()) {
            return;
        }
        const el = e.target;
        const posStart = 0;
        const posEnd = 0;
        if (el !== null &&
            el.selectionStart !== null &&
            el.selectionStart === el.selectionEnd &&
            el.selectionStart > this._maskService.prefix.length &&
            e.keyCode !== 38) {
            if (this._maskService.showMaskTyped && !this.keepCharacterPositions()) {
                // We are showing the mask in the input
                this._maskService.maskIsShown = this._maskService.showMaskInInput();
                if (el.setSelectionRange &&
                    this._maskService.prefix + this._maskService.maskIsShown === el.value) {
                    // the input ONLY contains the mask, so position the cursor at the start
                    el.focus();
                    el.setSelectionRange(posStart, posEnd);
                }
                else {
                    // the input contains some characters already
                    if (el.selectionStart > this._maskService.actualValue.length) {
                        // if the user clicked beyond our value's length, position the cursor at the end of our value
                        el.setSelectionRange(this._maskService.actualValue.length, this._maskService.actualValue.length);
                    }
                }
            }
        }
        const nextValue = el &&
            (el.value === this._maskService.prefix
                ? this._maskService.prefix + this._maskService.maskIsShown
                : el.value);
        /** Fix of cursor position jumping to end in most browsers no matter where cursor is inserted onFocus */
        if (el && el.value !== nextValue) {
            el.value = nextValue;
        }
        /** fix of cursor position with prefix when mouse click occur */
        if (el &&
            el.type !== 'number' &&
            (el.selectionStart || el.selectionEnd) <=
                this._maskService.prefix.length) {
            const specialCharactersAtTheStart = this._maskService.maskExpression.match(new RegExp(`^[${this._maskService.specialCharacters.map((c) => `\\${c}`).join('')}]+`))?.[0].length || 0;
            el.selectionStart = this._maskService.prefix.length + specialCharactersAtTheStart;
            return;
        }
        /** select only inserted text */
        if (el && el.selectionEnd > this._getActualInputLength()) {
            el.selectionEnd = this._getActualInputLength();
        }
    }
    onKeyDown(event) {
        const e = event;
        if (!this._maskValue()) {
            return;
        }
        if (this._isComposing()) {
            // User finalize their choice from IME composition, so trigger onInput() for the composed text.
            if (e.key === 'Enter') {
                this.onCompositionEnd(event);
                return;
            }
            // Android IMEs can keep a single composition open across many keystrokes
            // (#1293): still capture the pre-edit value and selection that onInput
            // relies on when it processes edits during composition.
            const composingEl = e.target;
            this._inputValue.set(composingEl.value);
            this._maskService.selStart = composingEl.selectionStart;
            this._maskService.selEnd = composingEl.selectionEnd;
            return;
        }
        this._code.set(e.code ? e.code : e.key);
        const el = e.target;
        this._inputValue.set(el.value);
        this._setMask();
        const isTextarea = el.tagName.toLowerCase() === 'textarea';
        if (el.type !== 'number') {
            if (this._insertNumpadDecimalMarker(e, el)) {
                return;
            }
            if (e.key === MaskExpression.ARROW_UP && !isTextarea) {
                e.preventDefault();
            }
            if (e.key === MaskExpression.ARROW_LEFT ||
                e.key === MaskExpression.BACKSPACE ||
                e.key === MaskExpression.DELETE) {
                if (e.key === MaskExpression.BACKSPACE && el.value.length === 0) {
                    el.selectionStart = el.selectionEnd;
                }
                if (e.key === MaskExpression.BACKSPACE && el.selectionStart !== 0) {
                    const prefixLength = this.prefix().length;
                    // Use the service value: it holds the config defaults when the input is not
                    // bound and the bound value otherwise, so an explicitly bound empty array is
                    // respected instead of silently falling back to the defaults (#1512).
                    const specialCharacters = this._maskService.specialCharacters;
                    if (prefixLength > 1 && el.selectionStart <= prefixLength) {
                        el.setSelectionRange(prefixLength, el.selectionEnd);
                    }
                    else {
                        if (this._inputValue().length !== el.selectionStart &&
                            el.selectionStart !== 1) {
                            while (specialCharacters.includes((this._inputValue()[el.selectionStart - 1] ??
                                MaskExpression.EMPTY_STRING).toString()) &&
                                ((prefixLength >= 1 &&
                                    el.selectionStart > prefixLength) ||
                                    prefixLength === 0)) {
                                el.setSelectionRange(el.selectionStart - 1, el.selectionEnd);
                            }
                        }
                    }
                }
                this.checkSelectionOnDeletion(el);
                if (this._maskService.prefix.length &&
                    el.selectionStart <= this._maskService.prefix.length &&
                    el.selectionEnd <= this._maskService.prefix.length) {
                    e.preventDefault();
                }
                const cursorStart = el.selectionStart;
                if (e.key === MaskExpression.BACKSPACE &&
                    !el.readOnly &&
                    cursorStart === 0 &&
                    el.selectionEnd === el.value.length &&
                    el.value.length !== 0) {
                    // Handle the clear fully here instead of emitting the model change and
                    // relying on the browser's default deletion + input event to update the
                    // view: Firefox can drop the default action after the emission below
                    // triggers change detection (DOM churn around the input), leaving the
                    // model empty but the view untouched until a second Backspace (#1350).
                    e.preventDefault();
                    // The default action (and its input event) is suppressed, so the
                    // inputTransformFn would never see the cleared value; run it here (#1651).
                    const transformedEmpty = this._maskService.inputTransformFn
                        ? this._maskService.inputTransformFn(MaskExpression.EMPTY_STRING)
                        : MaskExpression.EMPTY_STRING;
                    const clearedValue = typeof transformedEmpty === 'string' || typeof transformedEmpty === 'number'
                        ? String(transformedEmpty)
                        : MaskExpression.EMPTY_STRING;
                    const displayValue = this._maskService.applyMask(clearedValue, this._maskService.maskExpression, 0, false, true);
                    el.value = displayValue;
                    this._inputValue.set(displayValue);
                    const caret = Math.min(this._maskService.prefix.length, displayValue.length);
                    el.setSelectionRange(caret, caret);
                }
            }
            if (!!this.suffix() &&
                this.suffix().length > 1 &&
                this._inputValue().length - this.suffix().length < el.selectionStart) {
                el.setSelectionRange(this._inputValue().length - this.suffix().length, this._inputValue().length);
            }
            else if ((e.code === 'KeyA' && e.ctrlKey) ||
                (e.code === 'KeyA' && e.metaKey) // Cmd + A (Mac)
            ) {
                el.setSelectionRange(0, this._getActualInputLength());
                e.preventDefault();
            }
            this._maskService.selStart = el.selectionStart;
            this._maskService.selEnd = el.selectionEnd;
        }
    }
    /**
     * #1641: on layouts whose decimal separator is a comma, the numeric keypad still emits
     * a period, which a separator mask configured with a comma decimal marker rejects. The
     * keypad decimal key (`key: 'Decimal'`, or a `.` from `code: 'NumpadDecimal'`) is
     * therefore turned into the configured marker. Only the keypad key is intercepted: a
     * period typed on the main keyboard keeps its normal behavior (e.g. as a thousand
     * separator). Returns true when the key press was handled.
     */
    _insertNumpadDecimalMarker(e, el) {
        const isNumpadDecimal = e.key === MaskExpression.NUMPAD_DECIMAL ||
            (e.code === MaskExpression.NUMPAD_DECIMAL_CODE && e.key === MaskExpression.DOT);
        if (!isNumpadDecimal ||
            e.ctrlKey ||
            e.metaKey ||
            e.altKey ||
            el.readOnly ||
            !this._maskValue().startsWith(MaskExpression.SEPARATOR)) {
            return false;
        }
        const configuredMarker = this._maskService.decimalMarker;
        const marker = Array.isArray(configuredMarker)
            ? configuredMarker.find((item) => item !== this._maskService.thousandSeparator)
            : configuredMarker;
        if (marker !== MaskExpression.COMMA) {
            return false;
        }
        const start = el.selectionStart ?? el.value.length;
        const end = el.selectionEnd ?? start;
        e.preventDefault();
        this._code.set(MaskExpression.NUMPAD_DECIMAL_CODE);
        this._inputValue.set(el.value);
        this._maskService.selStart = start;
        this._maskService.selEnd = end;
        el.value = `${el.value.slice(0, start)}${marker}${el.value.slice(end)}`;
        el.setSelectionRange(start + 1, start + 1);
        el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: marker, bubbles: true }));
        return true;
    }
    /**
     * Restores the control's pristine/untouched state after a writeValue-driven emission.
     *
     * writeValue is a one-way model->view sync. When the mask normalizes the written value
     * (e.g. leadZero '10.2' -> '10.20'), the directive must still emit the corrected value so
     * the model adopts it — but that emission runs through Angular's view-change pipeline, which
     * calls markAsDirty()/markAsTouched(). A programmatic setValue/patchValue must leave the
     * control pristine, so we undo that side effect here when the control was pristine before
     * the write. `onlySelf: true` keeps parent group state untouched.
     */
    _restoreControlStateAfterWrite(wasPristine, wasUntouched) {
        const ngControl = this._resolveNgControl();
        const control = ngControl?.control;
        if (!control) {
            return;
        }
        if (wasPristine && ngControl.dirty && typeof control.markAsPristine === 'function') {
            control.markAsPristine({ onlySelf: true });
        }
        if (wasUntouched && ngControl.touched && typeof control.markAsUntouched === 'function') {
            control.markAsUntouched({ onlySelf: true });
        }
    }
    /** It writes the value in the input */
    writeValue(controlValue) {
        if (!this._configApplied && this.mask()) {
            // Called before the first ngOnChanges pass configured the mask service (happens with
            // Signal Forms' [formField], whose control-sync instruction runs before sibling
            // directives' ngOnChanges). Defer and replay once the configuration is applied.
            this._pendingInitialValue = controlValue;
            this._hasPendingInitialValue = true;
            return;
        }
        // Skip the model echo of a value we just propagated ourselves (see the
        // _lastPropagatedValue doc). Empty writes are never skipped: form reset('') must
        // always clear the display and the currentValue/previousValue service state below.
        const lastPropagated = this._lastPropagatedValue;
        this._lastPropagatedValue = null;
        if (lastPropagated !== null &&
            lastPropagated !== '' &&
            (typeof controlValue === 'string' || typeof controlValue === 'number') &&
            String(controlValue) === lastPropagated) {
            return;
        }
        const ngControl = this._resolveNgControl();
        const wasPristine = ngControl ? Boolean(ngControl.pristine) : true;
        const wasUntouched = ngControl ? Boolean(ngControl.untouched) : true;
        let value = controlValue;
        const inputTransformFn = this._maskService.inputTransformFn;
        if (typeof value === 'object' && value !== null && 'value' in value) {
            if ('disable' in value) {
                this.setDisabledState(Boolean(value.disable));
            }
            value = value.value;
        }
        if (value !== null) {
            value = inputTransformFn ? inputTransformFn(value) : value;
        }
        if (typeof value === 'string' ||
            typeof value === 'number' ||
            value === null ||
            typeof value === 'undefined') {
            if (value === null || typeof value === 'undefined' || value === '') {
                this._maskService.currentValue = '';
                this._maskService.previousValue = '';
            }
            let inputValue = value;
            if (typeof inputValue === 'number' ||
                this._maskValue().startsWith(MaskExpression.SEPARATOR)) {
                inputValue = String(inputValue);
                const localeDecimalMarker = this._maskService.currentLocaleDecimalMarker();
                if (!Array.isArray(this._maskService.decimalMarker)) {
                    inputValue =
                        this._maskService.decimalMarker !== localeDecimalMarker
                            ? inputValue.replace(localeDecimalMarker, this._maskService.decimalMarker)
                            : inputValue;
                }
                if (this._maskService.leadZero &&
                    inputValue &&
                    this.mask() &&
                    this.dropSpecialCharacters() !== false) {
                    inputValue = this._maskService._checkPrecision(this._maskService.maskExpression, inputValue);
                }
                if (this._maskService.decimalMarker === MaskExpression.COMMA ||
                    (Array.isArray(this._maskService.decimalMarker) &&
                        this._maskService.thousandSeparator === MaskExpression.DOT)) {
                    inputValue = inputValue
                        .toString()
                        .replace(MaskExpression.DOT, MaskExpression.COMMA);
                }
                if (this._resolvedMaskInput().startsWith(MaskExpression.SEPARATOR) &&
                    this.leadZero()) {
                    const isFirstWrite = !this._maskService.isInitialized;
                    requestAnimationFrame(() => {
                        // On initial load, temporarily set isInitialized to false
                        // so formControlResult returns early and doesn't mark form as dirty.
                        // On later writeValue-driven writes, leave isInitialized as-is: this pass
                        // may legitimately need to push a leadZero-normalized value (e.g.
                        // '10.2' -> '10.20') back to the FormControl, which Angular's forms
                        // pipeline can only do via the same onChange callback used for real user
                        // edits — see the `should change formValue` unit tests, which assert the
                        // normalized value lands in `form.value` after a programmatic setValue.
                        if (isFirstWrite) {
                            this._maskService.isInitialized = false;
                        }
                        this._maskService.applyMask(inputValue?.toString() ?? '', this._maskService.maskExpression);
                        if (isFirstWrite) {
                            this._maskService.isInitialized = true;
                        }
                        // The leadZero normalization above emits the corrected value through the
                        // view-change pipeline, which dirties/touches the control. Undo that so a
                        // programmatic write stays pristine.
                        this._restoreControlStateAfterWrite(wasPristine, wasUntouched);
                        // Zoneless CD does not auto-flush after requestAnimationFrame; request
                        // a check so form.pristine/form.value bindings reflect the new state.
                        this._changeDetectorRef.markForCheck();
                    });
                }
                this._maskService.isNumberValue = true;
            }
            if (typeof inputValue !== 'string' || value === null || typeof value === 'undefined') {
                inputValue = '';
            }
            this._inputValue.set(inputValue);
            this._setMask();
            if ((inputValue && this._maskService.maskExpression) ||
                (this._maskService.maskExpression &&
                    (this._maskService.prefix || this._maskService.showMaskTyped))) {
                // Let the service we know we are writing value so that triggering onChange function won't happen during applyMask
                this._maskService.writingValue = true;
                const displayValue = this._maskedOrVerbatim(inputValue);
                this._maskService.formElementProperty = ['value', displayValue];
                this._writeElementValueSync(displayValue);
                // Let the service know we've finished writing value
                this._maskService.writingValue = false;
                this._maskService.isInitialized = true;
            }
            else {
                this._maskService.formElementProperty = ['value', inputValue];
                this._writeElementValueSync(inputValue);
                this._maskService.isInitialized = true;
            }
            // A writeValue-driven emission may have dirtied/touched the control via the
            // view-change pipeline; undo that so programmatic writes stay pristine.
            this._restoreControlStateAfterWrite(wasPristine, wasUntouched);
            // Programmatic writes (setValue/patchValue called outside a signal/effect context)
            // don't schedule CD under zoneless change detection. Request a check so bindings
            // reading form.pristine/form.dirty/form.value on the host template stay in sync.
            this._changeDetectorRef.markForCheck();
        }
        else {
            // eslint-disable-next-line no-console
            console.warn('Ngx-mask writeValue work with string | number, your current value:', typeof value);
        }
    }
    /**
     * Mirrors a writeValue-driven render into the DOM synchronously, in the same
     * change-detection pass (#1305). The service's `formElementProperty` setter defers all
     * writes via queueMicrotask (to keep FIFO ordering with config-driven re-renders and
     * dodge ExpressionChanged issues), but consumers that read `nativeElement.value` DURING
     * the CD pass — Angular Material's floating label (`MatInput.empty`), CDK autofill —
     * never see a value that only lands in a later microtask. The deferred write still runs
     * afterwards and re-applies the same final value, so ordering guarantees are preserved.
     *
     * Skipped while a mask reconfiguration is pending (`mask()` input changed but
     * `ngOnChanges` has not applied it yet — e.g. `mask.set(...)` + `setValue(...)` before
     * the next CD pass): the value just computed used the STALE mask config, and rendering
     * it synchronously would expose an intermediate state that the deferred pipeline is
     * about to supersede. Multi-masks (`||`) resolve `_maskValue` to one alternative and
     * therefore also fall back to the deferred-only path.
     */
    _writeElementValueSync(value) {
        if (this._resolvedMaskInput() !== this._maskValue()) {
            return;
        }
        this._renderer.setProperty(this._elementRef.nativeElement, 'value', value);
    }
    /** The `mask` input with a user-defined alias (config maskAliases) expanded, if any. */
    _resolvedMaskInput() {
        return resolveMaskAlias(this.mask(), this._config.maskAliases);
    }
    registerOnChange(fn) {
        // Angular only calls this in classic ControlValueAccessor mode (reactive/template forms).
        // Its invocation is therefore our reliable signal that we are NOT in Signal Forms mode.
        this._isCvaMode.set(true);
        const originalFn = fn;
        this._maskService.onChange = this.onChange = (value) => {
            originalFn(value);
            this._propagateToValueModel(value);
        };
    }
    registerOnTouched(fn) {
        this.onTouch = () => {
            fn();
            if (!this.touched()) {
                this.touched.set(true);
            }
        };
    }
    /**
     * Pushes the current unmasked value into the `value` model input. In Signal Forms mode this
     * fires the `valueChange` output that Angular listens to; in CVA mode it is a harmless write
     * to a model nobody reads. We flag `_skipNextValueEffect` so the resulting model change does
     * not bounce back through the value effect and overwrite the raw `_inputValue`.
     */
    _propagateToValueModel(value) {
        const stringValue = value === null || typeof value === 'undefined' ? '' : String(value);
        this._lastPropagatedValue = stringValue;
        untracked(() => {
            if (String(this.value()) !== stringValue) {
                this._skipNextValueEffect.set(true);
                this.value.set(stringValue);
            }
        });
    }
    /**
     * Focus the input element.
     * Required by FormValueControl interface for Signal Forms.
     */
    focus() {
        this._maskService._elementRef?.nativeElement?.focus();
    }
    _getActiveElement(document = this.document) {
        const shadowRootEl = document?.activeElement?.shadowRoot;
        if (!shadowRootEl?.activeElement) {
            return document.activeElement;
        }
        else {
            return this._getActiveElement(shadowRootEl);
        }
    }
    checkSelectionOnDeletion(el) {
        const prefixLength = this.prefix().length;
        const suffixLength = this.suffix().length;
        const inputValueLength = this._inputValue().length;
        el.selectionStart = Math.min(Math.max(prefixLength, el.selectionStart), inputValueLength - suffixLength);
        el.selectionEnd = Math.min(Math.max(prefixLength, el.selectionEnd), inputValueLength - suffixLength);
    }
    /**
     * It disables the input element.
     *
     * Mirrors `_writeElementValueSync` (#1305): the service's `formElementProperty` setter
     * defers ALL DOM writes via `queueMicrotask` to dodge ExpressionChangedAfterItHasBeenChecked
     * and keep FIFO ordering with config-driven re-renders. For `disabled` specifically, that
     * deferral leaves `nativeElement.disabled` stale for at least one microtask after Angular
     * Forms' `setUpControl` calls this method synchronously during init — long enough for a
     * consumer reading the DOM property in the same synchronous phase (or another deferred write
     * racing in FIFO order) to observe the wrong value, e.g. an initially-disabled FormControl
     * whose native input briefly (or, depending on interleaving, persistently) reports
     * `disabled === false` (#1633). Writing synchronously here closes that gap; the deferred
     * write below still runs afterwards and re-applies the same final value, preserving the
     * existing ordering guarantees other call sites rely on.
     */
    setDisabledState(isDisabled) {
        this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
        this._maskService.formElementProperty = ['disabled', isDisabled];
    }
    _applyMask() {
        this._maskService.maskExpression = this._maskService._repeatPatternSymbols(this._maskValue() || '');
        this._maskService.formElementProperty = [
            'value',
            this._maskedOrVerbatim(this._inputValue()),
        ];
    }
    /**
     * Renders `inputValue` through the mask, falling back to the raw value verbatim when the
     * mask cannot process ANY of it (#1615, e.g. a sentinel like 'ONGOING' written into a
     * digits-only control). Values that PARTIALLY match keep regular masking.
     *
     * Shared by writeValue() and _applyMask() (called from every ngOnChanges pass, including
     * ones triggered by an UNRELATED input like `disabled`) so the verbatim verdict for a
     * value written once via writeValue() is not lost on a later re-render that replays the
     * same raw `inputValue` outside of writeValue — which would otherwise re-run regular
     * masking, produce an empty result, and emit it through onChange, clobbering the model.
     *
     * Excludes an actual mask RECONFIGURATION (`maskChanged`): when the mask itself just
     * changed, a value that no longer matches must clear through the regular path (see
     * trigger-on-mask-change.spec.ts) — verbatim passthrough only covers re-renders of the
     * SAME mask.
     */
    _maskedOrVerbatim(inputValue) {
        // Snapshot before applyMask(): it resets maskChanged internally as part of emitting
        // (or skipping) the change, so it must be read before the call, not after.
        const wasMaskChanged = this._maskService.maskChanged;
        const maskedResult = this._maskService.applyMask(inputValue, this._maskService.maskExpression);
        return !maskedResult &&
            inputValue &&
            !wasMaskChanged &&
            this._maskService.removeMask(inputValue)
            ? inputValue
            : maskedResult;
    }
    _validateTime(value) {
        const rowMaskLen = this._maskValue()
            .split(MaskExpression.EMPTY_STRING)
            .filter((s) => s !== ':').length;
        if (!value) {
            return null; // Don't validate empty values to allow for optional form control
        }
        if ((+(value[value.length - 1] ?? -1) === 0 && value.length < rowMaskLen) ||
            value.length <= rowMaskLen - 2) {
            return this._createValidationError(value);
        }
        return null;
    }
    _getActualInputLength() {
        return (this._maskService.actualValue.length ||
            this._maskService.actualValue.length + this._maskService.prefix.length);
    }
    /**
     * For `||` multi-masks only (#1583): a value shorter than the currently selected
     * alternative is still valid when it is a pattern-valid prefix of that alternative
     * ending exactly at a special-character boundary (e.g. `0` for `0,N`) and it meets
     * the length requirement of at least one alternative (e.g. `1`). Values stopping
     * mid-pattern-block (e.g. `112A` for `000SS`) remain invalid.
     */
    _isCompleteAlternativeBoundary(processedValue) {
        const alternatives = this._maskExpressionArray();
        if (!alternatives.length) {
            return false;
        }
        const maskValue = this._maskValue();
        const cleanValue = this._maskService.removeMask(processedValue);
        const cleanMask = this._maskService.removeMask(maskValue);
        const isPatternPrefix = cleanValue
            .split(MaskExpression.EMPTY_STRING)
            .every((character, index) => this._maskService._checkSymbolMask(character, cleanMask.charAt(index)));
        if (!isPatternPrefix) {
            return false;
        }
        let patternCount = 0;
        let boundaryCharacter = MaskExpression.EMPTY_STRING;
        for (const maskCharacter of maskValue) {
            if (patternCount === cleanValue.length) {
                boundaryCharacter = maskCharacter;
                break;
            }
            if (!this._maskService.specialCharacters.includes(maskCharacter)) {
                patternCount++;
            }
        }
        if (!boundaryCharacter ||
            !this._maskService.specialCharacters.includes(boundaryCharacter)) {
            return false;
        }
        return alternatives.some((alternative) => {
            const requiredLength = this._maskService.dropSpecialCharacters
                ? alternative.length - this._maskService.checkDropSpecialCharAmount(alternative)
                : this.prefix()
                    ? alternative.length + this.prefix().length
                    : alternative.length;
            // Exact match, not `>=`: a longer value must not pass on the strength of a
            // shorter alternative it has already outgrown (#1645, 7 digits vs `00000 9`).
            return processedValue.length === requiredLength;
        });
    }
    /**
     * True when every character of the mask expression is either a pattern token or a
     * special character — i.e. the mask has no quantifiers (`*`, `?`), curly-bracket
     * repetitions or other constructs the position-aware matcher does not model.
     */
    _isPlainTokenMask(mask) {
        return mask
            .split(MaskExpression.EMPTY_STRING)
            .every((symbol) => !!this._maskService.patterns[symbol] ||
            this._maskService.specialCharacters.includes(symbol));
    }
    /**
     * Backtracking match of a value against a mask mixing optional and mandatory pattern
     * tokens (#1515, e.g. `999SSS`). Optional tokens may be left unfilled; special
     * characters may be absent from the value (dropSpecialCharacters). The value is valid
     * when it is fully consumed and every remaining mask token is optional or special.
     */
    _matchesMaskWithOptionalSkip(value, mask) {
        const patterns = this._maskService.patterns;
        const match = (maskIndex, valueIndex) => {
            if (valueIndex === value.length) {
                return mask
                    .slice(maskIndex)
                    .split(MaskExpression.EMPTY_STRING)
                    .every((symbol) => !patterns[symbol] || !!patterns[symbol]?.optional);
            }
            if (maskIndex === mask.length) {
                return false;
            }
            const maskSymbol = mask[maskIndex];
            const valueSymbol = value[valueIndex];
            const pattern = patterns[maskSymbol];
            if (pattern) {
                if (pattern.pattern.test(valueSymbol) && match(maskIndex + 1, valueIndex + 1)) {
                    return true;
                }
                return !!pattern.optional && match(maskIndex + 1, valueIndex);
            }
            // Special character: consume it when present in the value, otherwise treat it
            // as dropped (dropSpecialCharacters).
            if (valueSymbol === maskSymbol && match(maskIndex + 1, valueIndex + 1)) {
                return true;
            }
            return match(maskIndex + 1, valueIndex);
        };
        return match(0, 0);
    }
    _createValidationError(actualValue) {
        return {
            mask: {
                requiredMask: this._maskValue(),
                actualValue,
            },
        };
    }
    _setMask() {
        this._maskExpressionArray().some((mask) => {
            const specialChart = mask
                .split(MaskExpression.EMPTY_STRING)
                .some((char) => this._maskService.specialCharacters.includes(char));
            if ((specialChart &&
                this._inputValue() &&
                this._areAllCharactersInEachStringSame(this._maskExpressionArray())) ||
                mask.includes(MaskExpression.CURLY_BRACKETS_LEFT)) {
                const test = this._maskService.removeMask(this._inputValue())?.length <=
                    this._maskService.removeMask(mask)?.length;
                if (test) {
                    const maskValue = mask.includes(MaskExpression.CURLY_BRACKETS_LEFT)
                        ? this._maskService._repeatPatternSymbols(mask)
                        : mask;
                    this._maskValue.set(maskValue);
                    this._maskService.maskExpression = maskValue;
                    return test;
                }
                else {
                    const expression = this._maskExpressionArray()[this._maskExpressionArray().length - 1] ??
                        MaskExpression.EMPTY_STRING;
                    const maskValue = expression.includes(MaskExpression.CURLY_BRACKETS_LEFT)
                        ? this._maskService._repeatPatternSymbols(expression)
                        : expression;
                    this._maskValue.set(maskValue);
                    this._maskService.maskExpression = maskValue;
                }
            }
            else {
                const cleanMask = this._maskService.removeMask(mask);
                const check = this._maskService
                    .removeMask(this._inputValue())
                    ?.split(MaskExpression.EMPTY_STRING)
                    .every((character, index) => {
                    const indexMask = cleanMask.charAt(index);
                    return this._maskService._checkSymbolMask(character, indexMask);
                });
                if (check || this._justPasted()) {
                    this._maskValue.set(mask);
                    this._maskService.maskExpression = mask;
                    return check;
                }
            }
        });
    }
    _areAllCharactersInEachStringSame(array) {
        const specialCharacters = this._maskService.specialCharacters;
        function removeSpecialCharacters(str) {
            const regex = new RegExp(`[${specialCharacters.map((ch) => `\\${ch}`).join('')}]`, 'g');
            return str.replace(regex, '');
        }
        const processedArr = array.map(removeSpecialCharacters);
        return processedArr.every((str) => {
            const uniqueCharacters = new Set(str);
            return uniqueCharacters.size === 1;
        });
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
    static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: NgxMaskDirective, isStandalone: true, selector: "input[mask], textarea[mask]", inputs: { mask: { classPropertyName: "mask", publicName: "mask", isSignal: true, isRequired: false, transformFunction: null }, specialCharacters: { classPropertyName: "specialCharacters", publicName: "specialCharacters", isSignal: true, isRequired: false, transformFunction: null }, patterns: { classPropertyName: "patterns", publicName: "patterns", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, thousandSeparator: { classPropertyName: "thousandSeparator", publicName: "thousandSeparator", isSignal: true, isRequired: false, transformFunction: null }, decimalMarker: { classPropertyName: "decimalMarker", publicName: "decimalMarker", isSignal: true, isRequired: false, transformFunction: null }, dropSpecialCharacters: { classPropertyName: "dropSpecialCharacters", publicName: "dropSpecialCharacters", isSignal: true, isRequired: false, transformFunction: null }, hiddenInput: { classPropertyName: "hiddenInput", publicName: "hiddenInput", isSignal: true, isRequired: false, transformFunction: null }, showMaskTyped: { classPropertyName: "showMaskTyped", publicName: "showMaskTyped", isSignal: true, isRequired: false, transformFunction: null }, placeHolderCharacter: { classPropertyName: "placeHolderCharacter", publicName: "placeHolderCharacter", isSignal: true, isRequired: false, transformFunction: null }, shownMaskExpression: { classPropertyName: "shownMaskExpression", publicName: "shownMaskExpression", isSignal: true, isRequired: false, transformFunction: null }, clearIfNotMatch: { classPropertyName: "clearIfNotMatch", publicName: "clearIfNotMatch", isSignal: true, isRequired: false, transformFunction: null }, validation: { classPropertyName: "validation", publicName: "validation", isSignal: true, isRequired: false, transformFunction: null }, separatorLimit: { classPropertyName: "separatorLimit", publicName: "separatorLimit", isSignal: true, isRequired: false, transformFunction: null }, typeFromDecimals: { classPropertyName: "typeFromDecimals", publicName: "typeFromDecimals", isSignal: true, isRequired: false, transformFunction: null }, allowNegativeNumbers: { classPropertyName: "allowNegativeNumbers", publicName: "allowNegativeNumbers", isSignal: true, isRequired: false, transformFunction: null }, leadZeroDateTime: { classPropertyName: "leadZeroDateTime", publicName: "leadZeroDateTime", isSignal: true, isRequired: false, transformFunction: null }, leadZero: { classPropertyName: "leadZero", publicName: "leadZero", isSignal: true, isRequired: false, transformFunction: null }, triggerOnMaskChange: { classPropertyName: "triggerOnMaskChange", publicName: "triggerOnMaskChange", isSignal: true, isRequired: false, transformFunction: null }, apm: { classPropertyName: "apm", publicName: "apm", isSignal: true, isRequired: false, transformFunction: null }, inputTransformFn: { classPropertyName: "inputTransformFn", publicName: "inputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, outputTransformFn: { classPropertyName: "outputTransformFn", publicName: "outputTransformFn", isSignal: true, isRequired: false, transformFunction: null }, keepCharacterPositions: { classPropertyName: "keepCharacterPositions", publicName: "keepCharacterPositions", isSignal: true, isRequired: false, transformFunction: null }, instantPrefix: { classPropertyName: "instantPrefix", publicName: "instantPrefix", isSignal: true, isRequired: false, transformFunction: null }, defaultValueOnBlur: { classPropertyName: "defaultValueOnBlur", publicName: "defaultValueOnBlur", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", maskFilled: "maskFilled" }, host: { listeners: { "paste": "onPaste()", "focus": "onFocus()", "ngModelChange": "onModelChange($event)", "input": "onInput($event)", "compositionstart": "onCompositionStart()", "compositionend": "onCompositionEnd($event)", "blur": "onBlur($event)", "click": "onClick($event)", "keydown": "onKeyDown($event)" } }, providers: [
            {
                provide: NG_VALUE_ACCESSOR,
                useExisting: NgxMaskDirective,
                multi: true,
            },
            {
                provide: NG_VALIDATORS,
                useExisting: NgxMaskDirective,
                multi: true,
            },
            NgxMaskService,
        ], exportAs: ["mask", "ngxMask"], usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: 'input[mask], textarea[mask]',
                    standalone: true,
                    providers: [
                        {
                            provide: NG_VALUE_ACCESSOR,
                            useExisting: NgxMaskDirective,
                            multi: true,
                        },
                        {
                            provide: NG_VALIDATORS,
                            useExisting: NgxMaskDirective,
                            multi: true,
                        },
                        NgxMaskService,
                    ],
                    exportAs: 'mask,ngxMask',
                }]
        }], ctorParameters: () => [], propDecorators: { mask: [{ type: i0.Input, args: [{ isSignal: true, alias: "mask", required: false }] }], specialCharacters: [{ type: i0.Input, args: [{ isSignal: true, alias: "specialCharacters", required: false }] }], patterns: [{ type: i0.Input, args: [{ isSignal: true, alias: "patterns", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], thousandSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "thousandSeparator", required: false }] }], decimalMarker: [{ type: i0.Input, args: [{ isSignal: true, alias: "decimalMarker", required: false }] }], dropSpecialCharacters: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropSpecialCharacters", required: false }] }], hiddenInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenInput", required: false }] }], showMaskTyped: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMaskTyped", required: false }] }], placeHolderCharacter: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolderCharacter", required: false }] }], shownMaskExpression: [{ type: i0.Input, args: [{ isSignal: true, alias: "shownMaskExpression", required: false }] }], clearIfNotMatch: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearIfNotMatch", required: false }] }], validation: [{ type: i0.Input, args: [{ isSignal: true, alias: "validation", required: false }] }], separatorLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorLimit", required: false }] }], typeFromDecimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeFromDecimals", required: false }] }], allowNegativeNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowNegativeNumbers", required: false }] }], leadZeroDateTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadZeroDateTime", required: false }] }], leadZero: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadZero", required: false }] }], triggerOnMaskChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerOnMaskChange", required: false }] }], apm: [{ type: i0.Input, args: [{ isSignal: true, alias: "apm", required: false }] }], inputTransformFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputTransformFn", required: false }] }], outputTransformFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputTransformFn", required: false }] }], keepCharacterPositions: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepCharacterPositions", required: false }] }], instantPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "instantPrefix", required: false }] }], defaultValueOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValueOnBlur", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], maskFilled: [{ type: i0.Output, args: ["maskFilled"] }], onPaste: [{
                type: HostListener,
                args: ['paste']
            }], onFocus: [{
                type: HostListener,
                args: ['focus']
            }], onModelChange: [{
                type: HostListener,
                args: ['ngModelChange', ['$event']]
            }], onInput: [{
                type: HostListener,
                args: ['input', ['$event']]
            }], onCompositionStart: [{
                type: HostListener,
                args: ['compositionstart']
            }], onCompositionEnd: [{
                type: HostListener,
                args: ['compositionend', ['$event']]
            }], onBlur: [{
                type: HostListener,
                args: ['blur', ['$event']]
            }], onClick: [{
                type: HostListener,
                args: ['click', ['$event']]
            }], onKeyDown: [{
                type: HostListener,
                args: ['keydown', ['$event']]
            }] } });

class NgxMaskPipe {
    defaultOptions = inject(NGX_MASK_CONFIG);
    _maskService = inject(NgxMaskService);
    _maskExpressionArray = [];
    mask = '';
    transform(value, mask, { patterns, maskAliases, ...config } = {}) {
        let processedValue = value;
        // User-defined aliases expand BEFORE any other mask processing (incl. `||` handling).
        const resolvedMask = resolveMaskAlias(mask, {
            ...this.defaultOptions.maskAliases,
            ...maskAliases,
        });
        const currentConfig = {
            maskExpression: resolvedMask,
            ...this.defaultOptions,
            ...config,
            patterns: {
                ...this._maskService.patterns,
                ...patterns,
            },
        };
        Object.entries(currentConfig).forEach(([key, val]) => {
            this._maskService[key] = val;
        });
        if (resolvedMask.includes('||')) {
            const maskParts = resolvedMask.split('||');
            if (maskParts.length > 1) {
                this._maskExpressionArray = maskParts.sort((a, b) => a.length - b.length);
                this._setMask(`${processedValue}`);
                return this._maskService.applyMask(`${processedValue}`, this.mask);
            }
            else {
                this._maskExpressionArray = [];
                return this._maskService.applyMask(`${processedValue}`, this.mask);
            }
        }
        if (resolvedMask.includes(MaskExpression.CURLY_BRACKETS_LEFT)) {
            return this._maskService.applyMask(`${processedValue}`, this._maskService._repeatPatternSymbols(resolvedMask));
        }
        if (resolvedMask.startsWith(MaskExpression.SEPARATOR)) {
            if (config.decimalMarker) {
                this._maskService.decimalMarker = config.decimalMarker;
            }
            if (config.thousandSeparator) {
                this._maskService.thousandSeparator = config.thousandSeparator;
            }
            if (config.leadZero) {
                this._maskService.leadZero = config.leadZero;
            }
            processedValue = String(processedValue);
            const localeDecimalMarker = this._maskService.currentLocaleDecimalMarker();
            if (!Array.isArray(this._maskService.decimalMarker)) {
                processedValue =
                    this._maskService.decimalMarker !== localeDecimalMarker
                        ? processedValue.replace(localeDecimalMarker, this._maskService.decimalMarker)
                        : processedValue;
            }
            if (this._maskService.leadZero &&
                processedValue &&
                this._maskService.dropSpecialCharacters !== false) {
                processedValue = this._maskService._checkPrecision(resolvedMask, processedValue);
            }
            if (this._maskService.decimalMarker === MaskExpression.COMMA) {
                processedValue = processedValue.replace(MaskExpression.DOT, MaskExpression.COMMA);
            }
            this._maskService.isNumberValue = true;
        }
        if (processedValue === null || typeof processedValue === 'undefined') {
            return this._maskService.applyMask('', resolvedMask);
        }
        return this._maskService.applyMask(`${processedValue}`, resolvedMask);
    }
    _setMask(value) {
        if (this._maskExpressionArray.length > 0) {
            this._maskExpressionArray.some((mask) => {
                const test = this._maskService.removeMask(value)?.length <=
                    this._maskService.removeMask(mask)?.length;
                if (value && test) {
                    this.mask = mask;
                    return test;
                }
                else {
                    this.mask =
                        this._maskExpressionArray[this._maskExpressionArray.length - 1] ??
                            MaskExpression.EMPTY_STRING;
                }
            });
        }
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
    static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskPipe, isStandalone: true, name: "mask" });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: NgxMaskPipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'mask',
                    pure: true,
                    standalone: true,
                }]
        }] });

/**
 * Generated bundle index. Do not edit.
 */

export { INITIAL_CONFIG, NEW_CONFIG, NGX_MASK_CONFIG, NgxMaskDirective, NgxMaskPipe, NgxMaskService, initialConfig, provideEnvironmentNgxMask, provideNgxMask, resolveMaskAlias, timeMasks, withoutValidation };
//# sourceMappingURL=ngx-mask.mjs.map