Add basic support for the version of DataTransferItemList.add that takes a File
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 4 Oct 2017 22:28:08 +0000 (22:28 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 4 Oct 2017 22:28:08 +0000 (22:28 +0000)
https://bugs.webkit.org/show_bug.cgi?id=177853
<rdar://problem/34807346>

Reviewed by Ryosuke Niwa.

Source/WebCore:

Adds very basic support for DataTransferItemList.add(File). So far, a File added in this way can only be read
back from the same DataTransfer, during dragstart or copy. This File isn't written to the platform pasteboard
yet, so even dropping or pasting in the same page will not transfer the File, but this brings us closer to
parity with other browsers. See per-method comments for details.

Tests: editing/pasteboard/data-transfer-item-list-add-file-multiple-times.html
       editing/pasteboard/data-transfer-item-list-add-file-on-copy.html
       editing/pasteboard/data-transfer-item-list-add-file-on-drag.html

* dom/DataTransfer.cpp:
(WebCore::DataTransfer::updateFileList):

Recompute the DataTransfer's FileList. This behaves the same way as destroying the FileList altogether and
building it from scratch, but we avoid that approach because the FileList object needs to maintain the same DOM
wrapper after a File-backed item is removed.

(WebCore::DataTransfer::itemListDidAddFile):

Add the newly appended DataTransferItem's File to the DataTransfer's FileList.

(WebCore::DataTransfer::types const):

Return only the "Files" type if there are file-backed items in the DataTransfer's item list.

(WebCore::DataTransfer::updatedFilesForFileList const):
(WebCore::DataTransfer::files const):
* dom/DataTransfer.h:
* dom/DataTransferItem.h:
(WebCore::DataTransferItem::file const):
* dom/DataTransferItemList.cpp:
(WebCore::DataTransferItemList::add):
(WebCore::DataTransferItemList::remove):
(WebCore::DataTransferItemList::clear):

When removing a File, only clear from the DataTransfer's pasteboard if the removed item is not a File (otherwise,
clearing a File that shares the same type as some other item in the pasteboard will erroneously clear that other
item as well). Additionally, call out to the DataTransfer to update the FileList.

* dom/DataTransferItemList.h:
(WebCore::DataTransferItemList::hasItems const):
(WebCore::DataTransferItemList::items const):

Add helpers for directly accessing an item list's items. items() should be used in conjunction with hasItems().
This route is taken to (1) avoid having to copy the vector of Files, and (2) to avoid generating m_items if it
doesn't already exist.

LayoutTests:

Add tests to verify that Files can be added to and removed from the DataTransferItemList, and also read back via
both the item list and the DataTransfer's FileList when copying and dragging. Additionally, adds a test that adds
and removes the same File to the DataTransferItemList multiple times.

* TestExpectations:
* editing/pasteboard/data-transfer-item-list-add-file-multiple-times-expected.txt: Added.
* editing/pasteboard/data-transfer-item-list-add-file-multiple-times.html: Added.
* editing/pasteboard/data-transfer-item-list-add-file-on-copy-expected.txt: Added.
* editing/pasteboard/data-transfer-item-list-add-file-on-copy.html: Added.
* editing/pasteboard/data-transfer-item-list-add-file-on-drag-expected.txt: Added.
* editing/pasteboard/data-transfer-item-list-add-file-on-drag.html: Added.
* platform/ios-simulator-wk1/TestExpectations:
* platform/mac-wk1/TestExpectations:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@222885 268f45cc-cd09-0410-ab3c-d52691b4dbfc

16 files changed:
LayoutTests/ChangeLog
LayoutTests/TestExpectations
LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-multiple-times-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-multiple-times.html [new file with mode: 0644]
LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-copy-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-copy.html [new file with mode: 0644]
LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-drag-expected.txt [new file with mode: 0644]
LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-drag.html [new file with mode: 0644]
LayoutTests/platform/ios-simulator-wk1/TestExpectations
LayoutTests/platform/mac-wk1/TestExpectations
Source/WebCore/ChangeLog
Source/WebCore/dom/DataTransfer.cpp
Source/WebCore/dom/DataTransfer.h
Source/WebCore/dom/DataTransferItem.h
Source/WebCore/dom/DataTransferItemList.cpp
Source/WebCore/dom/DataTransferItemList.h

index 508e3f417240c12649a987e8e2a2a1b99931ccbe..8e3200fda42302ff85cdeab816bdc5f7509e044a 100644 (file)
@@ -1,3 +1,25 @@
+2017-10-04  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Add basic support for the version of DataTransferItemList.add that takes a File
+        https://bugs.webkit.org/show_bug.cgi?id=177853
+        <rdar://problem/34807346>
+
+        Reviewed by Ryosuke Niwa.
+
+        Add tests to verify that Files can be added to and removed from the DataTransferItemList, and also read back via
+        both the item list and the DataTransfer's FileList when copying and dragging. Additionally, adds a test that adds
+        and removes the same File to the DataTransferItemList multiple times.
+
+        * TestExpectations:
+        * editing/pasteboard/data-transfer-item-list-add-file-multiple-times-expected.txt: Added.
+        * editing/pasteboard/data-transfer-item-list-add-file-multiple-times.html: Added.
+        * editing/pasteboard/data-transfer-item-list-add-file-on-copy-expected.txt: Added.
+        * editing/pasteboard/data-transfer-item-list-add-file-on-copy.html: Added.
+        * editing/pasteboard/data-transfer-item-list-add-file-on-drag-expected.txt: Added.
+        * editing/pasteboard/data-transfer-item-list-add-file-on-drag.html: Added.
+        * platform/ios-simulator-wk1/TestExpectations:
+        * platform/mac-wk1/TestExpectations:
+
 2017-10-04  Per Arne Vollan  <pvollan@apple.com>
 
         Mark http/wpt/cache-storage/cache-quota.any.html as flaky on Windows.
index 755a0b95c0c6b0eb452138a52a2600740015ea8e..0c97ad962e367e87a6b43f373eabc705c3a29598 100644 (file)
@@ -74,6 +74,7 @@ editing/pasteboard/data-transfer-get-data-on-drop-plain-text.html [ Skip ]
 editing/pasteboard/data-transfer-get-data-on-drop-rich-text.html [ Skip ]
 editing/pasteboard/data-transfer-get-data-on-drop-url.html [ Skip ]
 editing/pasteboard/drag-end-crash-accessing-item-list.html [ Skip ]
+editing/pasteboard/data-transfer-item-list-add-file-on-drag.html [ Skip ]
 
 # Only iOS supports QuickLook
 quicklook [ Skip ]
diff --git a/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-multiple-times-expected.txt b/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-multiple-times-expected.txt
new file mode 100644 (file)
index 0000000..d801606
--- /dev/null
@@ -0,0 +1,401 @@
+Copy this text!
+To manually test, copy the above text. The output below dumps DataTransfer state following each operation,
+
+described directly above the output text for each step. The DataTransfer state should be consistent with the
+
+operation performed at each step.
+
+
+1. After adding all items
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/uri-list",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        },
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        },
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        },
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        }
+    ]
+}
+
+2. After removing at index 4
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/uri-list",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        },
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        },
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+3. After removing at index 1
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/uri-list",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        },
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+4. After removing at index 3
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/uri-list",
+            "kind": "string",
+            "file": null
+        }
+    ],
+    "files": [
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+5. After clearing items
+{
+    "data": {},
+    "items": [],
+    "files": []
+}
+
+6. After adding two files and some string data again
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        },
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        }
+    ]
+}
+
+7. After removing at index 2
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        },
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+8. After removing at index 2
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        }
+    ],
+    "files": [
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+9. After removing at index 1
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "file.txt",
+                "bytes": 20,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "file.txt",
+            "bytes": 20,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+10. After removing at index 0
+{
+    "data": {},
+    "items": [],
+    "files": []
+}
+removedItem.getAsFile() should be null: null
+
diff --git a/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-multiple-times.html b/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-multiple-times.html
new file mode 100644 (file)
index 0000000..32e11f5
--- /dev/null
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta charset="utf-8">
+<style>
+body, html {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+</style>
+<body>
+    <div style="font-size: 40px;" id="source">Copy this text!</div>
+    <p>To manually test, copy the above text. The output below dumps DataTransfer state following each operation,</p>
+    <p>described directly above the output text for each step. The DataTransfer state should be consistent with the</p>
+    <p>operation performed at each step.</p>
+    <pre style="width: 100%; height: 100%" id="output"></pre>
+</body>
+<script>
+function write(message) {
+    output.textContent += `${message}\n`;
+}
+
+function representationForFile(file) {
+    return file ? {
+        name: file.name,
+        bytes: file.size,
+        type: file.type
+    } : null;
+}
+
+function removeAt(itemList, index) {
+    const removedItem = itemList[index];
+    itemList.remove(index);
+    return removedItem;
+}
+
+function updateOutputText(description, event, itemList, fileList) {
+    const dataInfo = {};
+    for (const type of event.clipboardData.types)
+        dataInfo[type] = event.clipboardData.getData(type);
+    const itemsInfo = []
+    for (const item of itemList) {
+        itemsInfo.push({
+            type: item.type,
+            kind: item.kind,
+            file: representationForFile(item.getAsFile())
+        });
+    }
+    write(`\n${description}\n${JSON.stringify({
+        data: dataInfo,
+        items: itemsInfo,
+        files: Array.from(fileList).map(representationForFile)
+    }, null, "    ")}`);
+}
+
+source.addEventListener("copy", event => {
+    const file = new File([ "This is a text file." ], "file.txt", { type: "text/plain" });
+
+    let itemList = event.clipboardData.items;
+    let fileList = event.clipboardData.files;
+    event.clipboardData.items.add(file);
+    event.clipboardData.items.add(file);
+    event.clipboardData.items.add("plain text string", "text/plain");
+    event.clipboardData.items.add("https://webkit.org", "text/uri-list");
+    event.clipboardData.items.add(file);
+    event.clipboardData.items.add(file);
+    updateOutputText("1. After adding all items", event, itemList, fileList);
+
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    let removedItem = removeAt(itemList, 4);
+    updateOutputText("2. After removing at index 4", event, itemList, fileList);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    removedItem = removeAt(itemList, 1);
+    updateOutputText("3. After removing at index 1", event, itemList, fileList);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    removedItem = removeAt(itemList, 3);
+    updateOutputText("4. After removing at index 3", event, itemList, fileList);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    event.clipboardData.items.clear();
+    updateOutputText("5. After clearing items", event, itemList, fileList);
+
+    event.clipboardData.items.add(file);
+    event.clipboardData.items.add("<strong>some styled text</strong>", "text/html");
+    event.clipboardData.items.add("some plain text", "text/plain");
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    event.clipboardData.items.add(file);
+    updateOutputText("6. After adding two files and some string data again", event, itemList, fileList);
+
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    removedItem = removeAt(itemList, 2);
+    updateOutputText("7. After removing at index 2", event, itemList, fileList);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    removedItem = removeAt(itemList, 2);
+    updateOutputText("8. After removing at index 2", event, itemList, fileList);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    removedItem = removeAt(itemList, 1);
+    updateOutputText("9. After removing at index 1", event, itemList, fileList);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    itemList = event.clipboardData.items;
+    fileList = event.clipboardData.files;
+    removedItem = removeAt(itemList, 0);
+    updateOutputText("10. After removing at index 0", event, itemList, fileList);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    event.preventDefault();
+});
+
+getSelection().setBaseAndExtent(source, 0, source, 1);
+
+if (window.testRunner && window.internals) {
+    internals.settings.setCustomPasteboardDataEnabled(true);
+    testRunner.dumpAsText();
+    document.execCommand("Copy");
+}
+</script>
+</html>
diff --git a/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-copy-expected.txt b/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-copy-expected.txt
new file mode 100644 (file)
index 0000000..ffdf67c
--- /dev/null
@@ -0,0 +1,304 @@
+Copy this text!
+To manually test, copy the above text. The output below dumps DataTransfer state following each operation,
+
+described directly above the output text for each step. The DataTransfer state should be consistent with the
+
+operation performed at each step.
+
+
+1. After adding a string
+{
+    "data": {
+        "text/plain": "hello world"
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        }
+    ],
+    "files": []
+}
+
+2. After adding a file of custom type
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        }
+    ]
+}
+
+3. After adding the first plain text file
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "first.txt",
+                "bytes": 72,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        },
+        {
+            "name": "first.txt",
+            "bytes": 72,
+            "type": "text/plain"
+        }
+    ]
+}
+
+4. After removing the last file
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+5. After adding an HTML string
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        }
+    ]
+}
+
+6. After adding another plain text file
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "second.txt",
+                "bytes": 27,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        },
+        {
+            "name": "second.txt",
+            "bytes": 27,
+            "type": "text/plain"
+        }
+    ]
+}
+
+7. After removing the custom file
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "second.txt",
+                "bytes": 27,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "second.txt",
+            "bytes": 27,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+8. After removing the HTML string
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "second.txt",
+                "bytes": 27,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "second.txt",
+            "bytes": 27,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+9. After removing the second text file
+{
+    "data": {
+        "text/plain": "hello world"
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        }
+    ],
+    "files": []
+}
+removedItem.getAsFile() should be null: null
+
+10. After removing the plain text string
+{
+    "data": {},
+    "items": [],
+    "files": []
+}
+removedItem.getAsFile() should be null: null
+The DataTransfer's FileList should be the same object: true
+
diff --git a/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-copy.html b/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-copy.html
new file mode 100644 (file)
index 0000000..647af86
--- /dev/null
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta charset="utf-8">
+<style>
+body, html {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+</style>
+<body>
+    <div style="font-size: 40px;" id="source">Copy this text!</div>
+    <p>To manually test, copy the above text. The output below dumps DataTransfer state following each operation,</p>
+    <p>described directly above the output text for each step. The DataTransfer state should be consistent with the</p>
+    <p>operation performed at each step.</p>
+    <pre style="height: 100%; width: 100%;" id="output"></pre>
+</body>
+<script>
+function write(message) {
+    output.textContent += `${message}\n`;
+}
+
+function representationForFile(file) {
+    return file ? {
+        name: file.name,
+        bytes: file.size,
+        type: file.type
+    } : null;
+}
+
+function updateOutputText(description, event) {
+    const dataInfo = {};
+    for (const type of event.clipboardData.types)
+        dataInfo[type] = event.clipboardData.getData(type);
+    const itemsInfo = [];
+    for (const item of event.clipboardData.items) {
+        itemsInfo.push({
+            type: item.type,
+            kind: item.kind,
+            file: representationForFile(item.getAsFile())
+        });
+    }
+    write(`\n${description}\n${JSON.stringify({
+        data: dataInfo,
+        items: itemsInfo,
+        files: Array.from(event.clipboardData.files).map(representationForFile)
+    }, null, "    ")}`);
+}
+
+function removeAt(itemList, index) {
+    const removedItem = itemList[index];
+    itemList.remove(index);
+    return removedItem;
+}
+
+source.addEventListener("copy", event => {
+    const fileList = event.clipboardData.files;
+    event.clipboardData.items.add("hello world", "text/plain");
+    updateOutputText("1. After adding a string", event);
+
+    const buffer = new ArrayBuffer(64);
+    const array = new Int8Array(buffer);
+    array.fill(15);
+    event.clipboardData.items.add(new File([ buffer ], "foo", { type: "custom" }));
+    updateOutputText("2. After adding a file of custom type", event);
+
+    event.clipboardData.items.add(new File([
+        new Blob(["This part is from a JavaScript Blob"], { type : "text/plain" }),
+        "This part is just from a plain string"
+    ], "first.txt", { type: "text/plain" }));
+    updateOutputText("3. After adding the first plain text file", event);
+
+    removedItem = removeAt(event.clipboardData.items, 2);
+    updateOutputText("4. After removing the last file", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    event.clipboardData.items.add("<a>goodbye world</a>", "text/html");
+    updateOutputText("5. After adding an HTML string", event);
+
+    event.clipboardData.items.add(new File([ "This is just a plain string" ], "second.txt", { type: "text/plain" }));
+    updateOutputText("6. After adding another plain text file", event);
+
+    removedItem = removeAt(event.clipboardData.items, 1);
+    updateOutputText("7. After removing the custom file", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    removedItem = removeAt(event.clipboardData.items, 1);
+    updateOutputText("8. After removing the HTML string", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    removedItem = removeAt(event.clipboardData.items, 1);
+    updateOutputText("9. After removing the second text file", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    removedItem = removeAt(event.clipboardData.items, 0);
+    updateOutputText("10. After removing the plain text string", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+    write(`The DataTransfer's FileList should be the same object: ${fileList == event.clipboardData.files}`);
+
+    event.preventDefault();
+});
+
+getSelection().setBaseAndExtent(source, 0, source, 1);
+
+if (window.testRunner && window.internals) {
+    internals.settings.setCustomPasteboardDataEnabled(true);
+    testRunner.dumpAsText();
+    document.execCommand("Copy");
+}
+</script>
+</html>
diff --git a/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-drag-expected.txt b/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-drag-expected.txt
new file mode 100644 (file)
index 0000000..7ada654
--- /dev/null
@@ -0,0 +1,304 @@
+Drag me out.
+To manually test, drag the above text. The output below dumps DataTransfer state following each operation,
+
+described directly above the output text for each step. The DataTransfer state should be consistent with the
+
+operation performed at each step.
+
+
+1. After adding a string
+{
+    "data": {
+        "text/plain": "hello world"
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        }
+    ],
+    "files": []
+}
+
+2. After adding a file of custom type
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        }
+    ]
+}
+
+3. After adding the first plain text file
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "first.txt",
+                "bytes": 72,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        },
+        {
+            "name": "first.txt",
+            "bytes": 72,
+            "type": "text/plain"
+        }
+    ]
+}
+
+4. After removing the last file
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+5. After adding an HTML string
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        }
+    ]
+}
+
+6. After adding another plain text file
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "custom",
+            "kind": "file",
+            "file": {
+                "name": "foo",
+                "bytes": 64,
+                "type": "custom"
+            }
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "second.txt",
+                "bytes": 27,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "foo",
+            "bytes": 64,
+            "type": "custom"
+        },
+        {
+            "name": "second.txt",
+            "bytes": 27,
+            "type": "text/plain"
+        }
+    ]
+}
+
+7. After removing the custom file
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/html",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "second.txt",
+                "bytes": 27,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "second.txt",
+            "bytes": 27,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+8. After removing the HTML string
+{
+    "data": {
+        "Files": ""
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        },
+        {
+            "type": "text/plain",
+            "kind": "file",
+            "file": {
+                "name": "second.txt",
+                "bytes": 27,
+                "type": "text/plain"
+            }
+        }
+    ],
+    "files": [
+        {
+            "name": "second.txt",
+            "bytes": 27,
+            "type": "text/plain"
+        }
+    ]
+}
+removedItem.getAsFile() should be null: null
+
+9. After removing the second text file
+{
+    "data": {
+        "text/plain": "hello world"
+    },
+    "items": [
+        {
+            "type": "text/plain",
+            "kind": "string",
+            "file": null
+        }
+    ],
+    "files": []
+}
+removedItem.getAsFile() should be null: null
+
+10. After removing the plain text string
+{
+    "data": {},
+    "items": [],
+    "files": []
+}
+removedItem.getAsFile() should be null: null
+The DataTransfer's FileList should be the same object: true
+
diff --git a/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-drag.html b/LayoutTests/editing/pasteboard/data-transfer-item-list-add-file-on-drag.html
new file mode 100644 (file)
index 0000000..e44908a
--- /dev/null
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta charset="utf-8">
+<style>
+body, html {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+</style>
+<body>
+    <div style="font-size: 40px;" id="source" draggable="true">Drag me out.</div>
+    <p>To manually test, drag the above text. The output below dumps DataTransfer state following each operation,</p>
+    <p>described directly above the output text for each step. The DataTransfer state should be consistent with the</p>
+    <p>operation performed at each step.</p>
+    <pre style="width: 100%; height: 100%" id="output"></pre>
+</body>
+<script>
+function write(message) {
+    output.textContent += `${message}\n`;
+}
+
+function representationForFile(file) {
+    return file ? {
+        name: file.name,
+        bytes: file.size,
+        type: file.type
+    } : null;
+}
+
+function updateOutputText(description, event) {
+    const dataInfo = {};
+    for (const type of event.dataTransfer.types)
+        dataInfo[type] = event.dataTransfer.getData(type);
+    const itemsInfo = []
+    for (const item of event.dataTransfer.items) {
+        itemsInfo.push({
+            type: item.type,
+            kind: item.kind,
+            file: representationForFile(item.getAsFile())
+        });
+    }
+    write(`\n${description}\n${JSON.stringify({
+        data: dataInfo,
+        items: itemsInfo,
+        files: Array.from(event.dataTransfer.files).map(representationForFile)
+    }, null, "    ")}`);
+}
+
+function removeAt(itemList, index) {
+    const removedItem = itemList[index];
+    itemList.remove(index);
+    return removedItem;
+}
+
+output.addEventListener("dragover", event => event.preventDefault());
+output.addEventListener("drop", event => event.preventDefault());
+source.addEventListener("dragstart", event => {
+    const fileList = event.dataTransfer.files;
+    event.dataTransfer.items.add("hello world", "text/plain");
+    updateOutputText("1. After adding a string", event);
+
+    const buffer = new ArrayBuffer(64);
+    const array = new Int8Array(buffer);
+    array.fill(15);
+    event.dataTransfer.items.add(new File([ buffer ], "foo", { type: "custom" }));
+    updateOutputText("2. After adding a file of custom type", event);
+
+    event.dataTransfer.items.add(new File([
+        new Blob(["This part is from a JavaScript Blob"], { type : "text/plain" }),
+        "This part is just from a plain string"
+    ], "first.txt", { type: "text/plain" }));
+    updateOutputText("3. After adding the first plain text file", event);
+
+    removedItem = removeAt(event.dataTransfer.items, 2);
+    updateOutputText("4. After removing the last file", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    event.dataTransfer.items.add("<a>goodbye world</a>", "text/html");
+    updateOutputText("5. After adding an HTML string", event);
+
+    event.dataTransfer.items.add(new File([ "This is just a plain string" ], "second.txt", { type: "text/plain" }));
+    updateOutputText("6. After adding another plain text file", event);
+
+    removedItem = removeAt(event.dataTransfer.items, 1);
+    updateOutputText("7. After removing the custom file", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    removedItem = removeAt(event.dataTransfer.items, 1);
+    updateOutputText("8. After removing the HTML string", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    removedItem = removeAt(event.dataTransfer.items, 1);
+    updateOutputText("9. After removing the second text file", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+
+    removedItem = removeAt(event.dataTransfer.items, 0);
+    updateOutputText("10. After removing the plain text string", event);
+    write(`removedItem.getAsFile() should be null: ${removedItem.getAsFile()}`);
+    write(`The DataTransfer's FileList should be the same object: ${fileList == event.dataTransfer.files}`);
+
+    event.preventDefault();
+});
+
+if (window.testRunner && window.eventSender && window.internals) {
+    internals.settings.setCustomPasteboardDataEnabled(true);
+    testRunner.dumpAsText();
+    eventSender.mouseMoveTo(100, 25);
+    eventSender.mouseDown();
+    eventSender.leapForward(1000);
+    eventSender.mouseMoveTo(100, 400);
+    eventSender.mouseUp();
+}
+</script>
+</html>
index 4f9c44c3301104e6caf437c24de53d6e4fe2dfd0..061e74bfebd3ef49628d6990636e58fe73b8229a 100644 (file)
@@ -11,3 +11,5 @@ editing/pasteboard/data-transfer-get-data-on-paste-custom.html [ Pass ]
 editing/pasteboard/data-transfer-get-data-on-paste-plain-text.html [ Pass ]
 editing/pasteboard/data-transfer-get-data-on-paste-rich-text.html [ Pass ]
 editing/pasteboard/data-transfer-get-data-non-normalized-types.html [ Pass ]
+editing/pasteboard/data-transfer-item-list-add-file-on-copy.html [ Pass ]
+editing/pasteboard/data-transfer-item-list-add-file-multiple-times.html [ Pass ]
index ec49fc48a4e9b0b2a0f4c3cb70e6fd19ae9decdd..ddbb081db73713f56cf1608eb4da0150bca7de38 100644 (file)
@@ -12,6 +12,7 @@ editing/pasteboard/data-transfer-get-data-on-drop-plain-text.html [ Pass ]
 editing/pasteboard/data-transfer-get-data-on-drop-rich-text.html [ Pass ]
 editing/pasteboard/data-transfer-get-data-on-drop-url.html [ Pass ]
 editing/pasteboard/drag-end-crash-accessing-item-list.html [ Pass ]
+editing/pasteboard/data-transfer-item-list-add-file-on-drag.html [ Pass ]
 
 #//////////////////////////////////////////////////////////////////////////////////////////
 # End platform-specific directories.
index 9904bfe0d979c8af653901da440d4c9a74ec8d24..febdc7969f2fe1e9746ec35951b5169d6c011bcc 100644 (file)
@@ -1,3 +1,57 @@
+2017-10-04  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Add basic support for the version of DataTransferItemList.add that takes a File
+        https://bugs.webkit.org/show_bug.cgi?id=177853
+        <rdar://problem/34807346>
+
+        Reviewed by Ryosuke Niwa.
+
+        Adds very basic support for DataTransferItemList.add(File). So far, a File added in this way can only be read
+        back from the same DataTransfer, during dragstart or copy. This File isn't written to the platform pasteboard
+        yet, so even dropping or pasting in the same page will not transfer the File, but this brings us closer to
+        parity with other browsers. See per-method comments for details.
+
+        Tests: editing/pasteboard/data-transfer-item-list-add-file-multiple-times.html
+               editing/pasteboard/data-transfer-item-list-add-file-on-copy.html
+               editing/pasteboard/data-transfer-item-list-add-file-on-drag.html
+
+        * dom/DataTransfer.cpp:
+        (WebCore::DataTransfer::updateFileList):
+
+        Recompute the DataTransfer's FileList. This behaves the same way as destroying the FileList altogether and
+        building it from scratch, but we avoid that approach because the FileList object needs to maintain the same DOM
+        wrapper after a File-backed item is removed.
+
+        (WebCore::DataTransfer::itemListDidAddFile):
+
+        Add the newly appended DataTransferItem's File to the DataTransfer's FileList.
+
+        (WebCore::DataTransfer::types const):
+
+        Return only the "Files" type if there are file-backed items in the DataTransfer's item list.
+
+        (WebCore::DataTransfer::updatedFilesForFileList const):
+        (WebCore::DataTransfer::files const):
+        * dom/DataTransfer.h:
+        * dom/DataTransferItem.h:
+        (WebCore::DataTransferItem::file const):
+        * dom/DataTransferItemList.cpp:
+        (WebCore::DataTransferItemList::add):
+        (WebCore::DataTransferItemList::remove):
+        (WebCore::DataTransferItemList::clear):
+
+        When removing a File, only clear from the DataTransfer's pasteboard if the removed item is not a File (otherwise,
+        clearing a File that shares the same type as some other item in the pasteboard will erroneously clear that other
+        item as well). Additionally, call out to the DataTransfer to update the FileList.
+
+        * dom/DataTransferItemList.h:
+        (WebCore::DataTransferItemList::hasItems const):
+        (WebCore::DataTransferItemList::items const):
+
+        Add helpers for directly accessing an item list's items. items() should be used in conjunction with hasItems().
+        This route is taken to (1) avoid having to copy the vector of Files, and (2) to avoid generating m_items if it
+        doesn't already exist.
+
 2017-10-04  Zalan Bujtas  <zalan@apple.com>
 
         RenderMultiColumnFlow populate/evacuate should not disable layout state.
index addef0f9173c456a81eae74905124f0c2f493fe2..f982d57e774efa7fc0f2060155e5346067c4ab9f 100644 (file)
@@ -28,6 +28,7 @@
 
 #include "CachedImage.h"
 #include "CachedImageClient.h"
+#include "DataTransferItem.h"
 #include "DataTransferItemList.h"
 #include "DragData.h"
 #include "Editor.h"
@@ -161,6 +162,26 @@ void DataTransfer::setData(const String& type, const String& data)
         m_itemList->didSetStringData(normalizedType);
 }
 
+void DataTransfer::updateFileList()
+{
+    // If we're removing an item, then the item list must exist, which implies that the file list must have been initialized already.
+    ASSERT(m_fileList);
+    ASSERT(canWriteData());
+
+    m_fileList->m_files = filesFromPasteboardAndItemList();
+}
+
+void DataTransfer::didAddFileToItemList()
+{
+    ASSERT(canWriteData());
+    if (!m_fileList)
+        return;
+
+    auto& newItem = m_itemList->items().last();
+    ASSERT(newItem->isFile());
+    m_fileList->append(*newItem->file());
+}
+
 DataTransferItemList& DataTransfer::items()
 {
     if (!m_itemList)
@@ -181,6 +202,9 @@ Vector<String> DataTransfer::types() const
         return types;
     }
 
+    if (m_itemList && m_itemList->hasItems() && m_itemList->items().findMatching([] (const auto& item) { return item->isFile(); }) != notFound)
+        return { "Files" };
+
     if (m_pasteboard->containsFiles()) {
         ASSERT(!m_pasteboard->typesSafeForBindings().contains("Files"));
         return { "Files" };
@@ -191,6 +215,31 @@ Vector<String> DataTransfer::types() const
     return types;
 }
 
+Vector<Ref<File>> DataTransfer::filesFromPasteboardAndItemList() const
+{
+    bool addedFilesFromPasteboard = false;
+    Vector<Ref<File>> files;
+    if (!forDrag() || forFileDrag()) {
+        WebCorePasteboardFileReader reader;
+        m_pasteboard->read(reader);
+        files = WTFMove(reader.files);
+        addedFilesFromPasteboard = !files.isEmpty();
+    }
+
+    bool itemListContainsItems = false;
+    if (m_itemList && m_itemList->hasItems()) {
+        for (auto& item : m_itemList->items()) {
+            if (auto file = item->file()) {
+                files.append(*file);
+            }
+        }
+        itemListContainsItems = true;
+    }
+
+    ASSERT(!itemListContainsItems || !addedFilesFromPasteboard);
+    return files;
+}
+
 FileList& DataTransfer::files() const
 {
     if (!canReadData()) {
@@ -201,19 +250,9 @@ FileList& DataTransfer::files() const
         return *m_fileList;
     }
 
-    if (forDrag() && !forFileDrag()) {
-        if (m_fileList)
-            ASSERT(m_fileList->isEmpty());
-        else
-            m_fileList = FileList::create();
-        return *m_fileList;
-    }
+    if (!m_fileList)
+        m_fileList = FileList::create(filesFromPasteboardAndItemList());
 
-    if (!m_fileList) {
-        WebCorePasteboardFileReader reader;
-        m_pasteboard->read(reader);
-        m_fileList = FileList::create(WTFMove(reader.files));
-    }
     return *m_fileList;
 }
 
index 09d4dff32b11fb8fef2e07bc8e15b3c405e2f16f..eda0edeb77ee61fb1f13cb2bf891ef2ba150f0d4 100644 (file)
@@ -36,6 +36,7 @@ class DragData;
 class DragImageLoader;
 class Element;
 class FileList;
+class File;
 class Pasteboard;
 
 class DataTransfer : public RefCounted<DataTransfer> {
@@ -99,6 +100,9 @@ public:
     bool hasDragImage() const;
 #endif
 
+    void didAddFileToItemList();
+    void updateFileList();
+
 private:
     enum class Type { CopyAndPaste, DragAndDropData, DragAndDropFiles, InputEvent };
     DataTransfer(StoreMode, std::unique_ptr<Pasteboard>, Type = Type::CopyAndPaste);
@@ -111,6 +115,8 @@ private:
     bool forFileDrag() const { return false; }
 #endif
 
+    Vector<Ref<File>> filesFromPasteboardAndItemList() const;
+
     StoreMode m_storeMode;
     std::unique_ptr<Pasteboard> m_pasteboard;
     std::unique_ptr<DataTransferItemList> m_itemList;
index 9ac11e7cb335f731003fa165a18624fe7faee256..9685529187e66d439bbf502a7076d06b488c9746 100644 (file)
@@ -54,6 +54,7 @@ public:
 
     ~DataTransferItem();
 
+    RefPtr<File> file() const { return m_file; }
     void clearListAndPutIntoDisabledMode();
 
     bool isFile() const { return m_file; }
index 59704b4b560c8d63d8d1be7af86a5282ed2ab283..690a2ab0795e29bbacfa9eb697ac431e09632eaa 100644 (file)
@@ -84,9 +84,14 @@ ExceptionOr<RefPtr<DataTransferItem>> DataTransferItemList::add(const String& da
     return RefPtr<DataTransferItem> { m_items->last().copyRef() };
 }
 
-RefPtr<DataTransferItem> DataTransferItemList::add(Ref<File>&&)
+RefPtr<DataTransferItem> DataTransferItemList::add(Ref<File>&& file)
 {
-    return nullptr;
+    if (!m_dataTransfer.canWriteData())
+        return nullptr;
+
+    ensureItems().append(DataTransferItem::create(m_weakPtrFactory.createWeakPtr(*this), file->type(), file.copyRef()));
+    m_dataTransfer.didAddFileToItemList();
+    return RefPtr<DataTransferItem> { m_items->last().ptr() };
 }
 
 ExceptionOr<void> DataTransferItemList::remove(unsigned index)
@@ -98,13 +103,16 @@ ExceptionOr<void> DataTransferItemList::remove(unsigned index)
     if (items.size() <= index)
         return Exception { IndexSizeError }; // Matches Gecko. See https://github.com/whatwg/html/issues/2925
 
-    // FIXME: Handle the removal of files once we added the support for writing a File.
-    ASSERT(!items[index]->isFile());
-
+    // Since file-backed DataTransferItems are not actually written to the pasteboard yet, we don't need to remove any
+    // temporary files. When we support writing file-backed DataTransferItems to the platform pasteboard, we will need
+    // to clean up here.
     auto& removedItem = items[index].get();
-    m_dataTransfer.pasteboard().clear(removedItem.type());
+    if (!removedItem.isFile())
+        m_dataTransfer.pasteboard().clear(removedItem.type());
     removedItem.clearListAndPutIntoDisabledMode();
     items.remove(index);
+    if (removedItem.isFile())
+        m_dataTransfer.updateFileList();
 
     return { };
 }
@@ -112,11 +120,17 @@ ExceptionOr<void> DataTransferItemList::remove(unsigned index)
 void DataTransferItemList::clear()
 {
     m_dataTransfer.pasteboard().clear();
+    bool removedItemContainingFile = false;
     if (m_items) {
-        for (auto& item : *m_items)
+        for (auto& item : *m_items) {
+            removedItemContainingFile |= item->isFile();
             item->clearListAndPutIntoDisabledMode();
+        }
         m_items->clear();
     }
+
+    if (removedItemContainingFile)
+        m_dataTransfer.updateFileList();
 }
 
 Vector<Ref<DataTransferItem>>& DataTransferItemList::ensureItems() const
index fce694741c2d6451d68f15d9329d58f5b07a89f0..13120e54ad3680ae66983cffe2cca2d44bb67b8a 100644 (file)
@@ -65,6 +65,12 @@ public:
 
     void didClearStringData(const String& type);
     void didSetStringData(const String& type);
+    bool hasItems() const { return m_items.has_value(); }
+    const Vector<Ref<DataTransferItem>>& items() const
+    {
+        ASSERT(m_items);
+        return *m_items;
+    }
 
 private:
     Vector<Ref<DataTransferItem>>& ensureItems() const;