const DISPLAY_DATE_PATTERN = /^(\d{2})\.(\d{2})\.(\d{4})$/; const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/; const boundDateInputs = new WeakSet(); export function formatDateInputValue(value) { const text = String(value || ''); const isoMatch = text.match(ISO_DATE_PATTERN); if (isoMatch) { return `${isoMatch[3]}.${isoMatch[2]}.${isoMatch[1]}`; } const digits = text.replace(/\D/g, '').slice(0, 8); if (digits.length <= 2) return digits; if (digits.length <= 4) return `${digits.slice(0, 2)}.${digits.slice(2)}`; return `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`; } export function dateInputValueToIso(value) { const text = String(value || '').trim(); if (!text) return ''; const match = text.match(DISPLAY_DATE_PATTERN); if (!match) { throw new TypeError('Введите дату полностью в формате ДД.ММ.ГГГГ (год — 4 цифры)'); } const [, dayText, monthText, yearText] = match; const day = Number(dayText); const month = Number(monthText); const year = Number(yearText); if (year < 1 || month < 1 || month > 12 || day < 1 || day > daysInMonth(month, year)) { throw new TypeError('Введите существующую дату в формате ДД.ММ.ГГГГ'); } return `${yearText}-${monthText}-${dayText}`; } export function isoDateToInputValue(value) { const text = String(value || '').trim(); if (!text) return ''; const match = text.match(ISO_DATE_PATTERN); if (!match) { throw new TypeError('Ожидалась дата в формате ГГГГ-ММ-ДД'); } const [, year, month, day] = match; dateInputValueToIso(`${day}.${month}.${year}`); return `${day}.${month}.${year}`; } export function bindDateInputMask(input) { if (!input || boundDateInputs.has(input)) return; boundDateInputs.add(input); input.addEventListener('input', () => { const rawValue = input.value; const caret = input.selectionStart ?? rawValue.length; const digitsBeforeCaret = rawValue.slice(0, caret).replace(/\D/g, '').length; const caretWasAtEnd = caret === rawValue.length; const formattedValue = formatDateInputValue(rawValue); input.value = formattedValue; clearDateInputError(input); const nextCaret = caretPositionAfterDigits(formattedValue, digitsBeforeCaret, caretWasAtEnd); input.setSelectionRange?.(nextCaret, nextCaret); }); input.addEventListener('keydown', event => { if (event.key === 'Backspace' && input.selectionStart === input.selectionEnd && input.selectionStart > 0 && input.value[input.selectionStart - 1] === '.') { event.preventDefault(); const nextCaret = input.selectionStart - 1; input.setSelectionRange(nextCaret, nextCaret); } if (event.key === 'Delete' && input.selectionStart === input.selectionEnd && input.value[input.selectionStart] === '.') { event.preventDefault(); const nextCaret = input.selectionStart + 1; input.setSelectionRange(nextCaret, nextCaret); } }); input.addEventListener('blur', () => { if (!input.value) { clearDateInputError(input); return; } try { const isoValue = dateInputValueToIso(input.value); input.value = isoDateToInputValue(isoValue); clearDateInputError(input); } catch (error) { setDateInputError(input, error.message); } }); input.form?.addEventListener('reset', () => clearDateInputError(input)); } export function readDateInputIso(input, label = 'Дата') { try { const value = dateInputValueToIso(input?.value); clearDateInputError(input); return value; } catch (error) { const message = `${label}: ${lowercaseFirst(error.message)}`; setDateInputError(input, message); input?.focus(); throw new Error(message); } } export function setDateInputIso(input, value) { if (!input) return; input.value = isoDateToInputValue(value); clearDateInputError(input); } function daysInMonth(month, year) { const monthLengths = [ 31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; return monthLengths[month - 1] || 0; } function isLeapYear(year) { return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } function caretPositionAfterDigits(value, digitCount, preferEnd) { if (digitCount <= 0) return 0; let seenDigits = 0; for (let index = 0; index < value.length; index += 1) { if (!/\d/.test(value[index])) continue; seenDigits += 1; if (seenDigits === digitCount) { let position = index + 1; if (preferEnd) { while (value[position] === '.') position += 1; } return position; } } return value.length; } function setDateInputError(input, message) { if (!input) return; input.setCustomValidity?.(message); input.setAttribute?.('aria-invalid', 'true'); } function clearDateInputError(input) { if (!input) return; input.setCustomValidity?.(''); input.removeAttribute?.('aria-invalid'); } function lowercaseFirst(value) { const text = String(value || ''); return text ? text[0].toLowerCase() + text.slice(1) : text; }