script/dom/html/htmldialogelement.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4use std::borrow::Borrow;
5
6use dom_struct::dom_struct;
7use html5ever::{LocalName, Prefix, local_name, ns};
8use js::context::JSContext;
9use js::rust::HandleObject;
10use script_bindings::cell::DomRefCell;
11use script_bindings::codegen::GenericBindings::HTMLElementBinding::HTMLElementMethods;
12use script_bindings::error::{Error, ErrorResult};
13use stylo_dom::ElementState;
14
15use crate::dom::bindings::codegen::Bindings::HTMLDialogElementBinding::HTMLDialogElementMethods;
16use crate::dom::bindings::inheritance::Castable;
17use crate::dom::bindings::refcounted::Trusted;
18use crate::dom::bindings::root::DomRoot;
19use crate::dom::bindings::str::DOMString;
20use crate::dom::document::Document;
21use crate::dom::element::Element;
22use crate::dom::event::{Event, EventBubbles, EventCancelable};
23use crate::dom::eventtarget::EventTarget;
24use crate::dom::html::htmlelement::HTMLElement;
25use crate::dom::htmlbuttonelement::{CommandState, HTMLButtonElement};
26use crate::dom::iterators::ShadowIncluding;
27use crate::dom::node::virtualmethods::VirtualMethods;
28use crate::dom::node::{Node, NodeTraits};
29use crate::dom::toggleevent::ToggleEvent;
30
31#[dom_struct]
32pub(crate) struct HTMLDialogElement {
33 htmlelement: HTMLElement,
34 return_value: DomRefCell<DOMString>,
35}
36
37impl HTMLDialogElement {
38 fn new_inherited(
39 local_name: LocalName,
40 prefix: Option<Prefix>,
41 document: &Document,
42 ) -> HTMLDialogElement {
43 HTMLDialogElement {
44 htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
45 return_value: DomRefCell::new(DOMString::new()),
46 }
47 }
48
49 pub(crate) fn new(
50 cx: &mut js::context::JSContext,
51 local_name: LocalName,
52 prefix: Option<Prefix>,
53 document: &Document,
54 proto: Option<HandleObject>,
55 ) -> DomRoot<HTMLDialogElement> {
56 Node::reflect_node_with_proto(
57 cx,
58 Box::new(HTMLDialogElement::new_inherited(
59 local_name, prefix, document,
60 )),
61 document,
62 proto,
63 )
64 }
65
66 /// <https://html.spec.whatwg.org/multipage/#show-a-modal-dialog>
67 pub fn show_a_modal(
68 &self,
69 cx: &mut js::context::JSContext,
70 source: Option<DomRoot<Element>>,
71 ) -> ErrorResult {
72 let subject = self.upcast::<Element>();
73 // Step 1. If subject has an open attribute and is modal of subject is true, then return.
74 if subject.has_attribute(&local_name!("open")) &&
75 subject.state().contains(ElementState::MODAL)
76 {
77 return Ok(());
78 }
79
80 // Step 2. If subject has an open attribute, then throw an "InvalidStateError" DOMException.
81 if subject.has_attribute(&local_name!("open")) {
82 return Err(Error::InvalidState(Some(
83 "Cannot call showModal() on an already open dialog.".into(),
84 )));
85 }
86
87 // Step 3. If subject's node document is not fully active, then throw an "InvalidStateError" DOMException.
88 if !subject.owner_document().is_fully_active() {
89 return Err(Error::InvalidState(Some(
90 "Cannot call showModal() on a dialog whose document is not fully active.".into(),
91 )));
92 }
93
94 // Step 4. If subject is not connected, then throw an "InvalidStateError" DOMException.
95 if !subject.is_connected() {
96 return Err(Error::InvalidState(Some(
97 "Cannot call showModal() on a dialog that is not connected.".into(),
98 )));
99 }
100
101 // TODO: Step 5. If subject is in the popover showing state, then throw an "InvalidStateError" DOMException.
102
103 // Step 6. If the result of firing an event named beforetoggle, using ToggleEvent, with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", the newState attribute initialized to "open", and the source attribute initialized to source at subject is false, then return.
104 let event = ToggleEvent::new(
105 cx,
106 &self.owner_window(),
107 atom!("beforetoggle"),
108 EventBubbles::DoesNotBubble,
109 EventCancelable::Cancelable,
110 DOMString::from("closed"),
111 DOMString::from("open"),
112 source.borrow().clone(),
113 );
114 let event = event.upcast::<Event>();
115 if !event.fire(cx, self.upcast::<EventTarget>()) {
116 return Ok(());
117 }
118
119 // Step 7. If subject has an open attribute, then return.
120 if subject.has_attribute(&local_name!("open")) {
121 return Ok(());
122 }
123
124 // Step 8. If subject is not connected, then return.
125 if !subject.is_connected() {
126 return Ok(());
127 }
128
129 // TODO: Step 9. If subject is in the popover showing state, then return.
130
131 // Step 10. Queue a dialog toggle event task given subject, "closed", "open", and source.
132 self.queue_dialog_toggle_event_task("closed", "open", source);
133
134 // Step 11. Add an open attribute to subject, whose value is the empty string.
135 subject.set_bool_attribute(cx, &local_name!("open"), true);
136 subject.set_open_state(true);
137
138 // TODO: Step 12. Assert: subject's close watcher is not null.
139
140 // Step 13. Set is modal of subject to true.
141 self.upcast::<Element>().set_modal_state(true);
142
143 // TODO: Step 14. Set subject's node document to be blocked by the modal dialog subject.
144
145 // TODO: Step 15. If subject's node document's top layer does not already contain subject, then add an element to the top layer given subject.
146
147 // Step 16. Set subject's previously focused element to the focused element.
148 self.upcast::<HTMLElement>().set_previously_focused_element(
149 self.owner_document()
150 .focus_handler()
151 .focused_area()
152 .element(),
153 );
154
155 // TODO: Step 17. Let document be subject's node document.
156
157 // TODO: Step 18. Let hideUntil be the result of running topmost popover ancestor given subject, document's showing hint popover list, null, and false.
158
159 // TODO: Step 19. If hideUntil is null, then set hideUntil to the result of running topmost popover ancestor given subject, document's showing auto popover list, null, and false.
160
161 // TODO: Step 20. If hideUntil is null, then set hideUntil to document.
162
163 // TODO: Step 21. Run hide all popovers until given hideUntil, false, and true.
164
165 // Step 22. Run the dialog focusing steps given subject.
166 self.run_dialog_focusing_steps(cx);
167 Ok(())
168 }
169
170 /// <https://html.spec.whatwg.org/multipage/#close-the-dialog>
171 pub fn close_the_dialog(
172 &self,
173 cx: &mut js::context::JSContext,
174 result: Option<DOMString>,
175 source: Option<DomRoot<Element>>,
176 ) {
177 let subject = self.upcast::<Element>();
178 // Step 1. If subject does not have an open attribute, then return.
179 if !subject.has_attribute(&local_name!("open")) {
180 return;
181 }
182
183 // Step 2. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open", the newState attribute initialized to "closed", and the source attribute initialized to source at subject.
184 let event = ToggleEvent::new(
185 cx,
186 &self.owner_window(),
187 atom!("beforetoggle"),
188 EventBubbles::DoesNotBubble,
189 EventCancelable::NotCancelable,
190 DOMString::from("open"),
191 DOMString::from("closed"),
192 source.borrow().clone(),
193 );
194 let event = event.upcast::<Event>();
195 event.fire(cx, self.upcast::<EventTarget>());
196
197 // Step 3. If subject does not have an open attribute, then return.
198 if !subject.has_attribute(&local_name!("open")) {
199 return;
200 }
201
202 // Step 4. Queue a dialog toggle event task given subject, "open", "closed", and source.
203 self.queue_dialog_toggle_event_task("open", "closed", source);
204
205 // Step 5. Remove subject's open attribute.
206 subject.remove_attribute(cx, &ns!(), &local_name!("open"));
207 subject.set_open_state(false);
208
209 // TODO: Step 6. If is modal of subject is true, then request an element to be removed from the top layer given subject.
210
211 // Step 7. Let wasModal be the value of subject's is modal flag.
212 let was_modal = subject.state().contains(ElementState::MODAL);
213
214 // Step 8. Set is modal of subject to false.
215 self.upcast::<Element>().set_modal_state(false);
216
217 // Step 9. If result is not null, then set subject's returnValue attribute to result.
218 if let Some(new_value) = result {
219 *self.return_value.borrow_mut() = new_value;
220 }
221
222 // TODO: Step 10. Set subject's request close return value to null.
223
224 // TODO: Step 11. Set subject's request close source element to null.
225
226 // Step 12. If subject's previously focused element is not null, then:
227 if let Some(element) = self.upcast::<HTMLElement>().previously_focused_element() {
228 // Step 12.1. Let element be subject's previously focused element.
229 // Step 12.2. Set subject's previously focused element to null.
230 self.upcast::<HTMLElement>()
231 .set_previously_focused_element(None);
232
233 // Step 12.3. If subject's node document's focused area of the document's DOM anchor is
234 // a shadow-including inclusive descendant of subject, or wasModal is true, then run the
235 // focusing steps for element; the viewport should not be scrolled by doing this step.
236 let subject_node = subject.upcast::<Node>();
237 let document = subject.owner_document();
238 if document
239 .focus_handler()
240 .focused_area()
241 .dom_anchor(&document)
242 .traverse_preorder(ShadowIncluding::Yes)
243 .any(|node| &*node == subject_node) ||
244 was_modal
245 {
246 element.upcast::<Node>().run_the_focusing_steps(cx, None);
247 }
248 }
249
250 // Step 13. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject.
251 let target = self.upcast::<EventTarget>();
252 self.owner_global()
253 .task_manager()
254 .user_interaction_task_source()
255 .queue_simple_event(target, atom!("close"));
256 }
257
258 /// <https://html.spec.whatwg.org/multipage/#queue-a-dialog-toggle-event-task>
259 pub fn queue_dialog_toggle_event_task(
260 &self,
261 old_state: &str,
262 new_state: &str,
263 source: Option<DomRoot<Element>>,
264 ) {
265 // TODO: Step 1. If element's dialog toggle task tracker is not null, then:
266 // TODO: Step 1.1. Set oldState to element's dialog toggle task tracker's old state.
267 // TODO: Step 1.2. Remove element's dialog toggle task tracker's task from its task queue.
268 // TODO: Step 1.3. Set element's dialog toggle task tracker to null.
269 // Step 2. Queue an element task given the DOM manipulation task source and element to run the following steps:
270 let this = Trusted::new(self);
271 let old_state = old_state.to_string();
272 let new_state = new_state.to_string();
273
274 let trusted_source = source
275 .as_ref()
276 .map(|el| Trusted::new(el.upcast::<EventTarget>()));
277
278 self.owner_global()
279 .task_manager()
280 .dom_manipulation_task_source()
281 .queue(task!(fire_toggle_event: move |cx| {
282 let this = this.root();
283
284 let source = trusted_source.as_ref().map(|s| {
285 DomRoot::from_ref(s.root().downcast::<Element>().unwrap())
286 });
287
288 // Step 2.1. Fire an event named toggle at element, using ToggleEvent, with the oldState attribute initialized to oldState, the newState attribute initialized to newState, and the source attribute initialized to source.
289 let event = ToggleEvent::new(
290 cx,
291 &this.owner_window(),
292 atom!("toggle"),
293 EventBubbles::DoesNotBubble,
294 EventCancelable::NotCancelable,
295 DOMString::from(old_state),
296 DOMString::from(new_state),
297 source,
298 );
299 let event = event.upcast::<Event>();
300 event.fire(cx, this.upcast::<EventTarget>());
301
302 // TODO: Step 2.2. Set element's dialog toggle task tracker to null.
303 }));
304 // TODO: Step 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.
305 }
306
307 /// <https://html.spec.whatwg.org/multipage/#dialog-focusing-steps>
308 fn run_dialog_focusing_steps(&self, cx: &mut JSContext) {
309 // TODO: Step 1. If the allow focus steps given subject's node document return false, then return.
310
311 // Step 2. Let control be null.
312 let mut control = None;
313
314 // Step 3. If subject has the autofocus attribute, then set control to subject.
315 if self.upcast::<HTMLElement>().Autofocus() {
316 control = self.upcast::<Node>().get_the_focusable_area();
317 }
318
319 // Step 4. If control is null, then set control to the focus delegate of subject.
320 if control.is_none() {
321 control = self.upcast::<Node>().focus_delegate();
322 }
323
324 // Step 5. If control is null, then set control to subject.
325 if control.is_none() {
326 control = self.upcast::<Node>().get_the_focusable_area();
327 }
328
329 // Step 6. Run the focusing steps for control.
330 // FIXME: Use the focusing step once they support a focusable area as an argument
331 if let Some(control) = control {
332 let document = self.owner_document();
333 document.focus_handler().focus(cx, control);
334 }
335
336 // TODO: Step 7. Let topDocument be control's node navigable's top-level traversable's active document.
337 // TODO: Step 8. If control's node document's origin is not the same as the origin of topDocument, then return.
338 // TODO: Step 9. Empty topDocument's autofocus candidates.
339 // TODO: Step 10. Set topDocument's autofocus processed flag to true.
340 }
341}
342
343impl HTMLDialogElementMethods<crate::DomTypeHolder> for HTMLDialogElement {
344 // https://html.spec.whatwg.org/multipage/#dom-dialog-open
345 make_bool_getter!(Open, "open");
346
347 // https://html.spec.whatwg.org/multipage/#dom-dialog-open
348 make_bool_setter!(SetOpen, "open");
349
350 /// <https://html.spec.whatwg.org/multipage/#dom-dialog-returnvalue>
351 fn ReturnValue(&self) -> DOMString {
352 let return_value = self.return_value.borrow();
353 return_value.clone()
354 }
355
356 /// <https://html.spec.whatwg.org/multipage/#dom-dialog-returnvalue>
357 fn SetReturnValue(&self, _cx: &mut JSContext, return_value: DOMString) {
358 *self.return_value.borrow_mut() = return_value;
359 }
360
361 /// <https://html.spec.whatwg.org/multipage/#dom-dialog-show>
362 fn Show(&self, cx: &mut js::context::JSContext) -> ErrorResult {
363 let element = self.upcast::<Element>();
364 // Step 1. If this has an open attribute and is modal of this is false, then return.
365 if element.has_attribute(&local_name!("open")) &&
366 !element.state().contains(ElementState::MODAL)
367 {
368 return Ok(());
369 }
370
371 // Step 2. If this has an open attribute, then throw an "InvalidStateError" DOMException.
372 if element.has_attribute(&local_name!("open")) {
373 return Err(Error::InvalidState(Some(
374 "Cannot call show() on an already open dialog.".into(),
375 )));
376 }
377
378 // Step 3. If the result of firing an event named beforetoggle, using ToggleEvent, with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", and the newState attribute initialized to "open" at this is false, then return.
379 let event = ToggleEvent::new(
380 cx,
381 &self.owner_window(),
382 atom!("beforetoggle"),
383 EventBubbles::DoesNotBubble,
384 EventCancelable::Cancelable,
385 DOMString::from("closed"),
386 DOMString::from("open"),
387 None,
388 );
389 let event = event.upcast::<Event>();
390 if !event.fire(cx, self.upcast::<EventTarget>()) {
391 return Ok(());
392 }
393
394 // Step 4. If this has an open attribute, then return.
395 if element.has_attribute(&local_name!("open")) {
396 return Ok(());
397 }
398
399 // Step 5. Queue a dialog toggle event task given this, "closed", "open", and null.
400 self.queue_dialog_toggle_event_task("closed", "open", None);
401
402 // Step 6. Add an open attribute to this, whose value is the empty string.
403 element.set_bool_attribute(cx, &local_name!("open"), true);
404 element.set_open_state(true);
405
406 // Step 7. Set this's previously focused element to the focused element.
407 self.upcast::<HTMLElement>().set_previously_focused_element(
408 self.owner_document()
409 .focus_handler()
410 .focused_area()
411 .element(),
412 );
413
414 // TODO: Step 8. Let document be this's node document.
415
416 // TODO: Step 9. Let hideUntil be the result of running topmost popover ancestor given this, document's showing hint popover list, null, and false.
417
418 // TODO: Step 10. If hideUntil is null, then set hideUntil to the result of running topmost popover ancestor given this, document's showing auto popover list, null, and false.
419
420 // TODO: Step 11. If hideUntil is null, then set hideUntil to document.
421
422 // TODO: Step 12. Run hide all popovers until given hideUntil, false, and true.
423
424 // Step 13. Run the dialog focusing steps given this.
425 self.run_dialog_focusing_steps(cx);
426 Ok(())
427 }
428
429 /// <https://html.spec.whatwg.org/multipage/#dom-dialog-showmodal>
430 fn ShowModal(&self, cx: &mut js::context::JSContext) -> ErrorResult {
431 // The showModal() method steps are to show a modal dialog given this and null.
432 self.show_a_modal(cx, None)
433 }
434
435 /// <https://html.spec.whatwg.org/multipage/#dom-dialog-close>
436 fn Close(&self, cx: &mut js::context::JSContext, return_value: Option<DOMString>) {
437 // Step 1. If returnValue is not given, then set it to null.
438 // Step 2. Close the dialog this with returnValue and null.
439 self.close_the_dialog(cx, return_value, None);
440 }
441}
442
443impl VirtualMethods for HTMLDialogElement {
444 fn super_type(&self) -> Option<&dyn VirtualMethods> {
445 Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
446 }
447
448 /// <https://html.spec.whatwg.org/multipage/#the-dialog-element:is-valid-command-steps>
449 fn is_valid_command_steps(&self, command: CommandState) -> bool {
450 // Step 1. If command is in the Close state, the Request Close state (TODO), or the
451 // ShowModal state, then return true.
452 if command == CommandState::Close || command == CommandState::ShowModal {
453 return true;
454 }
455 // Step 2. Return false.
456 false
457 }
458
459 /// <https://html.spec.whatwg.org/multipage/#the-dialog-element:command-steps>
460 fn command_steps(
461 &self,
462 cx: &mut js::context::JSContext,
463 source: DomRoot<HTMLButtonElement>,
464 command: CommandState,
465 ) -> bool {
466 if self
467 .super_type()
468 .unwrap()
469 .command_steps(cx, source.clone(), command)
470 {
471 return true;
472 }
473
474 // TODO Step 1. If element is in the popover showing state, then return.
475 let element = self.upcast::<Element>();
476
477 // Step 2. If command is in the Close state and element has an open attribute, then
478 // close the dialog element with source's optional value and source.
479 if command == CommandState::Close && element.has_attribute(&local_name!("open")) {
480 let button_element = DomRoot::from_ref(source.upcast::<Element>());
481 self.close_the_dialog(cx, source.optional_value(), Some(button_element));
482 return true;
483 }
484
485 // TODO Step 3. If command is in the Request Close state and element has an open attribute,
486 // then request to close the dialog element with source's optional value and source.
487
488 // Step 4. If command is the Show Modal state and element does not have an open attribute,
489 // then show a modal dialog given element and source.
490 if command == CommandState::ShowModal && !element.has_attribute(&local_name!("open")) {
491 let button_element = DomRoot::from_ref(source.upcast::<Element>());
492 let _ = self.show_a_modal(cx, Some(button_element));
493 return true;
494 }
495
496 false
497 }
498}