@salve-software/react-native-nitro-jsdom 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/android/CMakeLists.txt +6 -0
  2. package/cpp/HybridHtmlSandbox.cpp +14 -7
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +65 -3
  5. package/cpp/lexbor/LexborDocument.hpp +17 -2
  6. package/cpp/quickjs/DOMBindings.cpp +14 -1
  7. package/cpp/quickjs/DOMBindingsInternal.cpp +67 -0
  8. package/cpp/quickjs/DOMBindingsInternal.hpp +39 -0
  9. package/cpp/quickjs/QuickJSRuntime.cpp +13 -0
  10. package/cpp/quickjs/QuickJSRuntime.hpp +29 -0
  11. package/cpp/quickjs/bindings/AbortBindings.cpp +14 -0
  12. package/cpp/quickjs/bindings/BlobBindings.cpp +28 -1
  13. package/cpp/quickjs/bindings/BlobBindings.hpp +3 -3
  14. package/cpp/quickjs/bindings/CSSOMBindings.cpp +96 -0
  15. package/cpp/quickjs/bindings/CSSOMBindings.hpp +3 -1
  16. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +1 -1
  17. package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
  18. package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
  19. package/cpp/quickjs/bindings/DocumentBindings.cpp +174 -11
  20. package/cpp/quickjs/bindings/ElementBindings.cpp +516 -20
  21. package/cpp/quickjs/bindings/EventBindings.cpp +281 -5
  22. package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
  23. package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
  24. package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
  25. package/cpp/quickjs/bindings/FetchBindings.cpp +43 -8
  26. package/cpp/quickjs/bindings/FormBindings.cpp +294 -0
  27. package/cpp/quickjs/bindings/FormBindings.hpp +9 -5
  28. package/cpp/quickjs/bindings/IntlBindings.cpp +352 -0
  29. package/cpp/quickjs/bindings/IntlBindings.hpp +29 -0
  30. package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
  31. package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
  32. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +45 -0
  33. package/cpp/quickjs/bindings/TimerBindings.cpp +79 -0
  34. package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
  35. package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
  36. package/cpp/quickjs/bindings/UrlBindings.cpp +43 -0
  37. package/cpp/quickjs/bindings/WindowBindings.cpp +295 -4
  38. package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
  39. package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.cpp +241 -0
  40. package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.hpp +30 -0
  41. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +2 -1
  42. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  43. package/lib/module/classes/JSDOM/JSDOM.class.js +2 -1
  44. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  45. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  46. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts +5 -4
  47. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts.map +1 -1
  48. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  49. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  50. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  51. package/package.json +17 -2
  52. package/src/classes/JSDOM/JSDOM.class.ts +2 -1
  53. package/src/classes/JSDOM/types/IJSDOMOptions.ts +5 -4
  54. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -34,9 +34,17 @@ JSValue js_Event_constructor(JSContext* ctx, JSValue, int argc, JSValue* argv) {
34
34
  else { cancelable = JS_ToBool(ctx, c) > 0; JS_FreeValue(ctx, c); }
35
35
  }
36
36
 
37
+ bool composed = false;
38
+ if (argc >= 2 && JS_IsObject(argv[1])) {
39
+ JSValue c = JS_GetPropertyStr(ctx, argv[1], "composed");
40
+ if (JS_IsException(c)) { JS_FreeValue(ctx, JS_GetException(ctx)); }
41
+ else { composed = JS_ToBool(ctx, c) > 0; JS_FreeValue(ctx, c); }
42
+ }
43
+
37
44
  JS_SetPropertyStr(ctx, obj, "defaultPrevented", JS_NewBool(ctx, false));
38
45
  JS_SetPropertyStr(ctx, obj, "bubbles", JS_NewBool(ctx, bubbles));
39
46
  JS_SetPropertyStr(ctx, obj, "cancelable", JS_NewBool(ctx, cancelable));
47
+ JS_SetPropertyStr(ctx, obj, "composed", JS_NewBool(ctx, composed));
40
48
  return obj;
41
49
  }
42
50
 
@@ -64,9 +72,17 @@ JSValue js_CustomEvent_constructor(JSContext* ctx, JSValue, int argc, JSValue* a
64
72
  else { cancelable = JS_ToBool(ctx, c) > 0; JS_FreeValue(ctx, c); }
65
73
  }
66
74
 
75
+ bool composed = false;
76
+ if (argc >= 2 && JS_IsObject(argv[1])) {
77
+ JSValue c = JS_GetPropertyStr(ctx, argv[1], "composed");
78
+ if (JS_IsException(c)) { JS_FreeValue(ctx, JS_GetException(ctx)); }
79
+ else { composed = JS_ToBool(ctx, c) > 0; JS_FreeValue(ctx, c); }
80
+ }
81
+
67
82
  JS_SetPropertyStr(ctx, obj, "detail", detail);
68
83
  JS_SetPropertyStr(ctx, obj, "bubbles", JS_NewBool(ctx, bubbles));
69
84
  JS_SetPropertyStr(ctx, obj, "cancelable", JS_NewBool(ctx, cancelable));
85
+ JS_SetPropertyStr(ctx, obj, "composed", JS_NewBool(ctx, composed));
70
86
  JS_SetPropertyStr(ctx, obj, "defaultPrevented", JS_NewBool(ctx, false));
71
87
  return obj;
72
88
  }
@@ -113,6 +129,7 @@ JSValue js_KeyboardEvent_constructor(JSContext* ctx, JSValue, int argc, JSValue*
113
129
  JSValue init = (argc >= 2) ? argv[1] : JS_UNDEFINED;
114
130
  JS_SetPropertyStr(ctx, obj, "bubbles", JS_NewBool(ctx, get_bool_init_field(ctx, init, "bubbles")));
115
131
  JS_SetPropertyStr(ctx, obj, "cancelable", JS_NewBool(ctx, get_bool_init_field(ctx, init, "cancelable")));
132
+ JS_SetPropertyStr(ctx, obj, "composed", JS_NewBool(ctx, get_bool_init_field(ctx, init, "composed")));
116
133
  JS_SetPropertyStr(ctx, obj, "defaultPrevented", JS_NewBool(ctx, false));
117
134
 
118
135
  std::string key = get_str_init_field(ctx, init, "key", "");
@@ -137,6 +154,7 @@ JSValue js_MouseEvent_constructor(JSContext* ctx, JSValue, int argc, JSValue* ar
137
154
  JSValue init = (argc >= 2) ? argv[1] : JS_UNDEFINED;
138
155
  JS_SetPropertyStr(ctx, obj, "bubbles", JS_NewBool(ctx, get_bool_init_field(ctx, init, "bubbles")));
139
156
  JS_SetPropertyStr(ctx, obj, "cancelable", JS_NewBool(ctx, get_bool_init_field(ctx, init, "cancelable")));
157
+ JS_SetPropertyStr(ctx, obj, "composed", JS_NewBool(ctx, get_bool_init_field(ctx, init, "composed")));
140
158
  JS_SetPropertyStr(ctx, obj, "defaultPrevented", JS_NewBool(ctx, false));
141
159
 
142
160
  JS_SetPropertyStr(ctx, obj, "clientX", JS_NewFloat64(ctx, get_num_init_field(ctx, init, "clientX", 0)));
@@ -173,6 +191,18 @@ JSValue js_event_stopImmediatePropagation(JSContext* ctx, JSValue this_val, int,
173
191
  return JS_UNDEFINED;
174
192
  }
175
193
 
194
+ // __composedPath is populated by dispatch_event_on_target() right before
195
+ // listener invocation begins; an event that was never dispatched (just
196
+ // constructed) has none, matching the spec's "not currently dispatched" case.
197
+ JSValue js_event_composedPath(JSContext* ctx, JSValue this_val, int, JSValue*) {
198
+ JSValue path = JS_GetPropertyStr(ctx, this_val, "__composedPath");
199
+ if (JS_IsUndefined(path) || JS_IsException(path)) {
200
+ if (JS_IsException(path)) JS_FreeValue(ctx, JS_GetException(ctx));
201
+ return JS_NewArray(ctx);
202
+ }
203
+ return path;
204
+ }
205
+
176
206
  // ── addEventListener / removeEventListener (element) ──────────────────────────
177
207
 
178
208
  JSValue js_el_addEventListener(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
@@ -189,7 +219,7 @@ JSValue js_el_addEventListener(JSContext* ctx, JSValue this_val, int argc, JSVal
189
219
 
190
220
  void* node = lxb_dom_interface_node(el);
191
221
  for (auto& l : rctx->listeners) {
192
- if (l.node == node && l.event_type == event_type) {
222
+ if (l.node == node && l.event_type == event_type && !l.is_handler_property) {
193
223
  JSValue* stored = static_cast<JSValue*>(l.callback);
194
224
  if (JS_VALUE_GET_TAG(*stored) == JS_VALUE_GET_TAG(argv[1]) &&
195
225
  JS_VALUE_GET_PTR(*stored) == JS_VALUE_GET_PTR(argv[1]))
@@ -219,7 +249,7 @@ JSValue js_el_removeEventListener(JSContext* ctx, JSValue this_val, int argc, JS
219
249
 
220
250
  void* node = lxb_dom_interface_node(el);
221
251
  for (auto it = rctx->listeners.begin(); it != rctx->listeners.end(); ) {
222
- if (it->node == node && it->event_type == event_type) {
252
+ if (it->node == node && it->event_type == event_type && !it->is_handler_property) {
223
253
  JSValue* stored_cb = static_cast<JSValue*>(it->callback);
224
254
  if (JS_VALUE_GET_TAG(*stored_cb) == JS_VALUE_GET_TAG(argv[1]) &&
225
255
  JS_VALUE_GET_PTR(*stored_cb) == JS_VALUE_GET_PTR(argv[1])) {
@@ -254,6 +284,7 @@ JSValue dispatch_event_on_target(JSContext* ctx, RuntimeContext* rctx, JSValue e
254
284
  JS_FreeCString(ctx, type_str);
255
285
 
256
286
  bool bubbles = get_bool_prop(ctx, event_obj, "bubbles");
287
+ bool composed = get_bool_prop(ctx, event_obj, "composed");
257
288
 
258
289
  lxb_dom_node_t* doc_node = nullptr;
259
290
  if (rctx->document) {
@@ -267,7 +298,46 @@ JSValue dispatch_event_on_target(JSContext* ctx, RuntimeContext* rctx, JSValue e
267
298
  std::vector<lxb_dom_node_t*> chain;
268
299
  chain.push_back(target_node);
269
300
  if (bubbles) {
270
- for (lxb_dom_node_t* p = target_node->parent; p; p = p->parent) chain.push_back(p);
301
+ // Climb ancestors; when we reach the top of a shadow tree (a registered
302
+ // ShadowRoot — it has no `parent` in the node tree, since Lexbor doesn't
303
+ // link a shadow root into its host's child list) and the event is
304
+ // composed, retarget through the shadow host (found by reverse-lookup in
305
+ // element_shadow_roots) and keep climbing into the light tree above it —
306
+ // this is how a composed event (e.g. 'click') escapes a shadow boundary
307
+ // while a non-composed one stays contained. The ShadowRoot node itself is
308
+ // never added to the chain/composedPath (unlike real DOM, which does
309
+ // include it): this sandbox's ShadowRoot wrapper only exists via
310
+ // ShadowRootBindings' host-keyed cache, so building one here via the
311
+ // generic make_element() path (used for every other chain entry) would
312
+ // produce a broken, non-ShadowRoot-shaped object instead. Simplification:
313
+ // this sandbox has no separate capture phase, so composedPath()/
314
+ // retargeting are only computed along the bubble chain — a non-bubbling
315
+ // event's composedPath() is just [target].
316
+ lxb_dom_node_t* p = target_node->parent;
317
+ while (p) {
318
+ void* host = nullptr;
319
+ for (auto& kv : rctx->element_shadow_roots) {
320
+ if (kv.second == static_cast<void*>(p)) { host = kv.first; break; }
321
+ }
322
+ if (host) {
323
+ if (!composed) break;
324
+ p = static_cast<lxb_dom_node_t*>(host);
325
+ }
326
+ chain.push_back(p);
327
+ p = p->parent;
328
+ }
329
+ }
330
+
331
+ {
332
+ JSValue path_arr = JS_NewArray(ctx);
333
+ for (size_t i = 0; i < chain.size(); i++) {
334
+ lxb_dom_node_t* level = chain[i];
335
+ JSValue entry = (level == doc_node)
336
+ ? ([&]() { JSValue g = JS_GetGlobalObject(ctx); JSValue d = JS_GetPropertyStr(ctx, g, "document"); JS_FreeValue(ctx, g); return d; })()
337
+ : make_element(ctx, level);
338
+ JS_SetPropertyUint32(ctx, path_arr, (uint32_t)i, entry);
339
+ }
340
+ JS_SetPropertyStr(ctx, event_obj, "__composedPath", path_arr);
271
341
  }
272
342
 
273
343
  for (lxb_dom_node_t* level : chain) {
@@ -287,11 +357,13 @@ JSValue dispatch_event_on_target(JSContext* ctx, RuntimeContext* rctx, JSValue e
287
357
 
288
358
  std::vector<JSValue> cbs_to_fire;
289
359
  std::vector<JSValue*> cb_ptrs;
360
+ std::vector<bool> cb_is_handler_property;
290
361
  for (auto& listener : rctx->listeners) {
291
362
  if (listener.node == level && listener.event_type == event_type) {
292
363
  JSValue* cb = static_cast<JSValue*>(listener.callback);
293
364
  cbs_to_fire.push_back(JS_DupValue(ctx, *cb));
294
365
  cb_ptrs.push_back(cb);
366
+ cb_is_handler_property.push_back(listener.is_handler_property);
295
367
  }
296
368
  }
297
369
 
@@ -314,7 +386,27 @@ JSValue dispatch_event_on_target(JSContext* ctx, RuntimeContext* rctx, JSValue e
314
386
  JS_FreeValue(ctx, cbs_to_fire[i]);
315
387
  continue;
316
388
  }
317
- JSValue ret = JS_Call(ctx, cbs_to_fire[i], current_target, 1, &event_obj);
389
+ JSValue ret;
390
+ if (cb_is_handler_property[i] && event_type == "error") {
391
+ JSValue message_val = JS_GetPropertyStr(ctx, event_obj, "message");
392
+ JSValue error_val = JS_GetPropertyStr(ctx, event_obj, "error");
393
+ JSValue global = JS_GetGlobalObject(ctx);
394
+ JSValue location_val = JS_GetPropertyStr(ctx, global, "location");
395
+ JSValue source_val = JS_GetPropertyStr(ctx, location_val, "href");
396
+ JS_FreeValue(ctx, location_val);
397
+ JS_FreeValue(ctx, global);
398
+ JSValue lineno_val = JS_NewInt32(ctx, 0);
399
+ JSValue colno_val = JS_NewInt32(ctx, 0);
400
+ JSValue args[5] = { message_val, source_val, lineno_val, colno_val, error_val };
401
+ ret = JS_Call(ctx, cbs_to_fire[i], current_target, 5, args);
402
+ JS_FreeValue(ctx, message_val);
403
+ JS_FreeValue(ctx, source_val);
404
+ JS_FreeValue(ctx, lineno_val);
405
+ JS_FreeValue(ctx, colno_val);
406
+ JS_FreeValue(ctx, error_val);
407
+ } else {
408
+ ret = JS_Call(ctx, cbs_to_fire[i], current_target, 1, &event_obj);
409
+ }
318
410
  JS_FreeValue(ctx, cbs_to_fire[i]);
319
411
  if (JS_IsException(ret)) {
320
412
  JS_FreeValue(ctx, ret);
@@ -331,14 +423,23 @@ JSValue dispatch_event_on_target(JSContext* ctx, RuntimeContext* rctx, JSValue e
331
423
  JS_FreeValue(ctx, msg_prop);
332
424
  JS_FreeValue(ctx, ex);
333
425
  JS_ThrowInternalError(ctx, "%s", err.c_str());
426
+ JS_SetPropertyStr(ctx, event_obj, "__composedPath", JS_NewArray(ctx));
334
427
  return JS_EXCEPTION;
335
428
  }
429
+ if (cb_is_handler_property[i] && JS_IsBool(ret) && JS_ToBool(ctx, ret) == 0 &&
430
+ get_bool_prop(ctx, event_obj, "cancelable")) {
431
+ JS_SetPropertyStr(ctx, event_obj, "defaultPrevented", JS_NewBool(ctx, true));
432
+ }
336
433
  JS_FreeValue(ctx, ret);
337
434
  }
338
435
  JS_FreeValue(ctx, current_target);
339
436
  }
340
437
 
341
438
  bool defaultPrevented = get_bool_prop(ctx, event_obj, "defaultPrevented");
439
+ // Per spec, composedPath() only returns a populated path while the event is
440
+ // actively being dispatched — reset it so a reference to `event` saved by
441
+ // a listener returns [] if composedPath() is called after dispatch ends.
442
+ JS_SetPropertyStr(ctx, event_obj, "__composedPath", JS_NewArray(ctx));
342
443
  return JS_NewBool(ctx, !defaultPrevented);
343
444
  }
344
445
 
@@ -367,7 +468,7 @@ JSValue js_doc_addEventListener(JSContext* ctx, JSValue, int argc, JSValue* argv
367
468
  static_cast<lxb_html_document_t*>(rctx->document->documentHtmlPtr())));
368
469
 
369
470
  for (auto& l : rctx->listeners) {
370
- if (l.node == doc_node && l.event_type == event_type) {
471
+ if (l.node == doc_node && l.event_type == event_type && !l.is_handler_property) {
371
472
  JSValue* stored = static_cast<JSValue*>(l.callback);
372
473
  if (JS_VALUE_GET_TAG(*stored) == JS_VALUE_GET_TAG(argv[1]) &&
373
474
  JS_VALUE_GET_PTR(*stored) == JS_VALUE_GET_PTR(argv[1]))
@@ -427,6 +528,130 @@ JSValue js_doc_dispatchEvent(JSContext* ctx, JSValue, int argc, JSValue* argv) {
427
528
  return dispatch_event_on_target(ctx, rctx, argv[0], node);
428
529
  }
429
530
 
531
+ const char* kHandlerEventTypes[] = { "click", "load", "error", "unhandledrejection", "change", "input", "submit", "reset" };
532
+
533
+ JSValue get_handler_prop_for_node(JSContext* ctx, RuntimeContext* rctx, void* node, const std::string& event_type) {
534
+ if (!rctx || !node) return JS_NULL;
535
+ for (auto& l : rctx->listeners) {
536
+ if (l.node == node && l.event_type == event_type && l.is_handler_property)
537
+ return JS_DupValue(ctx, *static_cast<JSValue*>(l.callback));
538
+ }
539
+ return JS_NULL;
540
+ }
541
+
542
+ JSValue set_handler_prop_for_node(JSContext* ctx, RuntimeContext* rctx, void* node, const std::string& event_type, JSValue val) {
543
+ if (!rctx || !node) return JS_UNDEFINED;
544
+ for (auto it = rctx->listeners.begin(); it != rctx->listeners.end(); ++it) {
545
+ if (it->node == node && it->event_type == event_type && it->is_handler_property) {
546
+ JSValue* stored = static_cast<JSValue*>(it->callback);
547
+ JS_FreeValue(ctx, *stored);
548
+ delete stored;
549
+ rctx->listeners.erase(it);
550
+ break;
551
+ }
552
+ }
553
+ if (JS_IsFunction(ctx, val)) {
554
+ EventListener listener;
555
+ listener.node = node;
556
+ listener.event_type = event_type;
557
+ listener.callback = new JSValue(JS_DupValue(ctx, val));
558
+ listener.is_handler_property = true;
559
+ rctx->listeners.push_back(std::move(listener));
560
+ }
561
+ return JS_UNDEFINED;
562
+ }
563
+
564
+ JSValue js_el_get_handler(JSContext* ctx, JSValue this_val, int magic) {
565
+ auto* rctx = get_ctx(ctx);
566
+ auto* node = unwrap_node(ctx, this_val);
567
+ return get_handler_prop_for_node(ctx, rctx, node, kHandlerEventTypes[magic]);
568
+ }
569
+
570
+ JSValue js_el_set_handler(JSContext* ctx, JSValue this_val, JSValue val, int magic) {
571
+ auto* rctx = get_ctx(ctx);
572
+ auto* node = unwrap_node(ctx, this_val);
573
+ return set_handler_prop_for_node(ctx, rctx, node, kHandlerEventTypes[magic], val);
574
+ }
575
+
576
+ void* doc_node_of(RuntimeContext* rctx) {
577
+ if (!rctx || !rctx->document) return nullptr;
578
+ return lxb_dom_interface_node(lxb_dom_interface_document(
579
+ static_cast<lxb_html_document_t*>(rctx->document->documentHtmlPtr())));
580
+ }
581
+
582
+ JSValue js_doc_get_handler(JSContext* ctx, JSValue, int magic) {
583
+ auto* rctx = get_ctx(ctx);
584
+ return get_handler_prop_for_node(ctx, rctx, doc_node_of(rctx), kHandlerEventTypes[magic]);
585
+ }
586
+
587
+ JSValue js_doc_set_handler(JSContext* ctx, JSValue, JSValue val, int magic) {
588
+ auto* rctx = get_ctx(ctx);
589
+ return set_handler_prop_for_node(ctx, rctx, doc_node_of(rctx), kHandlerEventTypes[magic], val);
590
+ }
591
+
592
+ void define_element_handler(JSContext* ctx, JSValue obj, const char* name, int magic) {
593
+ JSAtom atom = JS_NewAtom(ctx, name);
594
+ JSValue get_fn = JS_NewCFunction2(ctx, (JSCFunction*)js_el_get_handler, name, 0, JS_CFUNC_getter_magic, magic);
595
+ JSValue set_fn = JS_NewCFunction2(ctx, (JSCFunction*)js_el_set_handler, name, 1, JS_CFUNC_setter_magic, magic);
596
+ JS_DefinePropertyGetSet(ctx, obj, atom, get_fn, set_fn, JS_PROP_CONFIGURABLE);
597
+ JS_FreeAtom(ctx, atom);
598
+ }
599
+
600
+ void define_doc_handler(JSContext* ctx, JSValue obj, const char* name, int magic) {
601
+ JSAtom atom = JS_NewAtom(ctx, name);
602
+ JSValue get_fn = JS_NewCFunction2(ctx, (JSCFunction*)js_doc_get_handler, name, 0, JS_CFUNC_getter_magic, magic);
603
+ JSValue set_fn = JS_NewCFunction2(ctx, (JSCFunction*)js_doc_set_handler, name, 1, JS_CFUNC_setter_magic, magic);
604
+ JS_DefinePropertyGetSet(ctx, obj, atom, get_fn, set_fn, JS_PROP_CONFIGURABLE);
605
+ JS_FreeAtom(ctx, atom);
606
+ }
607
+
608
+ JSValue make_simple_event(JSContext* ctx, const char* type, bool bubbles, bool cancelable) {
609
+ JSValue obj = JS_NewObjectClass(ctx, js_event_class_id);
610
+ JS_SetPropertyStr(ctx, obj, "type", JS_NewString(ctx, type));
611
+ JS_SetPropertyStr(ctx, obj, "bubbles", JS_NewBool(ctx, bubbles));
612
+ JS_SetPropertyStr(ctx, obj, "cancelable", JS_NewBool(ctx, cancelable));
613
+ JS_SetPropertyStr(ctx, obj, "defaultPrevented", JS_NewBool(ctx, false));
614
+ return obj;
615
+ }
616
+
617
+ void fire_simple_event(JSContext* ctx, RuntimeContext* rctx, const char* type, bool bubbles, bool cancelable, lxb_dom_node_t* node) {
618
+ JSValue evt = make_simple_event(ctx, type, bubbles, cancelable);
619
+ JSValue r = dispatch_event_on_target(ctx, rctx, evt, node);
620
+ JS_FreeValue(ctx, evt);
621
+ if (JS_IsException(r)) JS_FreeValue(ctx, JS_GetException(ctx));
622
+ else JS_FreeValue(ctx, r);
623
+ }
624
+
625
+ JSValue js_el_click(JSContext* ctx, JSValue this_val, int, JSValue*) {
626
+ auto* rctx = get_ctx(ctx);
627
+ auto* node = unwrap_node(ctx, this_val);
628
+ if (!rctx || !node) return JS_UNDEFINED;
629
+ fire_simple_event(ctx, rctx, "click", true, true, node);
630
+ return JS_UNDEFINED;
631
+ }
632
+
633
+ JSValue js_el_focus(JSContext* ctx, JSValue this_val, int, JSValue*) {
634
+ auto* rctx = get_ctx(ctx);
635
+ auto* node = unwrap_node(ctx, this_val);
636
+ if (!rctx || !node || rctx->active_element == node) return JS_UNDEFINED;
637
+
638
+ void* previous = rctx->active_element;
639
+ rctx->active_element = node;
640
+ if (previous) fire_simple_event(ctx, rctx, "blur", false, false, static_cast<lxb_dom_node_t*>(previous));
641
+ fire_simple_event(ctx, rctx, "focus", false, false, node);
642
+ return JS_UNDEFINED;
643
+ }
644
+
645
+ JSValue js_el_blur(JSContext* ctx, JSValue this_val, int, JSValue*) {
646
+ auto* rctx = get_ctx(ctx);
647
+ auto* node = unwrap_node(ctx, this_val);
648
+ if (!rctx || !node || rctx->active_element != node) return JS_UNDEFINED;
649
+
650
+ rctx->active_element = nullptr;
651
+ fire_simple_event(ctx, rctx, "blur", false, false, node);
652
+ return JS_UNDEFINED;
653
+ }
654
+
430
655
  } // namespace
431
656
 
432
657
  void EventBindings::install(JSContext* ctx) {
@@ -438,6 +663,7 @@ void EventBindings::install(JSContext* ctx) {
438
663
  JS_SetPropertyStr(ctx, event_proto, "preventDefault", JS_NewCFunction(ctx, js_event_preventDefault, "preventDefault", 0));
439
664
  JS_SetPropertyStr(ctx, event_proto, "stopPropagation", JS_NewCFunction(ctx, js_event_stopPropagation, "stopPropagation", 0));
440
665
  JS_SetPropertyStr(ctx, event_proto, "stopImmediatePropagation", JS_NewCFunction(ctx, js_event_stopImmediatePropagation, "stopImmediatePropagation", 0));
666
+ JS_SetPropertyStr(ctx, event_proto, "composedPath", JS_NewCFunction(ctx, js_event_composedPath, "composedPath", 0));
441
667
  JS_SetClassProto(ctx, js_event_class_id, event_proto);
442
668
 
443
669
  // ── Event / CustomEvent constructors ───────────────────────────────────────
@@ -457,14 +683,32 @@ void EventBindings::install(JSContext* ctx) {
457
683
  JS_SetPropertyStr(ctx, node_proto, "addEventListener", JS_NewCFunction(ctx, js_el_addEventListener, "addEventListener", 2));
458
684
  JS_SetPropertyStr(ctx, node_proto, "removeEventListener", JS_NewCFunction(ctx, js_el_removeEventListener, "removeEventListener", 2));
459
685
  JS_SetPropertyStr(ctx, node_proto, "dispatchEvent", JS_NewCFunction(ctx, js_el_dispatchEvent, "dispatchEvent", 1));
686
+
687
+ define_element_handler(ctx, node_proto, "onclick", 0);
688
+ define_element_handler(ctx, node_proto, "onload", 1);
689
+ define_element_handler(ctx, node_proto, "onerror", 2);
690
+ define_element_handler(ctx, node_proto, "onchange", 4);
691
+ define_element_handler(ctx, node_proto, "oninput", 5);
692
+ define_element_handler(ctx, node_proto, "onsubmit", 6);
693
+ define_element_handler(ctx, node_proto, "onreset", 7);
460
694
  JS_FreeValue(ctx, node_proto);
461
695
 
696
+ JSValue element_proto = JS_GetClassProto(ctx, js_element_class_id);
697
+ JS_SetPropertyStr(ctx, element_proto, "click", JS_NewCFunction(ctx, js_el_click, "click", 0));
698
+ JS_SetPropertyStr(ctx, element_proto, "focus", JS_NewCFunction(ctx, js_el_focus, "focus", 0));
699
+ JS_SetPropertyStr(ctx, element_proto, "blur", JS_NewCFunction(ctx, js_el_blur, "blur", 0));
700
+ JS_FreeValue(ctx, element_proto);
701
+
462
702
  // ── addEventListener/removeEventListener/dispatchEvent on document ────────
463
703
  // DocumentBindings::install() must have already run so globalThis.document exists.
464
704
  JSValue doc = JS_GetPropertyStr(ctx, global, "document");
465
705
  JS_SetPropertyStr(ctx, doc, "addEventListener", JS_NewCFunction(ctx, js_doc_addEventListener, "addEventListener", 2));
466
706
  JS_SetPropertyStr(ctx, doc, "removeEventListener", JS_NewCFunction(ctx, js_doc_removeEventListener, "removeEventListener", 2));
467
707
  JS_SetPropertyStr(ctx, doc, "dispatchEvent", JS_NewCFunction(ctx, js_doc_dispatchEvent, "dispatchEvent", 1));
708
+
709
+ define_doc_handler(ctx, doc, "onclick", 0);
710
+ define_doc_handler(ctx, doc, "onload", 1);
711
+ define_doc_handler(ctx, doc, "onerror", 2);
468
712
  JS_FreeValue(ctx, doc);
469
713
 
470
714
  // ── addEventListener/removeEventListener/dispatchEvent on window ──────────
@@ -480,7 +724,39 @@ void EventBindings::install(JSContext* ctx) {
480
724
  JS_SetPropertyStr(ctx, global, "removeEventListener", JS_NewCFunction(ctx, js_doc_removeEventListener, "removeEventListener", 2));
481
725
  JS_SetPropertyStr(ctx, global, "dispatchEvent", JS_NewCFunction(ctx, js_doc_dispatchEvent, "dispatchEvent", 1));
482
726
 
727
+ define_doc_handler(ctx, global, "onclick", 0);
728
+ define_doc_handler(ctx, global, "onload", 1);
729
+ define_doc_handler(ctx, global, "onerror", 2);
730
+ define_doc_handler(ctx, global, "onunhandledrejection", 3);
731
+
483
732
  JS_FreeValue(ctx, global);
484
733
  }
485
734
 
735
+ void EventBindings::dispatchErrorEvent(JSContext* ctx, const std::string& message, JSValue error_value) {
736
+ auto* rctx = get_ctx(ctx);
737
+ void* node = doc_node_of(rctx);
738
+ if (!rctx || !node) return;
739
+
740
+ JSValue evt = make_simple_event(ctx, "error", false, true);
741
+ JS_SetPropertyStr(ctx, evt, "message", JS_NewStringLen(ctx, message.data(), message.size()));
742
+ JS_SetPropertyStr(ctx, evt, "error", JS_DupValue(ctx, error_value));
743
+ JSValue r = dispatch_event_on_target(ctx, rctx, evt, static_cast<lxb_dom_node_t*>(node));
744
+ JS_FreeValue(ctx, evt);
745
+ if (JS_IsException(r)) JS_FreeValue(ctx, JS_GetException(ctx));
746
+ else JS_FreeValue(ctx, r);
747
+ }
748
+
749
+ void EventBindings::dispatchUnhandledRejectionEvent(JSContext* ctx, JSValue reason) {
750
+ auto* rctx = get_ctx(ctx);
751
+ void* node = doc_node_of(rctx);
752
+ if (!rctx || !node) return;
753
+
754
+ JSValue evt = make_simple_event(ctx, "unhandledrejection", false, true);
755
+ JS_SetPropertyStr(ctx, evt, "reason", JS_DupValue(ctx, reason));
756
+ JSValue r = dispatch_event_on_target(ctx, rctx, evt, static_cast<lxb_dom_node_t*>(node));
757
+ JS_FreeValue(ctx, evt);
758
+ if (JS_IsException(r)) JS_FreeValue(ctx, JS_GetException(ctx));
759
+ else JS_FreeValue(ctx, r);
760
+ }
761
+
486
762
  } // namespace margelo::nitro::nitrojsdom
@@ -10,12 +10,15 @@
10
10
  // attach addEventListener/dispatchEvent there too).
11
11
 
12
12
  #include "quickjs.h"
13
+ #include <string>
13
14
 
14
15
  namespace margelo::nitro::nitrojsdom {
15
16
 
16
17
  class EventBindings {
17
18
  public:
18
19
  static void install(JSContext* ctx);
20
+ static void dispatchErrorEvent(JSContext* ctx, const std::string& message, JSValue error_value);
21
+ static void dispatchUnhandledRejectionEvent(JSContext* ctx, JSValue reason);
19
22
  };
20
23
 
21
24
  } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,75 @@
1
+ #include "EventTargetBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kEventTargetBootstrapScript = R"JS(
9
+ (function() {
10
+ function EventTarget() {
11
+ this._listeners = Object.create(null);
12
+ }
13
+
14
+ EventTarget.prototype.addEventListener = function(type, listener, options) {
15
+ if (typeof listener !== 'function' && !(listener && typeof listener.handleEvent === 'function')) return;
16
+ type = String(type);
17
+ var capture = !!(options === true || (options && options.capture));
18
+ var once = !!(options && options.once);
19
+ var list = this._listeners[type] || (this._listeners[type] = []);
20
+ for (var i = 0; i < list.length; i++) {
21
+ if (list[i].listener === listener && list[i].capture === capture) return;
22
+ }
23
+ list.push({ listener: listener, capture: capture, once: once });
24
+ };
25
+
26
+ EventTarget.prototype.removeEventListener = function(type, listener, options) {
27
+ var list = this._listeners[String(type)];
28
+ if (!list) return;
29
+ var capture = !!(options === true || (options && options.capture));
30
+ for (var i = 0; i < list.length; i++) {
31
+ if (list[i].listener === listener && list[i].capture === capture) { list.splice(i, 1); return; }
32
+ }
33
+ };
34
+
35
+ EventTarget.prototype.dispatchEvent = function(event) {
36
+ var list = this._listeners[event.type];
37
+ Object.defineProperty(event, 'target', { value: this, configurable: true });
38
+ Object.defineProperty(event, 'currentTarget', { value: this, configurable: true });
39
+ try {
40
+ if (list) {
41
+ var snapshot = list.slice();
42
+ for (var i = 0; i < snapshot.length; i++) {
43
+ var entry = snapshot[i];
44
+ if (list.indexOf(entry) === -1) continue;
45
+ if (entry.once) {
46
+ var idx = list.indexOf(entry);
47
+ if (idx !== -1) list.splice(idx, 1);
48
+ }
49
+ if (typeof entry.listener === 'function') entry.listener.call(this, event);
50
+ else entry.listener.handleEvent(event);
51
+ if (event.__immediatePropagationStopped) break;
52
+ }
53
+ }
54
+ } finally {
55
+ Object.defineProperty(event, 'currentTarget', { value: null, configurable: true });
56
+ }
57
+ return !event.defaultPrevented;
58
+ };
59
+
60
+ globalThis.EventTarget = EventTarget;
61
+ })();
62
+ )JS";
63
+
64
+ } // namespace
65
+
66
+ void EventTargetBindings::install(JSContext* ctx) {
67
+ JSValue result = JS_Eval(ctx, kEventTargetBootstrapScript, strlen(kEventTargetBootstrapScript),
68
+ "<event-target-bootstrap>", JS_EVAL_TYPE_GLOBAL);
69
+ if (JS_IsException(result)) {
70
+ JS_FreeValue(ctx, JS_GetException(ctx));
71
+ }
72
+ JS_FreeValue(ctx, result);
73
+ }
74
+
75
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+
3
+ // globalThis.EventTarget — a standalone, constructible EventTarget for
4
+ // scripts that want addEventListener/removeEventListener/dispatchEvent
5
+ // pub-sub semantics without extending an Element/document/window (which
6
+ // already have their own dispatch paths — see EventBindings). Pure JS: its
7
+ // own per-instance listener map, no bubbling (a bare EventTarget has no
8
+ // ancestors to bubble through), reusing the Event/CustomEvent classes
9
+ // EventBindings already installs.
10
+ //
11
+ // Must run after EventBindings (globalThis.Event, DOMException already
12
+ // available for the "listener not a function" TypeError path — actually
13
+ // mirrors native EventTarget, which silently ignores a non-callable
14
+ // listener rather than throwing).
15
+
16
+ #include "quickjs.h"
17
+
18
+ namespace margelo::nitro::nitrojsdom {
19
+
20
+ struct EventTargetBindings {
21
+ static void install(JSContext* ctx);
22
+ };
23
+
24
+ } // namespace margelo::nitro::nitrojsdom
@@ -118,7 +118,7 @@ const char* kFetchBootstrapScript = R"JS(
118
118
  this.status = init.status !== undefined ? init.status : 200;
119
119
  this.statusText = init.statusText !== undefined ? init.statusText : '';
120
120
  this.ok = this.status >= 200 && this.status < 300;
121
- this.headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
121
+ this.headers = new Headers(init.headers);
122
122
  this.bodyUsed = false;
123
123
  }
124
124
  Response.prototype.text = function() {
@@ -138,19 +138,54 @@ const char* kFetchBootstrapScript = R"JS(
138
138
  };
139
139
  globalThis.Response = Response;
140
140
 
141
+ function Request(input, init) {
142
+ init = init || {};
143
+ var fromRequest = input instanceof Request;
144
+ if (fromRequest && input.bodyUsed) {
145
+ throw new TypeError('Cannot construct a Request with a Request object that has already been used.');
146
+ }
147
+ this.url = fromRequest ? input.url : String(input);
148
+ this.method = (init.method || (fromRequest ? input.method : 'GET')).toUpperCase();
149
+ this.headers = new Headers(init.headers || (fromRequest ? input.headers : undefined));
150
+ var initBody = (init.body === undefined) ? (fromRequest ? input._body : undefined) : init.body;
151
+ this._body = (initBody === undefined || initBody === null) ? undefined : initBody;
152
+ this.signal = init.signal || (fromRequest ? input.signal : undefined);
153
+ this.bodyUsed = false;
154
+ if (fromRequest) input.bodyUsed = true;
155
+ }
156
+ Request.prototype.text = function() {
157
+ if (this.bodyUsed) return Promise.reject(new TypeError('Body has already been consumed.'));
158
+ this.bodyUsed = true;
159
+ return Promise.resolve(this._body === undefined ? '' : String(this._body));
160
+ };
161
+ Request.prototype.json = function() {
162
+ if (this.bodyUsed) return Promise.reject(new TypeError('Body has already been consumed.'));
163
+ this.bodyUsed = true;
164
+ try {
165
+ return Promise.resolve(JSON.parse(this._body));
166
+ } catch (e) {
167
+ return Promise.reject(e);
168
+ }
169
+ };
170
+ Request.prototype.clone = function() {
171
+ if (this.bodyUsed) throw new TypeError('Failed to execute clone: body is already used.');
172
+ return new Request(this.url, { method: this.method, headers: this.headers, body: this._body, signal: this.signal });
173
+ };
174
+ globalThis.Request = Request;
175
+
141
176
  globalThis.fetch = function(input, init) {
142
177
  return new Promise(function(resolve, reject) {
143
178
  try {
144
- init = init || {};
145
- if (init.signal && init.signal.aborted) {
146
- reject(init.signal.reason !== undefined ? init.signal.reason : new Error('The operation was aborted'));
179
+ var req = new Request(input, init);
180
+ if (req.signal && req.signal.aborted) {
181
+ reject(req.signal.reason !== undefined ? req.signal.reason : new Error('The operation was aborted'));
147
182
  return;
148
183
  }
149
- var url = typeof input === 'string' ? input : (input && input.url);
150
- var method = (init.method || 'GET').toUpperCase();
151
- var headers = normalizeHeaders(init.headers);
184
+ var url = req.url;
185
+ var method = req.method;
186
+ var headers = normalizeHeaders(req.headers);
152
187
  var headersJson = JSON.stringify(headers);
153
- var body = (init.body === undefined || init.body === null) ? undefined : String(init.body);
188
+ var body = req._body === undefined ? undefined : String(req._body);
154
189
 
155
190
  var raw = __nativeFetchSync(url, method, headersJson, body);
156
191
  if (raw.error) {