комит
This commit is contained in:
180
frontend/admin/js/date-input.js
Normal file
180
frontend/admin/js/date-input.js
Normal file
@@ -0,0 +1,180 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user