Recently, I have developed a visual operation platform, which can be revoked or redone after user operation. I searched some solutions on the Internet to improve the solutions I envisaged.

Record the gist of the requirements

  • It can be stored in localStorage
  • Undo or redo multiple times
  • Click on an item in the list to rewind or advance history to the specified location

Seemingly simple requirements, mistakes in infrastructure design, will lead to more work in the future. Therefore, combined with the above two requirements, it is found that the basic idea of VUEX is very suitable for completing this requirement, and redux is the same.

Implementation approach

This project uses typescript to enhance the rigor of the code to facilitate future maintenance.

1. Define the historical data structure

interface HistoryItem {
  timestrap: number; // Record the timestamp
  name: string; // Record the name
  redo: string; / / redo Mutation
  undo: string; / / cancellation Mutation
  redoParams: any[]; // do the Mutation submission parameter
  undoParams: any[]; // undo the Mutation submission parameter
}

interface HistoryStatus {
  historys: HistoryItem[]; // Record the history array
  _currentHistory: number; // Index of the current node
}
Copy the code

2. Write the History status module

Write the vuex module for the history state of the base operation, and create actions for Mutation, redo, and undo records

A record contains redo and undo operations for this step. So when the user clicks on an item in the list, it should loop back to the previous undo or redo to the current item

Therefore, it is necessary to add an empty record, so that users can click the empty record to undo the initial operation.

Use the Vuex-module-decorators to write more maintainable code

import { VuexModule, Module, Mutation, Action } from "vuex-module-decorators";

@Module({ namespaced: true })
export class HistoryModule extends VuexModule<HistoryStatus> implements HistoryStatus {
  /** * The main reason for initializing an empty record is to facilitate list operations: * When the user clicks on the earliest record, the first step of the user operation can be normally undone **/
  public historys: HistoryItem[] = [
    {
      name: Open ` `.timestrap: Date.now(),
      redo: "".redoParams: [].undo: "".undoParams: []},];public _currentHistory: number = 0;

  // getter
  get current() {return this._currentHistory;
  }

  // getter
  get historyList() :HistoryItem[] {
    return this.historys || [];
  }

  // Create a history
  @Mutation
  public CREATE_HISTORY(payload: HistoryItem) {
    if (this._currentHistory < this.historys.length - 1) {
      this.historys = this.historys.slice(0.this._currentHistory);
    }
    // Because of the deep copy problem of JS, we need to make a deep copy of the data when creating
    // Try lodash's clone function, but find that json. stringify's clone function should be faster, after all, our data does not exist function
    // I will not change, mainly to express the idea
    this.historys.push(_.cloneDeep(payload));
    this._currentHistory = this.historys.length - 1;
  }

  @Mutation
  public SET_CURRENT_HISTORY(index: number) {
    this._currentHistory = index < 0 ? 0 : index;
  }

  / / redo
  @Action
  public RedoHistory(times: number = 1) {
    let { state, commit } = this.context;
    let historys: HistoryItem[] = state.historys;
    let current: number = state._currentHistory;
    if (current + times >= historys.length) return;
    while (times > 0) {
      current++;
      let history = historys[current];
      if(history) { commit(history.redo, ... history.redoParams, {root: true });
      }
      times--;
    }
    commit("SET_CURRENT_HISTORY", current);
  }

  / / cancel
  @Action
  public UndoHistory(times: number = 1) {
    let { state, commit } = this.context;
    let historys: HistoryItem[] = state.historys;
    let current: number = state._currentHistory;
    if (current - times < 0) return;
    while (times > 0) {
      let history = historys[current];
      if(history) { commit(history.undo, ... history.undoParams, {root: true });
      }
      times--;
      current--;
    }
    commit("SET_CURRENT_HISTORY", current); }}Copy the code

3. Write features that can be undone or redone

After completing the above two steps, we are ready to write the various operations

  1. Write operations on the data baseMutation
@Mutation
public CREATE_PAGE(payload: { page: PageItem; index: number }) {
  this.pages.splice(payload.index, 0, _.cloneDeep(payload.page));
  this._currentPage = this.pages.length - 1;
}

@Mutation
public REMOVE_PAGE(id: string) {
  let index = this.pages.findIndex((p) = > p.id == id);
  index > -1 && this.pages.splice(index, 1);
  if (this._currentPage == index) {
    this._currentPage = this.pages.length > 0 ? 0 : -1; }}Copy the code
  1. The basic operation is packaged into a tape to save -> record -> execute as requiredAction
// Wrap the page creation function
@Action
public CreatePage(type: "page" | "dialog") {
  let { state, commit } = this.context;
  
  // Record the page to be created
  let id = _.uniqueId(type) + Date.now();
  let pageName = pageType[type];
  let page: PageItem = {
    id,
    name: `${pageName}${state.pages.length + 1}`.type.layers: [].style: { width: 720.height: 1280}};// Create a history
  let history: HistoryItem = {
    name: ` create${pageName}`.timestrap: Date.now(),
    redo: "Page/CREATE_PAGE".redoParams: [{ index: state.pages.length - 1, page }],
    undo: "Page/REMOVE_PAGE".undoParams: [id],
  };
  // Save this historical record
  commit("Histroy/CREATE_HISTORY", history, { root: true}); commit(history.redo, ... history.redoParams, {root: true });
}
Copy the code
@Action
public RemovePage(id: string) {
  // Record the field status
  let index = this.pages.findIndex((p) = > p.id == id);
  if (index < 0) return;
  let page: PageItem = this.context.state.pages[index];

  // Create a history
  let history: HistoryItem = {
    name: ` delete${page.name}`.timestrap: Date.now(),
    redo: "Page/REMOVE_PAGE".redoParams: [id],
    undo: "Page/CREATE_PAGE".undoParams: [{ page, index }],
  };

  // Save this historical record
  this.context.commit("Histroy/CREATE_HISTORY", history, { root: true });
  this.context.commit(history.redo, ... history.redoParams, {root: true });
}
Copy the code

Above, undo and redo functions are basically complete

Use 4.

1. We now only use typescript private create(type: "page" | "dialog") { this.$store.dispatch("Page/CreatePage", type); } private remove(id: number) { this.$store.dispatch("Page/RemovePage", id); } 2. Configure the global hotkey typescript app.vue... private mounted() { let self = this; hotkeys("ctrl+z", function (event, handler) { self.$store.dispatch("History/UndoHistory"); }); hotkeys("ctrl+y", function (event, handler) { self.$store.dispatch("History/RedoHistory"); }); }... ` ` `Copy the code

The effect