Skip to main content

atmos/os_lib/js/interp/
custom_elements.rs

1//! カスタム要素(`customElements.define`)の構築・昇格・反応。
2//!
3//! 【2026-10-20】従来は `define` が既存要素の「使い捨てのハンドル」に原型を付けるだけで、
4//! 要素は昇格せず(クラスのコンストラクタが走らない)、`createElement` もクラスを構築
5//! しなかった。YouTube の視聴ページ(Polymer 3.5)は `<ytd-app>` が昇格してテンプレートを
6//! 展開しないと何も描かれず、骨組み(skeleton)のままだった。
7//!
8//! - 要素の原型は `DomBridge::node_protos` に持つ(ハンドルは毎回作り直すため)。
9//! - `HTMLElement` を継承したクラスの `new`(`construct_object`)は、`this` を DOM 要素の
10//!   ハンドルにする。昇格中ならその要素、そうでなければ登録名で要素を作る。
11//! - 挿入・削除・複製は `DomBridge::ce_queue` に積まれ、[`Interp::process_ce_reactions`] が
12//!   昇格と `connectedCallback`/`disconnectedCallback` を行う(呼び出しの後にまとめて。
13//!   仕様の CEReactions に相当)。
14
15use super::*;
16
17impl Interp {
18    /// `customElements` の登録表(名前 → コンストラクタ)。
19    fn ce_registry(&self) -> Option<ObjRef> {
20        match super::scope_get(&self.global, "customElements")? {
21            Value::Object(reg) => match reg.borrow().props.get("_registry") {
22                Some(Value::Object(m)) => Some(m.clone()),
23                _ => None,
24            },
25            _ => None,
26        }
27    }
28
29    /// 要素名 `tag` に定義されたコンストラクタ。
30    pub(crate) fn ce_ctor_for(&self, tag: &str) -> Option<Value> {
31        if self.dom.borrow().custom_elements.is_empty() {
32            return None;
33        }
34        let reg = self.ce_registry()?;
35        let v = reg.borrow().props.get(tag).cloned();
36        v
37    }
38
39    /// コンストラクタ `func` を登録した要素名。
40    fn ce_name_for_ctor(&self, func: &Value) -> Option<String> {
41        let Value::Object(f) = func else { return None };
42        let reg = self.ce_registry()?;
43        let b = reg.borrow();
44        b.props
45            .iter()
46            .find(|(_, v)| matches!(v, Value::Object(o) if Rc::ptr_eq(o, f)))
47            .map(|(k, _)| k.clone())
48    }
49
50    /// `func.prototype` の連鎖が `HTMLElement.prototype` に届くか(カスタム要素のクラスか)。
51    pub(crate) fn element_class_proto(&self, func: &Value) -> Option<ObjRef> {
52        let Value::Object(f) = func else { return None };
53        let proto = match f.borrow().props.get("prototype") {
54            Some(Value::Object(p)) => p.clone(),
55            _ => return None,
56        };
57        let html = match super::scope_get(&self.global, "HTMLElement")? {
58            Value::Object(h) => match h.borrow().props.get("prototype") {
59                Some(Value::Object(p)) => p.clone(),
60                _ => return None,
61            },
62            _ => return None,
63        };
64        let mut cur = Some(proto.clone());
65        let mut guard = 0;
66        while let Some(p) = cur {
67            if Rc::ptr_eq(&p, &html) {
68                return Some(proto);
69            }
70            guard += 1;
71            if guard > 64 {
72                break;
73            }
74            cur = p.borrow().proto.clone();
75        }
76        None
77    }
78
79    /// カスタム要素のクラスの `new`: `this` にする DOM 要素の添字を決め、原型を付ける。
80    /// 戻り値の bool は新しく構築を始めたか(構築スタックに積むか)。
81    ///
82    /// - 構築中の要素と同じ new.target の入れ子の構築(ES5 版の
83    ///   `Reflect.construct(HTMLElement, [], this.constructor)`)は、同じ要素を返す(仕様の構築スタック)。
84    /// - 昇格中ならその要素、そうでなければ new.target の登録名で要素を作る(未登録なら TypeError)。
85    pub(crate) fn ce_construct_target(&mut self, new_target: &Value, proto: ObjRef) -> Result<(usize, bool), Value> {
86        if let (Some((i, nt)), Value::Object(t)) = (self.ce_stack.last(), new_target) {
87            if matches!(nt, Value::Object(o) if Rc::ptr_eq(o, t)) {
88                return Ok((*i, false));
89            }
90        }
91        let idx = match self.ce_upgrade_target.take() {
92            Some(i) => i,
93            None => {
94                let Some(name) = self.ce_name_for_ctor(new_target) else {
95                    return Err(self.type_error("Failed to construct 'HTMLElement': Illegal constructor"));
96                };
97                self.dom.borrow_mut().create_element(&name)
98            }
99        };
100        self.dom.borrow_mut().node_protos.insert(idx, proto);
101        Ok((idx, true))
102    }
103
104    /// 要素 `idx` を(定義があり、未昇格なら)昇格する。`connected` なら `connectedCallback` も呼ぶ。
105    /// 昇格したら true。
106    pub(crate) fn ce_try_upgrade(&mut self, idx: usize, connected: bool) -> bool {
107        let tag = {
108            let dom = self.dom.borrow();
109            if dom.node_protos.contains_key(&idx) {
110                return false;
111            }
112            match dom.nodes.get(idx) {
113                Some(n) if !n.is_text && n.tag.contains('-') => n.tag.to_lowercase(),
114                _ => return false,
115            }
116        };
117        let Some(ctor) = self.ce_ctor_for(&tag) else { return false };
118        self.ce_upgrade_target = Some(idx);
119        let r = self.construct_object(&ctor, &[]);
120        self.ce_upgrade_target = None;
121        if let Err(e) = r {
122            crate::warn!("[CE] <{}> の昇格で例外: {}", tag, super::calls::describe_thrown(&e, 2));
123            return false;
124        }
125        if !self.dom.borrow().node_protos.contains_key(&idx) {
126            return false;
127        }
128        // 既にある属性のうち、監視対象のものは attributeChangedCallback を呼ぶ(仕様の昇格手順)。
129        let observed: Vec<(String, String)> = {
130            let dom = self.dom.borrow();
131            match (dom.custom_elements.get(&tag), dom.nodes.get(idx)) {
132                (Some(def), Some(n)) => {
133                    let mut v: Vec<(String, String)> = n
134                        .attrs
135                        .iter()
136                        .filter(|(k, _)| def.observes_attribute(k))
137                        .map(|(k, v)| (k.clone(), v.clone()))
138                        .collect();
139                    if !n.id.is_empty() && def.observes_attribute("id") {
140                        v.push((String::from("id"), n.id.clone()));
141                    }
142                    v
143                }
144                _ => Vec::new(),
145            }
146        };
147        let el = Value::Object(Obj::dom(idx));
148        if !observed.is_empty() {
149            if let Ok(cb) = self.get_property(&el, "attributeChangedCallback") {
150                if matches!(&cb, Value::Object(o) if o.borrow().is_callable()) {
151                    for (k, v) in observed {
152                        let _ = self.call_value(&cb, el.clone(), &[Value::str(k), Value::Null, Value::str(v)]);
153                    }
154                }
155            }
156        }
157        if connected {
158            self.ce_callback(idx, "connectedCallback");
159        }
160        true
161    }
162
163    /// 昇格済みのカスタム要素のライフサイクル関数を呼ぶ(無ければ何もしない)。
164    fn ce_callback(&mut self, idx: usize, name: &str) {
165        let el = Value::Object(Obj::dom(idx));
166        let Ok(cb) = self.get_property(&el, name) else { return };
167        if matches!(&cb, Value::Object(o) if o.borrow().is_callable()) {
168            if let Err(e) = self.call_value(&cb, el, &[]) {
169                crate::warn!("[CE] {} で例外: {}", name, super::calls::describe_thrown(&e, 2));
170            }
171        }
172    }
173
174    /// 溜まったカスタム要素の反応を処理する(DOM を変える呼び出しの後に呼ぶ)。
175    pub(crate) fn process_ce_reactions(&mut self) {
176        if self.ce_processing || self.dom.borrow().ce_queue.is_empty() {
177            return;
178        }
179        self.ce_processing = true;
180        let mut rounds = 0;
181        loop {
182            let events: Vec<crate::os_lib::js::dom_bridge::CeEvent> =
183                core::mem::take(&mut self.dom.borrow_mut().ce_queue);
184            if events.is_empty() {
185                break;
186            }
187            rounds += 1;
188            if rounds > 10_000 {
189                break;
190            }
191            for ev in events {
192                use crate::os_lib::js::dom_bridge::CeEvent;
193                match ev {
194                    CeEvent::Created(root) => {
195                        let els = self.dom.borrow().subtree_elements(root);
196                        for i in els {
197                            self.ce_try_upgrade(i, false);
198                        }
199                    }
200                    CeEvent::Inserted(root) => {
201                        let (connected, els) = {
202                            let dom = self.dom.borrow();
203                            (dom.is_attached(root), dom.subtree_elements(root))
204                        };
205                        if !connected {
206                            continue;
207                        }
208                        for i in els {
209                            // 昇格したら connectedCallback まで済んでいる。既に昇格済みなら呼ぶ。
210                            let was = self.dom.borrow().node_protos.contains_key(&i);
211                            if was {
212                                self.ce_callback(i, "connectedCallback");
213                            } else {
214                                self.ce_try_upgrade(i, true);
215                            }
216                        }
217                    }
218                    CeEvent::Attr(idx, name, old, new) => {
219                        let el = Value::Object(Obj::dom(idx));
220                        if let Ok(cb) = self.get_property(&el, "attributeChangedCallback") {
221                            if matches!(&cb, Value::Object(o) if o.borrow().is_callable()) {
222                                let v = |x: Option<String>| x.map(Value::str).unwrap_or(Value::Null);
223                                if let Err(e) = self.call_value(&cb, el, &[Value::str(name), v(old), v(new)]) {
224                                    crate::warn!("[CE] attributeChangedCallback で例外: {}", super::calls::describe_thrown(&e, 2));
225                                }
226                            }
227                        }
228                    }
229                    CeEvent::Removed(root) => {
230                        let els = self.dom.borrow().subtree_elements(root);
231                        for i in els {
232                            if self.dom.borrow().node_protos.contains_key(&i) {
233                                self.ce_callback(i, "disconnectedCallback");
234                            }
235                        }
236                    }
237                }
238            }
239        }
240        self.ce_processing = false;
241    }
242}