web架构师编辑器内容-快捷键操作的实现
作者:mmseoamin日期:2024-02-02

快捷键操作的需求

元素选择 前提都是在元素被选中的情况下

  • 拷贝图层 - ⌘C / Ctrl+C : 新建当前选择的元素的一个数据结构
  • 粘贴图层 - ⌘V / Ctrl+V : 将新建的元素添加到 components 数组中
  • 删除图层 - Backspace / Delete : 在 components 数组中删除选择的元素
  • 取消选中 - ESC : currentElement 设置为空

    元素移动

    • 上下左右移动一像素 - ↑ ↓ → ← : 更新选中元素 props 的 top/left 值
    • 上下左右移动十像素 - Shift + ↑ ↓ → ←:更新选中元素 props 的 top/left 值

      撤销/重做

      • 撤销 - ⌘Z / Ctrl+Z
      • 重做 - ⌘⇧Z / Ctrl+Shift+Z

        好用的按键响应库 - HotKeys.js

        之前我们在InlineEdit.vue组件中,使用到了useKeyPress帮助了我们对esc和enter进行了绑定:

        const useKeyPress = (key: string, cb: () => any) => {
          const trigger = (event: KeyboardEvent) => {
            if (event.key === key) {
              cb()
            }
          }
          onMounted(() => {
            document.addEventListener('keydown', trigger)
          })
          onUnmounted(() => {
            document.removeEventListener('keydown', trigger)
          })
        }
        

        但是现在我们的需求复杂了很多,因为我们有很多的组合键,所以我们需要好用的第三方库来完成对应的工作:

        项目地址:https://github.com/jaywcjlove/hotkeys

        演示地址:https://wangchujiang.com/hotkeys/

        完成对应的编码
        // useHotKey.ts
        import hotkeys, { KeyHandler } from 'hotkeys-js'
        import { onMounted, onUnmounted } from 'vue'
        const useHotKey = (keys: string, callback: KeyHandler) => {
          onMounted(() => {
            hotkeys(keys, callback)
          })
          onUnmounted(() => {
            hotkeys.unbind(keys, callback)
          })
        }
        export default useHotKey
        

        对于快捷键的操作,是独立于整个应用,为了更好的交互,而单独添加的,看起来相对独立的功能,所以可以把整个模块称之为系统的插件。

        创建plugins文件夹:

        // plugins/hotKey.ts
        import useHotKey from '../hooks/useHotKey'
        export default function initHotKeys() {
          useHotKey('ctrl+c, command+c', () => {
            alert('ctrl+c, command+c')
          })
          useHotKey('ctrl+v, command+v', () => {
            alert('ctrl+v, command+v')
          })
        }
        // 在Editor.vue中的setup函数中进行调用,测试效果
        setup(){
        	initHotKeys()
        }
        

        完成ctrl+c快捷键的操作:

        // plugins/hotKey.ts
        import { useStore } from "vuex";
        import { computed } from "vue";
        import { GlobalDataProps } from "../store";
        useHotKey("ctrl+c, command+c", () => {
          const store = useStore();
          const currentId = computed(() => store.state.editor.currentElement);
          store.commit("copyComponent", currentId.value);
        }
        // store/editor.ts mutations中的
        copyComponent(state, id) {
          // 这里是在geeters定义了一个getElement,引入store,就可以在mutations中拿到getters中的属性了
          const currentComponent = store.getters.getElement(id)
          if (currentComponent) {
            state.copiedComponent = currentComponent;
            message.success('已拷贝当前图层', 1);
          }
        },
        // 在editoreProps中添加copiedComponent
        interface EditorProps {
          // 供中间编辑器渲染的数组
          components: ComponentData[];
          // 当前编辑的是哪个元素,uuid
          currentElement: string;
          // 当然最后保存的时候还有有一些项目信息,这里并没有写出,等做到的时候再补充
          page: PageData;
          // 当前被复制的组件
          copiedComponent?: ComponentData;
        }
        

        完成ctrl+v快捷键的操作:

        // plugins/hotKey.ts
        useHotKey('ctrl+v, command+v', () => {
          store.commit('pasteCopiedComponent')
        })
        // store/editors.ts中mutations中的
        pasteCopiedComponent(){
          if (state.copiedComponent) {
            state.components.push(state.copiedComponent)
            message.success('已黏贴当前图层', 1);
          }
         }
        

        这里如果直接进行push操作,就会发现复制出来的元素也粘贴出来的元素是一摸一样的,最终就会导致在移动复制的元素的时候,被复制的元素也会被同样移动。所以我们要对pasteCopiedComponent进行稍微的改造,拷贝一下数据:

        // store/editors.ts中mutations中的
        pasteCopiedComponent: setDirtyWrapper((state) => {
          if (state.copiedComponent) {
           // 使用lodash中的cloneDeep进行拷贝,可以丧失响应式
            const clone = cloneDeep(state.copiedComponent);
            clone.id = uuidv4();
            clone.layerName = clone.layerName + '副本';
            state.components.push(clone);
            message.success('已黏贴当前图层', 1);
          }
        }),
        // 删除图层:
        deleteComponent: setDirtyWrapper((state, id) => {
          const currentComponent = state.components.find(
            (component) => component.id === id
          );
          if (currentComponent) {
            state.components = state.components.filter(
              (component) => component.id !== id
            );
            message.success("删除当前图层成功", 1);
          }
        }),
        

        添加移动元素的快捷键:

        // store/editors.ts
        moveComponent(
        	state,
        	data: { direction: MoveDirection; amount: number; id: string }
        ) {
        	const currentComponent = state.components.find(
        		(component) => component.id === data.id
        	);
        	if (currentComponent) {
        	    // 获取旧的值
        		const oldTop = parseInt(currentComponent.props.top || "0");
        		const oldLeft = parseInt(currentComponent.props.left || "0");
        		const { direction, amount } = data;
        		switch (direction) {
        			case "Up": {
        				const newValue = oldTop - amount + "px";
        				store.commit("updateComponent", {
        					key: "top",
        					value: newValue,
        					id: data.id,
        				});
        				break;
        			}
        			case "Down": {
        				const newValue = oldTop + amount + "px";
        				store.commit("updateComponent", {
        					key: "top",
        					value: newValue,
        					id: data.id,
        				});
        				break;
        			}
        			case "Left": {
        				const newValue = oldLeft - amount + "px";
        				store.commit("updateComponent", {
        					key: "left",
        					value: newValue,
        					id: data.id,
        				});
        				break;
        			}
        			case "Right": {
        				const newValue = oldLeft + amount + "px";
        				store.commit("updateComponent", {
        					key: "left",
        					value: newValue,
        					id: data.id,
        				});
        				break;
        			}
        			default:
        				break;
        		}
        	}
        },
        // hotKey.ts
        function initHotKeys(){
          ...
          useHotKey('down', () => {
            store.commit('moveComponent', { direction: 'Down', amount: 1, id: currentId.value})
        })
          useHotKey('left', () => {
            store.commit('moveComponent', { direction: 'Left', amount: 1, id: currentId.value})
        })
          useHotKey('right', () => {
            store.commit('moveComponent', { direction: 'Right', amount: 1, id: currentId.value})
        })
          useHotKey('shift+up', () => {
            store.commit('moveComponent', { direction: 'Up', amount: 10, id: currentId.value})
        })
          useHotKey('shift+down', () => {
            store.commit('moveComponent', { direction: 'Down', amount: 10, id: currentId.value})
        })
          useHotKey('shift+left', () => {
            store.commit('moveComponent', { direction: 'Left', amount: 10, id: currentId.value})
        })
          useHotKey('shift+right', () => {
            store.commit('moveComponent', { direction: 'Right', amount: 10, id: currentId.value})
        })
        }
        

        实现之后发现会有一个问题:如果滚动条在下面的话,滚动条会跟着一起移动。

        滚动条跟随元素一起移动,这个行为是浏览器默认的一个行为,所以我们可以使用e.preventDefault来阻止浏览器默认行为,在useKotKey回调函数里面,第一个参数就是事件对象,我们可以在这里进行添加,其他事件也是如此。但是如果要是都是写同样的逻辑,是比较繁琐的,有什么办法能够完成无数个这样的逻辑呢?我们可以给回调函数在来一套包装,返回一个新的function,这就是高阶函数,高阶函数在处理多次逻辑重复的回调中比较适用:

        const wrap = (callback: KeyHandler) => {
          const wrapperFn = (e: KeyboardEvent, event: HotkeysEvent) => {
            e.preventDefault()
            callback(e, event)
          }
          return wrapperFn
        }
        useHotKey('up', wrap(() => {
          store.commit('moveComponent', { direction: 'Up', amount: 1, id: currentId.value })
        }))
        useHotKey('down', wrap(() => {
          store.commit('moveComponent', { direction: 'Down', amount: 1, id: currentId.value})
        }))