飞道的博客

可视化搭建平台之跨iframe拖拽

609人阅读  评论(0)

前言

前段时间做运营活动搭建平台,其中一个主要功能:编辑页面分为左侧-组件区与右侧-预览区,需要实现组件区的内容可自由放置到预览区内。

类似下图所示:

社区内有一些类似的功能实现,但使用的方式大同小异,都离不开拖拽能力。我们日常开发中会经常用到的拖拽,如拖拽排序,拖拽上传等。当然拖拽的 npm 包也有很多,比较好用的包有 react-dnd, vue 自带的拖拽能力等。

但我们的预览区采用的是 iframe 方式,社区好用的类库一般不支持跨 iframe 的拖拽的能力。此处我们选择了使用原生拖拽 drag 和 dropAPI

需要实现的主要功能,有两点:

1、检测拖动到 iframe 内部和外部。

2、数据驱动来进行 iframe 内部组件的展示。

我们简单生成页面的功能:


   
  1. //搭建编辑页
  2. //drag.jsx
  3. import React, { useState, useEffect } from  'react';
  4. import Drag from  './drag.js';
  5. require( './styles.less');
  6. //iframe hooks
  7. const useIframeLoad = () => {
  8.    const [iframeState, setIframeState] = useState( false);
  9.    const [windowState, setWindowState] = useState( document.readyState ===  "complete");
  10.    const iframeLoad = () => {
  11.      const iframeEle = document.getElementById( "my-iframe");
  12.     iframeEle && setIframeState(iframeEle.contentDocument.readyState ===  "complete");
  13.      if (!iframeState && iframeEle) {
  14.       iframeEle.onload = () => {
  15.         setIframeState( true);
  16.       };
  17.     }
  18.   };
  19.   useEffect(() => {
  20.      if (!windowState) {
  21.       setIframeState( false);
  22.       window.addEventListener( 'load', () => {
  23.         setWindowState( true);
  24.         iframeLoad();
  25.       })
  26.     }  else {
  27.       iframeLoad();
  28.     }
  29.   }, []);
  30.    return iframeState;
  31. }
  32. export  default () => {
  33.    const init = () => {
  34.     Drag.init({
  35.       dragEle: document.getElementById( 'drag-box'),
  36.       dropEle: document.getElementById( 'my-iframe').contentDocument.getElementById( 'drop-box')
  37.     })
  38.   }
  39.   useIframeLoad() && init();
  40.    return <>
  41.    <!-- 组件区 -->
  42.     <div id= "drag-box">
  43.       <div className= "drag-item">拖动元素</div>
  44.       <div className= "drag-item">拖动元素</div>
  45.       <div className= "drag-item">拖动元素</div>
  46.     </div>
  47.     <!-- 预览区 -->
  48.     <div className= "drop-content">
  49.       <iframe id= "my-iframe" src= "#/iframe" style={{ width:  "100%", height:  "480px", border:  "none" }}/>
  50.     </div>
  51.   </>
  52. }

预览区 iframe 页:


   
  1. //iframe.jsx
  2. import React from  'react';
  3. require( './styles.less');
  4. export  default () => {
  5.    return <div id= "drop-box">
  6.     <div className= "item">元素 1</div>
  7.     <div className= "item">元素 2</div>
  8.     <div className= "item">元素 3</div>
  9.   </div>
  10. }

此时,简单的搭建编辑布局已完成。接下来,我们看下拖拽部分:

跨 iframe 拖拽

首先我们可以看下有哪些原生事件

原生事件


   
  1. drag  // 拖动元素或文本选择时将触发此事件 (相当于拖动过程中,一直触发此事件)
  2. dragstart  //当用户开始拖动一个元素或者一个选择文本的时候 ,将触发此事件
  3. dragend  //当拖动操作结束时(通过释放鼠标按钮或按退出键),将触发此事件
  4. dragover  //当被拖动元素在释放区内移动时,将触发此事件
  5. dragenter  //被拖动元素进入到释放区所占据得屏幕空间时,将触发此事件
  6. dragleave  //当被拖动元素没有放下就离开释放区时,将触发此事件
  7. dragexit  //当元素不再是拖动操作的立即选择目标时,将触发此事件
  8. drop  //当被拖动元素在释放区里放下时,将触发此事件

原生 drag 和 drop 拖拽

基于需求,拆分出拖拽的关键流程:

  • 初始化元素 设置拖动元素和目标节点

  • 注册事件 对拖动元素和目标节点元素注册 drag 事件

  • 监听事件 拖动过程中生成占位节点,拖动结束删除此占位节点

不完全代码如下:


   
  1. //drag.js
  2. class Drag {
  3.   params = {}
  4.   init = (params) => {
  5.     ....
  6.   };
  7.    //初始化设置拖动元素
  8.   initDrag = dragEle => {
  9.      if(dragEle.childNodes.length) {
  10.        const { length } = dragEle.childNodes;
  11.       let i =  0
  12.       while (i< length) {
  13.         this.setDrag(dragEle.childNodes[i]);
  14.         i +=  1;
  15.       }
  16.     }  else {
  17.       this.setDrag(dragEle);
  18.     }
  19.   }
  20.    //初始化释放区
  21.   initDrop = dropEle => {
  22.      if (dropEle.childNodes.length) {
  23.        const { length } = dropEle.childNodes;
  24.       let i =  0;
  25.       while (i < length) {
  26.         this.setDrop(dropEle.childNodes[i]);
  27.         i +=  1;
  28.       }
  29.     }  else {
  30.       this.setDrop(dropEle);
  31.     }
  32.   }
  33.    //拖动元素注册事件
  34.   setDrag = el => {
  35.     el.setAttribute( "draggable""true");
  36.     el.ondragstart = this.dragStartEvent;
  37.     el.ondrag = this.dragEvent;
  38.     el.ondragend = this.dragEndEvent;
  39.   };
  40.    //释放区注册事件
  41.   setDrop = el => {
  42.     el.ondrop = this.dropEvent;
  43.     el.ondragenter = this.dragEnterEvent;
  44.     el.ondragover = this.dragOverEvent;
  45.     el.ondragleave = this.dragLeaveEvent;
  46.   }
  47.    ......
  48.    //创建占位元素
  49.   createElePlaceholder = (() => {
  50.     let ele = null;
  51.      return () => {
  52.        if (!ele) {
  53.         ele = document.createElement( "div");
  54.         ele.setAttribute( "id""drag-ele-placeholder");
  55.         ele.innerHTML =  `<div style="width: 100%; height:50px; position: relative">
  56.             <div style="width: 150px; height: 40px; text-align: center; position: absolute;
  57.             left: 0; right: 0; top: 0; bottom:0; margin: auto; background: #878; line-height: 40px">放置组件</div>
  58.           </div>`;
  59.       }
  60.        return ele;
  61.     };
  62.   })();
  63.    //移除占位元素
  64.   removePlaceholderEle = () => {
  65.      const iframe = this.getIframe();
  66.      const removeEle = iframe.contentDocument.getElementById( "drag-ele-placeholder");
  67.      const { dropEle } = this.params;
  68.      if(this.isHasPlaceholderEle()) { dropEle.removeChild(removeEle) };
  69.   }
  70.    /****** 事件处理 ******/
  71.   dragEndEvent = ev => {
  72.     this.removePlaceholderEle()
  73.     console.log( '拖拽结束');
  74.     console.log( '删除占位元素');
  75.   };
  76.    //插入占位元素
  77.   dragEnterEvent = ev => {
  78.     ev.preventDefault();
  79.      const insertEle = this.createElePlaceholder();
  80.     ev.target.before(insertEle);
  81.     console.log( '进入到可放置区');
  82.     console.log( '插入占位元素');
  83.   };
  84.     //删除占位元素
  85.   dragLeaveEvent = ev => {
  86.     ev.preventDefault();
  87.     this.removePlaceholderEle()
  88.     console.log( '离开放置区');
  89.     console.log( '删除占位元素');
  90.   };
  91.   dropEvent = ev => {
  92.     ev.preventDefault();
  93.     console.log( '在放置区放开鼠标');
  94.   }
  95. }
  96. export  default  new Drag();

初步完成后,效果如下:

此处存在一些问题:

  1. 在插入时,页面闪烁

  2. 只有鼠标位置进入释放区,才触发进入事件

  3. 无法实现第一个元素的添加

问题分析

  • 当拖到预览区时,会触发预览区内的节点 dragenter 事件。每当在当前节点上插入占位元素时,此节点的位置会发生变化,触发节点 dragleave 事件,同时删除占位元素。此过程一直重复,导致一直闪烁。

  • 上述 2,3 问题,是由于 drag/drop 本身 api 限制

由于现在的方式无法真正完美的实现功能,决定弃用 dragover,dragenter,dragleave 事件

重新梳理需要优化的功能点:

  1. 当拖动元素和 iframe 的边有接触的时候,就代表进入释放区

  2. 拖动可以实现元素上面插入,和元素下面插入

使用坐标精准计算,来处理进入释放区和在元素上面和下面插入

对 drag.js 做些改造:


   
  1. class Drag {
  2.   params = {}
  3.    // 声明
  4.   mouseOffsetBottom =  0;
  5.   mouseOffsetRight =  0;
  6.   init = (params) => {
  7.     ...
  8.   };
  9.    //初始化设置拖动元素
  10.   initDrag = dragEle => {
  11.     ....
  12.   }
  13.    //初始化释放区
  14.   initDrop = dropEle => {
  15.     ...
  16.   }
  17.    //拖动元素注册事件
  18.   setDrag = el => {
  19.     ...
  20.   };
  21.    //释放区注册事件
  22.   setDrop = el => {
  23.     ...
  24.   }
  25.    //获取iframe的位置
  26.   getIframeOffset = () => {
  27.      const iframeEle = this.getIframe();
  28.      return iframeEle
  29.       ? this.getRealOffset(iframeEle)
  30.       : { offsetLeft:  0, offsetTop:  0 };
  31.   };
  32.    //递归计算元素距离父元素的offset
  33.   getRealOffset = (el, parentName) => {
  34.     let left = el.offsetLeft;
  35.     let top = el.offsetTop;
  36.      if (el.offsetParent && el.offsetParent.tagName !== parentName) {
  37.        const p = this.getRealOffset(el.offsetParent, parentName);
  38.       left += p.offsetLeft;
  39.       top += p.offsetTop;
  40.     }
  41.      return { offsetLeft: left, offsetTop: top };
  42.   }
  43.    //获取元素位置
  44.   getElOffset = el => {
  45.      const { offsetTop: iframeTop } = this.getIframeOffset();
  46.      const { offsetTop: targetOffsetTop } = this.getRealOffset(el);
  47.      return {
  48.       midLine: el.clientHeight /  2 + targetOffsetTop + iframeTop,
  49.       topLine: targetOffsetTop + iframeTop,
  50.       bottomLine: el.clientHeight + targetOffsetTop + iframeTop
  51.     };
  52.   };
  53.    //释放区内部元素位置
  54.   getDropOffset = () => {
  55.      const result = [];
  56.      const { dropEle } = this.params;
  57.      const el = dropEle.childNodes;
  58.     let i =  0;
  59.     while (i < el.length) {
  60.        const midLine = this.getElOffset(el[i]);
  61.       result.push(midLine);
  62.       i +=  1;
  63.     }
  64.      return result;
  65.   };
  66.    //位置比较
  67.   locationCompare = (ev) => {
  68.     let inside =  false;
  69.      const { dropEle } = this.params;
  70.     console.log(ev.clientX);
  71.      // 拖动元素的位置
  72.      const sourceRight = ev.clientX + this.mouseOffsetRight;
  73.      const sourceLeft = sourceRight - ev.currentTarget.clientWidth;
  74.      const { offsetLeft: iframeLeft } = this.getIframeOffset();
  75.      const { offsetLeft: targetLeft } = this.getRealOffset(dropEle);
  76.      /*释放区的位置*/
  77.      const targetOffsetLeft = iframeLeft + targetLeft;
  78.      const targetOffsetRight = targetOffsetLeft + dropEle.clientWidth;
  79.      if (sourceRight > targetOffsetLeft && sourceLeft < targetOffsetRight) {
  80.        //拖动到释放区
  81.       inside =  true;
  82.     }  else {
  83.        //释放区外面
  84.       inside =  false;
  85.     }
  86.      return inside;
  87.   }
  88.    //插入占位元素
  89.   insertPlaceholderEle = (sourceMidLine) => {
  90.      const dropOffset = this.getDropOffset();  //释放区的位置属性
  91.      const insertEl = this.createElePlaceholder();
  92.      const { dropEle } = this.params;
  93.      const dropEleChild = dropEle.childNodes;
  94.      if (dropOffset.length) {
  95.       dropOffset. map((item, i) => {
  96.          const Ele = dropEleChild[i];
  97.          //在元素前面插入占位元素
  98.          if (sourceMidLine > item.topLine && sourceMidLine < item.midLine) {
  99.           Ele.before(insertEl);
  100.         }
  101.          //在元素后面插入占位元素
  102.          if (sourceMidLine < item.bottomLine && sourceMidLine > item.midLine) {
  103.           this.index = i +  1;
  104.           Ele.after(insertEl);
  105.         }
  106.          //追加一个占位元素
  107.          if (sourceMidLine > dropOffset[dropOffset.length -  1].bottomLine) {
  108.           dropEle. append(insertEl);
  109.         }
  110.          return item;
  111.       });
  112.     }
  113.      //插入第一个占位元素(当iframe内部没有组件)
  114.      if (!dropEleChild.length) {
  115.       dropEle. append(insertEl);
  116.     }
  117.   }
  118.    /****** 事件处理 ******/
  119.   dragStartEvent = ev => {
  120.      // console.log('开始拖拽');
  121.      //获得鼠标距离拖拽元素的下边的距离
  122.     this.mouseOffsetBottom = ev.currentTarget.clientHeight - ev.offsetY;
  123.      //获得鼠标距离拖拽元素的右边的距离
  124.     this.mouseOffsetRight = ev.currentTarget.clientWidth - ev.offsetX;
  125.   };
  126.   dragEvent = ev => {
  127.      //获取拖拽元素中线距离屏幕上方的距离
  128.      const sourceMidLine =
  129.       ev.clientY + this.mouseOffsetBottom - ev.currentTarget.clientHeight /  2;
  130.      if(this.locationCompare(ev)) {
  131.       this.insertPlaceholderEle(sourceMidLine)
  132.       console.log( '释放区内部')
  133.     }  else {
  134.       this.removePlaceholderEle()
  135.       console.log( '释放区外面')
  136.     }
  137.   };
  138. }
  139. export  default  new Drag();

生成结果如下:

此时已经解决了不停闪烁的问题,以及精准坐标计算,实现元素的上下插入。

但是还是存在一些问题:

  • 演示图中可以明显看到,拖动元素右边刚进入 iframe 的时候,可以插入占位元素,但是等到鼠标位置进入 iframe 的时候,就会又删除了元素

这是什么原因呢?

我们看一下打印的鼠标的坐标,可以看到鼠标位置进入 iframe 的时候,ev.clientX 突变成 0,由此可见,鼠标坐标进入 iframe 的时候,就以 iframe 为窗口了。导致鼠标的位置突变成 0,就导致计算位置出现偏差,从而拖拽元素被认为不在释放区内,所以就删除了占位元素。

怎么解决这个问题呢?

想到了几个方案:

  1. 一个是监听坐标的突变情况,然后重新计算位置,进一步进行比较位置。

  2. 把 iframe 放大和屏幕大于等于屏幕的大小,从拖动开始就使得在 iframe 里面。

方案分析:

  • 第一个方案,监听坐标突变为 0 这个临界条件不靠谱,因为每隔 50ms 拖动事件才触发,根据你移动鼠标的快慢,每次鼠标进入 iframe 获取的 clientX 不一致,第一种方案不可行

  • 第二个方案,iframe 放大,理论上是可以的,我们来试试。主要是改变布局

代码如下:


   
  1. .drop-content {
  2.   position: absolute;
  3.   width:  100vw;  //iframe放大和窗口一般大
  4.   height:  100%;
  5. }
  6. #drop-box {
  7.   width:  375px;  //iframe内部元素设置宽度
  8.   margin:  100px auto;
  9.   .item {
  10.     ...
  11.   }
  12. }

演示效果如下演示可以看到,覆盖了左边的组件区。这是由于右边视图区 z-index 比较高导致的。

优化方案

有两个方案

  • 元素布局移动调换位置,让右边视图区 dom 元素放在组件区的前边。

  • 更改 z-index,让右边视图区的 z-index 低一点

方案 1

核心代码


   
  1. //drag.jsx
  2. //调换两个元素的位置
  3. <>
  4.   <div className= "drop-content">
  5.     <iframe id= "my-iframe" src= "#/iframe" style={{ width:  "100%", height:  "480px", border:  "none" }}/>
  6.   </div>
  7.   <div id= "drag-box">
  8.     <div className= "drag-item">拖动元素</div>
  9.     <div className= "drag-item">拖动元素</div>
  10.     <div className= "drag-item">拖动元素</div>
  11.   </div>
  12. </>

实现后的效果

可以看出来,完美解决了拖动的问题。但是就是对布局进行了改变。

方案 2

核心代码


   
  1. .drop-content {
  2.   position: absolute;
  3.   z-index:  -1//让iframe的z-index低一点
  4.   width:  100vw;  //iframe放大和窗口一般大
  5.   height:  100%;
  6. }
  7. #drop-box {
  8.   width:  375px;  //iframe内部元素设置宽度
  9.   margin:  100px auto;
  10.   .item {
  11.     width:  100%;
  12.     height:  50px;
  13.     background-color: # 875;
  14.   }
  15. }

实现后的效果

演示中可以看出来,拖拽的问题完美解决,但是 iframe 的里面元素点击事件没有触发。

想了想,既然 z-index 可以解决 clientX 的突变问题,那是不是可以不用放大 iframe 来做?这样也会不影响事件的触发,那我们试试吧。

核心代码


   
  1. //drag.js
  2. //开始拖拽
  3. dragStartEvent = ev => {
  4.   document.getElementsByClassName( "drop-content")[ 0].style.zIndex =
  5.      "-1";
  6. };
  7. //拖拽结束
  8. dragEndEvent = ev => {
  9.   ev.preventDefault();
  10.   document.getElementsByClassName( "drop-content")[ 0].style.zIndex =  "0";
  11. };

演示效果如下

很好,这样也可以完美解决拖动的问题,而且不用改变 dom 的位置。

滚动处理

当视图区元素比较多,页面出现滚动条时,会不会出现问题呢?我们试着把 iframe 的高度写高一点

 <iframe id="my-iframe" src="#/iframe" style={{ width: "100%", height: "880px", border: "none" }}/>

演示效果如下

演示中可以看出来,页面出现滚动条,视图区滚动上去,iframe 顶部滚入到屏幕顶部的时候,我们来拖动元素插入的时候,就会出现,错位插入,这是计算又出了问题?

仔细看看代码,iframe 顶部滚入到屏幕顶部的时候,就会出现计算出负数的情况,导致计算偏差,从而导致插入占位元素错位。


   
  1. //递归计算元素距离父元素的offset
  2. getRealOffset = (el, parentName) => {
  3.   let left = el.offsetLeft;
  4.   let top = el.offsetTop;
  5.    if (el.offsetParent && el.offsetParent.tagName !== parentName) {
  6.      const p = this.getRealOffset(el.offsetParent, parentName);
  7.     left += p.offsetLeft;
  8.     top += p.offsetTop;
  9.   }
  10.    return { offsetLeft: left, offsetTop: top };
  11. }

优化计算方案

核心代码


   
  1. //计算元素距离父元素的offset
  2. getRealOffset = (el, parentName) => {
  3.    const { left, top } = el.getBoundingClientRect();
  4.    return { offsetLeft: left, offsetTop: top };
  5. }

使用 getBoundingClientRect 这个方法获得具体窗口的位置

演示如下本次优化,可以很完美的解决了拖动的一些问题,以上两种方案都是行的。

跨 iframe 通信

如何在拖动元素插入之后,让 iframe 内部的数据也实时更新渲染呢?

思路如下:

  • iframe 内挂载一个 update 方法

  • 在拖动完成后的回调里面,调用 update,传入数据

  • 触发 iframe 内部元素的渲染

  1. 维护一个组件的数据 store,getStore,和 setStore方法


   
  1. //store.js
  2. class Store {
  3.   state = {
  4.     list: []
  5.   }
  6.   getStore = () => this.state
  7.   setStore = (data) => {
  8.     this.state = { ...this.state, ...data }
  9.   }
  10. }
  11. export  default  new Store()
  1. 组件的插入对应数据的处理,包含,add 和 insert操作,以及同步更新 iframe的方法


   
  1. // update.js
  2. import Store from  './store';
  3. const add = (params) => {
  4.    const { list } = Store.getStore()
  5.   Store.setStore({ list: [...list, params.data]})
  6. };
  7. const insert = (params) => {
  8.    const { list } = Store.getStore()
  9.    const { index } = params;
  10.   list.splice(index,  0, params.data)
  11.   Store.setStore({ list: [...list] })
  12. };
  13. const update = {
  14.   add,
  15.   insert
  16. }
  17. //更新iframe内部数据方法
  18. const iframeUpdate = (params) => {
  19.   document.getElementById( "my-iframe") &&
  20.     document.getElementById( "my-iframe").contentWindow &&
  21.     document.getElementById( "my-iframe").contentWindow.update &&
  22.     document.getElementById( "my-iframe").contentWindow.update(params);
  23. }
  24. export  default (params) => {
  25.    const {  type, ...argv } = params;
  26.    if(! typereturn Promise.reject()
  27.    return  new Promise(r => r())
  28.     .then(() => update[ type](argv))
  29.     .then(() => {
  30.        const { list } = Store.getStore()
  31.       iframeUpdate(list)
  32.     })
  33. }
  1. 拖动的时候,拖动完毕后,将元素的操作类型,以及要插入的元素的位置,通过回调函数传递出去


   
  1. //drag.js
  2. class Drag {
  3.   params = {}
  4.   mouseOffsetBottom =  0;
  5.   mouseOffsetRight =  0;
  6.   index =  0//插入元素的下标
  7.    type =  'add'//操作类型
  8.   init = (params) => {
  9.     ...
  10.   };
  11.   ...
  12.    //计算元素距离父元素的offset
  13.   getRealOffset = (el, parentName) => {
  14.      const { left, top } = el.getBoundingClientRect();
  15.      return { offsetLeft: left, offsetTop: top };
  16.   }
  17.    //获取元素位置
  18.   getElOffset = el => {
  19.      const { offsetTop: iframeTop } = this.getIframeOffset();
  20.      const { offsetTop: targetOffsetTop } = this.getRealOffset(el);
  21.      return {
  22.       midLine: el.clientHeight /  2 + targetOffsetTop + iframeTop,
  23.       topLine: targetOffsetTop + iframeTop,
  24.       bottomLine: el.clientHeight + targetOffsetTop + iframeTop
  25.     };
  26.   };
  27.    //释放区内部元素位置
  28.   getDropOffset = () => {
  29.      const result = [];
  30.      const { dropEle } = this.params;
  31.      const el = dropEle.childNodes;
  32.     let i =  0;
  33.     while (i < el.length) {
  34.        const midLine = this.getElOffset(el[i]);
  35.       result.push(midLine);
  36.       i +=  1;
  37.     }
  38.      return result;
  39.   };
  40.   ...
  41.    //插入占位元素
  42.   insertPlaceholderEle = (sourceMidLine) => {
  43.      const dropOffset = this.getDropOffset();  //释放区的位置属性
  44.      const insertEl = this.createElePlaceholder();
  45.      const { dropEle } = this.params;
  46.      const dropEleChild = dropEle.childNodes;
  47.      if (dropOffset.length) {
  48.       dropOffset. map((item, i) => {
  49.          const Ele = dropEleChild[i];
  50.          //在元素前面插入占位元素
  51.          if (sourceMidLine > item.topLine && sourceMidLine < item.midLine) {
  52.           Ele.before(insertEl);
  53.           this.index = i;
  54.           this. type =  'insert'
  55.         }
  56.          //在元素后面插入占位元素
  57.          if (sourceMidLine < item.bottomLine && sourceMidLine > item.midLine) {
  58.           this.index = i +  1;
  59.           Ele.after(insertEl);
  60.           this. type =  'insert'
  61.         }
  62.          //追加一个占位元素
  63.          if (sourceMidLine > dropOffset[dropOffset.length -  1].bottomLine) {
  64.           dropEle. append(insertEl);
  65.           this. type =  'add'
  66.         }
  67.          return item;
  68.       });
  69.     }
  70.      //插入第一个占位元素(当iframe内部没有组件)
  71.      if (!dropEleChild.length) {
  72.       this. type =  'add'
  73.       dropEle. append(insertEl);
  74.     }
  75.   }
  76.    /****** 事件处理 ******/
  77.    //开始拖拽
  78.   dragStartEvent = ev => {
  79.     document.getElementsByClassName( "drop-content")[ 0].style.zIndex =
  80.        "-1";
  81.      //获得鼠标距离拖拽元素的下边的距离
  82.     this.mouseOffsetBottom = ev.currentTarget.clientHeight - ev.offsetY;
  83.      //获得鼠标距离拖拽元素的右边的距离
  84.     this.mouseOffsetRight = ev.currentTarget.clientWidth - ev.offsetX;
  85.   };
  86.   dragEvent = ev => {
  87.      //获取拖拽元素中线距离屏幕上方的距离
  88.      const sourceMidLine =
  89.       ev.clientY + this.mouseOffsetBottom - ev.currentTarget.clientHeight /  2;
  90.      if(this.locationCompare(ev)) {
  91.       this.insertPlaceholderEle(sourceMidLine)
  92.        // console.log('释放区内部')
  93.     }  else {
  94.       this.removePlaceholderEle()
  95.        // console.log('释放区外面')
  96.     }
  97.   };
  98.    //拖拽结束
  99.   dragEndEvent = ev => {
  100.     ev.preventDefault();
  101.     document.getElementsByClassName( "drop-content")[ 0].style.zIndex =  "0";
  102.      const { callback } = this.params;
  103.     this.locationCompare(ev) &&
  104.       callback &&
  105.       callback({
  106.          type: this. type,
  107.         index: this.index
  108.       });
  109.   };
  110. }
  111. export  default  new Drag();
  1. 在拖动完毕后调用 update,更新数据源


   
  1. //drag.jsx
  2. import React, { useState, useEffect } from  'react';
  3. import Drag from  './drag';
  4. import update from  '@/store/update';
  5. require( './styles.less');
  6. //iframe hooks
  7. const useIframeLoad = () => {
  8.   ...
  9.    //iframe加载状态的hooks
  10.    return iframeState;
  11. }
  12. export  default () => {
  13.    const callback = params => {
  14.     update({ ...params, data: { name:  new Date().getTime() } })
  15.   }
  16.    const init = () => {
  17.     Drag.init({
  18.       dragEle: document.getElementById( 'drag-box'),
  19.       dropEle: document.getElementById( 'my-iframe').contentDocument.getElementById( 'drop-box'),
  20.       callback
  21.     })
  22.   }
  23.   useIframeLoad() && init();
  24.    return <>
  25.     ...
  26.   </>
  27. }
  1. iframe 内部 update 方法被调用,就会触发数据更新和组件的渲染。


   
  1. //iframe.jsx
  2. import React, { useState } from  'react';
  3. require( './styles.less');
  4. export  default () => {
  5.    const [list, setList] = useState([]);
  6.    //挂载update方法,跨iframe数据传递,更新
  7.   window.update = params => {
  8.     setList(params);
  9.   }
  10.    return <div id= "drop-box">
  11.     {
  12.       list. map((item) =>
  13.         <div className= "item" key={item.name} onClick={() => alert( '点击事件')}>元素{item.name}</div>
  14.       )
  15.     }
  16.   </div>
  17. }

演示效果如下

最终实现了跨 iframe 的拖拽及通信。

总结

此次运营页搭建拖拽通信功能,是在不断打怪升级中完成。其中涉及到以下几个点:

  • 元素进入视图区的判断 iframe 的左边距离屏幕的 x 的坐标 < 被拖元素的右边距离屏幕的 x 的坐标 < iframe 的右边距离屏幕的 x 的坐标。

  • 元素上下插入 被拖元素的中线距离屏幕的 y 的坐标 < iframe 内部元素中线距离屏幕的 y 的坐标 属于前面插入,被拖元素的中线距离屏幕的 y 的坐标 > iframe 内部元素中线距离屏幕的 y 的坐标 属于后面插入。

  • clientX 坐标突变的问题 z-index 解决处理。

  • 滚动位置问题 getBoundingClientRect 解决。

希望本篇文章对你有所帮助,欢迎大家一起交流分享呀。

每天进步一点点,质变从关注 大转转 FE 开始。


转载:https://blog.csdn.net/P6P7qsW6ua47A2Sb/article/details/114297621
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场