|
前言前段时间做运营活动搭建平台,其中一个主要功能:编辑页面分为左侧-组件区与右侧-预览区,需要实现组件区的内容可自由放置到预览区内。类似下图所示:社区内有一些类似的功能实现,但使用的方式大同小异,都离不开拖拽能力。我们日常开发中会经常用到的拖拽,如拖拽排序,拖拽上传等。当然拖拽的npm包也有很多,比较好用的包有react-dnd,vue自带的拖拽能力等。但我们的预览区采用的是iframe方式,社区好用的类库一般不支持跨iframe的拖拽的能力。此处我们选择了使用原生拖拽drag和dropAPI需要实现的主要功能,有两点:1、检测拖动到iframe内部和外部。2、数据驱动来进行iframe内部组件的展示。我们简单生成页面的功能://搭建编辑页//drag.jsximport React, { useState, useEffect } from 'react';import Drag from './drag.js';require('./styles.less');//iframe hooksconst useIframeLoad = () => { const [iframeState, setIframeState] = useState(false); const [windowState, setWindowState] = useState( document.readyState === "complete"); const iframeLoad = () => { const iframeEle = document.getElementById("my-iframe"); iframeEle & setIframeState(iframeEle.contentDocument.readyState === "complete"); if (!iframeState & iframeEle) { iframeEle.onload = () => { setIframeState(true); }; } }; useEffect(() => { if (!windowState) { setIframeState(false); window.addEventListener('load', () => { setWindowState(true); iframeLoad(); }) } else { iframeLoad(); } }, []); return iframeState;}export default () => { const init = () => { Drag.init({ dragEle: document.getElementById('drag-box'), dropEle: document.getElementById('my-iframe').contentDocument.getElementById('drop-box') }) } useIframeLoad() & init(); return 拖动元素 拖动元素 拖动元素 }预览区iframe页://iframe.jsximport React from 'react';require('./styles.less');export default () => { return 元素1 元素2 元素3 }此时,简单的搭建编辑布局已完成。接下来,我们看下拖拽部分:跨iframe拖拽首先我们可以看下有哪些原生事件原生事件drag // 拖动元素或文本选择时将触发此事件 (相当于拖动过程中,一直触发此事件)dragstart //当用户开始拖动一个元素或者一个选择文本的时候 ,将触发此事件dragend //当拖动操作结束时(通过释放鼠标按钮或按退出键),将触发此事件dragover //当被拖动元素在释放区内移动时,将触发此事件dragenter //被拖动元素进入到释放区所占据得屏幕空间时,将触发此事件dragleave //当被拖动元素没有放下就离开释放区时,将触发此事件dragexit //当元素不再是拖动操作的立即选择目标时,将触发此事件drop //当被拖动元素在释放区里放下时,将触发此事件原生drag和drop拖拽基于需求,拆分出拖拽的关键流程:初始化元素设置拖动元素和目标节点注册事件对拖动元素和目标节点元素注册drag事件监听事件拖动过程中生成占位节点,拖动结束删除此占位节点不完全代码如下://drag.jsclass Drag { params = {} init = (params) => { .... }; //初始化设置拖动元素 initDrag = dragEle => { if(dragEle.childNodes.length) { const { length } = dragEle.childNodes; let i = 0 while (i { if (dropEle.childNodes.length) { const { length } = dropEle.childNodes; let i = 0; while (i { el.setAttribute("draggable", "true"); el.ondragstart = this.dragStartEvent; el.ondrag = this.dragEvent; el.ondragend = this.dragEndEvent; }; //释放区注册事件 setDrop = el => { el.ondrop = this.dropEvent; el.ondragenter = this.dragEnterEvent; el.ondragover = this.dragOverEvent; el.ondragleave = this.dragLeaveEvent; } ...... //创建占位元素 createElePlaceholder = (() => { let ele = null; return () => { if (!ele) { ele = document.createElement("div"); ele.setAttribute("id", "drag-ele-placeholder"); ele.innerHTML = ` 放置组件 `; } return ele; }; })(); //移除占位元素 removePlaceholderEle = () => { const iframe = this.getIframe(); const removeEle = iframe.contentDocument.getElementById("drag-ele-placeholder"); const { dropEle } = this.params; if(this.isHasPlaceholderEle()) { dropEle.removeChild(removeEle) }; } /****** 事件处理 ******/ dragEndEvent = ev => { this.removePlaceholderEle() console.log('拖拽结束'); console.log('删除占位元素'); }; //插入占位元素 dragEnterEvent = ev => { ev.preventDefault(); const insertEle = this.createElePlaceholder(); ev.target.before(insertEle); console.log('进入到可放置区'); console.log('插入占位元素'); }; //删除占位元素 dragLeaveEvent = ev => { ev.preventDefault(); this.removePlaceholderEle() console.log('离开放置区'); console.log('删除占位元素'); }; dropEvent = ev => { ev.preventDefault(); console.log('在放置区放开鼠标'); }}export default new Drag();初步完成后,效果如下:此处存在一些问题:在插入时,页面闪烁只有鼠标位置进入释放区,才触发进入事件无法实现第一个元素的添加问题分析当拖到预览区时,会触发预览区内的节点dragenter事件。每当在当前节点上插入占位元素时,此节点的位置会发生变化,触发节点dragleave事件,同时删除占位元素。此过程一直重复,导致一直闪烁。上述2,3问题,是由于drag/drop本身api限制由于现在的方式无法真正完美的实现功能,决定弃用dragover,dragenter,dragleave事件重新梳理需要优化的功能点:当拖动元素和iframe的边有接触的时候,就代表进入释放区拖动可以实现元素上面插入,和元素下面插入使用坐标精准计算,来处理进入释放区和在元素上面和下面插入对drag.js做些改造:class Drag { params = {} // 声明 mouseOffsetBottom = 0; mouseOffsetRight = 0; init = (params) => { ... }; //初始化设置拖动元素 initDrag = dragEle => { .... } //初始化释放区 initDrop = dropEle => { ... } //拖动元素注册事件 setDrag = el => { ... }; //释放区注册事件 setDrop = el => { ... } //获取iframe的位置 getIframeOffset = () => { const iframeEle = this.getIframe(); return iframeEle ? this.getRealOffset(iframeEle) : { offsetLeft: 0, offsetTop: 0 }; }; //递归计算元素距离父元素的offset getRealOffset = (el, parentName) => { let left = el.offsetLeft; let top = el.offsetTop; if (el.offsetParent & el.offsetParent.tagName !== parentName) { const p = this.getRealOffset(el.offsetParent, parentName); left += p.offsetLeft; top += p.offsetTop; } return { offsetLeft: left, offsetTop: top }; } //获取元素位置 getElOffset = el => { const { offsetTop: iframeTop } = this.getIframeOffset(); const { offsetTop: targetOffsetTop } = this.getRealOffset(el); return { midLine: el.clientHeight / 2 + targetOffsetTop + iframeTop, topLine: targetOffsetTop + iframeTop, bottomLine: el.clientHeight + targetOffsetTop + iframeTop }; }; //释放区内部元素位置 getDropOffset = () => { const result = []; const { dropEle } = this.params; const el = dropEle.childNodes; let i = 0; while (i { let inside = false; const { dropEle } = this.params; console.log(ev.clientX); // 拖动元素的位置 const sourceRight = ev.clientX + this.mouseOffsetRight; const sourceLeft = sourceRight - ev.currentTarget.clientWidth; const { offsetLeft: iframeLeft } = this.getIframeOffset(); const { offsetLeft: targetLeft } = this.getRealOffset(dropEle); /*释放区的位置*/ const targetOffsetLeft = iframeLeft + targetLeft; const targetOffsetRight = targetOffsetLeft + dropEle.clientWidth; if (sourceRight > targetOffsetLeft & sourceLeft { const dropOffset = this.getDropOffset(); //释放区的位置属性 const insertEl = this.createElePlaceholder(); const { dropEle } = this.params; const dropEleChild = dropEle.childNodes; if (dropOffset.length) { dropOffset.map((item, i) => { const Ele = dropEleChild[i]; //在元素前面插入占位元素 if (sourceMidLine > item.topLine & sourceMidLine item.midLine) { this.index = i + 1; Ele.after(insertEl); } //追加一个占位元素 if (sourceMidLine > dropOffset[dropOffset.length - 1].bottomLine) { dropEle.append(insertEl); } return item; }); } //插入第一个占位元素(当iframe内部没有组件) if (!dropEleChild.length) { dropEle.append(insertEl); } } /****** 事件处理 ******/ dragStartEvent = ev => { // console.log('开始拖拽'); //获得鼠标距离拖拽元素的下边的距离 this.mouseOffsetBottom = ev.currentTarget.clientHeight - ev.offsetY; //获得鼠标距离拖拽元素的右边的距离 this.mouseOffsetRight = ev.currentTarget.clientWidth - ev.offsetX; }; dragEvent = ev => { //获取拖拽元素中线距离屏幕上方的距离 const sourceMidLine = ev.clientY + this.mouseOffsetBottom - ev.currentTarget.clientHeight / 2; if(this.locationCompare(ev)) { this.insertPlaceholderEle(sourceMidLine) console.log('释放区内部') } else { this.removePlaceholderEle() console.log('释放区外面') } };}export default new Drag();生成结果如下:此时已经解决了不停闪烁的问题,以及精准坐标计算,实现元素的上下插入。但是还是存在一些问题:演示图中可以明显看到,拖动元素右边刚进入iframe的时候,可以插入占位元素,但是等到鼠标位置进入iframe的时候,就会又删除了元素这是什么原因呢?我们看一下打印的鼠标的坐标,可以看到鼠标位置进入iframe的时候,ev.clientX突变成0,由此可见,鼠标坐标进入iframe的时候,就以iframe为窗口了。导致鼠标的位置突变成0,就导致计算位置出现偏差,从而拖拽元素被认为不在释放区内,所以就删除了占位元素。怎么解决这个问题呢?想到了几个方案:一个是监听坐标的突变情况,然后重新计算位置,进一步进行比较位置。把iframe放大和屏幕大于等于屏幕的大小,从拖动开始就使得在iframe里面。方案分析:第一个方案,监听坐标突变为0这个临界条件不靠谱,因为每隔50ms拖动事件才触发,根据你移动鼠标的快慢,每次鼠标进入iframe获取的clientX不一致,第一种方案不可行。第二个方案,iframe放大,理论上是可以的,我们来试试。主要是改变布局。代码如下:.drop-content { position: absolute; width: 100vw; //iframe放大和窗口一般大 height: 100%;}#drop-box { width: 375px; //iframe内部元素设置宽度 margin: 100px auto; .item { ... }}演示效果如下演示可以看到,覆盖了左边的组件区。这是由于右边视图区z-index比较高导致的。优化方案有两个方案元素布局移动调换位置,让右边视图区dom元素放在组件区的前边。更改z-index,让右边视图区的z-index低一点方案1核心代码//drag.jsx//调换两个元素的位置 拖动元素 拖动元素 拖动元素 实现后的效果可以看出来,完美解决了拖动的问题。但是就是对布局进行了改变。方案2核心代码.drop-content { position: absolute; z-index: -1; //让iframe的z-index低一点 width: 100vw; //iframe放大和窗口一般大 height: 100%;}#drop-box { width: 375px; //iframe内部元素设置宽度 margin: 100px auto; .item { width: 100%; height: 50px; background-color: #875; }}实现后的效果
|
|