Skip to main content

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::error::{Error, ErrorResult};
12use stylo_dom::ElementState;
13
14use crate::dom::bindings::codegen::Bindings::HTMLDialogElementBinding::HTMLDialogElementMethods;
15use crate::dom::bindings::inheritance::Castable;
16use crate::dom::bindings::refcounted::Trusted;
17use crate::dom::bindings::root::DomRoot;
18use crate::dom::bindings::str::DOMString;
19use crate::dom::document::Document;
20use crate::dom::element::Element;
21use crate::dom::event::{Event, EventBubbles, EventCancelable};
22use crate::dom::eventtarget::EventTarget;
23use crate::dom::html::htmlelement::HTMLElement;
24use crate::dom::htmlbuttonelement::{CommandState, HTMLButtonElement};
25use crate::dom::iterators::ShadowIncluding;
26use crate::dom::node::focus::FocusTrigger;
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.as_deref(),
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            cx.no_gc(),
154        );
155
156        // TODO: Step 17. Let document be subject's node document.
157
158        // TODO: Step 18. Let hideUntil be the result of running topmost popover ancestor given subject, document's showing hint popover list, null, and false.
159
160        // 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.
161
162        // TODO: Step 20. If hideUntil is null, then set hideUntil to document.
163
164        // TODO: Step 21. Run hide all popovers until given hideUntil, false, and true.
165
166        // Step 22. Run the dialog focusing steps given subject.
167        self.run_dialog_focusing_steps(cx);
168        Ok(())
169    }
170
171    /// <https://html.spec.whatwg.org/multipage/#close-the-dialog>
172    pub fn close_the_dialog(
173        &self,
174        cx: &mut js::context::JSContext,
175        result: Option<DOMString>,
176        source: Option<DomRoot<Element>>,
177    ) {
178        let subject = self.upcast::<Element>();
179        // Step 1. If subject does not have an open attribute, then return.
180        if !subject.has_attribute(&local_name!("open")) {
181            return;
182        }
183
184        // 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.
185        let event = ToggleEvent::new(
186            cx,
187            &self.owner_window(),
188            atom!("beforetoggle"),
189            EventBubbles::DoesNotBubble,
190            EventCancelable::NotCancelable,
191            DOMString::from("open"),
192            DOMString::from("closed"),
193            source.as_deref(),
194        );
195        let event = event.upcast::<Event>();
196        event.fire(cx, self.upcast::<EventTarget>());
197
198        // Step 3. If subject does not have an open attribute, then return.
199        if !subject.has_attribute(&local_name!("open")) {
200            return;
201        }
202
203        // Step 4. Queue a dialog toggle event task given subject, "open", "closed", and source.
204        self.queue_dialog_toggle_event_task("open", "closed", source);
205
206        // Step 5. Remove subject's open attribute.
207        subject.remove_attribute(cx, &ns!(), &local_name!("open"));
208        subject.set_open_state(false);
209
210        // TODO: Step 6. If is modal of subject is true, then request an element to be removed from the top layer given subject.
211
212        // Step 7. Let wasModal be the value of subject's is modal flag.
213        let was_modal = subject.state().contains(ElementState::MODAL);
214
215        // Step 8. Set is modal of subject to false.
216        self.upcast::<Element>().set_modal_state(false);
217
218        // Step 9. If result is not null, then set subject's returnValue attribute to result.
219        if let Some(new_value) = result {
220            *self.return_value.borrow_mut() = new_value;
221        }
222
223        // TODO: Step 10. Set subject's request close return value to null.
224
225        // TODO: Step 11. Set subject's request close source element to null.
226
227        // Step 12. If subject's previously focused element is not null, then:
228        if let Some(element) = self
229            .upcast::<HTMLElement>()
230            .previously_focused_element(cx.no_gc())
231        {
232            // Step 12.1. Let element be subject's previously focused element.
233            // Step 12.2. Set subject's previously focused element to null.
234            self.upcast::<HTMLElement>()
235                .set_previously_focused_element(None, cx.no_gc());
236
237            // Step 12.3. If subject's node document's focused area of the document's DOM anchor is
238            // a shadow-including inclusive descendant of subject, or wasModal is true, then run the
239            // focusing steps for element; the viewport should not be scrolled by doing this step.
240            let subject_node = subject.upcast::<Node>();
241            let document = subject.owner_document();
242            if document
243                .focus_handler()
244                .focused_area()
245                .dom_anchor(&document)
246                .traverse_preorder(ShadowIncluding::Yes)
247                .any(|node| &*node == subject_node) ||
248                was_modal
249            {
250                element
251                    .upcast::<Node>()
252                    .run_the_focusing_steps(cx, None, FocusTrigger::Other);
253            }
254        }
255
256        // Step 13. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject.
257        let target = self.upcast::<EventTarget>();
258        self.owner_global()
259            .task_manager()
260            .user_interaction_task_source()
261            .queue_simple_event(target, atom!("close"));
262    }
263
264    /// <https://html.spec.whatwg.org/multipage/#queue-a-dialog-toggle-event-task>
265    pub fn queue_dialog_toggle_event_task(
266        &self,
267        old_state: &str,
268        new_state: &str,
269        source: Option<DomRoot<Element>>,
270    ) {
271        // TODO: Step 1. If element's dialog toggle task tracker is not null, then:
272        // TODO: Step 1.1. Set oldState to element's dialog toggle task tracker's old state.
273        // TODO: Step 1.2. Remove element's dialog toggle task tracker's task from its task queue.
274        // TODO: Step 1.3. Set element's dialog toggle task tracker to null.
275        // Step 2. Queue an element task given the DOM manipulation task source and element to run the following steps:
276        let this = Trusted::new(self);
277        let old_state = old_state.to_string();
278        let new_state = new_state.to_string();
279
280        let trusted_source = source.map(|el| Trusted::new(&*el));
281
282        self.owner_global()
283            .task_manager()
284            .dom_manipulation_task_source()
285            .queue(task!(fire_toggle_event: move |cx| {
286                let this = this.root();
287
288                let source = trusted_source.map(|s| s.root());
289
290                // 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.
291                let event = ToggleEvent::new(
292                    cx,
293                    &this.owner_window(),
294                    atom!("toggle"),
295                    EventBubbles::DoesNotBubble,
296                    EventCancelable::NotCancelable,
297                    DOMString::from(old_state),
298                    DOMString::from(new_state),
299                    source.as_deref(),
300                );
301                let event = event.upcast::<Event>();
302                event.fire(cx, this.upcast::<EventTarget>());
303
304                // TODO: Step 2.2. Set element's dialog toggle task tracker to null.
305            }));
306        // 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.
307    }
308
309    /// <https://html.spec.whatwg.org/multipage/#dialog-focusing-steps>
310    fn run_dialog_focusing_steps(&self, cx: &mut JSContext) {
311        // TODO: Step 1. If the allow focus steps given subject's node document return false, then return.
312
313        // Step 2. Let control be null.
314        rooted!(&in(cx) let mut control = None);
315
316        // Step 3. If subject has the autofocus attribute, then set control to subject.
317        if self.upcast::<HTMLElement>().Autofocus() {
318            control.set(
319                self.upcast::<Node>()
320                    .get_the_focusable_area(cx, FocusTrigger::Other),
321            );
322        }
323
324        // Step 4. If control is null, then set control to the focus delegate of subject.
325        if control.is_none() {
326            control.set(self.upcast::<Node>().focus_delegate(cx, None));
327        }
328
329        // Step 5. If control is null, then set control to subject.
330        if control.is_none() {
331            control.set(
332                self.upcast::<Node>()
333                    .get_the_focusable_area(cx, FocusTrigger::Other),
334            );
335        }
336
337        // Step 6. Run the focusing steps for control.
338        // FIXME: Use the focusing step once they support a focusable area as an argument
339        if control.is_some() {
340            let document = self.owner_document();
341            document.focus_handler().focus(cx, &control.take().unwrap());
342        }
343
344        // TODO: Step 7. Let topDocument be control's node navigable's top-level traversable's active document.
345        // TODO: Step 8. If control's node document's origin is not the same as the origin of topDocument, then return.
346        // TODO: Step 9. Empty topDocument's autofocus candidates.
347        // TODO: Step 10. Set topDocument's autofocus processed flag to true.
348    }
349}
350
351impl HTMLDialogElementMethods<crate::DomTypeHolder> for HTMLDialogElement {
352    // https://html.spec.whatwg.org/multipage/#dom-dialog-open
353    make_bool_getter!(Open, "open");
354
355    // https://html.spec.whatwg.org/multipage/#dom-dialog-open
356    make_bool_setter!(SetOpen, "open");
357
358    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-returnvalue>
359    fn ReturnValue(&self) -> DOMString {
360        let return_value = self.return_value.borrow();
361        return_value.clone()
362    }
363
364    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-returnvalue>
365    fn SetReturnValue(&self, _cx: &mut JSContext, return_value: DOMString) {
366        *self.return_value.borrow_mut() = return_value;
367    }
368
369    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-show>
370    fn Show(&self, cx: &mut js::context::JSContext) -> ErrorResult {
371        let element = self.upcast::<Element>();
372        // Step 1. If this has an open attribute and is modal of this is false, then return.
373        if element.has_attribute(&local_name!("open")) &&
374            !element.state().contains(ElementState::MODAL)
375        {
376            return Ok(());
377        }
378
379        // Step 2. If this has an open attribute, then throw an "InvalidStateError" DOMException.
380        if element.has_attribute(&local_name!("open")) {
381            return Err(Error::InvalidState(Some(
382                "Cannot call show() on an already open dialog.".into(),
383            )));
384        }
385
386        // 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.
387        let event = ToggleEvent::new(
388            cx,
389            &self.owner_window(),
390            atom!("beforetoggle"),
391            EventBubbles::DoesNotBubble,
392            EventCancelable::Cancelable,
393            DOMString::from("closed"),
394            DOMString::from("open"),
395            None,
396        );
397        let event = event.upcast::<Event>();
398        if !event.fire(cx, self.upcast::<EventTarget>()) {
399            return Ok(());
400        }
401
402        // Step 4. If this has an open attribute, then return.
403        if element.has_attribute(&local_name!("open")) {
404            return Ok(());
405        }
406
407        // Step 5. Queue a dialog toggle event task given this, "closed", "open", and null.
408        self.queue_dialog_toggle_event_task("closed", "open", None);
409
410        // Step 6. Add an open attribute to this, whose value is the empty string.
411        element.set_bool_attribute(cx, &local_name!("open"), true);
412        element.set_open_state(true);
413
414        // Step 7. Set this's previously focused element to the focused element.
415        self.upcast::<HTMLElement>().set_previously_focused_element(
416            self.owner_document()
417                .focus_handler()
418                .focused_area()
419                .element(),
420            cx.no_gc(),
421        );
422
423        // TODO: Step 8. Let document be this's node document.
424
425        // TODO: Step 9. Let hideUntil be the result of running topmost popover ancestor given this, document's showing hint popover list, null, and false.
426
427        // 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.
428
429        // TODO: Step 11. If hideUntil is null, then set hideUntil to document.
430
431        // TODO: Step 12. Run hide all popovers until given hideUntil, false, and true.
432
433        // Step 13. Run the dialog focusing steps given this.
434        self.run_dialog_focusing_steps(cx);
435        Ok(())
436    }
437
438    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-showmodal>
439    fn ShowModal(&self, cx: &mut js::context::JSContext) -> ErrorResult {
440        // The showModal() method steps are to show a modal dialog given this and null.
441        self.show_a_modal(cx, None)
442    }
443
444    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-close>
445    fn Close(&self, cx: &mut js::context::JSContext, return_value: Option<DOMString>) {
446        // Step 1. If returnValue is not given, then set it to null.
447        // Step 2. Close the dialog this with returnValue and null.
448        self.close_the_dialog(cx, return_value, None);
449    }
450}
451
452impl VirtualMethods for HTMLDialogElement {
453    fn super_type(&self) -> Option<&dyn VirtualMethods> {
454        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
455    }
456
457    /// <https://html.spec.whatwg.org/multipage/#the-dialog-element:is-valid-command-steps>
458    fn is_valid_command_steps(&self, command: CommandState) -> bool {
459        // Step 1. If command is in the Close state, the Request Close state (TODO), or the
460        // ShowModal state, then return true.
461        if command == CommandState::Close || command == CommandState::ShowModal {
462            return true;
463        }
464        // Step 2. Return false.
465        false
466    }
467
468    /// <https://html.spec.whatwg.org/multipage/#the-dialog-element:command-steps>
469    fn command_steps(
470        &self,
471        cx: &mut js::context::JSContext,
472        source: DomRoot<HTMLButtonElement>,
473        command: CommandState,
474    ) -> bool {
475        if self
476            .super_type()
477            .unwrap()
478            .command_steps(cx, source.clone(), command)
479        {
480            return true;
481        }
482
483        // TODO Step 1. If element is in the popover showing state, then return.
484        let element = self.upcast::<Element>();
485
486        // Step 2. If command is in the Close state and element has an open attribute, then
487        // close the dialog element with source's optional value and source.
488        if command == CommandState::Close && element.has_attribute(&local_name!("open")) {
489            let button_element = DomRoot::from_ref(source.upcast::<Element>());
490            self.close_the_dialog(cx, source.optional_value(), Some(button_element));
491            return true;
492        }
493
494        // TODO Step 3. If command is in the Request Close state and element has an open attribute,
495        // then request to close the dialog element with source's optional value and source.
496
497        // Step 4. If command is the Show Modal state and element does not have an open attribute,
498        // then show a modal dialog given element and source.
499        if command == CommandState::ShowModal && !element.has_attribute(&local_name!("open")) {
500            let button_element = DomRoot::from_ref(source.upcast::<Element>());
501            let _ = self.show_a_modal(cx, Some(button_element));
502            return true;
503        }
504
505        false
506    }
507}