@salve-software/react-native-nitro-jsdom 2.0.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 (33) hide show
  1. package/android/CMakeLists.txt +2 -0
  2. package/cpp/HybridHtmlSandbox.cpp +7 -4
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +3 -3
  5. package/cpp/lexbor/LexborDocument.hpp +3 -2
  6. package/cpp/quickjs/DOMBindings.cpp +5 -0
  7. package/cpp/quickjs/QuickJSRuntime.hpp +1 -0
  8. package/cpp/quickjs/bindings/CSSOMBindings.cpp +50 -0
  9. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +1 -1
  10. package/cpp/quickjs/bindings/DocumentBindings.cpp +8 -0
  11. package/cpp/quickjs/bindings/ElementBindings.cpp +221 -1
  12. package/cpp/quickjs/bindings/EventBindings.cpp +8 -4
  13. package/cpp/quickjs/bindings/FormBindings.cpp +71 -0
  14. package/cpp/quickjs/bindings/IntlBindings.cpp +352 -0
  15. package/cpp/quickjs/bindings/IntlBindings.hpp +29 -0
  16. package/cpp/quickjs/bindings/UrlBindings.cpp +34 -0
  17. package/cpp/quickjs/bindings/WindowBindings.cpp +9 -0
  18. package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.cpp +241 -0
  19. package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.hpp +30 -0
  20. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +2 -1
  21. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  22. package/lib/module/classes/JSDOM/JSDOM.class.js +2 -1
  23. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  24. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  25. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts +5 -4
  26. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts.map +1 -1
  27. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  28. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  29. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  30. package/package.json +1 -1
  31. package/src/classes/JSDOM/JSDOM.class.ts +2 -1
  32. package/src/classes/JSDOM/types/IJSDOMOptions.ts +5 -4
  33. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -61,6 +61,7 @@ add_library(${PACKAGE_NAME} SHARED
61
61
  ../cpp/quickjs/bindings/UrlBindings.cpp
62
62
  ../cpp/quickjs/bindings/AbortBindings.cpp
63
63
  ../cpp/quickjs/bindings/TextEncodingBindings.cpp
64
+ ../cpp/quickjs/bindings/IntlBindings.cpp
64
65
  ../cpp/quickjs/bindings/FormBindings.cpp
65
66
  ../cpp/quickjs/bindings/BlobBindings.cpp
66
67
  ../cpp/quickjs/bindings/CSSOMBindings.cpp
@@ -73,6 +74,7 @@ add_library(${PACKAGE_NAME} SHARED
73
74
  ../cpp/quickjs/bindings/LayoutStubBindings.cpp
74
75
  ../cpp/quickjs/bindings/TreeWalkerBindings.cpp
75
76
  ../cpp/quickjs/bindings/EventTargetBindings.cpp
77
+ ../cpp/quickjs/bindings/WindowNamedPropertiesBindings.cpp
76
78
  ${QUICKJS_SOURCES}
77
79
  )
78
80
 
@@ -43,10 +43,13 @@ void HybridHtmlSandbox::initialize(const std::string& html, bool runScripts, con
43
43
  _runtime->bindDocument(_document.get());
44
44
 
45
45
  if (runScripts) {
46
- for (const auto& script : _document->getScriptContents()) {
46
+ auto* rctx = _runtime->contextState();
47
+ for (const auto& [scriptEl, script] : _document->getScriptContents()) {
48
+ rctx->current_script = scriptEl;
47
49
  try {
48
50
  _runtime->evaluate(script);
49
51
  } catch (...) { }
52
+ rctx->current_script = nullptr;
50
53
  }
51
54
  }
52
55
 
@@ -111,7 +114,7 @@ void HybridHtmlSandbox::setConsoleCallback(
111
114
 
112
115
  void HybridHtmlSandbox::setDialogCallbacks(
113
116
  const std::optional<std::variant<nitro::NullType, std::function<void(const std::string&)>>>& onAlert,
114
- const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<bool>>(const std::string&)>>>& onConfirm,
117
+ const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string&)>>>& onConfirm,
115
118
  const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::variant<nitro::NullType, std::string>>>(const std::string&, const std::optional<std::string>&)>>>& onPrompt) {
116
119
  if (!_runtime) return;
117
120
 
@@ -129,10 +132,10 @@ void HybridHtmlSandbox::setDialogCallbacks(
129
132
  if (!onConfirm.has_value() || std::holds_alternative<nitro::NullType>(onConfirm.value())) {
130
133
  _runtime->setConfirmCallback(nullptr);
131
134
  } else {
132
- auto fn = std::get<std::function<std::shared_ptr<Promise<bool>>(const std::string&)>>(onConfirm.value());
135
+ auto fn = std::get<std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string&)>>(onConfirm.value());
133
136
  _runtime->setConfirmCallback([fn](const std::string& message) -> bool {
134
137
  try {
135
- return fn(message)->await().get();
138
+ return fn(message)->await().get()->await().get();
136
139
  } catch (...) {
137
140
  return false;
138
141
  }
@@ -30,7 +30,7 @@ public:
30
30
 
31
31
  void setDialogCallbacks(
32
32
  const std::optional<std::variant<nitro::NullType, std::function<void(const std::string& /* message */)>>>& onAlert,
33
- const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<bool>>(const std::string& /* message */)>>>& onConfirm,
33
+ const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string& /* message */)>>>& onConfirm,
34
34
  const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::variant<nitro::NullType, std::string>>>(const std::string& /* message */, const std::optional<std::string>& /* defaultValue */)>>>& onPrompt
35
35
  ) override;
36
36
 
@@ -199,9 +199,9 @@ void* LexborDocument::documentElement() const {
199
199
 
200
200
  // ── Script extraction ────────────────────────────────────────────────────────
201
201
 
202
- std::vector<std::string> LexborDocument::getScriptContents() const {
202
+ std::vector<std::pair<void*, std::string>> LexborDocument::getScriptContents() const {
203
203
  auto scriptEls = querySelectorAll_el("script");
204
- std::vector<std::string> contents;
204
+ std::vector<std::pair<void*, std::string>> contents;
205
205
  contents.reserve(scriptEls.size());
206
206
 
207
207
  for (void* el : scriptEls) {
@@ -217,7 +217,7 @@ std::vector<std::string> LexborDocument::getScriptContents() const {
217
217
  size_t len = 0;
218
218
  lxb_char_t* text = lxb_dom_node_text_content(node, &len);
219
219
  if (text && len > 0) {
220
- contents.emplace_back(reinterpret_cast<char*>(text), len);
220
+ contents.emplace_back(el, std::string(reinterpret_cast<char*>(text), len));
221
221
  lxb_dom_document_destroy_text(node->owner_document, text);
222
222
  }
223
223
  }
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include <string>
4
+ #include <utility>
4
5
  #include <vector>
5
6
 
6
7
  namespace margelo::nitro::nitrojsdom {
@@ -29,8 +30,8 @@ public:
29
30
  void* head() const;
30
31
  void* documentElement() const;
31
32
 
32
- // ── Script extraction ────────────────────────────────────────────────────
33
- std::vector<std::string> getScriptContents() const;
33
+ // ── Script extraction — (element, text content) pairs; void* = lxb_dom_element_t* ──
34
+ std::vector<std::pair<void*, std::string>> getScriptContents() const;
34
35
 
35
36
  // ── Node creation ─────────────────────────────────────────────────────────
36
37
  void* createElement(const std::string& tag);
@@ -16,6 +16,7 @@
16
16
  #include "bindings/UrlBindings.hpp"
17
17
  #include "bindings/AbortBindings.hpp"
18
18
  #include "bindings/TextEncodingBindings.hpp"
19
+ #include "bindings/IntlBindings.hpp"
19
20
  #include "bindings/FormBindings.hpp"
20
21
  #include "bindings/BlobBindings.hpp"
21
22
  #include "bindings/CSSOMBindings.hpp"
@@ -28,6 +29,7 @@
28
29
  #include "bindings/LayoutStubBindings.hpp"
29
30
  #include "bindings/TreeWalkerBindings.hpp"
30
31
  #include "bindings/EventTargetBindings.hpp"
32
+ #include "bindings/WindowNamedPropertiesBindings.hpp"
31
33
  #include <lexbor/html/html.h>
32
34
  #include <lexbor/dom/dom.h>
33
35
 
@@ -58,6 +60,7 @@ void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
58
60
  UrlBindings::install(ctx);
59
61
  AbortBindings::install(ctx);
60
62
  TextEncodingBindings::install(ctx);
63
+ IntlBindings::install(ctx);
61
64
  BlobBindings::install(ctx); // uses TextEncoder/TextDecoder + btoa, so must run after both
62
65
  FetchBindings::install(ctx); // XHR bootstrap uses `new Event(...)`, so must run after EventBindings
63
66
  FormBindings::install(ctx); // uses globalThis.Element + globalThis.Event, so must run after both
@@ -88,6 +91,8 @@ void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
88
91
  rctx->mutation_observers->install(ctx, doc_node);
89
92
  }
90
93
  }
94
+
95
+ WindowNamedPropertiesBindings::install(ctx); // must run last, after localStorage/sessionStorage exist
91
96
  }
92
97
 
93
98
  } // namespace margelo::nitro::nitrojsdom
@@ -77,6 +77,7 @@ struct RuntimeContext {
77
77
  double time_origin_ms { 0 };
78
78
 
79
79
  void* active_element { nullptr };
80
+ void* current_script { nullptr };
80
81
  std::string ready_state { "loading" };
81
82
 
82
83
  // node pointer → heap-allocated JSValue* (DupValue'd strong ref)
@@ -201,6 +201,56 @@ const char* kCSSOMBootstrapScript = R"JS(
201
201
  globalThis.CSSStyleRule = CSSStyleRule;
202
202
  globalThis.CSSStyleSheet = CSSStyleSheet;
203
203
 
204
+ function cssEscape(value) {
205
+ var string = String(value);
206
+ var length = string.length;
207
+ var index = -1;
208
+ var result = '';
209
+ var firstCodeUnit = string.charCodeAt(0);
210
+
211
+ if (length === 1 && firstCodeUnit === 0x002D) {
212
+ return '\\' + string;
213
+ }
214
+
215
+ while (++index < length) {
216
+ var codeUnit = string.charCodeAt(index);
217
+ if (codeUnit === 0x0000) {
218
+ result += '\uFFFD';
219
+ continue;
220
+ }
221
+ if (
222
+ (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit === 0x007F ||
223
+ (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
224
+ (index === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && firstCodeUnit === 0x002D)
225
+ ) {
226
+ result += '\\' + codeUnit.toString(16) + ' ';
227
+ continue;
228
+ }
229
+ if (
230
+ codeUnit >= 0x0080 || codeUnit === 0x002D || codeUnit === 0x005F ||
231
+ (codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
232
+ (codeUnit >= 0x0041 && codeUnit <= 0x005A) ||
233
+ (codeUnit >= 0x0061 && codeUnit <= 0x007A)
234
+ ) {
235
+ result += string.charAt(index);
236
+ continue;
237
+ }
238
+ result += '\\' + string.charAt(index);
239
+ }
240
+ return result;
241
+ }
242
+
243
+ globalThis.CSS = {
244
+ escape: cssEscape,
245
+ supports: function(propertyOrCondition, value) {
246
+ if (arguments.length >= 2) {
247
+ return typeof propertyOrCondition === 'string' && propertyOrCondition.length > 0 &&
248
+ value !== undefined && String(value).length > 0;
249
+ }
250
+ return typeof propertyOrCondition === 'string' && propertyOrCondition.trim().length > 0;
251
+ },
252
+ };
253
+
204
254
  Object.defineProperty(Element.prototype, 'sheet', {
205
255
  get: function() {
206
256
  if (this.tagName !== 'STYLE') return null;
@@ -27,7 +27,7 @@ const char* kCustomElementsBootstrapScript = R"JS(
27
27
  var n = node;
28
28
  while (n) {
29
29
  if (n === docEl) return true;
30
- n = ('host' in n) ? n.host : n.parentNode;
30
+ n = (n instanceof ShadowRoot) ? n.host : n.parentNode;
31
31
  }
32
32
  return false;
33
33
  }
@@ -269,6 +269,12 @@ JSValue js_doc_get_activeElement(JSContext* ctx, JSValue) {
269
269
  return make_element(ctx, get_doc(ctx)->body());
270
270
  }
271
271
 
272
+ JSValue js_doc_get_currentScript(JSContext* ctx, JSValue) {
273
+ auto* rctx = get_ctx(ctx);
274
+ if (rctx && rctx->current_script) return make_element(ctx, rctx->current_script);
275
+ return JS_NULL;
276
+ }
277
+
272
278
  JSValue js_doc_get_readyState(JSContext* ctx, JSValue) {
273
279
  auto* rctx = get_ctx(ctx);
274
280
  const std::string& state = rctx ? rctx->ready_state : "loading";
@@ -350,6 +356,7 @@ void DocumentBindings::install(JSContext* ctx) {
350
356
  define_prop(ctx, doc, "scripts", js_doc_get_scripts, nullptr);
351
357
  define_prop(ctx, doc, "links", js_doc_get_links, nullptr);
352
358
  define_prop(ctx, doc, "activeElement", js_doc_get_activeElement, nullptr);
359
+ define_prop(ctx, doc, "currentScript", js_doc_get_currentScript, nullptr);
353
360
  define_prop(ctx, doc, "readyState", js_doc_get_readyState, nullptr);
354
361
  define_prop(ctx, doc, "compatMode", js_doc_get_compatMode, nullptr);
355
362
  define_prop(ctx, doc, "baseURI", js_doc_get_baseURI, nullptr);
@@ -360,6 +367,7 @@ void DocumentBindings::install(JSContext* ctx) {
360
367
  RuntimeContext* rctx = get_ctx(ctx);
361
368
  bool hidden = !(rctx && rctx->pretend_to_be_visual);
362
369
  JS_SetPropertyStr(ctx, doc, "hidden", JS_NewBool(ctx, hidden));
370
+ JS_SetPropertyStr(ctx, doc, "visibilityState", JS_NewString(ctx, hidden ? "hidden" : "visible"));
363
371
  JS_SetPropertyStr(ctx, doc, "nodeType", JS_NewInt32(ctx, 9 /* DOCUMENT_NODE */));
364
372
  JS_SetPropertyStr(ctx, doc, "nodeName", JS_NewString(ctx, "#document"));
365
373
  JS_SetPropertyStr(ctx, doc, "ownerDocument", JS_NULL);
@@ -303,6 +303,107 @@ JSValue js_el_set_checked(JSContext* ctx, JSValue this_val, JSValue val) {
303
303
  return JS_UNDEFINED;
304
304
  }
305
305
 
306
+ JSValue bool_attr_get(JSContext* ctx, JSValue this_val, const char* attr, size_t attr_len) {
307
+ auto* el = unwrap_element(ctx, this_val);
308
+ if (!el) return JS_FALSE;
309
+ bool has = lxb_dom_element_has_attribute(el, reinterpret_cast<const lxb_char_t*>(attr), attr_len);
310
+ return JS_NewBool(ctx, has);
311
+ }
312
+
313
+ JSValue bool_attr_set(JSContext* ctx, JSValue this_val, JSValue val, const char* attr, size_t attr_len) {
314
+ auto* el = unwrap_element(ctx, this_val);
315
+ if (!el) return JS_UNDEFINED;
316
+ bool on = JS_ToBool(ctx, val) > 0;
317
+ auto* attr_name = reinterpret_cast<const lxb_char_t*>(attr);
318
+ bool has = lxb_dom_element_has_attribute(el, attr_name, attr_len);
319
+
320
+ auto* rctx = get_ctx(ctx);
321
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
322
+ std::optional<std::string> old_val;
323
+ if (has_obs && has && rctx->mutation_observers->hasAttributeOldValueObserver()) {
324
+ size_t len = 0;
325
+ const lxb_char_t* v = lxb_dom_element_get_attribute(el, attr_name, attr_len, &len);
326
+ if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
327
+ }
328
+
329
+ if (on && !has) {
330
+ lxb_dom_element_set_attribute(el, attr_name, attr_len, reinterpret_cast<const lxb_char_t*>(""), 0);
331
+ if (has_obs) {
332
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), attr, std::nullopt);
333
+ }
334
+ } else if (!on && has) {
335
+ lxb_dom_element_remove_attribute(el, attr_name, attr_len);
336
+ if (has_obs) {
337
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), attr, old_val);
338
+ }
339
+ }
340
+
341
+ return JS_UNDEFINED;
342
+ }
343
+
344
+ JSValue js_el_get_disabled(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "disabled", 8); }
345
+ JSValue js_el_set_disabled(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "disabled", 8); }
346
+
347
+ JSValue js_el_get_required(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "required", 8); }
348
+ JSValue js_el_set_required(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "required", 8); }
349
+
350
+ JSValue js_el_get_readOnly(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "readonly", 8); }
351
+ JSValue js_el_set_readOnly(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "readonly", 8); }
352
+
353
+ JSValue js_el_get_multiple(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "multiple", 8); }
354
+ JSValue js_el_set_multiple(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "multiple", 8); }
355
+
356
+ JSValue js_el_get_autofocus(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "autofocus", 9); }
357
+ JSValue js_el_set_autofocus(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "autofocus", 9); }
358
+
359
+ JSValue js_el_get_selected(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "selected", 8); }
360
+ JSValue js_el_set_selected(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "selected", 8); }
361
+
362
+ JSValue js_el_get_hidden(JSContext* ctx, JSValue this_val) { return bool_attr_get(ctx, this_val, "hidden", 6); }
363
+ JSValue js_el_set_hidden(JSContext* ctx, JSValue this_val, JSValue val) { return bool_attr_set(ctx, this_val, val, "hidden", 6); }
364
+
365
+ JSValue string_attr_get(JSContext* ctx, JSValue this_val, const char* attr, size_t attr_len) {
366
+ auto* el = unwrap_element(ctx, this_val);
367
+ if (!el) return JS_NewString(ctx, "");
368
+ size_t len = 0;
369
+ const lxb_char_t* val = lxb_dom_element_get_attribute(el, reinterpret_cast<const lxb_char_t*>(attr), attr_len, &len);
370
+ return val ? JS_NewStringLen(ctx, reinterpret_cast<const char*>(val), len) : JS_NewString(ctx, "");
371
+ }
372
+
373
+ JSValue string_attr_set(JSContext* ctx, JSValue this_val, JSValue val, const char* attr, size_t attr_len) {
374
+ auto* el = unwrap_element(ctx, this_val);
375
+ if (!el) return JS_UNDEFINED;
376
+ const char* str = JS_ToCString(ctx, val);
377
+ if (!str) return JS_UNDEFINED;
378
+ auto* attr_name = reinterpret_cast<const lxb_char_t*>(attr);
379
+
380
+ auto* rctx = get_ctx(ctx);
381
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
382
+ std::optional<std::string> old_val;
383
+ if (has_obs && rctx->mutation_observers->hasAttributeOldValueObserver()) {
384
+ size_t len = 0;
385
+ const lxb_char_t* v = lxb_dom_element_get_attribute(el, attr_name, attr_len, &len);
386
+ if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
387
+ }
388
+
389
+ lxb_dom_element_set_attribute(el, attr_name, attr_len, reinterpret_cast<const lxb_char_t*>(str), strlen(str));
390
+ JS_FreeCString(ctx, str);
391
+
392
+ if (has_obs) {
393
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), attr, old_val);
394
+ }
395
+ return JS_UNDEFINED;
396
+ }
397
+
398
+ JSValue js_el_get_title(JSContext* ctx, JSValue this_val) { return string_attr_get(ctx, this_val, "title", 5); }
399
+ JSValue js_el_set_title(JSContext* ctx, JSValue this_val, JSValue val) { return string_attr_set(ctx, this_val, val, "title", 5); }
400
+
401
+ JSValue js_el_get_lang(JSContext* ctx, JSValue this_val) { return string_attr_get(ctx, this_val, "lang", 4); }
402
+ JSValue js_el_set_lang(JSContext* ctx, JSValue this_val, JSValue val) { return string_attr_set(ctx, this_val, val, "lang", 4); }
403
+
404
+ JSValue js_el_get_dir(JSContext* ctx, JSValue this_val) { return string_attr_get(ctx, this_val, "dir", 3); }
405
+ JSValue js_el_set_dir(JSContext* ctx, JSValue this_val, JSValue val) { return string_attr_set(ctx, this_val, val, "dir", 3); }
406
+
306
407
  JSValue js_el_get_textContent(JSContext* ctx, JSValue this_val) {
307
408
  lxb_dom_node_t* node = unwrap_node(ctx, this_val);
308
409
  if (!node) return JS_NewString(ctx, "");
@@ -1135,6 +1236,113 @@ JSValue js_el_insertAdjacentHTML(JSContext* ctx, JSValue this_val, int argc, JSV
1135
1236
  return JS_UNDEFINED;
1136
1237
  }
1137
1238
 
1239
+ JSValue js_el_insertAdjacentElement(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
1240
+ auto* el = unwrap_element(ctx, this_val);
1241
+ if (!el || argc < 2) return JS_NULL;
1242
+ const char* position = JS_ToCString(ctx, argv[0]);
1243
+ if (!position) return JS_NULL;
1244
+ std::string pos(position);
1245
+ JS_FreeCString(ctx, position);
1246
+
1247
+ auto* inserted_el = unwrap_element(ctx, argv[1]);
1248
+ if (!inserted_el) return JS_NULL;
1249
+ lxb_dom_node_t* inserted = lxb_dom_interface_node(inserted_el);
1250
+
1251
+ lxb_dom_node_t* node = lxb_dom_interface_node(el);
1252
+ lxb_dom_node_t* parent = nullptr;
1253
+ lxb_dom_node_t* prev_sib = nullptr;
1254
+ lxb_dom_node_t* next_sib = nullptr;
1255
+
1256
+ if (pos == "beforebegin") {
1257
+ parent = node->parent;
1258
+ if (!parent) return JS_NULL;
1259
+ prev_sib = node->prev;
1260
+ lxb_dom_node_insert_before(node, inserted);
1261
+ next_sib = node;
1262
+ } else if (pos == "afterbegin") {
1263
+ parent = node;
1264
+ lxb_dom_node_t* ref = node->first_child;
1265
+ if (ref) lxb_dom_node_insert_before(ref, inserted); else lxb_dom_node_insert_child(node, inserted);
1266
+ next_sib = ref;
1267
+ } else if (pos == "beforeend") {
1268
+ parent = node;
1269
+ prev_sib = node->last_child;
1270
+ lxb_dom_node_insert_child(node, inserted);
1271
+ } else if (pos == "afterend") {
1272
+ parent = node->parent;
1273
+ if (!parent) return JS_NULL;
1274
+ prev_sib = node;
1275
+ lxb_dom_node_t* ref = node->next;
1276
+ if (ref) lxb_dom_node_insert_before(ref, inserted); else lxb_dom_node_insert_child(parent, inserted);
1277
+ next_sib = ref;
1278
+ } else {
1279
+ return throw_dom_exception(ctx, "SyntaxError", ("invalid insertAdjacentElement position '" + pos + "'").c_str());
1280
+ }
1281
+
1282
+ auto* rctx = get_ctx(ctx);
1283
+ if (rctx && rctx->mutation_observers && !rctx->mutation_observers->empty()) {
1284
+ rctx->mutation_observers->notifyChildList(ctx, parent, {inserted}, {}, prev_sib, next_sib);
1285
+ }
1286
+ return JS_DupValue(ctx, argv[1]);
1287
+ }
1288
+
1289
+ JSValue js_el_insertAdjacentText(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
1290
+ auto* el = unwrap_element(ctx, this_val);
1291
+ if (!el || argc < 2) return JS_UNDEFINED;
1292
+ const char* position = JS_ToCString(ctx, argv[0]);
1293
+ const char* text = JS_ToCString(ctx, argv[1]);
1294
+ if (!position || !text) {
1295
+ if (position) JS_FreeCString(ctx, position);
1296
+ if (text) JS_FreeCString(ctx, text);
1297
+ return JS_UNDEFINED;
1298
+ }
1299
+ std::string pos(position);
1300
+ JS_FreeCString(ctx, position);
1301
+
1302
+ lxb_dom_node_t* node = lxb_dom_interface_node(el);
1303
+ void* text_node_v = doc_for_node(ctx, node)->createTextNode(text);
1304
+ JS_FreeCString(ctx, text);
1305
+ if (!text_node_v) return JS_UNDEFINED;
1306
+ auto* text_node = static_cast<lxb_dom_node_t*>(text_node_v);
1307
+
1308
+ lxb_dom_node_t* parent = nullptr;
1309
+ lxb_dom_node_t* prev_sib = nullptr;
1310
+ lxb_dom_node_t* next_sib = nullptr;
1311
+
1312
+ if (pos == "beforebegin") {
1313
+ parent = node->parent;
1314
+ if (!parent) { lxb_dom_node_destroy_deep(text_node); return JS_UNDEFINED; }
1315
+ prev_sib = node->prev;
1316
+ lxb_dom_node_insert_before(node, text_node);
1317
+ next_sib = node;
1318
+ } else if (pos == "afterbegin") {
1319
+ parent = node;
1320
+ lxb_dom_node_t* ref = node->first_child;
1321
+ if (ref) lxb_dom_node_insert_before(ref, text_node); else lxb_dom_node_insert_child(node, text_node);
1322
+ next_sib = ref;
1323
+ } else if (pos == "beforeend") {
1324
+ parent = node;
1325
+ prev_sib = node->last_child;
1326
+ lxb_dom_node_insert_child(node, text_node);
1327
+ } else if (pos == "afterend") {
1328
+ parent = node->parent;
1329
+ if (!parent) { lxb_dom_node_destroy_deep(text_node); return JS_UNDEFINED; }
1330
+ prev_sib = node;
1331
+ lxb_dom_node_t* ref = node->next;
1332
+ if (ref) lxb_dom_node_insert_before(ref, text_node); else lxb_dom_node_insert_child(parent, text_node);
1333
+ next_sib = ref;
1334
+ } else {
1335
+ lxb_dom_node_destroy_deep(text_node);
1336
+ return throw_dom_exception(ctx, "SyntaxError", ("invalid insertAdjacentText position '" + pos + "'").c_str());
1337
+ }
1338
+
1339
+ auto* rctx = get_ctx(ctx);
1340
+ if (rctx && rctx->mutation_observers && !rctx->mutation_observers->empty()) {
1341
+ rctx->mutation_observers->notifyChildList(ctx, parent, {text_node}, {}, prev_sib, next_sib);
1342
+ }
1343
+ return JS_UNDEFINED;
1344
+ }
1345
+
1138
1346
  JSValue js_el_matches(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
1139
1347
  auto* el = unwrap_element(ctx, this_val);
1140
1348
  if (!el || argc < 1) return JS_FALSE;
@@ -1502,6 +1710,8 @@ void ElementBindings::install(JSContext* ctx) {
1502
1710
  JS_SetPropertyStr(ctx, proto, "getElementsByTagName", JS_NewCFunction(ctx, js_el_getElementsByTagName, "getElementsByTagName", 1));
1503
1711
  JS_SetPropertyStr(ctx, proto, "closest", JS_NewCFunction(ctx, js_el_closest, "closest", 1));
1504
1712
  JS_SetPropertyStr(ctx, proto, "insertAdjacentHTML", JS_NewCFunction(ctx, js_el_insertAdjacentHTML, "insertAdjacentHTML", 2));
1713
+ JS_SetPropertyStr(ctx, proto, "insertAdjacentElement", JS_NewCFunction(ctx, js_el_insertAdjacentElement, "insertAdjacentElement", 2));
1714
+ JS_SetPropertyStr(ctx, proto, "insertAdjacentText", JS_NewCFunction(ctx, js_el_insertAdjacentText, "insertAdjacentText", 2));
1505
1715
  JS_SetPropertyStr(ctx, proto, "append", JS_NewCFunction(ctx, js_el_append, "append", 0));
1506
1716
  JS_SetPropertyStr(ctx, proto, "prepend", JS_NewCFunction(ctx, js_el_prepend, "prepend", 0));
1507
1717
  JS_SetPropertyStr(ctx, proto, "getBoundingClientRect", JS_NewCFunction(ctx, js_el_getBoundingClientRect, "getBoundingClientRect", 0));
@@ -1513,6 +1723,16 @@ void ElementBindings::install(JSContext* ctx) {
1513
1723
  define_prop(ctx, proto, "className", js_el_get_className, js_el_set_className);
1514
1724
  define_prop(ctx, proto, "value", js_el_get_value, js_el_set_value);
1515
1725
  define_prop(ctx, proto, "checked", js_el_get_checked, js_el_set_checked);
1726
+ define_prop(ctx, proto, "disabled", js_el_get_disabled, js_el_set_disabled);
1727
+ define_prop(ctx, proto, "required", js_el_get_required, js_el_set_required);
1728
+ define_prop(ctx, proto, "readOnly", js_el_get_readOnly, js_el_set_readOnly);
1729
+ define_prop(ctx, proto, "multiple", js_el_get_multiple, js_el_set_multiple);
1730
+ define_prop(ctx, proto, "autofocus", js_el_get_autofocus, js_el_set_autofocus);
1731
+ define_prop(ctx, proto, "selected", js_el_get_selected, js_el_set_selected);
1732
+ define_prop(ctx, proto, "hidden", js_el_get_hidden, js_el_set_hidden);
1733
+ define_prop(ctx, proto, "title", js_el_get_title, js_el_set_title);
1734
+ define_prop(ctx, proto, "lang", js_el_get_lang, js_el_set_lang);
1735
+ define_prop(ctx, proto, "dir", js_el_get_dir, js_el_set_dir);
1516
1736
  define_prop(ctx, proto, "innerHTML", js_el_get_innerHTML, js_el_set_innerHTML);
1517
1737
  define_prop(ctx, proto, "outerHTML", js_el_get_outerHTML, nullptr);
1518
1738
  define_prop(ctx, proto, "innerText", js_el_get_textContent, js_el_set_textContent);
@@ -1553,7 +1773,7 @@ void ElementBindings::install(JSContext* ctx) {
1553
1773
  while (true) {
1554
1774
  var parent = node.parentNode;
1555
1775
  if (parent) { node = parent; continue; }
1556
- if (composed && node.host) { node = node.host; continue; }
1776
+ if (composed && node instanceof ShadowRoot) { node = node.host; continue; }
1557
1777
  return __nativeCanonicalizeRootNode(node);
1558
1778
  }
1559
1779
  };
@@ -528,7 +528,7 @@ JSValue js_doc_dispatchEvent(JSContext* ctx, JSValue, int argc, JSValue* argv) {
528
528
  return dispatch_event_on_target(ctx, rctx, argv[0], node);
529
529
  }
530
530
 
531
- const char* kHandlerEventTypes[] = { "click", "load", "error", "unhandledrejection" };
531
+ const char* kHandlerEventTypes[] = { "click", "load", "error", "unhandledrejection", "change", "input", "submit", "reset" };
532
532
 
533
533
  JSValue get_handler_prop_for_node(JSContext* ctx, RuntimeContext* rctx, void* node, const std::string& event_type) {
534
534
  if (!rctx || !node) return JS_NULL;
@@ -684,9 +684,13 @@ void EventBindings::install(JSContext* ctx) {
684
684
  JS_SetPropertyStr(ctx, node_proto, "removeEventListener", JS_NewCFunction(ctx, js_el_removeEventListener, "removeEventListener", 2));
685
685
  JS_SetPropertyStr(ctx, node_proto, "dispatchEvent", JS_NewCFunction(ctx, js_el_dispatchEvent, "dispatchEvent", 1));
686
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);
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);
690
694
  JS_FreeValue(ctx, node_proto);
691
695
 
692
696
  JSValue element_proto = JS_GetClassProto(ctx, js_element_class_id);
@@ -125,6 +125,77 @@ const char* kFormBootstrapScript = R"JS(
125
125
  // Per spec, submit() bypasses the "submit" event and constraint validation.
126
126
  // There is no real navigation in this sandbox, so this is intentionally inert.
127
127
  };
128
+ Element.prototype.reset = function() {
129
+ if (!isFormElement(this)) return;
130
+ this.dispatchEvent(new Event('reset', { bubbles: true, cancelable: true }));
131
+ };
132
+
133
+ // ── select.options / .selectedIndex / .selectedOptions ────────────────────
134
+ function isSelectElement(el) {
135
+ return !!el && el.tagName === 'SELECT';
136
+ }
137
+
138
+ function selectOptionsArray(select) {
139
+ return Array.prototype.slice.call(select.querySelectorAll('option'));
140
+ }
141
+
142
+ Object.defineProperty(Element.prototype, 'options', {
143
+ configurable: true,
144
+ get: function() {
145
+ if (!isSelectElement(this)) return undefined;
146
+ var select = this;
147
+ var opts = selectOptionsArray(select);
148
+ opts.item = function(index) { return this[index] !== undefined ? this[index] : null; };
149
+ opts.namedItem = function(name) {
150
+ for (var i = 0; i < this.length; i++) {
151
+ if (this[i].id === name || this[i].getAttribute('name') === name) return this[i];
152
+ }
153
+ return null;
154
+ };
155
+ opts.add = function(option, before) {
156
+ var refNode = null;
157
+ if (typeof before === 'number') refNode = this[before] || null;
158
+ else if (before) refNode = before;
159
+ if (refNode) select.insertBefore(option, refNode);
160
+ else select.appendChild(option);
161
+ };
162
+ opts.remove = function(index) {
163
+ var target = this[index];
164
+ if (target) select.removeChild(target);
165
+ };
166
+ return opts;
167
+ },
168
+ });
169
+
170
+ Object.defineProperty(Element.prototype, 'selectedIndex', {
171
+ configurable: true,
172
+ get: function() {
173
+ if (!isSelectElement(this)) return -1;
174
+ var opts = selectOptionsArray(this);
175
+ for (var i = 0; i < opts.length; i++) {
176
+ if (opts[i].selected) return i;
177
+ }
178
+ return (!this.multiple && opts.length > 0) ? 0 : -1;
179
+ },
180
+ set: function(index) {
181
+ if (!isSelectElement(this)) return;
182
+ var opts = selectOptionsArray(this);
183
+ for (var i = 0; i < opts.length; i++) {
184
+ opts[i].selected = (i === index);
185
+ }
186
+ },
187
+ });
188
+
189
+ Object.defineProperty(Element.prototype, 'selectedOptions', {
190
+ configurable: true,
191
+ get: function() {
192
+ if (!isSelectElement(this)) return undefined;
193
+ var opts = selectOptionsArray(this);
194
+ var selected = opts.filter(function(o) { return o.selected; });
195
+ if (selected.length === 0 && !this.multiple && opts.length > 0) return [opts[0]];
196
+ return selected;
197
+ },
198
+ });
128
199
 
129
200
  // ── Constraint Validation API (ValidityState, checkValidity, ...) ─────────
130
201
  // Covers the subset real-world CMS forms actually hit: required, pattern,