Отмена и повтор операций

Механизм отмены и повторного выполнения изменений в числовых полях с автоформатированием строится вокруг хранения последовательности состояний значения и управления переходами между ними. В контексте AutoNumeric подобная функциональность обычно реализуется на уровне обёртки над стандартным input-элементом, поскольку сама библиотека ориентирована прежде всего на форматирование и валидацию числового ввода, а не на управление историей изменений.

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

Типичная структура элемента истории:

  • текущее форматированное значение (строка)
  • числовое значение (number или decimal-строка)
  • позиция курсора (selection start / end)
  • дополнительные метаданные (например, тип события: ввод, вставка, программное изменение)

Поддерживаются два независимых стека:

  • стек отмены (undo stack)
  • стек повтора (redo stack)

При каждом новом изменении стек повтора очищается, что соответствует классической модели undo/redo.

Фиксация изменений при вводе

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

Используется стратегия «дебаунса состояния»:

  • изменения группируются в логические транзакции
  • фиксация происходит после стабилизации ввода (например, 300–500 мс без новых событий)
  • либо при потере фокуса input-поля

Пример логики фиксации:

function pushState(instance, input) {
  const state = {
    formatted: input.value,
    numeric: instance.getNumericString(),
    selection: {
      start: input.selectionStart,
      end: input.selectionEnd
    }
  };

  instance.history.undo.push(state);
  instance.history.redo.length = 0;
}

Операция отмены

Отмена представляет собой возврат к предыдущему состоянию из undo-стека с переносом текущего состояния в redo-стек.

Алгоритм:

  1. извлекается последний элемент undo
  2. текущий state переносится в redo
  3. состояние применяется к input без генерации нового history-события

Ключевой момент — подавление повторной фиксации при программном изменении значения.

function undo(instance, input) {
  const undoStack = instance.history.undo;
  const redoStack = instance.history.redo;

  if (undoStack.length === 0) return;

  const current = {
    formatted: input.value,
    numeric: instance.getNumericString()
  };

  const previous = undoStack.pop();
  redoStack.push(current);

  applyState(instance, input, previous, { silent: true });
}

Операция повтора

Повтор (redo) симметричен отмене и использует стек redo.

function redo(instance, input) {
  const undoStack = instance.history.undo;
  const redoStack = instance.history.redo;

  if (redoStack.length === 0) return;

  const current = {
    formatted: input.value,
    numeric: instance.getNumericString()
  };

  const next = redoStack.pop();
  undoStack.push(current);

  applyState(instance, input, next, { silent: true });
}

Применение состояния без генерации событий

Критически важным аспектом является применение состояния без повторного триггера механизмов фиксации истории. Для этого вводится режим подавления событий.

function applyState(instance, input, state, options) {
  instance._suspendHistory = options?.silent === true;

  instance.set(state.numeric);

  input.value = state.formatted;

  if (state.selection) {
    input.setSelectionRange(state.selection.start, state.selection.end);
  }

  instance._suspendHistory = false;
}

Внутренние обработчики событий обязаны учитывать флаг _suspendHistory и пропускать фиксацию состояния при его активном значении.

Интеграция с AutoNumeric событиями

AutoNumeric предоставляет набор событий (например, изменения значения, форматирование, обновление). Исторический механизм подключается к наиболее стабильной точке — событию изменения «пользовательского ввода».

Типовая схема:

  • onInput → потенциальное изменение
  • onChange → фиксация устойчивого состояния
  • onBlur → финализация транзакции

При этом непосредственная фиксация в undo-стек выполняется только при переходе в устойчивое состояние.

Группировка операций ввода

Последовательный ввод символов не должен создавать отдельное состояние на каждый символ. Для этого используется буферизация.

Упрощённая модель:

  • старт таймера при первом изменении
  • сброс таймера при каждом новом вводе
  • фиксация при истечении интервала
let timer = null;

function scheduleState(instance, input) {
  clearTimeout(timer);

  timer = setTimeout(() => {
    if (!instance._suspendHistory) {
      pushState(instance, input);
    }
  }, 400);
}

Особенности работы с форматированием

AutoNumeric постоянно преобразует отображаемое значение (разделители тысяч, десятичные символы, валютные символы). Это создаёт дополнительную сложность:

  • визуальное значение ≠ числовое значение
  • курсор может смещаться при форматировании
  • восстановление позиции требует пересчёта

Поэтому каждый snapshot должен учитывать как минимум две формы представления значения.

Восстановление позиции курсора

При отмене и повторе важно восстановить корректное положение каретки. Простейший вариант — сохранение индексов selectionStart/selectionEnd. Более сложный — пересчёт позиции с учётом изменения длины строки при форматировании.

При различии длины строк применяется эвристика:

  • привязка к числовым символам
  • игнорирование разделителей
  • пересчёт позиции относительно «чистого» значения

Ограничение размера истории

Для предотвращения утечек памяти стек ограничивается фиксированным размером.

Типичная стратегия:

  • максимум 50–100 состояний
  • при превышении удаляется самый старый элемент
function limitStack(stack, maxSize) {
  if (stack.length > maxSize) {
    stack.shift();
  }
}

Программные изменения и исключения из истории

Изменения, инициированные программно (например, set()), не всегда должны попадать в историю. Для этого вводится классификация источников изменений:

  • user input
  • paste event
  • programmatic update

История фиксируется только для первых двух категорий.

Обработка граничных случаев

Особое внимание требуется следующим ситуациям:

  • вставка больших чисел с форматированием
  • удаление всего содержимого поля
  • смена локали (изменение разделителей)
  • переключение конфигурации AutoNumeric во время активной истории

В таких случаях возможно принудительное сбрасывание истории для предотвращения неконсистентного состояния.

Производительность

При интенсивном вводе важно минимизировать накладные расходы:

  • хранение только строк и примитивов
  • отсутствие глубокого клонирования DOM
  • использование кольцевого буфера вместо динамического массива (в высоконагруженных сценариях)
  • дебаунс фиксации состояния

При корректной реализации система undo/redo остаётся линейной по сложности операций O(1) для push/pop.