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