import type {MaskInputOptions} from 'maska' // Un champ numérique du gabarit : sa longueur et la plage de valeurs autorisée. interface Field { length: number min: number max: number } // Découpe un gabarit (ex. `JJ/MM/AAAA HH:MM`) en champs numériques bornés. // Le `M` désigne le mois avant les heures, et les minutes après — d'où le suivi // de `seenHour`, pour ne pas borner les minutes comme un mois (0-59 vs 1-12). function parseFields(template: string): Field[] { const fields: Field[] = [] let seenHour = false let i = 0 while (i < template.length) { const ch = template[i]! if (!/[A-Za-z]/.test(ch)) { i++ // séparateur (/, espace, :) continue } let j = i while (j < template.length && template[j] === ch) j++ const length = j - i const letter = ch.toUpperCase() if (letter === 'H') seenHour = true if (letter === 'J') fields.push({length, min: 1, max: 31}) else if (letter === 'M') fields.push(seenHour ? {length, min: 0, max: 59} : {length, min: 1, max: 12}) else if (letter === 'H') fields.push({length, min: 0, max: 23}) else fields.push({length, min: 0, max: 10 ** length - 1}) // année (ou autre) : libre i = j } return fields } // Un chiffre est accepté tant qu'il existe encore une complétion valide du champ : // on borne la valeur partielle [min possible (padding 0), max possible (padding 9)] // et on vérifie qu'elle croise la plage autorisée [field.min, field.max]. function canComplete(partial: string, field: Field): boolean { const low = Number(partial.padEnd(field.length, '0')) const high = Number(partial.padEnd(field.length, '9')) return high >= field.min && low <= field.max } // Ne conserve que les chiffres qui gardent chaque champ complétable, et s'arrête // au premier chiffre invalide (rien de ce qui suit n'est réinterprété). maska // réinsère ensuite les séparateurs via le masque structurel. function clampDigits(rawDigits: string, fields: Field[]): string { let result = '' let di = 0 for (const field of fields) { let fieldDigits = '' while (fieldDigits.length < field.length) { if (di >= rawDigits.length) return result + fieldDigits // plus de saisie const candidate = fieldDigits + rawDigits[di] if (!canComplete(candidate, field)) return result + fieldDigits // 1er chiffre invalide → stop fieldDigits = candidate di++ } result += fieldDigits } return result } /** * Construit les options maska d'un champ date/heure à partir d'un gabarit * d'affichage (ex. `JJ/MM/AAAA`, `JJ/MM/AAAA HH:MM`). * * - `mask` : masque structurel (chiffres + séparateurs), pour le formatage/eager. * - `preProcess` : borne la saisie AVANT masquage, sur le 1er **et** le 2e chiffre * de chaque champ (jour 1-31, mois 1-12, heure 0-23, minute 0-59), si bien * qu'une valeur impossible (99/99/9999, 33, 19 en mois…) ne peut pas être tapée. * Les impossibilités calendaires fines (31/02, 29/02 non bissextile) et les * bornes `min`/`max` restent du ressort de la validation, en filet. */ export function buildBoundedMask(template: string): Pick { const mask = template.replace(/[A-Za-z]/g, '#') const fields = parseFields(template) return { mask, preProcess: (value: string) => clampDigits(value.replace(/\D/g, ''), fields), } }