web架构师编辑器内容-拖动元素改变元素的位置和大小的完成
拖动移动元素
改变编辑器的定位系统
我们目前的元素都是按照块级元素直接自上而下的排列在画布中,为了让元素实现精确的定位和调整,我们需要改变这些元素的定位实现。我们需要让这些元素画布区域来进行绝对定位。如果我们有一个元素有这些已经保存的 css 属性,那么它就可以在编辑器,或者是在另外的 H5 端渲染出这样的一个样式。
基本指导思想
交互的最终结果只是修改这些样式而已,比如拖动定位,最终就是在修改 top 和 left 的值而已,那么缩放大小,最终就是在修改 width 和 height 的值而已。
基本分析
1 拖动是在按下鼠标,然后鼠标移动这个过程中发生的。所以首先我们要响应的是鼠标按下按下的时候,也就是 MouseDown 的时候开始运作。
2 在鼠标移动的时候,我们需要将 top,left 的值更新到新的值,这个就是过程的重点。
结合交互图进行分析:可以在线查看,地址为:https://whimsical.com/RTJphPrwzksyotCdA32LQU@VsSo8s35WxESA3XwhpMUni

计算鼠标点下去,元素偏移量:
getBoundingClientRect
Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。

<template><div class="edit-wrapper"ref="editWrapper":style="styles"@mousedown="startMove"@click="onItemClick(id)" :class="{ active: active, hidden: hidden }"><slot></slot></div>
</template><script lang="ts">
import { defineComponent, computed, ref } from 'vue'
import { pick } from 'lodash-es'
export default defineComponent({props: {id: {type: String,required: true},active: {type: Boolean,default: false},hidden: {type: Boolean,default: false},props: {type: Object}},emits: ['set-active'],setup(props, context) {const editWrapper = ref<null | HTMLElement>(null)const onItemClick = (id: string) => {context.emit('set-active', id)}const gap = {x: 0,y: 0}const styles = computed(() => pick(props.props, ['position', 'top', 'left', 'width', 'height']))const startMove = (e: MouseEvent) => {const currentElement = editWrapper.valueif (currentElement) {const { left, top } = currentElement.getBoundingClientRect() gap.x = e.clientX - leftgap.y = e.clientY - topconsole.log(gap)}}return {onItemClick,styles,editWrapper,startMove}}
})
</script><style>
.edit-wrapper {padding: 0px;cursor: pointer;border: 1px solid transparent;user-select: none;
}
.edit-wrapper > * {position: static !important;width: 100% !important;height: 100% !important;left: auto !important;top: auto !important;
}
.edit-wrapper:hover {border: 1px dashed #ccc;
}
.edit-wrapper.hidden {display: none;
}
.edit-wrapper.active {border: 1px solid #1890ff;user-select: none;z-index: 1500;
}
</style>
拖动移动实现元素移动:
HTMLElement.offsetTop
HTMLElement.offsetTop 为只读属性,它返回当前元素相对于其 offsetParent 元素的顶部内边距的距离。和getBoundingClientRect有些类似
// EditWrapper.vue
<divclass="edit-wrapper"ref="editWrapper":style="styles":data-component-id="id"@mousedown="startMove"@click="onItemClick(id)":class="{ active: active, hidden: hidden }"
></div>// 在移动的过程中,计算top和left的值
const caculateMovePosition = (e: MouseEvent) => {// 拿到画布最外层的dom元素(offsetLeft也可以使用Element.getBoundingClientRect())// 由于 canvas-area 元素的定位是fixed,所以其offsetParent为null,返回的值和 Element.getBoundingClientRect()是一样的const container = document.getElementById('canvas-area') as HTMLElementconst left = e.clientX - gap.x - container.offsetLeft;const top = e.clientY - gap.y - container.offsetTopconsole.log(container.offsetParent);console.log(container.offsetLeft, container.getBoundingClientRect().left);return {left,top,};
};
const startMove = (e: MouseEvent) => {const currentElement = editWrapper.value;if (currentElement) {const { left, top } = currentElement.getBoundingClientRect();gap.x = e.clientX - left;gap.y = e.clientY - top;console.log(gap);}const handleMove = (e: MouseEvent) => {const { left, top } = caculateMovePosition(e);console.log(left, top);if (currentElement) {currentElement.style.top = top + 'px'currentElement.style.left = left + 'px'}};// 鼠标松开的时候,做一些清除的工作const handleMouseUp = () => {document.removeEventListener('mousemove', handleMove)} document.addEventListener('mousemove', handleMove);document.addEventListener('mouseup', handleMouseUp);
};
这里还是有个问题:松开鼠标的时候,位置恢复到了原来的位置。
原因是我们的数据流是自上而下的,这个坐标值是从上面的属性中props中传递下来的,我们现在是直接在样式中进行修改的,所以当松开鼠标的时候,原来的属性并没有进行修改,就会回到原来的位置。现在需要在松开鼠标的时候,发射一个事件,触发对应的mutation,更新鼠标的坐标值。
拖动移动更新元素属性:
<template><divclass="edit-wrapper"ref="editWrapper":style="styles"@mousedown="startMove"@click="onItemClick(id)":class="{ active: active, hidden: hidden }"><slot></slot></div>
</template><script lang="ts">
// EditWrapper.vue
import { defineComponent, computed, ref } from 'vue';
import { pick } from 'lodash-es';
export default defineComponent({props: {id: {type: String,required: true,},active: {type: Boolean,default: false,},hidden: {type: Boolean,default: false,},props: {type: Object,},},emits: ['set-active', 'update-position'],setup(props, context) {const editWrapper = ref<null | HTMLElement>(null);const onItemClick = (id: string) => {context.emit('set-active', id);};const gap = {x: 0,y: 0,};const styles = computed(() =>pick(props.props, ['position', 'top', 'left', 'width', 'height']));const caculateMovePosition = (e: MouseEvent) => {const container = document.getElementById('canvas-area') as HTMLElement;const left = e.clientX - gap.x - container.offsetLeft;const top = e.clientY - gap.y - container.offsetTop;return {left,top,};};// 这里添加这个标识,主要是为了让鼠标只有在移动完成之后才能进行更新,直接在元素上面进行点击,触发一套mouseup,mousedown动作,是不需要更新的。let isMoving = false;// @mousedown="startMove"const startMove = (e: MouseEvent) => {const currentElement = editWrapper.value;if (currentElement) {const { left, top } = currentElement.getBoundingClientRect();gap.x = e.clientX - left;gap.y = e.clientY - top;console.log(gap);}const handleMove = (e: MouseEvent) => {const { left, top } = caculateMovePosition(e);isMoving = true;console.log(left, top);if (currentElement) {currentElement.style.top = top + 'px';currentElement.style.left = left + 'px';}};const handleMouseUp = (e: MouseEvent) => {document.removeEventListener('mousemove', handleMove);if (isMoving) {const { left, top } = caculateMovePosition(e);context.emit('update-position', { left, top, id: props.id });isMoving = false;}// 做清理工作nextTick(() => {document.removeEventListener('mouseup', handleMouseUp);
});};document.addEventListener('mousemove', handleMove);document.addEventListener('mouseup', handleMouseUp);};return {onItemClick,styles,editWrapper,startMove,};},
});
</script><style>
.edit-wrapper {padding: 0px;cursor: pointer;border: 1px solid transparent;user-select: none;
}
.edit-wrapper > * {position: static !important;width: 100% !important;height: 100% !important;left: auto !important;top: auto !important;
}
.edit-wrapper:hover {border: 1px dashed #ccc;
}
.edit-wrapper.hidden {display: none;
}
.edit-wrapper.active {border: 1px solid #1890ff;user-select: none;z-index: 1500;
}
</style>
拖动改变大小
根本目的
改变大小最终的目的也是通过一系列的鼠标事件来改变一系列定位的值,上一次我们改变的值只有 top,left,现在还有有 width 和 height。
创建 handler
创建四个点就可以了,分别位于这个图层的四个角上。
创建这四个 handler 应该不是很难,我们只需要创建四个对应的 div,将他们做成圆形,然后让它们使用绝对定位,设置 top,left,right,bottom 值即可,就可以创建出这样的一个样式。
添加事件
我们分别在四个点,添加 mouseDown,mouseMove,然后到 mouseUp 的一系列事件,完成整个过程。
之前在改变定位的过程中,我们只需要在移动的时候改变 top,left 值即可,现在拖动改变大小要比原来复杂一些,还有 width 和 height 值的修改,同时对于四个角度的拖动,有不同的处理。
请看具体的交互图

拖动改变大小代码实现
- 实现右下方拖拽大小
首先对先择块的样式进行处理,添四个点的css 样式
.edit-wrapper .resizers {display: none;
}
.edit-wrapper.active .resizers {display: block;
}
.edit-wrapper.active .resizers .resizer {width: 10px;height: 10px;border-radius: 50%;background: #fff;border: 3px solid #1890ff;position: absolute;
}
.edit-wrapper .resizers .resizer.top-left {left: -5px;top: -5px;cursor: nwse-resize;
}
.edit-wrapper .resizers .resizer.top-right {right: -5px;top: -5px;cursor: nesw-resize;
}
.edit-wrapper .resizers .resizer.bottom-left {left: -5px;bottom: -5px;cursor: nesw-resize;
}
.edit-wrapper .resizers .resizer.bottom-right {right: -5px;bottom: -5px;cursor: nwse-resize;
}

- 接下来添加拖动右下脚圆点改区块大小(最简单方法)
// EditWrapper.vue
// 如果不给 resizer添加stop事件,由于冒泡事件机制,所以会冒泡到最外层editWrapper上面,从而触发 startMove事件
<divclass="edit-wrapper"ref="editWrapper":style="styles":data-component-id="id"@mousedown="startMove"@click="onItemClick(id)":class="{ active: active, hidden: hidden }"
><slot></slot><div class="resizers"><divclass="resizer top-left"@mousedown.stop="startResize('top-left')"></div><divclass="resizer top-right"@mousedown.stop="startResize('top-right')"></div><divclass="resizer bottom-left"@mousedown.stop="startResize('bottom-left')"></div><divclass="resizer bottom-right"@mousedown.stop="startResize('bottom-right')></div></div>
</div>const startResize = () => {const currentElement = editWrapper.value;const handleMove = (e: MouseEvent) => {if (currentElement) {const { left, top } = currentElement.getBoundingClientRect();currentElement.style.height = e.clientY - top + 'px';currentElement.style.width = e.clientX - left + 'px';}};const handleMouseUp = () => {document.removeEventListener('mousemove', handleMove);};document.addEventListener('mousemove', handleMove);document.addEventListener('mouseup', handleMouseUp);};
我们已实现右下脚拖动改变
现在就是在其他几个方向重用这个方法进行尺寸改变
type ResizeDirection = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
interface OriginalPositions {left: number;right: number;top: number;bottom: number;
}const caculateMovePosition = (e: MouseEvent) => {const container = document.getElementById('canvas-area') as HTMLElement;const left = e.clientX - gap.x - container.offsetLeft;const top = e.clientY - gap.y - container.offsetTop;return {left,top,};};const caculateSize = (direction: ResizeDirection,e: MouseEvent,positions: OriginalPositions) => {const { clientX, clientY } = e;const { left, right, top, bottom } = positions;const container = document.getElementById('canvas-area') as HTMLElement;const rightWidth = clientX - left;const leftWidth = right - clientX;const bottomHeight = clientY - top;const topHeight = bottom - clientY;const topOffset = clientY - container.offsetTop;const leftOffset = clientX - container.offsetLeft;switch (direction) {case 'top-left':return {width: leftWidth,height: topHeight,top: topOffset,left: leftOffset,};case 'top-right':return {width: rightWidth,height: topHeight,top: topOffset,};case 'bottom-left':return {width: leftWidth,height: bottomHeight,left: leftOffset,};case 'bottom-right':return {width: rightWidth,height: bottomHeight,};default:break;}};const startResize = (direction: ResizeDirection) => {const currentElement = editWrapper.value as HTMLElement;const { left, right, top, bottom } =currentElement.getBoundingClientRect();const handleMove = (e: MouseEvent) => {const size = caculateSize(direction, e, { left, right, top, bottom });const { style } = currentElement;if (size) {style.width = size.width + 'px';style.height = size.height + 'px';if (size.left) {style.left = size.left + 'px';}if (size.top) {style.top = size.top + 'px';}}};const handleMouseUp = () => {document.removeEventListener('mousemove', handleMove);};document.addEventListener('mousemove', handleMove);document.addEventListener('mouseup', handleMouseUp);};
- 数据更新
- 将变化数据发射出去:
const handleMouseUp = (e: MouseEvent) => {document.removeEventListener('mousemove', handleMove);const size = caculateSize(direction, e, { left, right, top, bottom });context.emit('update-position', { ...size, id: props.id})nextTick(() => {document.removeEventListener('mouseup', handleMouseUp)})};
修改Editor.vue中事件监听
const updatePosition = (data: {left: number;top: number;id: string;
}) => {const { id } = data;const updatedData = pickBy<number>(data, (v,k) => k !== 'id')forEach(updatedData, (v, key) => {store.commit('updateComponent', { key, value: v + 'px', id})})
};
修复有滚动条时的Bug:
在contanier出现滚动条,并且把滚动条滚动到下方,将元素向上拖,元素会出现向上的突然抖动,会造成数据的错误。

最终的效果:

相关文章:
web架构师编辑器内容-拖动元素改变元素的位置和大小的完成
拖动移动元素 改变编辑器的定位系统 我们目前的元素都是按照块级元素直接自上而下的排列在画布中,为了让元素实现精确的定位和调整,我们需要改变这些元素的定位实现。我们需要让这些元素画布区域来进行绝对定位。如果我们有一个元素有这些已经保存的 c…...
基于CNN的水果识别-含数据集训练模型
数据集介绍,下载本资源后,界面如下: 有一个文件夹一个是存放数据集的文件。 数据集介绍: 一共含有:8个类别,包含:Apple, Banana, Cherry, Dragon Fruit, Mango, Orange, Papaya, Pineapple等。 然后本地的train.txt和…...
Hadoop基本概论
目录 一、大数据概论 1.大数据的概念 2.大数据的特点 3.大数据应用场景 二、Hadoop概述 1.Hadoop定义 2.Hadoop发展历史 3.Hadoop发行版本 4.Hadoop优势 5.Hadoop1.x/2.x/3.x 6.HDFS架构 7.Yarn架构 8.MapReduce架构 9.大数据技术生态体系 一、大数据概论 1.大数…...
2023年12月 Scratch 图形化(一级)真题解析#中国电子学会#全国青少年软件编程等级考试
Scratch图形化等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 观察下列每个圆形中的四个数,找出规律,在括号里填上适当的数?( ) A:9 B:17 C:21 D:5 答案:C 左上角的数=下面两个数的和+右上角的数...
burp靶场--访问控制【越权】
【Burp系列】超全越权漏洞实验总结 https://portswigger.net/web-security/access-control/lab-unprotected-admin-functionality 1. 访问控制【越权】 https://portswigger.net/web-security/access-control#what-is-access-control ### 什么是访问控制: 访问控…...
C#使用DateTime.Now静态属性动态获得系统当前日期和时间
目录 一、实例 1.源码 2.生成效果 二、相关知识点 1.Thread类 (1)Thread.Sleep()方法 (2)Thread(ThreadStart) (3)IsBackground (4)Invoke( ) 2.CreateGrap…...
华为机考入门python3--(0)模拟题2-vowel元音字母翻译
分类:字符串 知识点: 字符串转list,每个字符成为list中的一个元素 list(string) 字符串变大小写 str.upper(), str.lower() 题目来自【华为招聘模拟考试】 # If you need to import additional packages or classes, please import …...
【轮式平衡机器人】——角度/速度/方向控制分析软件控制框架
轮式平衡机器人具有自不稳定性,可类比一级倒立摆系统的控制方法,常见有反馈线性化方法、非线性PID控制、自适应控制、自抗扰控制,还有改进的传统缺乏对外界干扰和参数改变鲁棒性的滑模变结构控制。我们采用较为简单的双闭环PID控制实现平衡模…...
HYBBS 表白墙网站PHP程序源码 可封装成APP
源码介绍 PHP表白墙网站源码,可以做校园内的,也可以做校区间的,可封装成APP。告别QQ空间的表白墙吧。 安装PHP5.6以上随意 上传程序安装,然后设置账号密码,登陆后台切换模板手机PC都要换开启插件访问前台。 安装完…...
【设计模式】适配器和桥接器模式有什么区别?
今天我探讨一下适配器模式和桥接模式,这两种模式往往容易被混淆,我们希望通过比较他们的区别和联系,能够让大家有更清晰的认识。 适配器模式:连接不兼容接口 当你有一个类的接口不兼容你的系统,而你又不希望修改这个…...
C语言应用层程序热补丁
一、热补丁简介 一个正在运行的程序,要是有某函数或某流程有问题,需要修改,有两个方式: 1.通过设置LD_PRELOAD把需要的库重新定向,但这种方式需要重启正在运行的程序。 2.通过修改可执行文件某个函数指向的地址&…...
【代码随想录+力扣hot100】双指针
文章目录 27. 移除元素思路:代码: 26. 删除有序数组中的重复项代码:思路一:重复元素必相邻思路二:从第一个位置开始考虑快慢指针 977.有序数组的平方思路:代码: 283. 移动零代码:思路…...
【Java程序员面试专栏 专业技能篇】MySQL核心面试指引(三):性能优化策略
关于MySQL部分的核心知识进行一网打尽,包括三部分:基础知识考察、核心机制策略、性能优化策略,通过一篇文章串联面试重点,并且帮助加强日常基础知识的理解,全局思维导图如下所示 本篇Blog为第三部分:性能优化策略,子节点表示追问或同级提问 读写分离 分布式数据库的…...
qnx 上screen + egl + opengles 最简实例
文章目录 前言一、qnx 上的窗口系统——screen二、screen + egl + opengles 最简实例1.使用 addvariant 命令创建工程目录2. 添加源码文件3. common.mk 文件4. 编译与执行总结参考资料前言 本文主要介绍如何在QNX 系统上使用egl和opengles 控制GPU渲染一个三角形并显示到屏幕上…...
python基础学习-02
基本的程序设计模式 任何的程序设计都包含IPO,它们分别代表如下: I:Input 输入,程序的输入 P:Process 处理,程序的主要逻辑过程 O:Output 输出,程序的输出 因此如果想要通过计算…...
服务调用Ribbon,LoadBalance,Feign
服务调用Ribbon、Fegin Ribbon实现负载均衡的原理 1:LoadBalancerAutoConfiguration这个类,这个类主要做的就是把LoadBalancer拦截器封装到RestTemplte拦截器集合里面去。 2:然后在代码里面调用restTemplate.getForObject或者其他方法的时候&…...
一条sql是如何运行的
在我们平时使用sql的时候,基本是基于黑盒的使用方式,在客户端输入一条sql语句,然后回显想要的数据,对于mysql server端内部如何运行的以及与存储引擎如何交互的不得而知。 通过下面一幅图,大致描述客户端和服务端交互…...
SystemC学习笔记(三) - 查看模块的波形
简述 波形在Simulation/Emulation中地位十分重要,尤其是在研发初期,只能通过波形来查看软件hang住的位置。 对于TLM来说,查看波形一般是指查看pvbus上的transaction,而对于SystemC本身来说,查看波形就是使用Gtkwave或…...
计算机网络(第六版)复习提纲5
SS2.2 有关信道的几个基本概念 2.通信模型 三个主要部分:信源、信道、信宿 3.通信方式: a)术语:消息(传递的内容)、数据(传递的形式)、信号(数据表现形式,有模拟信号和数字信号两种&…...
JavaScript 学习笔记(WEB APIs Day3)
「写在前面」 本文为 b 站黑马程序员 pink 老师 JavaScript 教程的学习笔记。本着自己学习、分享他人的态度,分享学习笔记,希望能对大家有所帮助。推荐先按顺序阅读往期内容: 1. JavaScript 学习笔记(Day1) 2. JavaSc…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...
招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...
Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...
Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...
Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
Linux中《基础IO》详细介绍
目录 理解"文件"狭义理解广义理解文件操作的归类认知系统角度文件类别 回顾C文件接口打开文件写文件读文件稍作修改,实现简单cat命令 输出信息到显示器,你有哪些方法stdin & stdout & stderr打开文件的方式 系统⽂件I/O⼀种传递标志位…...
