Паттерны AM/PM

Работа с 12-часовым форматом времени требует не только ограничения диапазона часов, но и корректного управления суффиксами AM и PM, которые определяют половину суток. В Cleave.js отсутствует отдельный встроенный режим полноценной поддержки AM/PM как единого временного типа, поэтому реализация строится через комбинацию масок, ограничений диапазона и обработчиков событий ввода.

Основная задача при построении AM/PM-паттернов — разделить ввод на логические компоненты: часы, минуты и маркер половины суток, а затем обеспечить их синхронизацию.


Базовая структура 12-часового ввода

12-часовой формат предполагает диапазон часов от 1 до 12, при этом минутная часть ограничивается значениями 00–59. В Cleave.js это реализуется через числовую маску с контролем диапазонов.

const timeInput = new Cleave('.time-input', {
    numeral: true,
    numeralIntegerScale: 2,
    numeralDecimalScale: 0,
    numeralPositiveOnly: true,
    stripLeadingZeroes: false
});

Однако числовой режим сам по себе не решает задачу разделения времени на компоненты. Он используется как базовый слой, поверх которого строится логика форматирования.


Разделение компонентов времени через blocks

Для формирования структурированного ввода применяется механизм blocks, позволяющий разбить строку на сегменты фиксированной длины.

const ampmTime = new Cleave('.time-input', {
    blocks: [2, 2, 2],
    delimiters: [':', ' '],
    numericOnly: true
});

Такая конфигурация задаёт структуру:

  • блок 1 — часы
  • блок 2 — минуты
  • блок 3 — маркер AM/PM (вводится вручную или нормализуется)

Однако Cleave.js не интерпретирует содержимое третьего блока как временной маркер, поэтому необходим дополнительный уровень обработки.


Нормализация часов в диапазон 1–12

Ключевой момент при работе с 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();
    }
});

Здесь реализуется ключевой принцип:

  • любые вводимые значения автоматически приводятся к допустимому диапазону
  • 0 трансформируется в 01
  • значения выше 12 фиксируются на 12

Управление AM и PM как отдельным состоянием

AM/PM не является числовым значением, поэтому обычно выносится в отдельное состояние, не связанное напрямую с маской ввода.

let period = 'AM';

const togglePeriod = () => {
    period = period === 'AM' ? 'PM' : 'AM';
};

В интерфейсных реализациях переключение может происходить через кнопку или автоматическую логику (например, при выходе за границу 12:00).


Интеграция AM/PM в строковое представление

После разделения логики времени и периода необходимо формировать единое значение. 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 внутри одной строки

В ряде интерфейсов требуется, чтобы 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 по значению часов

При некоторых сценариях интерфейса AM/PM вычисляется автоматически на основе введённых часов.

function detectPeriod(hours) {
    return hours >= 12 ? 'PM' : 'AM';
}

Дополнительная нормализация 12-часового формата:

  • 00 часов интерпретируется как 12 AM
  • 12 часов остаётся 12 PM
  • 13–23 преобразуются в 1–11 PM
function to12HourFormat(hours) {
    const period = hours >= 12 ? 'PM' : 'AM';
    const normalized = hours % 12 || 12;

    return { hours: normalized, period };
}

Синхронизация Cleave.js с внешним состоянием

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:7
  • 13: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 };
}

Этот слой применяется после каждого изменения значения.


Комбинированный паттерн AM/PM с Cleave.js

Наиболее устойчивый паттерн строится как композиция трёх уровней:

  1. Cleave.js — управление маской и структурой ввода
  2. onValueChanged — нормализация значений
  3. внешняя модель — хранение AM/PM и логики преобразования
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}`;
    }
});

Особенности проектирования AM/PM-форматов в масках ввода

При построении интерфейсов с Cleave.js важно учитывать ограничения:

  • маска не интерпретирует смысл AM/PM как времени суток
  • блоки фиксированной длины не подходят для динамических значений AM/PM без обработки
  • логика преобразования всегда выносится в обработчики событий

В результате AM/PM в Cleave.js всегда представляет собой надстройку над базовой маской, а не встроенный тип данных