快捷键操作的需求
元素选择 前提都是在元素被选中的情况下
- 拷贝图层 - ⌘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进行了绑定:
ts
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/
完成对应的编码
ts
// 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文件夹:
ts
// 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快捷键的操作:
ts
// plugins/hotKey.ts
import { useStore } from "vuex";
import { computed } from "vue";
import { GlobalDataProps } from "../store";
useHotKey("ctrl+c, command+c", () => {
const store = useStore<GlobalDataProps>();
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快捷键的操作:
ts
// 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
进行稍微的改造,拷贝一下数据:
ts
// 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);
}
}),
添加移动元素的快捷键:
ts
// 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
,这就是高阶函数,高阶函数在处理多次逻辑重复的回调中比较适用:
ts
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})
}))