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