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