網(wǎng)站網(wǎng)頁設計要求真正免費的網(wǎng)站建站平臺運營
最近碰到了個需求,大概就是要通過可視化拖拽的方式配置一個冰柜,需要把預設好的冰柜內部架子模板一個個拖到冰箱內。一開始的想法是用鼠標事件(mousedown、mouseup等)那一套去實現(xiàn),能實現(xiàn)但是過程過于復雜,需要控制的狀態(tài)太多了。其實
Web Api
為 html 元素拖拽量身定制了一套HTML
拖放API
,用這個方法實現(xiàn)一些簡單的拖拽功能簡直不要太簡單。為此寫了這篇文章,下面將詳細介紹HTML 拖放 API
的核心知識點
文檔
一、被拖拽元素和放置被拖拽元素的元素
通常我們所了解的拖放是按住鼠標左鍵不放然后移動鼠標把一個頁面元素從某個位置移動到另一個位置,然后松開鼠標左鍵,至此完成了整個拖放過程。在這個過程中我們需要先重點關注兩個東西,一個是
被拖拽元素
另一個是放置被拖拽元素的元素
。
1.1 被拖拽元素
我們得先有個概念,頁面上顯示的元素默認并不都是可以被拖拽的(除了圖片、被選中的文字、鏈接),所以如果當前元素默認不可被拖拽那么就得先把它設置為可拖拽的。ps:可拖拽元素被拖拽時會有一個半透明的快照跟著鼠標移動。
將 HTML 元素的 draggable 屬性設置為 true, 元素就可以變?yōu)榭赏献г?。效果如下圖。
<div id="box" draggable="true">draggable box</div>
1.2 可放置被拖拽元素的元素
所有的元素區(qū)域默認是不支持放置被拖拽元素的,直觀的表現(xiàn)是,當被拖拽元素經(jīng)過不可放置區(qū)域時鼠標的樣式是一個禁止放置的一個圖標(圓圈帶一個斜杠),所以需要將目標元素設置為一個可放置區(qū)域
默認情況下是這樣:
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置區(qū)域</div>
設置為放置區(qū)域需要給元素綁定一個事件 dragover 且要 阻止默認行為
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置區(qū)域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragover', (e) => {e.preventDefault()})
</script>
React:
放置區(qū)域
需要綁定onDragOver
事件,且要 阻止默認行為 – 其他事件一樣加on
<div draggable="true">draggable box</div>
<div onDragOver={(e) => {e.preventDefault()}}>放置區(qū)域</div>
設置為可放置區(qū)域后鼠標樣式也變了不再是禁止圖標,而是一個加號圖標(圖標可以設置,下面會講解):
然而你會發(fā)現(xiàn)被拖放元素并沒有真正的被放置到放置區(qū)域,這是必然的,放置操作需要開發(fā)者自行定義
,以上的設置只是是為了向用戶表明這個區(qū)域是允許放東西的,那么至于怎么放需要開發(fā)者自行決定。
二、拖拽過程觸發(fā)的一些事件
這一小節(jié)將帶你了解整個拖放過程的其他細節(jié),比如拖拽過程中會觸發(fā)哪些事件
2.1 被拖放目標觸發(fā)的事件
給被拖放目標元素綁定三個事件 dragstart、drag、dragend。
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置區(qū)域</div>
<script>let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {console.log('開始拖動');})dragDom.addEventListener('drag', (e) => {console.log('拖動中');})dragDom.addEventListener('dragend', (e) => {console.log('結束拖動');})
</script>
React
<div draggable="true"onDragStart={(e) => {console.log("開始拖動", e);}}onDrag={(e) => {console.log("拖動中", e);}}onDragEnd={(e) => {console.log("結束拖動", e);}}
>
>draggable box</div>
<div onDragOver={(e) => {e.preventDefault()}}>放置區(qū)域</div>
開始拖動觸發(fā) dragstart
,拖動過程中(鼠標不松開)觸發(fā)drag
,松開鼠標(或者按下 Esc
鍵)觸發(fā) dragend
。
2.2 被拖拽元素在放置區(qū)域內會觸發(fā)的事件
先給放置目標元素綁定四個事件
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置區(qū)域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragenter', (e) => {console.log('進入到了放置區(qū)域~');})dropDom.addEventListener('dragover', (e) => {e.preventDefault()console.log('在放置區(qū)域內拖拽中~');})dropDom.addEventListener('dragleave', (e) => {console.log('離開了放置區(qū)域~');})dropDom.addEventListener('drop', (e) => {console.log('在放置區(qū)域內,放下了被拖拽元素~')})
</script>
拖拽元素進入放置區(qū)域內時觸發(fā) dragenter
事件,在放置區(qū)域內移動被拖放(鼠標不松開)元素觸發(fā) dragover
事件,被拖放元素離開放置區(qū)域觸發(fā) dragleave
事件,在放置區(qū)域內松開鼠標觸發(fā) drop
事件。
三、實現(xiàn)真正意義上的元素拖放
通過上面觸發(fā)的事件我們可以知道,用戶真正在放置區(qū)域釋放鼠標的時候只有 drop 事件能夠監(jiān)聽到。所以開發(fā)者需要在這個事件里做真正的放置操作,放置什么由開發(fā)者決定,可以是被拖拽元素,也可以是自定義的一些內容。
放置被拖拽元素:
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置區(qū)域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragover', (e) => {e.preventDefault()console.log('在放置區(qū)域內拖拽中~');})dropDom.addEventListener('drop', (e) => {console.log('在放置區(qū)域內,放下了被拖拽元素~')e.target.appendChild(document.getElementById('box'))})
</script>
放置自定義內容
dropDom.addEventListener('drop', (e) => {console.log('在放置區(qū)域內,放下了被拖拽元素~')let customCOntent = '<p>自定義內容</p>'e.target.innerHTML = e.target.innerHTML + customCOntent
})
四、dataTransfer 對象
4.1 從被拖放元素向可放置元素傳遞數(shù)據(jù)
dataTransfer
對象提供了一個setData()
方法,它接受兩個參數(shù),第一個參數(shù)是傳遞數(shù)據(jù)的類型(一般是標準的MIME類型
),第二個數(shù)據(jù)是數(shù)據(jù)值。dataTransfer
還提供了getData()
的方法用于獲取傳遞的數(shù)據(jù),它接受一個參數(shù),參數(shù)值為setData
對應的第一個參數(shù)。
傳遞一個簡單的字符串數(shù)據(jù)
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置區(qū)域</div>
<script>let dropDom = document.getElementById('droppable')let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定義數(shù)據(jù)')})dropDom.addEventListener('dragover', (e) => {e.preventDefault()})dropDom.addEventListener('drop', (e) => {let data = e.dataTransfer.getData('text/plain')console.log('你傳遞的數(shù)據(jù)為:', data);})
</script>
?注意:只能在 dragstart 事件中設置數(shù)據(jù),在其他地方設置無效。且只能在 drop 事件中獲取設置的數(shù)據(jù),其他事件中獲取不到。
案例:根據(jù)傳遞的數(shù)據(jù)放置不同的內容。
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置區(qū)域</div>
<script>let dropDom = document.getElementById('droppable')let dragDom = document.getElementById('box')dropDom.addEventListener('dragover', (e) => {e.preventDefault()})dropDom.addEventListener('drop', (e) => {let num = e.dataTransfer.getData('num')console.log(num);if(num > 5)e.target.innerHTML = e.target.innerHTML + '<p>傳遞的數(shù)字大于5</p>'else if(num == 5) e.target.innerHTML = e.target.innerHTML + '<p>傳遞的數(shù)字等于5</p>'elsee.target.innerHTML = e.target.innerHTML + '<p>傳遞的數(shù)字小于5</p>'})dragDom.addEventListener('dragstart', (e) => {let num = Math.floor(Math.random() * 10) + 1;e.dataTransfer.setData('num', num)})
</script>
4.2 自定義拖拽過程中跟隨鼠標移動的內容
默認情況下元素被拖拽時會有一個半透明的元素快照跟隨著鼠標移動。通過 dataTransfer 提供的 setDragImage(elemnt, xOffset, yOffset) 方法是可以自定義跟隨內容。接受三個參數(shù) elemnt 可以是 dom 節(jié)點或者一個圖片對象,xOffset, yOffset 是相對于鼠標的偏移量。
語法
dataTransfer.setDragImage(img, xOffset, yOffset);
img | Element
用于拖曳反饋圖像的圖像 Element 元素。
如果 Element 是一個 img 元素,則將拖動位圖設置為該元素的圖像(保持大小);否則,將拖動數(shù)位圖設置為從給定元素所生成的圖片
xOffset
使用 long 指示相對于圖片的橫向偏移量
yOffset
使用 long 指示相對于圖片的縱向偏移量
解析
- 發(fā)生拖動時,從拖動目標 (
dragstart
事件觸發(fā)的元素) 生成半透明圖像,并在拖動過程中跟隨鼠標指針。這個圖片是自動創(chuàng)建的,你不需要自己去創(chuàng)建它。然而,如果想要設置為自定義圖像,那么DataTransfer.setDragImage()
方法就能派上用場。 - 圖像通常是一個
<image>
元素,但也可以是<canvas>
或任何其他圖像元素。該方法的 x 和 y 坐標是圖像應該相對于鼠標指針出現(xiàn)的偏移量。
坐標指定鼠標指針相對于圖片的偏移量。例如,要使圖像居中,請使用圖像寬度和高度的一半。通常在dragstart
事件處理程序中調用此方法。
實際用例
setDragImage 的第一個參數(shù)接受的是一個Element參數(shù),這樣的話,普通的
html元素
、image元素
、canvas
都可以傳遞
1、設置為一個圖片:
<script>import Tag from "../../style/imgs/attributeTag/路徑.png"; //已經(jīng)存在的圖片let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {let img = new Image()// 創(chuàng)建一個圖像并且使用它作為拖動圖像// 請注意: 改變 "example.gif" 為一個已經(jīng)存在的圖片// 或者,一個還沒有創(chuàng)建出來的圖片,那么瀏覽器將會使用默認的拖動圖片// 譯者注:默認的拖動圖片與拖動對象沒有聯(lián)系。一般是一個小型文件圖標// 例如:// mg.src = Tag //或// mg.src = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAmCAYAAACoPemuAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABklJREFUeNqsmFtwE1UYx/97Tdqkbei91Gk6IFouYjqjBR3aQTstoM40DD4gOIO++sL4pr45+q7iu+OMyJMD1RcZAXGEBxiUm0JrKfQiDoVeSEmv2WSP3znJJpt0d1Mgpz3d3bPJnt/+v8v5TqX9X89HGMNZ6iGGdGOZP9a11Riz3bddm4n4tz991Pg+SthkDmVmoPhE2Q5bLxwruIakRVHiJnMo+9s/SZMULfTW56PRUiuWB8UKjmCrIqNfva+kYPb5vRiYi5/lnuQrsWIOkzyWWixnzjc/K505VUcgN9Ai5qwo1w7duHknFgzoUDU5Ky+z+YrbuW3sajgcjqmsCAnzSBeFLcn06NJSPBoI+CBJMmQZjw1mmmZsbGzsQ3kFUBEopxewbi3DhyujBhYXl8TnJUl67C7Lcoj6F6rb61vD5TrQ3mKiNsiy45fHZIxPSw7fUnBrphwvPJqDrikIBMposidIP5IUUr38qWdTCn2RlICztyiNDU7IOHJaxXwi/95wrBJJI454fB6qKsPv93marsC/LLBcVBZGJwd6pyMfamou9/ot1TkV7aZeZj4MTqpIJk3Mzy8gkUg8fVRaE7RUMwHG2wI999hFFeeH0+5YG2A4sC2F/isKxmYkx+gcmirDpsY5cmQm4AKBcmia5qiOK5jT/Z7Nqez5kTMamU3K2pmrduSM6hmttx5yc84imTIhKxIFw2J6MlV9CsWotzWmpxgnRQRUZsZoe8oxSE5cVvLGEmTOfyZ1RMpN4WcUE5iYmsbQv/ex7plGtDbVFwdzEtTyHRF5tg84gfE2cE+mnlNVmHO6DFuayZykGiO42bkF/HrpKt2MINxY5+r4ropZTs7hWmrYqrL/ZHzlGDfn8bNnoOkSqSYhkUwKk/9xcwij9x5AymRflin+GmpC2LO93QbmMOMgvf2ODUwEwfNNTFzzdugbPfuwT95Ioq3JFMGRjVabryUo2Y7MlMHP7tLyRMlTTn+GKzdLAaEopImUgRM/KK7YuWGFwExxfrjbwFc8AAiOA/H0cXB7Goq3P8dkx1WAm7NmbQTd69ZSVOoUlTIID8i8w/lrg7g7+RDtbesR2RCGX9eL+xiH+OWGgt7N6Tz28R5DqDIVl7JAVio5ekF1tDP3mYlEPRRpGinys+a6NfD7NJE8lxMGxiemBGTHxnVYW1dTPCqtdvSCIrL63ozDc5+zJ1QO+uUpFQvLTusqy0SnHz9cuAfVGEVrcx0O7u5MW+TaQDpJN9SiuiJA4CkyrZL3fdUrz/E0cG5IRudzZjaFTM2lo5CPOy/2toDhpVDtFhgP/hMK/XV7HKFgAJcGbguLdkY2is8ZhpFeaW1warGKdZKUOW7PU8x9bc2Dyhy5ObtaW3B95A5OX7puuRhe2vRsXtpIUtRaFQZ/zoq10rXEdiiJikFZ5gxUt6GpphpLSwaWyL/qq6vQ+WLbynqO4Kgey6/54QXDvCtaNygr2f49oWFx2cg+anZuXqQNp4Dh/iYUg5tkrDgjf4AnlK0UejDzCDqtlfVrKoVq3588h/szMceX43DyEwjmCMS8qmAtCPjD2LdzO/Z370DDmioBd/Tk75gguMLlyNPHmEsht+IhHnuF3B5VQVO4gyIyKCqMA71daCA/W6Ra7buff3NUTmZm0rOitHfXWFjFXkGUQklGizqDT1NxcFdnVrlTF6877CtTy7FCACcQV9N6po/cOa9sByZUGFTZ8mfrVDi+u7sLW9eH8Xb3K3lq8aOyvuuD+7Rp2kkLqh+MQrVIZ/xoOo8z69q0nVudkFVa2jc2GNCoDFKogNTIrG3hZqiZxCrZdi7ibNenIyFRKK1iZ+vse5S9R4YPI4Wom015mb23pwqvbinLLuq0TcsmVAsqeyzVln5n37GorAZPiFKm0LykYF9vFba1V9GuSYVPV0kxWWzt8lTKZP6SgvH2+r7+h5Ksh/KVMhHtrSSoSrHX1AmKl9tuUNaYXNL/trFUP6+5cmteGqojUkmmU0TnvrWanVJJwZhp/JgOBEoLJm2WeyoIqoKUkkXnUGlBmCcUrzZKCna2f3+/aRoxrlRfTxAvbw0Ks/EuZ0prt1RkV5E2yTEVJW6pVKK/r7fivfbNZeQ4JhnWFFGbTKbyo67Ap6zo5FDUX/tfgAEAQ3WUFGFdgUwAAAAASUVORK5CYII=`;img.src = 'example.gif'e.dataTransfer.setDragImage(img, 10, 10)})
</script>
2、以官網(wǎng)例子為例,把canvas作為參數(shù)傳遞,我首先嘗試的是這種方式,發(fā)現(xiàn)并不能生效
。(官方
的例子沒有
運行成功)
function dragWithCustomImage(event) {var canvas = document.createElementNS("http://www.w3.org/1999/xhtml","canvas");canvas.width = canvas.height = 50;var ctx = canvas.getContext("2d");ctx.lineWidth = 4;ctx.moveTo(0, 0);ctx.lineTo(50, 50);ctx.moveTo(0, 50);ctx.lineTo(50, 0);ctx.stroke();var dt = event.dataTransfer;dt.setData('text/plain', 'Data to Drag');dt.setDragImage(canvas, 25, 25);
}
3、根據(jù)案例,我接著使用·HtmlDivElement·作為參數(shù)傳遞,創(chuàng)建了·DIV元素·,此時也·沒有生效·。
export function drawDragImage(dataTransfer: DataTransfer, context: string) {
const drawItem: API.EquipmentInfo = JSON.parse(context);
const div = document.createElement(‘div’);
div.style.height = itemObj.height + ‘px’;
div.style.width = itemObj.width + ‘px’;
div.style.border = ‘1px solid #000’;
const span = document.createElement(‘span’);
span.innerText = ‘2222’;
div.appendChild(span);
dataTransfer.setDragImage(div, drawItem.width / 2, drawItem.height / 2);
}
4、然后,我改進了canvas
,把canvas轉化為圖片,第一次拖拽的時候,因為image加載元素是異步導致了沒有生效,如圖1;第二以后拖拽的時候可以生效,如圖二
。
const imageContent = canvas.toDataURL('image/jpeg', 1);const image = new Image();image.src = imageContent;image.onload = () => {console.log('image2 load');};dataTransfer.setDragImage(image, drawItem.width / 2, drawItem.height / 2);
5、最后我嘗試了使用官網(wǎng)的方法,同樣因為image加載圖片是異步的
,而拖拽
事件是同步發(fā)生
的,導致了第一次執(zhí)行失敗
。
function dragstart_handler(ev) {console.log("dragStart");// 設置拖動的格式和數(shù)據(jù)。使用事件目標的 id 作為數(shù)據(jù)ev.dataTransfer.setData("text/plain", ev.target.id);// 創(chuàng)建一個圖像并且使用它作為拖動圖像// 請注意:改變 "example.gif" 為一個已經(jīng)存在的圖片// 或者,一個還沒有創(chuàng)建出來的圖片,那么瀏覽器將會使用默認的拖動圖片// 譯者注:默認的拖動圖片與拖動對象沒有聯(lián)系。一般是一個小型文件圖標var img = new Image();img.src = 'example.gif';ev.dataTransfer.setDragImage(img, 10, 10);
}
解決方案
在嘗試了不同方式設置拖拽反饋圖像,總結了一些解決方案
:
- 以html頁面的元素為模版,動態(tài)生成內容,然后設置
Element元素參數(shù)
,可以設置DIV元素的z-index
(使用z-index,必須使用position:relative | absolute
)–(嘗試使用過css1、position:absolute
定位出瀏覽器可視界面 2、display:none
無用),隱藏在實際頁面之下:這樣可以動態(tài)生成要拖拽的元素,并和生成的fabric的group保持一致。 完美的解決了問題 。
js
export function drawDragImage(dataTransfer: DataTransfer, context: string) {const drawItem: API.EquipmentInfo = JSON.parse(context);const dragElement = document.getElementById('dragItem');const idElement = dragElement?.getElementsByClassName('dragItemId')[0];const nameElement = dragElement?.getElementsByClassName('dragItemName')[0];if (idElement) {idElement.innerHTML = drawItem.id;}if (nameElement) {nameElement.innerHTML = drawItem.typeName || '';}if (dragElement) {dragElement.style.height = drawItem.height + 'px';dragElement.style.width = drawItem.width + 'px';dragElement.style.border = '1px solid #000';dragElement.style.background = '#fff';}if (dragElement) {dataTransfer.setDragImage(dragElement, drawItem.width / 2, drawItem.height / 2);}}
React
import {useRef} from "react";import { Modal, Space, Input, Tree, Button, Badge } from "antd";const mouseStyle = useRef<any>(null);<divdraggable="true"onDragStart={(e) => {//mouseStyle.currente.dataTransfer.setDragImage(mouseStyle.current, 10, 10);}}>移動位置</div>//
//absolute top-[10%] z-[1] h-10 css使用了tailwindcss
//npm install -D tailwindcss
//https://tailwindcss.com/docs/installation 文檔地址<div className="absolute top-[10%] z-[1] h-10" ref={mouseStyle}><Badge count={5}><div className=" border border-[#444444] leading-6 h-6 w-15">2023.09.22初級會計資格考試</div></Badge>
</div>
4.3 設置放置前的反饋圖標
dataTransfer 提供了一個
dropEffect
屬性設置放置前的反饋圖標,它有四種取值 none move copy link
在 dragover 中設置 dropEffect
的值
dropDom.addEventListener('dragover', (e) => {e.preventDefault()e.dataTransfer.dropEffect = 'link' // none || move || copy || link
})
- 值為 none 或者經(jīng)過不可放置區(qū)域,顯示禁止放置圖標
- 值為 move 時
- 值為 copy 時
- 值為 link 時
4.4 拖動文件上傳
通過 dataTransfer 的
files
屬性可以獲取用戶拖拽的文件信息
拖拽系統(tǒng)文件到放置區(qū)域,并打印拖拽的文件信息:
dropDom.addEventListener('drop', (e) => {e.preventDefault()// 上傳的文件列表let fileList = e.dataTransfer.filesfor (let i = 0; i < fileList.length; i++) {const file = fileList[i];console.log('文件名:' + file.name);console.log('文件大小:' + file.size);// 后續(xù)操作 比如:調接口上傳文件}
})
4.5 清除 setData() 的值
dataTransfer 提供了
clearData()
清除 setData 設置的值,傳參數(shù)則刪除指定類型的值,不傳則全部清除。
dropDom.addEventListener('drop', (e) => {console.log(e.dataTransfer.getData('text/plain'));console.log(e.dataTransfer.getData('text/html'));
})dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定義數(shù)據(jù)')e.dataTransfer.setData('text/html', '自定義數(shù)據(jù)2')e.dataTransfer.clearData('text/html')
})
4.6 查看設置了哪些類型的值
dataTransfer 提供了
types
屬性查看 setData 設置了哪些類型的值。
dropDom.addEventListener('drop', (e) => {console.log(e.dataTransfer.types);
})
dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定義數(shù)據(jù)')e.dataTransfer.setData('text/html', '自定義數(shù)據(jù)2')
})
4.7 effectAllowed 屬性取值會影響到 dropEffect 的取值效果。
effectAllowed 用于限制 dropEffect 只能設置哪些值
effectAllowed 的取值有: + none -> 此項表示 dropEffect 設置任何值都是禁止效果 + copy -> dropEffect 可以設置為 copy + copyLink -> dropEffect 可以設置為 copy 和 link + copyMove -> dropEffect 可以設置為 copy 和 Move + link -> dropEffect 可以設置為 link + linkMove -> dropEffect 可以設置為 link 和 Move + move -> dropEffect 可以設置為 Move + all -> dropEffect 可以設置為所有合法值 + uninitialized -> 等同 all 效果
dropDom.addEventListener('dragover', (e) => {e.preventDefault()e.dataTransfer.dropEffect = 'move'
})
dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.effectAllowed = 'none'
})
上面即使 dropEffect 設置為 move, 但是 effectAllowed 的值為 none,所有還是禁止放置的反饋圖標。
五、總結
- 實現(xiàn)一個拖拽功能時先定義好被拖拽元素和放置區(qū)域元素。
- 所有的放置操作都是在 drop 事件中完成。
- 放置前的反饋效果可以根據(jù)你傳遞的數(shù)據(jù)來設置 dropEffect 顯示不同的效果。
- 被拖拽元素也可以是放置區(qū)域,放置區(qū)域也可以是被拖拽元素,兩者沒有明確的界限。
- 功能自定義按需求開發(fā)