處理拖動資料儲存
DragEvent 介面有一個 dataTransfer 屬性,它是一個 DataTransfer 物件。DataTransfer 物件代表拖動操作的主要上下文,並且在不同事件觸發時保持一致。它包括拖動資料、拖動影像、放置效果等。本文重點介紹 dataTransfer 的資料儲存部分。
拖動資料儲存的結構
從根本上說,拖動資料儲存是一個專案列表,表示為 DataTransferItem 物件的 DataTransferItemList。每個專案可以是以下兩種型別之一:
string:其有效負載是一個字串,可以透過getAsString()檢索。file:其有效負載是一個檔案物件,可以透過getAsFile()(或getAsFileSystemHandle()或webkitGetAsEntry(),如果需要更復雜的檔案系統操作)檢索。
此外,該專案還由一個型別標識,按照慣例,該型別採用 MIME 型別的形式。此型別可以指導消費者如何解析或解碼有效負載。對於所有文字項,列表只能包含每種型別的一個專案,因此實際上,該列表包含兩個不相交的集合:一個可能包含重複型別的檔案列表,以及一個以型別為鍵的文字項 Map。通常,檔案列表表示多個正在拖動的檔案。文字對映不表示正在傳輸多個資源,而是以不同方式編碼的相同資源,以便接收端可以選擇最合適的受支援解釋。文字項按優先順序降序排序。
此列表可透過 DataTransfer.items 屬性訪問。
HTML 拖放 API 經歷了多次迭代,導致管理資料儲存的方式有兩種共存。在 DataTransferItemList 和 DataTransferItem 介面之前,“舊方式”使用 DataTransfer 上的以下屬性:
types:包含列表中文字項的type屬性,以及如果存在任何檔案項則包含值"files"。setData()、getData()、clearData():使用“型別到有效負載對映”模型提供對列表中文字項的訪問。files:以FileList的形式提供對列表中檔案項的訪問。
您可能會看到檔案項的型別未直接公開。它們仍然可以訪問,但只能透過 files 列表中每個 File 物件的 type 屬性訪問,因此如果您無法讀取檔案,則也無法知道它們的型別(有關何時可讀儲存,請參閱讀取拖動資料儲存)。
要獲取檔案及其型別,我們建議使用 items 屬性,因為它提供了一個更靈活和一致的介面。對於文字項,您也應該為了保持一致性而優先使用 items 屬性,儘管 getData() 方法更方便訪問或刪除特定型別。
DataTransfer 和 DataTransferItem 介面之間的另一個關鍵區別是,前者使用同步的 getData() 方法訪問文字有效負載,而後者則使用非同步的 getAsString() 方法。
修改拖動資料儲存
對於預設可拖動的專案,例如影像、連結和選區,拖動資料已由瀏覽器定義;對於使用 draggable 屬性定義的自定義可拖動元素,您必須自行定義拖動資料。唯一可以修改資料儲存的時間是在 dragstart 處理程式中——對於任何其他拖動事件的 dataTransfer,資料儲存都是不可修改的。
要將文字資料新增到拖動資料儲存,“新方式”使用 DataTransferItemList.add() 方法,而“舊方式”使用 DataTransfer.setData() 方法。
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,因此沒有直接的等效方法。
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 處理程式中的同步程式碼才能修改資料儲存。如果您在非同步操作後嘗試訪問資料儲存,您將不再擁有寫入許可權。例如,這不起作用:
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() 方法。新方式更方便遍歷所有專案,而舊方式更方便訪問特定型別。
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 屬性。
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);
保護模式
在 dragstart 和 drop 事件之外,資料儲存處於保護模式,不允許程式碼訪問任何有效負載。即:
- 所有修改嘗試都會靜默地不執行任何操作或丟擲
DOMException(僅適用於items.add()和items.remove())。 DataTransfer.getData()始終返回空字串。DataTransfer.files始終返回空列表。DataTransferItem.getAsString()返回而不呼叫回撥。DataTransferItem.getAsFile()始終返回null。
同樣,讀防寫是按作業進行的,這意味著只有 drop 處理程式中的同步程式碼才能讀取資料儲存。如果您在非同步操作後嘗試訪問資料儲存,您將不再擁有寫入許可權。例如,這不起作用:
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);
相反,您必須同步呼叫所有訪問方法,然後稍後等待它們的結果。
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 型別,拖動字串作為值。例如:
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-list 和 text/plain。兩種型別都應使用連結的 URL 作為其資料。注意:URL 型別是 uri-list,帶有 I,而不是 L。
像往常一樣,將 text/plain 型別放在最後,作為 text/uri-list 型別的備用。例如:
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 資料。
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 資料的純文字表示。資料應僅包含文字,不含任何源標籤或屬性。例如:
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> 格式的文字。例如:
event.dataTransfer.items.add(
"DownloadURL",
"image/png:example.png:data:image/png;base64,iVBORw0K...",
);
這允許在拖動到檔案瀏覽器時下載任意檔案,或者在拖放到另一個瀏覽器視窗時,就像正在拖放檔案一樣(儘管可能適用 CORS 限制)。有關實際用例,請參閱 像 Gmail 一樣拖出檔案。