script/dom/html/form_controls/
htmloptionelement.rs1use std::cell::Cell;
6use std::convert::TryInto;
7
8use dom_struct::dom_struct;
9use html5ever::{LocalName, Prefix, QualName, local_name, ns};
10use js::context::{JSContext, NoGC};
11use js::rust::HandleObject;
12use script_bindings::dom::{MutNullableDom, UnrootedDom};
13use script_bindings::root::Dom;
14use style::str::{split_html_space_chars, str_join};
15use stylo_dom::ElementState;
16
17use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
18use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
19use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElement_Binding::HTMLSelectElementMethods;
20use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
21use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
22use crate::dom::bindings::error::Fallible;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::reflector::DomGlobal;
25use crate::dom::bindings::root::DomRoot;
26use crate::dom::bindings::str::DOMString;
27use crate::dom::characterdata::CharacterData;
28use crate::dom::document::Document;
29use crate::dom::element::attributes::storage::AttrRef;
30use crate::dom::element::{AttributeMutation, CustomElementCreationMode, Element, ElementCreator};
31use crate::dom::html::htmlelement::HTMLElement;
32use crate::dom::html::htmlformelement::HTMLFormElement;
33use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement;
34use crate::dom::html::htmlscriptelement::HTMLScriptElement;
35use crate::dom::html::htmlselectelement::HTMLSelectElement;
36use crate::dom::iterators::ShadowIncluding;
37use crate::dom::node::virtualmethods::VirtualMethods;
38use crate::dom::node::{
39 BindContext, ChildrenMutation, CloneChildrenFlag, MoveContext, Node, NodeTraits, UnbindContext,
40};
41use crate::dom::text::Text;
42use crate::dom::types::DocumentFragment;
43use crate::dom::validation::Validatable;
44use crate::dom::validitystate::ValidationFlags;
45use crate::dom::window::Window;
46use crate::realms::enter_auto_realm;
47use crate::runtime::job_queue::MicrotaskRunnable;
48
49#[dom_struct]
50pub(crate) struct HTMLOptionElement {
51 htmlelement: HTMLElement,
52
53 selectedness: Cell<bool>,
55
56 dirtiness: Cell<bool>,
58
59 cached_nearest_ancestor_select: MutNullableDom<HTMLSelectElement>,
61}
62
63impl HTMLOptionElement {
64 fn new_inherited(
65 local_name: LocalName,
66 prefix: Option<Prefix>,
67 document: &Document,
68 ) -> HTMLOptionElement {
69 HTMLOptionElement {
70 htmlelement: HTMLElement::new_inherited_with_state(
71 ElementState::ENABLED,
72 local_name,
73 prefix,
74 document,
75 ),
76 selectedness: Cell::new(false),
77 dirtiness: Cell::new(false),
78 cached_nearest_ancestor_select: MutNullableDom::default(),
79 }
80 }
81
82 pub(crate) fn new(
83 cx: &mut js::context::JSContext,
84 local_name: LocalName,
85 prefix: Option<Prefix>,
86 document: &Document,
87 proto: Option<HandleObject>,
88 ) -> DomRoot<HTMLOptionElement> {
89 Node::reflect_node_with_proto(
90 cx,
91 Box::new(HTMLOptionElement::new_inherited(
92 local_name, prefix, document,
93 )),
94 document,
95 proto,
96 )
97 }
98
99 pub(crate) fn set_selectedness(&self, no_gc: &NoGC, selected: bool) -> bool {
104 if selected == self.selectedness.get() {
105 return false;
106 }
107
108 self.selectedness.set(selected);
109 self.upcast::<Element>()
110 .set_state(no_gc, ElementState::CHECKED, selected);
111 self.upcast::<Node>().rev_version(no_gc);
114 true
115 }
116
117 pub(crate) fn set_dirtiness(&self, dirtiness: bool) {
118 self.dirtiness.set(dirtiness);
119 }
120
121 fn pick_if_selected_in_select(&self, cx: &mut JSContext, select: &HTMLSelectElement) {
129 if !self.Selected() {
130 return;
131 }
132
133 debug_assert!(
134 select
135 .list_of_options(cx.no_gc())
136 .any(|option| *option == self)
137 );
138 select.pick_option(cx.no_gc(), self);
139
140 select
143 .validity_state(cx)
144 .perform_validation_and_update(cx, ValidationFlags::all());
145 select.update_shadow_tree(cx);
146 }
147
148 fn pick_if_selected_in_cached_select(&self, cx: &mut JSContext) {
149 let select = self.cached_nearest_ancestor_select.get();
150 debug_assert!(select == self.nearest_ancestor_select());
151 if let Some(select) = select {
152 self.pick_if_selected_in_select(cx, &select);
153 }
154 }
155
156 fn index(&self, no_gc: &NoGC) -> i32 {
158 let Some(owner_select) = self.cached_nearest_ancestor_select() else {
159 return 0;
160 };
161
162 let Some(position) = owner_select.list_of_options(no_gc).position(|n| *n == self) else {
163 warn!("HTMLOptionElement called index_in_select at a select that did not contain it");
165 return 0;
166 };
167
168 position.try_into().unwrap_or(0)
169 }
170
171 fn update_select_validity(&self, cx: &mut JSContext) {
172 if let Some(select) = self.cached_nearest_ancestor_select() {
173 select
174 .validity_state(cx)
175 .perform_validation_and_update(cx, ValidationFlags::all());
176 }
177 }
178
179 pub(crate) fn displayed_label(&self) -> DOMString {
183 let label = self
186 .upcast::<Element>()
187 .get_string_attribute(&local_name!("label"));
188
189 if label.is_empty() {
190 return self.Text();
191 }
192
193 label
194 }
195
196 pub(crate) fn cached_nearest_ancestor_select(&self) -> Option<DomRoot<HTMLSelectElement>> {
198 self.cached_nearest_ancestor_select.get()
199 }
200
201 pub(crate) fn nearest_ancestor_select(&self) -> Option<DomRoot<HTMLSelectElement>> {
203 let mut did_see_ancestor_optgroup = false;
206
207 for ancestor in self
209 .upcast::<Node>()
210 .ancestors()
211 .filter_map(DomRoot::downcast::<Element>)
212 {
213 if matches!(
215 ancestor.local_name(),
216 &local_name!("datalist") | &local_name!("hr") | &local_name!("option")
217 ) {
218 return None;
219 }
220
221 if ancestor.local_name() == &local_name!("optgroup") {
223 if did_see_ancestor_optgroup {
225 return None;
226 }
227
228 did_see_ancestor_optgroup = true;
230 }
231
232 if let Some(select) = DomRoot::downcast::<HTMLSelectElement>(ancestor) {
234 return Some(select);
235 }
236 }
237
238 None
240 }
241
242 pub(crate) fn maybe_clone_an_option_into_selectedcontent(&self, cx: &mut JSContext) {
244 let select = self.nearest_ancestor_select();
246
247 if self.selectedness.get() &&
253 let Some(selectedcontent) =
254 select.and_then(|select| select.get_enabled_selectedcontent())
255 {
256 self.clone_an_option_into_selectedcontent(cx, &selectedcontent);
257 }
258 }
259
260 pub(crate) fn clone_an_option_into_selectedcontent(
262 &self,
263 cx: &mut JSContext,
264 selectedcontent: &Element,
265 ) {
266 let document_fragment = DocumentFragment::new(cx, &self.owner_document());
268
269 for child in self.upcast::<Node>().children() {
271 let child_clone = Node::clone(cx, &child, None, CloneChildrenFlag::CloneChildren, None);
273
274 let _ = document_fragment
276 .upcast::<Node>()
277 .AppendChild(cx, &child_clone);
278 }
279
280 Node::replace_all(
282 cx,
283 Some(document_fragment.upcast()),
284 selectedcontent.upcast(),
285 );
286 }
287
288 fn update_nearest_ancestor_selection(
290 &self,
291 cx: &mut JSContext,
292 ) -> Vec<DomRoot<HTMLSelectElement>> {
293 let old_select = self.cached_nearest_ancestor_select.get();
295
296 let new_select = self.nearest_ancestor_select();
298
299 self.cached_nearest_ancestor_select
301 .set(new_select.as_deref());
302
303 if old_select == new_select {
305 return Vec::new();
306 }
307
308 let mut result = Vec::new();
310
311 for select in [old_select, new_select] {
313 let Some(select) = select else {
315 continue;
316 };
317
318 let has_changed = select.selectedness_setting_algorithm(cx.no_gc());
321
322 select
325 .validity_state(cx)
326 .perform_validation_and_update(cx, ValidationFlags::all());
327 select.update_shadow_tree(cx);
328
329 if self.Selected() || has_changed {
332 result.push(select);
333 }
334 }
335
336 result
338 }
339
340 fn ask_for_a_reset(&self, cx: &mut JSContext) {
342 let select = self.cached_nearest_ancestor_select.get();
344 let Some(select) = select else {
346 return;
347 };
348 select.selectedness_setting_algorithm(cx.no_gc());
350 select.update_a_selects_descendant_selectedcontent_elements(cx);
352
353 select
356 .validity_state(cx)
357 .perform_validation_and_update(cx, ValidationFlags::all());
358 select.update_shadow_tree(cx);
359 }
360
361 pub(crate) fn update_disabled_state(&self, no_gc: &NoGC) {
365 let element = self.upcast::<Element>();
366 let disabled = element.has_attribute(&local_name!("disabled")) ||
367 self.is_disabled_by_parent_optgroup(no_gc);
368 element.set_disabled_state(no_gc, disabled);
369 element.set_enabled_state(no_gc, !disabled);
370 }
371
372 fn is_disabled_by_parent_optgroup(&self, no_gc: &NoGC) -> bool {
373 self.upcast::<Node>()
374 .get_parent_node_unrooted(no_gc)
375 .and_then(UnrootedDom::downcast::<HTMLOptGroupElement>)
376 .is_some_and(|optgroup| optgroup.upcast::<Element>().disabled_state())
377 }
378}
379
380impl HTMLOptionElementMethods<crate::DomTypeHolder> for HTMLOptionElement {
381 fn Option(
383 cx: &mut JSContext,
384 window: &Window,
385 proto: Option<HandleObject>,
386 text: DOMString,
387 value: Option<DOMString>,
388 default_selected: bool,
389 selected: bool,
390 ) -> Fallible<DomRoot<HTMLOptionElement>> {
391 let element = Element::create(
392 cx,
393 QualName::new(None, ns!(html), local_name!("option")),
394 None,
395 &window.Document(),
396 ElementCreator::ScriptCreated,
397 CustomElementCreationMode::Synchronous,
398 proto,
399 );
400
401 let option = DomRoot::downcast::<HTMLOptionElement>(element).unwrap();
402
403 if !text.is_empty() {
404 option
405 .upcast::<Node>()
406 .set_text_content_for_element(cx, Some(text))
407 }
408
409 if let Some(val) = value {
410 option.SetValue(cx, val)
411 }
412
413 option.SetDefaultSelected(cx, default_selected);
414 option.set_selectedness(cx, selected);
415 option.update_select_validity(cx);
416 Ok(option)
417 }
418
419 make_bool_getter!(Disabled, "disabled");
421
422 make_bool_setter!(SetDisabled, "disabled");
424
425 fn Text(&self) -> DOMString {
427 let mut content = DOMString::new();
428
429 let mut iterator = self.upcast::<Node>().traverse_preorder(ShadowIncluding::No);
430 while let Some(node) = iterator.peek() {
431 if let Some(element) = node.downcast::<Element>() {
432 let html_script = element.is::<HTMLScriptElement>();
433 let svg_script = *element.namespace() == ns!(svg) &&
434 element.local_name() == &local_name!("script");
435 if html_script || svg_script {
436 iterator.next_skipping_children();
437 continue;
438 }
439 }
440
441 if node.is::<Text>() {
442 let characterdata = node.downcast::<CharacterData>().unwrap();
443 content.push_str(&characterdata.Data().str());
444 }
445
446 iterator.next();
447 }
448
449 DOMString::from(str_join(split_html_space_chars(&content.str()), " "))
450 }
451
452 fn SetText(&self, cx: &mut JSContext, value: DOMString) {
454 self.upcast::<Node>()
455 .set_text_content_for_element(cx, Some(value))
456 }
457
458 fn GetForm(&self) -> Option<DomRoot<HTMLFormElement>> {
460 self.cached_nearest_ancestor_select()
461 .and_then(|s| s.GetForm())
462 }
463
464 fn Value(&self) -> DOMString {
466 let element = self.upcast::<Element>();
467 let attr = &local_name!("value");
468 if element.has_attribute(attr) {
469 element.get_string_attribute(attr)
470 } else {
471 self.Text()
472 }
473 }
474
475 make_setter!(SetValue, "value");
477
478 fn Label(&self) -> DOMString {
480 let element = self.upcast::<Element>();
481 let attr = &local_name!("label");
482 if element.has_attribute(attr) {
483 element.get_string_attribute(attr)
484 } else {
485 self.Text()
486 }
487 }
488
489 make_setter!(SetLabel, "label");
491
492 make_bool_getter!(DefaultSelected, "selected");
494
495 make_bool_setter!(SetDefaultSelected, "selected");
497
498 fn Selected(&self) -> bool {
500 self.selectedness.get()
501 }
502
503 fn SetSelected(&self, cx: &mut JSContext, selected: bool) {
505 if self.set_selectedness(cx, selected) {
507 self.pick_if_selected_in_cached_select(cx);
508 }
509 self.dirtiness.set(true);
511 self.ask_for_a_reset(cx);
513 }
514
515 fn Index(&self, cx: &JSContext) -> i32 {
517 self.index(cx.no_gc())
518 }
519}
520
521impl VirtualMethods for HTMLOptionElement {
522 fn super_type(&self) -> Option<&dyn VirtualMethods> {
523 Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
524 }
525
526 fn attribute_mutated(
527 &self,
528 cx: &mut js::context::JSContext,
529 attr: AttrRef<'_>,
530 mutation: AttributeMutation,
531 ) {
532 self.super_type()
533 .unwrap()
534 .attribute_mutated(cx, attr, mutation);
535 match *attr.local_name() {
536 local_name!("disabled") => {
537 self.update_disabled_state(cx.no_gc());
538 self.update_select_validity(cx);
539 },
540 local_name!("selected") => {
541 let mut selectedness_changed = false;
542 match mutation {
543 AttributeMutation::Set(_) => {
544 if !self.dirtiness.get() && !self.selectedness.get() {
546 self.set_selectedness(cx.no_gc(), true);
547 selectedness_changed = true;
548 }
549 },
550 AttributeMutation::Removed => {
551 if !self.dirtiness.get() && self.selectedness.get() {
553 self.set_selectedness(cx.no_gc(), false);
554 selectedness_changed = true;
555 }
556 },
557 }
558
559 if selectedness_changed {
560 self.pick_if_selected_in_cached_select(cx);
561 self.ask_for_a_reset(cx);
562 }
563 self.update_select_validity(cx);
564 },
565 local_name!("label") => {
566 if let Some(select_element) = self.cached_nearest_ancestor_select() {
569 select_element.update_shadow_tree(cx);
570 }
571 },
572 _ => {},
573 }
574 }
575
576 fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
577 if let Some(super_type) = self.super_type() {
578 super_type.bind_to_tree(cx, context);
579 }
580
581 self.update_disabled_state(cx.no_gc());
582
583 if let Some(select) = self.nearest_ancestor_select() {
588 self.pick_if_selected_in_select(cx, &select);
589 }
590
591 self.update_nearest_ancestor_selection(cx);
592 self.update_select_validity(cx);
593 }
594
595 fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) {
596 self.super_type().unwrap().unbind_from_tree(cx, context);
597
598 let selects = self.update_nearest_ancestor_selection(cx);
601
602 for select in selects {
605 self.global().enqueue_microtask(
606 cx,
607 Box::new(UpdateSelectedContentMicrotask {
608 select: select.as_traced(),
609 }),
610 );
611 }
612
613 self.update_disabled_state(cx.no_gc());
614 }
615
616 fn children_changed(&self, cx: &mut JSContext, mutation: &ChildrenMutation) {
617 if let Some(super_type) = self.super_type() {
618 super_type.children_changed(cx, mutation);
619 }
620
621 if !self
624 .upcast::<Element>()
625 .has_attribute(&local_name!("label")) &&
626 let Some(owner_select) = self.cached_nearest_ancestor_select() &&
627 owner_select
628 .selected_option(cx.no_gc())
629 .is_some_and(|selected_option| *self == **selected_option)
630 {
631 owner_select.update_shadow_tree(cx);
632 }
633 }
634
635 fn moving_steps(&self, cx: &mut JSContext, context: &MoveContext) {
637 if let Some(super_type) = self.super_type() {
638 super_type.moving_steps(cx, context);
639 }
640
641 self.update_disabled_state(cx.no_gc());
642
643 if let Some(select) = self.nearest_ancestor_select() {
648 self.pick_if_selected_in_select(cx, &select);
649 }
650
651 let selects = self.update_nearest_ancestor_selection(cx);
654
655 for select in selects {
658 self.global().enqueue_microtask(
659 cx,
660 Box::new(UpdateSelectedContentMicrotask {
661 select: select.as_traced(),
662 }),
663 );
664 }
665 }
666}
667
668#[derive(JSTraceable, MallocSizeOf)]
669#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
670struct UpdateSelectedContentMicrotask {
671 select: Dom<HTMLSelectElement>,
672}
673
674impl MicrotaskRunnable for UpdateSelectedContentMicrotask {
675 fn handler(&self, cx: &mut JSContext) {
676 let mut realm = enter_auto_realm(cx, &*self.select);
677 let cx = &mut realm;
678 self.select
679 .update_a_selects_descendant_selectedcontent_elements(cx);
680 }
681}