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;
5use std::cell::Cell;
6
7use dom_struct::dom_struct;
8use html5ever::{LocalName, Prefix, local_name, ns};
9use js::rust::HandleObject;
10use script_bindings::error::{Error, ErrorResult};
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::node::{Node, NodeTraits};
24use crate::dom::toggleevent::ToggleEvent;
25use crate::script_runtime::CanGc;
26
27#[dom_struct]
28pub(crate) struct HTMLDialogElement {
29 htmlelement: HTMLElement,
30 return_value: DomRefCell<DOMString>,
31 is_modal: Cell<bool>,
32}
33
34impl HTMLDialogElement {
35 fn new_inherited(
36 local_name: LocalName,
37 prefix: Option<Prefix>,
38 document: &Document,
39 ) -> HTMLDialogElement {
40 HTMLDialogElement {
41 htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
42 return_value: DomRefCell::new(DOMString::new()),
43 is_modal: Cell::new(false),
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")) && self.is_modal.get() {
69 return Ok(());
70 }
71
72 // Step 2. If subject has an open attribute, then throw an "InvalidStateError" DOMException.
73 if subject.has_attribute(&local_name!("open")) {
74 return Err(Error::InvalidState(None));
75 }
76
77 // Step 3. If subject's node document is not fully active, then throw an "InvalidStateError" DOMException.
78 if !subject.owner_document().is_fully_active() {
79 return Err(Error::InvalidState(None));
80 }
81
82 // Step 4. If subject is not connected, then throw an "InvalidStateError" DOMException.
83 if !subject.is_connected() {
84 return Err(Error::InvalidState(None));
85 }
86
87 // TODO: Step 5. If subject is in the popover showing state, then throw an "InvalidStateError" DOMException.
88
89 // 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.
90 let event = ToggleEvent::new(
91 &self.owner_window(),
92 atom!("beforetoggle"),
93 EventBubbles::DoesNotBubble,
94 EventCancelable::Cancelable,
95 DOMString::from("closed"),
96 DOMString::from("open"),
97 source.borrow().clone(),
98 can_gc,
99 );
100 let event = event.upcast::<Event>();
101 if !event.fire(self.upcast::<EventTarget>(), can_gc) {
102 return Ok(());
103 }
104
105 // Step 7. If subject has an open attribute, then return.
106 if subject.has_attribute(&local_name!("open")) {
107 return Ok(());
108 }
109
110 // Step 8. If subject is not connected, then return.
111 if !subject.is_connected() {
112 return Ok(());
113 }
114
115 // TODO: Step 9. If subject is in the popover showing state, then return.
116
117 // Step 10. Queue a dialog toggle event task given subject, "closed", "open", and source.
118 self.queue_dialog_toggle_event_task(
119 DOMString::from("closed"),
120 DOMString::from("open"),
121 source,
122 );
123
124 // Step 11. Add an open attribute to subject, whose value is the empty string.
125 subject.set_bool_attribute(&local_name!("open"), true, can_gc);
126 subject.set_open_state(true);
127
128 // TODO: Step 12. Assert: subject's close watcher is not null.
129
130 // Step 13. Set is modal of subject to true.
131 self.is_modal.set(true);
132
133 // TODO: Step 14. Set subject's node document to be blocked by the modal dialog subject.
134
135 // 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.
136
137 // TODO: Step 16. Set subject's previously focused element to the focused element.
138
139 // TODO: Step 17. Let document be subject's node document.
140
141 // TODO: Step 18. Let hideUntil be the result of running topmost popover ancestor given subject, document's showing hint popover list, null, and false.
142
143 // 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.
144
145 // TODO: Step 20. If hideUntil is null, then set hideUntil to document.
146
147 // TODO: Step 21. Run hide all popovers until given hideUntil, false, and true.
148
149 // TODO(Issue #32702): Step 22. Run the dialog focusing steps given subject.
150 Ok(())
151 }
152
153 /// <https://html.spec.whatwg.org/multipage/#close-the-dialog>
154 pub fn close_the_dialog(
155 &self,
156 result: Option<DOMString>,
157 source: Option<DomRoot<Element>>,
158 can_gc: CanGc,
159 ) {
160 let subject = self.upcast::<Element>();
161 // Step 1. If subject does not have an open attribute, then return.
162 if !subject.has_attribute(&local_name!("open")) {
163 return;
164 }
165
166 // 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.
167 let event = ToggleEvent::new(
168 &self.owner_window(),
169 atom!("beforetoggle"),
170 EventBubbles::DoesNotBubble,
171 EventCancelable::NotCancelable,
172 DOMString::from("open"),
173 DOMString::from("closed"),
174 source.borrow().clone(),
175 can_gc,
176 );
177 let event = event.upcast::<Event>();
178 event.fire(self.upcast::<EventTarget>(), can_gc);
179
180 // Step 3. If subject does not have an open attribute, then return.
181 if !subject.has_attribute(&local_name!("open")) {
182 return;
183 }
184
185 // Step 4. Queue a dialog toggle event task given subject, "open", "closed", and source.
186 self.queue_dialog_toggle_event_task(
187 DOMString::from("open"),
188 DOMString::from("closed"),
189 source,
190 );
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.is_modal.set(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: DOMString,
229 new_state: DOMString,
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")) && !self.is_modal.get() {
298 return Ok(());
299 }
300
301 // Step 2. If this has an open attribute, then throw an "InvalidStateError" DOMException.
302 if element.has_attribute(&local_name!("open")) {
303 return Err(Error::InvalidState(None));
304 }
305
306 // 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.
307 let event = ToggleEvent::new(
308 &self.owner_window(),
309 atom!("beforetoggle"),
310 EventBubbles::DoesNotBubble,
311 EventCancelable::Cancelable,
312 DOMString::from("closed"),
313 DOMString::from("open"),
314 None,
315 can_gc,
316 );
317 let event = event.upcast::<Event>();
318 if !event.fire(self.upcast::<EventTarget>(), can_gc) {
319 return Ok(());
320 }
321
322 // Step 4. If this has an open attribute, then return.
323 if element.has_attribute(&local_name!("open")) {
324 return Ok(());
325 }
326
327 // Step 5. Queue a dialog toggle event task given this, "closed", "open", and null.
328 self.queue_dialog_toggle_event_task(
329 DOMString::from("closed"),
330 DOMString::from("open"),
331 None,
332 );
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}