前言
前段时间做运营活动搭建平台,其中一个主要功能:编辑页面分为左侧-组件区与右侧-预览区,需要实现组件区的内容可自由放置到预览区内。
类似下图所示:
社区内有一些类似的功能实现,但使用的方式大同小异,都离不开拖拽能力。我们日常开发中会经常用到的拖拽,如拖拽排序,拖拽上传等。当然拖拽的 npm 包也有很多,比较好用的包有 react-dnd
, vue
自带的拖拽能力等。
但我们的预览区采用的是 iframe 方式,社区好用的类库一般不支持跨 iframe 的拖拽的能力。此处我们选择了使用原生拖拽 drag 和 dropAPI
需要实现的主要功能,有两点:
1、检测拖动到 iframe 内部和外部。
2、数据驱动来进行 iframe 内部组件的展示。
我们简单生成页面的功能:
//搭建编辑页
//drag.jsx
import React, { useState, useEffect } from 'react';
import Drag from './drag.js';
require('./styles.less');
//iframe hooks
const 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 <>
<!-- 组件区 -->
<div id="drag-box">
<div className="drag-item">拖动元素</div>
<div className="drag-item">拖动元素</div>
<div className="drag-item">拖动元素</div>
</div>
<!-- 预览区 -->
<div className="drop-content">
<iframe id="my-iframe" src="#/iframe" style={
{ width: "100%", height: "480px", border: "none" }}/>
</div>
</>
}
预览区 iframe 页:
//iframe.jsx
import React from 'react';
require('./styles.less');
export default () => {
return <div id="drop-box">
<div className="item">元素1</div>
<div className="item">元素2</div>
<div className="item">元素3</div>
</div>
}
此时,简单的搭建编辑布局已完成。接下来,我们看下拖拽部分:
跨 iframe 拖拽
首先我们可以看下有哪些原生事件
原生事件
drag // 拖动元素或文本选择时将触发此事件 (相当于拖动过程中,一直触发此事件)
dragstart //当用户开始拖动一个元素或者一个选择文本的时候 ,将触发此事件
dragend //当拖动操作结束时(通过释放鼠标按钮或按退出键),将触发此事件
dragover //当被拖动元素在释放区内移动时,将触发此事件
dragenter //被拖动元素进入到释放区所占据得屏幕空间时,将触发此事件
dragleave //当被拖动元素没有放下就离开释放区时,将触发此事件
dragexit //当元素不再是拖动操作的立即选择目标时,将触发此事件
drop //当被拖动元素在释放区里放下时,将触发此事件
原生 drag 和 drop 拖拽
基于需求,拆分出拖拽的关键流程:
初始化元素 设置拖动元素和目标节点
注册事件 对拖动元素和目标节点元素注册 drag 事件
监听事件 拖动过程中生成占位节点,拖动结束删除此占位节点
不完全代码如下:
//drag.js
class Drag {
params = {}
init = (params) => {
....
};
//初始化设置拖动元素
initDrag = dragEle => {
if(dragEle.childNodes.length) {
const { length } = dragEle.childNodes;
let i = 0
while (i< length) {
this.setDrag(dragEle.childNodes[i]);
i += 1;
}
} else {
this.setDrag(dragEle);
}
}
//初始化释放区
initDrop = dropEle => {
if (dropEle.childNodes.length) {
const { length } = dropEle.childNodes;
let i = 0;
while (i < length) {
this.setDrop(dropEle.childNodes[i]);
i += 1;
}
} else {
this.setDrop(dropEle);
}
}
//拖动元素注册事件
setDrag = el => {
el.setAttribute("draggable", "true");
el.ondragstart = this.dragStartEvent;
el.ondrag = this.dragEvent;
el.ondragend = this.dragEndEvent;
};
//释放区注册事件
setDrop &#