Undo/Redo функциональность

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 операции

Функции 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.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-стеке.

Оптимизация истории

Для крупных интерфейсов с множеством интерактивных элементов важно оптимизировать хранение состояния:

  • Сохранять только изменённые свойства, а не полное состояние DOM
  • Группировать мелкие движения в одно состояние, используя таймеры или порог изменения координат
  • Лимитировать глубину стека, например, хранить последние 50–100 действий

Пример группировки изменений по таймеру:

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
  • Для Undo/Redo сохраняются именно координаты и размеры, а не сами трансформации, что упрощает восстановление состояния
  • Можно добавить плавные CSS-переходы при откате действий для лучшего UX
.draggable, .resizable {
  transition: transform 0.2s, width 0.2s, height 0.2s;
}

Итоговая структура

  1. Стек undoStack и redoStack
  2. Фиксация состояния элемента при dragend и resizeend
  3. Функции undo() и redo(), использующие стеки
  4. Применение состояния к элементу через applyState()
  5. Интеграция с Interact.js для событий движения и изменения размера
  6. Оптимизация истории (сжатие изменений, лимит стека)
  7. Поддержка нескольких элементов и интерфейса с клавишами/кнопками

Эта структура обеспечивает надёжную и масштабируемую систему Undo/Redo для интерактивных интерфейсов с помощью Interact.js в JavaScript.