/src/serenity/Userland/Libraries/LibWeb/HTML/DataTransfer.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Enumerate.h> |
8 | | #include <AK/Find.h> |
9 | | #include <LibJS/Runtime/Realm.h> |
10 | | #include <LibWeb/Bindings/DataTransferPrototype.h> |
11 | | #include <LibWeb/Bindings/Intrinsics.h> |
12 | | #include <LibWeb/FileAPI/Blob.h> |
13 | | #include <LibWeb/FileAPI/File.h> |
14 | | #include <LibWeb/FileAPI/FileList.h> |
15 | | #include <LibWeb/HTML/DataTransfer.h> |
16 | | #include <LibWeb/HTML/DataTransferItem.h> |
17 | | #include <LibWeb/HTML/DataTransferItemList.h> |
18 | | #include <LibWeb/Infra/Strings.h> |
19 | | |
20 | | namespace Web::HTML { |
21 | | |
22 | | JS_DEFINE_ALLOCATOR(DataTransfer); |
23 | | |
24 | | namespace DataTransferEffect { |
25 | | |
26 | | #define __ENUMERATE_DATA_TRANSFER_EFFECT(name) FlyString name = #name##_fly_string; |
27 | | ENUMERATE_DATA_TRANSFER_EFFECTS |
28 | | #undef __ENUMERATE_DATA_TRANSFER_EFFECT |
29 | | |
30 | | } |
31 | | |
32 | | JS::NonnullGCPtr<DataTransfer> DataTransfer::create(JS::Realm& realm, NonnullRefPtr<DragDataStore> drag_data_store) |
33 | 0 | { |
34 | 0 | return realm.heap().allocate<DataTransfer>(realm, realm, move(drag_data_store)); |
35 | 0 | } |
36 | | |
37 | | // https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer |
38 | | JS::NonnullGCPtr<DataTransfer> DataTransfer::construct_impl(JS::Realm& realm) |
39 | 0 | { |
40 | | // 1. Set the drag data store's item list to be an empty list. |
41 | 0 | auto drag_data_store = DragDataStore::create(); |
42 | | |
43 | | // 2. Set the drag data store's mode to read/write mode. |
44 | 0 | drag_data_store->set_mode(DragDataStore::Mode::ReadWrite); |
45 | | |
46 | | // 3. Set the dropEffect and effectAllowed to "none". |
47 | | // NOTE: This is done by the default-initializers. |
48 | |
|
49 | 0 | return realm.heap().allocate<DataTransfer>(realm, realm, move(drag_data_store)); |
50 | 0 | } |
51 | | |
52 | | DataTransfer::DataTransfer(JS::Realm& realm, NonnullRefPtr<DragDataStore> drag_data_store) |
53 | 0 | : PlatformObject(realm) |
54 | 0 | , m_associated_drag_data_store(move(drag_data_store)) |
55 | 0 | { |
56 | 0 | for (auto const& [i, item] : enumerate(m_associated_drag_data_store->item_list())) { |
57 | 0 | auto data_transfer_item = DataTransferItem::create(realm, *this, i); |
58 | 0 | m_item_list.append(data_transfer_item); |
59 | 0 | } |
60 | |
|
61 | 0 | update_data_transfer_types_list(); |
62 | 0 | } |
63 | | |
64 | 0 | DataTransfer::~DataTransfer() = default; |
65 | | |
66 | | void DataTransfer::initialize(JS::Realm& realm) |
67 | 0 | { |
68 | 0 | Base::initialize(realm); |
69 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(DataTransfer); |
70 | 0 | } |
71 | | |
72 | | void DataTransfer::visit_edges(JS::Cell::Visitor& visitor) |
73 | 0 | { |
74 | 0 | Base::visit_edges(visitor); |
75 | 0 | visitor.visit(m_items); |
76 | 0 | visitor.visit(m_item_list); |
77 | 0 | } |
78 | | |
79 | | void DataTransfer::set_drop_effect(String const& drop_effect) |
80 | 0 | { |
81 | 0 | set_drop_effect(FlyString { drop_effect }); |
82 | 0 | } |
83 | | |
84 | | void DataTransfer::set_drop_effect(FlyString drop_effect) |
85 | 0 | { |
86 | 0 | using namespace DataTransferEffect; |
87 | | |
88 | | // On setting, if the new value is one of "none", "copy", "link", or "move", then the attribute's current value must |
89 | | // be set to the new value. Other values must be ignored. |
90 | 0 | if (drop_effect.is_one_of(none, copy, link, move)) |
91 | 0 | m_drop_effect = AK::move(drop_effect); |
92 | 0 | } |
93 | | |
94 | | void DataTransfer::set_effect_allowed(String const& effect_allowed) |
95 | 0 | { |
96 | 0 | set_effect_allowed(FlyString { effect_allowed }); |
97 | 0 | } |
98 | | |
99 | | void DataTransfer::set_effect_allowed(FlyString effect_allowed) |
100 | 0 | { |
101 | | // On setting, if drag data store's mode is the read/write mode and the new value is one of "none", "copy", "copyLink", |
102 | | // "copyMove", "link", "linkMove", "move", "all", or "uninitialized", then the attribute's current value must be set |
103 | | // to the new value. Otherwise, it must be left unchanged. |
104 | 0 | if (m_associated_drag_data_store && m_associated_drag_data_store->mode() == DragDataStore::Mode::ReadWrite) |
105 | 0 | set_effect_allowed_internal(move(effect_allowed)); |
106 | 0 | } |
107 | | |
108 | | void DataTransfer::set_effect_allowed_internal(FlyString effect_allowed) |
109 | 0 | { |
110 | | // AD-HOC: We need to be able to set the effectAllowed attribute internally regardless of the state of the drag data store. |
111 | 0 | using namespace DataTransferEffect; |
112 | |
|
113 | 0 | if (effect_allowed.is_one_of(none, copy, copyLink, copyMove, link, linkMove, move, all, uninitialized)) |
114 | 0 | m_effect_allowed = AK::move(effect_allowed); |
115 | 0 | } |
116 | | |
117 | | // https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-items |
118 | | JS::NonnullGCPtr<DataTransferItemList> DataTransfer::items() |
119 | 0 | { |
120 | | // The items attribute must return a DataTransferItemList object associated with the DataTransfer object. |
121 | 0 | if (!m_items) |
122 | 0 | m_items = DataTransferItemList::create(realm(), *this); |
123 | 0 | return *m_items; |
124 | 0 | } |
125 | | |
126 | | // https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-types |
127 | | ReadonlySpan<String> DataTransfer::types() const |
128 | 0 | { |
129 | | // The types attribute must return this DataTransfer object's types array. |
130 | 0 | return m_types; |
131 | 0 | } |
132 | | |
133 | | // https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-getdata |
134 | | String DataTransfer::get_data(String const& format_argument) const |
135 | 0 | { |
136 | | // 1. If the DataTransfer object is no longer associated with a drag data store, then return the empty string. |
137 | 0 | if (!m_associated_drag_data_store) |
138 | 0 | return {}; |
139 | | |
140 | | // 2. If the drag data store's mode is the protected mode, then return the empty string. |
141 | 0 | if (m_associated_drag_data_store->mode() == DragDataStore::Mode::Protected) |
142 | 0 | return {}; |
143 | | |
144 | | // 3. Let format be the first argument, converted to ASCII lowercase. |
145 | 0 | auto format = format_argument.to_ascii_lowercase(); |
146 | | |
147 | | // 4. Let convert-to-URL be false. |
148 | 0 | [[maybe_unused]] bool convert_to_url = false; |
149 | | |
150 | | // 5. If format equals "text", change it to "text/plain". |
151 | 0 | if (format == "text"sv) { |
152 | 0 | format = "text/plain"_string; |
153 | 0 | } |
154 | | |
155 | | // 6. If format equals "url", change it to "text/uri-list" and set convert-to-URL to true. |
156 | 0 | else if (format == "url"sv) { |
157 | 0 | format = "text/uri-list"_string; |
158 | 0 | convert_to_url = true; |
159 | 0 | } |
160 | | |
161 | | // 7. If there is no item in the drag data store item list whose kind is text and whose type string is equal to |
162 | | // format, return the empty string. |
163 | 0 | auto item_list = m_associated_drag_data_store->item_list(); |
164 | |
|
165 | 0 | auto it = find_if(item_list.begin(), item_list.end(), [&](auto const& item) { |
166 | 0 | return item.kind == DragDataStoreItem::Kind::Text && item.type_string == format; |
167 | 0 | }); |
168 | |
|
169 | 0 | if (it == item_list.end()) |
170 | 0 | return {}; |
171 | | |
172 | | // 8. Let result be the data of the item in the drag data store item list whose kind is Plain Unicode string and |
173 | | // whose type string is equal to format. |
174 | 0 | auto const& result = it->data; |
175 | | |
176 | | // FIXME: 9. If convert-to-URL is true, then parse result as appropriate for text/uri-list data, and then set result to |
177 | | // the first URL from the list, if any, or the empty string otherwise. |
178 | | |
179 | | // 10. Return result. |
180 | 0 | return MUST(String::from_utf8(result)); |
181 | 0 | } |
182 | | |
183 | | // https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-files |
184 | | JS::NonnullGCPtr<FileAPI::FileList> DataTransfer::files() const |
185 | 0 | { |
186 | 0 | auto& realm = this->realm(); |
187 | | |
188 | | // 1. Start with an empty list L. |
189 | 0 | auto files = FileAPI::FileList::create(realm); |
190 | | |
191 | | // 2. If the DataTransfer object is no longer associated with a drag data store, the FileList is empty. Return |
192 | | // the empty list L. |
193 | 0 | if (!m_associated_drag_data_store) |
194 | 0 | return files; |
195 | | |
196 | | // 3. If the drag data store's mode is the protected mode, return the empty list L. |
197 | 0 | if (m_associated_drag_data_store->mode() == DragDataStore::Mode::Protected) |
198 | 0 | return files; |
199 | | |
200 | | // 4. For each item in the drag data store item list whose kind is File, add the item's data (the file, in |
201 | | // particular its name and contents, as well as its type) to the list L. |
202 | 0 | for (auto const& item : m_associated_drag_data_store->item_list()) { |
203 | 0 | if (item.kind != DragDataStoreItem::Kind::File) |
204 | 0 | continue; |
205 | | |
206 | 0 | auto blob = FileAPI::Blob::create(realm, item.data, item.type_string); |
207 | | |
208 | | // FIXME: The FileAPI should use ByteString for file names. |
209 | 0 | auto file_name = MUST(String::from_byte_string(item.file_name)); |
210 | | |
211 | | // FIXME: Fill in other fields (e.g. last_modified). |
212 | 0 | FileAPI::FilePropertyBag options {}; |
213 | 0 | options.type = item.type_string; |
214 | |
|
215 | 0 | auto file = MUST(FileAPI::File::create(realm, { JS::make_handle(blob) }, file_name, move(options))); |
216 | 0 | files->add_file(file); |
217 | 0 | } |
218 | | |
219 | | // 5. The files found by these steps are those in the list L. |
220 | 0 | return files; |
221 | 0 | } |
222 | | |
223 | | Optional<DragDataStore::Mode> DataTransfer::mode() const |
224 | 0 | { |
225 | 0 | if (m_associated_drag_data_store) |
226 | 0 | return m_associated_drag_data_store->mode(); |
227 | 0 | return {}; |
228 | 0 | } |
229 | | |
230 | | void DataTransfer::disassociate_with_drag_data_store() |
231 | 0 | { |
232 | 0 | m_associated_drag_data_store.clear(); |
233 | 0 | update_data_transfer_types_list(); |
234 | 0 | } |
235 | | |
236 | | JS::NonnullGCPtr<DataTransferItem> DataTransfer::add_item(DragDataStoreItem item) |
237 | 0 | { |
238 | 0 | auto& realm = this->realm(); |
239 | |
|
240 | 0 | VERIFY(m_associated_drag_data_store); |
241 | 0 | m_associated_drag_data_store->add_item(move(item)); |
242 | |
|
243 | 0 | auto data_transfer_item = DataTransferItem::create(realm, *this, m_associated_drag_data_store->size() - 1); |
244 | 0 | m_item_list.append(data_transfer_item); |
245 | |
|
246 | 0 | update_data_transfer_types_list(); |
247 | |
|
248 | 0 | return data_transfer_item; |
249 | 0 | } |
250 | | |
251 | | bool DataTransfer::contains_item_with_type(DragDataStoreItem::Kind kind, String const& type) const |
252 | 0 | { |
253 | 0 | VERIFY(m_associated_drag_data_store); |
254 | | |
255 | 0 | for (auto const& item : m_associated_drag_data_store->item_list()) { |
256 | 0 | if (item.kind == kind && item.type_string.equals_ignoring_ascii_case(type)) |
257 | 0 | return true; |
258 | 0 | } |
259 | | |
260 | 0 | return false; |
261 | 0 | } |
262 | | |
263 | | JS::NonnullGCPtr<DataTransferItem> DataTransfer::item(size_t index) const |
264 | 0 | { |
265 | 0 | VERIFY(index < m_item_list.size()); |
266 | 0 | return m_item_list[index]; |
267 | 0 | } |
268 | | |
269 | | DragDataStoreItem const& DataTransfer::drag_data(size_t index) const |
270 | 0 | { |
271 | 0 | VERIFY(m_associated_drag_data_store); |
272 | 0 | VERIFY(index < m_item_list.size()); |
273 | | |
274 | 0 | return m_associated_drag_data_store->item_list()[index]; |
275 | 0 | } |
276 | | |
277 | | size_t DataTransfer::length() const |
278 | 0 | { |
279 | 0 | if (m_associated_drag_data_store) |
280 | 0 | return m_associated_drag_data_store->size(); |
281 | 0 | return 0; |
282 | 0 | } |
283 | | |
284 | | // https://html.spec.whatwg.org/multipage/dnd.html#concept-datatransfer-types |
285 | | void DataTransfer::update_data_transfer_types_list() |
286 | 0 | { |
287 | | // 1. Let L be an empty sequence. |
288 | 0 | Vector<String> types; |
289 | | |
290 | | // 2. If the DataTransfer object is still associated with a drag data store, then: |
291 | 0 | if (m_associated_drag_data_store) { |
292 | 0 | bool contains_file = false; |
293 | | |
294 | | // 1. For each item in the DataTransfer object's drag data store item list whose kind is text, add an entry to L |
295 | | // consisting of the item's type string. |
296 | 0 | for (auto const& item : m_associated_drag_data_store->item_list()) { |
297 | 0 | switch (item.kind) { |
298 | 0 | case DragDataStoreItem::Kind::Text: |
299 | 0 | types.append(item.type_string); |
300 | 0 | break; |
301 | 0 | case DragDataStoreItem::Kind::File: |
302 | 0 | contains_file = true; |
303 | 0 | break; |
304 | 0 | } |
305 | 0 | } |
306 | | |
307 | | // 2. If there are any items in the DataTransfer object's drag data store item list whose kind is File, then add |
308 | | // an entry to L consisting of the string "Files". (This value can be distinguished from the other values |
309 | | // because it is not lowercase.) |
310 | 0 | if (contains_file) |
311 | 0 | types.append("Files"_string); |
312 | 0 | } |
313 | | |
314 | | // 3. Set the DataTransfer object's types array to the result of creating a frozen array from L. |
315 | 0 | m_types = move(types); |
316 | 0 | } |
317 | | } |