quickjs_runtime/reflection/
eventtarget.rs

1//! EventTarget utils
2//!
3
4use crate::jsutils::JsError;
5use crate::quickjs_utils;
6use crate::quickjs_utils::objects::{create_object_q, set_property_q};
7use crate::quickjs_utils::primitives::from_bool;
8use crate::quickjs_utils::{functions, objects, parse_args, primitives};
9use crate::quickjsrealmadapter::QuickJsRealmAdapter;
10use crate::quickjsvalueadapter::QuickJsValueAdapter;
11use crate::reflection::{get_proxy, get_proxy_instance_info, Proxy};
12use libquickjs_sys as q;
13use std::collections::HashMap;
14
15fn with_proxy_instances_map<C, R>(
16    q_ctx: &QuickJsRealmAdapter,
17    proxy_class_name: &str,
18    consumer: C,
19) -> R
20where
21    C: FnOnce(
22        &HashMap<usize, HashMap<String, HashMap<QuickJsValueAdapter, QuickJsValueAdapter>>>,
23    ) -> R,
24{
25    let listeners = &*q_ctx.proxy_event_listeners.borrow();
26    if listeners.contains_key(proxy_class_name) {
27        let proxy_instance_map = listeners.get(proxy_class_name).unwrap();
28        consumer(proxy_instance_map)
29    } else {
30        consumer(&HashMap::new())
31    }
32}
33
34fn with_proxy_instances_map_mut<C, R>(
35    q_ctx: &QuickJsRealmAdapter,
36    proxy_class_name: &str,
37    consumer: C,
38) -> R
39where
40    C: FnOnce(
41        &mut HashMap<usize, HashMap<String, HashMap<QuickJsValueAdapter, QuickJsValueAdapter>>>,
42    ) -> R,
43{
44    let listeners = &mut *q_ctx.proxy_event_listeners.borrow_mut();
45    if !listeners.contains_key(proxy_class_name) {
46        listeners.insert(proxy_class_name.to_string(), HashMap::new());
47    }
48    let proxy_instance_map = listeners.get_mut(proxy_class_name).unwrap();
49
50    consumer(proxy_instance_map)
51}
52
53fn with_listener_map_mut<C, R>(
54    q_ctx: &QuickJsRealmAdapter,
55    proxy_class_name: &str,
56    instance_id: usize,
57    event_id: &str,
58    consumer: C,
59) -> R
60where
61    C: FnOnce(&mut HashMap<QuickJsValueAdapter, QuickJsValueAdapter>) -> R,
62{
63    with_proxy_instances_map_mut(q_ctx, proxy_class_name, |proxy_instance_map| {
64        let event_id_map = proxy_instance_map.entry(instance_id).or_default();
65
66        if !event_id_map.contains_key(event_id) {
67            event_id_map.insert(event_id.to_string(), HashMap::new());
68        }
69
70        let listener_map = event_id_map.get_mut(event_id).unwrap();
71
72        consumer(listener_map)
73    })
74}
75
76fn with_listener_map<C, R>(
77    q_ctx: &QuickJsRealmAdapter,
78    proxy_class_name: &str,
79    instance_id: usize,
80    event_id: &str,
81    consumer: C,
82) -> R
83where
84    C: FnOnce(&HashMap<QuickJsValueAdapter, QuickJsValueAdapter>) -> R,
85{
86    with_proxy_instances_map(q_ctx, proxy_class_name, |proxy_instance_map| {
87        if let Some(event_id_map) = proxy_instance_map.get(&instance_id) {
88            if let Some(listener_map) = event_id_map.get(event_id) {
89                consumer(listener_map)
90            } else {
91                consumer(&HashMap::new())
92            }
93        } else {
94            consumer(&HashMap::new())
95        }
96    })
97}
98
99fn with_static_listener_map<C, R>(
100    q_ctx: &QuickJsRealmAdapter,
101    proxy_class_name: &str,
102    event_id: &str,
103    consumer: C,
104) -> R
105where
106    C: FnOnce(&mut HashMap<QuickJsValueAdapter, QuickJsValueAdapter>) -> R,
107{
108    let static_listeners = &mut *q_ctx.proxy_static_event_listeners.borrow_mut();
109    if !static_listeners.contains_key(proxy_class_name) {
110        static_listeners.insert(proxy_class_name.to_string(), HashMap::new());
111    }
112    let proxy_static_map = static_listeners.get_mut(proxy_class_name).unwrap();
113    if !proxy_static_map.contains_key(event_id) {
114        proxy_static_map.insert(event_id.to_string(), HashMap::new());
115    }
116    let event_map = proxy_static_map.get_mut(event_id).unwrap();
117    consumer(event_map)
118}
119
120pub fn add_event_listener(
121    q_ctx: &QuickJsRealmAdapter,
122    proxy_class_name: &str,
123    event_id: &str,
124    instance_id: usize,
125    listener_func: QuickJsValueAdapter,
126    options_obj: QuickJsValueAdapter,
127) {
128    log::trace!(
129        "eventtarget::add_listener_to_map p:{} e:{} i:{}",
130        proxy_class_name,
131        event_id,
132        instance_id
133    );
134    with_listener_map_mut(q_ctx, proxy_class_name, instance_id, event_id, |map| {
135        let _ = map.insert(listener_func, options_obj);
136    })
137}
138
139pub fn add_static_event_listener(
140    q_ctx: &QuickJsRealmAdapter,
141    proxy_class_name: &str,
142    event_id: &str,
143    listener_func: QuickJsValueAdapter,
144    options_obj: QuickJsValueAdapter,
145) {
146    log::trace!(
147        "eventtarget::add_static_listener_to_map p:{} e:{}",
148        proxy_class_name,
149        event_id
150    );
151    with_static_listener_map(q_ctx, proxy_class_name, event_id, |map| {
152        let _ = map.insert(listener_func, options_obj);
153    })
154}
155
156pub fn remove_event_listener(
157    q_ctx: &QuickJsRealmAdapter,
158    proxy_class_name: &str,
159    event_id: &str,
160    instance_id: usize,
161    listener_func: &QuickJsValueAdapter,
162) {
163    log::trace!(
164        "eventtarget::remove_listener_from_map p:{} e:{} i:{}",
165        proxy_class_name,
166        event_id,
167        instance_id
168    );
169    with_listener_map_mut(q_ctx, proxy_class_name, instance_id, event_id, |map| {
170        let _ = map.remove(listener_func);
171    })
172}
173
174pub fn remove_static_event_listener(
175    q_ctx: &QuickJsRealmAdapter,
176    proxy_class_name: &str,
177    event_id: &str,
178    listener_func: &QuickJsValueAdapter,
179) {
180    log::trace!(
181        "eventtarget::remove_static_listener_from_map p:{} e:{}",
182        proxy_class_name,
183        event_id
184    );
185    with_static_listener_map(q_ctx, proxy_class_name, event_id, |map| {
186        let _ = map.remove(listener_func);
187    })
188}
189
190fn remove_map(q_ctx: &QuickJsRealmAdapter, proxy_class_name: &str, instance_id: usize) {
191    log::trace!(
192        "eventtarget::remove_map p:{} i:{}",
193        proxy_class_name,
194        instance_id
195    );
196
197    with_proxy_instances_map_mut(q_ctx, proxy_class_name, |map| {
198        let _ = map.remove(&instance_id);
199    });
200}
201
202/// dispatch an Event on an instance of a Proxy class
203/// the return value is false if event is cancelable and at least one of the event listeners which received event called Event.preventDefault. Otherwise it returns true
204pub fn dispatch_event(
205    q_ctx: &QuickJsRealmAdapter,
206    proxy: &Proxy,
207    instance_id: usize,
208    event_id: &str,
209    event: QuickJsValueAdapter,
210) -> Result<bool, JsError> {
211    let proxy_class_name = proxy.get_class_name();
212
213    with_listener_map(
214        q_ctx,
215        proxy_class_name.as_str(),
216        instance_id,
217        event_id,
218        |listeners| -> Result<(), JsError> {
219            let func_args = [event];
220            for entry in listeners {
221                let listener = entry.0;
222                let _res = functions::call_function_q(q_ctx, listener, &func_args, None)?;
223
224                // todo chekc if _res is bool, for cancel and such
225                // and if event is cancelabble and preventDefault was called and such
226            }
227            Ok(())
228        },
229    )?;
230
231    Ok(true)
232}
233
234/// dispatch an Event on a Proxy class
235/// the return value is false if event is cancelable and at least one of the event listeners which received event called Event.preventDefault. Otherwise it returns true
236pub fn dispatch_static_event(
237    q_ctx: &QuickJsRealmAdapter,
238    proxy_class_name: &str,
239    event_id: &str,
240    event: QuickJsValueAdapter,
241) -> Result<bool, JsError> {
242    with_static_listener_map(
243        q_ctx,
244        proxy_class_name,
245        event_id,
246        |listeners| -> Result<(), JsError> {
247            let func_args = [event];
248            for entry in listeners {
249                let listener = entry.0;
250                let _res = functions::call_function_q(q_ctx, listener, &func_args, None)?;
251
252                // todo chekc if _res is bool, for cancel and such
253                // and if event is cancelabble and preventDefault was called and such
254            }
255            Ok(())
256        },
257    )?;
258
259    Ok(true)
260}
261
262pub fn _set_event_bubble_target() {
263    unimplemented!()
264}
265
266fn events_instance_finalizer(q_ctx: &QuickJsRealmAdapter, proxy_class_name: &str, id: usize) {
267    // drop all listeners,
268    remove_map(q_ctx, proxy_class_name, id);
269}
270
271pub(crate) fn impl_event_target(proxy: Proxy) -> Proxy {
272    // add (static)     addEventListener(), dispatchEvent(), removeEventListener()
273    // a fn getEventsObj will be used to conditionally create and return an Object with Sets per eventId to store the listeners
274
275    let proxy_class_name = proxy.get_class_name();
276
277    let mut proxy = proxy;
278    if proxy.is_event_target {
279        proxy = proxy
280            .native_method("addEventListener", Some(ext_add_event_listener))
281            .native_method("removeEventListener", Some(ext_remove_event_listener))
282            .native_method("dispatchEvent", Some(ext_dispatch_event))
283            .finalizer(move |_rt, q_ctx, id| {
284                let n = proxy_class_name.as_str();
285                events_instance_finalizer(q_ctx, n, id);
286            });
287    }
288    if proxy.is_static_event_target {
289        // todo, these should be finalized before context is destroyed, we really need a hook in QuickJsContext for that
290        proxy = proxy
291            .static_native_method("addEventListener", Some(ext_add_static_event_listener))
292            .static_native_method(
293                "removeEventListener",
294                Some(ext_remove_static_event_listener),
295            )
296            .static_native_method("dispatchEvent", Some(ext_dispatch_static_event));
297    }
298
299    proxy
300}
301
302unsafe extern "C" fn ext_add_event_listener(
303    ctx: *mut q::JSContext,
304    this_val: q::JSValue,
305    argc: ::std::os::raw::c_int,
306    argv: *mut q::JSValue,
307) -> q::JSValue {
308    // require 2 or 3 args, string, function, object
309    // if third is boolean it is option {capture: true}
310
311    // events_obj will be structured like this
312    // ___eventListeners___: {eventId<String>: Map<Function, Object>} // the key of the map is the function, the value are the options
313
314    let res = QuickJsRealmAdapter::with_context(ctx, |q_ctx| {
315        let args = parse_args(ctx, argc, argv);
316
317        let this_ref =
318            QuickJsValueAdapter::new(ctx, this_val, true, true, "add_event_listener_this");
319
320        let proxy_info = get_proxy_instance_info(this_ref.borrow_value());
321
322        if args.len() < 2 || !args[0].is_string() || !functions::is_function_q(q_ctx, &args[1]) {
323            Err(JsError::new_str("addEventListener requires at least 2 arguments (eventId: String and Listener: Function"))
324        } else {
325            let event_id = primitives::to_string_q(q_ctx, &args[0])?;
326            let listener_func = args[1].clone();
327
328            // use the passed options arg or create a new obj
329            let options_obj = if args.len() == 3 && args[2].is_object() {
330                args[2].clone()
331            } else {
332                create_object_q(q_ctx)?
333            };
334            // if the third args was a boolean then set that bool as the capture option
335            if args.len() == 3 && args[2].is_bool() {
336                set_property_q(q_ctx, &options_obj, "capture", &args[2])?;
337            }
338
339            add_event_listener(
340                q_ctx,
341                proxy_info.class_name.as_str(),
342                event_id.as_str(),
343                proxy_info.id,
344                listener_func,
345                options_obj,
346            );
347
348            Ok(())
349        }
350    });
351    match res {
352        Ok(_) => quickjs_utils::new_null(),
353        Err(e) => QuickJsRealmAdapter::report_ex_ctx(ctx, format!("{e}").as_str()),
354    }
355}
356
357unsafe extern "C" fn ext_remove_event_listener(
358    ctx: *mut q::JSContext,
359    this_val: q::JSValue,
360    argc: ::std::os::raw::c_int,
361    argv: *mut q::JSValue,
362) -> q::JSValue {
363    let res = QuickJsRealmAdapter::with_context(ctx, |q_ctx| {
364        let args = parse_args(ctx, argc, argv);
365
366        let this_ref =
367            QuickJsValueAdapter::new(ctx, this_val, true, true, "remove_event_listener_this");
368
369        let proxy_info = get_proxy_instance_info(this_ref.borrow_value());
370
371        if args.len() != 2 || !args[0].is_string() || !functions::is_function_q(q_ctx, &args[1]) {
372            Err(JsError::new_str("removeEventListener requires at least 2 arguments (eventId: String and Listener: Function"))
373        } else {
374            let event_id = primitives::to_string_q(q_ctx, &args[0])?;
375            let listener_func = args[1].clone();
376
377            remove_event_listener(
378                q_ctx,
379                proxy_info.class_name.as_str(),
380                event_id.as_str(),
381                proxy_info.id,
382                &listener_func,
383            );
384
385            Ok(())
386        }
387    });
388    match res {
389        Ok(_) => quickjs_utils::new_null(),
390        Err(e) => QuickJsRealmAdapter::report_ex_ctx(ctx, format!("{e}").as_str()),
391    }
392}
393
394unsafe extern "C" fn ext_dispatch_event(
395    ctx: *mut q::JSContext,
396    this_val: q::JSValue,
397    argc: ::std::os::raw::c_int,
398    argv: *mut q::JSValue,
399) -> q::JSValue {
400    let res = QuickJsRealmAdapter::with_context(ctx, |q_ctx| {
401        let args = parse_args(ctx, argc, argv);
402
403        let this_ref =
404            QuickJsValueAdapter::new(ctx, this_val, true, true, "remove_event_listener_this");
405
406        let proxy_info = get_proxy_instance_info(this_ref.borrow_value());
407
408        if args.len() != 2 || !args[0].is_string() {
409            Err(JsError::new_str(
410                "dispatchEvent requires at least 2 arguments (eventId: String and eventObj: Object)",
411            ))
412        } else {
413            let event_id = primitives::to_string_q(q_ctx, &args[0])?;
414            let evt_obj = args[1].clone();
415
416            let proxy = get_proxy(q_ctx, proxy_info.class_name.as_str()).unwrap();
417
418            let res = dispatch_event(q_ctx, &proxy, proxy_info.id, event_id.as_str(), evt_obj)?;
419
420            Ok(res)
421        }
422    });
423    match res {
424        Ok(res) => {
425            let b_ref = from_bool(res);
426            b_ref.clone_value_incr_rc()
427        }
428        Err(e) => QuickJsRealmAdapter::report_ex_ctx(ctx, format!("{e}").as_str()),
429    }
430}
431
432unsafe fn get_static_proxy_class_name(
433    q_ctx: &QuickJsRealmAdapter,
434    obj: &QuickJsValueAdapter,
435) -> String {
436    let proxy_name_ref = objects::get_property(q_ctx.context, obj, "name")
437        .ok()
438        .unwrap();
439    primitives::to_string(q_ctx.context, &proxy_name_ref)
440        .ok()
441        .unwrap()
442}
443
444unsafe extern "C" fn ext_add_static_event_listener(
445    ctx: *mut q::JSContext,
446    this_val: q::JSValue,
447    argc: ::std::os::raw::c_int,
448    argv: *mut q::JSValue,
449) -> q::JSValue {
450    let res = QuickJsRealmAdapter::with_context(ctx, |q_ctx| {
451        let args = parse_args(ctx, argc, argv);
452
453        let this_ref =
454            QuickJsValueAdapter::new(ctx, this_val, true, true, "add_event_listener_this");
455
456        let proxy_name = get_static_proxy_class_name(q_ctx, &this_ref);
457
458        if args.len() < 2 || !args[0].is_string() || !functions::is_function_q(q_ctx, &args[1]) {
459            Err(JsError::new_str("addEventListener requires at least 2 arguments (eventId: String and Listener: Function"))
460        } else {
461            let event_id = primitives::to_string_q(q_ctx, &args[0])?;
462            let listener_func = args[1].clone();
463
464            // use the passed options arg or create a new obj
465            let options_obj = if args.len() == 3 && args[2].is_object() {
466                args[2].clone()
467            } else {
468                create_object_q(q_ctx)?
469            };
470            // if the third args was a boolean then set that bool as the capture option
471            if args.len() == 3 && args[2].is_bool() {
472                set_property_q(q_ctx, &options_obj, "capture", &args[2])?;
473            }
474
475            add_static_event_listener(
476                q_ctx,
477                proxy_name.as_str(),
478                event_id.as_str(),
479                listener_func,
480                options_obj,
481            );
482
483            Ok(())
484        }
485    });
486    match res {
487        Ok(_) => quickjs_utils::new_null(),
488        Err(e) => QuickJsRealmAdapter::report_ex_ctx(ctx, format!("{e}").as_str()),
489    }
490}
491
492unsafe extern "C" fn ext_remove_static_event_listener(
493    ctx: *mut q::JSContext,
494    this_val: q::JSValue,
495    argc: ::std::os::raw::c_int,
496    argv: *mut q::JSValue,
497) -> q::JSValue {
498    let res = QuickJsRealmAdapter::with_context(ctx, |q_ctx| {
499        let args = parse_args(ctx, argc, argv);
500
501        let this_ref =
502            QuickJsValueAdapter::new(ctx, this_val, true, true, "remove_event_listener_this");
503
504        let proxy_name = get_static_proxy_class_name(q_ctx, &this_ref);
505
506        if args.len() != 2 || !args[0].is_string() || !functions::is_function_q(q_ctx, &args[1]) {
507            Err(JsError::new_str("removeEventListener requires at least 2 arguments (eventId: String and Listener: Function"))
508        } else {
509            let event_id = primitives::to_string_q(q_ctx, &args[0])?;
510            let listener_func = args[1].clone();
511
512            remove_static_event_listener(
513                q_ctx,
514                proxy_name.as_str(),
515                event_id.as_str(),
516                &listener_func,
517            );
518
519            Ok(())
520        }
521    });
522    match res {
523        Ok(_) => quickjs_utils::new_null(),
524        Err(e) => QuickJsRealmAdapter::report_ex_ctx(ctx, format!("{e}").as_str()),
525    }
526}
527
528unsafe extern "C" fn ext_dispatch_static_event(
529    ctx: *mut q::JSContext,
530    this_val: q::JSValue,
531    argc: ::std::os::raw::c_int,
532    argv: *mut q::JSValue,
533) -> q::JSValue {
534    let res = QuickJsRealmAdapter::with_context(ctx, |q_ctx| {
535        let args = parse_args(ctx, argc, argv);
536
537        let this_ref =
538            QuickJsValueAdapter::new(ctx, this_val, true, true, "remove_event_listener_this");
539
540        let proxy_name = get_static_proxy_class_name(q_ctx, &this_ref);
541
542        if args.len() != 2 || !args[0].is_string() {
543            Err(JsError::new_str(
544                "dispatchEvent requires at least 2 arguments (eventId: String and eventObj: Object)",
545            ))
546        } else {
547            let event_id = primitives::to_string_q(q_ctx, &args[0])?;
548            let evt_obj = args[1].clone();
549
550            let res =
551                dispatch_static_event(q_ctx, proxy_name.as_str(), event_id.as_str(), evt_obj)?;
552
553            Ok(res)
554        }
555    });
556    match res {
557        Ok(res) => {
558            let b_ref = from_bool(res);
559            b_ref.clone_value_incr_rc()
560        }
561        Err(e) => QuickJsRealmAdapter::report_ex_ctx(ctx, format!("{e}").as_str()),
562    }
563}
564
565#[cfg(test)]
566pub mod tests {
567    use crate::facades::tests::init_test_rt;
568    use crate::jsutils::Script;
569    use crate::quickjs_utils::get_global_q;
570    use crate::quickjs_utils::objects::{create_object_q, get_property_q};
571    use crate::quickjs_utils::primitives::to_i32;
572    use crate::reflection::eventtarget::dispatch_event;
573    use crate::reflection::{get_proxy, Proxy};
574    use std::sync::{Arc, Mutex};
575
576    #[test]
577    fn test_proxy_eh() {
578        let instance_ids: Arc<Mutex<Vec<usize>>> = Arc::new(Mutex::new(vec![]));
579
580        let instance_ids2 = instance_ids.clone();
581
582        let rt = init_test_rt();
583        let ct = rt.exe_rt_task_in_event_loop(move |q_js_rt| {
584            let q_ctx = q_js_rt.get_main_realm();
585            Proxy::new()
586                .namespace(&[])
587                .constructor(move |_rt, _q, id, _args| {
588                    log::debug!("construct id={}", id);
589                    let vec = &mut *instance_ids2.lock().unwrap();
590                    vec.push(id);
591                    Ok(())
592                })
593                .finalizer(|_rt, _q_ctx, id| {
594                    log::debug!("finalize id={}", id);
595                })
596                .name("MyThing")
597                .event_target()
598                .install(q_ctx, true)
599                .expect("proxy failed");
600
601            match q_ctx.eval(Script::new(
602                "test_proxy_eh.es",
603                "\
604            this.called = false;\
605            let test_proxy_eh_instance = new MyThing();\
606            this.ct = 0;\
607            let listener1 = (evt) => {this.ct++;};\
608            let listener2 = (evt) => {this.ct++;};\
609            test_proxy_eh_instance.addEventListener('someEvent', listener1);\
610            test_proxy_eh_instance.addEventListener('someEvent', listener2);\
611            test_proxy_eh_instance.removeEventListener('someEvent', listener2);\
612            ",
613            )) {
614                Ok(_) => {}
615                Err(e) => {
616                    log::error!("script failed: {}", e);
617                    panic!("script failed: {}", e);
618                }
619            };
620            let global = get_global_q(q_ctx);
621
622            let proxy = get_proxy(q_ctx, "MyThing").unwrap();
623            let vec = &mut *instance_ids.lock().unwrap();
624            let id = vec[0];
625            let evt = create_object_q(q_ctx).ok().unwrap();
626            let _ = dispatch_event(q_ctx, &proxy, id, "someEvent", evt).expect("dispatch failed");
627
628            let ct_ref = get_property_q(q_ctx, &global, "ct").ok().unwrap();
629
630            to_i32(&ct_ref).ok().unwrap()
631        });
632        log::info!("ok was {}", ct);
633        assert_eq!(ct, 1);
634    }
635
636    #[test]
637    fn test_proxy_eh_rcs() {
638        let rt = init_test_rt();
639        rt.exe_rt_task_in_event_loop(|q_js_rt| {
640            let q_ctx = q_js_rt.get_main_realm();
641            Proxy::new()
642                .namespace(&[])
643                .constructor(move |_rt, _q, id, _args| {
644                    log::debug!("construct id={}", id);
645                    Ok(())
646                })
647                .finalizer(|_rt, _q_ctx, id| {
648                    log::debug!("finalize id={}", id);
649                })
650                .name("MyThing")
651                .event_target()
652                .install(q_ctx, true)
653                .expect("proxy failed");
654
655            q_ctx
656                .eval(Script::new("e.es", "let target = new MyThing();"))
657                .expect("constr failed");
658            let _target_ref = q_ctx
659                .eval(Script::new("t.es", "(target);"))
660                .expect("could not get target");
661
662            #[cfg(feature = "bellard")]
663            assert_eq!(_target_ref.get_ref_count(), 2); // one for me one for global
664
665            q_ctx
666                .eval(Script::new(
667                    "r.es",
668                    "target.addEventListener('someEvent', (evt) => {console.log('got event');});",
669                ))
670                .expect("addlistnrfailed");
671            #[cfg(feature = "bellard")]
672            assert_eq!(_target_ref.get_ref_count(), 2); // one for me one for global
673        });
674    }
675}