我们这一部分主要是对最右侧图层面板功能进行剖析,完成对应的功能的开发:
每个图层都对应编辑器上面的元素,有多少个元素就对应多少个图层,主要的功能如下:
图层和编辑器中的元素都是一一对应的,
// editor.ts export interface EditorProps { // 供中间编辑器渲染的数组 components: ComponentData[]; // 当前编辑的是哪个元素,uuid currentElement: string } export interface ComponentData { // 这个元素的 属性,属性请详见下面 props: Partial; // id,uuid v4 生成 id: string; // 业务组件库名称 l-text,l-image 等等 name: 'l-text' | 'l-image' | 'l-shape'; }
在editor.ts中,components其实就是对应的图层,有对应的一些属性ComponentData,对于不同的状态,我们来添加对应的标识符来添加特定的标识符来表示他的状态即可。
{
…
isLocked: boolean;
isHidden: boolean;
}
// LayerList.vue
最开始的样子
进行锁定隐藏操作
// 隐藏// 锁定 const handleChange = (id: string, key: string, value: boolean) => { const data = { id, key, value, isRoot: true, }; context.emit("change", data); }; // 最终在子组件中emit chang事件,父组件中触发该方法, const handleChange = (e: any) => { console.log('event', e); store.commit('updateComponent', e); }; // 对store中的updateComponent进行稍微的改造 // 原来的updateComponent // 这个主要针对于最右侧面板设置区域中的属性设置进行更新的,改变的是props的值。 updateComponent(state, { key, value }) { const updatedComponent = state.components.find( (component) => component.id === state.currentElement ); if(updatedComponent) { updatedComponent.props[key as keyof TextComponentProps] = value; } } // 现在的 updateComponent(state, { key, value, id, isRoot }) { const updatedComponent = state.components.find( (component) => component.id === (id || state.currentElement) ); if(updatedComponent) { if(isRoot) { (updatedComponent as any)[key as string] = value; } updatedComponent.props[key as keyof TextComponentProps] = value; } } // 增加isRoot主要用来判断改变的是否是props中的某一项的值,我们进行的是展示隐藏,锁定不锁定的功能,所以直接改变key值就行: export interface ComponentData { // 这个元素的 属性,属性请详见下面 props: Partial ; // id,uuid v4 生成 id: string; // 业务组件库名称 l-text,l-image 等等 name: 'l-text' | 'l-image' | 'l-shape'; // 图层是否隐藏 isHidden?: boolean; // 图层是否锁定 isLocked?: boolean; // 图层名称 layerName?: string; } // Editor.vue // 根据isLocked来判断右侧面板设置区域属性设置是否可以进行编辑 // 根据hidden属性来控制中间画布区域是否可以进行显示与隐藏 // EditorWrapper.vue : 该元素已被锁定,无法被编辑 {{ currentElement && currentElement.props }}
图层重命名组件,就是在右侧面板设置中的图层设置区域,点击图层名称,变成可输入的输入框形式,可以完成图层名称的更新,并且可以添加一些键盘事件,点击回车可以显示新的值,点击esc后显示刚开始的旧的值。在点击input区域外侧恢复文本区域,并且显示新的值。基于这些,我们可以抽离出一个InlineEdit组件
InlineEdit
显示默认文本区域,点击以后显示为 Input
Input 中的值显示为文本中的值
更新值以后,键盘事件 - (useKeyPress)
- 点击回车以后恢复文本区域,并且显示新的值
- 点击 ESC 后恢复文本区域,并且显示刚开始的旧的值,更新值以后,点击事件 - (useClickOutside)
- 点击 Input 区域外侧恢复文本区域,并且显示新的值
简单验证
- 当 Input值为空的时候,不恢复,并且显示错误。
最初的InlineEdit组件
// InlineEdit.vue{{innerValue}}
// hooks/useKeyPress.ts import { onMounted, onUnmounted } from 'vue' 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) }) } // 组件中使用 InlineEdit.vue // 缓存之前编辑的值 watch(isEditing, (isEditing) => { if (isEditing) { cachedOldValue = innerValue.value } }) useKeyPress("Enter", () => { if (isEditing.value) { isEditing.value = false; context.emit("change", innerValue.value); } }); useKeyPress("Escape", () => { if (isEditing.value) { isEditing.value = false; innerValue.value = cachedOldValue; } }); // 父组件接受change事件{ handleChange(item.id, 'layerName', value) } " >
键盘响应的功能常规做法其实就是向document.addEventListener上添加各种一系列的回调,在项目后期还会遇到各种复杂的键盘响应,比如组合键,ctrl+c,ctrl+v,我们可能会进化到第三方库来完成对应的需求,先使用实际代码演示一个比较简单的功能,然后再使用第三方库的解决方案,这样能让我们了解第三方库的基本原理。上面就是按键响应的基本原理。
后来增加一个需求:在点击编辑,变成输入框的时候,增加自动聚焦的功能:
//这样写有问题 watch(isEditing, isEditing => { if (isEditing) { cachedOldValue = innerValue.value if (inputRef.value) { inputRef.value.focus() } } })
这样写的话,发现不起任何作用,input没有自动聚焦。
watchEffect
在vue3的官网api中,我们可以看到:
watchEffect的flush默认是pre,默认是在dom生成之前执行的,所以拿不到dom。但是vue没有提供可以改变flush的选项,没有办法在post中执行。所以我们这里可以vue提供的nextTick,等待dom生成完毕后,再运行,改写后的:
watch(isEditing, async (isEditing) => { if (isEditing) { cachedOldValue = innerValue.value await nextTick() if (inputRef.value) { inputRef.value.focus() } } })
// hooks/useClickOutside.ts import { ref, onMounted, onUnmounted, Ref } from 'vue'; const useClickOutside = (elementRef: Ref) => { const isClickOutside = ref(false) const handler = (e: MouseEvent) => { if (elementRef.value && e.target) { // 检查当前元素是否在目标元素范围内 if (elementRef.value.contains(e.target as HTMLElement)) { isClickOutside.value = false } else { isClickOutside.value = true } } } onMounted(() => { document.addEventListener('click', handler) }) onUnmounted(() => { document.removeEventListener('click', handler) }) return isClickOutside } // 组件中使用 InlineEdit.vue const inputRef = ref (null) const isOutside = useClickOutside(wrapper) watch(isOutside, (newValue) => { if (newValue && isEditing.value) { isEditing.value = false context.emit('change', innerValue.value) } // 注意这里要将isOutside重新复原到false,因为如果不恢复成false的话,在isOutside为true的时候,点击外部区域,不会走这里的回调,因为值(true => true)没有改变。 isOutside.value = false; })
在进行图层设置对于图层名称点击编辑的时候,遇到一个这样的问题:从属性设置到图层设置切换后,点击图层名称进行图层编辑,编辑完成后,点击外层空白区域输入框没有变成原来的文本域。
产生上面问题的原因:
在页面上打印的值是isOutside,在进行属性设置和图层设置的时候,其实触发了useClickOutside事件,返回了true,在鼠标进行文本点击的时候,由于在InlineEdit.vue 组件中加了@click.stop="handleClick",导致没有冒泡到document,所以这个时候useClickOutside事件没有被触发,isOutside的值并没有被改变,点击外面空白区域的时候,触发useClickOutside事件使isOutside为true,从true到true,watch第二个回调函数不会触发。所以需要再watch里面手动将isOutside置为false.
另外我们知道是事件冒泡导致的,我们把事件冒泡去掉,直接写成@click="handleClick"不可以嘛?答案是不可以的,我们来看一下效果:
点击除了文字外的区域进行编辑的时候没有问题,但是点击的文字的时候就有问题了,主要原因就是没有使用到冒泡,导致useClickOutside事件触发了
判断是否点击到了对应的dom节点的功能是比较常见的,比如说下拉菜单的关闭,点击下拉菜单的外面,会关闭下拉菜单使用的是同一个思想。