<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【W(wǎng)eb技術(shù)】1511- 前端拖拽:低代碼、可視化平臺(tái)的基石

          共 7827字,需瀏覽 16分鐘

           ·

          2022-11-24 19:18

          HTML5 Drag and Drop 接口

          html5中提供了一系列Drag and Drop 接口,主要包括四部分:DragEventDataTansfer,DataTransferItem 和DataTransferItemList。

          DragEvent

          源元素和目標(biāo)元素

          源元素: 即被拖拽的元素。

          目標(biāo)元素: 即合法的可釋放元素。

          每個(gè)事件的事件主體都是兩者之一。

          拖拽事件

          事件事件處理程序事件主體觸發(fā)時(shí)機(jī)
          dragstartondragstart源元素當(dāng)源元素開始被拖拽。
          dragondrag源元素當(dāng)源元素被拖拽(持續(xù)觸發(fā))。
          dragendondragend源元素當(dāng)源元素拖拽結(jié)束(鼠標(biāo)釋放或按下esc鍵)
          dragenterondragenter目標(biāo)元素當(dāng)被拖拽元素進(jìn)入該元素。
          dragoverondragover目標(biāo)元素當(dāng)被拖拽元素停留在該元素(持續(xù)觸發(fā))。
          dragleaveondragleave目標(biāo)元素當(dāng)被拖拽元素離開該元素。
          dropondrop目標(biāo)元素當(dāng)拖拽事件在合法的目標(biāo)元素上釋放。

          觸發(fā)順序及次數(shù)

          我們綁定相關(guān)的事件,拖放一次來查看相關(guān)事件的觸發(fā)情況。

          我們讓相應(yīng)事件處理程序打印事件名稱及事件觸發(fā)的主體是誰,下面截取部分展示。

          我們可以看到對(duì)于被拖拽元素,事件觸發(fā)順序是   dragstart->drag->dragend;對(duì)于目標(biāo)元素,事件觸發(fā)的順序是  dragenter->dragover->drop/dropleave

          其中dragdragover會(huì)分別在源元素和目標(biāo)元素反復(fù)觸發(fā)。整個(gè)流程一定是dragstart第一個(gè)觸發(fā),dragend最后一個(gè)觸發(fā)。

          這里還有一個(gè)注意的點(diǎn),如果某個(gè)元素同時(shí)設(shè)置了dragoverdrop的監(jiān)聽,那么必須阻止dragover的默認(rèn)行為,否則drop將不會(huì)被觸發(fā)。

          DataTansfer

          我們先用一張圖來直觀的感受一下:

          我們可以看到,DataTransfer如同它的名字,作用就是在拖放過程中對(duì)數(shù)據(jù)進(jìn)行傳輸,其中setData用來存放數(shù)據(jù),getData用來獲取數(shù)據(jù),出于安全的考量,數(shù)據(jù)只能在drop時(shí)獲取,而effectAlloweddropEffect則影響鼠標(biāo)展示的樣式,下面我們用一個(gè)例子來進(jìn)行展示:

          sourceElem.addEventListener('dragstart', (event) => {
              event.dataTransfer.effectAllowed = 'move';
              event.dataTransfer.setData('text/plain''放進(jìn)來了');
          });
          targetElem.addEventListener('dragover', (event) => {
              event.preventDefault();
              event.dataTransfer.dropEffect = 'move';
          });
          targetElem.addEventListener('drop', (event) => {
              event.target.innerHTML = event.dataTransfer.getData('text/plain');
          });


          可以看到在藍(lán)色方塊設(shè)置的數(shù)據(jù)被成功取得了。

          DataTransferItemList

          屬性

          length: 列表中拖動(dòng)項(xiàng)的數(shù)量。

          方法

          add(): 向拖動(dòng)項(xiàng)列表中添加新項(xiàng) (File對(duì)象或String),該方法返回一個(gè) DataTransferItem) 對(duì)象。

          remove(): 根據(jù)索引刪除拖動(dòng)項(xiàng)列表中的對(duì)象。

          clear(): 清空拖動(dòng)項(xiàng)列表。

          DataTransferItem(): 取值方法:返回給定下標(biāo)的DataTransferItem對(duì)象.

          DataTransferItem

          屬性

          kind: 拖拽項(xiàng)的種類,string 或是 file。

          type: 拖拽項(xiàng)的類型,一般是一個(gè)MIME 類型。

          方法

          getAsString: 使用拖拽項(xiàng)的字符串作為參數(shù)執(zhí)行指定回調(diào)函數(shù)。

          getAsFile: 返回一個(gè)關(guān)聯(lián)拖拽項(xiàng)的 File 對(duì)象 (當(dāng)拖拽項(xiàng)不是一個(gè)文件時(shí)返回 null)。

          實(shí)踐

          學(xué)習(xí)了上面的基礎(chǔ)知識(shí),我們從幾個(gè)常見的應(yīng)用場景入手,來實(shí)踐上面的知識(shí)

          可放置組件

          知道上面幾個(gè)事件后,我們來完成一個(gè)簡單可放置組件,為了方便大家理解,這里不使用任何框架,以免增加不會(huì)框架同學(xué)的學(xué)習(xí)成本。

          想讓組件可拖行,那么就要可以改變它的位置,有兩種思路:

          • pos:abs通過top/left等直接改變元素的位置
          • 使用css的transform屬性中的translate對(duì)元素的位置進(jìn)行改變

          我推薦第二種,首先translate是基于本身的移動(dòng),因此自身的坐標(biāo)就作為原點(diǎn)(0,0),但是第一種,元素本身的top/left等可能并不為0,計(jì)算起來比較復(fù)雜。

          其次,第一種是通過cpu去計(jì)算,而第二種是通過gpu去計(jì)算,并且會(huì)提升到一個(gè)新的層,這樣做非常有利于頁面的性能。原因是 Chrome 這樣將 DOM 轉(zhuǎn)變成一個(gè)屏幕圖像:

          1. 獲取 DOM 并將其分割為多個(gè)層
          2. 將層作為紋理上傳至 GPU
          3. 復(fù)合多個(gè)層來生成最終的屏幕圖像。

          但更新的幀可以走捷徑,不必經(jīng)歷所有過程:

          如果某些特定 CSS 屬性變化,并不需要發(fā)生重繪。Chrome 可以使用早已作為紋理而存在于 GPU 中的層來重新復(fù)合,但會(huì)使用不同的復(fù)合屬性(例如,出現(xiàn)在不同的位置,擁有不同的透明度等等)。

          如果圖層中某個(gè)元素需要重繪,那么整個(gè)圖層都需要重繪 。所以提升為一個(gè)新的層,可以減少重繪的次數(shù)。因?yàn)橹桓淖兾恢茫钥梢詮?fù)用紋理,提高性能。

          有了思路那么我們就開始吧!

          首先我們要知道這次拖拽的向量是怎樣的,因?yàn)?code style="margin: 0px 2px;padding: 2px 4px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word;font-size: 14px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;border-radius: 4px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">DragEvent繼承自MouseEvent ,所以我們可以通過MouseEvent接口的offsetX屬性和offsetY屬性獲取鼠標(biāo)現(xiàn)在相對(duì)于該物體的位置差。

          transform設(shè)置多個(gè)屬性值,效果就可以疊加,所以我們要獲得之前的移動(dòng)效果,再加上現(xiàn)在的移動(dòng)效果即可,之前的移動(dòng)效果可以通過window.getComputedStyle(e.target).transform獲得。

          sourceElem.addEventListener('dragend', (e) => {
              const startPosition = window.getComputedStyle(e.target).transform;
              e.target.style.transform = `${startPosition} translate(${e.offsetX}px, ${e.offsetY}px)`;
          }, true);

          我們給要拖拽的元素加上這段處理程序似乎就大功告成了。


          但是實(shí)際使用時(shí),這個(gè)元素并沒有停在預(yù)覽的位置,而是左上角移動(dòng)到鼠標(biāo)的位置,顯然不符合預(yù)期,相信大家都能猜到,我們少考慮了鼠標(biāo)在元素的位置,而鼠標(biāo)初始的位置同樣可以通過MouseEvent接口的offsetX屬性和offsetY屬性獲取(dragstart)。改善如下:

          function enableDrag(element{
              let mouseDiff = null;
              element.addEventListener('dragstart', (e) => {
                  //初始時(shí)鼠標(biāo)與元素的位置差
                  mouseDiff = `translate(${-e.offsetX}px, ${-e.offsetY}px)`
              }, true);
              element.addEventListener('dragend', (e) => {
                  //開始時(shí)元素的位置
                  const startPosition = window.getComputedStyle(e.target).transform;
                  //鼠標(biāo)移動(dòng)的位置
                  const mouseMove = `translate(${e.offsetX}px, ${e.offsetY}px)`;
                  e.target.style.transform = `${mouseDiff} ${startPosition} ${mouseMove}`;
              }, true);
          }
          enableDrag(souceElement);

          圖的連線

          節(jié)點(diǎn)使用DOM渲染,連線我們使用SVG來渲染,框架使用React,但除了state盡量使用較少的框架相關(guān)的,以防非React技術(shù)棧的同學(xué)看不懂。

          首先我們要先組織我們的state,作為一個(gè)圖,顯然應(yīng)該由nodesedges兩部分組成,我們都使用數(shù)組存儲(chǔ),我們給每個(gè)node一個(gè)唯一的id,使用Map去映射id與對(duì)應(yīng)的positon 形如 [x,y]的關(guān)系,而edge有源端與終端的id,通過id去獲得對(duì)應(yīng)的坐標(biāo)。

          我們先假設(shè)節(jié)點(diǎn)可以完成所有功能了,只考慮連線,可以定義如下的組件

          const Edge = ({nodes:[sourceNode,targetNode]}) =>(
              <svg key={sourceNode.id + targetNode.id||''} 
                  style={{position:'absolute',
                  overflow:'visible',
                  zIndex:'-1',
                  transform:'translate(15px,15px)'}}>

                  <path d={`M ${sourceNode.position[0]} ${sourceNode.position[1]} 
                      C
                      ${(targetNode.position[0]  + sourceNode.position[0])/2} ${sourceNode.position[1]}
                      ${(targetNode.position[0]  + sourceNode.position[0])/2} ${targetNode.position[1]}
                      ${targetNode.position[0]} ${targetNode.position[1]} `}
                      strokeWidth={6}
                      stroke={'red'}
                      fill='none'
                  >
          </path>
              </svg>

          )

          首先我們應(yīng)該從什么時(shí)候生成一個(gè)連線呢,顯然是dragstart,但這時(shí)還沒有對(duì)應(yīng)的終端,因此不應(yīng)該通過加入edges來循環(huán)渲染,而是單獨(dú)渲染一個(gè)出來。在dragstart我們設(shè)置一個(gè)虛擬節(jié)點(diǎn)temNode,并記錄開始節(jié)點(diǎn)的id。

          并如果有temNode則渲染一條預(yù)覽的edge。

          temNode && (<Edge nodes = {[sourceNode,temNode]}></Edge>)

          然后加入這條edge后,我們刪除虛擬節(jié)點(diǎn),變?yōu)檠h(huán)渲染來展示所有的邊。

          edges.map(([sourceId,targetId])=>{
              const sourceNode = getNode(sourceId);
              const targetNode = getNode(targetId);
              return (
                  <Edge nodes = {[sourceNode,targetNode]}></Edge>);
          })

          那么我們只考慮邊的展示了,節(jié)點(diǎn)的功能應(yīng)該如何完善呢?

          首先是dragstart,我們要設(shè)置起始節(jié)點(diǎn)和虛擬節(jié)點(diǎn)

          onDragStart={()=>{
              setStartNodeId(uid);
              setTemNode({position:[x,y]})
          }}

          然后既然邊可以跟著動(dòng),我們必然要在drag中動(dòng)態(tài)的改變虛擬節(jié)點(diǎn)的位置

          onDrag={(event)=>{
              position=[x+event.nativeEvent.offsetX,y+event.nativeEvent.offsetY];
              setTemNode({position})
          }}

          然后drop時(shí),我們加入一條新的邊

          onDrop={
              (event)=>{
                  event.preventDefault();
                  setEdges(edges.concat([[startNodeId,uid]]))
              }
          }

          最重要的是,不論在哪里事件結(jié)束了,要?jiǎng)h除虛擬節(jié)點(diǎn)

          onDragEnd={
              ()=>{
                  setTemNode(null);
              }
          }

          下面是最終成果:


          參考

          • HTML Drag and Drop API - Web APIs | MDN (mozilla.org)

          作者:灰兔呀

          https://juejin.cn/post/7075918201359433758


          往期回顧

          #

          如何使用 TypeScript 開發(fā) React 函數(shù)式組件?

          #

          11 個(gè)需要避免的 React 錯(cuò)誤用法

          #

          6 個(gè) Vue3 開發(fā)必備的 VSCode 插件

          #

          3 款非常實(shí)用的 Node.js 版本管理工具

          #

          6 個(gè)你必須明白 Vue3 的 ref 和 reactive 問題

          #

          6 個(gè)意想不到的 JavaScript 問題

          #

          試著換個(gè)角度理解低代碼平臺(tái)設(shè)計(jì)的本質(zhì)

          瀏覽 89
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  婷婷丁香五月综合 | 亚洲av乱伦小说 亚洲AV无码人妻 亚洲超碰成人在线 | AV日韩中文字幕 | 91精品国自产 | 超碰2021 |