web架构师编辑器内容-快捷键操作的实现

快捷键操作的需求

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

  • 拷贝图层 - ⌘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})
}))
相关推荐
FIRE22 分钟前
uniapp小程序分享使用canvas自定义绘制 vue3
前端·小程序·uni-app
四喜花露水23 分钟前
vue elementui el-dropdown-item设置@click无效的解决方案
前端·vue.js·elementui
jokerest1231 小时前
web——sqliabs靶场——第五关——报错注入和布尔盲注
前端
谢尔登1 小时前
前端开发调试之 PC 端调试
开发语言·前端
每天吃饭的羊1 小时前
在循环中只set一次
开发语言·前端·javascript
_默_4 小时前
adminPage-vue3依赖DetailsModule版本说明:V1.2.1——1) - 新增span与labelSpan属性
前端·javascript·vue.js·npm·开源
也无晴也无风雨6 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang7 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
dllmayday8 小时前
VSCode 多工程联合调试
ide·vscode·编辑器
霖雨8 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器