可視化搭建平臺(tái)之跨iframe拖拽
以下文章來源于大轉(zhuǎn)轉(zhuǎn)FE ,作者大轉(zhuǎn)轉(zhuǎn)FE
前言
前段時(shí)間做運(yùn)營(yíng)活動(dòng)搭建平臺(tái),其中一個(gè)主要功能:編輯頁面分為左側(cè)-組件區(qū)與右側(cè)-預(yù)覽區(qū),需要實(shí)現(xiàn)組件區(qū)的內(nèi)容可自由放置到預(yù)覽區(qū)內(nèi)。
類似下圖所示:
社區(qū)內(nèi)有一些類似的功能實(shí)現(xiàn),但使用的方式大同小異,都離不開拖拽能力。我們?nèi)粘i_發(fā)中會(huì)經(jīng)常用到的拖拽,如拖拽排序,拖拽上傳等。當(dāng)然拖拽的 npm 包也有很多,比較好用的包有 react-dnd, vue 自帶的拖拽能力等。
但我們的預(yù)覽區(qū)采用的是 iframe 方式,社區(qū)好用的類庫一般不支持跨 iframe 的拖拽的能力。此處我們選擇了使用原生拖拽 drag 和 dropAPI
需要實(shí)現(xiàn)的主要功能,有兩點(diǎn):
1、檢測(cè)拖動(dòng)到 iframe 內(nèi)部和外部。
2、數(shù)據(jù)驅(qū)動(dòng)來進(jìn)行 iframe 內(nèi)部組件的展示。
我們簡(jiǎn)單生成頁面的功能:
//搭建編輯頁
//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 <>
<!-- 組件區(qū) -->
<div id="drag-box">
<div className="drag-item">拖動(dòng)元素</div>
<div className="drag-item">拖動(dòng)元素</div>
<div className="drag-item">拖動(dòng)元素</div>
</div>
<!-- 預(yù)覽區(qū) -->
<div className="drop-content">
<iframe id="my-iframe" src="#/iframe" style={{ width: "100%", height: "480px", border: "none" }}/>
</div>
</>
}
預(yù)覽區(qū) 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>
}
此時(shí),簡(jiǎn)單的搭建編輯布局已完成。接下來,我們看下拖拽部分:
跨 iframe 拖拽
首先我們可以看下有哪些原生事件
原生事件
drag // 拖動(dòng)元素或文本選擇時(shí)將觸發(fā)此事件 (相當(dāng)于拖動(dòng)過程中,一直觸發(fā)此事件)
dragstart //當(dāng)用戶開始拖動(dòng)一個(gè)元素或者一個(gè)選擇文本的時(shí)候 ,將觸發(fā)此事件
dragend //當(dāng)拖動(dòng)操作結(jié)束時(shí)(通過釋放鼠標(biāo)按鈕或按退出鍵),將觸發(fā)此事件
dragover //當(dāng)被拖動(dòng)元素在釋放區(qū)內(nèi)移動(dòng)時(shí),將觸發(fā)此事件
dragenter //被拖動(dòng)元素進(jìn)入到釋放區(qū)所占據(jù)得屏幕空間時(shí),將觸發(fā)此事件
dragleave //當(dāng)被拖動(dòng)元素沒有放下就離開釋放區(qū)時(shí),將觸發(fā)此事件
dragexit //當(dāng)元素不再是拖動(dòng)操作的立即選擇目標(biāo)時(shí),將觸發(fā)此事件
drop //當(dāng)被拖動(dòng)元素在釋放區(qū)里放下時(shí),將觸發(fā)此事件
原生 drag 和 drop 拖拽
基于需求,拆分出拖拽的關(guān)鍵流程:
初始化元素 設(shè)置拖動(dòng)元素和目標(biāo)節(jié)點(diǎn)
注冊(cè)事件 對(duì)拖動(dòng)元素和目標(biāo)節(jié)點(diǎn)元素注冊(cè) drag 事件
監(jiān)聽事件 拖動(dòng)過程中生成占位節(jié)點(diǎn),拖動(dòng)結(jié)束刪除此占位節(jié)點(diǎn)
不完全代碼如下:
//drag.js
class Drag {
params = {}
init = (params) => {
....
};
//初始化設(shè)置拖動(dòng)元素
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);
}
}
//初始化釋放區(qū)
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);
}
}
//拖動(dòng)元素注冊(cè)事件
setDrag = el => {
el.setAttribute("draggable", "true");
el.ondragstart = this.dragStartEvent;
el.ondrag = this.dragEvent;
el.ondragend = this.dragEndEvent;
};
//釋放區(qū)注冊(cè)事件
setDrop = el => {
el.ondrop = this.dropEvent;
el.ondragenter = this.dragEnterEvent;
el.ondragover = this.dragOverEvent;
el.ondragleave = this.dragLeaveEvent;
}
......
//創(chuàng)建占位元素
createElePlaceholder = (() => {
let ele = null;
return () => {
if (!ele) {
ele = document.createElement("div");
ele.setAttribute("id", "drag-ele-placeholder");
ele.innerHTML = `<div style="width: 100%; height:50px; position: relative">
<div style="width: 150px; height: 40px; text-align: center; position: absolute;
left: 0; right: 0; top: 0; bottom:0; margin: auto; background: #878; line-height: 40px">放置組件</div>
</div>`;
}
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('拖拽結(jié)束');
console.log('刪除占位元素');
};
//插入占位元素
dragEnterEvent = ev => {
ev.preventDefault();
const insertEle = this.createElePlaceholder();
ev.target.before(insertEle);
console.log('進(jìn)入到可放置區(qū)');
console.log('插入占位元素');
};
//刪除占位元素
dragLeaveEvent = ev => {
ev.preventDefault();
this.removePlaceholderEle()
console.log('離開放置區(qū)');
console.log('刪除占位元素');
};
dropEvent = ev => {
ev.preventDefault();
console.log('在放置區(qū)放開鼠標(biāo)');
}
}
export default new Drag();
初步完成后,效果如下:
此處存在一些問題:
在插入時(shí),頁面閃爍
只有鼠標(biāo)位置進(jìn)入釋放區(qū),才觸發(fā)進(jìn)入事件
無法實(shí)現(xiàn)第一個(gè)元素的添加
問題分析
當(dāng)拖到預(yù)覽區(qū)時(shí),會(huì)觸發(fā)預(yù)覽區(qū)內(nèi)的節(jié)點(diǎn) dragenter 事件。每當(dāng)在當(dāng)前節(jié)點(diǎn)上插入占位元素時(shí),此節(jié)點(diǎn)的位置會(huì)發(fā)生變化,觸發(fā)節(jié)點(diǎn) dragleave 事件,同時(shí)刪除占位元素。此過程一直重復(fù),導(dǎo)致一直閃爍。
上述 2,3 問題,是由于 drag/drop 本身 api 限制
由于現(xiàn)在的方式無法真正完美的實(shí)現(xiàn)功能,決定棄用 dragover,dragenter,dragleave 事件
重新梳理需要優(yōu)化的功能點(diǎn):
當(dāng)拖動(dòng)元素和 iframe 的邊有接觸的時(shí)候,就代表進(jìn)入釋放區(qū)
拖動(dòng)可以實(shí)現(xiàn)元素上面插入,和元素下面插入
使用坐標(biāo)精準(zhǔn)計(jì)算,來處理進(jìn)入釋放區(qū)和在元素上面和下面插入
對(duì) drag.js 做些改造:
class Drag {
params = {}
// 聲明
mouseOffsetBottom = 0;
mouseOffsetRight = 0;
init = (params) => {
...
};
//初始化設(shè)置拖動(dòng)元素
initDrag = dragEle => {
....
}
//初始化釋放區(qū)
initDrop = dropEle => {
...
}
//拖動(dòng)元素注冊(cè)事件
setDrag = el => {
...
};
//釋放區(qū)注冊(cè)事件
setDrop = el => {
...
}
//獲取iframe的位置
getIframeOffset = () => {
const iframeEle = this.getIframe();
return iframeEle
? this.getRealOffset(iframeEle)
: { offsetLeft: 0, offsetTop: 0 };
};
//遞歸計(jì)算元素距離父元素的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
};
};
//釋放區(qū)內(nèi)部元素位置
getDropOffset = () => {
const result = [];
const { dropEle } = this.params;
const el = dropEle.childNodes;
let i = 0;
while (i < el.length) {
const midLine = this.getElOffset(el[i]);
result.push(midLine);
i += 1;
}
return result;
};
//位置比較
locationCompare = (ev) => {
let inside = false;
const { dropEle } = this.params;
console.log(ev.clientX);
// 拖動(dòng)元素的位置
const sourceRight = ev.clientX + this.mouseOffsetRight;
const sourceLeft = sourceRight - ev.currentTarget.clientWidth;
const { offsetLeft: iframeLeft } = this.getIframeOffset();
const { offsetLeft: targetLeft } = this.getRealOffset(dropEle);
/*釋放區(qū)的位置*/
const targetOffsetLeft = iframeLeft + targetLeft;
const targetOffsetRight = targetOffsetLeft + dropEle.clientWidth;
if (sourceRight > targetOffsetLeft && sourceLeft < targetOffsetRight) {
//拖動(dòng)到釋放區(qū)
inside = true;
} else {
//釋放區(qū)外面
inside = false;
}
return inside;
}
//插入占位元素
insertPlaceholderEle = (sourceMidLine) => {
const dropOffset = this.getDropOffset(); //釋放區(qū)的位置屬性
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) {
Ele.before(insertEl);
}
//在元素后面插入占位元素
if (sourceMidLine < item.bottomLine && sourceMidLine > item.midLine) {
this.index = i + 1;
Ele.after(insertEl);
}
//追加一個(gè)占位元素
if (sourceMidLine > dropOffset[dropOffset.length - 1].bottomLine) {
dropEle.append(insertEl);
}
return item;
});
}
//插入第一個(gè)占位元素(當(dāng)iframe內(nèi)部沒有組件)
if (!dropEleChild.length) {
dropEle.append(insertEl);
}
}
/****** 事件處理 ******/
dragStartEvent = ev => {
// console.log('開始拖拽');
//獲得鼠標(biāo)距離拖拽元素的下邊的距離
this.mouseOffsetBottom = ev.currentTarget.clientHeight - ev.offsetY;
//獲得鼠標(biāo)距離拖拽元素的右邊的距離
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('釋放區(qū)內(nèi)部')
} else {
this.removePlaceholderEle()
console.log('釋放區(qū)外面')
}
};
}
export default new Drag();
生成結(jié)果如下:
此時(shí)已經(jīng)解決了不停閃爍的問題,以及精準(zhǔn)坐標(biāo)計(jì)算,實(shí)現(xiàn)元素的上下插入。
但是還是存在一些問題:
演示圖中可以明顯看到,拖動(dòng)元素右邊剛進(jìn)入 iframe 的時(shí)候,可以插入占位元素,但是等到鼠標(biāo)位置進(jìn)入 iframe 的時(shí)候,就會(huì)又刪除了元素
這是什么原因呢?
我們看一下打印的鼠標(biāo)的坐標(biāo),可以看到鼠標(biāo)位置進(jìn)入 iframe 的時(shí)候,ev.clientX 突變成 0,由此可見,鼠標(biāo)坐標(biāo)進(jìn)入 iframe 的時(shí)候,就以 iframe 為窗口了。導(dǎo)致鼠標(biāo)的位置突變成 0,就導(dǎo)致計(jì)算位置出現(xiàn)偏差,從而拖拽元素被認(rèn)為不在釋放區(qū)內(nèi),所以就刪除了占位元素。
怎么解決這個(gè)問題呢?
想到了幾個(gè)方案:
一個(gè)是監(jiān)聽坐標(biāo)的突變情況,然后重新計(jì)算位置,進(jìn)一步進(jìn)行比較位置。
把 iframe 放大和屏幕大于等于屏幕的大小,從拖動(dòng)開始就使得在 iframe 里面。
方案分析:
第一個(gè)方案,監(jiān)聽坐標(biāo)突變?yōu)?0 這個(gè)臨界條件不靠譜,因?yàn)槊扛?50ms 拖動(dòng)事件才觸發(fā),根據(jù)你移動(dòng)鼠標(biāo)的快慢,每次鼠標(biāo)進(jìn)入 iframe 獲取的 clientX 不一致,第一種方案不可行。
第二個(gè)方案,iframe 放大,理論上是可以的,我們來試試。主要是改變布局。
代碼如下:
.drop-content {
position: absolute;
width: 100vw; //iframe放大和窗口一般大
height: 100%;
}
#drop-box {
width: 375px; //iframe內(nèi)部元素設(shè)置寬度
margin: 100px auto;
.item {
...
}
}
演示效果如下
演示可以看到,覆蓋了左邊的組件區(qū)。這是由于右邊視圖區(qū) z-index 比較高導(dǎo)致的。
優(yōu)化方案
有兩個(gè)方案
元素布局移動(dòng)調(diào)換位置,讓右邊視圖區(qū) dom 元素放在組件區(qū)的前邊。
更改 z-index,讓右邊視圖區(qū)的 z-index 低一點(diǎn)
方案 1
核心代碼
//drag.jsx
//調(diào)換兩個(gè)元素的位置
<>
<div className="drop-content">
<iframe id="my-iframe" src="#/iframe" style={{ width: "100%", height: "480px", border: "none" }}/>
</div>
<div id="drag-box">
<div className="drag-item">拖動(dòng)元素</div>
<div className="drag-item">拖動(dòng)元素</div>
<div className="drag-item">拖動(dòng)元素</div>
</div>
</>
實(shí)現(xiàn)后的效果
可以看出來,完美解決了拖動(dòng)的問題。但是就是對(duì)布局進(jìn)行了改變。
方案 2
核心代碼
.drop-content {
position: absolute;
z-index: -1; //讓iframe的z-index低一點(diǎn)
width: 100vw; //iframe放大和窗口一般大
height: 100%;
}
#drop-box {
width: 375px; //iframe內(nèi)部元素設(shè)置寬度
margin: 100px auto;
.item {
width: 100%;
height: 50px;
background-color: #875;
}
}
實(shí)現(xiàn)后的效果
演示中可以看出來,拖拽的問題完美解決,但是 iframe 的里面元素點(diǎn)擊事件沒有觸發(fā)。
想了想,既然 z-index 可以解決 clientX 的突變問題,那是不是可以不用放大 iframe 來做?這樣也會(huì)不影響事件的觸發(fā),那我們?cè)囋嚢伞?br>
核心代碼
//drag.js
//開始拖拽
dragStartEvent = ev => {
document.getElementsByClassName("drop-content")[0].style.zIndex =
"-1";
};
//拖拽結(jié)束
dragEndEvent = ev => {
ev.preventDefault();
document.getElementsByClassName("drop-content")[0].style.zIndex = "0";
};
演示效果如下
很好,這樣也可以完美解決拖動(dòng)的問題,而且不用改變 dom 的位置。
滾動(dòng)處理
當(dāng)視圖區(qū)元素比較多,頁面出現(xiàn)滾動(dòng)條時(shí),會(huì)不會(huì)出現(xiàn)問題呢?我們?cè)囍?iframe 的高度寫高一點(diǎn)
<iframe id="my-iframe" src="#/iframe" style={{ width: "100%", height: "880px", border: "none" }}/>
演示效果如下
演示中可以看出來,頁面出現(xiàn)滾動(dòng)條,視圖區(qū)滾動(dòng)上去,iframe 頂部滾入到屏幕頂部的時(shí)候,我們來拖動(dòng)元素插入的時(shí)候,就會(huì)出現(xiàn),錯(cuò)位插入,這是計(jì)算又出了問題?
仔細(xì)看看代碼,iframe 頂部滾入到屏幕頂部的時(shí)候,就會(huì)出現(xiàn)計(jì)算出負(fù)數(shù)的情況,導(dǎo)致計(jì)算偏差,從而導(dǎo)致插入占位元素錯(cuò)位。
//遞歸計(jì)算元素距離父元素的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 };
}
優(yōu)化計(jì)算方案
核心代碼
//計(jì)算元素距離父元素的offset
getRealOffset = (el, parentName) => {
const { left, top } = el.getBoundingClientRect();
return { offsetLeft: left, offsetTop: top };
}
使用 getBoundingClientRect 這個(gè)方法獲得具體窗口的位置
演示如下
本次優(yōu)化,可以很完美的解決了拖動(dòng)的一些問題,以上兩種方案都是行的。
跨 iframe 通信
如何在拖動(dòng)元素插入之后,讓 iframe 內(nèi)部的數(shù)據(jù)也實(shí)時(shí)更新渲染呢?
思路如下:
iframe 內(nèi)掛載一個(gè) update 方法
在拖動(dòng)完成后的回調(diào)里面,調(diào)用 update,傳入數(shù)據(jù)
觸發(fā) iframe 內(nèi)部元素的渲染
維護(hù)一個(gè)組件的數(shù)據(jù) store,getStore,和 setStore方法
//store.js
class Store {
state = {
list: []
}
getStore = () => this.state
setStore = (data) => {
this.state = { ...this.state, ...data }
}
}
export default new Store()
組件的插入對(duì)應(yīng)數(shù)據(jù)的處理,包含,add 和 insert操作,以及同步更新 iframe的方法
// update.js
import Store from './store';
const add = (params) => {
const { list } = Store.getStore()
Store.setStore({ list: [...list, params.data]})
};
const insert = (params) => {
const { list } = Store.getStore()
const { index } = params;
list.splice(index, 0, params.data)
Store.setStore({ list: [...list] })
};
const update = {
add,
insert
}
//更新iframe內(nèi)部數(shù)據(jù)方法
const iframeUpdate = (params) => {
document.getElementById("my-iframe") &&
document.getElementById("my-iframe").contentWindow &&
document.getElementById("my-iframe").contentWindow.update &&
document.getElementById("my-iframe").contentWindow.update(params);
}
export default (params) => {
const { type, ...argv } = params;
if(!type) return Promise.reject()
return new Promise(r => r())
.then(() => update[type](argv))
.then(() => {
const { list } = Store.getStore()
iframeUpdate(list)
})
}
拖動(dòng)的時(shí)候,拖動(dòng)完畢后,將元素的操作類型,以及要插入的元素的位置,通過回調(diào)函數(shù)傳遞出去
//drag.js
class Drag {
params = {}
mouseOffsetBottom = 0;
mouseOffsetRight = 0;
index = 0; //插入元素的下標(biāo)
type = 'add'; //操作類型
init = (params) => {
...
};
...
//計(jì)算元素距離父元素的offset
getRealOffset = (el, parentName) => {
const { left, top } = el.getBoundingClientRect();
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
};
};
//釋放區(qū)內(nèi)部元素位置
getDropOffset = () => {
const result = [];
const { dropEle } = this.params;
const el = dropEle.childNodes;
let i = 0;
while (i < el.length) {
const midLine = this.getElOffset(el[i]);
result.push(midLine);
i += 1;
}
return result;
};
...
//插入占位元素
insertPlaceholderEle = (sourceMidLine) => {
const dropOffset = this.getDropOffset(); //釋放區(qū)的位置屬性
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) {
Ele.before(insertEl);
this.index = i;
this.type = 'insert'
}
//在元素后面插入占位元素
if (sourceMidLine < item.bottomLine && sourceMidLine > item.midLine) {
this.index = i + 1;
Ele.after(insertEl);
this.type = 'insert'
}
//追加一個(gè)占位元素
if (sourceMidLine > dropOffset[dropOffset.length - 1].bottomLine) {
dropEle.append(insertEl);
this.type = 'add'
}
return item;
});
}
//插入第一個(gè)占位元素(當(dāng)iframe內(nèi)部沒有組件)
if (!dropEleChild.length) {
this.type = 'add'
dropEle.append(insertEl);
}
}
/****** 事件處理 ******/
//開始拖拽
dragStartEvent = ev => {
document.getElementsByClassName("drop-content")[0].style.zIndex =
"-1";
//獲得鼠標(biāo)距離拖拽元素的下邊的距離
this.mouseOffsetBottom = ev.currentTarget.clientHeight - ev.offsetY;
//獲得鼠標(biāo)距離拖拽元素的右邊的距離
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('釋放區(qū)內(nèi)部')
} else {
this.removePlaceholderEle()
// console.log('釋放區(qū)外面')
}
};
//拖拽結(jié)束
dragEndEvent = ev => {
ev.preventDefault();
document.getElementsByClassName("drop-content")[0].style.zIndex = "0";
const { callback } = this.params;
this.locationCompare(ev) &&
callback &&
callback({
type: this.type,
index: this.index
});
};
}
export default new Drag();
在拖動(dòng)完畢后調(diào)用 update,更新數(shù)據(jù)源
//drag.jsx
import React, { useState, useEffect } from 'react';
import Drag from './drag';
import update from '@/store/update';
require('./styles.less');
//iframe hooks
const useIframeLoad = () => {
...
//iframe加載狀態(tài)的hooks
return iframeState;
}
export default () => {
const callback = params => {
update({ ...params, data: { name: new Date().getTime() } })
}
const init = () => {
Drag.init({
dragEle: document.getElementById('drag-box'),
dropEle: document.getElementById('my-iframe').contentDocument.getElementById('drop-box'),
callback
})
}
useIframeLoad() && init();
return <>
...
</>
}
iframe 內(nèi)部 update 方法被調(diào)用,就會(huì)觸發(fā)數(shù)據(jù)更新和組件的渲染。
//iframe.jsx
import React, { useState } from 'react';
require('./styles.less');
export default () => {
const [list, setList] = useState([]);
//掛載update方法,跨iframe數(shù)據(jù)傳遞,更新
window.update = params => {
setList(params);
}
return <div id="drop-box">
{
list.map((item) =>
<div className="item" key={item.name} onClick={() => alert('點(diǎn)擊事件')}>元素{item.name}</div>
)
}
</div>
}
演示效果如下
最終實(shí)現(xiàn)了跨 iframe 的拖拽及通信。
總結(jié)
此次運(yùn)營(yíng)頁搭建拖拽通信功能,是在不斷打怪升級(jí)中完成。其中涉及到以下幾個(gè)點(diǎn):
元素進(jìn)入視圖區(qū)的判斷 iframe 的左邊距離屏幕的 x 的坐標(biāo) < 被拖元素的右邊距離屏幕的 x 的坐標(biāo) < iframe 的右邊距離屏幕的 x 的坐標(biāo)。
元素上下插入 被拖元素的中線距離屏幕的 y 的坐標(biāo) < iframe 內(nèi)部元素中線距離屏幕的 y 的坐標(biāo) 屬于前面插入,被拖元素的中線距離屏幕的 y 的坐標(biāo) > iframe 內(nèi)部元素中線距離屏幕的 y 的坐標(biāo) 屬于后面插入。
clientX 坐標(biāo)突變的問題 z-index 解決處理。
滾動(dòng)位置問題 getBoundingClientRect 解決。
希望本篇文章對(duì)你有所幫助,歡迎大家一起交流分享呀。
作者:大轉(zhuǎn)轉(zhuǎn)FE
歡迎關(guān)注微信公眾號(hào) :前端民工