JS分层架构低代码跨iframe拖拽示例详解

目录
  • 低代码引擎
  • 唤起渲染器 iframe
  • 拖拽定位
    • 1. 绑定拖放事件
    • 2. 获取拖拽过程中的 LocationEvent
    • 3. 获取离鼠标最近的 Node
    • 4. 获取拖拽对象最近的可放置容器
    • 5. 计算被拖动的对象在容器中的插入点
    • 6. 在界面上提示最近的插入位置
  • 写在后面

低代码引擎

低代码引擎是低代码分层架构中最复杂的部分,引擎的核心功能包含入料、设计、画布渲染和出码等,它们的含义如下:

  • 入料:向引擎注入设置器、插件和组件。
  • 设计:对组件进行布局设置、属性设置以及增删改操作后,形成符合页面搭建协议的JSON Schema。
  • 画布渲染:将 JSON Schema 渲染成 UI 界面。
  • 出码:将 JSON Schema 转化成手写代码,这通常发生在页面发布的时候。

本文主要介绍拖拽定位,即:拖拽过程中探测组件的可插入点。为了给渲染器提供一个纯净的渲染环境,渲染器和设计器处于不同的 iframe 中,因此拖拽组件,不仅涉及在同一个 iframe 中拖拽组件,还涉及跨 iframe 拖拽组件。渲染器所在的 iframe 由设计器唤起,在正式介绍拖拽定位之前,先介绍如何唤起渲染器 iframe。

唤起渲染器 iframe

iframe 元素的常见用法是将它的 src 属性设置成一个固定的网页地址,让它在当前网页嵌入另一个已经存在的网页,但渲染器没有固定的网页地址,所以在这里要使用一种不常见的用法,即调用 document.write 方法给 iframe 所在的文档写入它要加载的内容。设计器唤起渲染器 iframe 的流程如下图所示:

设计器环境和渲染器环境通过 host 相互通信,SimulatorRenderer 给 host 提供了一些 API 帮助设计器完成交互,设计器给 host 提供了一些 API 帮助渲染器完成画布渲染。

在设计器环境中与渲染器环境相关的只是一个 iframe 元素,如下:

<iframe
     name="SimulatorRenderer"
     className="vitis-simulator-frame"
     style={frameStyle}
     ref={this.mountContentFrame}
/>

往 iframe 写入内容发生在 host.mountContentFrame 方法中,代码片段如下:

this.frameDocument!.open()
this.frameDocument!.write(
     `<!doctype html>
      <html class="engine-design-mode">
        <head>
<meta charset="utf-8"/>
// 这里是渲染器环境要加载的css样式脚本
          ${styleTags}
        </head>
        <body>
	     // 这里是渲染器环境要加载的js脚本
            ${scriptTags}
        </body>
     </html>`
)
   this.frameDocument!.close()
// 监听iframe加载成功和加载失败的事件
this.frameWindow!.addEventListener('load', loaded);
this.frameWindow!.addEventListener('error', errored);

用低代码引擎设计界面时,为了让渲染器环境能成功的显示画布,上述 scriptTags 中至少包含 react、react-dom 和 vitis-lowcode-simulator-renderer 的js 脚本,在开发阶段 vitis-lowcode-simulator-renderer 的 js 脚本地址是 http://localhost:5555/js/simulator-renderer.js,等发布之后vitis-lowcode-simulator-renderer 的 js 脚本地址是其 npm 包的 js 地址。

拖拽定位

拖拽定位指的是当组件在画布区域拖动时,界面实时的显示组件最近的可放置位置,这是一个与设计器强相关的功能,所以与设计器处于同一个 iframe,相关的 DOM 元素被叠放在画布区域的上面,如下图所示:

上图蓝线所在的位置就是被拖动组件最近可放置的位置,实现该功能需用到 Element.getBoundingClientRect() 方法和 HTML5 的拖放事件。给渲染器中的低代码组件设置 ref 属性,当其装载到界面上即可得到组件的 DOM 元素,从而计算出拖拽过程中鼠标经过的低代码组件。

低代码组件的拖拽能力由 Dragon 实例提供,与拖拽相关的概念有如下3个:

  • DragObject:被拖拽的对象,它是画布中的低代码组件或组件面板上的低代码组件。
  • LocationEvent:携带了拖拽过程中产生的坐标信息和被拖拽的对象。
  • DropLocation:被拖拽对象在画布上最近的可放置点。

DragObject 是一个联合类型,拖拽不同位置的低代码组件,它的类型有所不同,其接口类型定义如下:

interface DragNodeObject {
    type: DragObjectType.Node; // 被拖拽的是画布中的低代码组件
    node: Node;
  }
  interface DragNodeDataObject {
    type: DragObjectType.NodeData; // 被拖拽的是组件面板上的低代码组件
    data: ComponentSpec;
  }
type DragObject = DragNodeObject | DragNodeDataObject

设计器用 LocationEvent 来计算被拖拽对象最近的可放置点,其接口类型定义如下:

interface LocationEvent {
  dragObject: DragObject,
  originalEvent: DragEvent,
  clientX: number,
  clientY: number
}

上述接口中 clientY 和 clientX 来自于 DragEvent 对象,它们用来计算画布中离鼠标最近的 Node。

DropLocation是拖拽操作要计算的结果,接口类型定义如下:

interface DropLocation {
  // 被拖拽对象可放置的容器
  containerNode: Node;
  // 被拖拽对象在容器中的插入点
  index: number;
}

以拖拽组件面板中的低代码组件为例,在画布区域显示组件最近的可放置点,总体而言,需经历6个步骤。

1. 绑定拖放事件

iframe 和组件面板中的低代码组件绑定拖放事件,得到 DragObject,代码片段如下:

// 当组件面板中的组件开始拖动时
<div draggable={true} onDragStart={() => onDragStart(item.packageName)}>xxx</div>
const onDragStart = (packageName: string) => {
  // 得到DragObject
dragon.onNodeDataDragStart(packageName)
}
// 给 iframe 绑定dragover事件,当拖动操作进入画布区域时触发事件
this.frameDocument?.addEventListener('dragover', (e: DragEvent) => {
      e.preventDefault()
      this.project.designer.dragon.onDragOver(e)
})

2. 获取拖拽过程中的 LocationEvent

LocationEvent 将在 iframe 的 dragover 事件处理程序中实时获取,代码如下:

onDragOver = (e: DragEvent) => {
    // 获取 locateEvent 只是简单的取值
	const locateEvent = this.createLocationEvent(e)
}
createLocationEvent = (e: DragEvent): LocationEvent => {
        return {
            dragObject: this.dragObject,
            originalEvent: e,
            clientX: e.clientX,
            clientY: e.clientY
        }
}

3. 获取离鼠标最近的 Node

Node 被装载在渲染器环境中,只有 SimulatorRenderer 实例才知道每个 Node 的位置,因此这一步需要调用 SimulatorRenderer 给 host 提供的getClosestNodeIdByLocation 方法,getClosestNodeIdByLocation 的代码如下:

getClosestNodeIdByLocation = (point: Point): string | undefined => {
    // 第一步:找出包含 point 的全部 dom 节点
    const suitableContainer = new Map<string, DomNode>()
    for (const [id, domNode] of reactInstanceCollector.domNodeMap) {
        const rect = this.getNodeRect(id)
        if (!domNode || !rect) continue
        const { width, height, left, top } = rect
        if (left < point.clientX && top < point.clientY && width + left > point.clientX && height + top > point.clientY) {
            suitableContainer.set(id, domNode)
        }
    }
    // 第二步:找出离 point 最近的 dom 节点
    const minGap: {id: string| undefined; minArea: number} = {
        id: undefined,
        minArea: Infinity
    }
    for (const [id, domNode] of suitableContainer) {
        const { width, height } = domNode.rect
        if (width *  height  < minGap.minArea) {
            minGap.id = id;
            minGap.minArea = width *  height
        }
    }
    return minGap.id
}

上述 reactInstanceCollector 对象中保存了画布上全部低代码组件的 DOM 节点,实现这个目的需借助 React 的 ref 属性,在这里不展开介绍。

4. 获取拖拽对象最近的可放置容器

每个低代码组件都能设置嵌套规则,规定哪些组件能做为它的子元素和父元素,不符合规则的组件则不可放置,在这一步将使用组件的嵌套规则,代码如下:

getDropContainer = (locateEvent: LocationEvent) => {
    // 从上一步得来的潜在容器
    let containerNode = this.host.getClosestNodeByLocation({clientX: locateEvent.clientX, clientY: locateEvent.clientY})
    const thisComponentSpec: ComponentSpec = locateEvent.dragObject.data
    while(containerNode) {
        if (containerNode.componentSpec.isCanInclude(thisComponentSpec)) {
            return containerNode
        } else {
            // 继续往上找父级
            containerNode = containerNode.parent
        }
    }
}

5. 计算被拖动的对象在容器中的插入点

容器可能包含多个子元素,在这一步将利用鼠标位置计算被拖动的对象在容器中的插入点,得到最终的 DropLocation ,代码如下:

// 初始值
const dropLocation: DropLocation = { index: 0, containerNode: container}
const { childrenSize, lastChild } = container
const { clientY } = locateEvent
if (lastChild) {
    const lastChildRect = this.designer.getNodeRect(lastChild.id)
    // 判断是否要插到容器的末尾
    if (lastChildRect &amp;&amp; clientY &gt; lastChildRect.bottom) {
        dropLocation.index = childrenSize
    } else {
        let minDistance = Infinity
        // 容器中最近的插入点
        let minIndex = 0
        for (let index = 0 ; index &lt; childrenSize; index ++) {
            const child = container.getChildAtIndex(index)!
            const rect = this.designer.getNodeRect(child.id)
            if (rect &amp;&amp; Math.abs(rect.top - clientY) &lt; minDistance) {
                minDistance = Math.abs(rect.top - clientY)
                minIndex = index
            }
        }
        dropLocation.index = minIndex
    }
}
return dropLocation

6. 在界面上提示最近的插入位置

经过前面的步骤已经得到了插入位置,现在需要在界面上给用户显示相应的提示,这里要用到状态管理库 MobX,在此之前需将 Dragon 实例变成一个可观察对象,再在React组件中使用 mobx-react 导出的 observer,代码如下:

import { observer } from 'mobx-react'
observer(function InsertionView() {
    const [style, setStyle] = useState<React.CSSProperties>({})
    useEffect(() => {
        const dropLocation = observableProject.designer.dragon.dropLocation
        if (!dropLocation) {
            setStyle({})
        } else {
            const { width, left, top } = dropLocation.containerRect
            setStyle({
                borderTopStyle: 'solid',
                width,
                left,
                top
            })
        }
    }, [observableProject.designer.dragon.dropLocation])
    return (
       // 这个元素被绝对定位到画布区域的上面
        <div className='vitis-insertion-view' style={style}></div>
    )
})

当 dragon.dropLocation 的值发生变化时,InsertionView 组件将重新渲染,实时的给用户提示拖拽对象对接的可插入点。

写在后面

低代码的拖拽定位远不止本文介绍的这些功能,至少还包含悬停探测,详情可查看开源项目。 该开源项目持续更新。

以上就是JS分层架构低代码跨iframe拖拽示例详解的详细内容,更多关于JS分层架构跨iframe拖拽的资料请关注我们其它相关文章!

(0)

相关推荐

  • JS前端canvas交互实现拖拽旋转及缩放示例

    目录 正文 拖拽 旋转 缩放 小结 正文 到目前为止,我们已经能够对物体进行点选和框选的操作了,但是这还不够,因为并没有什么实际性的改变,并且画布看起来也有点呆板,所以这个章节的主要目的就是让画布中的物体活起来,其实就是增加一些常见的交互而已啦,比如拖拽.旋转和缩放.这是这个系列最重要的章节之一,希望能够对你有所帮助. 拖拽 先来说说拖拽平移的实现吧,因为它最为简单

  • 图形编辑器中JS实现防误操作之拖拽阻塞

    目录 图形编辑器中 代码改造 结尾 图形编辑器中 在图形编辑器中,想象这么一个场景,我们撤销了一些重要的操作,然后想选中一个图形,看看它的属性.你点了上去,然后你发现你再也无法重做了. 你以为你点了一下,但其实你点击的时候,鼠标还是小小移动了一点,飘了一个像素点.对编辑器来说,它识别到让图形移动一个像素点的操作,就生成了一个新的版本,然后重做栈(redoStack)被清空了,你退回前的操作就没了. 为了解决这类用户微小操作的问题,我们可以巧妙地给拖拽行为加一个 阻塞阈值.具体就是就是按下鼠标后,

  • Fabric.js 拖拽平移画布方法示例

    目录 正文 原理解析 按下鼠标时 移动鼠标时 松开鼠标时 代码仓库 正文 使用 fabric.js 创建出来的画布默认是不能拖拽移动的. 不过我们可以利用一些小技巧让画布具有被拖拽的能力,fabric.js 官网也提供了一个 demo ,但文档上并没有详细的讲解拖拽画布的实现原理. 本文就粗略分析一下这个原理. 原理解析 鼠标拖拽的原理其实很简单,主要就3步: 鼠标点击元素 移动鼠标 松开鼠标 在鼠标移动时,获取鼠标当前位置,然后修改被拖拽元素的位置. 当松开鼠标时,也要获取松手那刻鼠标所在位置

  • JavaScript实现拖拽排序的方法详解

    目录 实现原理概述 代码实现 完整代码实现 可拖拽排序的菜单效果大家想必都很熟悉,本次我们通过一个可拖拽排序的九宫格案例来演示其实现原理. 先看一下完成效果: 实现原理概述 拖拽原理 当鼠标在[可拖拽小方块](以下简称砖头)身上按下时,开始监听鼠标移动事件 鼠标事件移动到什么位置,砖头就跟到什么位置 鼠标抬起时,取消鼠标移动事件的监听 排序原理 提前定义好9大坑位的位置(相对外层盒子的left和top) 将9大砖头丢入一个数组,以便后期通过splice方法随意安插和更改砖头的位置 当拖动某块砖头

  • js 实现div拖拽拉伸完整示例

    目录 前言 HTML CSS JS 前言 今天和大家分享一下.如何用js实现div拖拽拉伸等功能.功能比较简单,我就不赘述了.直接上代码. HTML <div class="resize" data-key="drag"> <img src="../images/liya.jpg" alt=""> <div class="line line-n" data-key="

  • 使用纯JS实现checkbox的框选效果(鼠标拖拽多选)

    目录 主要思路 css 代码如下 html结构如下 js主要逻辑如下 总结 主要思路 用一个盒子作为选区,通过定位让其固定在左上角,由于没有给定选区元素的宽高所以默认不显示,在 onmousemove 中动态获取选区定位的top left bottom right四个属性,同时将鼠标拖拽的距离作为选区的宽高,由于给选区元素的css设置了border就呈现出如图所示的框选效果.(注意:要想自己手动勾选复选框,要给选区元素的css设置pointer-events: none;否则点击复选框的事件会被

  • React结合Drag API实现拖拽示例详解

    目录 认识拖拽 被拖拽元素 可释放目标 生命周期 拖拽操作中的数据传输 代码实现 如何标记当前拖拽的元素? 在画布中拖动 数据结构 总结 认识拖拽 鼠标拖拽是一个常见的交互场景,在这个熟悉的过程将会发生哪些事件? 拖拽事件指用户通过鼠标(或其他指针设备)将元素移到一个新的位置上.拖拽过程涉及两个对象:被拖拽元素(上图中 A )和可释放目标(上图中 B ) 被拖拽元素 默认情况下,图片.链接和文本是可拖动的.HTML5 在所有 HTML 元素上规定了一个 draggable 属性, 表示元素是否可

  • JS前端同源策略和跨域及防抖节流详解

    目录 引言 jQuery中JSONP的实现 防抖[重要] 缓存搜索的列表 1 定义全局缓存对象 2:将搜索结果存储到缓存对象中 3优先从缓存中获取搜索列表 节流[重点] 防抖和节流的区别 引言 协议,域名,端口相同,就具有相同的源 同源策略:浏览器提供的一个安全策略 跨域的出现原因:浏览器的同源策略不允许非同源的URL之间进行资源的交互 解决跨域由两种方式:JSONP, CORS JSONP: 只支持GET请求 通过script标签的src属性,请求跨域的数据接口,并通过函数调用的形式,接收跨域

  • 微服务架构之服务注册与发现实践示例详解

    目录 1 服务注册中心 4种注册中心技术对比 2 Spring Cloud 框架下实现 2.1 Spring Cloud Eureka 2.1.1 创建注册中心 2.1.2 创建客户端 2.2 Spring Cloud Consul 2.2.1 Consul 的优势 2.2.2 Consul的特性 2.2.3 安装Consul注册中心 2.2.4 创建服务提供者 3 总结 微服务系列前篇 详解微服务架构及其演进史 微服务全景架构全面瓦解 微服务架构拆分策略详解 微服务架构之服务注册与发现功能详解

  • Vue Element Sortablejs实现表格列的拖拽案例详解

    1. css:    dragTable.css @charset "UTF-8"; .w-table{ height: 100%; width: 100%; float: left; } /* 拖动过程中,鼠标显示样式 */ .w-table_moving .el-table th .thead-cell{ cursor: move !important; } .w-table_moving .el-table__fixed{ cursor: not-allowed; } .w-ta

  • JavaScript鼠标拖拽事件详解

    本文实例为大家分享了js鼠标拖拽事件的详细实现代码,供大家参考,具体内容如下 图片如下: css代码 <style> *{ margin:0; padding:0; } #box{ width: 200px; height: 200px; background: url("./img/aio.png") no-repeat; background-size: cover; position: absolute;/*定位元素 父级元素window就是初始包含块*/ top:0

  • JavaScript使用面向对象实现的拖拽功能详解

    本文实例讲述了JavaScript使用面向对象实现的拖拽功能.分享给大家供大家参考,具体如下: 面向对象有个前提: 前提:所有东西都必须包含在onload里 改写:不能有函数嵌套,可以有全局变量 过程,如下 onload改成构造函数, 全局变量改成属性(通过this) 函数改写成方法 <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/

  • C#组件FormDragger窗体拖拽器详解

    适用:.net2.0+ winform项目 介绍: 类似QQ.迅雷等讲究UI体验的软件,都支持在窗口内多处地方拖动窗口,而不必老实巴交的去顶部标题栏拖,这个组件就是让winform也能这样随性拖拽,随性度或更甚.先看效果: 可拖拽的地方包括不限于: 窗体.Panel.GroupBox.TabControl等容器控件的空白区: 菜单栏.工具栏.状态栏等bar的空白区,以及无效项目: Label.PictureBox.ProgressBar等通常不与鼠标交互的控件: 一切无效控件(Enabled为f

  • js对table的td进行相同内容合并示例详解

    复制代码 代码如下: function hb(){ var tab = document.getElementById("subtable"); var maxCol = 3, val, count, start; var ys=""; for(var col = maxCol-1; col >= 0 ; col--) { count = 1; val = ""; for(var i=0; i<tab.rows.length; i++

  • Python代码缩进和测试模块示例详解

    前言 Python代码缩进和测试模块是大家学习python必不可少的一部分,本文主要介绍了关于Python代码缩进和测试模块的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 一.Python代码缩进 Python 函数没有明显的 begin 和 end ,没有标明函数的开始和结束的花括号.唯一的分隔符是一个冒号 ( : ),接着代码本身是缩进的. 例如:缩进 buil dCon necti onStr ing 函数 def buildConnectionString(

  • JS中DOM元素的attribute与property属性示例详解

    一.'表亲戚':attribute和property 为什么称attribute和property为'表亲戚'呢?因为他们既有共同处,也有不同点. attribute 是 dom 元素在文档中作为 html 标签拥有的属性: property 是 dom 元素在 js 中作为对象拥有的属性. 从定义上可以看出: 对于 html 的标准属性来说,attribute 和 property 是同步的,是会自动更新的 但是对于自定义的属性来说,他们是不同步的.(自定义属性不会自动添加到property)

随机推荐