Undo/Redo функциональность является критически важной при разработке
интерактивных интерфейсов, позволяя пользователям откатывать и повторять
действия с элементами. В сочетании с Interact.js это
становится возможным благодаря отслеживанию событий drag,
resize и других интерактивных операций. Основная идея
заключается в том, чтобы сохранять историю изменений состояния
элементов и управлять этим стеком при выполнении действий Undo
и Redo.
Interact.js предоставляет богатый набор событий:
dragstart — начало перетаскивания элементаdragmove — движение элементаdragend — завершение перетаскиванияresizestart, resizemove,
resizeend — аналогично для изменения размеровДля реализации Undo/Redo необходимо фиксировать ключевые
состояния элемента при завершении действий
(dragend, resizeend). Пример структуры
состояния элемента:
const elementState = {
id: 'box1',
x: 100,
y: 50,
width: 200,
height: 150
};
Сохраняются только существенные изменения, чтобы стек истории оставался компактным.
Для Undo/Redo обычно используют два стека:
undoStack — хранит последовательность предыдущих
состоянийredoStack — хранит состояния, которые были отменены и
могут быть восстановленыПри каждом завершении интерактивного действия:
function saveState(element) {
const state = {
id: element.id,
x: parseFloat(element.style.left),
y: parseFloat(element.style.top),
width: element.offsetWidth,
height: element.offsetHeight
};
undoStack.push(state);
// Очистка redoStack, если было новое действие
redoStack.length = 0;
}
Это гарантирует, что после любого нового действия возможность Redo сбрасывается.
Функции Undo и Redo применяют последнее состояние из соответствующего стека к элементу:
function undo() {
if (undoStack.length === 0) return;
const lastState = undoStack.pop();
redoStack.push(getCurrentState(lastState.id));
applyState(lastState);
}
function redo() {
if (redoStack.length === 0) return;
const state = redoStack.pop();
undoStack.push(getCurrentState(state.id));
applyState(state);
}
Где applyState изменяет свойства DOM-элемента:
function applyState(state) {
const el = document.getElementById(state.id);
el.style.left = state.x + 'px';
el.style.top = state.y + 'px';
el.style.width = state.width + 'px';
el.style.height = state.height + 'px';
}
Функция getCurrentState возвращает текущее состояние
элемента перед его заменой.
Interact.js позволяет отслеживать завершение действий и автоматически сохранять состояние:
interact('.draggable')
.draggable({
listeners: {
move(event) {
const target = event.target;
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
},
end(event) {
saveState(event.target);
}
}
});
interact('.resizable')
.resizable({
edges: { left: true, right: true, bottom: true, top: true },
listeners: {
move(event) {
let target = event.target;
target.style.width = event.rect.width + 'px';
target.style.height = event.rect.height + 'px';
target.style.transform = `translate(${event.deltaRect.left}px, ${event.deltaRect.top}px)`;
},
end(event) {
saveState(event.target);
}
}
});
Таким образом, любое завершённое перетаскивание или изменение размера автоматически фиксируется в Undo-стеке.
Для крупных интерфейсов с множеством интерактивных элементов важно оптимизировать хранение состояния:
Пример группировки изменений по таймеру:
let saveTimeout;
interact('.draggable')
.draggable({
listeners: {
move(event) { /* движение */ },
end(event) {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => saveState(event.target), 100);
}
}
});
Если интерфейс позволяет работать с несколькими элементами одновременно, необходимо хранить массив состояний:
const multiState = elements.map(el => getCurrentState(el.id));
undoStack.push(multiState);
При Undo/Redo применяется каждый объект состояния к соответствующему элементу.
Undo/Redo обычно связываются с кнопками или сочетаниями клавиш:
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'z') undo();
if (e.ctrlKey && e.key === 'y') redo();
});
Interact.js в этом случае не вмешивается в управление, а лишь обеспечивает точные данные состояния элементов.
transform: translate() позволяет плавно
перемещать элементы без перерисовки всего layout.draggable, .resizable {
transition: transform 0.2s, width 0.2s, height 0.2s;
}
undoStack и redoStackdragend и
resizeendundo() и redo(), использующие
стекиapplyState()Эта структура обеспечивает надёжную и масштабируемую систему Undo/Redo для интерактивных интерфейсов с помощью Interact.js в JavaScript.