處理拖動資料儲存

DragEvent 介面有一個 dataTransfer 屬性,它是一個 DataTransfer 物件。DataTransfer 物件代表拖動操作的主要上下文,並且在不同事件觸發時保持一致。它包括拖動資料拖動影像放置效果等。本文重點介紹 dataTransfer資料儲存部分。

拖動資料儲存的結構

從根本上說,拖動資料儲存是一個專案列表,表示為 DataTransferItem 物件的 DataTransferItemList。每個專案可以是以下兩種型別之一:

此外,該專案還由一個型別標識,按照慣例,該型別採用 MIME 型別的形式。此型別可以指導消費者如何解析或解碼有效負載。對於所有文字項,列表只能包含每種型別的一個專案,因此實際上,該列表包含兩個不相交的集合:一個可能包含重複型別的檔案列表,以及一個以型別為鍵的文字項 Map。通常,檔案列表表示多個正在拖動的檔案。文字對映表示正在傳輸多個資源,而是以不同方式編碼的相同資源,以便接收端可以選擇最合適的受支援解釋。文字項按優先順序降序排序。

此列表可透過 DataTransfer.items 屬性訪問。

HTML 拖放 API 經歷了多次迭代,導致管理資料儲存的方式有兩種共存。在 DataTransferItemListDataTransferItem 介面之前,“舊方式”使用 DataTransfer 上的以下屬性:

  • types:包含列表中文字項type 屬性,以及如果存在任何檔案項則包含值 "files"
  • setData()getData()clearData():使用“型別到有效負載對映”模型提供對列表中文字項的訪問。
  • files:以 FileList 的形式提供對列表中檔案項的訪問。

您可能會看到檔案項的型別未直接公開。它們仍然可以訪問,但只能透過 files 列表中每個 File 物件的 type 屬性訪問,因此如果您無法讀取檔案,則也無法知道它們的型別(有關何時可讀儲存,請參閱讀取拖動資料儲存)。

要獲取檔案及其型別,我們建議使用 items 屬性,因為它提供了一個更靈活和一致的介面。對於文字項,您也應該為了保持一致性而優先使用 items 屬性,儘管 getData() 方法更方便訪問或刪除特定型別。

DataTransferDataTransferItem 介面之間的另一個關鍵區別是,前者使用同步的 getData() 方法訪問文字有效負載,而後者則使用非同步的 getAsString() 方法。

修改拖動資料儲存

對於預設可拖動的專案,例如影像、連結和選區,拖動資料已由瀏覽器定義;對於使用 draggable 屬性定義的自定義可拖動元素,您必須自行定義拖動資料。唯一可以修改資料儲存的時間是在 dragstart 處理程式中——對於任何其他拖動事件的 dataTransfer,資料儲存都是不可修改的。

要將文字資料新增到拖動資料儲存,“新方式”使用 DataTransferItemList.add() 方法,而“舊方式”使用 DataTransfer.setData() 方法。

js
function dragstartHandler(ev) {
  // New way: add(data, type)
  ev.dataTransfer.items.add(ev.target.innerText, "text/plain");
  // Old way: setData(type, data)
  ev.dataTransfer.setData("text/html", ev.target.outerHTML);
}

const p1 = document.getElementById("p1");
p1.addEventListener("dragstart", dragstartHandler);

對於這兩種方法,如果在資料儲存不可修改時呼叫它們,則什麼都不會發生。如果存在具有相同型別的文字項,add() 會丟擲錯誤,而 setData() 會覆蓋現有項。

要將檔案資料新增到拖動資料儲存,“新方式”仍然使用 DataTransferItemList.add() 方法。由於“舊方式”將檔案項儲存在 DataTransfer.files 屬性中,這是一個只讀的 FileList,因此沒有直接的等效方法。

js
function dragstartHandler(ev) {
  // New way: add(data)
  ev.dataTransfer.items.add(new File([blob], "image.png"));
}

const p1 = document.getElementById("p1");
p1.addEventListener("dragstart", dragstartHandler);

請注意,在新增檔案資料時,add() 會忽略 type 引數,並使用 File 物件的 type 屬性。

注意:讀防寫是按作業進行的,這意味著只有 dragstart 處理程式中的同步程式碼才能修改資料儲存。如果您在非同步操作後嘗試訪問資料儲存,您將不再擁有寫入許可權。例如,這不起作用:

js
function dragstartHandler(ev) {
  canvas.toBlob((blob) => {
    ev.dataTransfer.items.add(new File([blob], "image.png"));
  });
}

刪除資料類似,使用 DataTransferItemList.remove()DataTransferItemList.clear()DataTransfer.clearData() 方法。

讀取拖動資料儲存

除了 dragstart 事件(您擁有對資料儲存的完全訪問許可權)之外,您唯一可以從資料儲存中讀取的時間是在 drop 事件期間,這允許放置目標檢索資料。

要從拖動資料儲存中讀取文字資料,“新方式”使用 DataTransferItemList 物件,而“舊方式”使用 DataTransfer.getData() 方法。新方式更方便遍歷所有專案,而舊方式更方便訪問特定型別。

js
function dropHandler(ev) {
  // New way: loop through items
  for (const item of ev.dataTransfer.items) {
    if (item.kind === "string") {
      item.getAsString((data) => {
        // Do something with data
      });
    }
  }
  // Old way: getData(type)
  const data = ev.dataTransfer.getData("text/plain");
}

const p1 = document.getElementById("p1");
p1.addEventListener("drop", dropHandler);

要從拖動資料儲存中讀取檔案資料,“新方式”仍然使用 DataTransferItemList 物件,而“舊方式”使用 DataTransfer.files 屬性。

js
function dropHandler(ev) {
  // New way: loop through items
  for (const item of ev.dataTransfer.items) {
    if (item.kind === "file") {
      const file = item.getAsFile(); // A File object
    }
  }
  // Old way: loop through files
  for (const file of ev.dataTransfer.files) {
    // Do something with file
  }
}

const p1 = document.getElementById("p1");
p1.addEventListener("drop", dropHandler);

保護模式

dragstartdrop 事件之外,資料儲存處於保護模式,不允許程式碼訪問任何有效負載。即:

  • 所有修改嘗試都會靜默地不執行任何操作或丟擲 DOMException(僅適用於 items.add()items.remove())。
  • DataTransfer.getData() 始終返回空字串。
  • DataTransfer.files 始終返回空列表。
  • DataTransferItem.getAsString() 返回而不呼叫回撥。
  • DataTransferItem.getAsFile() 始終返回 null

同樣,讀防寫是按作業進行的,這意味著只有 drop 處理程式中的同步程式碼才能讀取資料儲存。如果您在非同步操作後嘗試訪問資料儲存,您將不再擁有寫入許可權。例如,這不起作用:

js
function getDataPromise(item) {
  return new Promise((resolve) => {
    item.getAsString((data) => {
      resolve(data);
    });
  });
}

async function dropHandler(ev) {
  for (const item of ev.dataTransfer.items) {
    if (item.kind === "string") {
      // Bad: by the second time this runs, we are no longer in the same job
      const data = await getDataPromise(item);
    }
  }
}

const p1 = document.getElementById("p1");
p1.addEventListener("drop", dropHandler);

相反,您必須同步呼叫所有訪問方法,然後稍後等待它們的結果。

js
async function dropHandler(ev) {
  const promises = [];
  for (const item of ev.dataTransfer.items) {
    if (item.kind === "string") {
      // Bad: by the second time this runs, we are no longer in the same job
      promises.push(getDataPromise(item));
    }
  }
  const results = await Promise.all(promises);
}

常見的拖動資料型別

規範只定義了少數資料型別的行為,但瀏覽器有時對更多型別具有原生支援。通常,型別旨在像 MIME 型別一樣作為一種協議,只要接收端(另一個網頁、同一網頁的另一部分,甚至瀏覽器外部的某個地方)理解它,您就可以使用任何型別。本節描述了一些常見約定和瀏覽器的預設行為。

請注意,以下場景指的是意圖而不是行為。例如,當我們說“拖動連結”時,使用者可能沒有拖動實際的 <a> 元素;他們可能正在拖動一個包含一個或多個連結的容器,但意圖是傳輸連結作為資料,因此您準備的資料儲存可以與使用者拖動實際連結時相同。

拖動文字

對於拖動文字,使用 text/plain 型別,拖動字串作為值。例如:

js
event.dataTransfer.items.add("This is text to drag", "text/plain");

您應該始終新增 text/plain 型別的資料作為不支援其他型別的應用程式或放置目標的備用,除非沒有邏輯上的文字替代方案。始終將此 text/plain 型別最後新增,因為它最不具體,不應被優先考慮。

getData()setData()clearData() 中,Text 型別(不區分大小寫)被視為 text/plain

預設情況下,當選擇被拖動時,會建立以下資料項:

  • text/plain:包含選定的文字。Firefox 和 Safari 將此項排在 text/html 之後,儘管規範要求它排在第一位。
  • text/html:包含選定元素的完整 HTML 原始碼(所有樣式內聯)。

規範還要求另一個型別為 application/microdata+json 的項,其中包含從拖動選擇中的元素中提取的微資料。目前沒有瀏覽器實現此項。

當放置到可編輯文字欄位(例如 <textarea><input type="text">)時,text/plain 項預設會被複制到欄位中(沒有任何事件處理)。

拖動的超連結應包含兩種型別的資料:text/uri-listtext/plain兩種型別都應使用連結的 URL 作為其資料。注意:URL 型別是 uri-list,帶有 I,而不是 L

像往常一樣,將 text/plain 型別放在最後,作為 text/uri-list 型別的備用。例如:

js
event.dataTransfer.items.add("https://www.mozilla.org", "text/uri-list");
event.dataTransfer.items.add("https://www.mozilla.org", "text/plain");

要拖動多個連結,請使用 CRLF 換行符分隔 text/uri-list 資料中的每個連結。以井號(#)開頭的行是註釋,不應被視為 URL。您可以使用註釋來指示 URL 的目的、與 URL 關聯的標題或其他資料。

警告:多個連結的 text/plain 備用應包含所有 URL,但不包含註釋。

例如,此示例 text/uri-list 資料包含兩個連結和一個註釋:

https://www.mozilla.org
#A second link
http://www.example.com

檢索拖放的連結時,請確保處理多個連結被拖動的情況,包括任何註釋。

getData()setData()clearData() 中,URL 型別(不區分大小寫)被視為 text/uri-list。對於 getData(),結果只包含列表中的第一個 URL。

預設情況下,當拖動 <a> 元素時,會建立以下資料項:

  • text/x-moz-url (僅限 Firefox):包含 href 屬性和連結文字,用換行符分隔。
  • text/x-moz-url-data (僅限 Firefox):僅包含 href
  • text/x-moz-url-desc (僅限 Firefox):僅包含連結文字。
  • text/uri-list:包含 href 屬性。
  • text/html (僅限 Chrome 和 Firefox):包含 <a> 元素的完整 HTML 原始碼(所有樣式內聯)。
  • text/plain:也包含 href 屬性。Chrome 將此項排在 text/uri-list 之前。

拖動影像

直接拖動影像(即,資料是畫素內容)不常見,並且可能在某些平臺不受支援。相反,影像通常僅透過其 URL 拖動。為此,與其他 URL 一樣,使用 text/uri-list 型別。資料應該是影像的 URL,如果影像未儲存在網站或磁碟上,則為 data: URL

與連結一樣,text/plain 型別的資料也應包含 URL。但是,data: URL 在文字上下文中通常沒有用處,因此在這種情況下您可能希望排除 text/plain 資料。

js
event.dataTransfer.items.add(imageURL, "text/uri-list");
event.dataTransfer.items.add(imageURL, "text/plain");

預設情況下,當拖動 <img> 元素時,會建立以下資料項:

  • text/x-moz-url (僅限 Firefox):包含 src 屬性和 alt 文字(如果 alt 為空,則再次為 src),用換行符分隔。
  • text/x-moz-url-data (僅限 Firefox):僅包含 src 屬性。
  • text/x-moz-url-desc (僅限 Firefox):僅包含 alt 文字(如果 alt 為空,則為 src)。
  • text/uri-list:包含 src 屬性。
  • text/html:包含 <img> 元素的完整 HTML 原始碼(所有樣式內聯)。
  • text/plain (僅限 Firefox):包含 src 屬性。

Safari 還會建立一個檔案項,其中包含影像資料,並帶有適當的 MIME 型別,例如 image/png

拖動元素

當拖動的專案是帶有 draggable="true" 的任意元素時,要設定什麼資料取決於您打算傳輸什麼。

傳輸元素的常用方法是使用包含序列化 HTML 原始碼的 text/html 型別,接收端可以解析並插入。例如,將其資料設定為元素的 outerHTML 屬性的值是合適的。也可以使用 text/xml,但要確保資料是格式良好的 XML。

您還可以使用 text/plain 型別包含 HTML 或 XML 資料的純文字表示。資料應僅包含文字,不含任何源標籤或屬性。例如:

js
event.dataTransfer.items.add("text/html", element.outerHTML);
event.dataTransfer.items.add("text/plain", element.innerText);

您還可以使用為自定義目的而發明的其他型別。儘量始終包含一個 text/plain 替代方案,除非拖動的物件是特定於某個站點或應用程式的。在這種情況下,自定義型別可確保資料無法放置到其他位置。

從作業系統檔案瀏覽器拖動檔案

當拖動的專案是檔案時,會將一個型別為 file 的專案新增到拖動資料中。type 被設定為檔案的 MIME 型別(由作業系統提供),如果型別未知,則為 application/octet-stream。目前,拖動的檔案只能源自瀏覽器外部,例如來自檔案瀏覽器。

Firefox 還會新增一個非標準的文字項,型別為 application/x-moz-file,其中包含使用者檔案系統上檔案的完整路徑。除非在特權程式碼(例如擴充套件程式)中,否則其值為一個空字串。

將檔案拖到作業系統檔案瀏覽器

可以從瀏覽器傳輸的內容主要取決於瀏覽器及其拖動到的位置。將影像拖動到本地檔案系統通常受支援,並導致影像被下載。

Chrome 支援非標準的 DownloadURL 型別。有效負載應為 <MIME type>:<file name>:<file URL> 格式的文字。例如:

js
event.dataTransfer.items.add(
  "DownloadURL",
  "image/png:example.png:data:image/png;base64,iVBORw0K...",
);

這允許在拖動到檔案瀏覽器時下載任意檔案,或者在拖放到另一個瀏覽器視窗時,就像正在拖放檔案一樣(儘管可能適用 CORS 限制)。有關實際用例,請參閱 像 Gmail 一樣拖出檔案

另見