web架构师编辑器内容-拖动元素改变元素的位置和大小的完成

news2024/10/3 10:41:12

拖动移动元素

改变编辑器的定位系统

我们目前的元素都是按照块级元素直接自上而下的排列在画布中,为了让元素实现精确的定位和调整,我们需要改变这些元素的定位实现。我们需要让这些元素画布区域来进行绝对定位。如果我们有一个元素有这些已经保存的 css 属性,那么它就可以在编辑器,或者是在另外的 H5 端渲染出这样的一个样式。

基本指导思想

交互的最终结果只是修改这些样式而已,比如拖动定位,最终就是在修改 top 和 left 的值而已,那么缩放大小,最终就是在修改 width 和 height 的值而已。

基本分析

1 拖动是在按下鼠标,然后鼠标移动这个过程中发生的。所以首先我们要响应的是鼠标按下按下的时候,也就是 MouseDown 的时候开始运作。
2 在鼠标移动的时候,我们需要将 top,left 的值更新到新的值,这个就是过程的重点。
结合交互图进行分析:可以在线查看,地址为:https://whimsical.com/RTJphPrwzksyotCdA32LQU@VsSo8s35WxESA3XwhpMUni

图片描述

计算鼠标点下去,元素偏移量:

getBoundingClientRect
Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。
getBoundingClientRect返回值

<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.value
      if (currentElement) {
        const { left, top } = currentElement.getBoundingClientRect() 
        gap.x = e.clientX - left
        gap.y = e.clientY - top
        console.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
<div
  class="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 HTMLElement
	const left = e.clientX - gap.x - container.offsetLeft;
	const top = e.clientY - gap.y - container.offsetTop
	console.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>
  <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">
// 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事件
<div
  class="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">
    <div
      class="resizer top-left"
      @mousedown.stop="startResize('top-left')"
    ></div>
    <div
      class="resizer top-right"
      @mousedown.stop="startResize('top-right')"
    ></div>
    <div
      class="resizer bottom-left"
      @mousedown.stop="startResize('bottom-left')"
    ></div>
    <div
      class="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出现滚动条,并且把滚动条滚动到下方,将元素向上拖,元素会出现向上的突然抖动,会造成数据的错误。
在这里插入图片描述
最终的效果:
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第15届蓝桥杯嵌入式省赛准备第三天总结笔记(使用STM32cubeMX创建hal库工程+串口接收发送)

因为我是自己搞得板子&#xff0c;原本的下程序和串口1有问题&#xff0c;所以我用的是串口2&#xff0c;用的PA2和PA3 一&#xff0c;使用CubeMX配置串口 选择A开头的这个是异步通信。 配置串口参数&#xff0c;往届的题基本用的9600波特率&#xff0c;所以我这里设置为9600…

【Linux】Ubuntu的gnome切换KDE Plasma

文章目录 安装KDE Plasma桌面环境添加软件源并更新apt安装kubuntu-desktop&#xff08;作者没有成功&#xff09;aptitude安装kubuntu-desktop多次aptitude install&#xff08;特别重要特别重要&#xff09;其他kde软件包 卸载gnome桌面 Ubuntu自带的桌面环境是gnome&#xff…

cuda二进制文件中到底有些什么

大家好。今天我们来讨论一下&#xff0c;相比gcc编译器编译的二进制elf文件&#xff0c;包含有 cuda kernel 的源文件编译出来的 elf 文件有什么不同呢&#xff1f; 之前研究过一点 tvm。从 BYOC 的框架中可以得知&#xff0c;前端将模型 partition 成 host 和 accel(accel 表…

《WebKit 技术内幕》之六(2): CSS解释器和样式布局

2 CSS解释器和规则匹配 在了解了CSS的基本概念之后&#xff0c;下面来理解WebKit如何来解释CSS代码并选择相应的规则。通过介绍WebKit的主要设施帮助理解WebKit的内部工作原理和机制。 2.1 样式的WebKit表示类 在DOM树中&#xff0c;CSS样式可以包含在“style”元素中或者使…

【QT+QGIS跨平台编译】之四:【libSSH2+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

文章目录 一、libSSH2介绍二、文件下载三、文件分析四、pro文件五、编译实践 一、libSSH2介绍 libSSH2是一个开源的C函数库&#xff0c;用来实现SSH2协议。 SSH(Secure SHell)到目前为止有两个不兼容的版本——SSH1和SSH2。 SSH2避免了RSA的专利问题&#xff0c;并修补了CRC…

C#winform上位机开发学习笔记2-串口助手的定时发送功能添加

1.功能描述 选择自动发送功能后&#xff0c;按照设定的发送时间发送数据 2.代码部分 增加计时器空间Timer 使能计时器&#xff0c;默认设置定时时间为1秒 组合框设置默认复选信息 编写选择框事件函数 //自动发送事件private void checkBox27_CheckedChanged(object sender, E…

37-WEB漏洞-反序列化之PHPJAVA全解(上)

WEB漏洞-反序列化之PHP&JAVA全解&#xff08;上&#xff09; 一、PHP 反序列化原理二、案例演示2.1、无类测试2.1.1、本地2.1.2、CTF 反序列化小真题2.1.3、CTF 反序列化类似题 2.2、有类魔术方法触发2.2.1、本地2.2.2、网鼎杯 2020 青龙大真题 三、参考资料 一、PHP 反序列…

Excel新建文件打开后提示文件扩展名与文件格式不匹配

环境&#xff1a; Win10专业版 excel2016 问题描述&#xff1a; Excel新建文件打开后提示文件扩展名与文件格式不匹配 解决方案&#xff1a; 1.调出注册表编辑器&#xff0c;按层点击文件夹&#xff1a;HKEY_CURRENT_USER/Software/Microsoft/Office/12.0/Excel/Securit…

PSoc62™开发板之rtc时间获取

实验目的 1.使用PSoc62™芯片读取内部rtc时间 2.OLED屏幕显示当前时间戳 实验准备 PSoc62™开发板SSD1306 OLED模块公母头杜邦线 芯片资源 PSoC 6系列MCU时钟系统由以下几部分组成&#xff0c;PSoc62™开发板没有接外部时钟源&#xff0c;所以只能从IMO、ILO、PILO里边配…

Linux 命令大全 CentOS常用运维命令

文章目录 1、Linux 目录结构2、解释目录3、命令详解3.1、shutdown命令3.1、文件目录管理命令ls 命令cd 命令pwd 命令tree 命令mkdir 命令touch 命令cat 命令cp 命令more 命令less 命令head 命令mv 命令rm 命令ln 命令tail 命令cut命令 3.2、用户管理useradd/userdel 命令用户的…

STM32标准库——(2)GPIO输出

1.GPIO简介 GPIO&#xff08;General Purpose Input Output&#xff09;通用输入输出口可配置为8种输入输出模式引脚电平&#xff1a;0V~3.3V&#xff0c;部分引脚可容忍5V输出模式下可控制端口输出高低电平&#xff0c;用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等输入模…

小程序学习-20

建议每次构建npm之前都先删除miniprogram_npm

2024最新版Python 3.12.1安装使用指南

2024最新版Python 3.12.1安装使用指南 Installation and Configuration Guide to the latest version Python 3.12.1 in 2024 By Jackson Python编程语言&#xff0c;已经成为全球最受欢迎的编程语言之一&#xff1b;它简单易学易用&#xff0c;以标准库和功能强大且广泛外挂…

Docker(十)Docker Compose

作者主页&#xff1a; 正函数的个人主页 文章收录专栏&#xff1a; Docker 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01; Docker Compose 项目 Docker Compose 是 Docker 官方编排&#xff08;Orchestration&#xff09;项目之一&#xff0c;负责快速的部署分布式…

2024PMP考试新考纲-【过程领域】近期典型真题和很详细解析(9)

华研荟继续为您分享【过程Process领域】的新考纲下的真题&#xff0c;帮助大家体会和理解新考纲下PMP的考试特点和如何应用所学的知识和常识&#xff08;经验&#xff09;来解题&#xff0c;并且举一反三&#xff0c;一次性3A通过2024年PMP考试。 2024年PMP考试新考纲-【过程领…

智能算法 | Matlab实现改进黑猩猩优化算法SLWCHOA与多个基准函数对比与秩和检验

智能算法 | Matlab实现改进黑猩猩优化算法SLWCHOA与多个基准函数对比与秩和检验 目录 智能算法 | Matlab实现改进黑猩猩优化算法SLWCHOA与多个基准函数对比与秩和检验预测效果基本描述程序设计参考资料 预测效果 基本描述 1.Matlab实现改进黑猩猩优化算法SLWCHOA与多个基准函数…

Spring Boot3整合Druid(监控功能)

目录 1.前置条件 2.导依赖 错误依赖&#xff1a; 正确依赖&#xff1a; 3.配置 1.前置条件 已经初始化好一个spring boot项目且版本为3X&#xff0c;项目可正常启动。 作者版本为3.2.2最新版 2.导依赖 错误依赖&#xff1a; 这个依赖对于spring boot 3的支持不够&#…

Linux第34步_TF-A移植的第2步_修改设备树和tf-a.tsv

在虚拟机中&#xff0c;使用VSCode打开linux /atk-mp1/atk-mp1/my-tfa/目录下tf-a.code-workspace”&#xff1b; 找到“tf-a-stm32mp-2.2.r1/fdts”目录&#xff0c;就是设备树文件所在的目录。 见下图&#xff1a; 一、修改“stm32mp157d-atk.dts” 修改后&#xff0c;见下…

文件上传笔记整理

文件上传 web渗透的核心&#xff0c;内网渗透的基础 通过上传webshell文件到对方的服务器来获得对方服务器的控制权 成功条件 文件成功上传到对方的服务器&#xff08;躲过杀软&#xff09; 知道文件上传的具体路径 上传的文件可以执行成功 文件上传的流程 前端JS对上传文件进行…

python_ACM模式《剑指offer刷题》链表1

题目&#xff1a; 面试tips&#xff1a; 询问面试官是否可以改变链表结构 思路&#xff1a; 1. 翻转链表&#xff0c;再遍历链表打印。 2. 想要实现先遍历后输出&#xff0c;即先进后出&#xff0c;因此可借助栈结构。 3. 可用隐式的栈结构&#xff0c;递归来实现。 代码…