/src/serenity/Userland/Libraries/LibWeb/HTML/HTMLDialogElement.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2020, the SerenityOS developers. |
3 | | * Copyright (c) 2024, Sam Atkins <sam@ladybird.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <LibJS/Runtime/NativeFunction.h> |
9 | | #include <LibWeb/Bindings/HTMLDialogElementPrototype.h> |
10 | | #include <LibWeb/Bindings/Intrinsics.h> |
11 | | #include <LibWeb/DOM/Document.h> |
12 | | #include <LibWeb/DOM/Event.h> |
13 | | #include <LibWeb/DOM/IDLEventListener.h> |
14 | | #include <LibWeb/HTML/CloseWatcher.h> |
15 | | #include <LibWeb/HTML/Focus.h> |
16 | | #include <LibWeb/HTML/HTMLDialogElement.h> |
17 | | #include <LibWeb/HTML/ToggleEvent.h> |
18 | | |
19 | | namespace Web::HTML { |
20 | | |
21 | | JS_DEFINE_ALLOCATOR(HTMLDialogElement); |
22 | | |
23 | | HTMLDialogElement::HTMLDialogElement(DOM::Document& document, DOM::QualifiedName qualified_name) |
24 | 0 | : HTMLElement(document, move(qualified_name)) |
25 | 0 | { |
26 | 0 | } |
27 | | |
28 | 0 | HTMLDialogElement::~HTMLDialogElement() = default; |
29 | | |
30 | | void HTMLDialogElement::initialize(JS::Realm& realm) |
31 | 0 | { |
32 | 0 | Base::initialize(realm); |
33 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLDialogElement); |
34 | 0 | } |
35 | | |
36 | | void HTMLDialogElement::visit_edges(JS::Cell::Visitor& visitor) |
37 | 0 | { |
38 | 0 | Base::visit_edges(visitor); |
39 | |
|
40 | 0 | visitor.visit(m_close_watcher); |
41 | 0 | } |
42 | | |
43 | | void HTMLDialogElement::removed_from(Node* old_parent) |
44 | 0 | { |
45 | 0 | HTMLElement::removed_from(old_parent); |
46 | | |
47 | | // 1. If removedNode's close watcher is not null, then: |
48 | 0 | if (m_close_watcher) { |
49 | | // 1.1. Destroy removedNode's close watcher. |
50 | 0 | m_close_watcher->destroy(); |
51 | | // 1.2. Set removedNode's close watcher to null. |
52 | 0 | m_close_watcher = nullptr; |
53 | 0 | } |
54 | | |
55 | | // 2. If removedNode's node document's top layer contains removedNode, then remove an element from the top layer |
56 | | // immediately given removedNode. |
57 | 0 | if (document().top_layer_elements().contains(*this)) |
58 | 0 | document().remove_an_element_from_the_top_layer_immediately(*this); |
59 | 0 | } |
60 | | |
61 | | // https://html.spec.whatwg.org/multipage/interactive-elements.html#queue-a-dialog-toggle-event-task |
62 | | void HTMLDialogElement::queue_a_dialog_toggle_event_task(AK::String old_state, AK::String new_state) |
63 | 0 | { |
64 | | // 1. If element's dialog toggle task tracker is not null, then: |
65 | 0 | if (m_dialog_toggle_task_tracker.has_value()) { |
66 | | // 1. Set oldState to element's dialog toggle task tracker's old state. |
67 | 0 | old_state = m_dialog_toggle_task_tracker->old_state; |
68 | | |
69 | | // 2. Remove element's dialog toggle task tracker's task from its task queue. |
70 | 0 | HTML::main_thread_event_loop().task_queue().remove_tasks_matching([&](auto const& task) { |
71 | 0 | return task.id() == m_dialog_toggle_task_tracker->task_id; |
72 | 0 | }); |
73 | | |
74 | | // 3. Set element's dialog toggle task tracker to null. |
75 | 0 | m_dialog_toggle_task_tracker = {}; |
76 | 0 | } |
77 | | |
78 | | // 2. Queue an element task given the DOM manipulation task source and element to run the following steps: |
79 | 0 | auto task_id = queue_an_element_task(Task::Source::DOMManipulation, [this, old_state, new_state = move(new_state)]() { |
80 | | // 1. Fire an event named toggle at element, using ToggleEvent, with the oldState attribute initialized to |
81 | | // oldState and the newState attribute initialized to newState. |
82 | 0 | ToggleEventInit event_init {}; |
83 | 0 | event_init.old_state = move(old_state); |
84 | 0 | event_init.new_state = move(new_state); |
85 | |
|
86 | 0 | dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::toggle, move(event_init))); |
87 | | |
88 | | // 2. Set element's dialog toggle task tracker to null. |
89 | 0 | m_dialog_toggle_task_tracker = {}; |
90 | 0 | }); |
91 | | |
92 | | // 3. Set element's dialog toggle task tracker to a struct with task set to the just-queued task and old state set to oldState. |
93 | 0 | m_dialog_toggle_task_tracker = ToggleTaskTracker { |
94 | 0 | .task_id = task_id, |
95 | 0 | .old_state = move(old_state), |
96 | 0 | }; |
97 | 0 | } |
98 | | |
99 | | // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-show |
100 | | WebIDL::ExceptionOr<void> HTMLDialogElement::show() |
101 | 0 | { |
102 | | // 1. If this has an open attribute and the is modal flag of this is false, then return. |
103 | 0 | if (has_attribute(AttributeNames::open) && !m_is_modal) |
104 | 0 | return {}; |
105 | | |
106 | | // 2. If this has an open attribute, then throw an "InvalidStateError" DOMException. |
107 | 0 | if (has_attribute(AttributeNames::open)) |
108 | 0 | return WebIDL::InvalidStateError::create(realm(), "Dialog already open"_string); |
109 | | |
110 | | // 3. If the result of firing an event named beforetoggle, using ToggleEvent, |
111 | | // with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", |
112 | | // and the newState attribute initialized to "open" at this is false, then return. |
113 | 0 | ToggleEventInit event_init {}; |
114 | 0 | event_init.cancelable = true; |
115 | 0 | event_init.old_state = "closed"_string; |
116 | 0 | event_init.new_state = "open"_string; |
117 | |
|
118 | 0 | auto beforetoggle_result = dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init))); |
119 | 0 | if (!beforetoggle_result) |
120 | 0 | return {}; |
121 | | |
122 | | // 4. If this has an open attribute, then return. |
123 | 0 | if (has_attribute(AttributeNames::open)) |
124 | 0 | return {}; |
125 | | |
126 | | // 5. Queue a dialog toggle event task given subject, "closed", and "open". |
127 | 0 | queue_a_dialog_toggle_event_task("closed"_string, "open"_string); |
128 | | |
129 | | // 6. Add an open attribute to this, whose value is the empty string. |
130 | 0 | TRY(set_attribute(AttributeNames::open, {})); |
131 | | |
132 | | // FIXME: 7. Set this's previously focused element to the focused element. |
133 | | |
134 | | // FIXME: 8. Let hideUntil be the result of running topmost popover ancestor given this, null, and false. |
135 | | |
136 | | // FIXME: 9. If hideUntil is null, then set hideUntil to this's node document. |
137 | | |
138 | | // FIXME: 10. Run hide all popovers given this's node document. |
139 | | |
140 | | // 11. Run the dialog focusing steps given this. |
141 | 0 | run_dialog_focusing_steps(); |
142 | |
|
143 | 0 | return {}; |
144 | 0 | } |
145 | | |
146 | | // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-showmodal |
147 | | WebIDL::ExceptionOr<void> HTMLDialogElement::show_modal() |
148 | 0 | { |
149 | | // 1. If this has an open attribute and the is modal flag of this is true, then return. |
150 | 0 | if (has_attribute(AttributeNames::open) && m_is_modal) |
151 | 0 | return {}; |
152 | | |
153 | | // 2. If this has an open attribute, then throw an "InvalidStateError" DOMException. |
154 | 0 | if (has_attribute(AttributeNames::open)) |
155 | 0 | return WebIDL::InvalidStateError::create(realm(), "Dialog already open"_string); |
156 | | |
157 | | // 3. If this's node document is not fully active, then throw an "InvalidStateError" DOMException. |
158 | 0 | if (!document().is_fully_active()) |
159 | 0 | return WebIDL::InvalidStateError::create(realm(), "Document is not fully active"_string); |
160 | | |
161 | | // 4. If this is not connected, then throw an "InvalidStateError" DOMException. |
162 | 0 | if (!is_connected()) |
163 | 0 | return WebIDL::InvalidStateError::create(realm(), "Dialog not connected"_string); |
164 | | |
165 | | // FIXME: 5. If this is in the popover showing state, then throw an "InvalidStateError" DOMException. |
166 | | |
167 | | // 6. If the result of firing an event named beforetoggle, using ToggleEvent, |
168 | | // with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", |
169 | | // and the newState attribute initialized to "open" at this is false, then return. |
170 | 0 | ToggleEventInit event_init {}; |
171 | 0 | event_init.cancelable = true; |
172 | 0 | event_init.old_state = "closed"_string; |
173 | 0 | event_init.new_state = "open"_string; |
174 | |
|
175 | 0 | auto beforetoggle_result = dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init))); |
176 | 0 | if (!beforetoggle_result) |
177 | 0 | return {}; |
178 | | |
179 | | // 7. If this has an open attribute, then return. |
180 | 0 | if (has_attribute(AttributeNames::open)) |
181 | 0 | return {}; |
182 | | |
183 | | // 8. If this is not connected, then return. |
184 | 0 | if (!is_connected()) |
185 | 0 | return {}; |
186 | | |
187 | | // FIXME: 9. If this is in the popover showing state, then return. |
188 | | |
189 | | // 10. Queue a dialog toggle event task given subject, "closed", and "open". |
190 | 0 | queue_a_dialog_toggle_event_task("closed"_string, "open"_string); |
191 | | |
192 | | // 11. Add an open attribute to this, whose value is the empty string. |
193 | 0 | TRY(set_attribute(AttributeNames::open, {})); |
194 | | |
195 | | // 12. Set the is modal flag of this to true. |
196 | 0 | m_is_modal = true; |
197 | | |
198 | | // FIXME: 13. Let this's node document be blocked by the modal dialog this. |
199 | | |
200 | | // 14. If this's node document's top layer does not already contain this, then add an element to the top layer given this. |
201 | 0 | if (!document().top_layer_elements().contains(*this)) |
202 | 0 | document().add_an_element_to_the_top_layer(*this); |
203 | | |
204 | | // 15. Set this's close watcher to the result of establishing a close watcher given this's relevant global object, with: |
205 | 0 | m_close_watcher = CloseWatcher::establish(*document().window()); |
206 | | // - cancelAction given canPreventClose being to return the result of firing an event named cancel at this, with the cancelable attribute initialized to canPreventClose. |
207 | 0 | auto cancel_callback_function = JS::NativeFunction::create( |
208 | 0 | realm(), [this](JS::VM& vm) { |
209 | 0 | auto& event = verify_cast<DOM::Event>(vm.argument(0).as_object()); |
210 | 0 | bool can_prevent_close = event.cancelable(); |
211 | 0 | auto should_continue = dispatch_event(DOM::Event::create(realm(), HTML::EventNames::cancel, { .cancelable = can_prevent_close })); |
212 | 0 | if (!should_continue) |
213 | 0 | event.prevent_default(); |
214 | 0 | return JS::js_undefined(); |
215 | 0 | }, |
216 | 0 | 0, "", &realm()); |
217 | 0 | auto cancel_callback = realm().heap().allocate_without_realm<WebIDL::CallbackType>(*cancel_callback_function, Bindings::host_defined_environment_settings_object(realm())); |
218 | 0 | m_close_watcher->add_event_listener_without_options(HTML::EventNames::cancel, DOM::IDLEventListener::create(realm(), cancel_callback)); |
219 | | // - closeAction being to close the dialog given this and null. |
220 | 0 | auto close_callback_function = JS::NativeFunction::create( |
221 | 0 | realm(), [this](JS::VM&) { |
222 | 0 | close_the_dialog({}); |
223 | |
|
224 | 0 | return JS::js_undefined(); |
225 | 0 | }, |
226 | 0 | 0, "", &realm()); |
227 | 0 | auto close_callback = realm().heap().allocate_without_realm<WebIDL::CallbackType>(*close_callback_function, Bindings::host_defined_environment_settings_object(realm())); |
228 | 0 | m_close_watcher->add_event_listener_without_options(HTML::EventNames::close, DOM::IDLEventListener::create(realm(), close_callback)); |
229 | | |
230 | | // FIXME: 16. Set this's previously focused element to the focused element. |
231 | | |
232 | | // FIXME: 17. Let hideUntil be the result of running topmost popover ancestor given this, null, and false. |
233 | | |
234 | | // FIXME: 18. If hideUntil is null, then set hideUntil to this's node document. |
235 | | |
236 | | // FIXME: 19. Run hide all popovers until given hideUntil, false, and true. |
237 | | |
238 | | // 20. Run the dialog focusing steps given this. |
239 | 0 | run_dialog_focusing_steps(); |
240 | |
|
241 | 0 | return {}; |
242 | 0 | } |
243 | | |
244 | | // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-close |
245 | | void HTMLDialogElement::close(Optional<String> return_value) |
246 | 0 | { |
247 | | // 1. If returnValue is not given, then set it to null. |
248 | | // 2. Close the dialog this with returnValue. |
249 | 0 | close_the_dialog(move(return_value)); |
250 | 0 | } |
251 | | |
252 | | // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-returnvalue |
253 | | String HTMLDialogElement::return_value() const |
254 | 0 | { |
255 | 0 | return m_return_value; |
256 | 0 | } |
257 | | |
258 | | // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-returnvalue |
259 | | void HTMLDialogElement::set_return_value(String return_value) |
260 | 0 | { |
261 | 0 | m_return_value = move(return_value); |
262 | 0 | } |
263 | | |
264 | | // https://html.spec.whatwg.org/multipage/interactive-elements.html#close-the-dialog |
265 | | void HTMLDialogElement::close_the_dialog(Optional<String> result) |
266 | 0 | { |
267 | | // 1. If subject does not have an open attribute, then return. |
268 | 0 | if (!has_attribute(AttributeNames::open)) |
269 | 0 | return; |
270 | | |
271 | | // 2. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open" and the newState attribute initialized to "closed" at subject. |
272 | 0 | ToggleEventInit event_init {}; |
273 | 0 | event_init.old_state = "open"_string; |
274 | 0 | event_init.new_state = "closed"_string; |
275 | |
|
276 | 0 | dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init))); |
277 | | |
278 | | // 3. If subject does not have an open attribute, then return. |
279 | 0 | if (!has_attribute(AttributeNames::open)) |
280 | 0 | return; |
281 | | |
282 | | // 4. Queue a dialog toggle event task given subject, "open", and "closed". |
283 | 0 | queue_a_dialog_toggle_event_task("open"_string, "closed"_string); |
284 | | |
285 | | // 5. Remove subject's open attribute. |
286 | 0 | remove_attribute(AttributeNames::open); |
287 | | |
288 | | // 6. If the is modal flag of subject is true, then request an element to be removed from the top layer given subject. |
289 | 0 | if (m_is_modal) |
290 | 0 | document().request_an_element_to_be_remove_from_the_top_layer(*this); |
291 | | |
292 | | // FIXME: 7. Let wasModal be the value of subject's is modal flag. |
293 | | |
294 | | // 8. Set the is modal flag of subject to false. |
295 | 0 | m_is_modal = false; |
296 | | |
297 | | // 9. If result is not null, then set the returnValue attribute to result. |
298 | 0 | if (result.has_value()) |
299 | 0 | set_return_value(result.release_value()); |
300 | | |
301 | | // FIXME: 10. If subject's previously focused element is not null, then: |
302 | | // 1. Let element be subject's previously focused element. |
303 | | // 2. Set subject's previously focused element to null. |
304 | | // 3. If subject's node document's focused area of the document's DOM anchor is a shadow-including inclusive descendant of element, |
305 | | // or wasModal is true, then run the focusing steps for element; the viewport should not be scrolled by doing this step. |
306 | | |
307 | | // 11. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject. |
308 | 0 | queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { |
309 | 0 | auto close_event = DOM::Event::create(realm(), HTML::EventNames::close); |
310 | 0 | dispatch_event(close_event); |
311 | 0 | }); |
312 | | |
313 | | // 12. If subject's close watcher is not null, then: |
314 | 0 | if (m_close_watcher) { |
315 | | // 9.1 Destroy subject's close watcher. |
316 | 0 | m_close_watcher->destroy(); |
317 | | // 9.2 Set subject's close watcher to null. |
318 | 0 | m_close_watcher = nullptr; |
319 | 0 | } |
320 | 0 | } |
321 | | |
322 | | // https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-focusing-steps |
323 | | void HTMLDialogElement::run_dialog_focusing_steps() |
324 | 0 | { |
325 | | // 1. Let control be null |
326 | 0 | JS::GCPtr<Element> control = nullptr; |
327 | | |
328 | | // FIXME 2. If subject has the autofocus attribute, then set control to subject. |
329 | | // FIXME 3. If control is null, then set control to the focus delegate of subject. |
330 | | |
331 | | // 4. If control is null, then set control to subject. |
332 | 0 | if (!control) |
333 | 0 | control = this; |
334 | | |
335 | | // 5. Run the focusing steps for control. |
336 | 0 | run_focusing_steps(control); |
337 | 0 | } |
338 | | |
339 | | } |