本文将接着之前的低代码编辑器学习第三天往后写,主要实现鼠标移动到组件上时,组件会被一层蒙层覆盖
前言
实现蒙层的大体思路:给渲染组件的外层div绑定onMouseOver和onMouseLeave事件,通过onMouseOver事件获取到当前悬停组件的id,并将其保存,再在该div内(也就是渲染组件的同级下)创建另一个div,用于盛放蒙层组件,接着创建蒙层组件HoverMask,将当前悬停组件的位置获取到,然后蒙层组件使用绝对定位,将位置设置的和当前悬停组件位置一样,最后鼠标移出,调用onMouseLeave函数将当前悬停组件id设为undefined。
一、先为已创建的Page,Container,Button组件添加一个data-component-id属性
主要作用:为后续使用document.querySelector方法快速定位被选中的组件做准备
js
//Button/index.tsx
import { Button as AntdButton } from "antd"
import type { CommonComponentProps } from "../../interface"
export default function Button({id,type,text,styles}:CommonComponentProps) {
return (
<AntdButton data-component-id={id} type={type} style={styles}>{text}</AntdButton>
)
}
//Page/index.tsx
<div
data-component-id={id}
ref={dropRef as any}
className=" p-[20px] h-[100%] box-border"
style={{ border: canDrop ? '2px solid red' : 'none'}}
>
{children}
</div>
//Container/index.tsx
<div
data-component-id={id}
ref={dropRef as any}
className={`min-h-[100px] p-[20px]
${canDrop ? 'border-[2px] border-[blue]' : 'border-[1px] border-[#000] '}`}>
{children}
</div>
二、获取当前悬停组件的id,并保存
目前所编辑的文件为EditArea/index.tsx
1.在渲染组件的外层div上添加onMouseOver和onMouseLeave事件
js
//onMouseOver用于获取当前悬停组件id
//onMouseLeave鼠标离开事件,将hoverComponentId设置为undefined,表示无组件被选中
<div className=' h-[100%]' onMouseOver={handleMouseOver} onMouseLeave={() => { setHoverComponentId(undefined) }}>
{renderComponents(components)}
</div>
2.定义hoverComponentId变量,存储id
js
const [hoverComponentId, setHoverComponentId] = useState<number>()//存放鼠标悬停组件的id
3.编写handleMouseOver函数获取id
js
const handleMouseOver: React.MouseEventHandler = (e) => {
//拿到冒泡机制经历的所有容器的数组,数组排序为从内向外的容器
const path = e.nativeEvent.composedPath()
for (let i = 0; i < path.length; i++) {
const ele = path[i] as HTMLElement//拿到的DOM结构
//ele.dataset:访问 DOM 元素的 data-* 属性集合(如 data-component-id)
//ele.dataset.componentId:获取 data-component-id 属性的值(即组件唯一 ID)
const componentId = ele.dataset && ele.dataset.componentId
if (componentId) {//如果拿到了组件id
setHoverComponentId(Number(componentId))//则将组件id保存在hoverComponentId中
return
}
}
}
4. 添加一个类名为portal-wrapper的div容器
用于盛放蒙层组件
在最外层div上加上一个edit-area类名
js
<div className=' h-[100%] edit-area' onMouseOver={handleMouseOver} onMouseLeave={() => { setHoverComponentId(undefined) }}>
{renderComponents(components)}
<div className='portal-wrapper'></div>
</div>
三、创建蒙层组件HoverMask
1.在components文件夹下创建HoverMask文件夹,再在其内创建index.tsx文件
HoverMask需要接收父级传过来的三个参数 :
一个是包裹渲染组件和盛放蒙层组件div的外层div的类名:containerClassName ,用于获取当前悬停组件的DOM结构
一个是当前悬停组件的id:componentId ,也是用于获取当前悬停组件的DOM结构
一个是盛放蒙层的div的类名:portalWrapperClassName,用于盛放蒙层组件
2. 完成蒙层组件,包括以下逻辑
-
使用React自带的createPortal创建蒙层的DOM结构,同时使用useMemo缓存获取到的类名为portalWrapperClassName的div容器,后将蒙层DOM结构放入其内
-
定义position变量,用于存储蒙层位置数据
-
定义属性蒙层位置的函数updatePosition,用于获取当前悬停组件位置数据,将蒙层位置数据实时与其相等
-
当然,最后也可以在蒙层左上角加上每个组件的名称,这要去useComponentsStore仓库内取
以下为HoverMask组件的学习代码,内含详细注释
js
// 导入React核心库及必要钩子
import React, { useEffect, useMemo, useState } from 'react'
// 导入React Portal API,用于将组件渲染到DOM树的指定位置
import { createPortal } from 'react-dom'//创建html结构
// 导入组件状态管理相关工具函数
import { getComponentById, useComponentsStore } from '../../stores/components'
/**
* HoverMask组件属性接口定义
* @interface HoverMaskProps
* @property {string} containerClassName - 包裹渲染组件的容器元素的CSS类名,用于定位计算
* @property {number} componentId - 当前悬停组件的唯一ID
* @property {string} portalWrapperClassName - Portal挂载蒙层组件的CSS类名
*/
interface HoverMaskProps {
containerClassName: string
componentId: number
portalWrapperClassName: string
}
/**
* 组件悬停蒙层组件
* 功能:当鼠标移到组件上时触发,显示半透明遮盖层及组件信息标签
* @param {HoverMaskProps} props - 组件属性
* @returns {JSX.Element} 通过Portal渲染的蒙层元素
*/
//HoverMask会在鼠标移到组件上时触发,会用一层遮盖层遮盖当前组件
export default function HoverMask({ containerClassName, componentId, portalWrapperClassName }: HoverMaskProps) {
// 从组件状态管理中获取所有组件数据,用于获取悬停组件的名称
const { components } = useComponentsStore()
/**
* 蒙层位置与尺寸状态
* @type {{top: number, left: number, width: number, height: number, labelTop: number, labelLeft: number}}
* @property {number} top - 蒙层顶部定位(相对于容器)
* @property {number} left - 蒙层左侧定位(相对于容器)
* @property {number} width - 蒙层宽度(与目标组件一致)
* @property {number} height - 蒙层高度(与目标组件一致)
* @property {number} labelTop - 信息标签顶部定位
* @property {number} labelLeft - 信息标签左侧定位
*/
const [position, setPosition] = useState({
top: 0,
left: 0,
width: 0,
height: 0,
labelTop: 0,//控制文字位置
labelLeft: 0,//控制文字位置
})
/**
* 副作用:组件ID变化时更新蒙层位置
* 依赖项:[componentId] - 仅当组件ID变化时触发
*/
useEffect(() => {
updatePosition()//更新蒙层位置
}, [componentId])
/**
* 更新蒙层位置与尺寸
* 功能:计算蒙层组件在容器中的相对位置,并更新position状态
*/
function updatePosition() {
// 校验组件ID是否存在
if (!componentId) {
return
}
// 获取包裹渲染组件的容器元素(通过类名选择器)
const container = document.querySelector(`.${containerClassName}`)
if (!container) {
return
}
// 获取悬停组件元素(通过data属性选择器)
const node = container.querySelector(`[data-component-id="${componentId}"]`)
if (!node) {
return
}
// 获取悬停组件在视口中的绝对位置与尺寸
const { top, left, width, height } = node.getBoundingClientRect()
// 获取容器元素在视口中的绝对位置
const { top: containerTop, left: containerLeft } = container.getBoundingClientRect()
// 计算相对位置并更新状态(考虑容器滚动偏移)
setPosition({
top: top - containerTop + container.scrollTop,
left: left - containerLeft + container.scrollTop,
width,
height,
labelTop: top - containerTop + container.scrollTop,
labelLeft: left - containerLeft + width,
})
}
/**
* 缓存Portal挂载目标元素
* 功能:通过类名查找并缓存蒙层要挂载的DOM节点
* 依赖项:[] - 仅在组件初始化时执行一次
*/
const el = useMemo(() => {
return document.querySelector(`.${portalWrapperClassName}`)//将蒙层的组件添加到该类名的div上
}, [])
/**
* 缓存当前组件数据
* 功能:根据组件ID从全局状态中查找并缓存当前组件信息
* 依赖项:[componentId, components] - 组件ID或组件列表变化时更新
*/
const curComponent = useMemo(() => {
return getComponentById(componentId, components)
}, [componentId])
// 使用Portal将蒙层渲染到目标容器
return createPortal((
<>
{/* 半透明蓝色蒙层 - 用于高亮显示悬停组件 */}
<div style={{
position: 'absolute',
top: position.top,
left: position.left,
width: position.width,
height: position.height,
background: 'rgba(0,0,255,0.1)',// 10%透明度的蓝色背景
border: '1px dashed bule',// 蓝色虚线边框
borderRadius: '4px',// 圆角边框
boxSizing: 'border-box',// 盒模型计算方式:包含边框和内边距
pointerEvents: 'none',// 鼠标事件穿透,不干扰下层元素交互
zIndex: 12,// 层级高于普通组件,低于信息标签
}}></div>
{/* 组件信息标签 - 显示组件描述 */}
<div
style={{
position: 'absolute',
top: position.labelTop,
left: position.labelLeft,
fontSize: 12,
zIndex: 13,// 层级高于蒙层
display: (!position.width || position.width < 10 ? 'none' : 'inline-block'),// 组件宽度过小时隐藏标签
transform: 'translate(-100%,-100%)',// 向左上方向平移自身尺寸的100%(标签定位在组件左上角外部)
}}>
<div
style={{
padding: '0px 8px',
backgroundColor: 'blue',
color: '#fff',
borderRadius: 4,
cursor: 'pointer',
whiteSpace: 'nowrap',// 防止文本换行
}}
>{curComponent?.desc}</div> {/* 显示组件描述信息 */}
</div>
</>
), el as HTMLElement) // Portal挂载目标元素(强制类型断言为HTMLElement)
}
3.在EditArea/index.tsx中调用蒙层组件
js
import HoverMask from '../HoverMask'
<div className=' h-[100%] edit-area' onMouseOver={handleMouseOver} onMouseLeave={() => { setHoverComponentId(undefined) }}>
{renderComponents(components)}
{
hoverComponentId && <HoverMask containerClassName='edit-area' componentId={hoverComponentId} portalWrapperClassName='portal-wrapper' />
}
<div className='portal-wrapper'></div>
</div>
效果图

小结
本文实现了给鼠标悬停组件添加一层浅蓝色蒙层,下期实现鼠标点击选中后添加一层浅蓝色蒙层