学习低代码编辑器第四天

本文将接着之前的低代码编辑器学习第三天往后写,主要实现鼠标移动到组件上时,组件会被一层蒙层覆盖

前言

实现蒙层的大体思路:给渲染组件的外层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>

效果图

小结

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

相关推荐
GSDjisidi2 小时前
日本IT就职面试|仪容&礼仪篇分享建议
面试·职场和发展
一只小风华~3 小时前
JavaScript 函数
开发语言·前端·javascript·ecmascript·web
仰望星空的凡人4 小时前
【JS逆向基础】数据库之MongoDB
javascript·数据库·python·mongodb
樱花开了几轉5 小时前
React中为甚么强调props的不可变性
前端·javascript·react.js
Mr...Gan6 小时前
VUE3(四)、组件通信
前端·javascript·vue.js
楚轩努力变强8 小时前
前端工程化常见问题总结
开发语言·前端·javascript·vue.js·visual studio code
前端开发爱好者9 小时前
只有 7 KB!前端圈疯传的 Vue3 转场动效神库!效果炸裂!
前端·javascript·vue.js
Fly-ping9 小时前
【前端】JavaScript文件压缩指南
开发语言·前端·javascript
接口写好了吗9 小时前
【el-table滚动事件】el-table表格滚动时,获取可视窗口内的行数据
javascript·vue.js·elementui·可视窗口滚动
岁忧9 小时前
(LeetCode 面试经典 150 题 ) 155. 最小栈 (栈)
java·c++·算法·leetcode·面试·go