Skip to main content

atmos/os_lib/js/builtins/
observers.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4use crate::os_lib::layout::intersection_observer::{
5    compute_intersection, should_trigger_callback, IntersectionRect, RootMargin,
6};
7use crate::os_lib::layout::resize_observer::{
8    compute_resize_boxes, has_size_changed, ResizeObserverBoxOptions,
9};
10
11// ===== IntersectionObserver =====
12
13pub(crate) fn make_dom_rect_f64(r: &IntersectionRect) -> ObjRef {
14    let o = Obj::plain();
15    {
16        let mut b = o.borrow_mut();
17        b.props.insert("x".into(), Value::Number(r.x));
18        b.props.insert("left".into(), Value::Number(r.x));
19        b.props.insert("y".into(), Value::Number(r.y));
20        b.props.insert("top".into(), Value::Number(r.y));
21        b.props.insert("width".into(), Value::Number(r.width));
22        b.props.insert("height".into(), Value::Number(r.height));
23        b.props.insert("right".into(), Value::Number(r.right()));
24        b.props.insert("bottom".into(), Value::Number(r.bottom()));
25    }
26    o
27}
28
29/// 幾何交差計算に基づく `IntersectionObserverEntry` の構築
30pub(crate) fn compute_io_entry(
31    it: &mut Interp,
32    target: Value,
33    root_val: &Value,
34    root_margin: &RootMargin,
35) -> (Value, f64, bool) {
36    let target_idx = this_dom_idx(&target);
37    let (tx, ty, tw, th) = target_idx
38        .and_then(|idx| it.dom.borrow().get_rect(idx))
39        .unwrap_or((0, 0, 0, 0));
40    let target_rect = IntersectionRect::new(tx as f64, ty as f64, tw as f64, th as f64);
41
42    let root_rect = if let Some(r_idx) = this_dom_idx(root_val) {
43        let (rx, ry, rw, rh) = it.dom.borrow().get_rect(r_idx).unwrap_or((0, 0, 0, 0));
44        IntersectionRect::new(rx as f64, ry as f64, rw as f64, rh as f64)
45    } else {
46        let global_vars = it.global.borrow();
47        let vw = global_vars
48            .vars
49            .get("innerWidth")
50            .and_then(|v| match *v {
51                Value::Number(n) => Some(n),
52                _ => None,
53            })
54            .unwrap_or(800.0);
55        let vh = global_vars
56            .vars
57            .get("innerHeight")
58            .and_then(|v| match *v {
59                Value::Number(n) => Some(n),
60                _ => None,
61            })
62            .unwrap_or(600.0);
63        IntersectionRect::new(0.0, 0.0, vw, vh)
64    };
65
66    let state = compute_intersection(&target_rect, &root_rect, root_margin);
67
68    let entry = Obj::plain();
69    {
70        let mut b = entry.borrow_mut();
71        b.props
72            .insert("isIntersecting".into(), Value::Bool(state.is_intersecting));
73        b.props
74            .insert("intersectionRatio".into(), Value::Number(state.intersection_ratio));
75        let bcr = Value::Object(make_dom_rect_f64(&state.bounding_client_rect));
76        b.props.insert("boundingClientRect".into(), bcr.clone());
77        let ir = if state.is_intersecting
78            && state.bounding_client_rect == state.intersection_rect
79        {
80            bcr
81        } else {
82            Value::Object(make_dom_rect_f64(&state.intersection_rect))
83        };
84        b.props.insert("intersectionRect".into(), ir);
85        let root_bounds = if this_dom_idx(root_val).is_some() {
86            match &state.root_bounds {
87                Some(rb) => Value::Object(make_dom_rect_f64(rb)),
88                None => Value::Null,
89            }
90        } else {
91            Value::Null
92        };
93        b.props.insert("rootBounds".into(), root_bounds);
94        b.props
95            .insert("time".into(), Value::Number(next_perf_timestamp()));
96        b.props.insert("target".into(), target);
97    }
98    (Value::Object(entry), state.intersection_ratio, state.is_intersecting)
99}
100
101/// 互換用ラッパー
102pub(crate) fn make_io_entry(it: &mut Interp, target: Value) -> Value {
103    let (entry, _, _) = compute_io_entry(it, target, &Value::Null, &RootMargin::zero());
104    entry
105}
106
107pub(crate) fn io_observe(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
108    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
109    let obs_id = match self_obj.borrow().props.get("_id") {
110        Some(Value::Number(n)) => *n as u32,
111        _ => return Ok(Value::Undefined),
112    };
113    let target = arg(a, 0);
114    let target_idx = match this_dom_idx(&target) {
115        Some(idx) => idx,
116        None => return Ok(Value::Undefined),
117    };
118
119    let (cb, root_val, root_margin) = {
120        let dom = it.dom.borrow();
121        match dom.io_observers.iter().find(|o| o.id == obs_id) {
122            Some(o) => (o.callback.clone(), o.root.clone(), o.root_margin),
123            None => return Ok(Value::Undefined),
124        }
125    };
126
127    // 初回エントリの計算
128    let (entry, ratio, is_intersecting) =
129        compute_io_entry(it, target.clone(), &root_val, &root_margin);
130
131    // 監視対象ターゲットの追加・更新
132    {
133        let mut dom = it.dom.borrow_mut();
134        if let Some(obs) = dom.io_observers.iter_mut().find(|o| o.id == obs_id) {
135            obs.targets.retain(|t| t.target_idx != target_idx);
136            obs.targets.push(super::super::dom_bridge::ObservedTarget {
137                target: target.clone(),
138                target_idx,
139                prev_ratio: Some(ratio),
140                prev_is_intersecting: is_intersecting,
141            });
142        }
143    }
144
145    // 初回コールバック発火(W3C 仕様: observe 開始時に初回通知を発火)
146    if matches!(cb, Value::Object(_)) {
147        let entries = Value::Object(Obj::array(alloc::vec![entry]));
148        it.call_listener(
149            &cb,
150            Value::Undefined,
151            &[entries, this],
152            "IntersectionObserver callback",
153        );
154    }
155
156    Ok(Value::Undefined)
157}
158
159pub(crate) fn io_unobserve(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
160    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
161    let obs_id = match self_obj.borrow().props.get("_id") {
162        Some(Value::Number(n)) => *n as u32,
163        _ => return Ok(Value::Undefined),
164    };
165    let target = arg(a, 0);
166    if let Some(target_idx) = this_dom_idx(&target) {
167        let mut dom = it.dom.borrow_mut();
168        if let Some(obs) = dom.io_observers.iter_mut().find(|o| o.id == obs_id) {
169            obs.targets.retain(|t| t.target_idx != target_idx);
170        }
171    }
172    Ok(Value::Undefined)
173}
174
175pub(crate) fn io_disconnect(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
176    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
177    let obs_id = match self_obj.borrow().props.get("_id") {
178        Some(Value::Number(n)) => *n as u32,
179        _ => return Ok(Value::Undefined),
180    };
181    let mut dom = it.dom.borrow_mut();
182    if let Some(obs) = dom.io_observers.iter_mut().find(|o| o.id == obs_id) {
183        obs.targets.clear();
184        obs.queued_records.clear();
185    }
186    Ok(Value::Undefined)
187}
188
189pub(crate) fn io_take_records(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
190    let Value::Object(self_obj) = &this else {
191        return Ok(Value::Object(Obj::array(alloc::vec![])));
192    };
193    let obs_id = match self_obj.borrow().props.get("_id") {
194        Some(Value::Number(n)) => *n as u32,
195        _ => return Ok(Value::Object(Obj::array(alloc::vec![]))),
196    };
197    let records = {
198        let mut dom = it.dom.borrow_mut();
199        if let Some(obs) = dom.io_observers.iter_mut().find(|o| o.id == obs_id) {
200            core::mem::take(&mut obs.queued_records)
201        } else {
202            alloc::vec![]
203        }
204    };
205    Ok(Value::Object(Obj::array(records)))
206}
207
208pub(crate) fn intersection_observer_ctor(
209    it: &mut Interp,
210    _t: Value,
211    a: &[Value],
212) -> Result<Value, Value> {
213    let cb = arg(a, 0);
214    let options = arg(a, 1);
215    let root = obj_prop(&options, "root").unwrap_or(Value::Null);
216    let root_margin_str = obj_prop(&options, "rootMargin")
217        .map(|v| v.to_js_string())
218        .unwrap_or_else(|| String::from("0px 0px 0px 0px"));
219    let root_margin = RootMargin::parse(&root_margin_str).unwrap_or_else(|_| RootMargin::zero());
220
221    let threshold_val = obj_prop(&options, "threshold");
222    let thresholds: alloc::vec::Vec<f64> = match threshold_val {
223        Some(v) => {
224            let items = this_items(&v);
225            if items.is_empty() && !matches!(v, Value::Object(_)) {
226                alloc::vec![v.to_number()]
227            } else {
228                items.iter().map(|x| x.to_number()).collect()
229            }
230        }
231        None => alloc::vec![0.0],
232    };
233
234    let obs_id = {
235        let mut dom = it.dom.borrow_mut();
236        let id = dom.next_io_obs_id;
237        dom.next_io_obs_id += 1;
238        dom.io_observers.push(super::super::dom_bridge::IntersectionObserverReg {
239            id,
240            callback: cb.clone(),
241            root: root.clone(),
242            root_margin,
243            thresholds: thresholds.clone(),
244            targets: alloc::vec![],
245            queued_records: alloc::vec![],
246        });
247        id
248    };
249
250    let obs = Obj::plain();
251    {
252        let mut b = obs.borrow_mut();
253        b.props.insert("_cb".into(), cb);
254        b.props.insert("_id".into(), Value::Number(obs_id as f64));
255        b.props.insert("root".into(), root);
256        b.props.insert("rootMargin".into(), Value::str(root_margin_str));
257        b.props.insert(
258            "thresholds".into(),
259            Value::Object(Obj::array(thresholds.into_iter().map(Value::Number).collect())),
260        );
261        b.props.insert("observe".into(), nv("observe", io_observe));
262        b.props
263            .insert("unobserve".into(), nv("unobserve", io_unobserve));
264        b.props
265            .insert("disconnect".into(), nv("disconnect", io_disconnect));
266        b.props
267            .insert("takeRecords".into(), nv("takeRecords", io_take_records));
268    }
269    Ok(Value::Object(obs))
270}
271
272/// 即時 fulfilled な Promise オブジェクトを返す(animate().finished など用)。
273pub fn make_resolved_promise(val: Value) -> ObjRef {
274    use super::super::value::PromiseState;
275    let state = alloc::rc::Rc::new(core::cell::RefCell::new(PromiseState::pending()));
276    state.borrow_mut().status = super::super::value::PromiseStatus::Fulfilled;
277    state.borrow_mut().value = val;
278    let obj = Obj::plain();
279    obj.borrow_mut().kind = super::super::value::ObjKind::PromiseObj(state.clone());
280    // then / catch / finally はスタブ
281    let mut b = obj.borrow_mut();
282    b.props.insert("then".into(), nv("then", dom_noop));
283    b.props.insert("catch".into(), nv("catch", dom_noop));
284    b.props.insert("finally".into(), nv("finally", dom_noop));
285    drop(b);
286    obj
287}
288
289/// スクロール後やレイアウト更新後に IntersectionObserver コールバックを発火する。
290pub fn fire_intersection_observers(it: &mut Interp) {
291    let observers = {
292        let dom = it.dom.borrow();
293        dom.io_observers.clone()
294    };
295    if observers.is_empty() {
296        return;
297    }
298
299    for obs in observers {
300        if obs.targets.is_empty() || !matches!(obs.callback, Value::Object(_)) {
301            continue;
302        }
303
304        let mut triggered_entries = alloc::vec![];
305        let mut updated_targets = alloc::vec![];
306
307        for target_info in &obs.targets {
308            let (entry, ratio, is_intersecting) = compute_io_entry(
309                it,
310                target_info.target.clone(),
311                &obs.root,
312                &obs.root_margin,
313            );
314
315            if should_trigger_callback(
316                target_info.prev_ratio,
317                target_info.prev_is_intersecting,
318                ratio,
319                is_intersecting,
320                &obs.thresholds,
321            ) {
322                triggered_entries.push(entry);
323                updated_targets.push(super::super::dom_bridge::ObservedTarget {
324                    target: target_info.target.clone(),
325                    target_idx: target_info.target_idx,
326                    prev_ratio: Some(ratio),
327                    prev_is_intersecting: is_intersecting,
328                });
329            } else {
330                updated_targets.push(target_info.clone());
331            }
332        }
333
334        // 状態更新
335        {
336            let mut dom = it.dom.borrow_mut();
337            if let Some(reg) = dom.io_observers.iter_mut().find(|o| o.id == obs.id) {
338                reg.targets = updated_targets;
339            }
340        }
341
342        if !triggered_entries.is_empty() {
343            let entries_val = Value::Object(Obj::array(triggered_entries));
344            let obs_obj = Value::Object(Obj::plain());
345            it.call_listener(
346                &obs.callback,
347                Value::Undefined,
348                &[entries_val, obs_obj],
349                "IntersectionObserver callback",
350            );
351        }
352    }
353}
354
355/// DOM 変更後に MutationObserver のコールバックを発火する。
356/// `eval` 末尾と `dispatch_event` 後に呼ぶ。
357pub fn flush_mutation_observers(it: &mut Interp) {
358    // 全レコードを一括取得し observer_id でグループ化。
359    let all_recs = drain_mut_records(it);
360    if all_recs.is_empty() {
361        return;
362    }
363    // observer_id → (callback, observer_obj, Vec<MutRec>) にグループ化。
364    use alloc::collections::BTreeMap;
365    let mut groups: BTreeMap<u32, (Value, Value, alloc::vec::Vec<super::super::dom_bridge::MutRec>)> =
366        BTreeMap::new();
367    {
368        let dom = it.dom.borrow();
369        for (id, rec) in all_recs {
370            let (cb, obs_val) = dom
371                .mut_observers
372                .iter()
373                .find(|o| o.id == id)
374                .map(|o| (o.callback.clone(), o.observer.clone()))
375                .unwrap_or((Value::Undefined, Value::Undefined));
376            groups.entry(id).or_insert((cb, obs_val, alloc::vec![])).2.push(rec);
377        }
378    }
379    for (_id, (cb, obs_val, recs)) in groups {
380        if !matches!(cb, Value::Object(_)) {
381            continue;
382        }
383        let js_recs: alloc::vec::Vec<Value> =
384            recs.iter().map(|r| make_mut_record(it, r)).collect();
385        let arr = Value::Object(Obj::array(js_recs));
386        // W3C DOM MutationObserver 仕様: 第1引数に records 配列、第2引数に MutationObserver 自身を渡す
387        it.call_listener(&cb, Value::Undefined, &[arr, obs_val], "MutationObserver callback");
388    }
389}
390
391// ===== ResizeObserver =====
392
393pub(crate) fn compute_ro_entry(
394    it: &mut Interp,
395    target: Value,
396    box_opt: ResizeObserverBoxOptions,
397) -> (Value, f64, f64) {
398    let target_idx = this_dom_idx(&target);
399    let (_, _, w, h) = target_idx
400        .and_then(|idx| it.dom.borrow().get_rect(idx))
401        .unwrap_or((0, 0, 0, 0));
402    let (bw, bh) = (w as f64, h as f64);
403
404    // computed style から padding と border-width を取得
405    let (pt, pr, pb, pl, bt, br, bb, bl) = if let Some(idx) = target_idx {
406        let dom = it.dom.borrow();
407        let parse_px = |prop: &str| -> f64 {
408            let s = dom.get_computed(idx, prop);
409            s.trim_end_matches("px").parse::<f64>().unwrap_or(0.0)
410        };
411        (
412            parse_px("padding-top"),
413            parse_px("padding-right"),
414            parse_px("padding-bottom"),
415            parse_px("padding-left"),
416            parse_px("border-top-width"),
417            parse_px("border-right-width"),
418            parse_px("border-bottom-width"),
419            parse_px("border-left-width"),
420        )
421    } else {
422        (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
423    };
424
425    let dims = compute_resize_boxes(bw, bh, pt, pr, pb, pl, bt, br, bb, bl);
426
427    let entry = Obj::plain();
428    {
429        let mut b = entry.borrow_mut();
430        b.props.insert("target".into(), target);
431        let cr = Obj::plain();
432        {
433            let mut c = cr.borrow_mut();
434            c.props.insert("x".into(), Value::Number(dims.content_rect_x));
435            c.props.insert("y".into(), Value::Number(dims.content_rect_y));
436            c.props.insert("width".into(), Value::Number(dims.content_rect_width));
437            c.props.insert("height".into(), Value::Number(dims.content_rect_height));
438            c.props.insert("top".into(), Value::Number(dims.content_rect_y));
439            c.props.insert("left".into(), Value::Number(dims.content_rect_x));
440            c.props.insert(
441                "right".into(),
442                Value::Number(dims.content_rect_x + dims.content_rect_width),
443            );
444            c.props.insert(
445                "bottom".into(),
446                Value::Number(dims.content_rect_y + dims.content_rect_height),
447            );
448        }
449        b.props.insert("contentRect".into(), Value::Object(cr));
450
451        let make_size = |inline: f64, block: f64| {
452            let s = Obj::plain();
453            s.borrow_mut().props.insert("inlineSize".into(), Value::Number(inline));
454            s.borrow_mut().props.insert("blockSize".into(), Value::Number(block));
455            Value::Object(s)
456        };
457
458        b.props.insert(
459            "borderBoxSize".into(),
460            Value::Object(Obj::array(alloc::vec![make_size(
461                dims.border_box_inline_size,
462                dims.border_box_block_size,
463            )])),
464        );
465        b.props.insert(
466            "contentBoxSize".into(),
467            Value::Object(Obj::array(alloc::vec![make_size(
468                dims.content_box_inline_size,
469                dims.content_box_block_size,
470            )])),
471        );
472        b.props.insert(
473            "devicePixelContentBoxSize".into(),
474            Value::Object(Obj::array(alloc::vec![make_size(
475                dims.device_pixel_content_box_inline_size,
476                dims.device_pixel_content_box_block_size,
477            )])),
478        );
479    }
480
481    let (cur_inline, cur_block) = match box_opt {
482        ResizeObserverBoxOptions::BorderBox => (
483            dims.border_box_inline_size,
484            dims.border_box_block_size,
485        ),
486        ResizeObserverBoxOptions::DevicePixelContentBox => (
487            dims.device_pixel_content_box_inline_size,
488            dims.device_pixel_content_box_block_size,
489        ),
490        ResizeObserverBoxOptions::ContentBox => (
491            dims.content_box_inline_size,
492            dims.content_box_block_size,
493        ),
494    };
495
496    (Value::Object(entry), cur_inline, cur_block)
497}
498
499pub(crate) fn ro_observe(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
500    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
501    let obs_id = match self_obj.borrow().props.get("_id") {
502        Some(Value::Number(n)) => *n as u32,
503        _ => return Ok(Value::Undefined),
504    };
505    let cb = self_obj.borrow().props.get("_cb").cloned().unwrap_or(Value::Undefined);
506
507    let target = arg(a, 0);
508    let target_idx = match this_dom_idx(&target) {
509        Some(idx) => idx,
510        None => return Ok(Value::Undefined),
511    };
512
513    let options = arg(a, 1);
514    let box_str = obj_prop(&options, "box").map(|v| v.to_js_string()).unwrap_or_default();
515    let box_opt = ResizeObserverBoxOptions::parse(&box_str);
516
517    let (entry, inline_size, block_size) = compute_ro_entry(it, target.clone(), box_opt);
518
519    // targets に登録
520    let already_observing = {
521        let mut dom = it.dom.borrow_mut();
522        if let Some(obs) = dom.ro_observers.iter_mut().find(|o| o.id == obs_id) {
523            let was = obs.targets.iter().any(|t| t.target_idx == target_idx);
524            obs.targets.retain(|t| t.target_idx != target_idx);
525            obs.targets.push(super::super::dom_bridge::ObservedResizeTarget {
526                target: target.clone(),
527                target_idx,
528                box_option: box_opt,
529                last_inline_size: inline_size,
530                last_block_size: block_size,
531            });
532            was
533        } else {
534            false
535        }
536    };
537
538    // 初回コールバック発火(既に監視中の要素に対する重複 observe では再発火しない)
539    if !already_observing && matches!(cb, Value::Object(_)) {
540        let entries = Value::Object(Obj::array(alloc::vec![entry]));
541        it.call_listener(
542            &cb,
543            Value::Undefined,
544            &[entries, this],
545            "ResizeObserver callback",
546        );
547    }
548
549    Ok(Value::Undefined)
550}
551
552pub(crate) fn ro_unobserve(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
553    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
554    let obs_id = match self_obj.borrow().props.get("_id") {
555        Some(Value::Number(n)) => *n as u32,
556        _ => return Ok(Value::Undefined),
557    };
558    let target = arg(a, 0);
559    if let Some(target_idx) = this_dom_idx(&target) {
560        let mut dom = it.dom.borrow_mut();
561        if let Some(obs) = dom.ro_observers.iter_mut().find(|o| o.id == obs_id) {
562            obs.targets.retain(|t| t.target_idx != target_idx);
563        }
564    }
565    Ok(Value::Undefined)
566}
567
568pub(crate) fn ro_disconnect(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
569    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
570    let obs_id = match self_obj.borrow().props.get("_id") {
571        Some(Value::Number(n)) => *n as u32,
572        _ => return Ok(Value::Undefined),
573    };
574    let mut dom = it.dom.borrow_mut();
575    if let Some(obs) = dom.ro_observers.iter_mut().find(|o| o.id == obs_id) {
576        obs.targets.clear();
577    }
578    Ok(Value::Undefined)
579}
580
581pub(crate) fn resize_observer_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
582    let cb = arg(a, 0);
583    let obs_id = {
584        let mut dom = it.dom.borrow_mut();
585        let id = dom.next_ro_obs_id;
586        dom.next_ro_obs_id += 1;
587        dom.ro_observers.push(super::super::dom_bridge::ResizeObserverReg {
588            id,
589            callback: cb.clone(),
590            targets: alloc::vec![],
591        });
592        id
593    };
594
595    let obs = Obj::plain();
596    {
597        let mut b = obs.borrow_mut();
598        b.props.insert("_cb".into(), cb);
599        b.props.insert("_id".into(), Value::Number(obs_id as f64));
600        b.props.insert("observe".into(), nv("observe", ro_observe));
601        b.props
602            .insert("unobserve".into(), nv("unobserve", ro_unobserve));
603        b.props
604            .insert("disconnect".into(), nv("disconnect", ro_disconnect));
605    }
606    Ok(Value::Object(obs))
607}
608
609/// レイアウト更新後に ResizeObserver コールバックを発火する。
610pub fn fire_resize_observers(it: &mut Interp) {
611    let observers = {
612        let dom = it.dom.borrow();
613        dom.ro_observers.clone()
614    };
615    if observers.is_empty() {
616        return;
617    }
618
619    for obs in observers {
620        if obs.targets.is_empty() || !matches!(obs.callback, Value::Object(_)) {
621            continue;
622        }
623
624        let mut triggered_entries = alloc::vec![];
625        let mut updated_targets = alloc::vec![];
626
627        for target_info in &obs.targets {
628            let (entry, inline_size, block_size) = compute_ro_entry(
629                it,
630                target_info.target.clone(),
631                target_info.box_option,
632            );
633
634            if has_size_changed(
635                target_info.last_inline_size,
636                target_info.last_block_size,
637                inline_size,
638                block_size,
639            ) {
640                triggered_entries.push(entry);
641                updated_targets.push(super::super::dom_bridge::ObservedResizeTarget {
642                    target: target_info.target.clone(),
643                    target_idx: target_info.target_idx,
644                    box_option: target_info.box_option,
645                    last_inline_size: inline_size,
646                    last_block_size: block_size,
647                });
648            } else {
649                updated_targets.push(target_info.clone());
650            }
651        }
652
653        // 状態更新
654        {
655            let mut dom = it.dom.borrow_mut();
656            if let Some(reg) = dom.ro_observers.iter_mut().find(|o| o.id == obs.id) {
657                reg.targets = updated_targets;
658            }
659        }
660
661        if !triggered_entries.is_empty() {
662            let entries_val = Value::Object(Obj::array(triggered_entries));
663            let obs_obj = Value::Object(Obj::plain());
664            it.call_listener(
665                &obs.callback,
666                Value::Undefined,
667                &[entries_val, obs_obj],
668                "ResizeObserver callback",
669            );
670        }
671    }
672}
673
674
675// ===== MutationObserver =====
676pub(crate) fn mo_observe(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
677    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
678    let obs_id = match self_obj.borrow().props.get("_id") {
679        Some(Value::Number(n)) => *n as u32,
680        _ => return Ok(Value::Undefined),
681    };
682    let callback = self_obj.borrow().props.get("_cb").cloned().unwrap_or(Value::Undefined);
683    let target_idx = match arg(a, 0) {
684        Value::Object(o) => match o.borrow().kind {
685            super::super::value::ObjKind::DomElement(i) => i,
686            _ => return Ok(Value::Undefined),
687        },
688        _ => return Ok(Value::Undefined),
689    };
690    let opts = arg(a, 1);
691    let child_list = obj_prop(&opts, "childList")
692        .map(|v| v.truthy())
693        .unwrap_or(false);
694    let attributes_specified = obj_prop(&opts, "attributes");
695    let mut attributes = attributes_specified.as_ref().map(|v| v.truthy()).unwrap_or(false);
696    let subtree = obj_prop(&opts, "subtree")
697        .map(|v| v.truthy())
698        .unwrap_or(false);
699    let attribute_old_value = obj_prop(&opts, "attributeOldValue")
700        .map(|v| v.truthy())
701        .unwrap_or(false);
702    let attribute_filter = match obj_prop(&opts, "attributeFilter") {
703        Some(v @ Value::Object(_)) => Some(
704            generic_array_items(it, &v)
705                .iter()
706                .map(|item| item.to_js_string())
707                .collect::<Vec<String>>(),
708        ),
709        _ => None,
710    };
711    // `characterData`/`characterDataOldValue`(丸ごと未対応だった。
712    // `childList`/`attributes`は既に対応済みで`characterData`だけ抜けていた
713    // 兄弟ギャップ。詳細は`MutObsReg::character_data`のdocコメント参照。
714    // 2026-07-17 発見・実装)。
715    let character_data_specified = obj_prop(&opts, "characterData");
716    let mut character_data = character_data_specified.as_ref().map(|v| v.truthy()).unwrap_or(false);
717    let character_data_old_value = obj_prop(&opts, "characterDataOldValue")
718        .map(|v| v.truthy())
719        .unwrap_or(false);
720    // 仕様上(MutationObserver.observe()アルゴリズム)、`attributeOldValue`/
721    // `attributeFilter`が指定されかつ`attributes`自体が省略された場合は
722    // `attributes`を暗黙的に`true`とみなす(`characterDataOldValue`と
723    // `characterData`も同じ関係)。以前はこの省略時の暗黙有効化ロジックが
724    // 丸ごと無く、`observe(el, {attributeOldValue:true})`という定番の
725    // 省略記法(`attributes:true`を明示せずとも動くのが仕様)が黙って
726    // 何も監視しないバグだった。2026-07-17 発見・実装。
727    if attributes_specified.is_none() && (attribute_old_value || attribute_filter.is_some()) {
728        attributes = true;
729    }
730    if character_data_specified.is_none() && character_data_old_value {
731        character_data = true;
732    }
733    use super::super::dom_bridge::MutObsReg;
734    let mut dom = it.dom.borrow_mut();
735    // 同一IDかつ同一target_idxの既存登録のみを置換し、同一Observerの複数ターゲット監視をサポート(W3C DOM 仕様)
736    dom.mut_observers.retain(|o| !(o.id == obs_id && o.target_idx == target_idx));
737    dom.mut_observers.push(MutObsReg {
738        id: obs_id,
739        target_idx,
740        child_list,
741        attributes,
742        subtree,
743        callback,
744        observer: this,
745        attribute_old_value,
746        attribute_filter,
747        character_data,
748        character_data_old_value,
749    });
750    Ok(Value::Undefined)
751}
752
753pub(crate) fn mo_disconnect(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
754    let Value::Object(self_obj) = &this else { return Ok(Value::Undefined) };
755    let obs_id = match self_obj.borrow().props.get("_id") {
756        Some(Value::Number(n)) => *n as u32,
757        _ => return Ok(Value::Undefined),
758    };
759    it.dom.borrow_mut().mut_observers.retain(|o| o.id != obs_id);
760    it.dom.borrow_mut().mut_records.retain(|(id, _)| *id != obs_id);
761    Ok(Value::Undefined)
762}
763
764pub(crate) fn mo_take_records(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
765    let Value::Object(self_obj) = &this else {
766        return Ok(Value::Object(Obj::array(alloc::vec![])));
767    };
768    let obs_id = match self_obj.borrow().props.get("_id") {
769        Some(Value::Number(n)) => *n as u32,
770        _ => return Ok(Value::Object(Obj::array(alloc::vec![]))),
771    };
772    let records = build_mut_records_for(it, obs_id);
773    Ok(Value::Object(Obj::array(records)))
774}
775
776pub(crate) fn build_mut_records_for(it: &mut Interp, obs_id: u32) -> alloc::vec::Vec<Value> {
777    let mut matched = alloc::vec![];
778    let mut rest = alloc::vec![];
779    for (id, r) in core::mem::take(&mut it.dom.borrow_mut().mut_records) {
780        if id == obs_id { matched.push(r); } else { rest.push((id, r)); }
781    }
782    it.dom.borrow_mut().mut_records = rest;
783    matched.iter().map(|r| make_mut_record(it, r)).collect()
784}
785
786pub(crate) fn drain_mut_records(it: &mut Interp) -> alloc::vec::Vec<(u32, super::super::dom_bridge::MutRec)> {
787    core::mem::take(&mut it.dom.borrow_mut().mut_records)
788}
789
790pub(crate) fn make_mut_record(it: &mut Interp, r: &super::super::dom_bridge::MutRec) -> Value {
791    let rec = Obj::plain();
792    {
793        let mut b = rec.borrow_mut();
794        b.props.insert("type".into(), Value::str(r.type_));
795        let target = Value::Object(Obj::dom(r.target_idx));
796        b.props.insert("target".into(), target);
797        let added: alloc::vec::Vec<Value> = r.added.iter().map(|&i| Value::Object(Obj::dom(i))).collect();
798        b.props.insert("addedNodes".into(), Value::Object(Obj::array(added)));
799        let removed: alloc::vec::Vec<Value> = r.removed.iter().map(|&i| Value::Object(Obj::dom(i))).collect();
800        b.props.insert("removedNodes".into(), Value::Object(Obj::array(removed)));
801        b.props.insert("previousSibling".into(), Value::Null);
802        b.props.insert("nextSibling".into(), Value::Null);
803        b.props.insert(
804            "attributeName".into(),
805            r.attribute_name
806                .as_deref()
807                .map(Value::str)
808                .unwrap_or(Value::Null),
809        );
810        b.props.insert("attributeNamespace".into(), Value::Null);
811        b.props.insert(
812            "oldValue".into(),
813            r.old_value.as_deref().map(Value::str).unwrap_or(Value::Null),
814        );
815    }
816    let _ = it;
817    Value::Object(rec)
818}
819
820pub(crate) fn mutation_observer_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
821    let cb = arg(a, 0);
822    let obs_id = {
823        let mut dom = it.dom.borrow_mut();
824        let id = dom.next_mut_obs_id;
825        dom.next_mut_obs_id += 1;
826        id
827    };
828    let obs = Obj::plain();
829    {
830        let mut b = obs.borrow_mut();
831        b.props.insert("_cb".into(), cb);
832        b.props.insert("_id".into(), Value::Number(obs_id as f64));
833        b.props.insert("observe".into(), nv("observe", mo_observe));
834        b.props.insert("disconnect".into(), nv("disconnect", mo_disconnect));
835        b.props.insert("takeRecords".into(), nv("takeRecords", mo_take_records));
836    }
837    Ok(Value::Object(obs))
838}
839