Skip to main content

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/. */
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::virtualmethods::VirtualMethods;
27use crate::dom::node::{Node, NodeTraits};
28use crate::dom::toggleevent::ToggleEvent;
29
30#[dom_struct]
31pub(crate) struct HTMLDialogElement {
32    htmlelement: HTMLElement,
33    return_value: DomRefCell<DOMString>,
34}
35
36impl HTMLDialogElement {
37    fn new_inherited(
38        local_name: LocalName,
39        prefix: Option<Prefix>,
40        document: &Document,
41    ) -> HTMLDialogElement {
42        HTMLDialogElement {
43            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
44            return_value: DomRefCell::new(DOMString::new()),
45        }
46    }
47
48    pub(crate) fn new(
49        cx: &mut js::context::JSContext,
50        local_name: LocalName,
51        prefix: Option<Prefix>,
52        document: &Document,
53        proto: Option<HandleObject>,
54    ) -> DomRoot<HTMLDialogElement> {
55        Node::reflect_node_with_proto(
56            cx,
57            Box::new(HTMLDialogElement::new_inherited(
58                local_name, prefix, document,
59            )),
60            document,
61            proto,
62        )
63    }
64
65    /// <https://html.spec.whatwg.org/multipage/#show-a-modal-dialog>
66    pub fn show_a_modal(
67        &self,
68        cx: &mut js::context::JSContext,
69        source: Option<DomRoot<Element>>,
70    ) -> ErrorResult {
71        let subject = self.upcast::<Element>();
72        // Step 1. If subject has an open attribute and is modal of subject is true, then return.
73        if subject.has_attribute(&local_name!("open")) &&
74            subject.state().contains(ElementState::MODAL)
75        {
76            return Ok(());
77        }
78
79        // Step 2. If subject has an open attribute, then throw an "InvalidStateError" DOMException.
80        if subject.has_attribute(&local_name!("open")) {
81            return Err(Error::InvalidState(Some(
82                "Cannot call showModal() on an already open dialog.".into(),
83            )));
84        }
85
86        // Step 3. If subject's node document is not fully active, then throw an "InvalidStateError" DOMException.
87        if !subject.owner_document().is_fully_active() {
88            return Err(Error::InvalidState(Some(
89                "Cannot call showModal() on a dialog whose document is not fully active.".into(),
90            )));
91        }
92
93        // Step 4. If subject is not connected, then throw an "InvalidStateError" DOMException.
94        if !subject.is_connected() {
95            return Err(Error::InvalidState(Some(
96                "Cannot call showModal() on a dialog that is not connected.".into(),
97            )));
98        }
99
100        // TODO: Step 5. If subject is in the popover showing state, then throw an "InvalidStateError" DOMException.
101
102        // 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.
103        let event = ToggleEvent::new(
104            cx,
105            &self.owner_window(),
106            atom!("beforetoggle"),
107            EventBubbles::DoesNotBubble,
108            EventCancelable::Cancelable,
109            DOMString::from("closed"),
110            DOMString::from("open"),
111            source.as_deref(),
112        );
113        let event = event.upcast::<Event>();
114        if !event.fire(cx, self.upcast::<EventTarget>()) {
115            return Ok(());
116        }
117
118        // Step 7. If subject has an open attribute, then return.
119        if subject.has_attribute(&local_name!("open")) {
120            return Ok(());
121        }
122
123        // Step 8. If subject is not connected, then return.
124        if !subject.is_connected() {
125            return Ok(());
126        }
127
128        // TODO: Step 9. If subject is in the popover showing state, then return.
129
130        // Step 10. Queue a dialog toggle event task given subject, "closed", "open", and source.
131        self.queue_dialog_toggle_event_task("closed", "open", source);
132
133        // Step 11. Add an open attribute to subject, whose value is the empty string.
134        subject.set_bool_attribute(cx, &local_name!("open"), true);
135        subject.set_open_state(true);
136
137        // TODO: Step 12. Assert: subject's close watcher is not null.
138
139        // Step 13. Set is modal of subject to true.
140        self.upcast::<Element>().set_modal_state(true);
141
142        // TODO: Step 14. Set subject's node document to be blocked by the modal dialog subject.
143
144        // 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.
145
146        // Step 16. Set subject's previously focused element to the focused element.
147        self.upcast::<HTMLElement>().set_previously_focused_element(
148            self.owner_document()
149                .focus_handler()
150                .focused_area()
151                .element(),
152            cx.no_gc(),
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.as_deref(),
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
228            .upcast::<HTMLElement>()
229            .previously_focused_element(cx.no_gc())
230        {
231            // Step 12.1. Let element be subject's previously focused element.
232            // Step 12.2. Set subject's previously focused element to null.
233            self.upcast::<HTMLElement>()
234                .set_previously_focused_element(None, cx.no_gc());
235
236            // Step 12.3. If subject's node document's focused area of the document's DOM anchor is
237            // a shadow-including inclusive descendant of subject, or wasModal is true, then run the
238            // focusing steps for element; the viewport should not be scrolled by doing this step.
239            let subject_node = subject.upcast::<Node>();
240            let document = subject.owner_document();
241            if document
242                .focus_handler()
243                .focused_area()
244                .dom_anchor(&document)
245                .traverse_preorder(ShadowIncluding::Yes)
246                .any(|node| &*node == subject_node) ||
247                was_modal
248            {
249                element.upcast::<Node>().run_the_focusing_steps(cx, None);
250            }
251        }
252
253        // Step 13. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject.
254        let target = self.upcast::<EventTarget>();
255        self.owner_global()
256            .task_manager()
257            .user_interaction_task_source()
258            .queue_simple_event(target, atom!("close"));
259    }
260
261    /// <https://html.spec.whatwg.org/multipage/#queue-a-dialog-toggle-event-task>
262    pub fn queue_dialog_toggle_event_task(
263        &self,
264        old_state: &str,
265        new_state: &str,
266        source: Option<DomRoot<Element>>,
267    ) {
268        // TODO: Step 1. If element's dialog toggle task tracker is not null, then:
269        // TODO: Step 1.1. Set oldState to element's dialog toggle task tracker's old state.
270        // TODO: Step 1.2. Remove element's dialog toggle task tracker's task from its task queue.
271        // TODO: Step 1.3. Set element's dialog toggle task tracker to null.
272        // Step 2. Queue an element task given the DOM manipulation task source and element to run the following steps:
273        let this = Trusted::new(self);
274        let old_state = old_state.to_string();
275        let new_state = new_state.to_string();
276
277        let trusted_source = source.map(|el| Trusted::new(&*el));
278
279        self.owner_global()
280            .task_manager()
281            .dom_manipulation_task_source()
282            .queue(task!(fire_toggle_event: move |cx| {
283                let this = this.root();
284
285                let source = trusted_source.map(|s| s.root());
286
287                // 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.
288                let event = ToggleEvent::new(
289                    cx,
290                    &this.owner_window(),
291                    atom!("toggle"),
292                    EventBubbles::DoesNotBubble,
293                    EventCancelable::NotCancelable,
294                    DOMString::from(old_state),
295                    DOMString::from(new_state),
296                    source.as_deref(),
297                );
298                let event = event.upcast::<Event>();
299                event.fire(cx, this.upcast::<EventTarget>());
300
301                // TODO: Step 2.2. Set element's dialog toggle task tracker to null.
302            }));
303        // 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.
304    }
305
306    /// <https://html.spec.whatwg.org/multipage/#dialog-focusing-steps>
307    fn run_dialog_focusing_steps(&self, cx: &mut JSContext) {
308        // TODO: Step 1. If the allow focus steps given subject's node document return false, then return.
309
310        // Step 2. Let control be null.
311        rooted!(&in(cx) let mut control = None);
312
313        // Step 3. If subject has the autofocus attribute, then set control to subject.
314        if self.upcast::<HTMLElement>().Autofocus() {
315            control.set(self.upcast::<Node>().get_the_focusable_area(cx));
316        }
317
318        // Step 4. If control is null, then set control to the focus delegate of subject.
319        if control.is_none() {
320            control.set(self.upcast::<Node>().focus_delegate(cx));
321        }
322
323        // Step 5. If control is null, then set control to subject.
324        if control.is_none() {
325            control.set(self.upcast::<Node>().get_the_focusable_area(cx));
326        }
327
328        // Step 6. Run the focusing steps for control.
329        // FIXME: Use the focusing step once they support a focusable area as an argument
330        if control.is_some() {
331            let document = self.owner_document();
332            document.focus_handler().focus(cx, &control.take().unwrap());
333        }
334
335        // TODO: Step 7. Let topDocument be control's node navigable's top-level traversable's active document.
336        // TODO: Step 8. If control's node document's origin is not the same as the origin of topDocument, then return.
337        // TODO: Step 9. Empty topDocument's autofocus candidates.
338        // TODO: Step 10. Set topDocument's autofocus processed flag to true.
339    }
340}
341
342impl HTMLDialogElementMethods<crate::DomTypeHolder> for HTMLDialogElement {
343    // https://html.spec.whatwg.org/multipage/#dom-dialog-open
344    make_bool_getter!(Open, "open");
345
346    // https://html.spec.whatwg.org/multipage/#dom-dialog-open
347    make_bool_setter!(SetOpen, "open");
348
349    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-returnvalue>
350    fn ReturnValue(&self) -> DOMString {
351        let return_value = self.return_value.borrow();
352        return_value.clone()
353    }
354
355    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-returnvalue>
356    fn SetReturnValue(&self, _cx: &mut JSContext, return_value: DOMString) {
357        *self.return_value.borrow_mut() = return_value;
358    }
359
360    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-show>
361    fn Show(&self, cx: &mut js::context::JSContext) -> ErrorResult {
362        let element = self.upcast::<Element>();
363        // Step 1. If this has an open attribute and is modal of this is false, then return.
364        if element.has_attribute(&local_name!("open")) &&
365            !element.state().contains(ElementState::MODAL)
366        {
367            return Ok(());
368        }
369
370        // Step 2. If this has an open attribute, then throw an "InvalidStateError" DOMException.
371        if element.has_attribute(&local_name!("open")) {
372            return Err(Error::InvalidState(Some(
373                "Cannot call show() on an already open dialog.".into(),
374            )));
375        }
376
377        // 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.
378        let event = ToggleEvent::new(
379            cx,
380            &self.owner_window(),
381            atom!("beforetoggle"),
382            EventBubbles::DoesNotBubble,
383            EventCancelable::Cancelable,
384            DOMString::from("closed"),
385            DOMString::from("open"),
386            None,
387        );
388        let event = event.upcast::<Event>();
389        if !event.fire(cx, self.upcast::<EventTarget>()) {
390            return Ok(());
391        }
392
393        // Step 4. If this has an open attribute, then return.
394        if element.has_attribute(&local_name!("open")) {
395            return Ok(());
396        }
397
398        // Step 5. Queue a dialog toggle event task given this, "closed", "open", and null.
399        self.queue_dialog_toggle_event_task("closed", "open", None);
400
401        // Step 6. Add an open attribute to this, whose value is the empty string.
402        element.set_bool_attribute(cx, &local_name!("open"), true);
403        element.set_open_state(true);
404
405        // Step 7. Set this's previously focused element to the focused element.
406        self.upcast::<HTMLElement>().set_previously_focused_element(
407            self.owner_document()
408                .focus_handler()
409                .focused_area()
410                .element(),
411            cx.no_gc(),
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}