Skip to main content

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