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