下面是 AI 写的一个自定义的撤销和重做功能,代码很好理解:

class TextUndoRedoManager {
  constructor(textareaElement) {
    this.textarea = textareaElement;
    this.undoValueStack = []; // 撤销栈
    this.redoValueStack = []; // 重做栈
    this.currentValue = textareaElement.value;
    this.undoStartStack = []; // 撤销栈
    this.redoStartStack = []; // 重做栈
    this.currentStart = textareaElement.selectionStart;
    this.undoEndStack = []; // 撤销栈
    this.redoEndStack = []; // 重做栈
    this.currentEnd = textareaElement.selectionEnd;
    

    // 监听输入事件,记录状态变化
    this.textarea.addEventListener('input', () => {
      this.recordState();
    });
  }

  // 记录当前状态到撤销栈
  recordState() {
    // 将当前状态推入撤销栈
    this.undoValueStack.push(this.currentValue);
    this.undoStartStack.push(this.currentStart);
    this.undoEndStack.push(this.currentEnd);
//    console.log(this.undoValueStack, this.undoStartStack, this.undoEndStack)
    // 更新当前状态为文本区域的最新值
    this.currentValue = this.textarea.value;
    this.currentStart = this.textarea.selectionStart;
    this.currentEnd = this.textarea.selectionEnd;
//    console.log(this.currentValue, this.currentStart, this.currentEnd)
    // 清空重做栈(因为有了新的操作)
    this.redoValueStack = [];
    this.redoStartStack = [];
    this.redoEndStack = [];
  }

  // 设置文本,并记录状态
  setValue(newValue, newStart, newEnd) {
    // 先将当前状态记录一次
    this.recordState();
    // 更新文本区域的值
    this.textarea.value = newValue;
    this.textarea.selectionStart = newStart;
    this.textarea.selectionEnd = newEnd;
    this.currentValue = newValue;
    this.currentStart = newStart;
    this.currentEnd = newEnd;
  }

  // 撤销
  undo() {
    if (this.undoValueStack.length > 0 && this.undoStartStack.length > 0 && this.undoEndStack.length > 0) {
      // 将当前状态移入重做栈
      this.redoValueStack.push(this.currentValue);
      this.redoStartStack.push(this.currentStart);
      this.redoEndStack.push(this.currentEnd);
      // 从撤销栈弹出上一个状态
      const previousValue = this.undoValueStack.pop();
      const previousStart = this.undoStartStack.pop();
      const previousEnd = this.undoEndStack.pop();
      this.textarea.value = previousValue;
      this.textarea.selectionStart = previousStart;
      this.textarea.selectionEnd = previousEnd;
      this.currentValue = previousValue;
      this.currentStart = previousStart;
      this.currentEnd = previousEnd;
      // 可选:触发一个自定义事件,通知其他部分状态改变了
      this.textarea.dispatchEvent(new Event('change'));
    }
  }

  // 重做
  redo() {
    if (this.redoValueStack.length > 0 && this.redoStartStack.length > 0 && this.redoEndStack.length > 0) {
      this.undoValueStack.push(this.currentValue);
      this.undoStartStack.push(this.currentStart);
      this.undoEndStack.push(this.currentEnd);
      const nextValue = this.redoValueStack.pop();
      const nextStart = this.redoStartStack.pop();
      const nextEnd = this.redoEndStack.pop();
      this.textarea.value = nextValue;
      this.textarea.selectionStart = nextStart;
      this.textarea.selectionEnd = nextEnd;
      this.currentValue = nextValue;
      this.currentStart = nextStart;
      this.currentEnd = nextEnd;
      this.textarea.dispatchEvent(new Event('change'));
    }
  }
}

使用方法如下:

// 首先使用 textarea 元素实例化该类,这样 textarea 在被修改时就会自动将历史加入 undoManager 中
const undoManager = new TextUndoRedoManager(messageInput);

// 监听 textarea 的键盘事件,响应 ctrl + z 等输入,并在其中调用对应的 undoManager 方法
messageInput.addEventListener('keydown', function(e) {
    if (e.ctrlKey) {
        if (e.key === 'Enter') {
            // ctrl + enter 提交消息
            e.preventDefault();
            submitMessage();
        } else if (e.key === "z") {
            // ctrl + z 撤销
            e.preventDefault();
            undoManager.undo();
        } else if (e.key === "y") {
            // ctrl + y 重做
            e.preventDefault();
            undoManager.redo();
        }
    } else {
        if (e.key === 'Tab') {
            // Tab 键补全
            e.preventDefault();
            handleTabCompletion();
        }
    }
    // 单独的 Enter 键保持默认行为(换行)
});

// 以后 JS 脚本中修改 textarea 时都通过 undoManager.setValue() 来修改,就可以将界面手工输入和脚本输入都加入撤销和重做的历史中,大功告成!
undoManager.setValue(cplMessage, cplOffset, cplOffset)

以上代码没有记录手工移动光标的历史,这个需要根据具体需求来。