Skip to content

编辑器,如何按操作进行设计 undoredo? #123

@pfan123

Description

@pfan123

设计一个支持撤销(Undo)和重做(Redo)功能的编辑器是提高用户体验的重要部分。以下是一个基础的概念框架来实现这些功能,包括操作记录、状态管理和相关的实现策略。

1. 操作记录模型

  • 操作类型:
    • 每个操作可以被定义为一个对象,包含操作类型、目标状态和相关数据。
class Action {
    constructor(type, data) {
        this.type = type;  // 操作类型,例如 "ADD", "REMOVE", "UPDATE"
        this.data = data;  // 操作相关的数据,例如变更的内容
    }
}
  • 栈数据结构:
    • 使用两个栈来存储用户的操作记录:
      • Undo Stack: 存储需要撤销的操作。
      • Redo Stack: 存储已撤销的操作,以便可以重做。

2. 基本操作

  • 执行操作:
    • 当用户进行某个操作时,将操作记录推入 undo stack,并清空 redo stack
let undoStack = [];
let redoStack = [];

function performAction(action) {
    // 执行操作
    applyAction(action);

    // 压入撤销栈
    undoStack.push(action);
    redoStack = []; // 清空重做栈
}
  • 撤销操作:
    • undo stack 中弹出最后一个操作,并执行相应的撤销逻辑。将撤销的操作推入 redo stack
function undo() {
    if (undoStack.length === 0) return;

    const action = undoStack.pop();
    reverseAction(action);
    redoStack.push(action);
}

function reverseAction(action) {
    switch (action.type) {
        case 'ADD':
            // 撤销添加:从当前状态中删除最后添加的元素
            currentState.content.pop();
            break;
        case 'REMOVE':
            // 撤销删除:将被删除的元素重新插入
            const removedData = action.data; // 被删除的数据
            currentState.content.splice(removedData.index, 0, removedData.value); // 插入被删除的元素
            break;
        case 'UPDATE':
            // 撤销更新:恢复为旧值,需要在更新时保存旧值
            const indexToRestore = action.data.index; // 取得要恢复的索引
            currentState.content[indexToRestore] = action.data.oldValue; // 恢复为旧值
            break;
    }
}
  • 重做操作:
    • redo stack 中弹出最后一个操作,并重复前面的执行逻辑,将其压入 undo stack
function redo() {
    if (redoStack.length === 0) return;

    const action = redoStack.pop();
    performAction(action);
}

3. 数据结构

  • 状态管理:
    • 定期保存编辑器的状态(例如,文本、对象、图形等)。每个操作都应该修改当前状态,并可以在需要时恢复到先前的状态。
let textContent = []; // 用于存储文本内容的数组

// 当前编辑状态
let currentState = {
    content: textContent
};

function applyAction(action) {
    switch (action.type) {
        case 'ADD':
            currentState.content.push(action.data); // 在数组末尾添加元素
            break;
        case 'REMOVE':
            const indexToRemove = action.data.index; // 取得要删除的索引
            if (indexToRemove > -1) {
                currentState.content.splice(indexToRemove, 1); // 删除指定索引的元素
            }
            break;
        case 'UPDATE':
            const indexToUpdate = action.data.index; // 取得要更新的索引
            if (indexToUpdate > -1) {
                currentState.content[indexToUpdate] = action.data.newValue; // 更新指定索引的元素
            }
            break;
    }
}

4. 用户界面

  • 界面反馈:
    • 提供明确的用户界面元素(如按钮、菜单)来让用户进行撤销和重做操作,并在操作后更新按钮的可用性(即,当没有更多操作可以撤销或重做时,禁用相应的按钮)。

5. 复杂操作

  • 组合操作:
    • 如果用户在一个操作中进行了多个动作(例如,选择多个文本并更改样式),可以将这些操作封装为一个复合操作,进行单次撤销和重做。

完整示例

let undoStack = []
let redoStack = []
let textContent = []

interface IAction {
  type: 'ADD' | 'REMOVE' | 'UPDATE'
  data: {
    index?: number
    newValue?: string
    oldValue?: string
  } | string 
}

// 执行操作
function performAction(action: IAction) {
  applyAction(action)
  undoStack.push(action)
  redoStack = [] // 当执行新操作时,清空重做堆栈
}

function applyAction(action : IAction) {
  switch (action.type) {
    case 'ADD':
      // 处理新增操作
      textContent.push(action.data)
      break;
    case 'REMOVE':
      // 处理删除操作 
      const indexToRemove = action.data.index
      if (indexToRemove > -1) {
        action.data.oldValue = textContent[indexToRemove]
        textContent.splice(indexToRemove, 1)
      }
      break;
    case 'UPDATE':
      const indexToUpdate = action.data.index
      if (indexToUpdate > -1) {
        action.data.oldValue = textContent[indexToUpdate] // 保存旧值以便撤销
        textContent[indexToUpdate] = action.data.newValue
      }
      break;
  }
}

// 撤销操作
function reverseAction(action: IAction) {
  switch (action.type) {
    case 'ADD':
      // 撤销新增操作
      textContent.pop()
      break;
    case 'REMOVE':
      // 撤销删除操作
      const removeData = action.data
      textContent.splice(removeData.index, 0, removeData.oldValue)
      break;
    case 'UPDATE':
      const indexToUpdate = action.data.index
      if (indexToUpdate > -1) {
        textContent[indexToUpdate] = action.data.oldValue
      }
      break;
  }
} 

// 撤销操作
function undo() {
  if (undoStack.length === 0) return
  const action = undoStack.pop()
  reverseAction(action)
  redoStack.push(action)
}

// 重做操作
function redo() {
  if (redoStack.length === 0) return
  const action = redoStack.pop()
  performAction(action)
}

// 示例操作
performAction({ type: 'ADD', data: 'Hello' });
performAction({ type: 'ADD', data: 'World' });
performAction({ type: 'UPDATE', data: { index: 1, newValue: 'Everyone' } });
performAction({ type: 'REMOVE', data: { index: 0 } }); 

console.log(textContent); // ["Everyone"]
undo(); // 撤销删除
console.log(textContent); // ["Hello", "Everyone"]
redo(); // 重做删除
console.log(textContent); // ["Everyone"]

小结

通过维护撤销和重做的栈数据结构、定义操作模型、记录当前状态和提供 UI 反馈,可以有效地实现编辑器中的撤销/重做功能。将这些机制结合在一起,可以创建一个用户友好的编辑体验,方便用户管理和恢复他们的操作。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions