1use super::*;
5use crate::os_lib::js::builtins::this_dom_idx;
6
7impl Interp {
8 pub(crate) fn dom_get_property(&mut self, disp: DomDisp, key: &str) -> Value {
9 match disp {
10 DomDisp::Element(idx) => self.dom_element_get(idx, key),
11 DomDisp::Host(tag) => {
12 if let Some(rest) = tag.strip_prefix("style:") {
13 match key {
14 "setProperty" => {
15 return Value::Object(Obj::native(
16 "style.setProperty",
17 super::super::builtins::dom_style_set_property,
18 ))
19 }
20 "getPropertyValue" => {
21 return Value::Object(Obj::native(
22 "style.getPropertyValue",
23 super::super::builtins::dom_style_get_property_value,
24 ))
25 }
26 "removeProperty" => {
27 return Value::Object(Obj::native(
28 "style.removeProperty",
29 super::super::builtins::dom_style_remove_property,
30 ))
31 }
32 "getPropertyPriority" => {
35 return Value::Object(Obj::native(
36 "style.getPropertyPriority",
37 super::super::builtins::dom_style_get_property_priority,
38 ))
39 }
40 _ => {}
41 }
42 let idx: usize = rest.parse().unwrap_or(usize::MAX);
43 if key == "cssText" {
47 return Value::str(self.dom.borrow().get_css_text(idx));
48 }
49 let style_names = || {
56 self.dom
57 .borrow()
58 .get_css_text(idx)
59 .split(';')
60 .filter_map(|d| d.split_once(':').map(|(k, _)| k.trim().to_string()))
61 .filter(|k| !k.is_empty())
62 .collect::<alloc::vec::Vec<_>>()
63 };
64 if key == "length" {
65 return Value::Number(style_names().len() as f64);
66 }
67 if key == "item" {
68 return Value::Object(Obj::native("item", super::super::builtins::dom_style_item));
69 }
70 let key = if key == "cssFloat" { "float" } else { key };
74 return Value::str(self.dom.borrow().get_style(idx, key));
75 }
76 if let Some(rest) = tag.strip_prefix("stylesheet:") {
77 let idx: usize = rest.parse().unwrap_or(usize::MAX);
78 match key {
79 "ownerNode" => return Value::Object(Obj::dom(idx)),
80 "insertRule" => {
81 return Value::Object(Obj::native(
82 "StyleSheet.insertRule",
83 super::super::builtins::dom_stylesheet_insert_rule,
84 ))
85 }
86 "deleteRule" => {
87 return Value::Object(Obj::native(
88 "StyleSheet.deleteRule",
89 super::super::builtins::dom_stylesheet_delete_rule,
90 ))
91 }
92 "cssRules" => {
93 let text = self.dom.borrow().get_text_content(idx);
94 let rules = super::super::builtins::split_css_rules(&text);
95 let rule_objs: alloc::vec::Vec<Value> = rules
96 .into_iter()
97 .enumerate()
98 .map(|(r_idx, rule_text)| {
99 let r_obj = Obj::host(&format!("cssrule:{}:{}", idx, r_idx));
100 r_obj.borrow_mut().props.insert("cssText".into(), Value::str(rule_text.clone()));
101 let selector = rule_text.split('{').next().unwrap_or("").trim().to_string();
102 r_obj.borrow_mut().props.insert("selectorText".into(), Value::str(selector));
103 Value::Object(r_obj)
104 })
105 .collect();
106 return Value::Object(Obj::array(rule_objs));
107 }
108 _ => {}
109 }
110 }
111 if let Some(rest) = tag.strip_prefix("classList:") {
112 let cidx: usize = rest.parse().unwrap_or(usize::MAX);
113 return match key {
115 "add" => Value::Object(Obj::native(
116 "classList.add",
117 super::super::builtins::dom_classlist_add,
118 )),
119 "remove" => Value::Object(Obj::native(
120 "classList.remove",
121 super::super::builtins::dom_classlist_remove,
122 )),
123 "toggle" => Value::Object(Obj::native(
124 "classList.toggle",
125 super::super::builtins::dom_classlist_toggle,
126 )),
127 "contains" => Value::Object(Obj::native(
128 "classList.contains",
129 super::super::builtins::dom_classlist_contains,
130 )),
131 "replace" => Value::Object(Obj::native(
132 "classList.replace",
133 super::super::builtins::dom_classlist_replace,
134 )),
135 "item" => Value::Object(Obj::native(
136 "classList.item",
137 super::super::builtins::dom_classlist_item,
138 )),
139 "toString" => Value::Object(Obj::native(
140 "classList.toString",
141 super::super::builtins::dom_classlist_to_string,
142 )),
143 "forEach" => Value::Object(Obj::native(
144 "classList.forEach",
145 super::super::builtins::dom_classlist_for_each,
146 )),
147 "entries" => Value::Object(Obj::native(
148 "classList.entries",
149 super::super::builtins::dom_classlist_entries,
150 )),
151 "keys" => Value::Object(Obj::native(
152 "classList.keys",
153 super::super::builtins::dom_classlist_keys,
154 )),
155 "values" => Value::Object(Obj::native(
156 "classList.values",
157 super::super::builtins::dom_classlist_values,
158 )),
159 "supports" => Value::Object(Obj::native(
160 "classList.supports",
161 super::super::builtins::dom_classlist_supports,
162 )),
163 "length" => Value::Number(
164 self.dom
165 .borrow()
166 .nodes
167 .get(cidx)
168 .map(|n| n.classes.len())
169 .unwrap_or(0) as f64,
170 ),
171 "value" => Value::str(
172 self.dom
173 .borrow()
174 .nodes
175 .get(cidx)
176 .map(|n| n.classes.join(" "))
177 .unwrap_or_default(),
178 ),
179 _ => Value::Undefined,
180 };
181 }
182 if let Some(rest) = tag.strip_prefix("dataset:") {
183 let idx: usize = rest.parse().unwrap_or(usize::MAX);
184 let attr = camel_to_data_attr(key);
185 return match self.dom.borrow().get_attr(idx, &attr) {
186 Some(v) => Value::str(v),
187 None => Value::Undefined,
188 };
189 }
190 if let Some(rest) = tag.strip_prefix("computed:") {
191 if key == "getPropertyValue" {
192 return Value::Object(Obj::native(
193 "getPropertyValue",
194 super::super::builtins::computed_get_property_value,
195 ));
196 }
197 let idx: usize = rest.parse().unwrap_or(usize::MAX);
198 let prop = camel_to_kebab(key);
200 return Value::str(self.dom.borrow().get_computed(idx, &prop));
201 }
202 Value::Undefined
203 }
204 }
205 }
206
207 pub(crate) fn dom_builtin_get(&mut self, idx: usize, key: &str) -> Value {
210 self.dom_builtin_lookup = true;
211 let v = self.dom_element_get(idx, key);
212 self.dom_builtin_lookup = false;
213 v
214 }
215
216 pub(crate) fn dom_builtin_set(&mut self, idx: usize, key: &str, val: Value) {
218 self.dom_set_property(DomDisp::Element(idx), key, val);
219 self.process_ce_reactions();
220 }
221
222 fn dom_proto_override(&mut self, idx: usize, key: &str) -> Option<Value> {
227 let mut cur = self.get_dom_node_proto(idx);
228 let mut guard = 0;
229 let is_native = |v: &Value| matches!(v, Value::Object(o) if matches!(o.borrow().kind, ObjKind::Native { .. }));
230 while let Some(p) = cur {
231 let acc = p.borrow().accessors.get(key).map(|a| a.get.clone());
232 if let Some(g) = acc {
233 return match g {
234 Some(g) if !is_native(&g) => {
235 Some(self.call_value(&g, Value::Object(Obj::dom(idx)), &[]).unwrap_or(Value::Undefined))
236 }
237 _ => None,
238 };
239 }
240 if let Some(v) = p.borrow().props.get(key).cloned() {
241 return if is_native(&v) || !matches!(v, Value::Object(_)) { None } else { Some(v) };
242 }
243 guard += 1;
244 if guard > 64 {
245 break;
246 }
247 cur = p.borrow().proto.clone();
248 }
249 None
250 }
251
252 fn ce_chain_get(&mut self, idx: usize, key: &str) -> Option<Value> {
254 let mut cur = self.dom.borrow().node_protos.get(&idx).cloned();
255 let mut guard = 0;
256 while let Some(p) = cur {
257 let getter = p.borrow().accessors.get(key).map(|a| a.get.clone());
258 if let Some(g) = getter {
259 return Some(match g {
260 Some(g) => self.call_value(&g, Value::Object(Obj::dom(idx)), &[]).unwrap_or(Value::Undefined),
261 None => Value::Undefined,
262 });
263 }
264 if let Some(v) = p.borrow().props.get(key).cloned() {
265 return Some(v);
266 }
267 guard += 1;
268 if guard > 64 {
269 break;
270 }
271 cur = p.borrow().proto.clone();
272 }
273 None
274 }
275
276 pub(crate) fn dom_element_get(&mut self, idx: usize, key: &str) -> Value {
277 let builtin_only = core::mem::replace(&mut self.dom_builtin_lookup, false);
278 if !builtin_only {
279 if let Some(v) = self.dom.borrow().expandos.get(&idx).and_then(|m| m.get(key)) {
281 return v.clone();
282 }
283 if self.dom.borrow().node_protos.contains_key(&idx) {
288 if let Some(v) = self.ce_chain_get(idx, key) {
289 return v;
290 }
291 } else if let Some(v) = self.dom_proto_override(idx, key) {
292 return v;
293 }
294 }
295 match key {
296 "__proto__" => self.get_dom_node_proto(idx).map(Value::Object).unwrap_or(Value::Null),
297 "baseURI" => Value::str(super::super::builtins::location_href(&self.global).unwrap_or_default()),
301 "attributes" => {
307 let dom = self.dom.borrow();
308 let named: Vec<(String, Value)> = dom
309 .attr_names(idx)
310 .into_iter()
311 .map(|name| {
312 let value = dom.get_attr(idx, &name).unwrap_or_default();
313 let o = Obj::plain();
314 o.borrow_mut().props.insert("name".into(), Value::str(name.clone()));
315 o.borrow_mut().props.insert("localName".into(), Value::str(name.clone()));
316 o.borrow_mut().props.insert("value".into(), Value::str(value));
317 (name, Value::Object(o))
318 })
319 .collect();
320 let arr = Obj::array(named.iter().map(|(_, v)| v.clone()).collect());
323 {
324 let mut b = arr.borrow_mut();
325 for (n, v) in &named {
326 b.props.insert(n.clone(), v.clone());
327 }
328 b.props.insert("getNamedItem".into(), Value::Object(Obj::native("getNamedItem", super::super::builtins::named_node_map_get)));
329 b.props.insert("item".into(), Value::Object(Obj::native("item", super::super::builtins::named_node_map_item)));
330 }
331 Value::Object(arr)
332 }
333 "textContent" | "innerText" | "outerText" => {
334 Value::str(self.dom.borrow().get_text_content(idx))
335 }
336 "text" => Value::str(self.dom.borrow().get_text_content(idx)),
339 "innerHTML" => {
344 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
345 if is_template {
346 let frag_val = self.dom_element_get(idx, "content");
347 if let Some(fi) = this_dom_idx(&frag_val) {
348 Value::str(self.dom.borrow().get_inner_html(fi))
349 } else {
350 Value::str(self.dom.borrow().get_inner_html(idx))
351 }
352 } else {
353 Value::str(self.dom.borrow().get_inner_html(idx))
354 }
355 }
356 "outerHTML" => {
357 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
358 if is_template {
359 Value::str("<template></template>")
360 } else {
361 Value::str(self.dom.borrow().get_outer_html(idx))
362 }
363 }
364 "id" => Value::str(self.dom.borrow().get_attr(idx, "id").unwrap_or_default()),
365 "attachShadow" => {
366 Value::Object(Obj::native(
367 "attachShadow",
368 super::super::builtins::dom_attach_shadow,
369 ))
370 }
371 "shadowRoot" => {
372 if let Some(root_idx) = self.dom.borrow().get_shadow_root(idx) {
373 Value::Object(Obj::dom(root_idx))
374 } else {
375 Value::Null
376 }
377 }
378 "className" => Value::str(self.dom.borrow().get_attr(idx, "class").unwrap_or_default()),
379 "tagName" => {
380 let t = self
381 .dom
382 .borrow()
383 .nodes
384 .get(idx)
385 .map(|n| {
386 if n.attrs.contains_key("_xml") {
387 n.tag.clone()
388 } else {
389 n.tag.to_uppercase()
390 }
391 })
392 .unwrap_or_default();
393 Value::str(t)
394 }
395 "constructor" => {
402 match self.get_dom_node_proto(idx).and_then(|p| p.borrow().props.get("constructor").cloned()) {
403 Some(v) => v,
404 None => Value::Undefined,
405 }
406 }
407 "namespaceURI" => {
415 let is_text = self.dom.borrow().nodes.get(idx).map(|n| n.is_text).unwrap_or(false);
416 if is_text {
417 Value::Null
418 } else {
419 Value::str("http://www.w3.org/1999/xhtml")
420 }
421 }
422 "localName" => {
423 let dom = self.dom.borrow();
424 match dom.nodes.get(idx) {
425 Some(n) if !n.is_text => Value::str(n.tag.clone()),
426 _ => Value::Null,
427 }
428 }
429 "prefix" => Value::Null,
430 "value" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "li").unwrap_or(false) => {
446 let v = self
447 .dom
448 .borrow()
449 .get_attr(idx, "value")
450 .and_then(|s| s.trim().parse::<i64>().ok())
451 .unwrap_or(0);
452 Value::Number(v as f64)
453 }
454 "value" => {
455 let dom = self.dom.borrow();
456 let tag = dom.nodes.get(idx).map(|n| n.tag.as_str()).unwrap_or("");
457 if tag == "select" {
458 Value::str(dom.select_effective_value(idx))
459 } else if tag == "textarea" {
460 Value::str(
461 dom.get_attr(idx, "_live_value")
462 .or_else(|| dom.get_attr(idx, "value"))
463 .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default()),
464 )
465 } else if tag == "output" {
466 Value::str(dom.get_attr(idx, "value").unwrap_or_else(|| dom.get_text_content(idx)))
476 } else if tag == "progress" || tag == "meter" {
477 Value::Number(
481 dom.get_attr(idx, "value").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0),
482 )
483 } else if tag == "input" && dom.get_attr(idx, "type").as_deref() == Some("file") {
484 Value::str("")
485 } else if tag == "input"
486 && matches!(dom.get_attr(idx, "type").as_deref(), Some("checkbox") | Some("radio"))
487 {
488 let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
489 Value::str(raw_val.unwrap_or_else(|| String::from("on")))
490 } else if tag == "input"
491 && dom.get_attr(idx, "type").as_deref() == Some("color")
492 {
493 let is_valid_hex_color = |s: &str| {
494 s.len() == 7
495 && s.starts_with('#')
496 && s.chars().skip(1).all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
497 };
498 let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
499 match raw_val {
500 Some(v) if is_valid_hex_color(&v) => Value::str(v),
501 _ => Value::str("#000000"),
502 }
503 } else if tag == "input" && dom.get_attr(idx, "type").as_deref() == Some("range") {
504 let min = dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0);
505 let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(100.0);
506 let default = if max < min { min } else { min + (max - min) / 2.0 };
507 let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
508 let v = raw_val
509 .and_then(|s| s.trim().parse::<f64>().ok())
510 .unwrap_or(default);
511 let clamped = if max < min {
512 min
513 } else {
514 v.clamp(min, max)
515 };
516 Value::str(Value::Number(clamped).to_js_string())
517 } else if tag == "input"
518 && matches!(
519 dom.get_attr(idx, "type").as_deref(),
520 Some("date") | Some("month") | Some("datetime-local")
521 )
522 {
523 let ty = dom.get_attr(idx, "type").unwrap_or_default();
524 let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
525 match raw_val {
526 Some(v) if super::super::builtins::input_value_as_date_ms(&ty, &v).is_some() => {
527 Value::str(v)
528 }
529 _ => Value::str(""),
530 }
531 } else {
532 let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
533 Value::str(raw_val.unwrap_or_default())
534 }
535 }
536 "defaultValue" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
545 let dom = self.dom.borrow();
546 Value::str(
547 dom.get_attr(idx, "value")
548 .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default()),
549 )
550 }
551 "defaultValue" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false) => {
552 Value::str(self.dom.borrow().get_attr(idx, "value").unwrap_or_default())
553 }
554 "defaultValue" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "output").unwrap_or(false) => {
559 let dom = self.dom.borrow();
560 Value::str(
561 dom.get_attr(idx, "value")
562 .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default()),
563 )
564 }
565 "defaultChecked" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false) => {
566 Value::Bool(self.dom.borrow().has_attr(idx, "checked"))
567 }
568 "defaultSelected" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "option").unwrap_or(false) => {
569 Value::Bool(self.dom.borrow().has_attr(idx, "selected"))
570 }
571 "selectionStart" | "selectionEnd"
578 if self
579 .dom
580 .borrow()
581 .nodes
582 .get(idx)
583 .map(|n| n.tag == "input" || n.tag == "textarea")
584 .unwrap_or(false) =>
585 {
586 let attr = if key == "selectionStart" { "_selection_start" } else { "_selection_end" };
587 Value::Number(self.dom.borrow().get_attr(idx, attr).and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0))
588 }
589 "selectionDirection"
590 if self
591 .dom
592 .borrow()
593 .nodes
594 .get(idx)
595 .map(|n| n.tag == "input" || n.tag == "textarea")
596 .unwrap_or(false) =>
597 {
598 Value::str(self.dom.borrow().get_attr(idx, "_selection_direction").unwrap_or_else(|| "none".into()))
599 }
600 "textLength" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
604 let dom = self.dom.borrow();
605 let v = dom
606 .get_attr(idx, "_live_value")
607 .or_else(|| dom.get_attr(idx, "value"))
608 .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default());
609 Value::Number(v.chars().count() as f64)
610 }
611 "wrap" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
616 let v = self.dom.borrow().get_attr(idx, "wrap").map(|s| s.to_lowercase());
617 match v.as_deref() {
618 Some("hard") => Value::str("hard"),
619 _ => Value::str("soft"),
620 }
621 }
622 "setSelectionRange"
627 if self
628 .dom
629 .borrow()
630 .nodes
631 .get(idx)
632 .map(|n| n.tag == "input" || n.tag == "textarea")
633 .unwrap_or(false) =>
634 {
635 Value::Object(Obj::native("setSelectionRange", super::super::builtins::dom_set_selection_range))
636 }
637 "setRangeText"
638 if self
639 .dom
640 .borrow()
641 .nodes
642 .get(idx)
643 .map(|n| n.tag == "input" || n.tag == "textarea")
644 .unwrap_or(false) =>
645 {
646 Value::Object(Obj::native("setRangeText", super::super::builtins::dom_set_range_text))
647 }
648 "valueAsNumber" => {
652 let dom = self.dom.borrow();
653 let ty = dom.get_attr(idx, "type").unwrap_or_default();
654 if ty == "number" || ty == "range" {
655 let raw_val = dom.get_attr(idx, "_live_value").or_else(|| dom.get_attr(idx, "value"));
656 Value::Number(
657 raw_val
658 .and_then(|s| s.trim().parse::<f64>().ok())
659 .unwrap_or(f64::NAN),
660 )
661 } else {
662 Value::Number(f64::NAN)
663 }
664 }
665 "valueAsDate" => {
670 let dom = self.dom.borrow();
671 let ty = dom.get_attr(idx, "type").unwrap_or_default();
672 let value = dom.get_attr(idx, "_live_value")
673 .or_else(|| dom.get_attr(idx, "value"))
674 .unwrap_or_default();
675 match super::super::builtins::input_value_as_date_ms(&ty, &value) {
676 Some(ms) => Value::Object(Obj::date_obj(ms)),
677 None => Value::Null,
678 }
679 }
680 "max"
683 if self
684 .dom
685 .borrow()
686 .nodes
687 .get(idx)
688 .map(|n| n.tag == "progress" || n.tag == "meter")
689 .unwrap_or(false) =>
690 {
691 Value::Number(
692 self.dom
693 .borrow()
694 .get_attr(idx, "max")
695 .and_then(|s| s.trim().parse::<f64>().ok())
696 .unwrap_or(1.0),
697 )
698 }
699 "min"
700 if self
701 .dom
702 .borrow()
703 .nodes
704 .get(idx)
705 .map(|n| n.tag == "meter")
706 .unwrap_or(false) =>
707 {
708 Value::Number(
709 self.dom
710 .borrow()
711 .get_attr(idx, "min")
712 .and_then(|s| s.trim().parse::<f64>().ok())
713 .unwrap_or(0.0),
714 )
715 }
716 "position"
721 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "progress").unwrap_or(false) =>
722 {
723 let dom = self.dom.borrow();
724 match dom.get_attr(idx, "value").and_then(|s| s.trim().parse::<f64>().ok()) {
725 None => Value::Number(-1.0),
726 Some(v) => {
727 let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0);
728 let max = if max > 0.0 { max } else { 1.0 };
729 Value::Number((v / max).clamp(0.0, 1.0))
730 }
731 }
732 }
733 "low" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "meter").unwrap_or(false) => {
739 let dom = self.dom.borrow();
740 let min = dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0);
741 let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0);
742 let low = dom.get_attr(idx, "low").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(min);
743 Value::Number(low.clamp(min, max))
744 }
745 "high" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "meter").unwrap_or(false) => {
746 let dom = self.dom.borrow();
747 let min = dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0);
748 let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0);
749 let high = dom.get_attr(idx, "high").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(max);
750 Value::Number(high.clamp(min, max))
751 }
752 "optimum" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "meter").unwrap_or(false) => {
753 let dom = self.dom.borrow();
754 let min = dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0);
755 let max = dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0);
756 let optimum =
757 dom.get_attr(idx, "optimum").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or((min + max) / 2.0);
758 Value::Number(optimum.clamp(min, max))
759 }
760 "selectedIndex" => Value::Number(self.dom.borrow().select_selected_index(idx) as f64),
761 "add" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) => {
764 Value::Object(Obj::native("add", super::super::builtins::dom_select_add))
765 }
766 "length"
770 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) =>
771 {
772 Value::Number(self.dom.borrow().select_options(idx).len() as f64)
773 }
774 "selectedOptions" => {
778 let indices = self.dom.borrow().selected_option_indices(idx);
779 Value::Object(Obj::array(
780 indices.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
781 ))
782 }
783 "options" => {
787 let indices = self.dom.borrow().select_options(idx);
788 let arr = Obj::array(
789 indices.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
790 );
791 {
795 let mut b = arr.borrow_mut();
796 b.props.insert("_select_idx".into(), Value::Number(idx as f64));
797 b.props.insert(
798 "item".into(),
799 Value::Object(Obj::native("options.item", super::super::builtins::html_collection_item)),
800 );
801 b.props.insert(
802 "add".into(),
803 Value::Object(Obj::native("options.add", super::super::builtins::select_options_add)),
804 );
805 b.props.insert(
806 "remove".into(),
807 Value::Object(Obj::native("options.remove", super::super::builtins::select_options_remove)),
808 );
809 b.props.insert(
810 "namedItem".into(),
811 Value::Object(Obj::native(
812 "options.namedItem",
813 super::super::builtins::select_options_named_item,
814 )),
815 );
816 }
817 Value::Object(arr)
818 }
819 "form" => match self.dom.borrow().associated_form(idx) {
825 Some(fi) => Value::Object(Obj::dom(fi)),
826 None => Value::Null,
827 },
828 "control" => {
833 let dom = self.dom.borrow();
834 let is_label = dom.nodes.get(idx).map(|n| n.tag == "label").unwrap_or(false);
835 if !is_label {
836 Value::Undefined
837 } else {
838 let for_id = dom.get_attr(idx, "for");
839 let by_for = for_id.filter(|f| !f.is_empty()).and_then(|f| {
840 (0..dom.nodes.len())
841 .find(|&i| !dom.nodes[i].is_text && dom.nodes[i].id == f)
842 });
843 let target = by_for.or_else(|| {
844 dom.descendants_by_tags(idx, &["input", "select", "textarea", "button"])
845 .into_iter()
846 .next()
847 });
848 match target {
849 Some(i) => Value::Object(Obj::dom(i)),
850 None => Value::Null,
851 }
852 }
853 }
854 "labels" => {
858 let dom = self.dom.borrow();
859 let control_id = dom.nodes.get(idx).map(|n| n.id.clone()).unwrap_or_default();
860 let mut labels: Vec<usize> = Vec::new();
861 for (i, n) in dom.nodes.iter().enumerate() {
862 if n.is_text || n.tag != "label" {
863 continue;
864 }
865 let matches_for = !control_id.is_empty()
866 && dom.get_attr(i, "for").map(|f| f == control_id).unwrap_or(false);
867 let is_ancestor = dom.is_ancestor_of(i, idx);
868 if matches_for || is_ancestor {
869 labels.push(i);
870 }
871 }
872 Value::Object(Obj::array(
873 labels.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
874 ))
875 }
876 "elements" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "fieldset").unwrap_or(false) => {
889 let fields = self.dom.borrow().descendants_by_tags(idx, &["input", "select", "textarea", "button"]);
890 super::super::builtins::make_html_collection(self, fields)
891 }
892 "elements" => {
893 let dom = self.dom.borrow();
894 let is_form = dom.nodes.get(idx).map(|n| n.tag == "form").unwrap_or(false);
895 if is_form {
896 let fields =
897 dom.form_associated_controls(idx, &["input", "select", "textarea", "button"]);
898 drop(dom);
899 super::super::builtins::make_html_collection(self, fields)
900 } else {
901 Value::Undefined
902 }
903 }
904 "length" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "form").unwrap_or(false) => {
909 Value::Number(
910 self.dom
911 .borrow()
912 .form_associated_controls(idx, &["input", "select", "textarea", "button"])
913 .len() as f64,
914 )
915 }
916 "rows"
925 if self
926 .dom
927 .borrow()
928 .nodes
929 .get(idx)
930 .map(|n| matches!(n.tag.as_str(), "table" | "thead" | "tbody" | "tfoot"))
931 .unwrap_or(false) =>
932 {
933 let rows = self.dom.borrow().descendants_by_tags(idx, &["tr"]);
934 Value::Object(Obj::array(rows.into_iter().map(|i| Value::Object(Obj::dom(i))).collect()))
935 }
936 "cells" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
937 let cells = self.dom.borrow().descendants_by_tags(idx, &["td", "th"]);
938 Value::Object(Obj::array(cells.into_iter().map(|i| Value::Object(Obj::dom(i))).collect()))
939 }
940 "tHead" | "tFoot" | "caption"
945 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) =>
946 {
947 let want = match key {
948 "tHead" => "thead",
949 "tFoot" => "tfoot",
950 _ => "caption",
951 };
952 let dom = self.dom.borrow();
953 let found = dom.nodes[idx].children.iter().find(|&&c| dom.nodes[c].tag == want).copied();
954 match found {
955 Some(i) => Value::Object(Obj::dom(i)),
956 None => Value::Null,
957 }
958 }
959 "tBodies" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
961 let dom = self.dom.borrow();
962 let bodies: alloc::vec::Vec<usize> =
963 dom.nodes[idx].children.iter().filter(|&&c| dom.nodes[c].tag == "tbody").copied().collect();
964 Value::Object(Obj::array(bodies.into_iter().map(|i| Value::Object(Obj::dom(i))).collect()))
965 }
966 "rowIndex" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
968 let dom = self.dom.borrow();
969 let mut cur = dom.nodes.get(idx).and_then(|n| n.parent);
970 let table = loop {
971 match cur {
972 Some(p) if dom.nodes.get(p).map(|n| n.tag == "table").unwrap_or(false) => break Some(p),
973 Some(p) => cur = dom.nodes.get(p).and_then(|n| n.parent),
974 None => break None,
975 }
976 };
977 match table {
978 Some(t) => {
979 let rows = dom.descendants_by_tags(t, &["tr"]);
980 Value::Number(rows.iter().position(|&r| r == idx).map(|p| p as f64).unwrap_or(-1.0))
981 }
982 None => Value::Number(-1.0),
983 }
984 }
985 "sectionRowIndex" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
994 let dom = self.dom.borrow();
995 match dom.nodes.get(idx).and_then(|n| n.parent) {
996 Some(p) => {
997 let rows = dom.descendants_by_tags(p, &["tr"]);
998 Value::Number(rows.iter().position(|&r| r == idx).map(|i| i as f64).unwrap_or(-1.0))
999 }
1000 None => Value::Number(-1.0),
1001 }
1002 }
1003 "cellIndex" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "td" || n.tag == "th").unwrap_or(false) => {
1005 let dom = self.dom.borrow();
1006 match dom.nodes.get(idx).and_then(|n| n.parent) {
1007 Some(p) => {
1008 let cells = dom.descendants_by_tags(p, &["td", "th"]);
1009 Value::Number(cells.iter().position(|&c| c == idx).map(|i| i as f64).unwrap_or(-1.0))
1010 }
1011 None => Value::Number(-1.0),
1012 }
1013 }
1014 "colSpan" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "td" || n.tag == "th").unwrap_or(false) => {
1021 let v = self
1022 .dom
1023 .borrow()
1024 .get_attr(idx, "colspan")
1025 .and_then(|s| s.trim().parse::<i64>().ok())
1026 .filter(|n| *n >= 1)
1027 .unwrap_or(1);
1028 Value::Number(v.min(1000) as f64)
1029 }
1030 "rowSpan" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "td" || n.tag == "th").unwrap_or(false) => {
1031 let v = self
1032 .dom
1033 .borrow()
1034 .get_attr(idx, "rowspan")
1035 .and_then(|s| s.trim().parse::<i64>().ok())
1036 .filter(|n| *n >= 0)
1037 .unwrap_or(1);
1038 Value::Number(v.min(65534) as f64)
1039 }
1040 "headers" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "td" || n.tag == "th").unwrap_or(false) => {
1047 Value::str(self.dom.borrow().get_attr(idx, "headers").unwrap_or_default())
1048 }
1049 "scope" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "th").unwrap_or(false) => {
1050 let v = self.dom.borrow().get_attr(idx, "scope").map(|s| s.to_lowercase());
1051 match v.as_deref() {
1052 Some("row") => Value::str("row"),
1053 Some("col") => Value::str("col"),
1054 Some("rowgroup") => Value::str("rowgroup"),
1055 Some("colgroup") => Value::str("colgroup"),
1056 _ => Value::str(""),
1057 }
1058 }
1059 "abbr" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "th").unwrap_or(false) => {
1060 Value::str(self.dom.borrow().get_attr(idx, "abbr").unwrap_or_default())
1061 }
1062 "span" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "col" || n.tag == "colgroup").unwrap_or(false) => {
1066 let v = self
1067 .dom
1068 .borrow()
1069 .get_attr(idx, "span")
1070 .and_then(|s| s.trim().parse::<i64>().ok())
1071 .filter(|n| *n >= 1)
1072 .unwrap_or(1);
1073 Value::Number(v.min(1000) as f64)
1074 }
1075 "insertRow"
1087 if self
1088 .dom
1089 .borrow()
1090 .nodes
1091 .get(idx)
1092 .map(|n| matches!(n.tag.as_str(), "table" | "thead" | "tbody" | "tfoot"))
1093 .unwrap_or(false) =>
1094 {
1095 Value::Object(Obj::native("insertRow", super::super::builtins::dom_table_insert_row))
1096 }
1097 "deleteRow"
1098 if self
1099 .dom
1100 .borrow()
1101 .nodes
1102 .get(idx)
1103 .map(|n| matches!(n.tag.as_str(), "table" | "thead" | "tbody" | "tfoot"))
1104 .unwrap_or(false) =>
1105 {
1106 Value::Object(Obj::native("deleteRow", super::super::builtins::dom_table_delete_row))
1107 }
1108 "insertCell" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
1109 Value::Object(Obj::native("insertCell", super::super::builtins::dom_table_insert_cell))
1110 }
1111 "deleteCell" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "tr").unwrap_or(false) => {
1112 Value::Object(Obj::native("deleteCell", super::super::builtins::dom_table_delete_cell))
1113 }
1114 "createTHead" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1118 Value::Object(Obj::native("createTHead", super::super::builtins::dom_table_create_thead))
1119 }
1120 "createTFoot" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1121 Value::Object(Obj::native("createTFoot", super::super::builtins::dom_table_create_tfoot))
1122 }
1123 "createCaption" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1124 Value::Object(Obj::native("createCaption", super::super::builtins::dom_table_create_caption))
1125 }
1126 "deleteTHead" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1127 Value::Object(Obj::native("deleteTHead", super::super::builtins::dom_table_delete_thead))
1128 }
1129 "deleteTFoot" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1130 Value::Object(Obj::native("deleteTFoot", super::super::builtins::dom_table_delete_tfoot))
1131 }
1132 "deleteCaption" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) => {
1133 Value::Object(Obj::native("deleteCaption", super::super::builtins::dom_table_delete_caption))
1134 }
1135 "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "fieldset").unwrap_or(false) => {
1143 Value::str("fieldset")
1144 }
1145 "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
1149 Value::str("textarea")
1150 }
1151 "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "output").unwrap_or(false) => {
1156 Value::str("output")
1157 }
1158 "htmlFor" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "output").unwrap_or(false) => {
1165 let v = self.dom.borrow().get_attr(idx, "for").unwrap_or_default();
1166 Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
1167 }
1168 "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) => {
1174 if self.dom.borrow().has_attr(idx, "multiple") {
1175 Value::str("select-multiple")
1176 } else {
1177 Value::str("select-one")
1178 }
1179 }
1180 "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "button").unwrap_or(false) => {
1188 let v = self.dom.borrow().get_attr(idx, "type").map(|s| s.to_lowercase());
1189 match v.as_deref() {
1190 Some("reset") => Value::str("reset"),
1191 Some("button") => Value::str("button"),
1192 _ => Value::str("submit"),
1193 }
1194 }
1195 "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false) => {
1202 const KNOWN_TYPES: [&str; 22] = [
1203 "text", "hidden", "search", "tel", "url", "email", "password", "date", "month",
1204 "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio",
1205 "file", "submit", "image", "reset", "button",
1206 ];
1207 let v = self.dom.borrow().get_attr(idx, "type").map(|s| s.to_lowercase());
1208 match v {
1209 Some(t) if KNOWN_TYPES.contains(&t.as_str()) => Value::str(t),
1210 _ => Value::str("text"),
1211 }
1212 }
1213 "type" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "ol").unwrap_or(false) => {
1218 let v = self.dom.borrow().get_attr(idx, "type");
1219 match v.as_deref() {
1220 Some("1") => Value::str("1"),
1221 Some("a") => Value::str("a"),
1222 Some("A") => Value::str("A"),
1223 Some("i") => Value::str("i"),
1224 Some("I") => Value::str("I"),
1225 _ => Value::str("1"),
1226 }
1227 }
1228 "type" => Value::str(self.dom.borrow().get_attr(idx, "type").unwrap_or_default()),
1229 "checked" => {
1235 let dom = self.dom.borrow();
1236 if let Some(lc) = dom.get_attr(idx, "_live_checked") {
1237 Value::Bool(lc == "true")
1238 } else {
1239 Value::Bool(dom.has_attr(idx, "checked"))
1240 }
1241 }
1242 "files"
1248 if self.dom.borrow().get_attr(idx, "type").as_deref() == Some("file") =>
1249 {
1250 Value::Object(Obj::array(alloc::vec![]))
1251 }
1252
1253 "contentWindow"
1261 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "iframe").unwrap_or(false) =>
1262 {
1263 self.global.borrow().vars.get("window").cloned().unwrap_or(Value::Undefined)
1264 }
1265 "contentDocument"
1270 if self
1271 .dom
1272 .borrow()
1273 .nodes
1274 .get(idx)
1275 .map(|n| n.tag == "iframe" || n.tag == "object")
1276 .unwrap_or(false) =>
1277 {
1278 self.global.borrow().vars.get("document").cloned().unwrap_or(Value::Undefined)
1279 }
1280 "data" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "object").unwrap_or(false) => {
1287 let raw = self.dom.borrow().get_attr(idx, "data").unwrap_or_default();
1288 Value::str(self.resolve_url_against_page(&raw))
1289 }
1290 "open" => Value::Bool(self.dom.borrow().has_attr(idx, "open")),
1293 "inert" => Value::Bool(self.dom.borrow().has_attr(idx, "inert")),
1297 "returnValue" => Value::str(
1300 self.dom
1301 .borrow()
1302 .get_attr(idx, "_return_value")
1303 .unwrap_or_default(),
1304 ),
1305 "indeterminate" => Value::Bool(self.dom.borrow().has_attr(idx, "_indeterminate")),
1311 "disabled" => Value::Bool(self.dom.borrow().is_disabled(idx)),
1322 "selected" => Value::Bool(self.dom.borrow().has_attr(idx, "selected")),
1323 "index"
1327 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "option").unwrap_or(false) =>
1328 {
1329 Value::Number(self.dom.borrow().option_index(idx) as f64)
1330 }
1331 "placeholder" => {
1332 Value::str(self.dom.borrow().get_attr(idx, "placeholder").unwrap_or_default())
1333 }
1334 "pattern" => Value::str(self.dom.borrow().get_attr(idx, "pattern").unwrap_or_default()),
1341 "name" => Value::str(self.dom.borrow().get_attr(idx, "name").unwrap_or_default()),
1342 "required" => Value::Bool(self.dom.borrow().has_attr(idx, "required")),
1347 "noValidate" => Value::Bool(self.dom.borrow().has_attr(idx, "novalidate")),
1352 "readOnly" => Value::Bool(self.dom.borrow().has_attr(idx, "readonly")),
1353 "multiple" => Value::Bool(self.dom.borrow().has_attr(idx, "multiple")),
1354 "capture" => Value::str(self.dom.borrow().get_attr(idx, "capture").unwrap_or_default()),
1359 "accept" => Value::str(self.dom.borrow().get_attr(idx, "accept").unwrap_or_default()),
1363 "src" => {
1369 let raw = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
1370 Value::str(self.resolve_url_against_page(&raw))
1371 }
1372 "srcObject" => Value::Null,
1373 "alt" => Value::str(self.dom.borrow().get_attr(idx, "alt").unwrap_or_default()),
1374 "href" => {
1375 let raw = self.dom.borrow().get_attr(idx, "href").unwrap_or_default();
1376 Value::str(self.resolve_url_against_page(&raw))
1377 }
1378 "protocol" | "host" | "hostname" | "port" | "pathname" | "search" | "hash" | "origin"
1387 | "username" | "password"
1388 if self
1389 .dom
1390 .borrow()
1391 .nodes
1392 .get(idx)
1393 .map(|n| n.tag == "a" || n.tag == "area")
1394 .unwrap_or(false) =>
1395 {
1396 let raw = self.dom.borrow().get_attr(idx, "href").unwrap_or_default();
1397 let abs = self.resolve_url_against_page(&raw);
1398 let comps = super::super::builtins::location_components(&abs);
1399 Value::str(comps.iter().find(|(k, _)| *k == key).map(|(_, v)| v.clone()).unwrap_or_default())
1400 }
1401 "htmlFor" => Value::str(self.dom.borrow().get_attr(idx, "for").unwrap_or_default()),
1406 "acceptCharset" => Value::str(self.dom.borrow().get_attr(idx, "accept-charset").unwrap_or_default()),
1410 "httpEquiv" => Value::str(self.dom.borrow().get_attr(idx, "http-equiv").unwrap_or_default()),
1415 "content" => {
1416 let tag = self.dom.borrow().nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
1417 if tag == "meta" {
1418 Value::str(self.dom.borrow().get_attr(idx, "content").unwrap_or_default())
1419 } else if tag == "template" {
1420 let existing_attr = self.dom.borrow().get_attr(idx, "_template_content_idx");
1421 let frag_idx = existing_attr.and_then(|s| s.parse::<usize>().ok());
1422 let frag_idx = match frag_idx {
1423 Some(fi) => fi,
1424 None => {
1425 let fi = self.dom.borrow_mut().create_element("#document-fragment");
1426 self.dom.borrow_mut().set_attr(idx, "_template_content_idx", &fi.to_string());
1427 let direct_children = self.dom.borrow().nodes.get(idx).map(|n| n.children.clone()).unwrap_or_default();
1428 if !direct_children.is_empty() {
1429 let mut dom = self.dom.borrow_mut();
1430 if let Some(template_node) = dom.nodes.get_mut(idx) {
1431 template_node.children.clear();
1432 }
1433 if let Some(frag_node) = dom.nodes.get_mut(fi) {
1434 frag_node.children = direct_children.clone();
1435 }
1436 for &child_idx in &direct_children {
1437 if let Some(child_node) = dom.nodes.get_mut(child_idx) {
1438 child_node.parent = Some(fi);
1439 }
1440 }
1441 }
1442 fi
1443 }
1444 };
1445 Value::Object(Obj::dom(frag_idx))
1446 } else {
1447 Value::str(self.dom.borrow().get_attr(idx, "content").unwrap_or_default())
1448 }
1449 }
1450 "charset" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "meta").unwrap_or(false) => {
1451 Value::str(self.dom.borrow().get_attr(idx, "charset").unwrap_or_default())
1452 }
1453 "charset" => Value::str(self.dom.borrow().get_attr(idx, "charset").unwrap_or_default()),
1458 "cite" => Value::str(self.dom.borrow().get_attr(idx, "cite").unwrap_or_default()),
1463 "dateTime" => Value::str(self.dom.borrow().get_attr(idx, "datetime").unwrap_or_default()),
1464 "action" => {
1465 let raw = self.dom.borrow().get_attr(idx, "action").unwrap_or_default();
1466 Value::str(self.resolve_url_against_page(&raw))
1467 }
1468 "method" => {
1474 let v = self.dom.borrow().get_attr(idx, "method").map(|s| s.to_lowercase());
1475 match v.as_deref() {
1476 Some("post") => Value::str("post"),
1477 Some("dialog") => Value::str("dialog"),
1478 _ => Value::str("get"),
1479 }
1480 }
1481 "enctype" | "encoding" => {
1487 let v = self.dom.borrow().get_attr(idx, "enctype").map(|s| s.to_lowercase());
1488 match v.as_deref() {
1489 Some("multipart/form-data") => Value::str("multipart/form-data"),
1490 Some("text/plain") => Value::str("text/plain"),
1491 _ => Value::str("application/x-www-form-urlencoded"),
1492 }
1493 }
1494 "formAction"
1502 if self
1503 .dom
1504 .borrow()
1505 .nodes
1506 .get(idx)
1507 .map(|n| n.tag == "button" || n.tag == "input")
1508 .unwrap_or(false) =>
1509 {
1510 let dom = self.dom.borrow();
1511 let raw = dom.get_attr(idx, "formaction").or_else(|| {
1512 dom.associated_form(idx).and_then(|fi| dom.get_attr(fi, "action"))
1513 }).unwrap_or_default();
1514 drop(dom);
1515 Value::str(self.resolve_url_against_page(&raw))
1516 }
1517 "formEnctype"
1518 if self
1519 .dom
1520 .borrow()
1521 .nodes
1522 .get(idx)
1523 .map(|n| n.tag == "button" || n.tag == "input")
1524 .unwrap_or(false) =>
1525 {
1526 let dom = self.dom.borrow();
1527 let raw = dom.get_attr(idx, "formenctype").or_else(|| {
1528 dom.associated_form(idx).and_then(|fi| dom.get_attr(fi, "enctype"))
1529 }).map(|s| s.to_lowercase());
1530 match raw.as_deref() {
1531 Some("multipart/form-data") => Value::str("multipart/form-data"),
1532 Some("text/plain") => Value::str("text/plain"),
1533 _ => Value::str("application/x-www-form-urlencoded"),
1534 }
1535 }
1536 "formMethod"
1537 if self
1538 .dom
1539 .borrow()
1540 .nodes
1541 .get(idx)
1542 .map(|n| n.tag == "button" || n.tag == "input")
1543 .unwrap_or(false) =>
1544 {
1545 let dom = self.dom.borrow();
1546 let raw = dom.get_attr(idx, "formmethod").or_else(|| {
1547 dom.associated_form(idx).and_then(|fi| dom.get_attr(fi, "method"))
1548 }).map(|s| s.to_lowercase());
1549 match raw.as_deref() {
1550 Some("post") => Value::str("post"),
1551 Some("dialog") => Value::str("dialog"),
1552 _ => Value::str("get"),
1553 }
1554 }
1555 "formNoValidate"
1556 if self
1557 .dom
1558 .borrow()
1559 .nodes
1560 .get(idx)
1561 .map(|n| n.tag == "button" || n.tag == "input")
1562 .unwrap_or(false) =>
1563 {
1564 Value::Bool(self.dom.borrow().has_attr(idx, "formnovalidate"))
1565 }
1566 "formTarget"
1567 if self
1568 .dom
1569 .borrow()
1570 .nodes
1571 .get(idx)
1572 .map(|n| n.tag == "button" || n.tag == "input")
1573 .unwrap_or(false) =>
1574 {
1575 let dom = self.dom.borrow();
1576 let raw = dom.get_attr(idx, "formtarget").or_else(|| {
1577 dom.associated_form(idx).and_then(|fi| dom.get_attr(fi, "target"))
1578 }).unwrap_or_default();
1579 Value::str(raw)
1580 }
1581 "min" => Value::str(self.dom.borrow().get_attr(idx, "min").unwrap_or_default()),
1582 "max" => Value::str(self.dom.borrow().get_attr(idx, "max").unwrap_or_default()),
1583 "step" => Value::str(self.dom.borrow().get_attr(idx, "step").unwrap_or_default()),
1584 "maxLength" => Value::Number(
1591 self.dom
1592 .borrow()
1593 .get_attr(idx, "maxlength")
1594 .and_then(|s| s.trim().parse::<i64>().ok())
1595 .unwrap_or(-1) as f64,
1596 ),
1597 "minLength" => Value::Number(
1598 self.dom
1599 .borrow()
1600 .get_attr(idx, "minlength")
1601 .and_then(|s| s.trim().parse::<i64>().ok())
1602 .unwrap_or(-1) as f64,
1603 ),
1604 "target" => Value::str(self.dom.borrow().get_attr(idx, "target").unwrap_or_default()),
1605 "rel" => Value::str(self.dom.borrow().get_attr(idx, "rel").unwrap_or_default()),
1606 "download" => Value::str(self.dom.borrow().get_attr(idx, "download").unwrap_or_default()),
1607 "coords"
1613 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "area").unwrap_or(false) =>
1614 {
1615 Value::str(self.dom.borrow().get_attr(idx, "coords").unwrap_or_default())
1616 }
1617 "shape"
1618 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "area").unwrap_or(false) =>
1619 {
1620 let v = self.dom.borrow().get_attr(idx, "shape").map(|s| s.to_lowercase());
1621 match v.as_deref() {
1622 Some("circle") | Some("circ") => Value::str("circle"),
1623 Some("poly") | Some("polygon") => Value::str("poly"),
1624 Some("default") => Value::str("default"),
1625 _ => Value::str("rect"),
1626 }
1627 }
1628 "start" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "ol").unwrap_or(false) => {
1632 let v = self
1633 .dom
1634 .borrow()
1635 .get_attr(idx, "start")
1636 .and_then(|s| s.trim().parse::<i64>().ok());
1637 Value::Number(v.unwrap_or(1) as f64)
1638 }
1639 "reversed" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "ol").unwrap_or(false) => {
1640 Value::Bool(self.dom.borrow().has_attr(idx, "reversed"))
1641 }
1642 "lang" => Value::str(self.dom.borrow().get_attr(idx, "lang").unwrap_or_default()),
1646 "dir" => Value::str(self.dom.borrow().get_attr(idx, "dir").unwrap_or_default()),
1647 "title" => Value::str(self.dom.borrow().get_attr(idx, "title").unwrap_or_default()),
1654 "label" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "option").unwrap_or(false) => {
1660 match self.dom.borrow().get_attr(idx, "label") {
1661 Some(v) => Value::str(v),
1662 None => Value::str(self.dom.borrow().get_text_content(idx).trim().to_string()),
1663 }
1664 }
1665 "label" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "optgroup").unwrap_or(false) => {
1666 Value::str(self.dom.borrow().get_attr(idx, "label").unwrap_or_default())
1667 }
1668 "label" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "track").unwrap_or(false) => {
1673 Value::str(self.dom.borrow().get_attr(idx, "label").unwrap_or_default())
1674 }
1675 "srclang" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "track").unwrap_or(false) => {
1676 Value::str(self.dom.borrow().get_attr(idx, "srclang").unwrap_or_default())
1677 }
1678 "default" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "track").unwrap_or(false) => {
1679 Value::Bool(self.dom.borrow().has_attr(idx, "default"))
1680 }
1681 "accessKey" => Value::str(self.dom.borrow().get_attr(idx, "accesskey").unwrap_or_default()),
1685 "translate" => Value::Bool(
1688 self.dom.borrow().get_attr(idx, "translate").as_deref() != Some("no"),
1689 ),
1690 "draggable" => Value::Bool(
1697 self.dom.borrow().get_attr(idx, "draggable").as_deref() == Some("true"),
1698 ),
1699 "slot" => Value::str(self.dom.borrow().get_attr(idx, "slot").unwrap_or_default()),
1706 "tabIndex" => Value::Number(
1711 self.dom
1712 .borrow()
1713 .get_attr(idx, "tabindex")
1714 .and_then(|v| v.trim().parse::<f64>().ok())
1715 .unwrap_or(-1.0),
1716 ),
1717 "contentEditable" => Value::str(
1723 match self.dom.borrow().get_attr(idx, "contenteditable").as_deref() {
1724 Some("true") | Some("") => "true",
1725 Some("false") => "false",
1726 Some("plaintext-only") => "plaintext-only",
1727 _ => "inherit",
1728 },
1729 ),
1730 "isContentEditable" => Value::Bool(matches!(
1731 self.dom.borrow().get_attr(idx, "contenteditable").as_deref(),
1732 Some("true") | Some("") | Some("plaintext-only")
1733 )),
1734 "spellcheck" => Value::Bool(
1739 self.dom.borrow().get_attr(idx, "spellcheck").as_deref() != Some("false"),
1740 ),
1741 "inputMode" => Value::str(self.dom.borrow().get_attr(idx, "inputmode").unwrap_or_default()),
1745 "size" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) => {
1751 let v = self
1752 .dom
1753 .borrow()
1754 .get_attr(idx, "size")
1755 .and_then(|s| s.trim().parse::<i64>().ok())
1756 .filter(|n| *n >= 0)
1757 .unwrap_or(0);
1758 Value::Number(v as f64)
1759 }
1760 "size" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false) => {
1761 let v = self
1762 .dom
1763 .borrow()
1764 .get_attr(idx, "size")
1765 .and_then(|s| s.trim().parse::<i64>().ok())
1766 .filter(|n| *n >= 1)
1767 .unwrap_or(20);
1768 Value::Number(v as f64)
1769 }
1770 "rows" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
1774 let v = self
1775 .dom
1776 .borrow()
1777 .get_attr(idx, "rows")
1778 .and_then(|s| s.trim().parse::<i64>().ok())
1779 .filter(|n| *n >= 1)
1780 .unwrap_or(2);
1781 Value::Number(v as f64)
1782 }
1783 "cols" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "textarea").unwrap_or(false) => {
1784 let v = self
1785 .dom
1786 .borrow()
1787 .get_attr(idx, "cols")
1788 .and_then(|s| s.trim().parse::<i64>().ok())
1789 .filter(|n| *n >= 1)
1790 .unwrap_or(20);
1791 Value::Number(v as f64)
1792 }
1793 "autocomplete" => Value::str(self.dom.borrow().get_attr(idx, "autocomplete").unwrap_or_default()),
1798 "autofocus" => Value::Bool(self.dom.borrow().has_attr(idx, "autofocus")),
1799 "enterKeyHint" => {
1800 Value::str(self.dom.borrow().get_attr(idx, "enterkeyhint").unwrap_or_default())
1801 }
1802 "autocapitalize" => {
1806 Value::str(self.dom.borrow().get_attr(idx, "autocapitalize").unwrap_or_default())
1807 }
1808 "autoplay" => Value::Bool(self.dom.borrow().has_attr(idx, "autoplay")),
1813 "controls" => Value::Bool(self.dom.borrow().has_attr(idx, "controls")),
1814 "loop" => Value::Bool(self.dom.borrow().has_attr(idx, "loop")),
1815 "muted" => Value::Bool(self.dom.borrow().has_attr(idx, "muted")),
1816 "playsInline" => Value::Bool(self.dom.borrow().has_attr(idx, "playsinline")),
1817 "disablePictureInPicture" => {
1822 Value::Bool(self.dom.borrow().has_attr(idx, "disablepictureinpicture"))
1823 }
1824 "disableRemotePlayback" => {
1828 Value::Bool(self.dom.borrow().has_attr(idx, "disableremoteplayback"))
1829 }
1830 "allowFullscreen" => Value::Bool(self.dom.borrow().has_attr(idx, "allowfullscreen")),
1831 "credentialless" => Value::Bool(self.dom.borrow().has_attr(idx, "credentialless")),
1835 "width" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1843 let v = self
1844 .dom
1845 .borrow()
1846 .get_attr(idx, "width")
1847 .and_then(|s| s.trim().parse::<i64>().ok())
1848 .filter(|n| *n >= 0)
1849 .unwrap_or(300);
1850 Value::Number(v as f64)
1851 }
1852 "height" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1853 let v = self
1854 .dom
1855 .borrow()
1856 .get_attr(idx, "height")
1857 .and_then(|s| s.trim().parse::<i64>().ok())
1858 .filter(|n| *n >= 0)
1859 .unwrap_or(150);
1860 Value::Number(v as f64)
1861 }
1862 "getContext" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1863 Value::Object(Obj::native("getContext", super::super::builtins::dom_canvas_get_context))
1864 }
1865 "toDataURL" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1866 Value::Object(Obj::native("toDataURL", super::super::builtins::dom_canvas_to_data_url))
1867 }
1868 "toBlob" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "canvas").unwrap_or(false) => {
1869 Value::Object(Obj::native("toBlob", super::super::builtins::dom_canvas_to_blob))
1870 }
1871 "width" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img" || n.tag == "video").unwrap_or(false) => {
1872 let v = self
1873 .dom
1874 .borrow()
1875 .get_attr(idx, "width")
1876 .and_then(|s| s.trim().parse::<i64>().ok())
1877 .filter(|n| *n >= 0)
1878 .unwrap_or(0);
1879 Value::Number(v as f64)
1880 }
1881 "height" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img" || n.tag == "video").unwrap_or(false) => {
1882 let v = self
1883 .dom
1884 .borrow()
1885 .get_attr(idx, "height")
1886 .and_then(|s| s.trim().parse::<i64>().ok())
1887 .filter(|n| *n >= 0)
1888 .unwrap_or(0);
1889 Value::Number(v as f64)
1890 }
1891 "width"
1897 if self
1898 .dom
1899 .borrow()
1900 .nodes
1901 .get(idx)
1902 .map(|n| n.tag == "embed" || n.tag == "object" || n.tag == "iframe")
1903 .unwrap_or(false) =>
1904 {
1905 Value::str(self.dom.borrow().get_attr(idx, "width").unwrap_or_default())
1906 }
1907 "height"
1908 if self
1909 .dom
1910 .borrow()
1911 .nodes
1912 .get(idx)
1913 .map(|n| n.tag == "embed" || n.tag == "object" || n.tag == "iframe")
1914 .unwrap_or(false) =>
1915 {
1916 Value::str(self.dom.borrow().get_attr(idx, "height").unwrap_or_default())
1917 }
1918 "paused" => match self.media_status(idx) {
1928 Some(st) if st.playing => Value::Bool(false),
1929 Some(st) if st.ended => Value::Bool(true),
1930 _ => Value::Bool(self.dom.borrow().get_attr(idx, "_paused").as_deref() != Some("false")),
1931 },
1932 "currentTime" => match self.media_status(idx) {
1933 Some(st) => Value::Number(st.current_time),
1934 None => Value::Number(
1935 self.dom
1936 .borrow()
1937 .get_attr(idx, "_current_time")
1938 .and_then(|v| v.parse::<f64>().ok())
1939 .unwrap_or(0.0),
1940 ),
1941 },
1942 "duration" => {
1943 let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
1944 Value::Number(self.media_source_duration(&src).unwrap_or(f64::NAN))
1945 }
1946 "volume" => Value::Number(
1947 self.dom
1948 .borrow()
1949 .get_attr(idx, "_volume")
1950 .and_then(|v| v.parse::<f64>().ok())
1951 .unwrap_or(1.0),
1952 ),
1953 "playbackRate" => Value::Number(
1954 self.dom
1955 .borrow()
1956 .get_attr(idx, "_playback_rate")
1957 .and_then(|v| v.parse::<f64>().ok())
1958 .unwrap_or(1.0),
1959 ),
1960 "ended" => match self.media_status(idx) {
1961 Some(st) => Value::Bool(st.ended),
1962 None => Value::Bool(self.dom.borrow().has_attr(idx, "_ended")),
1963 },
1964 "readyState" => {
1974 let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
1975 Value::Number(super::super::builtins::media_source_ready_state(&src).unwrap_or(0) as f64)
1976 }
1977 "networkState" => {
1978 let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
1979 if src.starts_with("blob:mediasource-") {
1980 let ended = self.media_status(idx).is_some_and(|st| st.ended);
1981 Value::Number(if ended { 1.0 } else { 2.0 })
1982 } else {
1983 let has_src = !src.is_empty() || self.dom.borrow().has_attr(idx, "srcset");
1984 Value::Number(if has_src { 3.0 } else { 0.0 })
1985 }
1986 }
1987 "error" => Value::Null,
1988 "nonce" => Value::str(self.dom.borrow().get_attr(idx, "nonce").unwrap_or_default()),
1993 "role" => match self.dom.borrow().get_attr(idx, "role") {
2003 Some(v) => Value::str(v),
2004 None => Value::Null,
2005 },
2006 "ariaActiveDescendantElement" => {
2013 let dom = self.dom.borrow();
2014 match dom.get_attr(idx, "aria-activedescendant").filter(|f| !f.is_empty()) {
2015 Some(target_id) => match dom.get_element_by_id(&target_id) {
2016 Some(ti) => Value::Object(Obj::dom(ti)),
2017 None => Value::Null,
2018 },
2019 None => Value::Null,
2020 }
2021 }
2022 _ if key.starts_with("aria") && key.ends_with("Elements") && key.len() > 12 => {
2033 let suffix: String =
2034 key.chars().skip(4).take(key.len() - 4 - 8).collect::<String>().to_lowercase();
2035 let attr = alloc::format!("aria-{}", suffix);
2036 let dom = self.dom.borrow();
2037 let ids = dom.get_attr(idx, &attr).unwrap_or_default();
2038 let elems: Vec<Value> = ids
2039 .split_whitespace()
2040 .filter_map(|id| dom.get_element_by_id(id))
2041 .map(|ti| Value::Object(Obj::dom(ti)))
2042 .collect();
2043 Value::Object(Obj::array(elems))
2044 }
2045 _ if key.starts_with("aria") && key.as_bytes().get(4).is_some_and(u8::is_ascii_uppercase) => {
2046 let suffix: String = key.chars().skip(4).collect::<String>().to_lowercase();
2047 let attr = alloc::format!("aria-{}", suffix);
2048 match self.dom.borrow().get_attr(idx, &attr) {
2049 Some(v) => Value::str(v),
2050 None => Value::Null,
2051 }
2052 }
2053 "buffered" if self.dom.borrow().get_attr(idx, "src").is_some_and(|s| s.starts_with("blob:mediasource-")) => {
2060 let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
2061 super::super::builtins::media_element_buffered(&src)
2062 .unwrap_or_else(|| Value::Object(super::super::builtins::make_empty_time_ranges()))
2063 }
2064 "seekable" if self.dom.borrow().get_attr(idx, "src").is_some_and(|s| s.starts_with("blob:mediasource-")) => {
2065 let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
2066 super::super::builtins::media_element_seekable(self.media_source_duration(&src))
2067 }
2068 "played" => match self.media_status(idx) {
2069 Some(st) if st.current_time > 0.0 => {
2070 super::super::builtins::media_element_seekable(Some(st.current_time))
2071 }
2072 _ => Value::Object(super::super::builtins::make_empty_time_ranges()),
2073 },
2074 "buffered" | "seekable" => {
2075 Value::Object(super::super::builtins::make_empty_time_ranges())
2076 }
2077 "videoWidth" | "videoHeight"
2080 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "video").unwrap_or(false) =>
2081 {
2082 let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
2083 if src.starts_with("blob:mediasource-") {
2084 if let Some((w, h)) = super::super::builtins::media_source_video_dimensions(&src) {
2085 return Value::Number(if key == "videoWidth" { w as f64 } else { h as f64 });
2086 }
2087 }
2088 Value::Number(0.0)
2089 }
2090 "seeking" => Value::Bool(false),
2091 "getVideoPlaybackQuality"
2094 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "video").unwrap_or(false) =>
2095 {
2096 Value::Object(Obj::native("getVideoPlaybackQuality", |_, _, _| {
2097 let (total, dropped) = {
2098 let pb = crate::os_lib::mse::PLAYBACK.lock();
2099 (pb.total_video_frames as f64, pb.dropped_video_frames as f64)
2100 };
2101 let q = Obj::plain();
2102 {
2103 let mut b = q.borrow_mut();
2104 b.props.insert("creationTime".into(), Value::Number(0.0));
2105 b.props.insert("totalVideoFrames".into(), Value::Number(total));
2106 b.props.insert("droppedVideoFrames".into(), Value::Number(dropped));
2107 b.props.insert("corruptedVideoFrames".into(), Value::Number(0.0));
2108 }
2109 Ok(Value::Object(q))
2110 }))
2111 }
2112 "defaultPlaybackRate" => Value::Number(
2117 self.dom
2118 .borrow()
2119 .get_attr(idx, "_default_playback_rate")
2120 .and_then(|v| v.parse::<f64>().ok())
2121 .unwrap_or(1.0),
2122 ),
2123 "preload" => {
2129 let v = self.dom.borrow().get_attr(idx, "preload").map(|s| s.to_lowercase());
2130 match v.as_deref() {
2131 None => Value::str("metadata"),
2132 Some("none") => Value::str("none"),
2133 Some("metadata") => Value::str("metadata"),
2134 Some(_) => Value::str("auto"),
2135 }
2136 }
2137 "poster" => Value::str(self.dom.borrow().get_attr(idx, "poster").unwrap_or_default()),
2139 "kind" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "track").unwrap_or(false) => {
2145 let v = self.dom.borrow().get_attr(idx, "kind").map(|s| s.to_lowercase());
2146 match v.as_deref() {
2147 None => Value::str("subtitles"),
2148 Some("subtitles") => Value::str("subtitles"),
2149 Some("captions") => Value::str("captions"),
2150 Some("descriptions") => Value::str("descriptions"),
2151 Some("chapters") => Value::str("chapters"),
2152 Some(_) => Value::str("metadata"),
2153 }
2154 }
2155 "ping" => Value::str(self.dom.borrow().get_attr(idx, "ping").unwrap_or_default()),
2158 "hreflang" => Value::str(self.dom.borrow().get_attr(idx, "hreflang").unwrap_or_default()),
2159 "srcset" => Value::str(self.dom.borrow().get_attr(idx, "srcset").unwrap_or_default()),
2163 "sizes" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "link").unwrap_or(false) => {
2171 let v = self.dom.borrow().get_attr(idx, "sizes").unwrap_or_default();
2172 Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
2173 }
2174 "sizes" => Value::str(self.dom.borrow().get_attr(idx, "sizes").unwrap_or_default()),
2175 "media" => Value::str(self.dom.borrow().get_attr(idx, "media").unwrap_or_default()),
2179 "complete" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img").unwrap_or(false) => {
2190 Value::Bool(self.dom.borrow().get_attr(idx, "src").map(|s| !s.is_empty()).unwrap_or(false))
2191 }
2192 "naturalWidth" | "naturalHeight"
2193 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img").unwrap_or(false) =>
2194 {
2195 Value::Number(0.0)
2196 }
2197 "useMap" => Value::str(self.dom.borrow().get_attr(idx, "usemap").unwrap_or_default()),
2199 "decode" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "img").unwrap_or(false) => {
2203 Value::Object(Obj::native("decode", super::super::builtins::dom_img_decode))
2204 }
2205 "isMap" => Value::Bool(self.dom.borrow().has_attr(idx, "ismap")),
2206 "currentSrc" => Value::str(self.dom.borrow().get_attr(idx, "src").unwrap_or_default()),
2211 "crossOrigin" => match self.dom.borrow().get_attr(idx, "crossorigin") {
2215 Some(v) => Value::str(v),
2216 None => Value::Null,
2217 },
2218 "fetchPriority" => Value::str(
2221 self.dom.borrow().get_attr(idx, "fetchpriority").unwrap_or_else(|| "auto".into()),
2222 ),
2223 "integrity" => Value::str(self.dom.borrow().get_attr(idx, "integrity").unwrap_or_default()),
2226 "async" => Value::Bool(self.dom.borrow().has_attr(idx, "async")),
2229 "defer" => Value::Bool(self.dom.borrow().has_attr(idx, "defer")),
2230 "noModule" => Value::Bool(self.dom.borrow().has_attr(idx, "nomodule")),
2231 "referrerPolicy" => {
2235 Value::str(self.dom.borrow().get_attr(idx, "referrerpolicy").unwrap_or_default())
2236 }
2237 "as" => Value::str(self.dom.borrow().get_attr(idx, "as").unwrap_or_default()),
2241 "imageSrcset" => Value::str(self.dom.borrow().get_attr(idx, "imagesrcset").unwrap_or_default()),
2246 "imageSizes" => Value::str(self.dom.borrow().get_attr(idx, "imagesizes").unwrap_or_default()),
2247 "loading" => {
2253 let v = self.dom.borrow().get_attr(idx, "loading").map(|s| s.to_lowercase());
2254 match v.as_deref() {
2255 Some("lazy") => Value::str("lazy"),
2256 Some("eager") => Value::str("eager"),
2257 _ => Value::str("auto"),
2258 }
2259 }
2260 "decoding" => {
2264 let v = self.dom.borrow().get_attr(idx, "decoding").map(|s| s.to_lowercase());
2265 match v.as_deref() {
2266 Some("sync") => Value::str("sync"),
2267 Some("async") => Value::str("async"),
2268 _ => Value::str("auto"),
2269 }
2270 }
2271 "allow" => Value::str(self.dom.borrow().get_attr(idx, "allow").unwrap_or_default()),
2273 "srcdoc" => Value::str(self.dom.borrow().get_attr(idx, "srcdoc").unwrap_or_default()),
2277 "sandbox" => {
2282 let v = self.dom.borrow().get_attr(idx, "sandbox").unwrap_or_default();
2283 Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
2284 }
2285 "popover" => match self.dom.borrow().get_attr(idx, "popover") {
2290 Some(v) => Value::str(v),
2291 None => Value::Null,
2292 },
2293 "relList" => {
2306 let rel = self.dom.borrow().get_attr(idx, "rel").unwrap_or_default();
2307 let tokens: Vec<Value> =
2308 rel.split_whitespace().map(Value::str).collect();
2309 Value::Object(Obj::array(tokens))
2310 }
2311 "part" => {
2318 let v = self.dom.borrow().get_attr(idx, "part").unwrap_or_default();
2319 Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
2320 }
2321 "blocking" => {
2326 let v = self.dom.borrow().get_attr(idx, "blocking").unwrap_or_default();
2327 Value::Object(Obj::array(v.split_whitespace().map(Value::str).collect()))
2328 }
2329 "list" => {
2334 let dom = self.dom.borrow();
2335 match dom.get_attr(idx, "list").filter(|f| !f.is_empty()) {
2336 Some(target_id) => match dom.get_element_by_id(&target_id) {
2337 Some(ti) if dom.nodes.get(ti).map(|n| n.tag == "datalist").unwrap_or(false) => {
2338 Value::Object(Obj::dom(ti))
2339 }
2340 _ => Value::Null,
2341 },
2342 None => Value::Null,
2343 }
2344 }
2345 "popoverTargetElement" => {
2346 let dom = self.dom.borrow();
2347 match dom.get_attr(idx, "popovertarget").filter(|f| !f.is_empty()) {
2348 Some(target_id) => match dom.get_element_by_id(&target_id) {
2349 Some(ti) => Value::Object(Obj::dom(ti)),
2350 None => Value::Null,
2351 },
2352 None => Value::Null,
2353 }
2354 }
2355 "popoverTargetAction" => {
2356 Value::str(self.dom.borrow().get_attr(idx, "popovertargetaction").unwrap_or_default())
2357 }
2358 "command" => Value::str(self.dom.borrow().get_attr(idx, "command").unwrap_or_default()),
2368 "commandForElement" => {
2369 let dom = self.dom.borrow();
2370 match dom.get_attr(idx, "commandfor").filter(|f| !f.is_empty()) {
2371 Some(target_id) => match dom.get_element_by_id(&target_id) {
2372 Some(ti) => Value::Object(Obj::dom(ti)),
2373 None => Value::Null,
2374 },
2375 None => Value::Null,
2376 }
2377 }
2378 "style" => Value::Object(Obj::host(&format!("style:{}", idx))),
2379 "classList" => Value::Object(Obj::host(&format!("classList:{}", idx))),
2380 "dataset" => Value::Object(Obj::host(&format!("dataset:{}", idx))),
2381 "addEventListener" => Value::Object(Obj::native(
2382 "addEventListener",
2383 super::super::builtins::dom_add_event_listener,
2384 )),
2385 "removeEventListener" => Value::Object(Obj::native(
2386 "removeEventListener",
2387 super::super::builtins::dom_remove_event_listener,
2388 )),
2389 "dispatchEvent" => Value::Object(Obj::native(
2390 "dispatchEvent",
2391 super::super::builtins::dom_dispatch_event,
2392 )),
2393 "hasChildNodes" => Value::Object(Obj::native(
2395 "hasChildNodes",
2396 super::super::builtins::dom_has_child_nodes,
2397 )),
2398 "getAttribute" => Value::Object(Obj::native(
2399 "getAttribute",
2400 super::super::builtins::dom_get_attribute,
2401 )),
2402 "getAttributeNode" => Value::Object(Obj::native(
2406 "getAttributeNode",
2407 super::super::builtins::dom_get_attribute_node,
2408 )),
2409 "getAttributeNodeNS" => Value::Object(Obj::native(
2410 "getAttributeNodeNS",
2411 super::super::builtins::dom_get_attribute_node_ns,
2412 )),
2413 "setAttributeNode" => Value::Object(Obj::native(
2414 "setAttributeNode",
2415 super::super::builtins::dom_set_attribute_node,
2416 )),
2417 "removeAttributeNode" => Value::Object(Obj::native(
2418 "removeAttributeNode",
2419 super::super::builtins::dom_remove_attribute_node,
2420 )),
2421 "setAttribute" => Value::Object(Obj::native(
2422 "setAttribute",
2423 super::super::builtins::dom_set_attribute,
2424 )),
2425 "hasAttribute" => Value::Object(Obj::native(
2426 "hasAttribute",
2427 super::super::builtins::dom_has_attribute,
2428 )),
2429 "removeAttribute" => Value::Object(Obj::native(
2430 "removeAttribute",
2431 super::super::builtins::dom_remove_attribute,
2432 )),
2433 "getAttributeNS" => Value::Object(Obj::native(
2434 "getAttributeNS",
2435 super::super::builtins::dom_get_attribute_ns,
2436 )),
2437 "setAttributeNS" => Value::Object(Obj::native(
2438 "setAttributeNS",
2439 super::super::builtins::dom_set_attribute_ns,
2440 )),
2441 "hasAttributeNS" => Value::Object(Obj::native(
2442 "hasAttributeNS",
2443 super::super::builtins::dom_has_attribute_ns,
2444 )),
2445 "removeAttributeNS" => Value::Object(Obj::native(
2446 "removeAttributeNS",
2447 super::super::builtins::dom_remove_attribute_ns,
2448 )),
2449 "getAttributeNames" => Value::Object(Obj::native(
2450 "getAttributeNames",
2451 super::super::builtins::dom_get_attribute_names,
2452 )),
2453 "hasAttributes" => Value::Object(Obj::native(
2454 "hasAttributes",
2455 super::super::builtins::dom_has_attributes,
2456 )),
2457 "normalize" => Value::Object(Obj::native("normalize", super::super::builtins::dom_normalize)),
2458 "isSameNode" => Value::Object(Obj::native("isSameNode", super::super::builtins::dom_is_same_node)),
2459 "isEqualNode" => {
2460 Value::Object(Obj::native("isEqualNode", super::super::builtins::dom_is_equal_node))
2461 }
2462 "matches" | "webkitMatchesSelector" => {
2463 Value::Object(Obj::native("matches", super::super::builtins::dom_matches))
2464 }
2465 "closest" => Value::Object(Obj::native("closest", super::super::builtins::dom_closest)),
2466 "getBoundingClientRect" => Value::Object(Obj::native(
2467 "getBoundingClientRect",
2468 super::super::builtins::dom_get_bounding_client_rect,
2469 )),
2470 "getClientRects" => Value::Object(Obj::native(
2471 "getClientRects",
2472 super::super::builtins::dom_get_client_rects,
2473 )),
2474 "setPointerCapture" => Value::Object(Obj::native(
2475 "setPointerCapture",
2476 super::super::builtins::dom_set_pointer_capture,
2477 )),
2478 "releasePointerCapture" => Value::Object(Obj::native(
2479 "releasePointerCapture",
2480 super::super::builtins::dom_release_pointer_capture,
2481 )),
2482 "hasPointerCapture" => Value::Object(Obj::native(
2483 "hasPointerCapture",
2484 super::super::builtins::dom_has_pointer_capture,
2485 )),
2486 "offsetWidth" => {
2487 Value::Number(self.dom.borrow().get_rect(idx).map(|r| r.2).unwrap_or(0) as f64)
2488 }
2489 "clientWidth" => {
2490 let dom = self.dom.borrow();
2491 let w = dom.get_rect(idx).map(|r| r.2).unwrap_or(0);
2492 let borders = dom.get_border_widths(idx);
2493 Value::Number((w - borders.3 - borders.1).max(0) as f64)
2494 }
2495 "offsetHeight" => {
2496 Value::Number(self.dom.borrow().get_rect(idx).map(|r| r.3).unwrap_or(0) as f64)
2497 }
2498 "clientHeight" => {
2499 let dom = self.dom.borrow();
2500 let h = dom.get_rect(idx).map(|r| r.3).unwrap_or(0);
2501 let borders = dom.get_border_widths(idx);
2502 Value::Number((h - borders.0 - borders.2).max(0) as f64)
2503 }
2504 "offsetLeft" => {
2505 Value::Number(self.dom.borrow().get_rect(idx).map(|r| r.0).unwrap_or(0) as f64)
2506 }
2507 "offsetTop" => {
2508 Value::Number(self.dom.borrow().get_rect(idx).map(|r| r.1).unwrap_or(0) as f64)
2509 }
2510 "offsetParent" => match self.dom.borrow().offset_parent(idx) {
2514 Some(pi) => Value::Object(Obj::dom(pi)),
2515 None => Value::Null,
2516 },
2517 "scrollWidth" => {
2518 let dom = self.dom.borrow();
2519 let v = dom.scroll_widths.get(&idx).copied().unwrap_or_else(|| {
2520 dom.get_rect(idx).map(|r| r.2).unwrap_or(0)
2521 });
2522 Value::Number(v as f64)
2523 }
2524 "scrollHeight" => {
2525 let dom = self.dom.borrow();
2526 let v = dom.scroll_heights.get(&idx).copied().unwrap_or_else(|| {
2527 dom.get_rect(idx).map(|r| r.3).unwrap_or(0)
2528 });
2529 Value::Number(v as f64)
2530 }
2531 "scrollTop" => {
2532 let v = self.dom.borrow().scroll_tops.get(&idx).copied().unwrap_or(0);
2533 Value::Number(v as f64)
2534 }
2535 "scrollLeft" => {
2536 let v = self.dom.borrow().scroll_lefts.get(&idx).copied().unwrap_or(0);
2537 Value::Number(v as f64)
2538 }
2539 "clientLeft" => {
2540 let borders = self.dom.borrow().get_border_widths(idx);
2541 Value::Number(borders.3 as f64)
2542 }
2543 "clientTop" => {
2544 let borders = self.dom.borrow().get_border_widths(idx);
2545 Value::Number(borders.0 as f64)
2546 }
2547 "nodeType" => {
2552 let tag = self.dom.borrow().nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
2553 let is_text = self.dom.borrow().nodes.get(idx).map(|n| n.is_text).unwrap_or(false);
2554 let num = if is_text {
2555 3.0
2556 } else if tag == "#comment" {
2557 8.0
2558 } else if tag == "#document-fragment" || tag == "#fragment" {
2559 11.0
2560 } else {
2561 1.0
2562 };
2563 Value::Number(num)
2564 }
2565 "nodeName" => {
2566 let dom = self.dom.borrow();
2567 let name = match dom.nodes.get(idx) {
2568 Some(n) if n.is_text => String::from("#text"),
2569 Some(n) if n.tag == "#comment" => String::from("#comment"),
2570 Some(n) if n.tag == "#document-fragment" || n.tag == "#fragment" => String::from("#document-fragment"),
2571 Some(n) => n.tag.to_uppercase(),
2572 None => String::new(),
2573 };
2574 Value::str(name)
2575 }
2576 "nodeValue" => {
2581 let dom = self.dom.borrow();
2582 match dom.nodes.get(idx) {
2583 Some(n) if n.is_text || n.tag == "#comment" => Value::str(dom.get_text_content(idx)),
2584 _ => Value::Null,
2585 }
2586 }
2587 "data" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2594 Value::str(self.dom.borrow().get_text_content(idx))
2595 }
2596 "length" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2597 Value::Number(self.dom.borrow().get_text_content(idx).chars().count() as f64)
2598 }
2599 "appendData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2604 Value::Object(Obj::native("appendData", super::super::builtins::dom_char_data_append))
2605 }
2606 "deleteData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2607 Value::Object(Obj::native("deleteData", super::super::builtins::dom_char_data_delete))
2608 }
2609 "insertData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2610 Value::Object(Obj::native("insertData", super::super::builtins::dom_char_data_insert))
2611 }
2612 "replaceData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2613 Value::Object(Obj::native("replaceData", super::super::builtins::dom_char_data_replace))
2614 }
2615 "substringData" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false) => {
2616 Value::Object(Obj::native("substringData", super::super::builtins::dom_char_data_substring))
2617 }
2618 "splitText" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text).unwrap_or(false) => {
2620 Value::Object(Obj::native("splitText", super::super::builtins::dom_split_text))
2621 }
2622 "wholeText" if self.dom.borrow().nodes.get(idx).map(|n| n.is_text).unwrap_or(false) => {
2627 let dom = self.dom.borrow();
2628 let parent = dom.nodes.get(idx).and_then(|n| n.parent);
2629 let out = match parent {
2630 Some(p) => {
2631 let is_text = |c: usize| dom.nodes.get(c).map(|n| n.is_text).unwrap_or(false);
2632 let children = &dom.nodes[p].children;
2633 let pos = children.iter().position(|&c| c == idx).unwrap_or(0);
2634 let mut start = pos;
2635 while start > 0 && is_text(children[start - 1]) {
2636 start -= 1;
2637 }
2638 let mut end = pos;
2639 while end + 1 < children.len() && is_text(children[end + 1]) {
2640 end += 1;
2641 }
2642 children[start..=end].iter().map(|&c| dom.get_text_content(c)).collect()
2643 }
2644 None => dom.get_text_content(idx),
2645 };
2646 Value::str(out)
2647 }
2648 "childNodes" => {
2654 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2655 let kids = if is_template {
2656 Vec::new()
2657 } else {
2658 self.dom
2659 .borrow()
2660 .nodes
2661 .get(idx)
2662 .map(|n| n.children.clone())
2663 .unwrap_or_default()
2664 };
2665 Value::Object(Obj::array(
2666 kids.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
2667 ))
2668 }
2669 "firstChild" => {
2670 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2671 let first = if is_template {
2672 None
2673 } else {
2674 self.dom
2675 .borrow()
2676 .nodes
2677 .get(idx)
2678 .and_then(|n| n.children.first().copied())
2679 };
2680 match first {
2681 Some(i) => Value::Object(Obj::dom(i)),
2682 None => Value::Null,
2683 }
2684 }
2685 "lastChild" => {
2686 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2687 let last = if is_template {
2688 None
2689 } else {
2690 self.dom
2691 .borrow()
2692 .nodes
2693 .get(idx)
2694 .and_then(|n| n.children.last().copied())
2695 };
2696 match last {
2697 Some(i) => Value::Object(Obj::dom(i)),
2698 None => Value::Null,
2699 }
2700 }
2701 "nextSibling" | "previousSibling" => {
2702 let dom = self.dom.borrow();
2703 let sib = dom.nodes.get(idx).and_then(|n| n.parent).and_then(|p| {
2704 let kids = &dom.nodes.get(p)?.children;
2705 let pos = kids.iter().position(|&c| c == idx)?;
2706 if key == "nextSibling" {
2707 kids.get(pos + 1).copied()
2708 } else if pos > 0 {
2709 kids.get(pos - 1).copied()
2710 } else {
2711 None
2712 }
2713 });
2714 match sib {
2715 Some(i) => Value::Object(Obj::dom(i)),
2716 None => Value::Null,
2717 }
2718 }
2719 "hidden" => Value::Bool(self.dom.borrow().has_attr(idx, "hidden")),
2720 "querySelector" => Value::Object(Obj::native(
2721 "querySelector",
2722 super::super::builtins::dom_element_query_selector,
2723 )),
2724 "querySelectorAll" => Value::Object(Obj::native(
2725 "querySelectorAll",
2726 super::super::builtins::dom_element_query_selector_all,
2727 )),
2728 "getElementsByClassName" => Value::Object(Obj::native(
2732 "getElementsByClassName",
2733 super::super::builtins::dom_element_get_elements_by_class_name,
2734 )),
2735 "getElementsByTagName" => Value::Object(Obj::native(
2736 "getElementsByTagName",
2737 super::super::builtins::dom_element_get_elements_by_tag_name,
2738 )),
2739 "getElementsByTagNameNS" => Value::Object(Obj::native(
2740 "getElementsByTagNameNS",
2741 super::super::builtins::dom_element_get_elements_by_tag_name_ns,
2742 )),
2743 "appendChild" => Value::Object(Obj::native(
2744 "appendChild",
2745 super::super::builtins::dom_append_child,
2746 )),
2747 "insertBefore" => Value::Object(Obj::native(
2748 "insertBefore",
2749 super::super::builtins::dom_insert_before,
2750 )),
2751 "moveBefore" => Value::Object(Obj::native(
2754 "moveBefore",
2755 super::super::builtins::dom_move_before,
2756 )),
2757 "replaceChild" => Value::Object(Obj::native(
2758 "replaceChild",
2759 super::super::builtins::dom_replace_child,
2760 )),
2761 "cloneNode" => Value::Object(Obj::native("cloneNode", super::super::builtins::dom_clone_node)),
2762 "contains" => Value::Object(Obj::native("contains", super::super::builtins::dom_contains)),
2763 "compareDocumentPosition" => Value::Object(Obj::native(
2764 "compareDocumentPosition",
2765 super::super::builtins::dom_compare_document_position,
2766 )),
2767 "getRootNode" => Value::Object(Obj::native(
2768 "getRootNode",
2769 super::super::builtins::dom_get_root_node,
2770 )),
2771 "lookupNamespaceURI" => Value::Object(Obj::native(
2775 "lookupNamespaceURI",
2776 super::super::builtins::dom_lookup_namespace_uri,
2777 )),
2778 "lookupPrefix" => Value::Object(Obj::native(
2779 "lookupPrefix",
2780 super::super::builtins::dom_lookup_prefix,
2781 )),
2782 "checkVisibility" => Value::Object(Obj::native(
2784 "checkVisibility",
2785 super::super::builtins::dom_check_visibility,
2786 )),
2787 "isDefaultNamespace" => Value::Object(Obj::native(
2788 "isDefaultNamespace",
2789 super::super::builtins::dom_is_default_namespace,
2790 )),
2791 "requestFullscreen"
2794 | "webkitRequestFullscreen"
2795 | "webkitRequestFullScreen"
2796 | "mozRequestFullScreen"
2797 | "msRequestFullscreen" => Value::Object(Obj::native(
2798 "requestFullscreen",
2799 super::super::builtins::dom_request_fullscreen,
2800 )),
2801 "requestPointerLock" => Value::Object(Obj::native(
2804 "requestPointerLock",
2805 super::super::builtins::dom_request_pointer_lock,
2806 )),
2807 "requestPictureInPicture" => Value::Object(Obj::native(
2810 "requestPictureInPicture",
2811 super::super::builtins::dom_request_picture_in_picture,
2812 )),
2813 "append" => Value::Object(Obj::native("append", super::super::builtins::dom_append)),
2814 "prepend" => Value::Object(Obj::native("prepend", super::super::builtins::dom_prepend)),
2815 "replaceChildren" => Value::Object(Obj::native(
2818 "replaceChildren",
2819 super::super::builtins::dom_replace_children,
2820 )),
2821 "before" => Value::Object(Obj::native("before", super::super::builtins::dom_before)),
2822 "after" => Value::Object(Obj::native("after", super::super::builtins::dom_after)),
2823 "replaceWith" => Value::Object(Obj::native(
2824 "replaceWith",
2825 super::super::builtins::dom_replace_with,
2826 )),
2827 "insertAdjacentHTML" => Value::Object(Obj::native(
2828 "insertAdjacentHTML",
2829 super::super::builtins::dom_insert_adjacent_html,
2830 )),
2831 "getHTML" => Value::Object(Obj::native("getHTML", super::super::builtins::dom_get_html)),
2835 "setHTMLUnsafe" => Value::Object(Obj::native(
2836 "setHTMLUnsafe",
2837 super::super::builtins::dom_set_html_unsafe,
2838 )),
2839 "insertAdjacentElement" => Value::Object(Obj::native(
2840 "insertAdjacentElement",
2841 super::super::builtins::dom_insert_adjacent_element,
2842 )),
2843 "insertAdjacentText" => Value::Object(Obj::native(
2844 "insertAdjacentText",
2845 super::super::builtins::dom_insert_adjacent_text,
2846 )),
2847 "toggleAttribute" => Value::Object(Obj::native(
2848 "toggleAttribute",
2849 super::super::builtins::dom_toggle_attribute,
2850 )),
2851 "show" => Value::Object(Obj::native("show", super::super::builtins::dom_dialog_show)),
2855 "showModal" => {
2856 Value::Object(Obj::native("showModal", super::super::builtins::dom_dialog_show_modal))
2857 }
2858 "close" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "dialog").unwrap_or(false) => {
2859 Value::Object(Obj::native("close", super::super::builtins::dom_dialog_close))
2860 }
2861 "requestClose"
2865 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "dialog").unwrap_or(false) =>
2866 {
2867 Value::Object(Obj::native("requestClose", super::super::builtins::dom_dialog_request_close))
2868 }
2869 "showPopover" => Value::Object(Obj::native(
2872 "showPopover",
2873 super::super::builtins::dom_show_popover,
2874 )),
2875 "hidePopover" => Value::Object(Obj::native(
2876 "hidePopover",
2877 super::super::builtins::dom_hide_popover,
2878 )),
2879 "togglePopover" => Value::Object(Obj::native(
2880 "togglePopover",
2881 super::super::builtins::dom_toggle_popover,
2882 )),
2883 "removeChild" => Value::Object(Obj::native(
2884 "removeChild",
2885 super::super::builtins::dom_remove_child,
2886 )),
2887 "remove" => Value::Object(Obj::native("remove", super::super::builtins::dom_remove)),
2888 "ownerDocument" => match self.global.borrow().vars.get("document") {
2893 Some(d) => d.clone(),
2894 None => Value::Null,
2895 },
2896 "isConnected" => Value::Bool(self.dom.borrow().is_attached(idx)),
2902 "parentElement" | "parentNode" => {
2904 let p = self.dom.borrow().nodes.get(idx).and_then(|n| n.parent);
2905 match p {
2906 Some(pi) => Value::Object(Obj::dom(pi)),
2907 None => Value::Null,
2908 }
2909 }
2910 "children" => {
2911 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2912 let kids = if is_template { Vec::new() } else { self.dom.borrow().element_children(idx) };
2913 super::super::builtins::make_html_collection(self, kids)
2914 }
2915 "childElementCount" => {
2916 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2917 let count = if is_template { 0 } else { self.dom.borrow().element_children(idx).len() };
2918 Value::Number(count as f64)
2919 }
2920 "firstElementChild" => {
2921 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2922 let first = if is_template { None } else { self.dom.borrow().element_children(idx).first().copied() };
2923 match first {
2924 Some(i) => Value::Object(Obj::dom(i)),
2925 None => Value::Null,
2926 }
2927 }
2928 "lastElementChild" => {
2929 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
2930 let last = if is_template { None } else { self.dom.borrow().element_children(idx).last().copied() };
2931 match last {
2932 Some(i) => Value::Object(Obj::dom(i)),
2933 None => Value::Null,
2934 }
2935 }
2936 "nextElementSibling" | "previousElementSibling" => {
2937 let dom = self.dom.borrow();
2938 let sib = dom.nodes.get(idx).and_then(|n| n.parent).and_then(|p| {
2939 let kids = dom.element_children(p);
2940 let pos = kids.iter().position(|&c| c == idx);
2941 match pos {
2942 Some(pi) if key == "nextElementSibling" => kids.get(pi + 1).copied(),
2943 Some(pi) if pi > 0 => kids.get(pi - 1).copied(),
2944 _ => None,
2945 }
2946 });
2947 match sib {
2948 Some(i) => Value::Object(Obj::dom(i)),
2949 None => Value::Null,
2950 }
2951 }
2952 "click" => Value::Object(Obj::native("click", super::super::builtins::dom_click)),
2953 "requestSubmit" => {
2962 Value::Object(Obj::native("requestSubmit", super::super::builtins::dom_request_submit))
2963 }
2964 "submit" => Value::Object(Obj::native("domNoop", |_, _, _| Ok(Value::Undefined))),
2965 "reset" => Value::Object(Obj::native("reset", super::super::builtins::dom_form_reset)),
2966 "showPicker" => Value::Object(Obj::native("domNoop", |_, _, _| Ok(Value::Undefined))),
2973 "getAnimations" => Value::Object(Obj::native(
2978 "getAnimations",
2979 |_, _, _| Ok(Value::Object(Obj::array(alloc::vec![]))),
2980 )),
2981 "stepUp" => Value::Object(Obj::native("stepUp", super::super::builtins::dom_step_up)),
2984 "stepDown" => Value::Object(Obj::native("stepDown", super::super::builtins::dom_step_down)),
2985 "scroll" => Value::Object(Obj::native("scroll", super::super::builtins::dom_scroll_to)),
2988 "scrollTo" => Value::Object(Obj::native("scrollTo", super::super::builtins::dom_scroll_to)),
2989 "scrollBy" => Value::Object(Obj::native("scrollBy", super::super::builtins::dom_scroll_by)),
2990 "scrollIntoView" => Value::Object(Obj::native("scrollIntoView", super::super::builtins::dom_scroll_into_view)),
2994 "scrollIntoViewIfNeeded" => Value::Object(Obj::native("scrollIntoViewIfNeeded", super::super::builtins::dom_scroll_into_view)),
2995 "focus" => Value::Object(Obj::native("focus", super::super::builtins::dom_focus)),
2999 "blur" => Value::Object(Obj::native("blur", super::super::builtins::dom_blur)),
3000 "play" => Value::Object(Obj::native("play", super::super::builtins::dom_media_play)),
3007 "pause" => Value::Object(Obj::native("pause", super::super::builtins::dom_media_pause)),
3008 "load" => Value::Object(Obj::native("domNoop", |_, _, _| Ok(Value::Undefined))),
3009 "canPlayType" => {
3012 Value::Object(Obj::native("canPlayType", |_, _, args| {
3013 let type_str = args.first().map(|v| v.to_js_string()).unwrap_or_default();
3014 let res = crate::os_lib::web_engine::media_controls::can_play_type(&type_str);
3015 Ok(Value::str(res))
3016 }))
3017 }
3018 "animate" => Value::Object(Obj::native("animate", |_, _, _| {
3020 let anim = Obj::plain();
3021 {
3022 let mut b = anim.borrow_mut();
3023 b.props.insert(
3024 "play".into(),
3025 Value::Object(Obj::native("play", |_, _, _| Ok(Value::Undefined))),
3026 );
3027 b.props.insert(
3028 "pause".into(),
3029 Value::Object(Obj::native("pause", |_, _, _| Ok(Value::Undefined))),
3030 );
3031 b.props.insert(
3032 "cancel".into(),
3033 Value::Object(Obj::native("cancel", |_, _, _| Ok(Value::Undefined))),
3034 );
3035 b.props.insert(
3039 "reverse".into(),
3040 Value::Object(Obj::native("reverse", |_, _, _| Ok(Value::Undefined))),
3041 );
3042 b.props.insert(
3043 "finish".into(),
3044 Value::Object(Obj::native("finish", |_, _, _| Ok(Value::Undefined))),
3045 );
3046 b.props.insert(
3047 "finished".into(),
3048 Value::Object(super::super::builtins::make_resolved_promise(Value::Undefined)),
3049 );
3050 b.props.insert("playState".into(), Value::str("finished"));
3051 b.props.insert("currentTime".into(), Value::Number(0.0));
3052 b.props.insert("playbackRate".into(), Value::Number(1.0));
3056 }
3057 Ok(Value::Object(anim))
3058 })),
3059 "checkValidity" => Value::Object(Obj::native(
3061 "checkValidity",
3062 super::super::builtins::dom_check_validity,
3063 )),
3064 "reportValidity" => Value::Object(Obj::native(
3065 "reportValidity",
3066 super::super::builtins::dom_check_validity,
3067 )),
3068 "setCustomValidity" => Value::Object(Obj::native(
3069 "setCustomValidity",
3070 super::super::builtins::dom_set_custom_validity,
3071 )),
3072 "validationMessage" => {
3073 let v = self.dom.borrow().effective_form_value(idx);
3074 Value::str(
3075 self.dom
3076 .borrow()
3077 .validate_field(idx, &v)
3078 .unwrap_or_default(),
3079 )
3080 }
3081 "willValidate" => {
3082 let dom = self.dom.borrow();
3091 let tag = dom.nodes.get(idx).map(|n| n.tag.as_str()).unwrap_or("");
3092 let is_candidate_tag = matches!(tag, "button" | "input" | "select" | "textarea");
3093 let disabled = dom.has_attr(idx, "disabled");
3094 let ty = dom.get_attr(idx, "type").unwrap_or_default().to_lowercase();
3095 let excluded_type = matches!(
3096 ty.as_str(),
3097 "submit" | "button" | "reset" | "hidden" | "image"
3098 );
3099 Value::Bool(is_candidate_tag && !disabled && !excluded_type)
3100 }
3101 "validity" => {
3108 let v = self.dom.borrow().effective_form_value(idx);
3109 let flags = self.dom.borrow().validity_flags(idx, &v);
3110 let vs = Obj::plain();
3111 {
3112 let mut b = vs.borrow_mut();
3113 b.props.insert("valid".into(), Value::Bool(flags.valid()));
3114 b.props
3115 .insert("valueMissing".into(), Value::Bool(flags.value_missing));
3116 b.props
3117 .insert("typeMismatch".into(), Value::Bool(flags.type_mismatch));
3118 b.props
3119 .insert("patternMismatch".into(), Value::Bool(flags.pattern_mismatch));
3120 b.props.insert("tooLong".into(), Value::Bool(flags.too_long));
3121 b.props.insert("tooShort".into(), Value::Bool(flags.too_short));
3122 b.props
3123 .insert("rangeOverflow".into(), Value::Bool(flags.range_overflow));
3124 b.props
3125 .insert("rangeUnderflow".into(), Value::Bool(flags.range_underflow));
3126 b.props
3127 .insert("stepMismatch".into(), Value::Bool(flags.step_mismatch));
3128 b.props.insert("badInput".into(), Value::Bool(flags.bad_input));
3129 b.props
3130 .insert("customError".into(), Value::Bool(flags.custom_error));
3131 }
3132 Value::Object(vs)
3133 }
3134 key if aria_attr_name(key).is_some() => {
3141 let attr = aria_attr_name(key).unwrap_or_default();
3142 match self.dom.borrow().get_attr(idx, attr) {
3143 Some(v) => Value::str(v),
3144 None => Value::Null,
3145 }
3146 }
3147 key if key.starts_with("on") && key.len() > 2 => {
3152 if let Some(val) = self.dom.borrow().get_on_handler(idx, key) {
3153 if !matches!(val, Value::Undefined) {
3154 return val;
3155 }
3156 }
3157 match self.dom.borrow().get_attr(idx, key) {
3158 Some(v) => Value::str(v),
3159 None => Value::Null,
3160 }
3161 }
3162 _ if builtin_only => Value::Undefined,
3167 _ => self.dom_proto_chain_get(idx, key),
3168 }
3169 }
3170
3171 fn dom_proto_chain_get(&mut self, idx: usize, key: &str) -> Value {
3173 let recv = Value::Object(Obj::dom(idx));
3174 let mut cur = self.get_dom_node_proto(idx);
3175 let mut guard = 0;
3176 while let Some(p) = cur {
3177 let getter = p.borrow().accessors.get(key).map(|a| a.get.clone());
3178 if let Some(g) = getter {
3179 return match g {
3180 Some(g) => self.call_value(&g, recv, &[]).unwrap_or(Value::Undefined),
3181 None => Value::Undefined,
3182 };
3183 }
3184 if let Some(v) = p.borrow().props.get(key).cloned() {
3185 return v;
3186 }
3187 guard += 1;
3188 if guard > 64 {
3189 break;
3190 }
3191 cur = p.borrow().proto.clone();
3192 }
3193 super::super::builtins::object_get(&Obj::plain(), key)
3196 }
3197
3198 pub(crate) fn dom_set_property(&mut self, disp: DomDisp, key: &str, val: Value) {
3199 let s = val.to_js_string();
3200 let s_nullable = if matches!(val, Value::Null | Value::Undefined) {
3208 String::new()
3209 } else {
3210 s.clone()
3211 };
3212 match disp {
3213 DomDisp::Element(idx) => match key {
3214 "textContent" | "innerText" | "text" => {
3215 self.dom.borrow_mut().set_text_content(idx, &s_nullable)
3216 }
3217 "outerText" => {
3224 let parent = self.dom.borrow().nodes.get(idx).and_then(|n| n.parent);
3225 if let Some(parent) = parent {
3226 let text_idx = self.dom.borrow_mut().create_text_node(&s_nullable);
3227 self.dom.borrow_mut().replace_child(parent, text_idx, idx);
3228 }
3229 }
3230 "nodeValue" => {
3236 let has_val = self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false);
3237 if has_val {
3238 self.dom.borrow_mut().set_text_content(idx, &s_nullable);
3239 }
3240 }
3241 "data" if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "object").unwrap_or(false) => {
3245 self.dom.borrow_mut().set_attr(idx, "data", &s);
3246 }
3247 "data" => {
3250 let has_data = self.dom.borrow().nodes.get(idx).map(|n| n.is_text || n.tag == "#comment").unwrap_or(false);
3251 if has_data {
3252 self.dom.borrow_mut().set_text_content(idx, &s_nullable);
3253 }
3254 }
3255 "innerHTML" => {
3256 let is_template = self.dom.borrow().nodes.get(idx).map(|n| n.tag == "template").unwrap_or(false);
3257 if is_template {
3258 let frag_val = self.dom_element_get(idx, "content");
3259 if let Some(fi) = this_dom_idx(&frag_val) {
3260 self.dom.borrow_mut().set_inner_html(fi, &s_nullable);
3261 } else {
3262 self.dom.borrow_mut().set_inner_html(idx, &s_nullable);
3263 }
3264 } else {
3265 self.dom.borrow_mut().set_inner_html(idx, &s_nullable);
3266 }
3267 }
3268 "outerHTML" => self.dom.borrow_mut().set_outer_html(idx, &s_nullable),
3269 "id" => self.dom.borrow_mut().set_attr(idx, "id", &s),
3270 "className" => self.dom.borrow_mut().set_attr(idx, "class", &s),
3271 "value" => {
3272 let tag = self.dom.borrow().nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
3273 if tag == "select" {
3274 self.dom.borrow_mut().set_select_value(idx, &s);
3275 } else if tag == "input" || tag == "textarea" {
3276 let sanitized = if tag == "input"
3277 && self.dom.borrow().get_attr(idx, "type").as_deref() == Some("color")
3278 {
3279 let is_valid_hex = s.len() == 7
3280 && s.starts_with('#')
3281 && s.chars().skip(1).all(|c| c.is_ascii_hexdigit());
3282 if is_valid_hex {
3283 s.to_lowercase()
3284 } else {
3285 String::from("#000000")
3286 }
3287 } else {
3288 s
3289 };
3290 self.dom.borrow_mut().set_attr(idx, "_live_value", &sanitized);
3291 } else {
3292 self.dom.borrow_mut().set_attr(idx, "value", &s);
3293 }
3294 }
3295 "valueAsNumber" => {
3296 let n = val.to_number();
3297 let mut dom = self.dom.borrow_mut();
3298 if n.is_nan() {
3299 dom.set_attr(idx, "_live_value", "");
3300 dom.set_attr(idx, "value", "");
3301 } else {
3302 let str_v = Value::Number(n).to_js_string();
3303 dom.set_attr(idx, "_live_value", &str_v);
3304 dom.set_attr(idx, "value", &str_v);
3305 }
3306 }
3307 "valueAsDate" => {
3308 let ty = self.dom.borrow().get_attr(idx, "type").unwrap_or_default();
3309 let mut dom = self.dom.borrow_mut();
3310 if matches!(val, Value::Null | Value::Undefined) {
3311 dom.set_attr(idx, "_live_value", "");
3312 dom.set_attr(idx, "value", "");
3313 } else {
3314 let ms = super::super::builtins::date_value_ms(&val);
3315 if let Some(s) = super::super::builtins::input_date_ms_to_value(&ty, ms) {
3316 dom.set_attr(idx, "_live_value", &s);
3317 dom.set_attr(idx, "value", &s);
3318 } else {
3319 dom.set_attr(idx, "_live_value", "");
3320 dom.set_attr(idx, "value", "");
3321 }
3322 }
3323 }
3324 "selectedIndex" => {
3325 let n = val.to_number() as i32;
3326 self.dom.borrow_mut().set_select_selected_index(idx, n);
3327 }
3328 "length"
3333 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false) =>
3334 {
3335 let n = val.to_number();
3336 let n = if n.is_finite() && n > 0.0 { n as usize } else { 0 };
3337 self.dom.borrow_mut().set_select_length(idx, n);
3338 }
3339 "selectionStart" => self.dom.borrow_mut().set_attr(idx, "_selection_start", &s),
3343 "selectionEnd" => self.dom.borrow_mut().set_attr(idx, "_selection_end", &s),
3344 "selectionDirection" => self.dom.borrow_mut().set_attr(idx, "_selection_direction", &s),
3345 "type" => self.dom.borrow_mut().set_attr(idx, "type", &s),
3346 "defaultValue" => self.dom.borrow_mut().set_attr(idx, "value", &s),
3349 "defaultChecked" => {
3350 let mut dom = self.dom.borrow_mut();
3351 if val.truthy() {
3352 dom.set_attr(idx, "checked", "checked");
3353 } else {
3354 dom.remove_attr(idx, "checked");
3355 }
3356 }
3357 "defaultSelected" => {
3358 let mut dom = self.dom.borrow_mut();
3359 if val.truthy() {
3360 dom.set_attr(idx, "selected", "selected");
3361 } else {
3362 dom.remove_attr(idx, "selected");
3363 }
3364 }
3365 "checked" => {
3369 let mut dom = self.dom.borrow_mut();
3370 if val.truthy() {
3371 dom.set_attr(idx, "_live_checked", "true");
3372 dom.uncheck_radio_group_siblings(idx);
3373 } else {
3374 dom.set_attr(idx, "_live_checked", "false");
3375 }
3376 }
3377 "open" => {
3380 let mut dom = self.dom.borrow_mut();
3381 if val.truthy() {
3382 dom.set_attr(idx, "open", "open");
3383 } else {
3384 dom.remove_attr(idx, "open");
3385 }
3386 }
3387 "inert" => {
3389 let mut dom = self.dom.borrow_mut();
3390 if val.truthy() {
3391 dom.set_attr(idx, "inert", "");
3392 } else {
3393 dom.remove_attr(idx, "inert");
3394 }
3395 }
3396 "returnValue" => {
3398 self.dom
3399 .borrow_mut()
3400 .set_attr(idx, "_return_value", &val.to_js_string());
3401 }
3402 "indeterminate" => {
3403 let mut dom = self.dom.borrow_mut();
3404 if val.truthy() {
3405 dom.set_attr(idx, "_indeterminate", "true");
3406 } else {
3407 dom.remove_attr(idx, "_indeterminate");
3408 }
3409 }
3410 "disabled" => {
3411 let mut dom = self.dom.borrow_mut();
3412 if val.truthy() {
3413 dom.set_attr(idx, "disabled", "disabled");
3414 } else {
3415 dom.remove_attr(idx, "disabled");
3416 }
3417 }
3418 "selected" => {
3419 let mut dom = self.dom.borrow_mut();
3420 if val.truthy() {
3421 dom.set_attr(idx, "selected", "selected");
3422 } else {
3423 dom.remove_attr(idx, "selected");
3424 }
3425 }
3426
3427 "placeholder" => self.dom.borrow_mut().set_attr(idx, "placeholder", &s),
3428 "pattern" => self.dom.borrow_mut().set_attr(idx, "pattern", &s),
3431 "capture" => self.dom.borrow_mut().set_attr(idx, "capture", &s),
3434 "accept" => self.dom.borrow_mut().set_attr(idx, "accept", &s),
3437 "name" => self.dom.borrow_mut().set_attr(idx, "name", &s),
3438 "required" => {
3439 let mut dom = self.dom.borrow_mut();
3440 if val.truthy() {
3441 dom.set_attr(idx, "required", "required");
3442 } else {
3443 dom.remove_attr(idx, "required");
3444 }
3445 }
3446 "noValidate" => {
3447 let mut dom = self.dom.borrow_mut();
3448 if val.truthy() {
3449 dom.set_attr(idx, "novalidate", "novalidate");
3450 } else {
3451 dom.remove_attr(idx, "novalidate");
3452 }
3453 }
3454 "formNoValidate" => {
3455 let mut dom = self.dom.borrow_mut();
3456 if val.truthy() {
3457 dom.set_attr(idx, "formnovalidate", "formnovalidate");
3458 } else {
3459 dom.remove_attr(idx, "formnovalidate");
3460 }
3461 }
3462 "reversed" => {
3465 let mut dom = self.dom.borrow_mut();
3466 if val.truthy() {
3467 dom.set_attr(idx, "reversed", "reversed");
3468 } else {
3469 dom.remove_attr(idx, "reversed");
3470 }
3471 }
3472 "start" => self.dom.borrow_mut().set_attr(idx, "start", &s),
3473 "readOnly" => {
3474 let mut dom = self.dom.borrow_mut();
3475 if val.truthy() {
3476 dom.set_attr(idx, "readonly", "readonly");
3477 } else {
3478 dom.remove_attr(idx, "readonly");
3479 }
3480 }
3481 "hidden" => {
3486 let mut dom = self.dom.borrow_mut();
3487 if val.truthy() {
3488 dom.set_attr(idx, "hidden", "hidden");
3489 } else {
3490 dom.remove_attr(idx, "hidden");
3491 }
3492 }
3493 "multiple" => {
3494 let mut dom = self.dom.borrow_mut();
3495 if val.truthy() {
3496 dom.set_attr(idx, "multiple", "multiple");
3497 } else {
3498 dom.remove_attr(idx, "multiple");
3499 }
3500 }
3501 "src" => {
3502 let s_val = s.clone();
3503 self.dom.borrow_mut().set_attr(idx, "src", &s);
3504 let is_media = self
3505 .dom
3506 .borrow()
3507 .nodes
3508 .get(idx)
3509 .map(|n| n.tag == "video" || n.tag == "audio")
3510 .unwrap_or(false);
3511 if is_media && s_val.starts_with("blob:") {
3512 let ms_val = match self.global.borrow().vars.get("__media_source_urls") {
3513 Some(Value::Object(r)) => r.borrow().props.get(&s_val).cloned(),
3514 _ => None,
3515 };
3516 if let Some(ms) = ms_val {
3517 super::super::builtins::media_source_open_internal(self, &ms);
3518 }
3519 }
3520 }
3521 "srcObject" => {
3522 let is_media = self
3523 .dom
3524 .borrow()
3525 .nodes
3526 .get(idx)
3527 .map(|n| n.tag == "video" || n.tag == "audio")
3528 .unwrap_or(false);
3529 if is_media {
3530 let is_ms = if let Value::Object(o) = &val {
3531 let b = o.borrow();
3532 b.props.contains_key("sourceBuffers")
3533 || b.props.get("_is_media_source").map(|v| v.truthy()).unwrap_or(false)
3534 } else {
3535 false
3536 };
3537 if is_ms {
3538 super::super::builtins::media_source_open_internal(self, &val);
3539 }
3540 }
3541 }
3542 "alt" => self.dom.borrow_mut().set_attr(idx, "alt", &s),
3543 "autoplay" => {
3544 let mut dom = self.dom.borrow_mut();
3545 if val.truthy() {
3546 dom.set_attr(idx, "autoplay", "autoplay");
3547 } else {
3548 dom.remove_attr(idx, "autoplay");
3549 }
3550 }
3551 "controls" => {
3552 let mut dom = self.dom.borrow_mut();
3553 if val.truthy() {
3554 dom.set_attr(idx, "controls", "controls");
3555 } else {
3556 dom.remove_attr(idx, "controls");
3557 }
3558 }
3559 "loop" => {
3560 let mut dom = self.dom.borrow_mut();
3561 if val.truthy() {
3562 dom.set_attr(idx, "loop", "loop");
3563 } else {
3564 dom.remove_attr(idx, "loop");
3565 }
3566 }
3567 "muted" => {
3568 let is_muted = val.truthy();
3569 {
3570 let mut dom = self.dom.borrow_mut();
3571 if is_muted {
3572 dom.set_attr(idx, "muted", "muted");
3573 } else {
3574 dom.remove_attr(idx, "muted");
3575 }
3576 }
3577 let src = self.dom.borrow().get_attr(idx, "src").unwrap_or_default();
3581 let mut pb = crate::os_lib::mse::PLAYBACK.lock();
3582 if !src.is_empty() && (pb.url.is_empty() || pb.url == src) {
3583 pb.muted = is_muted;
3584 drop(pb);
3585 if is_muted {
3586 crate::kernel::audio::Audio::mute();
3587 }
3588 }
3589 let _ = self.dispatch_event_in_interp(idx, "volumechange", &[]);
3591 }
3592 "playsInline" => {
3593 let mut dom = self.dom.borrow_mut();
3594 if val.truthy() {
3595 dom.set_attr(idx, "playsinline", "playsinline");
3596 } else {
3597 dom.remove_attr(idx, "playsinline");
3598 }
3599 }
3600 "disablePictureInPicture" => {
3603 let mut dom = self.dom.borrow_mut();
3604 if val.truthy() {
3605 dom.set_attr(idx, "disablepictureinpicture", "disablepictureinpicture");
3606 } else {
3607 dom.remove_attr(idx, "disablepictureinpicture");
3608 }
3609 }
3610 "disableRemotePlayback" => {
3613 let mut dom = self.dom.borrow_mut();
3614 if val.truthy() {
3615 dom.set_attr(idx, "disableremoteplayback", "disableremoteplayback");
3616 } else {
3617 dom.remove_attr(idx, "disableremoteplayback");
3618 }
3619 }
3620 "allowFullscreen" => {
3621 let mut dom = self.dom.borrow_mut();
3622 if val.truthy() {
3623 dom.set_attr(idx, "allowfullscreen", "allowfullscreen");
3624 } else {
3625 dom.remove_attr(idx, "allowfullscreen");
3626 }
3627 }
3628 "credentialless" => {
3631 let mut dom = self.dom.borrow_mut();
3632 if val.truthy() {
3633 dom.set_attr(idx, "credentialless", "credentialless");
3634 } else {
3635 dom.remove_attr(idx, "credentialless");
3636 }
3637 }
3638 "width"
3641 if self
3642 .dom
3643 .borrow()
3644 .nodes
3645 .get(idx)
3646 .map(|n| n.tag == "canvas" || n.tag == "img" || n.tag == "video")
3647 .unwrap_or(false) =>
3648 {
3649 self.dom.borrow_mut().set_attr(idx, "width", &s);
3650 }
3651 "height"
3652 if self
3653 .dom
3654 .borrow()
3655 .nodes
3656 .get(idx)
3657 .map(|n| n.tag == "canvas" || n.tag == "img" || n.tag == "video")
3658 .unwrap_or(false) =>
3659 {
3660 self.dom.borrow_mut().set_attr(idx, "height", &s);
3661 }
3662 "width"
3665 if self
3666 .dom
3667 .borrow()
3668 .nodes
3669 .get(idx)
3670 .map(|n| n.tag == "embed" || n.tag == "object" || n.tag == "iframe")
3671 .unwrap_or(false) =>
3672 {
3673 self.dom.borrow_mut().set_attr(idx, "width", &s);
3674 }
3675 "height"
3676 if self
3677 .dom
3678 .borrow()
3679 .nodes
3680 .get(idx)
3681 .map(|n| n.tag == "embed" || n.tag == "object" || n.tag == "iframe")
3682 .unwrap_or(false) =>
3683 {
3684 self.dom.borrow_mut().set_attr(idx, "height", &s);
3685 }
3686 "currentTime" => {
3694 let t = val.to_number();
3695 self.dom.borrow_mut().set_attr(idx, "_current_time", &t.to_string());
3696 let has_src = self.dom.borrow().get_attr(idx, "src").is_some_and(|s| !s.is_empty());
3698 if t.is_finite() && has_src {
3699 *crate::os_lib::mse::SEEK_REQUEST.lock() = Some(t.max(0.0));
3700 crate::os_lib::mse::PLAYBACK.lock().current_time = t.max(0.0);
3701 }
3702 let _ = self.dispatch_event_in_interp(idx, "seeking", &[]);
3703 let _ = self.dispatch_event_in_interp(idx, "seeked", &[]);
3704 }
3705 "volume" => {
3706 let v = val.to_number().clamp(0.0, 1.0);
3707 self.dom.borrow_mut().set_attr(idx, "_volume", &v.to_string());
3708 let _ = crate::os_lib::mse::PLAYBACK.lock().set_volume(v);
3709 if v <= 0.0 {
3710 crate::kernel::audio::Audio::mute();
3711 }
3712 let _ = self.dispatch_event_in_interp(idx, "volumechange", &[]);
3713 }
3714 "playbackRate" => {
3715 let r = val.to_number();
3716 self.dom.borrow_mut().set_attr(idx, "_playback_rate", &r.to_string());
3717 let _ = crate::os_lib::mse::PLAYBACK.lock().set_playback_rate(r);
3718 let _ = self.dispatch_event_in_interp(idx, "ratechange", &[]);
3719 }
3720 "defaultPlaybackRate" => self
3721 .dom
3722 .borrow_mut()
3723 .set_attr(idx, "_default_playback_rate", &val.to_number().to_string()),
3724 "nonce" => self.dom.borrow_mut().set_attr(idx, "nonce", &s),
3725 "role" => {
3729 if matches!(val, Value::Null) {
3730 self.dom.borrow_mut().remove_attr(idx, "role");
3731 } else {
3732 self.dom.borrow_mut().set_attr(idx, "role", &s);
3733 }
3734 }
3735 _ if key.starts_with("aria") && key.ends_with("Elements") && key.len() > 12 => {
3748 let suffix: String =
3749 key.chars().skip(4).take(key.len() - 4 - 8).collect::<String>().to_lowercase();
3750 let attr = alloc::format!("aria-{}", suffix);
3751 if matches!(val, Value::Null | Value::Undefined) {
3752 self.dom.borrow_mut().remove_attr(idx, &attr);
3753 } else {
3754 let items = self.iterate_values(&val, true);
3755 let ids: Vec<String> = items
3756 .iter()
3757 .filter_map(|v| match v {
3758 Value::Object(o) => match &o.borrow().kind {
3759 ObjKind::DomElement(ti) => {
3760 self.dom.borrow().nodes.get(*ti).map(|n| n.id.clone())
3761 }
3762 _ => None,
3763 },
3764 _ => None,
3765 })
3766 .filter(|s| !s.is_empty())
3767 .collect();
3768 self.dom.borrow_mut().set_attr(idx, &attr, &ids.join(" "));
3769 }
3770 }
3771 "ariaActiveDescendantElement" => match val {
3772 Value::Null => {
3773 self.dom.borrow_mut().remove_attr(idx, "aria-activedescendant");
3774 }
3775 Value::Object(o) if matches!(&o.borrow().kind, ObjKind::DomElement(_)) => {
3776 if let ObjKind::DomElement(ti) = &o.borrow().kind {
3777 let target_id = self.dom.borrow().nodes.get(*ti).map(|n| n.id.clone());
3778 if let Some(tid) = target_id.filter(|s| !s.is_empty()) {
3779 self.dom.borrow_mut().set_attr(idx, "aria-activedescendant", &tid);
3780 }
3781 }
3782 }
3783 _ => {}
3784 },
3785 _ if key.starts_with("aria") && key.as_bytes().get(4).is_some_and(u8::is_ascii_uppercase) => {
3786 let suffix: String = key.chars().skip(4).collect::<String>().to_lowercase();
3787 let attr = alloc::format!("aria-{}", suffix);
3788 if matches!(val, Value::Null) {
3789 self.dom.borrow_mut().remove_attr(idx, &attr);
3790 } else {
3791 self.dom.borrow_mut().set_attr(idx, &attr, &s);
3792 }
3793 }
3794 "preload" => self.dom.borrow_mut().set_attr(idx, "preload", &s),
3795 "wrap" => self.dom.borrow_mut().set_attr(idx, "wrap", &s),
3798 "poster" => self.dom.borrow_mut().set_attr(idx, "poster", &s),
3799 "kind" => self.dom.borrow_mut().set_attr(idx, "kind", &s),
3801 "label" => self.dom.borrow_mut().set_attr(idx, "label", &s),
3807 "srclang" => self.dom.borrow_mut().set_attr(idx, "srclang", &s),
3808 "default" => {
3809 let mut dom = self.dom.borrow_mut();
3810 if val.truthy() {
3811 dom.set_attr(idx, "default", "default");
3812 } else {
3813 dom.remove_attr(idx, "default");
3814 }
3815 }
3816 "ping" => self.dom.borrow_mut().set_attr(idx, "ping", &s),
3817 "hreflang" => self.dom.borrow_mut().set_attr(idx, "hreflang", &s),
3818 "srcset" => self.dom.borrow_mut().set_attr(idx, "srcset", &s),
3819 "sizes" => self.dom.borrow_mut().set_attr(idx, "sizes", &s),
3820 "media" => self.dom.borrow_mut().set_attr(idx, "media", &s),
3821 "useMap" => self.dom.borrow_mut().set_attr(idx, "usemap", &s),
3822 "isMap" => {
3823 let mut dom = self.dom.borrow_mut();
3824 if val.truthy() {
3825 dom.set_attr(idx, "ismap", "ismap");
3826 } else {
3827 dom.remove_attr(idx, "ismap");
3828 }
3829 }
3830 "referrerPolicy" => self.dom.borrow_mut().set_attr(idx, "referrerpolicy", &s),
3831 "as" => self.dom.borrow_mut().set_attr(idx, "as", &s),
3833 "imageSrcset" => self.dom.borrow_mut().set_attr(idx, "imagesrcset", &s),
3836 "imageSizes" => self.dom.borrow_mut().set_attr(idx, "imagesizes", &s),
3837 "loading" => self.dom.borrow_mut().set_attr(idx, "loading", &s),
3838 "decoding" => self.dom.borrow_mut().set_attr(idx, "decoding", &s),
3839 "allow" => self.dom.borrow_mut().set_attr(idx, "allow", &s),
3840 "srcdoc" => self.dom.borrow_mut().set_attr(idx, "srcdoc", &s),
3843 "crossOrigin" => {
3845 if matches!(val, Value::Null) {
3846 self.dom.borrow_mut().remove_attr(idx, "crossorigin");
3847 } else {
3848 self.dom.borrow_mut().set_attr(idx, "crossorigin", &s);
3849 }
3850 }
3851 "fetchPriority" => self.dom.borrow_mut().set_attr(idx, "fetchpriority", &s),
3852 "integrity" => self.dom.borrow_mut().set_attr(idx, "integrity", &s),
3853 "async" => {
3854 let mut dom = self.dom.borrow_mut();
3855 if val.truthy() {
3856 dom.set_attr(idx, "async", "async");
3857 } else {
3858 dom.remove_attr(idx, "async");
3859 }
3860 }
3861 "defer" => {
3862 let mut dom = self.dom.borrow_mut();
3863 if val.truthy() {
3864 dom.set_attr(idx, "defer", "defer");
3865 } else {
3866 dom.remove_attr(idx, "defer");
3867 }
3868 }
3869 "noModule" => {
3870 let mut dom = self.dom.borrow_mut();
3871 if val.truthy() {
3872 dom.set_attr(idx, "nomodule", "nomodule");
3873 } else {
3874 dom.remove_attr(idx, "nomodule");
3875 }
3876 }
3877 "href" => self.dom.borrow_mut().set_attr(idx, "href", &s),
3878 "protocol" | "host" | "hostname" | "port" | "pathname" | "search" | "hash"
3887 | "username" | "password"
3888 if self
3889 .dom
3890 .borrow()
3891 .nodes
3892 .get(idx)
3893 .map(|n| n.tag == "a" || n.tag == "area")
3894 .unwrap_or(false) =>
3895 {
3896 let raw = self.dom.borrow().get_attr(idx, "href").unwrap_or_default();
3897 let abs = self.resolve_url_against_page(&raw);
3898 let comps = super::super::builtins::location_components(&abs);
3899 let get = |k: &str| {
3900 comps.iter().find(|(kk, _)| *kk == k).map(|(_, v)| v.clone()).unwrap_or_default()
3901 };
3902 let mut protocol = get("protocol");
3903 let mut hostname = get("hostname");
3904 let mut port = get("port");
3905 let mut pathname = get("pathname");
3906 let mut search = get("search");
3907 let mut hash = get("hash");
3908 let mut username = get("username");
3909 let mut password = get("password");
3910 match key {
3911 "protocol" => protocol = if s.ends_with(':') { s.clone() } else { alloc::format!("{}:", s) },
3912 "hostname" => hostname = s.clone(),
3913 "port" => port = s.clone(),
3914 "host" => match s.split_once(':') {
3915 Some((h, p)) => {
3916 hostname = String::from(h);
3917 port = String::from(p);
3918 }
3919 None => {
3920 hostname = s.clone();
3921 port = String::new();
3922 }
3923 },
3924 "pathname" => pathname = if s.starts_with('/') { s.clone() } else { alloc::format!("/{}", s) },
3925 "search" => {
3926 search = if s.is_empty() || s.starts_with('?') { s.clone() } else { alloc::format!("?{}", s) }
3927 }
3928 "hash" => {
3929 hash = if s.is_empty() || s.starts_with('#') { s.clone() } else { alloc::format!("#{}", s) }
3930 }
3931 "username" => username = s.clone(),
3932 "password" => password = s.clone(),
3933 _ => {}
3934 }
3935 let hostport =
3936 if port.is_empty() { hostname.clone() } else { alloc::format!("{}:{}", hostname, port) };
3937 let userinfo = if username.is_empty() && password.is_empty() {
3938 String::new()
3939 } else {
3940 alloc::format!("{}:{}@", username, password)
3941 };
3942 let new_href =
3943 alloc::format!("{}//{}{}{}{}{}", protocol, userinfo, hostport, pathname, search, hash);
3944 self.dom.borrow_mut().set_attr(idx, "href", &new_href);
3945 }
3946 "htmlFor" => self.dom.borrow_mut().set_attr(idx, "for", &s),
3947 "acceptCharset" => self.dom.borrow_mut().set_attr(idx, "accept-charset", &s),
3950 "httpEquiv" => self.dom.borrow_mut().set_attr(idx, "http-equiv", &s),
3953 "content" => self.dom.borrow_mut().set_attr(idx, "content", &s),
3954 "charset" => self.dom.borrow_mut().set_attr(idx, "charset", &s),
3955 "cite" => self.dom.borrow_mut().set_attr(idx, "cite", &s),
3958 "dateTime" => self.dom.borrow_mut().set_attr(idx, "datetime", &s),
3959 "action" => self.dom.borrow_mut().set_attr(idx, "action", &s),
3960 "method" => self.dom.borrow_mut().set_attr(idx, "method", &s),
3961 "enctype" | "encoding" => self.dom.borrow_mut().set_attr(idx, "enctype", &s),
3964 "formAction" => self.dom.borrow_mut().set_attr(idx, "formaction", &s),
3967 "formEnctype" => self.dom.borrow_mut().set_attr(idx, "formenctype", &s),
3968 "formMethod" => self.dom.borrow_mut().set_attr(idx, "formmethod", &s),
3969 "formTarget" => self.dom.borrow_mut().set_attr(idx, "formtarget", &s),
3970 "min" => self.dom.borrow_mut().set_attr(idx, "min", &s),
3971 "max" => self.dom.borrow_mut().set_attr(idx, "max", &s),
3972 "low" => self.dom.borrow_mut().set_attr(idx, "low", &s),
3975 "high" => self.dom.borrow_mut().set_attr(idx, "high", &s),
3976 "optimum" => self.dom.borrow_mut().set_attr(idx, "optimum", &s),
3977 "colSpan" => self.dom.borrow_mut().set_attr(idx, "colspan", &s),
3980 "rowSpan" => self.dom.borrow_mut().set_attr(idx, "rowspan", &s),
3981 "headers" => self.dom.borrow_mut().set_attr(idx, "headers", &s),
3984 "scope" => self.dom.borrow_mut().set_attr(idx, "scope", &s),
3985 "abbr" => self.dom.borrow_mut().set_attr(idx, "abbr", &s),
3986 "span" => self.dom.borrow_mut().set_attr(idx, "span", &s),
3989 "step" => self.dom.borrow_mut().set_attr(idx, "step", &s),
3990 "maxLength" => self.dom.borrow_mut().set_attr(idx, "maxlength", &s),
3991 "minLength" => self.dom.borrow_mut().set_attr(idx, "minlength", &s),
3994 "target" => self.dom.borrow_mut().set_attr(idx, "target", &s),
3995 "rel" => self.dom.borrow_mut().set_attr(idx, "rel", &s),
3996 "download" => self.dom.borrow_mut().set_attr(idx, "download", &s),
3997 "coords" => self.dom.borrow_mut().set_attr(idx, "coords", &s),
4000 "shape" => self.dom.borrow_mut().set_attr(idx, "shape", &s),
4001 "lang" => self.dom.borrow_mut().set_attr(idx, "lang", &s),
4002 "dir" => self.dom.borrow_mut().set_attr(idx, "dir", &s),
4003 "title" => self.dom.borrow_mut().set_attr(idx, "title", &s),
4006 "accessKey" => self.dom.borrow_mut().set_attr(idx, "accesskey", &s),
4007 "translate" => {
4010 let v = if val.truthy() { "yes" } else { "no" };
4011 self.dom.borrow_mut().set_attr(idx, "translate", v);
4012 }
4013 "draggable" => {
4016 let v = if val.truthy() { "true" } else { "false" };
4017 self.dom.borrow_mut().set_attr(idx, "draggable", v);
4018 }
4019 "slot" => self.dom.borrow_mut().set_attr(idx, "slot", &s),
4021 "contentEditable" => {
4026 if matches!(s.as_str(), "true" | "false" | "plaintext-only") {
4027 self.dom.borrow_mut().set_attr(idx, "contenteditable", &s);
4028 }
4029 }
4030
4031 "tabIndex" => {
4032 let n = val.to_number() as i64;
4033 self.dom.borrow_mut().set_attr(idx, "tabindex", &n.to_string());
4034 }
4035 "spellcheck" => {
4038 let b = val.truthy();
4039 self.dom.borrow_mut().set_attr(idx, "spellcheck", if b { "true" } else { "false" });
4040 }
4041 "inputMode" => self.dom.borrow_mut().set_attr(idx, "inputmode", &s),
4042 "size" => self.dom.borrow_mut().set_attr(idx, "size", &s),
4045 "rows" => self.dom.borrow_mut().set_attr(idx, "rows", &s),
4048 "cols" => self.dom.borrow_mut().set_attr(idx, "cols", &s),
4049 "autocomplete" => self.dom.borrow_mut().set_attr(idx, "autocomplete", &s),
4052 "autofocus" => {
4053 let mut dom = self.dom.borrow_mut();
4054 if val.truthy() {
4055 dom.set_attr(idx, "autofocus", "autofocus");
4056 } else {
4057 dom.remove_attr(idx, "autofocus");
4058 }
4059 }
4060 "enterKeyHint" => self.dom.borrow_mut().set_attr(idx, "enterkeyhint", &s),
4061 "autocapitalize" => self.dom.borrow_mut().set_attr(idx, "autocapitalize", &s),
4062 "popover" => {
4064 if matches!(val, Value::Null) {
4065 self.dom.borrow_mut().remove_attr(idx, "popover");
4066 } else {
4067 self.dom.borrow_mut().set_attr(idx, "popover", &s);
4068 }
4069 }
4070 "popoverTargetAction" => {
4071 self.dom.borrow_mut().set_attr(idx, "popovertargetaction", &s)
4072 }
4073 "popoverTargetElement" => match val {
4078 Value::Null => {
4079 self.dom.borrow_mut().remove_attr(idx, "popovertarget");
4080 }
4081 Value::Object(o) if matches!(&o.borrow().kind, ObjKind::DomElement(_)) => {
4082 if let ObjKind::DomElement(ti) = &o.borrow().kind {
4083 let target_id = self.dom.borrow().nodes.get(*ti).map(|n| n.id.clone());
4084 if let Some(tid) = target_id.filter(|s| !s.is_empty()) {
4085 self.dom.borrow_mut().set_attr(idx, "popovertarget", &tid);
4086 }
4087 }
4088 }
4089 _ => {}
4090 },
4091 "command" => self.dom.borrow_mut().set_attr(idx, "command", &s),
4095 "commandForElement" => match val {
4096 Value::Null => {
4097 self.dom.borrow_mut().remove_attr(idx, "commandfor");
4098 }
4099 Value::Object(o) if matches!(&o.borrow().kind, ObjKind::DomElement(_)) => {
4100 if let ObjKind::DomElement(ti) = &o.borrow().kind {
4101 let target_id = self.dom.borrow().nodes.get(*ti).map(|n| n.id.clone());
4102 if let Some(tid) = target_id.filter(|s| !s.is_empty()) {
4103 self.dom.borrow_mut().set_attr(idx, "commandfor", &tid);
4104 }
4105 }
4106 }
4107 _ => {}
4108 },
4109 "tHead" | "tFoot" | "caption"
4118 if self.dom.borrow().nodes.get(idx).map(|n| n.tag == "table").unwrap_or(false) =>
4119 {
4120 let want = match key {
4121 "tHead" => "thead",
4122 "tFoot" => "tfoot",
4123 _ => "caption",
4124 };
4125 let existing = {
4126 let dom = self.dom.borrow();
4127 dom.nodes[idx].children.iter().find(|&&c| dom.nodes[c].tag == want).copied()
4128 };
4129 if let Some(old) = existing {
4130 self.dom.borrow_mut().remove_node(old);
4131 }
4132 if let Value::Object(o) = &val {
4133 if let ObjKind::DomElement(new_idx) = &o.borrow().kind {
4134 let new_idx = *new_idx;
4135 let ref_child = if want == "tfoot" {
4136 None
4137 } else {
4138 self.dom.borrow().nodes[idx].children.first().copied()
4139 };
4140 self.dom.borrow_mut().insert_before(idx, new_idx, ref_child);
4141 }
4142 }
4143 }
4144 "scrollTop" => {
4145 let n = val.to_number() as i32;
4147 let mut dom = self.dom.borrow_mut();
4148 dom.scroll_tops.insert(idx, n.max(0));
4149 dom.dirty = true;
4150 }
4151 "scrollLeft" => {
4152 let n = val.to_number() as i32;
4154 let mut dom = self.dom.borrow_mut();
4155 dom.scroll_lefts.insert(idx, n.max(0));
4156 dom.dirty = true;
4157 }
4158 key if aria_attr_name(key).is_some() => {
4161 let attr = aria_attr_name(key).unwrap_or_default();
4162 let mut dom = self.dom.borrow_mut();
4163 if matches!(val, Value::Null | Value::Undefined) {
4164 dom.remove_attr(idx, attr);
4165 } else {
4166 dom.set_attr(idx, attr, &s);
4167 }
4168 }
4169 "width" | "height" if self.dom.borrow().nodes.get(idx)
4184 .map(|n| n.tag == "canvas").unwrap_or(false) =>
4185 {
4186 self.dom.borrow_mut().set_attr(idx, key, &s);
4187 let id = self
4188 .dom
4189 .borrow()
4190 .get_attr(idx, "_c2d_id")
4191 .and_then(|v| v.parse::<u32>().ok());
4192 if let Some(id) = id {
4193 let get = |k: &str, d: u32| {
4194 self.dom
4195 .borrow()
4196 .get_attr(idx, k)
4197 .and_then(|v| v.trim().parse::<u32>().ok())
4198 .unwrap_or(d)
4199 };
4200 let (w, h) = (get("width", 300), get("height", 150));
4201 crate::os_lib::canvas2d::resize_context(id, w, h);
4202 }
4203 }
4204 "__proto__" => {
4208 if let Value::Object(p) = &val {
4209 self.dom.borrow_mut().node_protos.insert(idx, p.clone());
4210 }
4211 }
4212 _ if key.starts_with("on") && key.len() > 2 => {
4213 self.dom.borrow_mut().set_on_handler(idx, key, val.clone());
4214 if let Value::Str(s_val) = &val {
4215 self.dom.borrow_mut().set_attr(idx, key, s_val);
4216 }
4217 }
4218 _ => {
4222 let own = self.dom.borrow().expandos.get(&idx).is_some_and(|m| m.contains_key(key));
4223 if !own {
4224 let mut cur = self.get_dom_node_proto(idx);
4225 let mut guard = 0;
4226 while let Some(p) = cur {
4227 let acc = p.borrow().accessors.get(key).map(|a| a.set.clone());
4228 if let Some(setter) = acc {
4229 if let Some(f) = setter {
4230 let _ = self.call_value(&f, Value::Object(Obj::dom(idx)), &[val]);
4231 }
4232 return;
4233 }
4234 if p.borrow().props.contains_key(key) {
4235 break;
4236 }
4237 guard += 1;
4238 if guard > 64 {
4239 break;
4240 }
4241 cur = p.borrow().proto.clone();
4242 }
4243 }
4244 self.dom
4245 .borrow_mut()
4246 .expandos
4247 .entry(idx)
4248 .or_default()
4249 .insert(String::from(key), val);
4250 }
4251 },
4252 DomDisp::Host(tag) => {
4253 if let Some(rest) = tag.strip_prefix("style:") {
4254 let idx: usize = rest.parse().unwrap_or(usize::MAX);
4255 if key == "cssText" {
4256 self.dom.borrow_mut().set_css_text(idx, &s);
4257 } else {
4258 let key = if key == "cssFloat" { "float" } else { key };
4261 self.dom.borrow_mut().set_style(idx, key, &s);
4262 }
4263 } else if let Some(rest) = tag.strip_prefix("dataset:") {
4264 let idx: usize = rest.parse().unwrap_or(usize::MAX);
4265 let attr = camel_to_data_attr(key);
4266 self.dom.borrow_mut().set_attr(idx, &attr, &s);
4267 } else if let Some(rest) = tag.strip_prefix("classList:") {
4268 let idx: usize = rest.parse().unwrap_or(usize::MAX);
4269 if key == "value" {
4270 self.dom.borrow_mut().set_attr(idx, "class", &s);
4271 }
4272 }
4273 }
4274 }
4275 }
4276}
4277
4278pub(crate) fn template_fragment_clone_node(
4279 it: &mut Interp,
4280 this: Value,
4281 args: &[Value],
4282) -> Result<Value, Value> {
4283 let deep = args.get(0).map(|v| v.truthy()).unwrap_or(false);
4284 let new_frag = Obj::plain();
4285 new_frag.borrow_mut().props.insert("nodeType".into(), Value::Number(11.0));
4286 new_frag.borrow_mut().props.insert("nodeName".into(), Value::str("#document-fragment"));
4287
4288 if let Value::Object(o) = &this {
4289 let child_vals = if let Some(Value::Object(arr_obj)) = o.borrow().props.get("childNodes") {
4290 if let ObjKind::Array(elems) = &arr_obj.borrow().kind {
4291 elems.clone()
4292 } else {
4293 alloc::vec::Vec::new()
4294 }
4295 } else {
4296 alloc::vec::Vec::new()
4297 };
4298
4299 let mut cloned_children = alloc::vec::Vec::new();
4300 for child_val in child_vals {
4301 if let Value::Object(co) = &child_val {
4302 if let ObjKind::DomElement(c_idx) = co.borrow().kind {
4303 let cloned_idx = it.dom.borrow_mut().clone_node(c_idx, deep);
4304 cloned_children.push(Value::Object(Obj::dom(cloned_idx)));
4305 continue;
4306 }
4307 }
4308 cloned_children.push(child_val.clone());
4309 }
4310
4311 let cloned_arr = Obj::array(cloned_children);
4312 new_frag.borrow_mut().props.insert("childNodes".into(), Value::Object(cloned_arr.clone()));
4313 new_frag.borrow_mut().props.insert("children".into(), Value::Object(cloned_arr));
4314 }
4315 let clone_fn = Obj::native("cloneNode", template_fragment_clone_node);
4316 new_frag.borrow_mut().props.insert("cloneNode".into(), Value::Object(clone_fn));
4317
4318 Ok(Value::Object(new_frag))
4319}