Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-17-12-51-47.md
admin b822bb1b47 fix: custom undo/redo stack and cursor positioning in TemplateManage
- Add undoStack/redoStack refs with pushHistory/handleUndo/handleRedo
- Replace execCmd('undo')/execCmd('redo') with custom stack handlers
- Call pushHistory before structural changes (delete, insert field/table/image, formatting)
- Add onMouseDown preventDefault to toolbar and field library buttons to stop focus loss
- Implement saveSelection/restoreSelection using savedRangeRef
- Bind onBlur/onMouseUp/onKeyUp on editor to persist cursor position
- Restore selection in insertSmartField and insertImage before insertHTML
- Update experience record (#20)
2026-04-17 13:18:54 +08:00

3.9 KiB
Raw Blame History

实现方案 — TemplateManage 撤销/重做修复与插入字段光标定位2026-04-17-12-51-47

一、修改文件清单

  1. src/pages/TemplateManage.tsx — 核心改动:自定义 undo/redo 栈 + 光标保存恢复 + 阻止焦点流失

二、详细改动

2.1 自定义 Undo/Redo 栈

A. 新增 Refs

在组件顶部增加:

const undoStack = useRef<string[]>([]);
const redoStack = useRef<string[]>([]);

B. pushHistory 函数

在执行任何会改变编辑器内容的操作前调用,将当前 innerHTML 推入 undo 栈,并清空 redo 栈:

const pushHistory = () => {
  if (!editorRef.current) return;
  undoStack.current.push(editorRef.current.innerHTML);
  redoStack.current = [];
};

C. handleUndo / handleRedo

替换原来调用的 execCmd('undo') / execCmd('redo')

const handleUndo = () => {
  if (undoStack.current.length === 0 || !editorRef.current) return;
  redoStack.current.push(editorRef.current.innerHTML);
  const prev = undoStack.current.pop();
  if (prev !== undefined) {
    editorRef.current.innerHTML = prev;
    saveTemplateContent();
  }
};

const handleRedo = () => {
  if (redoStack.current.length === 0 || !editorRef.current) return;
  undoStack.current.push(editorRef.current.innerHTML);
  const next = redoStack.current.pop();
  if (next !== undefined) {
    editorRef.current.innerHTML = next;
    saveTemplateContent();
  }
};

D. 埋点位置

在以下操作执行前调用 pushHistory()

  • handleEditorClick 中删除 smart field 之前
  • handleKeyDown 中删除 smart field 之前
  • insertSmartField 执行 insertHTML 之前
  • insertTable 执行 insertHTML 之前
  • insertImage 执行 insertHTML 之前
  • 工具栏按钮(粗体/斜体/下划线/对齐/颜色/字体等)操作前

注意:为了不过度累积历史记录,键盘输入不需要每次按键都 pushHistory浏览器原生 undo 可以处理普通文本输入。我们的自定义栈主要负责保护“结构性变更”(插入/删除字段、表格、图片等)。

2.2 阻止焦点流失 + 恢复光标位置

A. 阻止焦点流失

在字段库按钮和工具栏按钮上增加 onMouseDown={(e) => e.preventDefault()},这是最简洁有效的办法:

<button
  type="button"
  onMouseDown={(e) => e.preventDefault()}
  onClick={() => insertSmartField(field)}
  ...
>

B. 保存/恢复光标位置

利用已有的 savedRangeRef

const saveSelection = () => {
  const sel = window.getSelection();
  if (sel && sel.rangeCount > 0) {
    savedRangeRef.current = sel.getRangeAt(0);
  }
};

const restoreSelection = () => {
  if (!savedRangeRef.current) return;
  const sel = window.getSelection();
  sel?.removeAllRanges();
  sel?.addRange(savedRangeRef.current);
};

在编辑器 <div> 上绑定事件:

<div
  ref={editorRef}
  contentEditable
  className="..."
  onBlur={saveSelection}
  onMouseUp={saveSelection}
  onKeyUp={saveSelection}
>

insertSmartField 中恢复光标:

const insertSmartField = (field: FormField) => {
  editorRef.current?.focus();
  restoreSelection();
  // ... 唯一性校验 + insertHTML
};

2.3 工具栏按钮改造

所有工具栏按钮undo/redo 除外)增加 onMouseDown={(e) => e.preventDefault()},并在 onClick 中先 pushHistory() 再执行命令。Undo/Redo 按钮不需要 preventDefault,因为它们本身不需要保持编辑器焦点,但也可以加上统一处理。

三、风险与回滚

  • 风险:自定义 undo/redo 栈会占用少量内存(存储 HTML 字符串),但模板内容通常不大,几十步历史记录的内存开销可忽略。
  • 风险onMouseDown={e => e.preventDefault()} 会阻止按钮的默认按下行为,但不会影响 onClick 的触发,这是 React 中阻止焦点流失的标准做法。
  • 回滚:如出现问题,可回退 TemplateManage.tsx 的修改。