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        cx: &mut js::context::JSContext,
49        local_name: LocalName,
50        prefix: Option<Prefix>,
51        document: &Document,
52        proto: Option<HandleObject>,
53    ) -> DomRoot<HTMLDialogElement> {
54        Node::reflect_node_with_proto(
55            cx,
56            Box::new(HTMLDialogElement::new_inherited(
57                local_name, prefix, document,
58            )),
59            document,
60            proto,
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("closed", "open", source);
127
128        // Step 11. Add an open attribute to subject, whose value is the empty string.
129        subject.set_bool_attribute(&local_name!("open"), true, can_gc);
130        subject.set_open_state(true);
131
132        // TODO: Step 12. Assert: subject's close watcher is not null.
133
134        // Step 13. Set is modal of subject to true.
135        self.upcast::<Element>().set_modal_state(true);
136
137        // TODO: Step 14. Set subject's node document to be blocked by the modal dialog subject.
138
139        // 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.
140
141        // TODO: Step 16. Set subject's previously focused element to the focused element.
142
143        // TODO: Step 17. Let document be subject's node document.
144
145        // TODO: Step 18. Let hideUntil be the result of running topmost popover ancestor given subject, document's showing hint popover list, null, and false.
146
147        // 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.
148
149        // TODO: Step 20. If hideUntil is null, then set hideUntil to document.
150
151        // TODO: Step 21. Run hide all popovers until given hideUntil, false, and true.
152
153        // TODO(Issue #32702): Step 22. Run the dialog focusing steps given subject.
154        Ok(())
155    }
156
157    /// <https://html.spec.whatwg.org/multipage/#close-the-dialog>
158    pub fn close_the_dialog(
159        &self,
160        result: Option<DOMString>,
161        source: Option<DomRoot<Element>>,
162        can_gc: CanGc,
163    ) {
164        let subject = self.upcast::<Element>();
165        // Step 1. If subject does not have an open attribute, then return.
166        if !subject.has_attribute(&local_name!("open")) {
167            return;
168        }
169
170        // 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.
171        let event = ToggleEvent::new(
172            &self.owner_window(),
173            atom!("beforetoggle"),
174            EventBubbles::DoesNotBubble,
175            EventCancelable::NotCancelable,
176            DOMString::from("open"),
177            DOMString::from("closed"),
178            source.borrow().clone(),
179            can_gc,
180        );
181        let event = event.upcast::<Event>();
182        event.fire(self.upcast::<EventTarget>(), can_gc);
183
184        // Step 3. If subject does not have an open attribute, then return.
185        if !subject.has_attribute(&local_name!("open")) {
186            return;
187        }
188
189        // Step 4. Queue a dialog toggle event task given subject, "open", "closed", and source.
190        self.queue_dialog_toggle_event_task("open", "closed", source);
191
192        // Step 5. Remove subject's open attribute.
193        subject.remove_attribute(&ns!(), &local_name!("open"), can_gc);
194        subject.set_open_state(false);
195
196        // TODO: Step 6. If is modal of subject is true, then request an element to be removed from the top layer given subject.
197
198        // TODO: Step 7. Let wasModal be the value of subject's is modal flag.
199
200        // Step 8. Set is modal of subject to false.
201        self.upcast::<Element>().set_modal_state(false);
202
203        // Step 9. If result is not null, then set subject's returnValue attribute to result.
204        if let Some(new_value) = result {
205            *self.return_value.borrow_mut() = new_value;
206        }
207
208        // TODO: Step 10. Set subject's request close return value to null.
209
210        // TODO: Step 11. Set subject's request close source element to null.
211
212        // TODO: Step 12. If subject's previously focused element is not null, then:
213        // TODO: Step 12.1. Let element be subject's previously focused element.
214        // TODO: Step 12.2. Set subject's previously focused element to null.
215        // 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.
216
217        // Step 13. Queue an element task on the user interaction task source given the subject element to fire an event named close at subject.
218        let target = self.upcast::<EventTarget>();
219        self.owner_global()
220            .task_manager()
221            .user_interaction_task_source()
222            .queue_simple_event(target, atom!("close"));
223    }
224
225    /// <https://html.spec.whatwg.org/multipage/#queue-a-dialog-toggle-event-task>
226    pub fn queue_dialog_toggle_event_task(
227        &self,
228        old_state: &str,
229        new_state: &str,
230        source: Option<DomRoot<Element>>,
231    ) {
232        // TODO: Step 1. If element's dialog toggle task tracker is not null, then:
233        // TODO: Step 1.1. Set oldState to element's dialog toggle task tracker's old state.
234        // TODO: Step 1.2. Remove element's dialog toggle task tracker's task from its task queue.
235        // TODO: Step 1.3. Set element's dialog toggle task tracker to null.
236        // Step 2. Queue an element task given the DOM manipulation task source and element to run the following steps:
237        let this = Trusted::new(self);
238        let old_state = old_state.to_string();
239        let new_state = new_state.to_string();
240
241        let trusted_source = source
242            .as_ref()
243            .map(|el| Trusted::new(el.upcast::<EventTarget>()));
244
245        self.owner_global()
246            .task_manager()
247            .dom_manipulation_task_source()
248            .queue(task!(fire_toggle_event: move || {
249                let this = this.root();
250
251                let source = trusted_source.as_ref().map(|s| {
252                    DomRoot::from_ref(s.root().downcast::<Element>().unwrap())
253                });
254
255                // 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.
256                let event = ToggleEvent::new(
257                    &this.owner_window(),
258                    atom!("toggle"),
259                    EventBubbles::DoesNotBubble,
260                    EventCancelable::NotCancelable,
261                    DOMString::from(old_state),
262                    DOMString::from(new_state),
263                    source,
264                    CanGc::note(),
265                );
266                let event = event.upcast::<Event>();
267                event.fire(this.upcast::<EventTarget>(), CanGc::note());
268
269                // TODO: Step 2.2. Set element's dialog toggle task tracker to null.
270            }));
271        // 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.
272    }
273}
274
275impl HTMLDialogElementMethods<crate::DomTypeHolder> for HTMLDialogElement {
276    // https://html.spec.whatwg.org/multipage/#dom-dialog-open
277    make_bool_getter!(Open, "open");
278
279    // https://html.spec.whatwg.org/multipage/#dom-dialog-open
280    make_bool_setter!(SetOpen, "open");
281
282    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-returnvalue>
283    fn ReturnValue(&self) -> DOMString {
284        let return_value = self.return_value.borrow();
285        return_value.clone()
286    }
287
288    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-returnvalue>
289    fn SetReturnValue(&self, return_value: DOMString) {
290        *self.return_value.borrow_mut() = return_value;
291    }
292
293    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-show>
294    fn Show(&self, can_gc: CanGc) -> ErrorResult {
295        let element = self.upcast::<Element>();
296        // Step 1. If this has an open attribute and is modal of this is false, then return.
297        if element.has_attribute(&local_name!("open")) &&
298            !element.state().contains(ElementState::MODAL)
299        {
300            return Ok(());
301        }
302
303        // Step 2. If this has an open attribute, then throw an "InvalidStateError" DOMException.
304        if element.has_attribute(&local_name!("open")) {
305            return Err(Error::InvalidState(Some(
306                "Cannot call show() on an already open dialog.".into(),
307            )));
308        }
309
310        // 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.
311        let event = ToggleEvent::new(
312            &self.owner_window(),
313            atom!("beforetoggle"),
314            EventBubbles::DoesNotBubble,
315            EventCancelable::Cancelable,
316            DOMString::from("closed"),
317            DOMString::from("open"),
318            None,
319            can_gc,
320        );
321        let event = event.upcast::<Event>();
322        if !event.fire(self.upcast::<EventTarget>(), can_gc) {
323            return Ok(());
324        }
325
326        // Step 4. If this has an open attribute, then return.
327        if element.has_attribute(&local_name!("open")) {
328            return Ok(());
329        }
330
331        // Step 5. Queue a dialog toggle event task given this, "closed", "open", and null.
332        self.queue_dialog_toggle_event_task("closed", "open", None);
333
334        // Step 6. Add an open attribute to this, whose value is the empty string.
335        element.set_bool_attribute(&local_name!("open"), true, can_gc);
336        element.set_open_state(true);
337
338        // TODO: Step 7. Set this's previously focused element to the focused element.
339
340        // TODO: Step 8. Let document be this's node document.
341
342        // TODO: Step 9. Let hideUntil be the result of running topmost popover ancestor given this, document's showing hint popover list, null, and false.
343
344        // 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.
345
346        // TODO: Step 11. If hideUntil is null, then set hideUntil to document.
347
348        // TODO: Step 12. Run hide all popovers until given hideUntil, false, and true.
349
350        // TODO(Issue #32702): Step 13. Run the dialog focusing steps given this.
351
352        Ok(())
353    }
354
355    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-showmodal>
356    fn ShowModal(&self, can_gc: CanGc) -> ErrorResult {
357        // The showModal() method steps are to show a modal dialog given this and null.
358        self.show_a_modal(None, can_gc)
359    }
360
361    /// <https://html.spec.whatwg.org/multipage/#dom-dialog-close>
362    fn Close(&self, return_value: Option<DOMString>, can_gc: CanGc) {
363        // Step 1. If returnValue is not given, then set it to null.
364        // Step 2. Close the dialog this with returnValue and null.
365        self.close_the_dialog(return_value, None, can_gc);
366    }
367}
368
369impl VirtualMethods for HTMLDialogElement {
370    fn super_type(&self) -> Option<&dyn VirtualMethods> {
371        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
372    }
373
374    /// <https://html.spec.whatwg.org/multipage/#the-dialog-element:is-valid-command-steps>
375    fn is_valid_command_steps(&self, command: CommandState) -> bool {
376        // Step 1. If command is in the Close state, the Request Close state (TODO), or the
377        // ShowModal state, then return true.
378        if command == CommandState::Close || command == CommandState::ShowModal {
379            return true;
380        }
381        // Step 2. Return false.
382        false
383    }
384
385    /// <https://html.spec.whatwg.org/multipage/#the-dialog-element:command-steps>
386    fn command_steps(
387        &self,
388        source: DomRoot<HTMLButtonElement>,
389        command: CommandState,
390        can_gc: CanGc,
391    ) -> bool {
392        if self
393            .super_type()
394            .unwrap()
395            .command_steps(source.clone(), command, can_gc)
396        {
397            return true;
398        }
399
400        // TODO Step 1. If element is in the popover showing state, then return.
401        let element = self.upcast::<Element>();
402
403        // Step 2. If command is in the Close state and element has an open attribute, then
404        // close the dialog element with source's optional value and source.
405        if command == CommandState::Close && element.has_attribute(&local_name!("open")) {
406            let button_element = DomRoot::from_ref(source.upcast::<Element>());
407            self.close_the_dialog(source.optional_value(), Some(button_element), can_gc);
408            return true;
409        }
410
411        // TODO Step 3. If command is in the Request Close state and element has an open attribute,
412        // then request to close the dialog element with source's optional value and source.
413
414        // Step 4. If command is the Show Modal state and element does not have an open attribute,
415        // then show a modal dialog given element and source.
416        if command == CommandState::ShowModal && !element.has_attribute(&local_name!("open")) {
417            let button_element = DomRoot::from_ref(source.upcast::<Element>());
418            let _ = self.show_a_modal(Some(button_element), can_gc);
419            return true;
420        }
421
422        false
423    }
424}