Работа с 12-часовым форматом времени требует не только ограничения диапазона часов, но и корректного управления суффиксами AM и PM, которые определяют половину суток. В Cleave.js отсутствует отдельный встроенный режим полноценной поддержки AM/PM как единого временного типа, поэтому реализация строится через комбинацию масок, ограничений диапазона и обработчиков событий ввода.
Основная задача при построении AM/PM-паттернов — разделить ввод на логические компоненты: часы, минуты и маркер половины суток, а затем обеспечить их синхронизацию.
12-часовой формат предполагает диапазон часов от 1 до 12, при этом минутная часть ограничивается значениями 00–59. В Cleave.js это реализуется через числовую маску с контролем диапазонов.
const timeInput = new Cleave('.time-input', {
numeral: true,
numeralIntegerScale: 2,
numeralDecimalScale: 0,
numeralPositiveOnly: true,
stripLeadingZeroes: false
});
Однако числовой режим сам по себе не решает задачу разделения времени на компоненты. Он используется как базовый слой, поверх которого строится логика форматирования.
Для формирования структурированного ввода применяется механизм
blocks, позволяющий разбить строку на сегменты
фиксированной длины.
const ampmTime = new Cleave('.time-input', {
blocks: [2, 2, 2],
delimiters: [':', ' '],
numericOnly: true
});
Такая конфигурация задаёт структуру:
Однако Cleave.js не интерпретирует содержимое третьего блока как временной маркер, поэтому необходим дополнительный уровень обработки.
Ключевой момент при работе с AM/PM — принудительное ограничение
диапазона часов. Это реализуется через обработчик
onValueChanged.
const ampmTime = new Cleave('.time-input', {
blocks: [2, 2, 2],
delimiters: [':', ' '],
numericOnly: true,
onValueChanged: function (e) {
let value = e.target.value;
let parts = value.split(/[:\s]/);
let hours = parseInt(parts[0], 10);
if (!isNaN(hours)) {
if (hours === 0) hours = 1;
if (hours > 12) hours = 12;
}
parts[0] = String(hours).padStart(2, '0');
e.target.value = parts.join(' ').trim();
}
});
Здесь реализуется ключевой принцип:
AM/PM не является числовым значением, поэтому обычно выносится в отдельное состояние, не связанное напрямую с маской ввода.
let period = 'AM';
const togglePeriod = () => {
period = period === 'AM' ? 'PM' : 'AM';
};
В интерфейсных реализациях переключение может происходить через кнопку или автоматическую логику (например, при выходе за границу 12:00).
После разделения логики времени и периода необходимо формировать единое значение. Cleave.js работает только с отображением ввода, поэтому итоговая строка формируется вручную.
function formatTime(value, period) {
let [hours, minutes] = value.split(':');
hours = hours.padStart(2, '0');
minutes = minutes.padStart(2, '0');
return `${hours}:${minutes} ${period}`;
}
Такой подход позволяет сохранять визуальную маску Cleave.js и одновременно управлять бизнес-логикой отдельно.
Минутная часть требует отдельной валидации, аналогичной часовой, но с диапазоном 00–59.
onValueChanged: function (e) {
let value = e.target.value;
let parts = value.split(':');
let hours = parseInt(parts[0], 10);
let minutes = parseInt(parts[1], 10);
if (!isNaN(hours)) {
hours = Math.min(Math.max(hours, 1), 12);
}
if (!isNaN(minutes)) {
minutes = Math.min(Math.max(minutes, 0), 59);
}
e.target.value =
String(hours).padStart(2, '0') + ':' +
String(minutes).padStart(2, '0');
}
Такой механизм устраняет некорректные состояния ввода ещё на этапе
набора, предотвращая появление значений вроде 19:88.
В ряде интерфейсов требуется, чтобы AM/PM был частью той же строки, что и время. В таком случае применяется псевдо-блокировка третьего сегмента.
const ampmInput = new Cleave('.time-input', {
blocks: [2, 2, 2],
delimiters: [':', ' '],
uppercase: true,
onValueChanged: function (e) {
let value = e.target.value.toUpperCase();
let match = value.match(/^(\d{2}):(\d{2})\s?(AM|PM)?$/);
if (!match) return;
let hours = parseInt(match[1], 10);
let minutes = parseInt(match[2], 10);
let period = match[3] || 'AM';
hours = Math.min(Math.max(hours, 1), 12);
minutes = Math.min(Math.max(minutes, 0), 59);
e.target.value =
`${String(hours).padStart(2, '0')}:` +
`${String(minutes).padStart(2, '0')} ` +
period;
}
});
Такой подход позволяет поддерживать целостную строку времени, не вынося AM/PM в отдельное состояние.
При некоторых сценариях интерфейса AM/PM вычисляется автоматически на основе введённых часов.
function detectPeriod(hours) {
return hours >= 12 ? 'PM' : 'AM';
}
Дополнительная нормализация 12-часового формата:
function to12HourFormat(hours) {
const period = hours >= 12 ? 'PM' : 'AM';
const normalized = hours % 12 || 12;
return { hours: normalized, period };
}
Cleave.js управляет только отображением, поэтому синхронизация с внешней моделью данных является обязательной частью архитектуры.
const state = {
hours: 0,
minutes: 0,
period: 'AM'
};
function syncFromInput(value) {
const [h, m, p] = value.split(/[:\s]/);
state.hours = parseInt(h, 10);
state.minutes = parseInt(m, 10);
state.period = p || state.period;
}
Такой слой позволяет отделить UI-форматирование от внутреннего представления времени.
При ручном вводе часто возникают промежуточные некорректные состояния, например:
1:12:713:99 PMДля стабилизации используется стратегия мягкой коррекции:
function sanitizeTime(h, m) {
if (h < 1) h = 1;
if (h > 12) h = 12;
if (m < 0) m = 0;
if (m > 59) m = 59;
return { h, m };
}
Этот слой применяется после каждого изменения значения.
Наиболее устойчивый паттерн строится как композиция трёх уровней:
const time = new Cleave('.time-input', {
delimiters: [':', ' '],
blocks: [2, 2, 2],
onValueChanged: function (e) {
let value = e.target.value.toUpperCase();
let match = value.split(/[:\s]/);
let hours = parseInt(match[0], 10);
let minutes = parseInt(match[1], 10);
let period = match[2] || 'AM';
({ h: hours, m: minutes } = sanitizeTime(hours, minutes));
e.target.value =
`${String(hours).padStart(2, '0')}:` +
`${String(minutes).padStart(2, '0')} ${period}`;
}
});
При построении интерфейсов с Cleave.js важно учитывать ограничения:
В результате AM/PM в Cleave.js всегда представляет собой надстройку над базовой маской, а не встроенный тип данных