Skip to content

Commit

Permalink
feat: 实现History插件
Browse files Browse the repository at this point in the history
  • Loading branch information
JessYan0913 committed Jul 19, 2023
1 parent a9209f9 commit 6de07c0
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 45 deletions.
1 change: 1 addition & 0 deletions main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@pictode/core": "workspace:^0.0.1",
"@pictode/plugin-history": "workspace:^0.0.1",
"@pictode/utils": "workspace:^0.0.1",
"crypto-js": "^4.1.1",
"element-plus": "^2.2.28",
Expand Down
36 changes: 4 additions & 32 deletions main/src/view/canvas/index.vue
Original file line number Diff line number Diff line change
@@ -1,42 +1,14 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { Context, Plugin } from '@pictode/core';
import { Context } from '@pictode/core';
import { History } from '@pictode/plugin-history';
const containerRef = ref<HTMLDivElement>();
class MyPlugin implements Plugin {
public name: string = 'my-plugin';
constructor() {
console.log('=====>');
}
public install(context: Context) {
console.log('context', context);
}
public dispose() {
console.log('卸载');
}
public zoom() {
console.log('zoom');
}
}
// TODO: 插件开发参考Antv/X6
declare module '@pictode/core' {
interface Context {
zoom: () => void;
}
}
Context.prototype.zoom = () => {};
const history = new History();
const context = new Context();
const myPlugin = new MyPlugin();
context.use(myPlugin);
context.zoom();
context.use(history);
onMounted(() => {
if (containerRef.value) {
context.mount(containerRef.value);
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-history/src/api.ts

This file was deleted.

40 changes: 40 additions & 0 deletions packages/plugin-history/src/commands/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Context } from '@pictode/core';

import { Cmd, Command } from '../types';

export abstract class BaseCmd<T extends Cmd.Options = Cmd.Options> implements Command<T> {
public context?: Context;
public name: string;
public id: number = 0;
public executed: boolean = false;
public options?: T;
public executeTime: number = new Date().getTime();

constructor(context?: Context, options?: T) {
this.context = context;
this.name = this.constructor.name;
this.options = options;
}

public abstract execute(): void;

public abstract undo(): void;

public toJSON(): Command<T> {
return {
id: this.id,
name: this.name,
options: this.options,
executed: this.executed,
executeTime: this.executeTime,
};
}

public fromJSON(json: Command<T>): void {
this.id = json.id;
this.name = json.name;
this.options = json.options;
this.executed = json.executed;
this.executeTime = json.executeTime;
}
}
15 changes: 15 additions & 0 deletions packages/plugin-history/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BaseError } from '@pictode/core';

import { BaseCmd } from '../commands/base';

export class CmdNotRegisterError extends BaseError {
constructor(command: BaseCmd | string) {
super(`${command instanceof BaseCmd ? command.name : command} not registered`);
}
}

export class CmdNotOptionsError extends BaseError {
constructor(command: BaseCmd | string) {
super(`The options of the ${command instanceof BaseCmd ? command.name : command} command cannot be undefine`);
}
}
193 changes: 185 additions & 8 deletions packages/plugin-history/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,210 @@
import { BaseService, Context, Plugin } from '@pictode/core';
import { BaseService, Context, isSubclass, Plugin } from '@pictode/core';

import './api';
import { BaseCmd } from './commands/base';
import { CmdNotOptionsError, CmdNotRegisterError } from './errors';
import { Cmd, EventArgs, Options } from './types';

export class History extends BaseService implements Plugin {
export type CommandClass<T extends BaseCmd = BaseCmd, O extends Cmd.Options = Cmd.Options> = new (
context?: Context,
options?: O
) => T;

export class History extends BaseService<EventArgs> implements Plugin {
public name: string = 'history';
private context?: Context;
private enabled: boolean;
private stackSize: number;

private commands: Record<string, CommandClass> = {};
private undoStack: BaseCmd[] = [];
private redoStack: BaseCmd[] = [];
private idCounter: number = 0;

constructor() {
constructor(options?: Options) {
super();
const { enabled = true, stackSize = 500 } = options ?? {};
this.enabled = enabled;
this.stackSize = stackSize;
}

public setStackSize(size: number): void {
this.stackSize = size;
}

public registerCommands<T extends BaseCmd>(commandClasses: CommandClass<T> | Array<CommandClass<T>>): void {
if (!Array.isArray(commandClasses)) {
commandClasses = [commandClasses];
}
commandClasses.forEach((commandClass) => {
if (isSubclass(commandClass, BaseCmd)) {
this.commands[commandClass.name] = commandClass;
}
});
}

public getCommandClass(command: BaseCmd | string): CommandClass {
let result: CommandClass;
if (command instanceof BaseCmd) {
result = this.commands[command.name];
} else {
result = this.commands[command];
}
if (!result) {
throw new CmdNotRegisterError(command);
}
return result;
}

public execute<T extends Cmd.Options>(command: BaseCmd | string, options?: T): void {
let executeCommand: BaseCmd;
const Command = this.getCommandClass(command);
if (command instanceof BaseCmd) {
executeCommand = command;
} else {
if (!options) {
throw new CmdNotOptionsError(command);
}
executeCommand = new Command(this.context, options);
}

// 如果命令栈中的命令长度已经超出了最大栈长,则将最早的命令清除
if (this.undoStack.length > this.stackSize) {
this.undoStack.shift();
}

this.undoStack.push(executeCommand);
executeCommand.id = ++this.idCounter;

executeCommand.execute();
executeCommand.executed = true;
executeCommand.executeTime = new Date().getTime();
this.redoStack = [];
this.emit('stack:changed', {
undoStack: this.undoStack,
redoStack: this.redoStack,
});
}

public undo(step: number = 1): BaseCmd | undefined {
if (!this.enabled) {
return;
}

let command: BaseCmd | undefined;
while (step) {
if (this.undoStack.length > 0) {
command = this.undoStack.pop();

if (command) {
command.undo();
this.redoStack.push(command);
this.emit('stack:changed', {
undoStack: this.undoStack,
redoStack: this.redoStack,
});
}
}
--step;
}
this.emit('history:undo', {
step,
command: command?.toJSON(),
});
return command;
}

public redo(step: number = 1): BaseCmd | undefined {
if (!this.enabled) {
return;
}
let command: BaseCmd | undefined;
while (step) {
if (this.redoStack.length > 0) {
command = this.redoStack.pop();
if (command) {
command.execute();
this.undoStack.push(command);
this.emit('stack:changed', {
undoStack: this.undoStack,
redoStack: this.redoStack,
});
}
}
--step;
}
this.emit('history:redo', {
command: command?.toJSON(),
step,
});
return command;
}

public canUndo(): boolean {
return this.undoStack.length > 0;
}

public canRedo(): boolean {
return this.redoStack.length > 0;
}

public jump(id: number): void {
if (!this.enabled) {
return;
}

let command: BaseCmd | undefined =
this.undoStack.length > 0 ? this.undoStack[this.undoStack.length - 1] : undefined;

if (command === undefined || id > command.id) {
command = this.redo();

while (command !== undefined && id > command.id) {
command = this.redo();
}
} else {
// eslint-disable-next-line no-constant-condition
while (true) {
command = this.undoStack[this.undoStack.length - 1];

if (command === undefined || id === command.id) {
break;
}
this.undo();
}
}

this.emit('stack:changed', {
undoStack: this.undoStack,
redoStack: this.redoStack,
});
}

public install(context: Context) {
this.context = context;
}

public dispose(): void {
throw new Error('Method not implemented.');
this.undoStack = [];
this.redoStack = [];
this.emit('stack:changed', {
undoStack: this.undoStack,
redoStack: this.redoStack,
});
this.emit('history:destroy', {
history: this,
});
}

public enable(): void {
throw new Error('Method not implemented.');
this.enabled = true;
}

public disable(): void {
throw new Error('Method not implemented.');
this.enabled = false;
}

public isEnabled(): boolean {
throw new Error('Method not implemented.');
return this.enabled ?? false;
}
}

Expand Down
38 changes: 38 additions & 0 deletions packages/plugin-history/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
import { History } from './index';
export interface PluginMethods {}

export namespace Cmd {
export interface Options {
[key: string]: any;
}
}

export interface Options {
enabled?: boolean;
stackSize?: number;
}

export interface Command<T extends Cmd.Options = Cmd.Options> {
id: number;
name: string;
options?: T;
executed: boolean;
executeTime: number;
}

export interface CmdStack {
undoStack: Command[];
redoStack: Command[];
}

export interface EventArgs {
'stack:changed': CmdStack;
'history:destroy': {
history: History;
};
'history:undo': {
command: Command | undefined;
step: number;
};
'history:redo': {
command: Command | undefined;
step: number;
};
}
1 change: 0 additions & 1 deletion packages/plugin-history/src/vite-env.d.ts

This file was deleted.

Loading

0 comments on commit 6de07c0

Please sign in to comment.