拖放操作

拖放 API 的核心是各種拖動事件,它們以特定的順序觸發,並應以特定的方式處理。本文件描述了拖放操作期間發生的步驟,以及應用程式在每個處理程式中應執行的操作。

從宏觀層面看,拖放操作可能包含以下步驟:

  • 使用者在源節點上開始拖動dragstart 事件在源節點上觸發。在此事件中,源節點為拖動操作準備上下文,包括拖動資料、反饋影像和允許的放置效果。
  • 使用者拖動專案:每次進入新元素時,dragenter 事件在該元素上觸發,而 dragleave 事件在之前的元素上觸發。每隔幾百毫秒,dragover 事件在當前拖動所在的元素上觸發,而 drag 事件在源節點上觸發。
  • 拖動進入有效的放置目標:放置目標取消其 dragover 事件以表明它是一個有效的放置目標。某種形式的放置反饋向用戶指示預期的放置效果。
  • 使用者執行放置drop 事件在放置目標上觸發。在此事件中,目標節點讀取拖動資料。
  • 拖動操作結束dragend 事件在源節點上觸發。無論放置是否成功,此事件都會觸發。

開始拖動

拖動從一個可拖動項開始,該項可以是選區、可拖動元素(包括連結、圖片以及任何帶有 draggable="true" 的元素)、來自作業系統檔案管理器的檔案等。首先,dragstart 事件在源節點上觸發,源節點是可拖動元素,或者對於選區而言,是拖動開始的文字節點。如果此事件被取消,則拖動操作將中止。否則,pointercancel 事件也會在源節點上觸發。

dragstart 事件是唯一可以修改 dataTransfer 的時機。對於自定義可拖動元素,您幾乎總是需要修改拖動資料,這在修改拖動資料儲存中有詳細介紹。您還可以更改另外兩件事:反饋影像允許的放置效果

在此示例中,我們使用 addEventListener() 方法添加了一個dragstart 事件的監聽器。

html
<p draggable="true">This text <strong>may</strong> be dragged.</p>
js
const draggableElement = document.querySelector('p[draggable="true"]');
draggableElement.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", "This text may be dragged");
});

您也可以監聽更高層的祖先元素,因為拖動事件像大多數其他事件一樣會冒泡。因此,通常還會檢查事件的目標,這樣在包含此元素的選區內拖動就不會觸發 setData(儘管在元素內選擇文字很困難,但並非不可能)

js
draggableElement.addEventListener("dragstart", (event) => {
  if (event.target === draggableElement) {
    event.dataTransfer.setData("text/plain", "This text may be dragged");
  }
});

設定拖動反饋影像

當發生拖動時,會從源節點生成一個半透明影像,並在拖動過程中跟隨使用者的指標。此影像是自動建立的,因此您無需自行建立。但是,您可以使用 setDragImage() 指定自定義拖動反饋影像。

js
draggableElement.addEventListener("dragstart", (event) => {
  event.dataTransfer.setDragImage(image, xOffset, yOffset);
});

需要三個引數。第一個是對影像的引用。此引用通常是 <img> 元素,但也可以是 <canvas> 或任何其他元素。反饋影像將根據影像在螢幕上的外觀生成,儘管對於影像,它們將以其原始大小繪製。setDragImage() 方法的第二和第三個引數是影像相對於滑鼠指標應出現的位置的偏移量。

您也可以使用文件中不存在的影像和畫布。這種技術在繪製自定義拖動影像時使用 canvas 元素非常有用,如以下示例所示

js
draggableElement.addEventListener("dragstart", (event) => {
  const canvas = document.createElement("canvas");
  canvas.width = canvas.height = 50;

  const 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();

  event.dataTransfer.setDragImage(canvas, 25, 25);
});

在此示例中,我們使一個畫布成為拖動影像。由於畫布是 50x50 畫素,我們使用其一半的偏移量(25),使影像顯示在滑鼠指標的中心。

在元素上拖動並指定放置目標

在整個拖動操作過程中,所有裝置輸入事件(如滑鼠或鍵盤)都會被抑制。拖動的資料可以在文件中的各個元素上移動,甚至可以在其他文件中的元素上移動。每當進入一個新元素時,dragenter 事件就會在該元素上觸發,而 dragleave 事件則會在前一個元素上觸發。

注意: dragleave 總是 dragenter 之後觸發,因此從概念上講,在這兩個事件之間,目標已經進入了一個新元素但尚未離開前一個元素。

每隔幾百毫秒,就會觸發兩個事件:源節點上的 drag 事件,以及拖動當前所在元素上的 dragover 事件。網頁或應用程式的大部分割槽域都不是有效的資料放置位置,因此元素預設會忽略發生在它上面的任何放置操作。元素可以透過取消 dragover 事件來將自身選為有效的放置目標。如果該元素是一個可編輯的文字欄位,例如 <textarea><input type="text">,並且資料儲存包含一個 text/plain 項,那麼該元素預設是一個有效的放置目標,無需取消 dragover

html
<div id="drop-target">You can drag and then drop a draggable item here</div>
js
const dropElement = document.getElementById("drop-target");

dropElement.addEventListener("dragover", (event) => {
  event.preventDefault();
});

注意: 規範要求放置目標也取消 dragenter 事件,否則 dragoverdragleave 事件甚至不會在該元素上觸發;但在實踐中,沒有瀏覽器實現這一點,“當前元素”在每次進入新元素時都會更改。

注意: 規範要求取消 drag 事件會中止拖動;在實踐中,沒有瀏覽器實現這一點。請參見下面的示例。

條件放置目標

您通常只希望放置目標在某些情況下接受放置(例如,僅當拖動的是連結時)。為此,請檢查條件並在滿足條件時才取消事件。例如,您可以檢查拖動資料是否包含連結

js
dropElement.addEventListener("dragover", (event) => {
  const isLink = event.dataTransfer.types.includes("text/uri-list");
  if (isLink) {
    event.preventDefault();
  }
});

在此示例中,我們使用 includes 方法檢查型別 text/uri-list 是否存在於型別列表中。如果存在,我們將取消事件,以便允許放置。如果拖動資料不包含連結,則事件不會被取消,並且在該位置無法發生放置。

放置反饋

現在使用者正拖動到有效的放置目標。有幾種方法可以向用戶指示在該位置允許放置,以及如果發生放置可能會發生什麼。通常,滑鼠指標會根據 dropEffect 屬性的值進行必要的更新。儘管確切的外觀取決於使用者的平臺,但通常對於 copy,例如會出現一個加號圖示,而當不允許放置時,則會出現一個“此處無法放置”圖示。這種滑鼠指標反饋在許多情況下就足夠了。

放置效果

放置時,可能會執行以下幾種操作

copy

放置後,資料將同時存在於源位置和目標位置。

move

資料將僅存在於目標位置,並將從源位置刪除。

源位置和放置位置之間將建立某種形式的連結;源位置的資料只有一個例項。

none

什麼也沒發生;放置失敗。

透過 dragenterdragover 事件,dropEffect 屬性被初始化為使用者請求的效果。使用者可以透過按修改鍵修改所需效果。儘管使用的確切鍵因平臺而異,但通常會使用 ShiftControl 鍵來在複製、移動和連結之間切換。滑鼠指標將改變以指示所需的操作。例如,對於 copy,游標旁邊可能會出現一個加號。

您可以在 dragenterdragover 事件期間修改 dropEffect 屬性,例如,如果某個特定的放置目標僅支援某些操作。您可以修改 dropEffect 屬性以覆蓋使用者效果,並強制執行特定的放置操作。

js
target.addEventListener("dragover", (event) => {
  event.dataTransfer.dropEffect = "move";
});

在此示例中,執行的是移動效果。

您可以使用值 none 來指示在該位置不允許放置。如果元素只是暫時不接受放置,您通常應該這樣做;如果它不打算成為放置目標,您就不應該取消事件。

請注意,設定 dropEffect 僅指示此刻所需的效應;稍後的 dragover 排程可能會更改它。為了保持選擇,您必須在每個 dragover 事件中設定它。此外,此效應僅具有資訊性,最終實現的效果取決於源節點和目標節點(例如,如果源節點無法修改,那麼即使請求了“移動”效應,也可能無法實現)。

對於使用者手勢和程式設計設定 dropEffect,預設情況下,所有三种放置效果都可用。可拖動元素可以透過在 dragstart 事件監聽器中設定 effectAllowed 屬性來限制只允許某些效果。

js
draggableElement.addEventListener("dragstart", (event) => {
  event.dataTransfer.effectAllowed = "copyLink";
});

在此示例中,只允許複製或連結操作,而不能透過指令碼或使用者手勢選擇移動操作。

effectAllowed 的值是 dropEffect 的組合

描述
none 不允許任何操作
copy copy
move move
link link
copyMove copymove
copyLink copylink
linkMove linkmove
all copymovelink
未初始化 效果未設定時的預設值;通常等同於 all,除了預設的 dropEffect 可能不總是 copy

預設情況下,dropEffect 根據 effectAllowed 初始化,按 copylinkmove 的順序,選擇第一個允許的。如果合適,未選擇但允許的效果也可能被選為預設值;例如,在 Windows 上,按住 Alt 鍵會導致優先使用 link。如果 effectAlloweduninitialized 且被拖動元素是 <a> 連結,則預設 dropEffectlink;如果 effectAlloweduninitialized 且被拖動元素是可編輯文字欄位中的選區,則預設 dropEffectmove

自定義放置反饋

對於更復雜的視覺效果,您可以在 dragenter 事件期間執行其他操作,例如,在將發生放置的位置插入一個元素。這可能是一個插入標記或一個代表拖動元素在新位置的元素。為此,您可以建立一個 <img> 元素並在 dragenter 事件期間將其插入文件。

dragover 事件將在滑鼠指向的元素上觸發。自然地,您可能還需要在 dragover 事件處理程式中移動插入標記。您可以像其他滑鼠事件一樣使用事件的 clientXclientY 屬性來確定滑鼠指標的位置。

最後,當拖動離開元素時,dragleave 事件將在該元素上觸發。這時您應該移除任何插入標記或高亮。您無需取消此事件。dragleave 事件總是會觸發,即使拖動被取消,因此您始終可以確保在此事件期間完成任何插入點清理。

有關使用這些事件的實際示例,請參閱我們的 看板示例

執行放置

當用戶鬆開滑鼠時,拖放操作結束。

為了使放置可能成功,放置必須發生在有效的放置目標上,並且在滑鼠鬆開時 dropEffect 不能是 none。否則,放置操作被認為是失敗的。

如果放置可能成功,則在放置目標上會觸發 drop 事件。您需要使用 preventDefault() 取消此事件,以便將放置視為實際成功。否則,如果放置是將文字(資料包含 text/plain 項)放入可編輯文字欄位,則放置也被視為成功。在這種情況下,文字會被插入到欄位中(根據平臺慣例,插入到游標位置或末尾),並且如果 dropEffectmove 且源是可編輯區域內的選區,則會移除源。否則,對於所有其他拖動資料和放置目標,放置被視為失敗。

drop 事件期間,您應該使用 DataTransfer.getData() 從拖動資料儲存中檢索所需資料,並將其插入到放置位置。您可以使用 dropEffect 屬性來確定所需的拖動操作。drop 事件是除了 dragstart 之外唯一可以讀取拖動資料儲存的時間。

js
target.addEventListener("drop", (event) => {
  event.preventDefault();
  const data = event.dataTransfer.getData("text/plain");
  target.textContent = data;
});

在此示例中,一旦資料被檢索,我們將其字串作為目標的文字內容插入。這樣做會將拖動文字插入到其放置的位置,假設放置目標是文字區域,如 pdiv 元素。

如果資料儲存不包含指定型別的資料,getData() 方法將返回空字串。如果您實現了條件放置目標,這種情況應該不會發生,因為放置目標只在所需資料存在時才接受放置。

您還可以檢索其他型別的資料。如果資料是連結,其型別應為 text/uri-list。然後您可以將連結插入到內容中。

js
target.addEventListener("drop", (event) => {
  event.preventDefault();
  const lines = event.dataTransfer.getData("text/uri-list").split("\r\n");
  lines
    .filter((line) => !line.startsWith("#"))
    .forEach((line) => {
      const link = document.createElement("a");
      link.href = line;
      link.textContent = line;
      target.appendChild(link);
    });
});

有關如何讀取拖動資料的更多資訊,請參閱處理拖動資料儲存

源元素和目標元素也有責任協作實現 dropEffect——源監聽 dragend 事件,目標監聽 drop 事件。例如,如果 dropEffectmove,那麼其中一個元素必須從其舊位置移除拖動的項(通常是源元素本身,因為目標元素不一定知道或控制源)。

放置失敗

如果以下任一情況為真,則拖放操作被視為失敗:

  1. 使用者按下了 Escape
  2. 放置發生在有效的放置目標之外
  3. 在滑鼠鬆開時,放置效果為 none
  4. drop 事件未取消,並且放置操作不是將文字(包含 text/plain 資料)放入可編輯文字欄位(參見執行放置

對於情況 1 和 3,如果中止發生在懸停在有效放置目標上方時,放置目標會收到一個 dragleave 事件,就像放置不再發生在其上方一樣,以便它可以清除任何放置反饋。在所有情況下,後續事件的 dropEffect 都設定為 none

隨後,在源節點上會觸發一個 dragend 事件。瀏覽器可能會顯示一個動畫,將拖動的選區返回到拖放操作的源。

結束拖動

拖動完成後,dragend 事件將在拖動源(收到 dragstart 事件的同一元素)上觸發。無論拖動是否成功,此事件都會觸發。

如果在 dragend 期間 dropEffect 屬性的值為 none,則拖動已取消。否則,該效果指定執行了哪個操作。源可以在 move 操作後使用此資訊從舊位置移除拖動的專案。

放置可以發生在同一個視窗內或另一個應用程式上。dragend 事件始終會觸發。事件的 screenXscreenY 屬性將設定為放置發生時的螢幕座標。

dragend 事件傳播完成後,拖放操作完成。

另見