Fabric.js Document Engine

搜索文档

查找页面或章节

EN

撤销和重做

查看 Markdown

每个用户操作一步撤销,也包括你的代码做的改动。

Fabric.js 本身没有撤销功能。用这个包,创建引擎后 Fabric.js 撤销重做就已经开启。每个用户操作是一步,你只要调用 undo() 和 redo(),就能回到上一步或下一步。

正在加载在线示例,代码在下方。

undo-redo-demo.tsx
"use client";
 
import { bindKeyboardShortcuts } from "fabricjs-document-engine";
import {
  useDocumentEngine,
  useDocumentState,
} from "fabricjs-document-engine/react";
import {
  CircleIcon,
  PaletteIcon,
  Redo2Icon,
  SquareIcon,
  Undo2Icon,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef } from "react";
 
import {
  COLORS,
  DemoCanvas,
  DemoFrame,
  newShape,
  seedShapes,
  Status,
  Toolbar,
  ToolButton,
} from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
 
export default function UndoRedoDemo() {
  const t = useTranslations("demos");
  const { elementRef, canvas } = useFabricCanvas();
  const engine = useDocumentEngine(canvas);
  const state = useDocumentState(engine);
  const demoRef = useRef<HTMLDivElement>(null);
 
  // Start from a small drawing, with an empty history.
  useEffect(() => {
    if (!engine) return;
    engine.canvas.add(...seedShapes());
    engine.clearHistory();
  }, [engine]);
 
  // Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z, while focus is inside this demo.
  useEffect(() => {
    if (!engine || !demoRef.current) return;
    return bindKeyboardShortcuts(engine, { target: demoRef.current });
  }, [engine]);
 
  function recolor() {
    const target = canvas?.getActiveObject() ?? canvas?.getObjects().at(-1);
    if (!engine || !target) return;
 
    const next =
      COLORS[(COLORS.indexOf(String(target.fill)) + 1) % COLORS.length];
    // Fabric fires no event for `set`, so record it as one labelled step.
    engine.transaction("Recolor", () => target.set("fill", next));
    canvas?.requestRenderAll();
  }
 
  return (
    <DemoFrame ref={demoRef} tabIndex={-1}>
      <DemoCanvas elementRef={elementRef} label={t("canvas")} />
 
      <Toolbar>
        <ToolButton
          icon={SquareIcon}
          label={t("square")}
          onClick={() => canvas?.add(newShape("square"))}
        />
        <ToolButton
          icon={CircleIcon}
          label={t("circle")}
          onClick={() => canvas?.add(newShape("circle"))}
        />
        <ToolButton icon={PaletteIcon} label={t("recolor")} onClick={recolor} />
        <span className="ml-auto flex gap-1.5">
          <ToolButton
            icon={Undo2Icon}
            label={t("undo")}
            iconOnly
            disabled={!state?.canUndo}
            onClick={() => void engine?.undo()}
          />
          <ToolButton
            icon={Redo2Icon}
            label={t("redo")}
            iconOnly
            disabled={!state?.canRedo}
            onClick={() => void engine?.redo()}
          />
        </span>
      </Toolbar>
 
      <Status tone={state?.canUndo ? "ok" : "idle"}>
        {state?.canUndo
          ? t("undoRedo.steps", {
              count: engine?.getHistory().undo.length ?? 0,
            })
          : t("undoRedo.empty")}
      </Status>
    </DemoFrame>
  );
}

自动记录的内容

引擎监听 Fabric.js 事件,自动记录:

  • 添加和删除对象(同一时刻的多个改动合并为一步);
  • 用鼠标移动、缩放和旋转(Fabric 的 object:modified);
  • 完成的文本编辑。

每一步都有一个标签,比如 Add rect 或 Move circle,可以显示在按钮上。

await engine.undo();
await engine.redo();
 
engine.canUndo();
engine.getHistory(); // { undo: ["Move circle", "Add rect"], redo: [] }

调用会按顺序执行,快速按三次撤销,就会撤销三步。

代码做的改动

object.set("fill", "red") 或 canvas.bringObjectForward(object) 不会触发任何 Fabric.js 事件。把这类改动包在事务里,它们就成为带标签的一步:

engine.transaction("Arrange furniture", () => {
  chair.set({ left: 120, top: 80 });
  table.set("fill", "oak");
  canvas.bringObjectToFront(table);
});

或者先改,再调用 commit:

canvas.sendObjectBackwards(rug);
engine.commit("Send rug backwards");
  • 事务可以嵌套,使用最外层的标签。
  • 事务可以是异步的:await engine.transaction("Import", async () => { /* ... */ })。
  • 编组和取消编组是对对象列表的改动。把移除和添加放在同一个事务里,就是一步撤销。

键盘快捷键

import { bindKeyboardShortcuts } from "fabricjs-document-engine";
 
const unbind = bindKeyboardShortcuts(engine);

Ctrl+Z 或 Cmd+Z 撤销。Ctrl+Shift+Z、Cmd+Shift+Z 和 Ctrl+Y 重做。用户在输入框、文本域、contenteditable 元素或 Fabric.js 文本里打字时,快捷键会被忽略,原生的文本撤销照常工作。传入 { target: element } 可以只在某个元素上监听,而不是 window。

工具栏状态

engine.on("history:change", ({ canUndo, canRedo, undoLabel }) => {
  undoButton.disabled = !canUndo;
  redoButton.disabled = !canRedo;
  undoButton.textContent = undoLabel ? `Undo ${undoLabel}` : "Undo";
});

保存 id,而不是对象

撤销和重做会根据保存的状态重建改动过的对象。它们会以新的 Fabric.js 实例回来,id 不变。保存 id,再重新查找对象:

const id = shape.get("id") as string;
await engine.undo();
const current = engine.getObjectById(id);

历史记录的大小

每一步只存改动过的对象,所以大图里的一步也很小。默认情况下 Fabric.js 撤销重做的历史记录保留 100 步,最多 64 MB,超出时先丢最旧的步骤:

createDocumentEngine({
  canvas,
  history: { limit: 50, maxBytes: 32 * 1024 * 1024 },
});

撤销只覆盖本次会话里最近的编辑。想长期保留某个状态,请用版本。