web架构师编辑器内容-编辑器组件图层面板功能开发-锁定隐藏、键盘事件功能的开发

我们这一部分主要是对最右侧图层面板功能进行剖析,完成对应的功能的开发:
在这里插入图片描述
每个图层都对应编辑器上面的元素,有多少个元素就对应多少个图层,主要的功能如下:

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

图层属性需求分析

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

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

// editor.ts
export interface EditorProps {
  // 供中间编辑器渲染的数组
  components: ComponentData[];
  // 当前编辑的是哪个元素,uuid
  currentElement: string
}

export interface ComponentData {
  // 这个元素的 属性,属性请详见下面
  props: Partial<AllComponentProps>;
  // 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
<ul :list="list" class="ant-list-items ant-list-border">
  <li class="ant-list-item" v-for="item in list" :key="item.id">
    <a-tooltip :title="item.isHidden ? '显示' : '隐藏'">
      <a-button shape="circle">
        <template v-slot:icon v-if="item.isHidden"
          ><EyeInvisibleOutlined />
        </template>
        <template v-slot:icon v-else><EyeOutlined /> </template>
      </a-button>
    </a-tooltip>
    <a-tooltip :title="item.isLocked ? '解锁' : '锁定'">
      <a-button shape="circle">
        <template v-slot:icon v-if="item.isLocked"
          ><LockOutlined />
        </template>
        <template v-slot:icon v-else><UnlockOutlined /> </template>
      </a-button>
    </a-tooltip>
    <span>{{ item.layerName }}</span>
  </li>
</ul>

// list的数据来源:在点击左侧组件模板库的时候,会在store中发射一个事件:
// Editor.vue
// 右侧图层设置组件(其中components就是store中的components)
//  const components = computed(() => store.state.editor.components);
<layer-list
   :list="components"
   :selectedId="currentElement && currentElement.id"
    @change="handleChange"
    @select="setActive"
 >
</layer-list>
// 点击左侧模板库某个组件触发的事件
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";
	}

最开始的样子
在这里插入图片描述
进行锁定隐藏操作

// 隐藏
<a-tooltip :title="item.isHidden ? '显示' : '隐藏'">
  <a-button
    shape="circle"
    @click.stop="handleChange(item.id, 'isHidden', !item.isHidden)"
  >
    <template v-slot:icon v-if="item.isHidden"
      ><EyeInvisibleOutlined />
    </template>
    <template v-slot:icon v-else><EyeOutlined /> </template>
  </a-button>
</a-tooltip>
// 锁定
<a-tooltip :title="item.isLocked ? '解锁' : '锁定'">
  <a-button
    shape="circle"
    @click.stop="handleChange(item.id, 'isLocked', !item.isLocked)"
  >
    <template v-slot:icon v-if="item.isLocked"
      ><LockOutlined />
    </template>
    <template v-slot:icon v-else><UnlockOutlined /> </template>
  </a-button>
</a-tooltip>

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<AllComponentProps>;
  // 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来判断右侧面板设置区域属性设置是否可以进行编辑
<a-tab-pane key="component" tab="属性设置" class="no-top-radius">
  <div v-if="currentElement">
    <edit-group
      v-if="!currentElement.isLocked"
      :props="currentElement.props"
      @change="handleChange"
    ></edit-group>
    <div v-else>
      <a-empty>
        <template #description>
          该元素已被锁定,无法被编辑
        </template>
      </a-empty>
    </div>
  </div>
  <pre>
    {{ currentElement && currentElement.props }}
  </pre>
</a-tab-pane>

// 根据hidden属性来控制中间画布区域是否可以进行显示与隐藏
// EditorWrapper.vue
:class="{ active: active, hidden: hidden }"

图层重命名组件的开发

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

InlineEdit
显示默认文本区域,点击以后显示为 Input
Input 中的值显示为文本中的值
更新值以后,键盘事件 - (useKeyPress)

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

简单验证

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

最初的InlineEdit组件

// InlineEdit.vue
<template>
  <div class="inline-edit" @click.stop="handleClick" ref="wrapper">
    <input
      v-model="innerValue"
      v-if="isEditing"
      placeholder="文本不能为空"
      ref="inputRef"
    />
    <slot v-else :text="innerValue"><span>{{innerValue}}</span></slot>
  </div>
</template>

<script lang="ts">
import { defineComponent, nextTick, ref, watch } from 'vue'
export default defineComponent({
  name: 'inline-edit',
  props: {
    value: {
      type: String,
      required: true
    }
  },
  emits: ['change'],
  setup (props, context) {
    const innerValue = ref(props.value)
    const isEditing = ref(false)
    const handleClick = () => {
      isEditing.value = true
    }
    return {
      handleClick,
      innerValue,
      isEditing
    }
  }
})
</script>

<style>
.inline-edit {
  cursor: pointer;
}
.ant-input.input-error {
  border: 1px solid #f5222d;
}
.ant-input.input-error:focus {
  border-color:  #f5222d;
}
.ant-input.input-error::placeholder {
  color: #f5222d;
}
</style>
键盘事件整合成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事件
<inline-edit
  class="edit-area"
  :value="item.layerName"
  @change="
    (value) => {
      handleChange(item.id, 'layerName', value)
    }
  "
></inline-edit>

键盘响应的功能常规做法其实就是向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函数
// hooks/useClickOutside.ts
import { ref, onMounted, onUnmounted, Ref } from 'vue';
const useClickOutside = (elementRef: Ref<null | HTMLElement>) => {
  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 | HTMLInputElement>(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事件使isOutsidetrue,从truetruewatch第二个回调函数不会触发。所以需要再watch里面手动将isOutside置为false.
 另外我们知道是事件冒泡导致的,我们把事件冒泡去掉,直接写成@click="handleClick"不可以嘛?答案是不可以的,我们来看一下效果:
在这里插入图片描述
点击除了文字外的区域进行编辑的时候没有问题,但是点击的文字的时候就有问题了,主要原因就是没有使用到冒泡,导致useClickOutside事件触发了
判断是否点击到了对应的dom节点的功能是比较常见的,比如说下拉菜单的关闭,点击下拉菜单的外面,会关闭下拉菜单使用的是同一个思想。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/337161.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

响应式Web开发项目教程(HTML5+CSS3+Bootstrap)第2版 例4-6 fieldset

代码 <!doctype html> <html> <head> <meta charset"utf-8"> <title>fieldset</title> </head><body> <form action"#"><fieldset><legend>学生信息</legend>姓名&#xff1a;&…

基于python旅游推荐系统 协同过滤算法 爬虫 Echarts可视化 Django框架(源码)✅

毕业设计&#xff1a;2023-2024年计算机专业毕业设计选题汇总&#xff08;建议收藏&#xff09; 毕业设计&#xff1a;2023-2024年最新最全计算机专业毕设选题推荐汇总 &#x1f345;感兴趣的可以先收藏起来&#xff0c;点赞、关注不迷路&#xff0c;大家在毕设选题&#xff…

2024年一整年的考试报名时间表不许再错过考试啦

每个大学生都不能错过的超全考试报名表&#xff01; 有了它谁还会再错过考试哇&#xff01;&#xff01;&#xff01; 1月报名 专转本考试 12月底-1月报名 卫生资格考试 1月中旬报名 教师资格证笔试 1月报名 各省省考 2月报名 医师资格考试 2月报名 初级高级会计 2月报名 计算机…

专业137总分439东南大学920专业基础综合考研经验电子信息与通信电路系统芯片

我本科是南京信息工程大学&#xff0c;今年报考东南大学信息学院&#xff0c;成功逆袭&#xff0c;专业137&#xff0c;政治69&#xff0c;英语86&#xff0c;数一147&#xff0c;总分439。以下总结了自己的复习心得和经验&#xff0c;希望对大家复习有一点帮助。啰嗦一句&…

NLP论文阅读记录 - 2021 | WOS 基于多头自注意力机制和指针网络的文本摘要

文章目录 前言0、论文摘要一、Introduction1.1目标问题1.2相关的尝试1.3本文贡献 二.问题定义和解决问题的假设问题定义解决问题的假设 三.本文方法3.1 总结为两阶段学习3.1.1 基础系统 3.2 重构文本摘要 四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4…

[ VSCode ]command ‘python.setInterpreter‘ not found

ctrlshiftP 想用Python选择解释器 却出现如下报错&#xff0c;十分苦恼 开始解决问题 首先&#xff0c; 然后&#xff0c; 这里的路径是您安装在您电脑的路径&#xff0c;我的是如下 最后重启vscode或者电脑就好啦

vue.js js 雪花算法ID生成 vue.js之snowFlake算法

随着前端业务越来越复杂&#xff0c;自定义表单数据量比较大&#xff0c;每条数据的id生成则至关重要。想到前期IOS中实现的雪花算法ID&#xff0c;照着其实现JS版本&#xff0c;供大家学习参考。 一、库的建立引入 在你项目中创建一个snowFlake.js的文件&#xff1a;拷贝以下…

『MySQL快速上手』-⑩-索引特性

文章目录 1.索引的作用2.索引的理解建立测试表插入多条记录查看结果 2.1 MySQL与磁盘交互的基本单位2.1 为何IO交互要是 Page2.3 理解单个Page2.4 理解多个Page2.5 页目录2.6 单页情况2.7 多页情况2.8 B vs B2.9 聚簇索引 vs 非聚簇索引非聚簇索引聚簇索引 3.索引操作3.1 创建主…

webpack 核心武器:loader 和 plugin 的使用指南(下)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

c++函数重载(同名函数)功能,区别于c语言

c可以使用同名函数&#xff0c;实现功能类似的多个功能 规则&#xff1a; ①函数名相同&#xff0c;但是函数的参数&#xff08;形参&#xff09;绝不相同 ②参数个数不同 ③参数个数相同&#xff0c;参数类型不同 只有返回值类型不同&#xff0c;不可以&#xff1b;只有形…

Unity中ShaderGraph下获取主灯

文章目录 前言一、ShaderGraph获取主灯1、创建ShaderGraph2、创建一个自定义方法&#xff08;Custom Function&#xff09;节点3、新建两个 Vector3 类型的输出变量4、选择自定义节点程序体为 string 类型5、编写程序体6、我们输出主光方向看看效果7、我们输出主光颜色看看效果…

JRP Version 1.4.120

使用Flask学习制作网页一个月后&#xff1a; 借用HTML书籍学习&#xff0c;自己做的NAS管理系统终于是长得好看了一些&#xff1a; 使用模版继承&#xff0c;最开始是引用人家的库 from flask_bootstrap import Bootstrap&#xff0c; 效果&#xff1a; 我准备进一步管理但是发…

Arduino开发实例-MTH02温湿度传感器驱动

MTH02温湿度传感器驱动 文章目录 MTH02温湿度传感器驱动1、MTH02温湿度传感器介绍2、硬件准备及接线3、代码实现1、MTH02温湿度传感器介绍 市场上的温湿度传感器在价格、精度和测量范围等方面种类繁多。 MTH02O 是这些传感器中最小的一种。 该传感器使用数字引脚传输温度和湿度…

C++中const和constexpr的区别:了解常量的不同用法

C中const和constexpr的区别 一、C中的常量概念二、const关键字的用法和特点三、constexpr关键字的用法和特点四、const和constexpr的区别对比4.1、编译时计算能力4.2、可以赋值的范围4.3、对类和对象的适用性4.4、对函数的适用性4.5、性能和效率的差异 五、使用示例六、总结 一…

k3s x GitLab Runner Operator,GitLab CI 云原生构建新体验

GitLab CI 是非常常用的一款 CI/CD 工具&#xff0c;只需要在 .gitlab-ci.yml 文件中用 YAML 语法编写 CI/CD 流水线即可。而 GitLab CI 能够运行的关键组件是 GitLab Runner。GitLab Runner 是一个轻量级、高扩展的代理&#xff0c;主要用来执行 GitLab CI/CD 流水线中的 Job&…

《PCI Express体系结构导读》随记 —— 第I篇 第2章 PCI总线的桥与配置(20)

接前一篇文章&#xff1a;《PCI Express体系结构导读》随记 —— 第I篇 第2章 PCI总线的桥与配置&#xff08;19&#xff09; 2.4 PCI总线的配置 PCI总线定义了两类配置请求&#xff0c;一个是Type 00h配置请求&#xff0c;另一个是Type 01h配置请求。PCI总线使用这些配置请求…

1432 - 走出迷宫的最少步数-广度优先搜索BFS

代码 #include<bits/stdc.h> using namespace std; char a[51][51]; int r,c; int fx[4]{0,0,1,-1}; int fy[4]{1,-1,0,0}; int tx,ty; struct Node{int x,y,step; }; int bfs(int x,int y){a[x][y]#;queue<Node> q;q.push({x,y,1});while(!q.empty()){Node Curre…

Maven error in opening zip file?maven源码debug定位问题jar包

文章目录 问题发现调试Maven1. 查看maven版本2. 下载对应版本的maven源码3. 打开maven源码&#xff0c;配置启动选项 启动maven debug模式进入maven 源码&#xff0c;打断点调试找jar包算账 已录制视频 视频连接 问题发现 最近使用maven分析jar包的时候遇到了一个很搞的问题。…

鸿蒙星河版启航,开发者驶入生态新征程

操作系统市场的气候已经不同以往。在鸿蒙决定不再兼容安卓之后&#xff0c;这里正欲长出一片全新的天地。 四年前&#xff0c;华为鸿蒙系统横空出世&#xff0c;彼时它还不完全与安卓和iOS的性质划等号&#xff0c;而是定义为物联网操作系统。而如今的华为鸿蒙要改写故事篇章&…

前端基础面试题大全

一、Vue 文章目录 一、Vue1、vue 修改数据页面不重新渲染**数组/对象的响应式 &#xff0c;vue 里面是怎么处理的&#xff1f;** 2、生命周期Vue 生命周期都有哪些&#xff1f;父子组件生命周期执行顺序 3、watch 和 computed 的区别4、组件通信&#xff08;组件间传值&#xf…
最新文章