Механизм отмены и повторного выполнения изменений в числовых полях с автоформатированием строится вокруг хранения последовательности состояний значения и управления переходами между ними. В контексте AutoNumeric подобная функциональность обычно реализуется на уровне обёртки над стандартным input-элементом, поскольку сама библиотека ориентирована прежде всего на форматирование и валидацию числового ввода, а не на управление историей изменений.
Основой системы является стек состояний. Каждое значимое изменение значения фиксируется как отдельный снимок. Такой снимок включает не только отображаемое значение, но и его нормализованную форму.
Типичная структура элемента истории:
Поддерживаются два независимых стека:
При каждом новом изменении стек повтора очищается, что соответствует классической модели undo/redo.
AutoNumeric обрабатывает ввод через перехват событий
input, keydown, paste. Однако
фиксация состояния должна быть отделена от каждого микроввода, иначе
стек быстро переполняется и теряет смысловую целостность.
Используется стратегия «дебаунса состояния»:
Пример логики фиксации:
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-стек.
Алгоритм:
Ключевой момент — подавление повторной фиксации при программном изменении значения.
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 предоставляет набор событий (например, изменения значения, форматирование, обновление). Исторический механизм подключается к наиболее стабильной точке — событию изменения «пользовательского ввода».
Типовая схема:
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. Более сложный — пересчёт позиции с учётом изменения длины строки при форматировании.
При различии длины строк применяется эвристика:
Для предотвращения утечек памяти стек ограничивается фиксированным размером.
Типичная стратегия:
function limitStack(stack, maxSize) {
if (stack.length > maxSize) {
stack.shift();
}
}
Изменения, инициированные программно (например, set()),
не всегда должны попадать в историю. Для этого вводится классификация
источников изменений:
История фиксируется только для первых двух категорий.
Особое внимание требуется следующим ситуациям:
В таких случаях возможно принудительное сбрасывание истории для предотвращения неконсистентного состояния.
При интенсивном вводе важно минимизировать накладные расходы:
При корректной реализации система undo/redo остаётся линейной по сложности операций O(1) для push/pop.