前端里的“拖拖拽拽”

最近在項目中使用了 react-dnd撵割,一個基于 HTML5 的拖拽庫米愿,“拖拽能力”豐富了前端的交互方式富蓄,基于拖拽能力,會擴展各種各樣的拖拽反饋效果尚镰,因此有必要學習了解,最好的學習方式就是實操初烘!

image

拖拽交互常見于各種前端編輯器里,而“編輯器”是一個集成前端技術能力的綜合性工程分俯,其中就會涉及到各種形式的拖拽交互肾筐,因為“拖拽”是提升用戶體驗的重要交互方式澳迫,所以需要對拖拽的交互效果做各種定制化橄登,作為開發(fā)者理應熟練掌握“拖拽”的應用!

最近在開發(fā)一款低代碼平臺谣妻,所以借此機會分享一下關于“拖拽”這一交互的基礎知識和實踐經(jīng)驗卒稳,希望可以給有需要的同學提供一點參考充坑。

一、HTML5 中的拖放

拖(Drag)和放(Drop)是 HTML5 標準的組成部分辈灼,了解掌握之后也榄,舉一反三,有助于提升我們在拖拽場景下技術方案的設計能力降宅。

1.1 draggable 屬性

現(xiàn)代瀏覽器中腰根,不難發(fā)現(xiàn),圖片標簽(<img />)是可以被長按拖拽贸营,但如果需要自定義的 DOM 節(jié)點可以被拖拽需要配置以告訴瀏覽器提供對元素(Element / Tag)支持拖拽的能力岩睁。

而元素是否允許被拖放且可響應 API 操作依賴于 draggable 全局標簽屬性

draggable 是一個布爾值類型的標簽屬性:

  • true:元素可被拖拽
  • false:元素不可拖拽

當元素設置了 draggable 屬性捕儒,此時長按就可以自由拖拽了:

image

1.2 Darg & Drop 事件

HTML 的 drag & drop 使用了“DOM Event”和從“Mouse Event”繼承而來的“drag event” 刘莹。

一個典型的拖拽操作: 用戶選中一個可拖拽的(draggable)元素,并將其拖拽(鼠標按住不放)至一個可放置的(droppable)元素上扇调,然后松開鼠標抢肛。

在拖動元素期間捡絮,一些與拖放相關的事件會被觸發(fā),像 dragdragover 類型的事件會被頻繁觸發(fā)涎拉。

除了定義拖拽事件類型的圆,每個事件類型還賦予了對應的事件處理器

事件類型 事件處理器 觸發(fā)時機 綁定元素
dragstart ondragstart 當開始拖動一個元素時 拖拽
drag ondrag 當元素被拖動期間按一定頻率觸發(fā) 拖拽
dragend ondragend 當拖動的元素被釋放(???松開越妈、按鍵盤 ESC)時 拖拽
dragenter ondragenter 當拖動元素到一個可釋放目標元素時 放置
dragexit ondragexit 當元素變得不再是拖動操作的選中目標時 放置
dragleave ondragleave 當拖動元素離開一個可釋放目標元素 放置
dragover ondragover 當元素被拖到一個可釋放目標元素上時(100 ms/次) 放置
drop ondrop 當拖動元素在可釋放目標元素上釋放時 放置

各個事件的時機可以用下面這個圖簡單表示:

image

??注意: dragOver 事件的默認行為是:“Reset the current drag operation to "none"”。也就是說瓤檐,如果不阻止放置元素的 dragOver 事件,則放置元素不會響應“拖動元素”的“放置行為”

// 讓綁定該事件的元素支持放置
function handleDragOver(e) {
  // 阻止默認的重置行為
  // 即可成為拖拽元素的放置區(qū)
  e.preventDefault();
}

從設計事件標準來看祭示,如果我們需要自行實現(xiàn)拖拽的效果质涛,就需要從這關鍵的幾個事件去思考設計掰担。

1.3 DataTransfer

在上述的事件類型中带饱,不難發(fā)現(xiàn),放置元素和拖動元素分別綁定了自己的事件教寂,可如何將拖拽元素和放置元素建立聯(lián)系以及傳遞數(shù)據(jù)执庐?

這就涉及到 DataTransfer 對象:

DataTransfer 對象用于保存拖動并放下(drag and drop)過程中的數(shù)據(jù)轨淌。它可以保存一項或多項數(shù)據(jù),這些數(shù)據(jù)項可以是一種或者多種數(shù)據(jù)類型婚被。 —— DataTransfer - MDN

DataTransfer 對象在不同瀏覽器上因為標準可能不一樣使得 API 有差異梳虽,但有幾個“標準(常用)”屬性和方法需要熟悉

在 Chrome 瀏覽器上的 DataTransfer 實例如下:

image

(1) 屬性

屬性 說明
dropEffect 獲取當前選定的拖放操作類型或者設置的為一個新的類型窜觉。值為:none禀挫、copy、link描孟、move
effectAllowed 提供所有可用的操作類型匿醒。值是:none、copy溉痢、copyLink憋他、copyMove竹挡、link、linkMove汽畴、move耸序、all坎怪、uninitialized
files 包含數(shù)據(jù)傳輸中可用的所有本地文件的列表。如果拖動操作不涉及拖動文件嘁酿,則此屬性為空列表
items (只讀) 提供一個包含所有拖動數(shù)據(jù)列表的 DataTransferItemList 對象
types (只讀) 提供一個 dragstart 事件中設置的格式的 strings 數(shù)組闹司。

(2) 方法

屬性 說明
setData(format, value) 設置給定類型的數(shù)據(jù)沐飘。如果該類型的數(shù)據(jù)不存在,則將其添加到末尾保檐,以便類型列表中的最后一項將是新的格式影晓。如果該類型的數(shù)據(jù)已經(jīng)存在檩禾,則在相同位置替換現(xiàn)有數(shù)據(jù)锌订。
getData(format) 檢索給定類型的數(shù)據(jù)画株,如果該類型的數(shù)據(jù)不存在或 data transfer 不包含數(shù)據(jù)谓传,則返回空字符串
clearData([format]) 刪除與給定類型關聯(lián)的數(shù)據(jù)芹关。類型參數(shù)是可選的侥衬。如果類型為空或未指定,則刪除與所有類型關聯(lián)的數(shù)據(jù)直颅。如果指定類型的數(shù)據(jù)不存在功偿,或者 data transfer 中不包含任何數(shù)據(jù)往堡,則該方法不會產(chǎn)生任何效果虑灰。
`setDragImage(img element, xOffset, yOffset)` 設置自定義的拖動圖像,注意圖像需要提前加載颤诀,否則會無效

在簡單的拖拽場景中庸娱,其實可以類比 window.localStorage 對象的 setItem()getItem() 方法來理解記憶.

getData() 在測試中發(fā)現(xiàn)只能在 ondrop 事件中獲取到值:

image

1.4 一個案例掌握拖放 API

<div>
  <div class="drag" draggable="true" id="dragger" ondragstart="handleDragStart(event)">拖動元素</div>
  <div class="drop" ondrop="handleDrop(event)" ondragover="allowDrop(event)">放置區(qū)域</div>
</div>

<script>
  function handleDragStart(e) {
    e.dataTransfer.setData('DRAG_NODE_ID', e.target.id)
  }
  function handleDragOver(e) {
    e.preventDefault();
  }
  function handleDrop(e) {
    e.preventDefault();
    var data = e.dataTransfer.getData('DRAG_NODE_ID');
    e.target.appendChild(document.getElementById(data));
  }
</script>

演示案例: https://codepen.io/DYBOY/pen/eYeyvWm

效果:

演示
拖拽演示效果

1.6 兼容性

是 HTML5 標準提出的能力,因此各大瀏覽器廠商對于標準的支持有差異剧包,其兼容性參考如下:

image

相較于傳統(tǒng)的通過鼠標事件:mousedownmousemove一铅、mouseup 組合實現(xiàn)的拖拽要簡單很多潘飘,少了放入目標邊界的判斷掉缺,也少了對位置的實時獲取操作眶明。

另外目前的 API 不算多,例如我們想要定制化拖拽的圖片大小丑瞧、鼠標樣式等蜀肘,目前暫時沒發(fā)現(xiàn)比較方便的解決方式幌缝,但是從另一個角度來說,讓我們對于拖拽能力的設計和標準有了一個更深切的認識浴栽,對于設計實現(xiàn)拖拽交互有了一個“理論”基礎典鸡!

二坏晦、手搓一個

有了上面的基礎知識昆婿,那么實現(xiàn)一個列表拖拽排序并不是什么難事。

2.1 設計實現(xiàn)

結合上述的 Drag & Drop 的事件類型睁冬,那么拖拽排序主要是針對“拖動對象”之間相互作用關系的邏輯梳理看疙,此處我們暫且區(qū)分為:

  • 源對象: 拖拽列表中被拖動的單個列表項
  • 目標對象: 拖拽列表中和“源對象”產(chǎn)生“相互作用”的列表項

整體的交互事件的設計思路如下:

(1) ondragstart

此時開始拖拽“源對象”的時機,在此事件回調函數(shù)中改變“源對象”的樣式脚线,設置拖拽的一些傳遞參數(shù)等初始值弥搞。

// 源對象開始拖拽
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
  e.dataTransfer.effectAllowed = "move";
  setDragId(e.currentTarget.dataset.index); // 從 dataset 獲取拖拽項的 id
};

(2) ondragover

正與拖拽中的“源對象”產(chǎn)生相互影響的目標對象拓巧,此時“源對象”處于“目標對象”的正上方肛度,目標對象 100ms/次的頻率調用“目標對象”的 ondragover 中聲明的回調事件投慈。

此時伪煤,我們會計算改變“源對象”和“目標對象”的位置。

// 源對象在目標對象上方時
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
  e.preventDefault(); // 允許放置职烧,阻止默認事件
  const dropId = e.currentTarget.dataset.index;
  move(dragId, dropId); // 改變原列表數(shù)據(jù)
};

(3) ondrag

該事件作用于“源對象”蚀之,此時正處于拖拽過程中捷泞,此時可以改變源對象的 opacity锁右、display(none)visiblity 樣式屬性拂到,如果在 dragstart 事件改變兄旬,則會導致拖拽拷貝對象丟失浦夷。

// 源對象被拖拽過程中
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
  e.currentTarget.style.opacity = "0";
};

(4) ondragend

在松手完成“源對象”的放置時,主動調用綁定在“源對象”身上的事件呐馆,此時恢復更改的樣式莲兢。

// 源對象被放置完成時
const handleDragEnd = (e: React.DragEvent<HTMLDivElement>) => {
  e.currentTarget.style.opacity = "1";
};

2.2 實現(xiàn)效果

image

2.3 加點動畫

上面的實現(xiàn)中效果還算可以改艇,但是少了拖拽項的切換過程動畫谒兄,直接在 dragover 事件中通過 move(dragId, dropId) 方法直接修改了原列表數(shù)據(jù)的排序,導致切換突變邻耕。

借助 animation 新增 CSS 幀動畫:

@keyframes dropUp {
  100% {
    transform: translateY(5px);
  }
}

@keyframes dropDown {
  100% {
    transform: translateY(-5px);
  }
}

.drop-up{
  animation: dropUp 0.3s ease-in-out forwards;
}
.drop-down{
  animation: dropDown 0.3s ease-in-out forwards;
}

同樣的在 dragOver 事件中處理兄世,新增邏輯代碼:

// 源對象在目標對象上方時
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
  ...
  // 設置動畫
  const dropId = e.currentTarget.dataset.index;
  const dragIndex = findIndex(listData, (i) => i.id === dragId);
  const dropIndex = findIndex(listData, (i) => i.id === dropId);
  // 通過增加對應的 CSS class御滩,實現(xiàn)視覺上的動畫過渡
  e.currentTarget.classList.remove("drop-up", "drop-down");
  if (dragIndex < dropIndex) {
    e.currentTarget.classList.add("drop-down");
  } else if (dragIndex > dropIndex) {
    e.currentTarget.classList.add("drop-up");
  } 
  ...
};

增加了動畫的效果:

增加了動畫的效果

看起來似乎好一點了削解,當然大家可以去擴充動畫的效果麸锉,亦或者借助三方動畫庫花沉。

三碱屁、已有拖拽庫

目前主流的拖拽庫有:

關于幾者的差異,可以參閱:《關于react中使用拖拽插件的評測

image

四、總結

由于低代碼平臺其實會有豐富的拖拽場景,從可擴展和兼容性上考慮幻枉,最終選擇了 react-dnd 作為基礎拖拽庫熬甫,當然蔓罚,在復雜的拖拽場景下豺谈,是需要自行擴展該拖拽庫,上手難度相對會高一點厂榛,不過有了這些“拖拽知識”作為前置基礎噪沙,那么擴展功能也就不是什么難事了吐根。

朋友們可以關注筆者的微信公眾號:DYBOY拷橘,來一起玩耍呀~

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末冗疮,一起剝皮案震驚了整個濱河市檩帐,隨后出現(xiàn)的幾起案子湃密,更是在濱河造成了極大的恐慌,老刑警劉巖拔妥,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件没龙,死亡現(xiàn)場離奇詭異硬纤,居然都是意外死亡,警方通過查閱死者的電腦和手機洼裤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門逸邦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缕减,“玉大人芒珠,你說我怎么就攤上這事皱卓。” “怎么了嫂易?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵怜械,是天一觀的道長缕允。 經(jīng)常有香客問我蹭越,道長响鹃,這世上最難降的妖魔是什么茴迁? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任堕义,我火速辦了婚禮脆栋,結果婚禮上椿争,老公的妹妹穿的比我還像新娘熟嫩。我一直安慰自己掸茅,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著合住,像睡著了一般撒璧。 火紅的嫁衣襯著肌膚如雪卿樱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天贡珊,我揣著相機與錄音,去河邊找鬼烤送。 笑死糠悯,一個胖子當著我的面吹牛互艾,可吹牛的內容都是我干的纫普。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼节视,長吁一口氣:“原來是場噩夢啊……” “哼拳锚!你這毒婦竟也來了?” 一聲冷哼從身側響起寻行,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤霍掺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拌蜘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杆烁,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年简卧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贞滨。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡入热,死狀恐怖,靈堂內的尸體忽然破棺而出晓铆,到底是詐尸還是另有隱情勺良,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布骄噪,位于F島的核電站尚困,受9級特大地震影響,放射性物質發(fā)生泄漏链蕊。R本人自食惡果不足惜事甜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望滔韵。 院中可真熱鬧逻谦,春花似錦、人聲如沸陪蜻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宴卖。三九已至滋将,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間症昏,已是汗流浹背随闽。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肝谭,地道東北人掘宪。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓蛾扇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親添诉。 傳聞我的和親對象是個殘疾皇子屁桑,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容

  • HTML5篇是拖拽學習的最后一篇了,O(∩_∩)O哈哈~栏赴。蘑斧! 參考傳送門:http://www.cnblogs.c...
    迷緣火葉閱讀 713評論 0 3
  • https://www.cnblogs.com/moqiutao/p/6365113.html 抓取對象以后拖放到...
    skoll閱讀 617評論 0 0
  • 前言 拖放(drap && drop)在我們平時的工作中,經(jīng)常遇到须眷。它表示:抓取對象以后拖放到另一個位置竖瘾。目前,它...
    weiqinl閱讀 1,346評論 0 3
  • 前言 本文依據(jù)半年前本人的分享《淺談js拖拽》撰寫花颗,算是一篇遲到的文章捕传。 基本思路 雖然現(xiàn)在關于拖拽的組件庫到處都...
    lanberts閱讀 2,419評論 0 0
  • 1.處理步驟 a.定義可拖動目標 b.定義被拖動的數(shù)據(jù),可能為多種不同的格式 c.允許設置拖拽效果 d.定義放置區(qū)...
    阿迪呀dity閱讀 1,588評論 0 3