元素选择 前提都是在元素被选中的情况下
元素移动
撤销/重做
之前我们在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}) }))