Command.ts 6.66 KB
import {computed, reactive} from "vue";
import {KeyboardCode} from "@/utils/keyboard-code";

/**
 * 命令模式实现 撤销/重做的命令管理类
 * @author  韦胜健
 * @date    2020/4/30 12:08
 *
 * 1. const commander = useCommander() 得到 commander对象;
 * 2. state.commander.register(command:Command) 注册命令
 * 3. commander.commands.smaller() 调用命令
 * 4. commander.isEnable.value.smaller 判断命令是否可用
 *
 */


interface CommandQueueItem {
    undo?: () => void,
    redo: () => void,
}

export class Command {
    name: string                                                        // 命令名称
    execute: (...args: any[]) => CommandQueueItem                       // 命令执行的逻辑
    keyboard?: undefined | string | string[]                            // 监听的键盘事件:ctrl+shift+alt+a
    isEnable?: undefined | (() => boolean)                              // 判断当前是否可用
    isQueue?: boolean                                                   // 是否遵循命令队列
    doNothingWhenExecute?: boolean                                      // 在调用execute的时候是否什么也不做 (dragend 的时候,这个位置的更新已经由graph做好了,这时候execute不需要立即执行redu)
    data?: undefined | any                                              // 命令缓存的数据
    init?: undefined | (() => void)                                     // 命令初始化函数
    destroy?: undefined | (() => void)                                  // 命令初始化函数
    graph?: undefined | any                                             // g6对象

    constructor(command: Command) {
        Object.assign(this, command)
        this.isEnable = command.isEnable == null ? (() => true) : command.isEnable
        this.isQueue = command.isQueue == null ? true : command.isQueue
        this.doNothingWhenExecute = command.doNothingWhenExecute == null ? false : command.doNothingWhenExecute
        this.data = {}
    }

}

export function useCommander(editorState) {

    /**
     * 命令状态
     * @author  韦胜健
     * @date    2020/4/30 11:54
     */
    const state = reactive({
        queue: [] as CommandQueueItem[],                                    // 当前执行过的命令队列
        index: -1,                                                          // 当前命令在命令队列中的索引
        registerCommands: [] as Command[],                                  // 当前注册的命令
    })

    const commands: { [key: string]: (...args: any[]) => void } = {}

    const isEnable = computed(() => {
        return state.registerCommands.reduce((ret, item) => {
            ret[item.name] = item.isEnable()
            return ret
        }, {})
    })

    /**
     * 注册新命令
     * @author  韦胜健
     * @date    2020/4/30 11:55
     */
    const register = (command: Command) => {
        if (!command.name) {
            console.log(command)
            throw new Error("Commander: command's name can not be empty!")
        }

        state.registerCommands.push(command)

        commands[command.name] = (...args: any[]) => {

            if (!!command.isEnable && !command.isEnable()) {
                return
            }

            const {undo, redo} = command.execute(...args)

            if (!command.isQueue) {
                return (!command.doNothingWhenExecute && redo())
            }

            let {queue, index} = state
            if (queue.length > 0) {
                queue = queue.slice(0, index + 1)
                state.queue = queue
            }
            queue.push({undo, redo})
            state.index = index + 1;
            (!command.doNothingWhenExecute && redo());
        }
    }

    register(new Command({
        name: 'undo',
        keyboard: 'ctrl+z',
        isQueue: false,
        execute: () => {
            return {
                redo: () => {
                    if (state.index === -1) {
                        return
                    }
                    const queueItem = state.queue[state.index]
                    // console.log('queueItem',queueItem)
                    if (!!queueItem) {
                        queueItem.undo()
                        state.index--
                    }
                }
            }
        },
        isEnable: () => {
            if (editorState.props.disabledUndo) return false
            return !!state.queue[state.index]
        }
    }))

    register(new Command({
        name: 'redo',
        keyboard: 'ctrl+shift+z',
        isQueue: false,
        execute: () => {
            return {
                redo: () => {
                    const queueItem = state.queue[state.index + 1]
                    if (!!queueItem) {
                        queueItem.redo()
                        state.index++
                    }
                }
            }
        },
        isEnable: () => {
            if (editorState.props.disabledUndo) return false
            return !!state.queue[state.index + 1]
        }
    }))

    let onKeydown;

    function initEvent() {
        if (!onKeydown) {
            onKeydown = (e: KeyboardEvent) => {
                const names = [];
                e.ctrlKey && names.push('ctrl')
                e.shiftKey && names.push('shift')
                e.altKey && names.push('alt')

                names.push(KeyboardCode[e.keyCode])
                const compositionKeyName = names.join('+')

                state.registerCommands.forEach((command: Command) => {
                    if (!command.keyboard) return
                    const keys = Array.isArray(command.keyboard) ? command.keyboard : [command.keyboard]
                    if (keys.indexOf(compositionKeyName) > -1) {
                        e.stopPropagation()
                        e.preventDefault()

                        commands[command.name]()
                    }
                })
            }
            window.addEventListener('keydown', onKeydown)
        }
    }

    function destroyEvent() {
        if (!!onKeydown) {
            window.removeEventListener('keydown', onKeydown)
            onKeydown = null
        }
    }

    function init(graph: any) {
        state.registerCommands.forEach(command => {
            command.graph = graph
            if (!!command.init) {
                command.init()
            }
        })
    }

    function destroy() {
        destroyEvent()
        state.registerCommands.forEach(command => {
            if (!!command.destroy) {
                command.destroy()
            }
        })
    }

    return {
        state,
        register,
        commands,
        isEnable,
        initEvent,
        destroyEvent,
        init,
        destroy,
    }
}