《从0到1搭建低代码平台-编辑拖拽实现》

前言

为什么要写这篇文章?最近在公司中参与开发低代码平台项目,在学习过程中发现很多成熟的低代码平台业务包袱和设计较重不利于自己对于核心逻辑的理解,由此希望搭建一个迷你版的低代码平台加深自己的理解,也帮助更多人更轻松的去学习低代码平台的搭建。代码开源,有需要的朋友自取😁!本章我们将实现效果如下:

1、能从左侧物料面板将组件拖拽至编辑区域渲染

2、编辑区域组件能在编辑区域内自由拖拽

实现左侧物料面板拖拽

实现效果

属性方法介绍

1、draggable

draggable属性是HTML5中的一个属性,用于标识元素是否可以被拖动。

2、dragStart

dragstart事件是在拖动操作开始时触发的事件。具体来说,它是在用户开始拖动一个可拖动元素时触发的。

3、dragEnter

dragenter事件是在拖动元素进入一个可放置区域时触发的事件。它是拖放操作中的一个事件,用于标识拖动元素进入目标区域。

4、dragOver

dragover事件是在拖动元素在一个可放置区域上移动时持续触发的事件。它可以用来监听拖动元素在目标区域上的移动,并根据需要执行相应的操作。

5、dragLeave

dragLeave是一个拖放事件,它在拖动元素离开一个可放置区域时触发。当拖动元素从一个可放置区域拖出时,浏览器会触发dragLeave事件。

6、drop

drop事件是一个拖放事件,在拖动元素释放时触发。它用于处理拖放操作的最终放置行为。

实现过程

1、将每个组件 drabggble 属性设置 true 标识元素可拖拽,并监听 dragStart 事件在拖拽开始时记录被拖拽组件信息,代码如下所示:

javascript 复制代码
import './index.css';
import registerConfig, { IComponent } from './registerConfig';
import { GlobalContext } from '../../store';
import React from 'react';

let MaterialPanel = (_props: IProps) => {
    const { setCurrentMaterial } = React.useContext(GlobalContext);
    
    // 监听 dragStart 事件在拖拽开始时记录被拖拽组件信息
    const handleDragStart = (component: IComponent) => {
        setCurrentMaterial(component);
    };

    return (
        <div>
            {registerConfig.componentList.map(component => (
                <div key={component.type} className="editor-left-item">
                    <span>{component.label}</span>
                    <div draggable onDragStart={() => handleDragStart(component)}>
                        {component.preview()}
                    </div>
                </div>
            ))}
        </div>
    );
};

interface IProps {}

export { MaterialPanel };

2、组件渲染区域监听元素进入事件

  • dragEnter:当拖拽元素进入渲染区域时改变光标手势,表示元素进入渲染区域
  • dragLeave:当拖拽元素离开渲染区域时恢复正常光标,表示元素离开渲染区域
  • dragOver:取消默认的拖放行为,如阻止打开文件链接
  • drop:当放置元素时生成元素配置加入渲染区域渲染

代码实现如下:

javascript 复制代码
import './index.css';
import React, { useRef } from 'react';
import { GlobalContext } from '../../store';
import { Block } from './Block';
import { IComponent } from '../MaterialPanel/registerConfig';

//私有常量

//可抽离的逻辑处理函数/组件

/**
 * 【组件功能】
 *
 * 【应用模块】
 *
 */

let CanvasArea = (_props: IProps) => {
    //变量声明、解构
    const { currentMaterial, setCurrentMaterial, setCurrentSchema, currentSchema } = React.useContext(GlobalContext);
    const ref = useRef(null);

    //组件状态

    //网络IO

    //数据转换

    //逻辑处理函数
    //当拖拽元素进入渲染区域时改变光标手势,表示元素进入渲染区域
    const handleDragEnter = (event: { dataTransfer: { dropEffect: string } }) =>
        (event.dataTransfer.dropEffect = 'move');

    //当拖拽元素离开渲染区域时恢复正常光标,表示元素离开渲染区域
    const handleDragLeave = (event: { dataTransfer: { dropEffect: string } }) =>
        (event.dataTransfer.dropEffect = 'none');

    //取消默认的拖放行为,如阻止打开文件链接
    const handleDragOver = (event: { preventDefault: () => any }) => event.preventDefault();

    //当放置元素时生成元素配置加入渲染区域渲染
    const handleDrop = (event: any) => {
        const { offsetX, offsetY } = event.nativeEvent;
        const config = {
            type: currentMaterial?.type,
            alignCenter: true, // 表示拖拽到画布后,基于鼠标位置居中展示
            focus: false,
            style: {
                width: undefined,
                height: undefined,
                left: offsetX,
                top: offsetY,
                zIndex: 1,
            },
        };
        currentSchema.blocks.push(config);
        setCurrentSchema({ ...currentSchema });
        setCurrentMaterial(null);
    };


    return (
        <div
            id="canvas-container"
            onDragEnter={handleDragEnter}
            onDragOver={handleDragOver}
            onDragLeave={handleDragLeave}
            onDrop={handleDrop}
            style={{ ...currentSchema.container }}
            ref={ref}
        >
            {currentSchema.blocks.map((block: IComponent, index: number) => (
                <Block key={index} block={block} onMouseDown={e => handleMouseDown(e, index)} parentRef={ref} />
            ))}
        </div>
    );
};

interface IProps {}

export { CanvasArea };

元素拖放至编辑区域后是如何完成渲染的?

单独看下这个拖放监听事件代码:

javascript 复制代码
//当放置元素时将元素加入渲染区域渲染
const handleDrop = (event: any) => {
    // 1、 获取元素位置
    const { offsetX, offsetY } = event.nativeEvent;
    // 2、生成组件配置
    const config = {
        type: currentMaterial?.type,
        focus: false,
        style: {
            left: offsetX,
            top: offsetY,
            zIndex: 1,
        },
    };
    // 3、将组件配置加入画布配置中
    currentSchema.blocks.push(config);
    // 4、更新画布配置
    setCurrentSchema({ ...currentSchema });
    // 5、清空当前选中的物料组件
    setCurrentMaterial(null);
};

渲染区域是根据一份全局的配置文件(称为schema) 进行渲染的,我们拖放元素到渲染区域时做的事情就是将元素信息写入schema,然后触发渲染区域重新渲染。此时实现效果如下:

细心的🧑‍🎓可能会发现,元素存在偏移,因为我们是以鼠标相对容器的位置作为在元素在容器的位置,如果要正确渲染应该获取元素的左上角相对于容器的位置作为元素的偏移位置

如何获取元素左上角相对于容器的位置?

通过鼠标事件回调参数,我们可以获取到鼠标相对于元素左边缘的位置信息,因此我们只需要在开始拖拽元素时记录鼠标相对于元素的左边缘位置信息,还有在鼠标放置元素时元素相对于容器的左边缘的位置信息,这两个值相减,即可得到最终的偏移量,代码实现如下:

javascript 复制代码
// 拖拽开始
const handleDragStart = (e: any, component: IComponent) => {
    // 开始拖拽元素记录鼠标相对于元素的左边缘位置信息
    const { offsetX, offsetY } = e.nativeEvent;
    component.offsetInfo = {
        offsetX,
        offsetY,
    };
    component.element = e?.target ?? null;
    setCurrentMaterial(component);
};

// 拖拽放置
const handleDrop = (event: any) => {
    // 1、 获取元素位置
    // 放置元素时元素相对于容器的左边缘的位置信息
    const { offsetX, offsetY } = event.nativeEvent;

    // 开始拖拽元素时鼠标相对于元素的左边缘位置信息
    const offsetInfo = currentMaterial?.offsetInfo ?? { offsetX: 0, offsetY: 0 };


    // 两个值相减即可得修正后元素偏移量
    const left = offsetX - offsetInfo.offsetX;
    const top = offsetY - offsetInfo.offsetY;


    // 2、生成组件配置
    const config = {
        type: currentMaterial?.type,
        focus: false,
        style: {
            left,
            top,
            zIndex: 1,
        },
    };
    // 3、将组件配置加入画布配置中
    currentSchema.blocks.push(config);
    // 4、更新画布配置
    setCurrentSchema({ ...currentSchema });
    // 5、清空当前选中的物料组件
    setCurrentMaterial(null);
};

如何保障元素在正确的位置上渲染且不超出容器位置?

对元素偏移位置做限制,偏移量不可超过容器大小,代码如下所示:

javascript 复制代码
//当放置元素时将元素加入渲染区域渲染
    const handleDrop = (event: any) => {
        // 1、 获取元素位置
        const { offsetX, offsetY } = event.nativeEvent;
        const { clientWidth, clientHeight } = currentMaterial?.element ?? { clientWidth: 0, clientHeight: 0 };
        const offsetInfo = currentMaterial?.offsetInfo ?? { offsetX: 0, offsetY: 0 };

        // 修正元素偏移量
        const left = offsetX - offsetInfo.offsetX;
        const top = offsetY - offsetInfo.offsetY;
        
        // 元素可偏移的最大位置
        const maxLeft = currentSchema.container.width - clientWidth;
        const maxTop = currentSchema.container.height - clientHeight;

        // 限制元素位置不超出渲染区域
        const curLeft = Math.max(Math.min(left, maxLeft), 0);
        const curTop = Math.max(Math.min(top, maxTop), 0);

        // 2、生成组件配置
        const config = {
            type: currentMaterial?.type,
            focus: false,
            style: {
                left: curLeft,
                top: curTop,
                zIndex: 1,
            },
        };
        // 3、将组件配置加入画布配置中
        currentSchema.blocks.push(config);
        // 4、更新画布配置
        setCurrentSchema({ ...currentSchema });
        // 5、清空当前选中的物料组件
        setCurrentMaterial(null);
    };

编辑区域组件实现自由拖拽

实现效果

属性方法介绍

1、mousedown

mousedown是一个鼠标事件,它在用户按下鼠标按钮时触发。

注:click是按下并释放才触发,mousedown是按下时就触发

2、mousemove

mousemove是一个鼠标事件,它在用户移动鼠标时触发。

3、mouseup

mouseup是一个鼠标事件,它在用户抬起鼠标按钮时触发。

实现过程

1、监听渲染区域元素的鼠标按下事件,在按下时保留元素的起始位置并监听鼠标的移动和抬起事件

javascript 复制代码
let Block = (_props: IProps) => {

    const handleMouseDown = (e: any) => {
        onMouseDown(e);
        
        _onMouseDown(e);
    };

    const _onMouseDown = (e: { clientX: number; clientY: number }) => {
        // 记录起始位置
        const initialPlace = {
            startX: e.clientX,
            startY: e.clientY,
        };

        // 动态绑定移动事件和鼠标抬起事件
        document.addEventListener('mousemove', _blockMouseMove);
        document.addEventListener('mouseup', blockMouseUp);
    };

    return (
        <div
            className={`editor-block ${block.focus ? 'editor-block-focus' : ''}`}
            style={blockStyle}
            ref={blockRef}
            onMouseDown={handleMouseDown}
        >
            {RenderComponent}
        </div>
    );
};

export { Block };

mousemove、mouseup事件为什么要动态绑定?

在渲染区域节点较多的情况下可以减少绑定事件内存开销,跟react事件机制的思路一致

2、利用鼠标移动事件在移动的时候实时更新元素位置,代码实现如下:

javascript 复制代码
// 在移动的时候实时更新元素位置
const blockMouseMove = (initialPlace: { startX: number; startY: number }) => {
        return (e: { clientX: number; clientY: number }) => {
            const { clientX, clientY } = e;

            // 计算移动的距离
            const movX = clientX - initialPlace.startX;
            const movY = clientY - initialPlace.startY;

            if (parentRef.current && blockRef.current) {
                const { clientWidth: pW, clientHeight: pH } = parentRef.current;
                const { clientWidth: bW, clientHeight: bH } = blockRef.current;

                // 限制移动范围在渲染区域内 start
                const maxX = pW - bW;
                const maxY = pH - bH;

                // @ts-ignore
                let newLeft = blockStyle.left + movX;
                // @ts-ignore
                let newTop = blockStyle.top + movY;

                newLeft = Math.max(0, Math.min(newLeft, maxX));
                newTop = Math.max(0, Math.min(newTop, maxY));
                // 限制移动范围在渲染区域内 end


                // 更新位置
                // @ts-ignore
                block.style.top = newTop;
                // @ts-ignore
                block.style.left = newLeft;
                // 更新视图
                setCurrentSchema({ ...currentSchema });
            }
        };
    };

3、在抬起的时候移除移动和抬起事件,代码实现如下:

javascript 复制代码
// 在抬起的时候移除移动和抬起事件
const blockMouseUp = () => {
    document.removeEventListener('mousemove', _blockMouseMove);
    document.removeEventListener('mouseup', blockMouseUp);
};

学习资料

相关推荐
SameX5 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_32 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito35 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184552 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室3 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang4 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发4 小时前
解锁微前端的优秀库
前端
王解5 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js