相关推荐recommended
web架构师编辑器内容-编辑器组件图层面板功能开发-锁定隐藏、键盘事件功能的开发
作者:mmseoamin日期:2024-01-21

我们这一部分主要是对最右侧图层面板功能进行剖析,完成对应的功能的开发:

web架构师编辑器内容-编辑器组件图层面板功能开发-锁定隐藏、键盘事件功能的开发,在这里插入图片描述,第1张

每个图层都对应编辑器上面的元素,有多少个元素就对应多少个图层,主要的功能如下:

  1. 锁定功能:点击锁定,在编辑器中没法编辑对应的组件属性,再次点击是取消锁定,恢复到可编辑的模式
  2. 可见化:点击隐藏,在编辑器中消失,再次点击,进行展示
  3. 最外层图层也是可以进行点击,单击图层就是选中的效果。在编辑器上就是自动选中的效果。
  4. 图层的文字也可以进行修改,单击图层的文字,会切换到编辑模式,展示成input输入框,可以进行文字的修改。回车确认,点击esc退出,点击外部区域确定。
  5. 比较复杂的功能:拖动排序,按住这个按钮拖动以后,可以改变图层的顺序。

图层属性需求分析

图层锁定和隐藏/显示以及选中

图层和编辑器中的元素都是一一对应的,

// 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,对于不同的状态,我们来添加对应的标识符来添加特定的标识符来表示他的状态即可。

  • 在editor.ts的store中的components添加更多的标识符

    {

    isLocked: boolean;

    isHidden: boolean;

    }

    • 点击按钮切换为不同的值,使用这个值在页面上做判断
    • 点击选中,设置 currentElement的值

      图层名称编辑

      • 添加更多属性 - layerName
      • 点击图层名称的时候,在input和普通标签之间切换
      • 添加按钮响应 - 对于 esc 和 enter 键的响应
        • 可能抽象一个通用的 hooks函数 - useKeyPress,可以处理与键盘相关的事件
        • 点击到input外部区域的响应
          • 可能抽象一个通用的 hooks函数 - useClickOutside

            拖动改变顺序

            • 最有难度的一个需求,涉及到一个较复杂的交互
            • 最终目的其实就是改变store中components数组的顺序

              代码实现

              // LayerList.vue
              
              • {{ item.layerName }}
              // list的数据来源:在点击左侧组件模板库的时候,会在store中发射一个事件: // Editor.vue // 右侧图层设置组件(其中components就是store中的components) // const components = computed(() => store.state.editor.components); // 点击左侧模板库某个组件触发的事件 const addItem = (component: any) => { store.commit('addComponent', component); }; // editor.ts addComponent: setDirtyWrapper((state, component: ComponentData) => { component.layerName = '图层' + (state.components.length + 1); state.components.push(component); }), // 比如点击大标题,在addItem中对应的参数如下: component: { // 通过pageUUid生成的唯一主键 id: '3c78b476-7a8d-4ad1-b944-9b163993595d', // 动态需要渲染的组件 name: "l-text", props: { actionType: ""; backgroundColor: ""; borderColor: "#000"; borderRadius: "0"; borderStyle: "none"; borderWidth: "0"; boxShadow: "0 0 0 #000000"; color: "#000000"; fontFamily: ""; fontSize: "30px"; fontStyle: "normal"; fontWeight: "bold"; height: ""; left: "0"; lineHeight: "1"; opacity: "1"; paddingBottom: "0px"; paddingLeft: "0px"; paddingRight: "0px"; paddingTop: "0px"; position: "absolute"; right: "0"; tag: "h2"; text: "大标题"; textAlign: "left"; textDecoration: "none"; top: "0"; url: ""; width: "100px"; }

              最开始的样子

              web架构师编辑器内容-编辑器组件图层面板功能开发-锁定隐藏、键盘事件功能的开发,在这里插入图片描述,第2张

              进行锁定隐藏操作

              // 隐藏
              
                
                  
                  
                
              
              // 锁定
              
                
                  
                  
                
              
              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来判断右侧面板设置区域属性设置是否可以进行编辑
              
                
                  {{ currentElement && currentElement.props }}
                
              // 根据hidden属性来控制中间画布区域是否可以进行显示与隐藏 // EditorWrapper.vue :

              图层重命名组件的开发

              图层重命名组件,就是在右侧面板设置中的图层设置区域,点击图层名称,变成可输入的输入框形式,可以完成图层名称的更新,并且可以添加一些键盘事件,点击回车可以显示新的值,点击esc后显示刚开始的旧的值。在点击input区域外侧恢复文本区域,并且显示新的值。基于这些,我们可以抽离出一个InlineEdit组件

              InlineEdit

              显示默认文本区域,点击以后显示为 Input

              Input 中的值显示为文本中的值

              更新值以后,键盘事件 - (useKeyPress)

              • 点击回车以后恢复文本区域,并且显示新的值
              • 点击 ESC 后恢复文本区域,并且显示刚开始的旧的值,更新值以后,点击事件 - (useClickOutside)
              • 点击 Input 区域外侧恢复文本区域,并且显示新的值

                简单验证

                • 当 Input值为空的时候,不恢复,并且显示错误。

              最初的InlineEdit组件

              // InlineEdit.vue
              
              
              
              键盘事件整合成hooks函数:
              // 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事件
              
              

              键盘响应的功能常规做法其实就是向document.addEventListener上添加各种一系列的回调,在项目后期还会遇到各种复杂的键盘响应,比如组合键,ctrl+c,ctrl+v,我们可能会进化到第三方库来完成对应的需求,先使用实际代码演示一个比较简单的功能,然后再使用第三方库的解决方案,这样能让我们了解第三方库的基本原理。上面就是按键响应的基本原理。

              后来增加一个需求:在点击编辑,变成输入框的时候,增加自动聚焦的功能:

              //这样写有问题
              watch(isEditing, isEditing => {
                if (isEditing) {
                  cachedOldValue = innerValue.value
                  if (inputRef.value) {
                    inputRef.value.focus()
                  }
                }
              })
              

              这样写的话,发现不起任何作用,input没有自动聚焦。

              watchEffect

              在vue3的官网api中,我们可以看到:

              web架构师编辑器内容-编辑器组件图层面板功能开发-锁定隐藏、键盘事件功能的开发,在这里插入图片描述,第3张

              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函数
              // 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;
              })
              

              在进行图层设置对于图层名称点击编辑的时候,遇到一个这样的问题:从属性设置到图层设置切换后,点击图层名称进行图层编辑,编辑完成后,点击外层空白区域输入框没有变成原来的文本域。

              web架构师编辑器内容-编辑器组件图层面板功能开发-锁定隐藏、键盘事件功能的开发,在这里插入图片描述,第4张

              产生上面问题的原因:

               在页面上打印的值是isOutside,在进行属性设置和图层设置的时候,其实触发了useClickOutside事件,返回了true,在鼠标进行文本点击的时候,由于在InlineEdit.vue 组件中加了@click.stop="handleClick",导致没有冒泡到document,所以这个时候useClickOutside事件没有被触发,isOutside的值并没有被改变,点击外面空白区域的时候,触发useClickOutside事件使isOutside为true,从true到true,watch第二个回调函数不会触发。所以需要再watch里面手动将isOutside置为false.

               另外我们知道是事件冒泡导致的,我们把事件冒泡去掉,直接写成@click="handleClick"不可以嘛?答案是不可以的,我们来看一下效果:

              web架构师编辑器内容-编辑器组件图层面板功能开发-锁定隐藏、键盘事件功能的开发,在这里插入图片描述,第5张

              点击除了文字外的区域进行编辑的时候没有问题,但是点击的文字的时候就有问题了,主要原因就是没有使用到冒泡,导致useClickOutside事件触发了

              判断是否点击到了对应的dom节点的功能是比较常见的,比如说下拉菜单的关闭,点击下拉菜单的外面,会关闭下拉菜单使用的是同一个思想。