@bquery/bquery 1.7.0 → 1.8.2

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 (262) hide show
  1. package/README.md +760 -716
  2. package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
  3. package/dist/a11y-DVBCy09c.js.map +1 -0
  4. package/dist/a11y.es.mjs +1 -1
  5. package/dist/component/library.d.ts.map +1 -1
  6. package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
  7. package/dist/component-L3-JfOFz.js.map +1 -0
  8. package/dist/component.es.mjs +1 -1
  9. package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
  10. package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
  11. package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
  12. package/dist/constraints-D5RHQLmP.js.map +1 -0
  13. package/dist/core/collection.d.ts +86 -0
  14. package/dist/core/collection.d.ts.map +1 -1
  15. package/dist/core/element.d.ts +28 -0
  16. package/dist/core/element.d.ts.map +1 -1
  17. package/dist/core/shared.d.ts +6 -0
  18. package/dist/core/shared.d.ts.map +1 -1
  19. package/dist/core-DdtZHzsS.js +168 -0
  20. package/dist/core-DdtZHzsS.js.map +1 -0
  21. package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
  22. package/dist/core-EMYSLzaT.js.map +1 -0
  23. package/dist/core.es.mjs +48 -47
  24. package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
  25. package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
  26. package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
  27. package/dist/devtools-BhB2iDPT.js.map +1 -0
  28. package/dist/devtools.es.mjs +1 -1
  29. package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
  30. package/dist/dnd-NwZBYh4l.js.map +1 -0
  31. package/dist/dnd.es.mjs +1 -1
  32. package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
  33. package/dist/env-CTdvLaH2.js.map +1 -0
  34. package/dist/forms/create-form.d.ts.map +1 -1
  35. package/dist/forms/index.d.ts +3 -2
  36. package/dist/forms/index.d.ts.map +1 -1
  37. package/dist/forms/types.d.ts +46 -0
  38. package/dist/forms/types.d.ts.map +1 -1
  39. package/dist/forms/use-field.d.ts +34 -0
  40. package/dist/forms/use-field.d.ts.map +1 -0
  41. package/dist/forms/validators.d.ts +25 -0
  42. package/dist/forms/validators.d.ts.map +1 -1
  43. package/dist/forms-UcRHsYxC.js +227 -0
  44. package/dist/forms-UcRHsYxC.js.map +1 -0
  45. package/dist/forms.es.mjs +14 -12
  46. package/dist/full.d.ts +17 -26
  47. package/dist/full.d.ts.map +1 -1
  48. package/dist/full.es.mjs +206 -181
  49. package/dist/full.iife.js +33 -33
  50. package/dist/full.iife.js.map +1 -1
  51. package/dist/full.umd.js +33 -33
  52. package/dist/full.umd.js.map +1 -1
  53. package/dist/function-Cybd57JV.js +33 -0
  54. package/dist/function-Cybd57JV.js.map +1 -0
  55. package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
  56. package/dist/i18n-kuF6Ekj6.js.map +1 -0
  57. package/dist/i18n.es.mjs +1 -1
  58. package/dist/index.es.mjs +251 -228
  59. package/dist/media/breakpoints.d.ts.map +1 -1
  60. package/dist/media/types.d.ts +2 -2
  61. package/dist/media/types.d.ts.map +1 -1
  62. package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
  63. package/dist/media-i-fB5WxI.js.map +1 -0
  64. package/dist/media.es.mjs +1 -1
  65. package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
  66. package/dist/motion-BJsAuULb.js.map +1 -0
  67. package/dist/motion.es.mjs +1 -1
  68. package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
  69. package/dist/mount-B4Y8bk8Z.js.map +1 -0
  70. package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
  71. package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
  72. package/dist/platform.es.mjs +2 -2
  73. package/dist/plugin/registry.d.ts.map +1 -1
  74. package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
  75. package/dist/plugin-C2WuC8SF.js.map +1 -0
  76. package/dist/plugin.es.mjs +1 -1
  77. package/dist/reactive/async-data.d.ts +28 -3
  78. package/dist/reactive/async-data.d.ts.map +1 -1
  79. package/dist/reactive/computed.d.ts +3 -0
  80. package/dist/reactive/computed.d.ts.map +1 -1
  81. package/dist/reactive/effect.d.ts +3 -0
  82. package/dist/reactive/effect.d.ts.map +1 -1
  83. package/dist/reactive/http.d.ts +194 -0
  84. package/dist/reactive/http.d.ts.map +1 -0
  85. package/dist/reactive/index.d.ts +2 -2
  86. package/dist/reactive/index.d.ts.map +1 -1
  87. package/dist/reactive/pagination.d.ts +126 -0
  88. package/dist/reactive/pagination.d.ts.map +1 -0
  89. package/dist/reactive/polling.d.ts +55 -0
  90. package/dist/reactive/polling.d.ts.map +1 -0
  91. package/dist/reactive/readonly.d.ts +20 -1
  92. package/dist/reactive/readonly.d.ts.map +1 -1
  93. package/dist/reactive/rest.d.ts +293 -0
  94. package/dist/reactive/rest.d.ts.map +1 -0
  95. package/dist/reactive/scope.d.ts +140 -0
  96. package/dist/reactive/scope.d.ts.map +1 -0
  97. package/dist/reactive/signal.d.ts +16 -2
  98. package/dist/reactive/signal.d.ts.map +1 -1
  99. package/dist/reactive/to-value.d.ts +57 -0
  100. package/dist/reactive/to-value.d.ts.map +1 -0
  101. package/dist/reactive/websocket.d.ts +285 -0
  102. package/dist/reactive/websocket.d.ts.map +1 -0
  103. package/dist/reactive-DwkhUJfP.js +1148 -0
  104. package/dist/reactive-DwkhUJfP.js.map +1 -0
  105. package/dist/reactive.es.mjs +38 -19
  106. package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
  107. package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
  108. package/dist/router/constraints.d.ts.map +1 -1
  109. package/dist/router/index.d.ts +1 -1
  110. package/dist/router/index.d.ts.map +1 -1
  111. package/dist/router/router.d.ts.map +1 -1
  112. package/dist/router/state.d.ts +25 -2
  113. package/dist/router/state.d.ts.map +1 -1
  114. package/dist/router-CQikC9Ed.js +492 -0
  115. package/dist/router-CQikC9Ed.js.map +1 -0
  116. package/dist/router.es.mjs +9 -8
  117. package/dist/ssr/hydrate.d.ts.map +1 -1
  118. package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
  119. package/dist/ssr-_dAcGdzu.js.map +1 -0
  120. package/dist/ssr.es.mjs +1 -1
  121. package/dist/store/persisted.d.ts.map +1 -1
  122. package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
  123. package/dist/store-Cb3gPRve.js.map +1 -0
  124. package/dist/store.es.mjs +2 -2
  125. package/dist/storybook.es.mjs.map +1 -1
  126. package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
  127. package/dist/testing-C5Sjfsna.js.map +1 -0
  128. package/dist/testing.es.mjs +1 -1
  129. package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
  130. package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
  131. package/dist/untrack-D0fnO5k2.js +36 -0
  132. package/dist/untrack-D0fnO5k2.js.map +1 -0
  133. package/dist/view/custom-directives.d.ts.map +1 -1
  134. package/dist/view.es.mjs +4 -4
  135. package/package.json +178 -177
  136. package/src/a11y/announce.ts +131 -131
  137. package/src/a11y/audit.ts +314 -314
  138. package/src/a11y/index.ts +68 -68
  139. package/src/a11y/media-preferences.ts +255 -255
  140. package/src/a11y/roving-tab-index.ts +164 -164
  141. package/src/a11y/skip-link.ts +255 -255
  142. package/src/a11y/trap-focus.ts +184 -184
  143. package/src/a11y/types.ts +183 -183
  144. package/src/component/component.ts +599 -599
  145. package/src/component/html.ts +153 -153
  146. package/src/component/index.ts +52 -52
  147. package/src/component/library.ts +540 -542
  148. package/src/component/scope.ts +212 -212
  149. package/src/component/types.ts +310 -310
  150. package/src/core/collection.ts +876 -707
  151. package/src/core/element.ts +1015 -981
  152. package/src/core/env.ts +60 -60
  153. package/src/core/index.ts +49 -49
  154. package/src/core/shared.ts +77 -62
  155. package/src/core/utils/index.ts +148 -148
  156. package/src/devtools/devtools.ts +410 -410
  157. package/src/devtools/index.ts +48 -48
  158. package/src/devtools/types.ts +104 -104
  159. package/src/dnd/draggable.ts +296 -296
  160. package/src/dnd/droppable.ts +228 -228
  161. package/src/dnd/index.ts +62 -62
  162. package/src/dnd/sortable.ts +307 -307
  163. package/src/dnd/types.ts +293 -293
  164. package/src/forms/create-form.ts +320 -278
  165. package/src/forms/index.ts +70 -65
  166. package/src/forms/types.ts +203 -154
  167. package/src/forms/use-field.ts +231 -0
  168. package/src/forms/validators.ts +294 -265
  169. package/src/full.ts +554 -480
  170. package/src/i18n/formatting.ts +67 -67
  171. package/src/i18n/i18n.ts +200 -200
  172. package/src/i18n/index.ts +67 -67
  173. package/src/i18n/translate.ts +182 -182
  174. package/src/i18n/types.ts +171 -171
  175. package/src/index.ts +108 -108
  176. package/src/media/battery.ts +116 -116
  177. package/src/media/breakpoints.ts +129 -131
  178. package/src/media/clipboard.ts +80 -80
  179. package/src/media/device-sensors.ts +158 -158
  180. package/src/media/geolocation.ts +119 -119
  181. package/src/media/index.ts +76 -76
  182. package/src/media/media-query.ts +92 -92
  183. package/src/media/network.ts +115 -115
  184. package/src/media/types.ts +177 -177
  185. package/src/media/viewport.ts +84 -84
  186. package/src/motion/index.ts +57 -57
  187. package/src/motion/morph.ts +151 -151
  188. package/src/motion/parallax.ts +120 -120
  189. package/src/motion/reduced-motion.ts +66 -66
  190. package/src/motion/types.ts +271 -271
  191. package/src/motion/typewriter.ts +164 -164
  192. package/src/plugin/index.ts +37 -37
  193. package/src/plugin/registry.ts +284 -269
  194. package/src/plugin/types.ts +137 -137
  195. package/src/reactive/async-data.ts +250 -29
  196. package/src/reactive/computed.ts +144 -130
  197. package/src/reactive/effect.ts +29 -6
  198. package/src/reactive/http.ts +790 -0
  199. package/src/reactive/index.ts +60 -0
  200. package/src/reactive/pagination.ts +317 -0
  201. package/src/reactive/polling.ts +179 -0
  202. package/src/reactive/readonly.ts +52 -8
  203. package/src/reactive/rest.ts +859 -0
  204. package/src/reactive/scope.ts +276 -0
  205. package/src/reactive/signal.ts +61 -1
  206. package/src/reactive/to-value.ts +71 -0
  207. package/src/reactive/websocket.ts +849 -0
  208. package/src/router/bq-link.ts +279 -279
  209. package/src/router/constraints.ts +204 -201
  210. package/src/router/index.ts +49 -49
  211. package/src/router/match.ts +312 -312
  212. package/src/router/path-pattern.ts +52 -52
  213. package/src/router/query.ts +38 -38
  214. package/src/router/router.ts +421 -402
  215. package/src/router/state.ts +51 -3
  216. package/src/router/types.ts +139 -139
  217. package/src/router/use-route.ts +68 -68
  218. package/src/router/utils.ts +157 -157
  219. package/src/security/index.ts +12 -12
  220. package/src/ssr/hydrate.ts +84 -82
  221. package/src/ssr/index.ts +70 -70
  222. package/src/ssr/render.ts +508 -508
  223. package/src/ssr/serialize.ts +296 -296
  224. package/src/ssr/types.ts +81 -81
  225. package/src/store/create-store.ts +467 -467
  226. package/src/store/index.ts +27 -27
  227. package/src/store/persisted.ts +245 -249
  228. package/src/store/types.ts +247 -247
  229. package/src/store/utils.ts +135 -135
  230. package/src/storybook/index.ts +480 -480
  231. package/src/testing/index.ts +42 -42
  232. package/src/testing/testing.ts +593 -593
  233. package/src/testing/types.ts +170 -170
  234. package/src/view/custom-directives.ts +28 -30
  235. package/src/view/evaluate.ts +292 -292
  236. package/src/view/process.ts +108 -108
  237. package/dist/a11y-C5QOVvRn.js.map +0 -1
  238. package/dist/component-CuuTijA6.js.map +0 -1
  239. package/dist/constraints-3lV9yyBw.js.map +0 -1
  240. package/dist/core-Cjl7GUu8.js.map +0 -1
  241. package/dist/core-DnlyjbF2.js +0 -112
  242. package/dist/core-DnlyjbF2.js.map +0 -1
  243. package/dist/custom-directives-7wAShnnd.js.map +0 -1
  244. package/dist/devtools-D2fQLhDN.js.map +0 -1
  245. package/dist/dnd-B8EgyzaI.js.map +0 -1
  246. package/dist/env-NeVmr4Gf.js.map +0 -1
  247. package/dist/forms-C3yovgH9.js +0 -141
  248. package/dist/forms-C3yovgH9.js.map +0 -1
  249. package/dist/i18n-BnnhTFOS.js.map +0 -1
  250. package/dist/media-Di2Ta22s.js.map +0 -1
  251. package/dist/motion-qPj_TYGv.js.map +0 -1
  252. package/dist/mount-SM07RUa6.js.map +0 -1
  253. package/dist/plugin-cPoOHFLY.js.map +0 -1
  254. package/dist/reactive-Cfv0RK6x.js +0 -233
  255. package/dist/reactive-Cfv0RK6x.js.map +0 -1
  256. package/dist/router-BrthaP_z.js +0 -473
  257. package/dist/router-BrthaP_z.js.map +0 -1
  258. package/dist/ssr-B2qd_WBB.js.map +0 -1
  259. package/dist/store-DWpyH6p5.js.map +0 -1
  260. package/dist/testing-CsqjNUyy.js.map +0 -1
  261. package/dist/untrack-DJVQQ2WM.js +0 -33
  262. package/dist/untrack-DJVQQ2WM.js.map +0 -1
@@ -1,981 +1,1015 @@
1
- import { createElementFromHtml, insertContent, setHtml } from './dom';
2
- import { getOuterSize, isHTMLElement } from './shared';
3
- import { isPrototypePollutionKey } from './utils/object';
4
-
5
- /**
6
- * Wrapper for a single DOM element.
7
- * Provides a chainable, jQuery-like API for DOM manipulation.
8
- *
9
- * This class encapsulates a DOM element and provides methods for:
10
- * - Class manipulation (addClass, removeClass, toggleClass)
11
- * - Attribute and property access (attr, prop, data)
12
- * - Content manipulation (text, html, append, prepend)
13
- * - Style manipulation (css)
14
- * - Event handling (on, off, once, trigger)
15
- * - DOM traversal (find, closest, parent, children, siblings)
16
- *
17
- * All mutating methods return `this` for method chaining.
18
- *
19
- * @example
20
- * ```ts
21
- * $('#button')
22
- * .addClass('active')
23
- * .css({ color: 'blue' })
24
- * .on('click', () => console.log('clicked'));
25
- * ```
26
- */
27
- /** Handler signature for delegated events */
28
- type DelegatedHandler = (event: Event, target: Element) => void;
29
-
30
- type SerializableFormControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
31
-
32
- const isSerializableFormControl = (element: Element): element is SerializableFormControl => {
33
- const tagName = element.tagName.toLowerCase();
34
- return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
35
- };
36
-
37
- const collectFormEntries = (form: HTMLFormElement): Array<[string, string]> => {
38
- const entries: Array<[string, string]> = [];
39
- const elementCtor = form.ownerDocument.defaultView?.Element ?? Element;
40
-
41
- for (const control of Array.from(form.elements)) {
42
- if (!(control instanceof elementCtor) || !isSerializableFormControl(control)) {
43
- continue;
44
- }
45
-
46
- const name = control.name;
47
- if (!name || control.disabled || isPrototypePollutionKey(name)) {
48
- continue;
49
- }
50
-
51
- if (control.tagName.toLowerCase() === 'input') {
52
- const input = control as HTMLInputElement;
53
- const type = input.type.toLowerCase();
54
-
55
- if (type === 'checkbox' || type === 'radio') {
56
- if (input.checked) {
57
- entries.push([name, input.value]);
58
- }
59
- continue;
60
- }
61
-
62
- if (
63
- type === 'file' ||
64
- type === 'submit' ||
65
- type === 'button' ||
66
- type === 'reset' ||
67
- type === 'image'
68
- ) {
69
- continue;
70
- }
71
-
72
- entries.push([name, input.value]);
73
- continue;
74
- }
75
-
76
- if (control.tagName.toLowerCase() === 'select') {
77
- const select = control as HTMLSelectElement;
78
-
79
- if (select.multiple) {
80
- for (const option of Array.from(select.selectedOptions)) {
81
- entries.push([name, option.value]);
82
- }
83
- } else {
84
- entries.push([name, select.value]);
85
- }
86
- continue;
87
- }
88
-
89
- entries.push([name, (control as HTMLTextAreaElement).value]);
90
- }
91
-
92
- return entries;
93
- };
94
-
95
- const getFormEntries = (form: HTMLFormElement): Array<[string, string]> => {
96
- if (typeof FormData === 'function') {
97
- try {
98
- const entries: Array<[string, string]> = [];
99
-
100
- for (const [key, value] of new FormData(form).entries()) {
101
- if (isPrototypePollutionKey(key) || typeof value !== 'string') {
102
- continue;
103
- }
104
- entries.push([key, value]);
105
- }
106
-
107
- // Some environments expose FormData(form) but fail to populate entries for
108
- // successful controls. Fall back to manual collection only in that zero-entry case.
109
- return entries.length > 0 ? entries : collectFormEntries(form);
110
- } catch {
111
- // Fall back to manual collection when FormData is unavailable for this form
112
- // or the environment does not fully support constructing it.
113
- }
114
- }
115
-
116
- return collectFormEntries(form);
117
- };
118
-
119
- export class BQueryElement {
120
- /**
121
- * Stores delegated event handlers for cleanup via undelegate().
122
- * Key format: `${event}:${selector}`
123
- * @internal
124
- */
125
- private readonly delegatedHandlers = new Map<string, Map<DelegatedHandler, EventListener>>();
126
-
127
- /**
128
- * Creates a new BQueryElement wrapper.
129
- * @param element - The DOM element to wrap
130
- */
131
- constructor(private readonly element: Element) {}
132
-
133
- /**
134
- * Exposes the raw DOM element when direct access is needed.
135
- * Use sparingly; prefer the wrapper methods for consistency.
136
- */
137
- get raw(): Element {
138
- return this.element;
139
- }
140
-
141
- /**
142
- * Exposes the underlying DOM element.
143
- * Provided for spec compatibility and read-only access.
144
- */
145
- get node(): Element {
146
- return this.element;
147
- }
148
-
149
- /** Add one or more classes. */
150
- addClass(...classNames: string[]): this {
151
- this.element.classList.add(...classNames);
152
- return this;
153
- }
154
-
155
- /** Remove one or more classes. */
156
- removeClass(...classNames: string[]): this {
157
- this.element.classList.remove(...classNames);
158
- return this;
159
- }
160
-
161
- /** Toggle a class by name. */
162
- toggleClass(className: string, force?: boolean): this {
163
- this.element.classList.toggle(className, force);
164
- return this;
165
- }
166
-
167
- /** Get or set an attribute. */
168
- attr(name: string, value?: string): string | this {
169
- if (value === undefined) {
170
- return this.element.getAttribute(name) ?? '';
171
- }
172
- this.element.setAttribute(name, value);
173
- return this;
174
- }
175
-
176
- /** Remove an attribute. */
177
- removeAttr(name: string): this {
178
- this.element.removeAttribute(name);
179
- return this;
180
- }
181
-
182
- /** Toggle an attribute on/off. */
183
- toggleAttr(name: string, force?: boolean): this {
184
- const hasAttr = this.element.hasAttribute(name);
185
- const shouldAdd = force ?? !hasAttr;
186
- if (shouldAdd) {
187
- this.element.setAttribute(name, '');
188
- } else {
189
- this.element.removeAttribute(name);
190
- }
191
- return this;
192
- }
193
-
194
- /** Get or set a property. */
195
- prop<T extends keyof Element>(name: T, value?: Element[T]): Element[T] | this {
196
- if (value === undefined) {
197
- return this.element[name];
198
- }
199
- this.element[name] = value;
200
- return this;
201
- }
202
-
203
- /** Read or write data attributes in camelCase. */
204
- data(name: string, value?: string): string | this {
205
- const key = name.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
206
- if (value === undefined) {
207
- return this.element.getAttribute(`data-${key}`) ?? '';
208
- }
209
- this.element.setAttribute(`data-${key}`, value);
210
- return this;
211
- }
212
-
213
- /** Get or set text content. */
214
- text(value?: string): string | this {
215
- if (value === undefined) {
216
- return this.element.textContent ?? '';
217
- }
218
- this.element.textContent = value;
219
- return this;
220
- }
221
-
222
- /** Set HTML content using a sanitized string. */
223
- /**
224
- * Sets sanitized HTML content on the element.
225
- * Uses the security module to sanitize input and prevent XSS attacks.
226
- *
227
- * @param value - The HTML string to set (will be sanitized)
228
- * @returns The instance for method chaining
229
- *
230
- * @example
231
- * ```ts
232
- * $('#content').html('<strong>Hello</strong>');
233
- * ```
234
- */
235
- html(value: string): this {
236
- setHtml(this.element, value);
237
- return this;
238
- }
239
-
240
- /**
241
- * Sets HTML content without sanitization.
242
- * Use only when you trust the HTML source completely.
243
- *
244
- * @param value - The raw HTML string to set
245
- * @returns The instance for method chaining
246
- *
247
- * @warning This method bypasses XSS protection. Use with caution.
248
- */
249
- htmlUnsafe(value: string): this {
250
- this.element.innerHTML = value;
251
- return this;
252
- }
253
-
254
- /**
255
- * Gets or sets CSS styles on the element.
256
- *
257
- * @param property - A CSS property name or an object of property-value pairs
258
- * @param value - The value when setting a single property
259
- * @returns The computed style value when getting a single property, or the instance for method chaining when setting
260
- *
261
- * @example
262
- * ```ts
263
- * // Get a computed style value
264
- * const color = $('#box').css('color');
265
- *
266
- * // Set a single property
267
- * $('#box').css('color', 'red');
268
- *
269
- * // Set multiple properties
270
- * $('#box').css({ color: 'red', 'font-size': '16px' });
271
- * ```
272
- */
273
- css(property: string): string;
274
- css(property: string, value: string): this;
275
- css(property: Record<string, string>): this;
276
- css(property: string | Record<string, string>, value?: string): string | this {
277
- if (typeof property === 'string') {
278
- if (value !== undefined) {
279
- (this.element as HTMLElement).style.setProperty(property, value);
280
- return this;
281
- }
282
- const view = this.element.ownerDocument?.defaultView;
283
- if (!view || typeof view.getComputedStyle !== 'function') {
284
- return '';
285
- }
286
- return view.getComputedStyle(this.element).getPropertyValue(property);
287
- }
288
-
289
- for (const [key, val] of Object.entries(property)) {
290
- (this.element as HTMLElement).style.setProperty(key, val);
291
- }
292
- return this;
293
- }
294
-
295
- /**
296
- * Appends HTML or elements to the end of the element.
297
- *
298
- * @param content - HTML string or element(s) to append
299
- * @returns The instance for method chaining
300
- */
301
- append(content: string | Element | Element[]): this {
302
- this.insertContent(content, 'beforeend');
303
- return this;
304
- }
305
-
306
- /**
307
- * Prepends HTML or elements to the beginning of the element.
308
- *
309
- * @param content - HTML string or element(s) to prepend
310
- * @returns The instance for method chaining
311
- */
312
- prepend(content: string | Element | Element[]): this {
313
- this.insertContent(content, 'afterbegin');
314
- return this;
315
- }
316
-
317
- /**
318
- * Inserts content before this element.
319
- *
320
- * @param content - HTML string or element(s) to insert
321
- * @returns The instance for method chaining
322
- */
323
- before(content: string | Element | Element[]): this {
324
- this.insertContent(content, 'beforebegin');
325
- return this;
326
- }
327
-
328
- /**
329
- * Inserts content after this element.
330
- *
331
- * @param content - HTML string or element(s) to insert
332
- * @returns The instance for method chaining
333
- */
334
- after(content: string | Element | Element[]): this {
335
- this.insertContent(content, 'afterend');
336
- return this;
337
- }
338
-
339
- /**
340
- * Wraps the element with the specified wrapper element or tag.
341
- *
342
- * @param wrapper - Tag name string or Element to wrap with
343
- * @returns The instance for method chaining
344
- *
345
- * @example
346
- * ```ts
347
- * $('#content').wrap('div'); // Wraps with <div>
348
- * $('#content').wrap(document.createElement('section'));
349
- * ```
350
- */
351
- wrap(wrapper: string | Element): this {
352
- const wrapperEl = typeof wrapper === 'string' ? document.createElement(wrapper) : wrapper;
353
- this.element.parentNode?.insertBefore(wrapperEl, this.element);
354
- wrapperEl.appendChild(this.element);
355
- return this;
356
- }
357
-
358
- /**
359
- * Removes the parent element, keeping this element in its place.
360
- * Essentially the opposite of wrap().
361
- *
362
- * **Important**: This method only moves the current element out of its parent
363
- * before removing the parent. Any sibling elements will be removed along with
364
- * the parent. For unwrapping multiple siblings, use a collection: `$$(siblings).unwrap()`.
365
- *
366
- * @returns The instance for method chaining
367
- *
368
- * @example
369
- * ```ts
370
- * // Before: <div><span id="text">Hello</span></div>
371
- * $('#text').unwrap();
372
- * // After: <span id="text">Hello</span>
373
- * ```
374
- */
375
- unwrap(): this {
376
- const parent = this.element.parentElement;
377
- if (parent && parent.parentNode) {
378
- parent.parentNode.insertBefore(this.element, parent);
379
- parent.remove();
380
- }
381
- return this;
382
- }
383
-
384
- /**
385
- * Replaces this element with new content.
386
- *
387
- * @param content - HTML string (sanitized) or Element to replace with
388
- * @returns A new BQueryElement wrapping the replacement element
389
- *
390
- * @example
391
- * ```ts
392
- * const newEl = $('#old').replaceWith('<div id="new">Replaced</div>');
393
- * ```
394
- */
395
- replaceWith(content: string | Element): BQueryElement {
396
- const newEl = typeof content === 'string' ? createElementFromHtml(content) : content;
397
- this.element.replaceWith(newEl);
398
- return new BQueryElement(newEl);
399
- }
400
-
401
- /**
402
- * Removes the element from the DOM while keeping the wrapped node available
403
- * for later reuse.
404
- *
405
- * @returns The instance for method chaining
406
- *
407
- * @example
408
- * ```ts
409
- * const item = $('#item').detach();
410
- * document.body.appendChild(item.raw);
411
- * ```
412
- */
413
- detach(): this {
414
- return this.remove();
415
- }
416
-
417
- /**
418
- * Gets the zero-based index of the element among its element siblings.
419
- *
420
- * @returns Index within the parent element, or -1 when detached
421
- *
422
- * @example
423
- * ```ts
424
- * const index = $('#item').index();
425
- * ```
426
- */
427
- index(): number {
428
- const parent = this.element.parentElement;
429
- if (!parent) {
430
- return -1;
431
- }
432
- return Array.from(parent.children).indexOf(this.element);
433
- }
434
-
435
- /**
436
- * Returns all child nodes, including text nodes and comments.
437
- *
438
- * @returns Array of child nodes
439
- *
440
- * @example
441
- * ```ts
442
- * const nodes = $('#content').contents();
443
- * ```
444
- */
445
- contents(): ChildNode[] {
446
- return Array.from(this.element.childNodes);
447
- }
448
-
449
- /**
450
- * Gets the nearest positioned ancestor used for offset calculations.
451
- *
452
- * @returns The offset parent element, or null when unavailable
453
- *
454
- * @example
455
- * ```ts
456
- * const parent = $('#item').offsetParent();
457
- * ```
458
- */
459
- offsetParent(): Element | null {
460
- return isHTMLElement(this.element) ? this.element.offsetParent : null;
461
- }
462
-
463
- /**
464
- * Gets the current position relative to the offset parent.
465
- *
466
- * @returns Position object with top and left coordinates
467
- *
468
- * @example
469
- * ```ts
470
- * const { top, left } = $('#item').position();
471
- * ```
472
- */
473
- position(): { top: number; left: number } {
474
- if (!isHTMLElement(this.element)) {
475
- return { top: 0, left: 0 };
476
- }
477
-
478
- const el = this.element;
479
- return {
480
- top: el.offsetTop,
481
- left: el.offsetLeft,
482
- };
483
- }
484
-
485
- /**
486
- * Gets the outer width of the element, optionally including margins.
487
- *
488
- * @param includeMargin - When true, include horizontal margins
489
- * @returns Outer width in pixels
490
- *
491
- * @example
492
- * ```ts
493
- * const width = $('#panel').outerWidth();
494
- * const widthWithMargin = $('#panel').outerWidth(true);
495
- * ```
496
- */
497
- outerWidth(includeMargin: boolean = false): number {
498
- return getOuterSize(this.element, 'width', includeMargin);
499
- }
500
-
501
- /**
502
- * Gets the outer height of the element, optionally including margins.
503
- *
504
- * @param includeMargin - When true, include vertical margins
505
- * @returns Outer height in pixels
506
- *
507
- * @example
508
- * ```ts
509
- * const height = $('#panel').outerHeight();
510
- * const heightWithMargin = $('#panel').outerHeight(true);
511
- * ```
512
- */
513
- outerHeight(includeMargin: boolean = false): number {
514
- return getOuterSize(this.element, 'height', includeMargin);
515
- }
516
-
517
- /**
518
- * Scrolls the element into view with configurable behavior.
519
- *
520
- * @param options - ScrollIntoView options or boolean for legacy behavior
521
- * @returns The instance for method chaining
522
- *
523
- * @example
524
- * ```ts
525
- * $('#section').scrollTo(); // Smooth scroll
526
- * $('#section').scrollTo({ behavior: 'instant', block: 'start' });
527
- * ```
528
- */
529
- scrollTo(options: ScrollIntoViewOptions | boolean = { behavior: 'smooth' }): this {
530
- this.element.scrollIntoView(options);
531
- return this;
532
- }
533
-
534
- /**
535
- * Removes the element from the DOM.
536
- *
537
- * @returns The instance for method chaining (though element is now detached)
538
- */
539
- remove(): this {
540
- this.element.remove();
541
- return this;
542
- }
543
-
544
- /**
545
- * Clears all child nodes from the element.
546
- *
547
- * @returns The instance for method chaining
548
- */
549
- empty(): this {
550
- this.element.innerHTML = '';
551
- return this;
552
- }
553
-
554
- /**
555
- * Clones the element, optionally with all descendants.
556
- *
557
- * @param deep - If true, clone all descendants (default: true)
558
- * @returns A new BQueryElement wrapping the cloned element
559
- */
560
- clone(deep: boolean = true): BQueryElement {
561
- return new BQueryElement(this.element.cloneNode(deep) as Element);
562
- }
563
-
564
- /**
565
- * Finds all descendant elements matching the selector.
566
- *
567
- * @param selector - CSS selector to match
568
- * @returns Array of matching elements
569
- */
570
- find(selector: string): Element[] {
571
- return Array.from(this.element.querySelectorAll(selector));
572
- }
573
-
574
- /**
575
- * Finds the first descendant element matching the selector.
576
- *
577
- * @param selector - CSS selector to match
578
- * @returns The first matching element or null
579
- */
580
- findOne(selector: string): Element | null {
581
- return this.element.querySelector(selector);
582
- }
583
-
584
- /**
585
- * Finds the closest ancestor matching the selector.
586
- *
587
- * @param selector - CSS selector to match
588
- * @returns The matching ancestor or null
589
- */
590
- closest(selector: string): Element | null {
591
- return this.element.closest(selector);
592
- }
593
-
594
- /**
595
- * Gets the parent element.
596
- *
597
- * @returns The parent element or null
598
- */
599
- parent(): Element | null {
600
- return this.element.parentElement;
601
- }
602
-
603
- /**
604
- * Gets all child elements.
605
- *
606
- * @returns Array of child elements
607
- */
608
- children(): Element[] {
609
- return Array.from(this.element.children);
610
- }
611
-
612
- /**
613
- * Gets all sibling elements.
614
- *
615
- * @returns Array of sibling elements (excluding this element)
616
- */
617
- siblings(): Element[] {
618
- const parent = this.element.parentElement;
619
- if (!parent) return [];
620
- return Array.from(parent.children).filter((child) => child !== this.element);
621
- }
622
-
623
- /**
624
- * Gets the next sibling element.
625
- *
626
- * @returns The next sibling element or null
627
- */
628
- next(): Element | null {
629
- return this.element.nextElementSibling;
630
- }
631
-
632
- /**
633
- * Gets the previous sibling element.
634
- *
635
- * @returns The previous sibling element or null
636
- */
637
- prev(): Element | null {
638
- return this.element.previousElementSibling;
639
- }
640
-
641
- /**
642
- * Adds an event listener.
643
- *
644
- * @param event - Event type to listen for
645
- * @param handler - Event handler function
646
- * @returns The instance for method chaining
647
- */
648
- on(event: string, handler: EventListenerOrEventListenerObject): this {
649
- this.element.addEventListener(event, handler);
650
- return this;
651
- }
652
-
653
- /**
654
- * Adds a one-time event listener that removes itself after firing.
655
- *
656
- * @param event - Event type to listen for
657
- * @param handler - Event handler function
658
- * @returns The instance for method chaining
659
- */
660
- once(event: string, handler: EventListener): this {
661
- this.element.addEventListener(event, handler, { once: true });
662
- return this;
663
- }
664
-
665
- /**
666
- * Removes an event listener.
667
- *
668
- * @param event - Event type
669
- * @param handler - The handler to remove
670
- * @returns The instance for method chaining
671
- */
672
- off(event: string, handler: EventListenerOrEventListenerObject): this {
673
- this.element.removeEventListener(event, handler);
674
- return this;
675
- }
676
-
677
- /**
678
- * Triggers a custom event on the element.
679
- *
680
- * @param event - Event type to trigger
681
- * @param detail - Optional detail data to include with the event
682
- * @returns The instance for method chaining
683
- */
684
- trigger(event: string, detail?: unknown): this {
685
- this.element.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
686
- return this;
687
- }
688
-
689
- /**
690
- * Adds a delegated event listener that only triggers for matching descendants.
691
- * More efficient than adding listeners to many elements individually.
692
- *
693
- * Use `undelegate()` to remove the listener later.
694
- *
695
- * @param event - Event type to listen for
696
- * @param selector - CSS selector to match against event targets
697
- * @param handler - Event handler function, receives the matched element as context
698
- * @returns The instance for method chaining
699
- *
700
- * @example
701
- * ```ts
702
- * // Instead of adding listeners to each button:
703
- * const handler = (e, target) => console.log('Clicked:', target.textContent);
704
- * $('#list').delegate('click', '.item', handler);
705
- *
706
- * // Later, remove the delegated listener:
707
- * $('#list').undelegate('click', '.item', handler);
708
- * ```
709
- */
710
- delegate(
711
- event: string,
712
- selector: string,
713
- handler: (event: Event, target: Element) => void
714
- ): this {
715
- const key = `${event}:${selector}`;
716
- const wrapper: EventListener = (e: Event) => {
717
- const target = (e.target as Element).closest(selector);
718
- if (target && this.element.contains(target)) {
719
- handler(e, target);
720
- }
721
- };
722
-
723
- // Store the wrapper so it can be removed later
724
- if (!this.delegatedHandlers.has(key)) {
725
- this.delegatedHandlers.set(key, new Map());
726
- }
727
- this.delegatedHandlers.get(key)!.set(handler, wrapper);
728
-
729
- this.element.addEventListener(event, wrapper);
730
- return this;
731
- }
732
-
733
- /**
734
- * Removes a delegated event listener previously added with `delegate()`.
735
- *
736
- * @param event - Event type that was registered
737
- * @param selector - CSS selector that was used
738
- * @param handler - The original handler function passed to delegate()
739
- * @returns The instance for method chaining
740
- *
741
- * @example
742
- * ```ts
743
- * const handler = (e, target) => console.log('Clicked:', target.textContent);
744
- * $('#list').delegate('click', '.item', handler);
745
- *
746
- * // Remove the delegated listener:
747
- * $('#list').undelegate('click', '.item', handler);
748
- * ```
749
- */
750
- undelegate(
751
- event: string,
752
- selector: string,
753
- handler: (event: Event, target: Element) => void
754
- ): this {
755
- const key = `${event}:${selector}`;
756
- const handlers = this.delegatedHandlers.get(key);
757
-
758
- if (handlers) {
759
- const wrapper = handlers.get(handler);
760
- if (wrapper) {
761
- this.element.removeEventListener(event, wrapper);
762
- handlers.delete(handler);
763
-
764
- // Clean up empty maps
765
- if (handlers.size === 0) {
766
- this.delegatedHandlers.delete(key);
767
- }
768
- }
769
- }
770
-
771
- return this;
772
- }
773
-
774
- /**
775
- * Checks if the element matches a CSS selector.
776
- *
777
- * @param selector - CSS selector to match against
778
- * @returns True if the element matches the selector
779
- */
780
- matches(selector: string): boolean {
781
- return this.element.matches(selector);
782
- }
783
-
784
- /**
785
- * Alias for `matches()`. Checks if the element matches a CSS selector.
786
- *
787
- * @param selector - CSS selector to match against
788
- * @returns True if the element matches the selector
789
- *
790
- * @example
791
- * ```ts
792
- * if ($('#el').is('.active')) {
793
- * console.log('Element is active');
794
- * }
795
- * ```
796
- */
797
- is(selector: string): boolean {
798
- return this.matches(selector);
799
- }
800
-
801
- /**
802
- * Checks if the element has a specific class.
803
- *
804
- * @param className - Class name to check
805
- * @returns True if the element has the class
806
- */
807
- hasClass(className: string): boolean {
808
- return this.element.classList.contains(className);
809
- }
810
-
811
- /**
812
- * Shows the element by removing the hidden attribute and setting display.
813
- *
814
- * @param display - Optional display value (default: '')
815
- * @returns The instance for method chaining
816
- */
817
- show(display: string = ''): this {
818
- this.element.removeAttribute('hidden');
819
- (this.element as HTMLElement).style.display = display;
820
- return this;
821
- }
822
-
823
- /**
824
- * Hides the element by setting display to 'none'.
825
- *
826
- * @returns The instance for method chaining
827
- */
828
- hide(): this {
829
- (this.element as HTMLElement).style.display = 'none';
830
- return this;
831
- }
832
-
833
- /**
834
- * Toggles the visibility of the element.
835
- *
836
- * @param force - Optional force show (true) or hide (false)
837
- * @returns The instance for method chaining
838
- */
839
- toggle(force?: boolean): this {
840
- const isHidden = (this.element as HTMLElement).style.display === 'none';
841
- const shouldShow = force ?? isHidden;
842
- return shouldShow ? this.show() : this.hide();
843
- }
844
-
845
- /**
846
- * Focuses the element.
847
- *
848
- * @returns The instance for method chaining
849
- */
850
- focus(): this {
851
- (this.element as HTMLElement).focus();
852
- return this;
853
- }
854
-
855
- /**
856
- * Blurs (unfocuses) the element.
857
- *
858
- * @returns The instance for method chaining
859
- */
860
- blur(): this {
861
- (this.element as HTMLElement).blur();
862
- return this;
863
- }
864
-
865
- /**
866
- * Gets or sets the value of form elements.
867
- *
868
- * @param newValue - Optional value to set
869
- * @returns The current value when getting, or the instance when setting
870
- */
871
- val(newValue?: string): string | this {
872
- const input = this.element as HTMLInputElement;
873
- if (newValue === undefined) {
874
- return input.value ?? '';
875
- }
876
- input.value = newValue;
877
- return this;
878
- }
879
-
880
- /**
881
- * Serializes form data to a plain object.
882
- * Only works on form elements; returns empty object for non-forms.
883
- *
884
- * For security hardening, the returned object uses a null prototype,
885
- * so inherited members like `hasOwnProperty` are not available directly.
886
- * Prefer `Object.keys()` or `Object.prototype.hasOwnProperty.call(...)`
887
- * when checking for fields on the serialized result.
888
- *
889
- * @returns Object with form field names as keys and values
890
- *
891
- * @example
892
- * ```ts
893
- * // For a form with <input name="email" value="test@example.com">
894
- * const data = $('#myForm').serialize();
895
- * // { email: 'test@example.com' }
896
- * Object.prototype.hasOwnProperty.call(data, 'email'); // true
897
- * ```
898
- */
899
- serialize(): Record<string, string | string[]> {
900
- const form = this.element as HTMLFormElement;
901
- if (form.tagName.toLowerCase() !== 'form') {
902
- return {};
903
- }
904
-
905
- const result = Object.create(null) as Record<string, string | string[]>;
906
-
907
- for (const [key, value] of getFormEntries(form)) {
908
- if (Object.prototype.hasOwnProperty.call(result, key)) {
909
- // Handle multiple values (e.g., checkboxes)
910
- const existing = result[key];
911
- if (Array.isArray(existing)) {
912
- existing.push(value);
913
- } else {
914
- result[key] = [existing, value];
915
- }
916
- } else {
917
- result[key] = value;
918
- }
919
- }
920
-
921
- return result;
922
- }
923
-
924
- /**
925
- * Serializes form data to a URL-encoded query string.
926
- *
927
- * @returns URL-encoded string suitable for form submission
928
- *
929
- * @example
930
- * ```ts
931
- * const queryString = $('#myForm').serializeString();
932
- * // 'email=test%40example.com&name=John'
933
- * ```
934
- */
935
- serializeString(): string {
936
- const form = this.element as HTMLFormElement;
937
- if (form.tagName.toLowerCase() !== 'form') {
938
- return '';
939
- }
940
-
941
- const params = new URLSearchParams();
942
-
943
- for (const [key, value] of getFormEntries(form)) {
944
- params.append(key, value);
945
- }
946
-
947
- return params.toString();
948
- }
949
-
950
- /**
951
- * Gets the bounding client rectangle of the element.
952
- *
953
- * @returns The element's bounding rectangle
954
- */
955
- rect(): DOMRect {
956
- return this.element.getBoundingClientRect();
957
- }
958
-
959
- /**
960
- * Gets the offset dimensions (width, height, top, left).
961
- *
962
- * @returns Object with offset dimensions
963
- */
964
- offset(): { width: number; height: number; top: number; left: number } {
965
- const el = this.element as HTMLElement;
966
- return {
967
- width: el.offsetWidth,
968
- height: el.offsetHeight,
969
- top: el.offsetTop,
970
- left: el.offsetLeft,
971
- };
972
- }
973
-
974
- /**
975
- * Internal method to insert content at a specified position.
976
- * @internal
977
- */
978
- private insertContent(content: string | Element | Element[], position: InsertPosition) {
979
- insertContent(this.element, content, position);
980
- }
981
- }
1
+ import { createElementFromHtml, insertContent, setHtml } from './dom';
2
+ import { getInnerSize, getOuterSize, isHTMLElement } from './shared';
3
+ import { isPrototypePollutionKey } from './utils/object';
4
+
5
+ /**
6
+ * Wrapper for a single DOM element.
7
+ * Provides a chainable, jQuery-like API for DOM manipulation.
8
+ *
9
+ * This class encapsulates a DOM element and provides methods for:
10
+ * - Class manipulation (addClass, removeClass, toggleClass)
11
+ * - Attribute and property access (attr, prop, data)
12
+ * - Content manipulation (text, html, append, prepend)
13
+ * - Style manipulation (css)
14
+ * - Event handling (on, off, once, trigger)
15
+ * - DOM traversal (find, closest, parent, children, siblings)
16
+ *
17
+ * All mutating methods return `this` for method chaining.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * $('#button')
22
+ * .addClass('active')
23
+ * .css({ color: 'blue' })
24
+ * .on('click', () => console.log('clicked'));
25
+ * ```
26
+ */
27
+ /** Handler signature for delegated events */
28
+ type DelegatedHandler = (event: Event, target: Element) => void;
29
+
30
+ type SerializableFormControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
31
+
32
+ const isSerializableFormControl = (element: Element): element is SerializableFormControl => {
33
+ const tagName = element.tagName.toLowerCase();
34
+ return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
35
+ };
36
+
37
+ const collectFormEntries = (form: HTMLFormElement): Array<[string, string]> => {
38
+ const entries: Array<[string, string]> = [];
39
+ const elementCtor = form.ownerDocument.defaultView?.Element ?? Element;
40
+
41
+ for (const control of Array.from(form.elements)) {
42
+ if (!(control instanceof elementCtor) || !isSerializableFormControl(control)) {
43
+ continue;
44
+ }
45
+
46
+ const name = control.name;
47
+ if (!name || control.disabled || isPrototypePollutionKey(name)) {
48
+ continue;
49
+ }
50
+
51
+ if (control.tagName.toLowerCase() === 'input') {
52
+ const input = control as HTMLInputElement;
53
+ const type = input.type.toLowerCase();
54
+
55
+ if (type === 'checkbox' || type === 'radio') {
56
+ if (input.checked) {
57
+ entries.push([name, input.value]);
58
+ }
59
+ continue;
60
+ }
61
+
62
+ if (
63
+ type === 'file' ||
64
+ type === 'submit' ||
65
+ type === 'button' ||
66
+ type === 'reset' ||
67
+ type === 'image'
68
+ ) {
69
+ continue;
70
+ }
71
+
72
+ entries.push([name, input.value]);
73
+ continue;
74
+ }
75
+
76
+ if (control.tagName.toLowerCase() === 'select') {
77
+ const select = control as HTMLSelectElement;
78
+
79
+ if (select.multiple) {
80
+ for (const option of Array.from(select.selectedOptions)) {
81
+ entries.push([name, option.value]);
82
+ }
83
+ } else {
84
+ entries.push([name, select.value]);
85
+ }
86
+ continue;
87
+ }
88
+
89
+ entries.push([name, (control as HTMLTextAreaElement).value]);
90
+ }
91
+
92
+ return entries;
93
+ };
94
+
95
+ const getFormEntries = (form: HTMLFormElement): Array<[string, string]> => {
96
+ if (typeof FormData === 'function') {
97
+ try {
98
+ const entries: Array<[string, string]> = [];
99
+
100
+ for (const [key, value] of new FormData(form).entries()) {
101
+ if (isPrototypePollutionKey(key) || typeof value !== 'string') {
102
+ continue;
103
+ }
104
+ entries.push([key, value]);
105
+ }
106
+
107
+ // Some environments expose FormData(form) but fail to populate entries for
108
+ // successful controls. Fall back to manual collection only in that zero-entry case.
109
+ return entries.length > 0 ? entries : collectFormEntries(form);
110
+ } catch {
111
+ // Fall back to manual collection when FormData is unavailable for this form
112
+ // or the environment does not fully support constructing it.
113
+ }
114
+ }
115
+
116
+ return collectFormEntries(form);
117
+ };
118
+
119
+ export class BQueryElement {
120
+ /**
121
+ * Stores delegated event handlers for cleanup via undelegate().
122
+ * Key format: `${event}:${selector}`
123
+ * @internal
124
+ */
125
+ private readonly delegatedHandlers = new Map<string, Map<DelegatedHandler, EventListener>>();
126
+
127
+ /**
128
+ * Creates a new BQueryElement wrapper.
129
+ * @param element - The DOM element to wrap
130
+ */
131
+ constructor(private readonly element: Element) {}
132
+
133
+ /**
134
+ * Exposes the raw DOM element when direct access is needed.
135
+ * Use sparingly; prefer the wrapper methods for consistency.
136
+ */
137
+ get raw(): Element {
138
+ return this.element;
139
+ }
140
+
141
+ /**
142
+ * Exposes the underlying DOM element.
143
+ * Provided for spec compatibility and read-only access.
144
+ */
145
+ get node(): Element {
146
+ return this.element;
147
+ }
148
+
149
+ /** Add one or more classes. */
150
+ addClass(...classNames: string[]): this {
151
+ this.element.classList.add(...classNames);
152
+ return this;
153
+ }
154
+
155
+ /** Remove one or more classes. */
156
+ removeClass(...classNames: string[]): this {
157
+ this.element.classList.remove(...classNames);
158
+ return this;
159
+ }
160
+
161
+ /** Toggle a class by name. */
162
+ toggleClass(className: string, force?: boolean): this {
163
+ this.element.classList.toggle(className, force);
164
+ return this;
165
+ }
166
+
167
+ /** Get or set an attribute. */
168
+ attr(name: string, value?: string): string | this {
169
+ if (value === undefined) {
170
+ return this.element.getAttribute(name) ?? '';
171
+ }
172
+ this.element.setAttribute(name, value);
173
+ return this;
174
+ }
175
+
176
+ /** Remove an attribute. */
177
+ removeAttr(name: string): this {
178
+ this.element.removeAttribute(name);
179
+ return this;
180
+ }
181
+
182
+ /** Toggle an attribute on/off. */
183
+ toggleAttr(name: string, force?: boolean): this {
184
+ const hasAttr = this.element.hasAttribute(name);
185
+ const shouldAdd = force ?? !hasAttr;
186
+ if (shouldAdd) {
187
+ this.element.setAttribute(name, '');
188
+ } else {
189
+ this.element.removeAttribute(name);
190
+ }
191
+ return this;
192
+ }
193
+
194
+ /** Get or set a property. */
195
+ prop<T extends keyof Element>(name: T, value?: Element[T]): Element[T] | this {
196
+ if (value === undefined) {
197
+ return this.element[name];
198
+ }
199
+ this.element[name] = value;
200
+ return this;
201
+ }
202
+
203
+ /** Read or write data attributes in camelCase. */
204
+ data(name: string, value?: string): string | this {
205
+ const key = name.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
206
+ if (value === undefined) {
207
+ return this.element.getAttribute(`data-${key}`) ?? '';
208
+ }
209
+ this.element.setAttribute(`data-${key}`, value);
210
+ return this;
211
+ }
212
+
213
+ /** Get or set text content. */
214
+ text(value?: string): string | this {
215
+ if (value === undefined) {
216
+ return this.element.textContent ?? '';
217
+ }
218
+ this.element.textContent = value;
219
+ return this;
220
+ }
221
+
222
+ /** Set HTML content using a sanitized string. */
223
+ /**
224
+ * Sets sanitized HTML content on the element.
225
+ * Uses the security module to sanitize input and prevent XSS attacks.
226
+ *
227
+ * @param value - The HTML string to set (will be sanitized)
228
+ * @returns The instance for method chaining
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * $('#content').html('<strong>Hello</strong>');
233
+ * ```
234
+ */
235
+ html(value: string): this {
236
+ setHtml(this.element, value);
237
+ return this;
238
+ }
239
+
240
+ /**
241
+ * Sets HTML content without sanitization.
242
+ * Use only when you trust the HTML source completely.
243
+ *
244
+ * @param value - The raw HTML string to set
245
+ * @returns The instance for method chaining
246
+ *
247
+ * @warning This method bypasses XSS protection. Use with caution.
248
+ */
249
+ htmlUnsafe(value: string): this {
250
+ this.element.innerHTML = value;
251
+ return this;
252
+ }
253
+
254
+ /**
255
+ * Gets or sets CSS styles on the element.
256
+ *
257
+ * @param property - A CSS property name or an object of property-value pairs
258
+ * @param value - The value when setting a single property
259
+ * @returns The computed style value when getting a single property, or the instance for method chaining when setting
260
+ *
261
+ * @example
262
+ * ```ts
263
+ * // Get a computed style value
264
+ * const color = $('#box').css('color');
265
+ *
266
+ * // Set a single property
267
+ * $('#box').css('color', 'red');
268
+ *
269
+ * // Set multiple properties
270
+ * $('#box').css({ color: 'red', 'font-size': '16px' });
271
+ * ```
272
+ */
273
+ css(property: string): string;
274
+ css(property: string, value: string): this;
275
+ css(property: Record<string, string>): this;
276
+ css(property: string | Record<string, string>, value?: string): string | this {
277
+ if (typeof property === 'string') {
278
+ if (value !== undefined) {
279
+ (this.element as HTMLElement).style.setProperty(property, value);
280
+ return this;
281
+ }
282
+ const view = this.element.ownerDocument?.defaultView;
283
+ if (!view || typeof view.getComputedStyle !== 'function') {
284
+ return '';
285
+ }
286
+ return view.getComputedStyle(this.element).getPropertyValue(property);
287
+ }
288
+
289
+ for (const [key, val] of Object.entries(property)) {
290
+ (this.element as HTMLElement).style.setProperty(key, val);
291
+ }
292
+ return this;
293
+ }
294
+
295
+ /**
296
+ * Appends HTML or elements to the end of the element.
297
+ *
298
+ * @param content - HTML string or element(s) to append
299
+ * @returns The instance for method chaining
300
+ */
301
+ append(content: string | Element | Element[]): this {
302
+ this.insertContent(content, 'beforeend');
303
+ return this;
304
+ }
305
+
306
+ /**
307
+ * Prepends HTML or elements to the beginning of the element.
308
+ *
309
+ * @param content - HTML string or element(s) to prepend
310
+ * @returns The instance for method chaining
311
+ */
312
+ prepend(content: string | Element | Element[]): this {
313
+ this.insertContent(content, 'afterbegin');
314
+ return this;
315
+ }
316
+
317
+ /**
318
+ * Inserts content before this element.
319
+ *
320
+ * @param content - HTML string or element(s) to insert
321
+ * @returns The instance for method chaining
322
+ */
323
+ before(content: string | Element | Element[]): this {
324
+ this.insertContent(content, 'beforebegin');
325
+ return this;
326
+ }
327
+
328
+ /**
329
+ * Inserts content after this element.
330
+ *
331
+ * @param content - HTML string or element(s) to insert
332
+ * @returns The instance for method chaining
333
+ */
334
+ after(content: string | Element | Element[]): this {
335
+ this.insertContent(content, 'afterend');
336
+ return this;
337
+ }
338
+
339
+ /**
340
+ * Wraps the element with the specified wrapper element or tag.
341
+ *
342
+ * @param wrapper - Tag name string or Element to wrap with
343
+ * @returns The instance for method chaining
344
+ *
345
+ * @example
346
+ * ```ts
347
+ * $('#content').wrap('div'); // Wraps with <div>
348
+ * $('#content').wrap(document.createElement('section'));
349
+ * ```
350
+ */
351
+ wrap(wrapper: string | Element): this {
352
+ const wrapperEl = typeof wrapper === 'string' ? document.createElement(wrapper) : wrapper;
353
+ this.element.parentNode?.insertBefore(wrapperEl, this.element);
354
+ wrapperEl.appendChild(this.element);
355
+ return this;
356
+ }
357
+
358
+ /**
359
+ * Removes the parent element, keeping this element in its place.
360
+ * Essentially the opposite of wrap().
361
+ *
362
+ * **Important**: This method only moves the current element out of its parent
363
+ * before removing the parent. Any sibling elements will be removed along with
364
+ * the parent. For unwrapping multiple siblings, use a collection: `$$(siblings).unwrap()`.
365
+ *
366
+ * @returns The instance for method chaining
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * // Before: <div><span id="text">Hello</span></div>
371
+ * $('#text').unwrap();
372
+ * // After: <span id="text">Hello</span>
373
+ * ```
374
+ */
375
+ unwrap(): this {
376
+ const parent = this.element.parentElement;
377
+ if (parent && parent.parentNode) {
378
+ parent.parentNode.insertBefore(this.element, parent);
379
+ parent.remove();
380
+ }
381
+ return this;
382
+ }
383
+
384
+ /**
385
+ * Replaces this element with new content.
386
+ *
387
+ * @param content - HTML string (sanitized) or Element to replace with
388
+ * @returns A new BQueryElement wrapping the replacement element
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * const newEl = $('#old').replaceWith('<div id="new">Replaced</div>');
393
+ * ```
394
+ */
395
+ replaceWith(content: string | Element): BQueryElement {
396
+ const newEl = typeof content === 'string' ? createElementFromHtml(content) : content;
397
+ this.element.replaceWith(newEl);
398
+ return new BQueryElement(newEl);
399
+ }
400
+
401
+ /**
402
+ * Removes the element from the DOM while keeping the wrapped node available
403
+ * for later reuse.
404
+ *
405
+ * @returns The instance for method chaining
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * const item = $('#item').detach();
410
+ * document.body.appendChild(item.raw);
411
+ * ```
412
+ */
413
+ detach(): this {
414
+ return this.remove();
415
+ }
416
+
417
+ /**
418
+ * Gets the zero-based index of the element among its element siblings.
419
+ *
420
+ * @returns Index within the parent element, or -1 when detached
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * const index = $('#item').index();
425
+ * ```
426
+ */
427
+ index(): number {
428
+ const parent = this.element.parentElement;
429
+ if (!parent) {
430
+ return -1;
431
+ }
432
+ return Array.from(parent.children).indexOf(this.element);
433
+ }
434
+
435
+ /**
436
+ * Returns all child nodes, including text nodes and comments.
437
+ *
438
+ * @returns Array of child nodes
439
+ *
440
+ * @example
441
+ * ```ts
442
+ * const nodes = $('#content').contents();
443
+ * ```
444
+ */
445
+ contents(): ChildNode[] {
446
+ return Array.from(this.element.childNodes);
447
+ }
448
+
449
+ /**
450
+ * Gets the nearest positioned ancestor used for offset calculations.
451
+ *
452
+ * @returns The offset parent element, or null when unavailable
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * const parent = $('#item').offsetParent();
457
+ * ```
458
+ */
459
+ offsetParent(): Element | null {
460
+ return isHTMLElement(this.element) ? this.element.offsetParent : null;
461
+ }
462
+
463
+ /**
464
+ * Gets the current position relative to the offset parent.
465
+ *
466
+ * @returns Position object with top and left coordinates
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * const { top, left } = $('#item').position();
471
+ * ```
472
+ */
473
+ position(): { top: number; left: number } {
474
+ if (!isHTMLElement(this.element)) {
475
+ return { top: 0, left: 0 };
476
+ }
477
+
478
+ const el = this.element;
479
+ return {
480
+ top: el.offsetTop,
481
+ left: el.offsetLeft,
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Gets the inner width of the element (content + padding, excluding border).
487
+ *
488
+ * This corresponds to the element's `clientWidth` and mirrors jQuery's
489
+ * `innerWidth()` method.
490
+ *
491
+ * @returns Inner width in pixels, or 0 for non-HTML elements
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * const innerW = $('#panel').innerWidth();
496
+ * ```
497
+ */
498
+ innerWidth(): number {
499
+ return getInnerSize(this.element, 'width');
500
+ }
501
+
502
+ /**
503
+ * Gets the inner height of the element (content + padding, excluding border).
504
+ *
505
+ * This corresponds to the element's `clientHeight` and mirrors jQuery's
506
+ * `innerHeight()` method.
507
+ *
508
+ * @returns Inner height in pixels, or 0 for non-HTML elements
509
+ *
510
+ * @example
511
+ * ```ts
512
+ * const innerH = $('#panel').innerHeight();
513
+ * ```
514
+ */
515
+ innerHeight(): number {
516
+ return getInnerSize(this.element, 'height');
517
+ }
518
+
519
+ /**
520
+ * Gets the outer width of the element, optionally including margins.
521
+ *
522
+ * @param includeMargin - When true, include horizontal margins
523
+ * @returns Outer width in pixels
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * const width = $('#panel').outerWidth();
528
+ * const widthWithMargin = $('#panel').outerWidth(true);
529
+ * ```
530
+ */
531
+ outerWidth(includeMargin: boolean = false): number {
532
+ return getOuterSize(this.element, 'width', includeMargin);
533
+ }
534
+
535
+ /**
536
+ * Gets the outer height of the element, optionally including margins.
537
+ *
538
+ * @param includeMargin - When true, include vertical margins
539
+ * @returns Outer height in pixels
540
+ *
541
+ * @example
542
+ * ```ts
543
+ * const height = $('#panel').outerHeight();
544
+ * const heightWithMargin = $('#panel').outerHeight(true);
545
+ * ```
546
+ */
547
+ outerHeight(includeMargin: boolean = false): number {
548
+ return getOuterSize(this.element, 'height', includeMargin);
549
+ }
550
+
551
+ /**
552
+ * Scrolls the element into view with configurable behavior.
553
+ *
554
+ * @param options - ScrollIntoView options or boolean for legacy behavior
555
+ * @returns The instance for method chaining
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * $('#section').scrollTo(); // Smooth scroll
560
+ * $('#section').scrollTo({ behavior: 'instant', block: 'start' });
561
+ * ```
562
+ */
563
+ scrollTo(options: ScrollIntoViewOptions | boolean = { behavior: 'smooth' }): this {
564
+ this.element.scrollIntoView(options);
565
+ return this;
566
+ }
567
+
568
+ /**
569
+ * Removes the element from the DOM.
570
+ *
571
+ * @returns The instance for method chaining (though element is now detached)
572
+ */
573
+ remove(): this {
574
+ this.element.remove();
575
+ return this;
576
+ }
577
+
578
+ /**
579
+ * Clears all child nodes from the element.
580
+ *
581
+ * @returns The instance for method chaining
582
+ */
583
+ empty(): this {
584
+ this.element.innerHTML = '';
585
+ return this;
586
+ }
587
+
588
+ /**
589
+ * Clones the element, optionally with all descendants.
590
+ *
591
+ * @param deep - If true, clone all descendants (default: true)
592
+ * @returns A new BQueryElement wrapping the cloned element
593
+ */
594
+ clone(deep: boolean = true): BQueryElement {
595
+ return new BQueryElement(this.element.cloneNode(deep) as Element);
596
+ }
597
+
598
+ /**
599
+ * Finds all descendant elements matching the selector.
600
+ *
601
+ * @param selector - CSS selector to match
602
+ * @returns Array of matching elements
603
+ */
604
+ find(selector: string): Element[] {
605
+ return Array.from(this.element.querySelectorAll(selector));
606
+ }
607
+
608
+ /**
609
+ * Finds the first descendant element matching the selector.
610
+ *
611
+ * @param selector - CSS selector to match
612
+ * @returns The first matching element or null
613
+ */
614
+ findOne(selector: string): Element | null {
615
+ return this.element.querySelector(selector);
616
+ }
617
+
618
+ /**
619
+ * Finds the closest ancestor matching the selector.
620
+ *
621
+ * @param selector - CSS selector to match
622
+ * @returns The matching ancestor or null
623
+ */
624
+ closest(selector: string): Element | null {
625
+ return this.element.closest(selector);
626
+ }
627
+
628
+ /**
629
+ * Gets the parent element.
630
+ *
631
+ * @returns The parent element or null
632
+ */
633
+ parent(): Element | null {
634
+ return this.element.parentElement;
635
+ }
636
+
637
+ /**
638
+ * Gets all child elements.
639
+ *
640
+ * @returns Array of child elements
641
+ */
642
+ children(): Element[] {
643
+ return Array.from(this.element.children);
644
+ }
645
+
646
+ /**
647
+ * Gets all sibling elements.
648
+ *
649
+ * @returns Array of sibling elements (excluding this element)
650
+ */
651
+ siblings(): Element[] {
652
+ const parent = this.element.parentElement;
653
+ if (!parent) return [];
654
+ return Array.from(parent.children).filter((child) => child !== this.element);
655
+ }
656
+
657
+ /**
658
+ * Gets the next sibling element.
659
+ *
660
+ * @returns The next sibling element or null
661
+ */
662
+ next(): Element | null {
663
+ return this.element.nextElementSibling;
664
+ }
665
+
666
+ /**
667
+ * Gets the previous sibling element.
668
+ *
669
+ * @returns The previous sibling element or null
670
+ */
671
+ prev(): Element | null {
672
+ return this.element.previousElementSibling;
673
+ }
674
+
675
+ /**
676
+ * Adds an event listener.
677
+ *
678
+ * @param event - Event type to listen for
679
+ * @param handler - Event handler function
680
+ * @returns The instance for method chaining
681
+ */
682
+ on(event: string, handler: EventListenerOrEventListenerObject): this {
683
+ this.element.addEventListener(event, handler);
684
+ return this;
685
+ }
686
+
687
+ /**
688
+ * Adds a one-time event listener that removes itself after firing.
689
+ *
690
+ * @param event - Event type to listen for
691
+ * @param handler - Event handler function
692
+ * @returns The instance for method chaining
693
+ */
694
+ once(event: string, handler: EventListener): this {
695
+ this.element.addEventListener(event, handler, { once: true });
696
+ return this;
697
+ }
698
+
699
+ /**
700
+ * Removes an event listener.
701
+ *
702
+ * @param event - Event type
703
+ * @param handler - The handler to remove
704
+ * @returns The instance for method chaining
705
+ */
706
+ off(event: string, handler: EventListenerOrEventListenerObject): this {
707
+ this.element.removeEventListener(event, handler);
708
+ return this;
709
+ }
710
+
711
+ /**
712
+ * Triggers a custom event on the element.
713
+ *
714
+ * @param event - Event type to trigger
715
+ * @param detail - Optional detail data to include with the event
716
+ * @returns The instance for method chaining
717
+ */
718
+ trigger(event: string, detail?: unknown): this {
719
+ this.element.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
720
+ return this;
721
+ }
722
+
723
+ /**
724
+ * Adds a delegated event listener that only triggers for matching descendants.
725
+ * More efficient than adding listeners to many elements individually.
726
+ *
727
+ * Use `undelegate()` to remove the listener later.
728
+ *
729
+ * @param event - Event type to listen for
730
+ * @param selector - CSS selector to match against event targets
731
+ * @param handler - Event handler function, receives the matched element as context
732
+ * @returns The instance for method chaining
733
+ *
734
+ * @example
735
+ * ```ts
736
+ * // Instead of adding listeners to each button:
737
+ * const handler = (e, target) => console.log('Clicked:', target.textContent);
738
+ * $('#list').delegate('click', '.item', handler);
739
+ *
740
+ * // Later, remove the delegated listener:
741
+ * $('#list').undelegate('click', '.item', handler);
742
+ * ```
743
+ */
744
+ delegate(
745
+ event: string,
746
+ selector: string,
747
+ handler: (event: Event, target: Element) => void
748
+ ): this {
749
+ const key = `${event}:${selector}`;
750
+ const wrapper: EventListener = (e: Event) => {
751
+ const target = (e.target as Element).closest(selector);
752
+ if (target && this.element.contains(target)) {
753
+ handler(e, target);
754
+ }
755
+ };
756
+
757
+ // Store the wrapper so it can be removed later
758
+ if (!this.delegatedHandlers.has(key)) {
759
+ this.delegatedHandlers.set(key, new Map());
760
+ }
761
+ this.delegatedHandlers.get(key)!.set(handler, wrapper);
762
+
763
+ this.element.addEventListener(event, wrapper);
764
+ return this;
765
+ }
766
+
767
+ /**
768
+ * Removes a delegated event listener previously added with `delegate()`.
769
+ *
770
+ * @param event - Event type that was registered
771
+ * @param selector - CSS selector that was used
772
+ * @param handler - The original handler function passed to delegate()
773
+ * @returns The instance for method chaining
774
+ *
775
+ * @example
776
+ * ```ts
777
+ * const handler = (e, target) => console.log('Clicked:', target.textContent);
778
+ * $('#list').delegate('click', '.item', handler);
779
+ *
780
+ * // Remove the delegated listener:
781
+ * $('#list').undelegate('click', '.item', handler);
782
+ * ```
783
+ */
784
+ undelegate(
785
+ event: string,
786
+ selector: string,
787
+ handler: (event: Event, target: Element) => void
788
+ ): this {
789
+ const key = `${event}:${selector}`;
790
+ const handlers = this.delegatedHandlers.get(key);
791
+
792
+ if (handlers) {
793
+ const wrapper = handlers.get(handler);
794
+ if (wrapper) {
795
+ this.element.removeEventListener(event, wrapper);
796
+ handlers.delete(handler);
797
+
798
+ // Clean up empty maps
799
+ if (handlers.size === 0) {
800
+ this.delegatedHandlers.delete(key);
801
+ }
802
+ }
803
+ }
804
+
805
+ return this;
806
+ }
807
+
808
+ /**
809
+ * Checks if the element matches a CSS selector.
810
+ *
811
+ * @param selector - CSS selector to match against
812
+ * @returns True if the element matches the selector
813
+ */
814
+ matches(selector: string): boolean {
815
+ return this.element.matches(selector);
816
+ }
817
+
818
+ /**
819
+ * Alias for `matches()`. Checks if the element matches a CSS selector.
820
+ *
821
+ * @param selector - CSS selector to match against
822
+ * @returns True if the element matches the selector
823
+ *
824
+ * @example
825
+ * ```ts
826
+ * if ($('#el').is('.active')) {
827
+ * console.log('Element is active');
828
+ * }
829
+ * ```
830
+ */
831
+ is(selector: string): boolean {
832
+ return this.matches(selector);
833
+ }
834
+
835
+ /**
836
+ * Checks if the element has a specific class.
837
+ *
838
+ * @param className - Class name to check
839
+ * @returns True if the element has the class
840
+ */
841
+ hasClass(className: string): boolean {
842
+ return this.element.classList.contains(className);
843
+ }
844
+
845
+ /**
846
+ * Shows the element by removing the hidden attribute and setting display.
847
+ *
848
+ * @param display - Optional display value (default: '')
849
+ * @returns The instance for method chaining
850
+ */
851
+ show(display: string = ''): this {
852
+ this.element.removeAttribute('hidden');
853
+ (this.element as HTMLElement).style.display = display;
854
+ return this;
855
+ }
856
+
857
+ /**
858
+ * Hides the element by setting display to 'none'.
859
+ *
860
+ * @returns The instance for method chaining
861
+ */
862
+ hide(): this {
863
+ (this.element as HTMLElement).style.display = 'none';
864
+ return this;
865
+ }
866
+
867
+ /**
868
+ * Toggles the visibility of the element.
869
+ *
870
+ * @param force - Optional force show (true) or hide (false)
871
+ * @returns The instance for method chaining
872
+ */
873
+ toggle(force?: boolean): this {
874
+ const isHidden = (this.element as HTMLElement).style.display === 'none';
875
+ const shouldShow = force ?? isHidden;
876
+ return shouldShow ? this.show() : this.hide();
877
+ }
878
+
879
+ /**
880
+ * Focuses the element.
881
+ *
882
+ * @returns The instance for method chaining
883
+ */
884
+ focus(): this {
885
+ (this.element as HTMLElement).focus();
886
+ return this;
887
+ }
888
+
889
+ /**
890
+ * Blurs (unfocuses) the element.
891
+ *
892
+ * @returns The instance for method chaining
893
+ */
894
+ blur(): this {
895
+ (this.element as HTMLElement).blur();
896
+ return this;
897
+ }
898
+
899
+ /**
900
+ * Gets or sets the value of form elements.
901
+ *
902
+ * @param newValue - Optional value to set
903
+ * @returns The current value when getting, or the instance when setting
904
+ */
905
+ val(newValue?: string): string | this {
906
+ const input = this.element as HTMLInputElement;
907
+ if (newValue === undefined) {
908
+ return input.value ?? '';
909
+ }
910
+ input.value = newValue;
911
+ return this;
912
+ }
913
+
914
+ /**
915
+ * Serializes form data to a plain object.
916
+ * Only works on form elements; returns empty object for non-forms.
917
+ *
918
+ * For security hardening, the returned object uses a null prototype,
919
+ * so inherited members like `hasOwnProperty` are not available directly.
920
+ * Prefer `Object.keys()` or `Object.prototype.hasOwnProperty.call(...)`
921
+ * when checking for fields on the serialized result.
922
+ *
923
+ * @returns Object with form field names as keys and values
924
+ *
925
+ * @example
926
+ * ```ts
927
+ * // For a form with <input name="email" value="test@example.com">
928
+ * const data = $('#myForm').serialize();
929
+ * // { email: 'test@example.com' }
930
+ * Object.prototype.hasOwnProperty.call(data, 'email'); // true
931
+ * ```
932
+ */
933
+ serialize(): Record<string, string | string[]> {
934
+ const form = this.element as HTMLFormElement;
935
+ if (form.tagName.toLowerCase() !== 'form') {
936
+ return {};
937
+ }
938
+
939
+ const result = Object.create(null) as Record<string, string | string[]>;
940
+
941
+ for (const [key, value] of getFormEntries(form)) {
942
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
943
+ // Handle multiple values (e.g., checkboxes)
944
+ const existing = result[key];
945
+ if (Array.isArray(existing)) {
946
+ existing.push(value);
947
+ } else {
948
+ result[key] = [existing, value];
949
+ }
950
+ } else {
951
+ result[key] = value;
952
+ }
953
+ }
954
+
955
+ return result;
956
+ }
957
+
958
+ /**
959
+ * Serializes form data to a URL-encoded query string.
960
+ *
961
+ * @returns URL-encoded string suitable for form submission
962
+ *
963
+ * @example
964
+ * ```ts
965
+ * const queryString = $('#myForm').serializeString();
966
+ * // 'email=test%40example.com&name=John'
967
+ * ```
968
+ */
969
+ serializeString(): string {
970
+ const form = this.element as HTMLFormElement;
971
+ if (form.tagName.toLowerCase() !== 'form') {
972
+ return '';
973
+ }
974
+
975
+ const params = new URLSearchParams();
976
+
977
+ for (const [key, value] of getFormEntries(form)) {
978
+ params.append(key, value);
979
+ }
980
+
981
+ return params.toString();
982
+ }
983
+
984
+ /**
985
+ * Gets the bounding client rectangle of the element.
986
+ *
987
+ * @returns The element's bounding rectangle
988
+ */
989
+ rect(): DOMRect {
990
+ return this.element.getBoundingClientRect();
991
+ }
992
+
993
+ /**
994
+ * Gets the offset dimensions (width, height, top, left).
995
+ *
996
+ * @returns Object with offset dimensions
997
+ */
998
+ offset(): { width: number; height: number; top: number; left: number } {
999
+ const el = this.element as HTMLElement;
1000
+ return {
1001
+ width: el.offsetWidth,
1002
+ height: el.offsetHeight,
1003
+ top: el.offsetTop,
1004
+ left: el.offsetLeft,
1005
+ };
1006
+ }
1007
+
1008
+ /**
1009
+ * Internal method to insert content at a specified position.
1010
+ * @internal
1011
+ */
1012
+ private insertContent(content: string | Element | Element[], position: InsertPosition) {
1013
+ insertContent(this.element, content, position);
1014
+ }
1015
+ }