fabricjs / fabric.js

Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
http://fabricjs.com
Other
29.2k stars 3.52k forks source link

[Feature]: history manager w/ eventListener restoring between history version #10241

Open shpaw415 opened 1 month ago

shpaw415 commented 1 month ago

CheckList

Description

a class that manage the history that manage prev and next and restore the eventListener state that was on the element

type fabricObjectWithID = fabric.FabricObject & { __id__?: string };

class HistoryManager {
  canvas: fabric.Canvas;
  history: any[] = [];
  restoreEffects: Array<{
    __id__: string;
    callback?: (el: fabricObjectWithID) => any;
  }> = [];
  defaultEffects: Array<(el: fabric.FabricObject) => any> = [];
  currentIndex: number = 0;
  maxHistoryLen = 10 as const;
  preventPush = false;

  constructor(canvas: fabric.Canvas) {
    this.canvas = canvas;

    this.canvas.on("object:modified", () => this.push());
    this.canvas.on("object:added", () => this.push());
    this.canvas.on("object:removed", () => this.push());

    this.history.push(this.canvas.toObject());
  }

  add(
    el: fabricObjectWithID,
    restoreEffect?: (el: fabric.FabricObject | any) => any
  ) {
    const __id__ = this.randomUUID();
    el.__id__ = __id__;
    this.canvas.add(el);
    this.restoreEffects.push({
      __id__,
      callback: restoreEffect,
    });
  }

  remove(el: fabricObjectWithID) {
    this.canvas.remove(el);
  }

  prev() {
    if (this.currentIndex == 0) return;
    this.currentIndex--;
    this.renderWithEventListener();
  }

  next() {
    if (this.currentIndex >= this.history.length - 1) return;
    this.currentIndex++;
    this.renderWithEventListener();
  }

  async renderWithEventListener() {
    this.preventPush = true;
    const c = await this.canvas.loadFromJSON(this.GetnewData());
    c.requestRenderAll();
    for await (const obj of c.getObjects() as Array<fabricObjectWithID>) {
      this.preventPush = true;
      await this.restoreEffects
        .find((e) => e.__id__ == obj.__id__)
        ?.callback?.(obj);
    }
    this.preventPush = false;
  }

  /**
   * restore from a past session with defaultEffects
   * @param history HistoryManager.history
   */
  restore(history: any[], setToHystoryIndex = -1) {
    this.history = history;
    this.canvas
      .loadFromJSON(this.history.at(setToHystoryIndex))
      .then((canvas) => {
        canvas.requestRenderAll();
        for (const obj of canvas.getObjects())
          for (const callback of this.defaultEffects) callback(obj);
      });
  }
  /**
   *
   * @param props callbacks when HistoryManager.restore is called
   */
  addDefaulteffects(...props: Array<(el: fabric.FabricObject) => any>) {
    this.defaultEffects.push(...props);
  }

  private push() {
    if (this.preventPush) return;
    if (this.history.length > this.currentIndex + 1)
      this.history.splice(this.currentIndex + 2);

    if (this.history.length >= this.maxHistoryLen) this.history.shift();
    else this.currentIndex++;

    this.history.push(this.canvas.toObject(["__id__"]));
  }

  private GetnewData() {
    const data = this.history.at(this.currentIndex);
    if (!data) throw new Error("no history found");
    return data;
  }

  randomUUID() {
    const makeRand = () => Math.random().toString(36).slice(2, -1);
    return `${makeRand()}-${makeRand()}-${makeRand()}-${makeRand()}`.toUpperCase();
  }
}

Current State

no history management currently

Additional Context

that will make devX much more interesting

asturur commented 1 month ago

Every application has its own rules and goals. Whoever wants to write an history manager for fabricJS can do that and can open a repo for it For what regards fabricJS per se history management is out of scope, fabricJS is a rendering library with an interactive layer. Is not a white label app with undo/redo