@simplysm/core-browser 13.0.71 → 13.0.74

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 (2) hide show
  1. package/README.md +451 -15
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -2,29 +2,465 @@
2
2
 
3
3
  Simplysm package - Core module (browser)
4
4
 
5
+ Browser-only utilities including DOM element extensions, file download helpers, fetch utilities, and file dialog helpers.
6
+
5
7
  ## Installation
6
8
 
9
+ ```bash
7
10
  pnpm add @simplysm/core-browser
11
+ ```
12
+
13
+ Import the package to activate side-effect extensions on `Element` and `HTMLElement`:
14
+
15
+ ```ts
16
+ import "@simplysm/core-browser";
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Element Extensions
22
+
23
+ Extends the global `Element` interface with utility methods. These are activated as side effects when the package is imported.
24
+
25
+ ### `element.findAll<T>(selector)`
26
+
27
+ Finds all child elements matching a CSS selector.
28
+
29
+ - Returns an empty array if the selector is empty.
30
+
31
+ ```ts
32
+ import "@simplysm/core-browser";
33
+
34
+ const items = containerEl.findAll<HTMLLIElement>("li.active");
35
+ ```
36
+
37
+ **Signature:**
38
+
39
+ ```ts
40
+ findAll<T extends Element = Element>(selector: string): T[]
41
+ ```
42
+
43
+ ---
44
+
45
+ ### `element.findFirst<T>(selector)`
46
+
47
+ Finds the first child element matching a CSS selector.
48
+
49
+ - Returns `undefined` if the selector is empty or no match is found.
50
+
51
+ ```ts
52
+ import "@simplysm/core-browser";
53
+
54
+ const input = formEl.findFirst<HTMLInputElement>("input[name='email']");
55
+ ```
56
+
57
+ **Signature:**
58
+
59
+ ```ts
60
+ findFirst<T extends Element = Element>(selector: string): T | undefined
61
+ ```
62
+
63
+ ---
64
+
65
+ ### `element.prependChild<T>(child)`
66
+
67
+ Inserts a child element as the first child of the element.
68
+
69
+ ```ts
70
+ import "@simplysm/core-browser";
71
+
72
+ const newEl = document.createElement("div");
73
+ containerEl.prependChild(newEl);
74
+ ```
75
+
76
+ **Signature:**
77
+
78
+ ```ts
79
+ prependChild<T extends Element>(child: T): T
80
+ ```
81
+
82
+ ---
83
+
84
+ ### `element.getParents()`
85
+
86
+ Returns all ancestor elements in order from closest to farthest.
87
+
88
+ ```ts
89
+ import "@simplysm/core-browser";
90
+
91
+ const parents = someEl.getParents();
92
+ // parents[0] is the immediate parent, parents[n-1] is the farthest ancestor
93
+ ```
94
+
95
+ **Signature:**
96
+
97
+ ```ts
98
+ getParents(): Element[]
99
+ ```
100
+
101
+ ---
102
+
103
+ ### `element.findFocusableParent()`
104
+
105
+ Finds the first focusable ancestor element using the `tabbable` library's focusability check.
106
+
107
+ - Returns `undefined` if no focusable parent is found.
108
+
109
+ ```ts
110
+ import "@simplysm/core-browser";
111
+
112
+ const focusableParent = someEl.findFocusableParent();
113
+ focusableParent?.focus();
114
+ ```
115
+
116
+ **Signature:**
117
+
118
+ ```ts
119
+ findFocusableParent(): HTMLElement | undefined
120
+ ```
121
+
122
+ ---
123
+
124
+ ### `element.findFirstFocusableChild()`
125
+
126
+ Finds the first focusable descendant element using a `TreeWalker` and the `tabbable` library's focusability check.
127
+
128
+ - Returns `undefined` if no focusable child is found.
129
+
130
+ ```ts
131
+ import "@simplysm/core-browser";
132
+
133
+ const firstFocusable = dialogEl.findFirstFocusableChild();
134
+ firstFocusable?.focus();
135
+ ```
136
+
137
+ **Signature:**
138
+
139
+ ```ts
140
+ findFirstFocusableChild(): HTMLElement | undefined
141
+ ```
142
+
143
+ ---
144
+
145
+ ### `element.isOffsetElement()`
146
+
147
+ Checks whether the element has a CSS `position` of `relative`, `absolute`, `fixed`, or `sticky`.
148
+
149
+ ```ts
150
+ import "@simplysm/core-browser";
151
+
152
+ if (someEl.isOffsetElement()) {
153
+ // element is a positioning context
154
+ }
155
+ ```
156
+
157
+ **Signature:**
158
+
159
+ ```ts
160
+ isOffsetElement(): boolean
161
+ ```
162
+
163
+ ---
164
+
165
+ ### `element.isVisible()`
166
+
167
+ Checks whether the element is visible on screen.
168
+
169
+ Considers:
170
+ - Presence of client rects (`getClientRects().length > 0`)
171
+ - `visibility: hidden`
172
+ - `opacity: 0`
173
+
174
+ ```ts
175
+ import "@simplysm/core-browser";
176
+
177
+ if (someEl.isVisible()) {
178
+ // element is visible
179
+ }
180
+ ```
181
+
182
+ **Signature:**
183
+
184
+ ```ts
185
+ isVisible(): boolean
186
+ ```
187
+
188
+ ---
189
+
190
+ ## HTMLElement Extensions
191
+
192
+ Extends the global `HTMLElement` interface with utility methods. These are activated as side effects when the package is imported.
193
+
194
+ ### `htmlElement.repaint()`
195
+
196
+ Forces a browser repaint by triggering a synchronous reflow.
197
+
198
+ Accessing `offsetHeight` forces the browser to flush pending style changes immediately.
199
+
200
+ ```ts
201
+ import "@simplysm/core-browser";
202
+
203
+ el.classList.add("animate");
204
+ el.repaint(); // flush styles
205
+ el.classList.add("animate-active");
206
+ ```
207
+
208
+ **Signature:**
209
+
210
+ ```ts
211
+ repaint(): void
212
+ ```
213
+
214
+ ---
215
+
216
+ ### `htmlElement.getRelativeOffset(parent)`
217
+
218
+ Calculates the element's position relative to a parent element, returning coordinates suitable for use directly in CSS `top`/`left` properties.
219
+
220
+ Accounts for:
221
+ - Viewport-relative position (`getBoundingClientRect`)
222
+ - Document scroll position (`window.scrollX/Y`)
223
+ - Parent element internal scroll (`scrollTop/Left`)
224
+ - Border widths of intermediate elements
225
+ - CSS `transform` on element and parent
226
+
227
+ Typical use: positioning dropdowns or popups appended to `document.body`.
228
+
229
+ - `parent` — a reference `HTMLElement` or a CSS selector string (resolved via `closest`)
230
+ - Throws `ArgumentError` if the parent element cannot be found.
231
+
232
+ ```ts
233
+ import "@simplysm/core-browser";
234
+
235
+ const popup = document.createElement("div");
236
+ document.body.appendChild(popup);
237
+ const { top, left } = triggerEl.getRelativeOffset(document.body);
238
+ popup.style.top = `${top}px`;
239
+ popup.style.left = `${left}px`;
240
+ ```
241
+
242
+ **Signature:**
243
+
244
+ ```ts
245
+ getRelativeOffset(parent: HTMLElement | string): { top: number; left: number }
246
+ ```
247
+
248
+ ---
249
+
250
+ ### `htmlElement.scrollIntoViewIfNeeded(target, offset?)`
251
+
252
+ Scrolls the element so that a target position becomes visible, but only if the target is hidden behind an offset area (e.g., a fixed header or fixed column).
253
+
254
+ - Only handles cases where the target is obscured from the top or left. Downward/rightward scrolling relies on the browser's default focus behavior.
255
+ - Typically used with focus events on tables with fixed headers or columns.
256
+
257
+ ```ts
258
+ import "@simplysm/core-browser";
259
+
260
+ tableEl.scrollIntoViewIfNeeded(
261
+ { top: row.offsetTop, left: row.offsetLeft },
262
+ { top: 48, left: 120 }, // fixed header height and fixed column width
263
+ );
264
+ ```
265
+
266
+ **Signature:**
267
+
268
+ ```ts
269
+ scrollIntoViewIfNeeded(
270
+ target: { top: number; left: number },
271
+ offset?: { top: number; left: number },
272
+ ): void
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Static Element Utilities
278
+
279
+ Standalone functions exported from `element-ext`.
280
+
281
+ ### `copyElement(event)`
282
+
283
+ Copies the value of the first `input` or `textarea` inside the event target element to the clipboard.
284
+
285
+ Intended for use as a `copy` event handler.
286
+
287
+ ```ts
288
+ import { copyElement } from "@simplysm/core-browser";
289
+
290
+ someEl.addEventListener("copy", copyElement);
291
+ ```
292
+
293
+ **Signature:**
294
+
295
+ ```ts
296
+ function copyElement(event: ClipboardEvent): void
297
+ ```
298
+
299
+ ---
300
+
301
+ ### `pasteToElement(event)`
302
+
303
+ Pastes plain-text clipboard content into the first `input` or `textarea` inside the event target element, replacing its entire value.
304
+
305
+ Intended for use as a `paste` event handler. Does not consider cursor position or selection.
306
+
307
+ ```ts
308
+ import { pasteToElement } from "@simplysm/core-browser";
309
+
310
+ someEl.addEventListener("paste", pasteToElement);
311
+ ```
312
+
313
+ **Signature:**
314
+
315
+ ```ts
316
+ function pasteToElement(event: ClipboardEvent): void
317
+ ```
318
+
319
+ ---
320
+
321
+ ### `getBounds(els, timeout?)`
322
+
323
+ Retrieves bounding box information for multiple elements using `IntersectionObserver`. Results are returned in the same order as the input array.
324
+
325
+ - Deduplicates elements internally.
326
+ - Returns an empty array if `els` is empty.
327
+ - Throws `TimeoutError` if results are not received within `timeout` milliseconds (default: `5000`).
328
+
329
+ ```ts
330
+ import { getBounds } from "@simplysm/core-browser";
331
+
332
+ const bounds = await getBounds([el1, el2]);
333
+ for (const { target, top, left, width, height } of bounds) {
334
+ console.log(target, top, left, width, height);
335
+ }
336
+ ```
337
+
338
+ **Signature:**
339
+
340
+ ```ts
341
+ function getBounds(els: Element[], timeout?: number): Promise<ElementBounds[]>
342
+ ```
343
+
344
+ ---
345
+
346
+ ## Download Utilities
347
+
348
+ ### `downloadBlob(blob, fileName)`
349
+
350
+ Triggers a file download in the browser for a given `Blob` object.
351
+
352
+ Creates a temporary object URL, simulates an anchor click, and revokes the URL after 1 second.
353
+
354
+ ```ts
355
+ import { downloadBlob } from "@simplysm/core-browser";
356
+
357
+ const blob = new Blob(["hello"], { type: "text/plain" });
358
+ downloadBlob(blob, "hello.txt");
359
+ ```
360
+
361
+ **Signature:**
362
+
363
+ ```ts
364
+ function downloadBlob(blob: Blob, fileName: string): void
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Fetch Utilities
370
+
371
+ ### `fetchUrlBytes(url, options?)`
372
+
373
+ Downloads binary data from a URL and returns it as a `Uint8Array`, with optional progress reporting.
374
+
375
+ - When `Content-Length` is known, pre-allocates memory for efficiency.
376
+ - When `Content-Length` is unknown (chunked transfer encoding), collects chunks then merges.
377
+ - Throws an `Error` if the response is not OK or the body is not readable.
378
+
379
+ ```ts
380
+ import { fetchUrlBytes } from "@simplysm/core-browser";
381
+
382
+ const bytes = await fetchUrlBytes("https://example.com/file.bin", {
383
+ onProgress: ({ receivedLength, contentLength }) => {
384
+ console.log(`${receivedLength} / ${contentLength}`);
385
+ },
386
+ });
387
+ ```
388
+
389
+ **Signature:**
390
+
391
+ ```ts
392
+ function fetchUrlBytes(
393
+ url: string,
394
+ options?: { onProgress?: (progress: DownloadProgress) => void },
395
+ ): Promise<Uint8Array>
396
+ ```
397
+
398
+ ---
399
+
400
+ ## File Dialog Utilities
401
+
402
+ ### `openFileDialog(options?)`
403
+
404
+ Programmatically opens a browser file selection dialog and returns the selected files.
405
+
406
+ - Returns `undefined` if the user cancels or selects no files.
407
+ - `options.accept` — MIME type or file extension filter (e.g., `"image/*"`, `".pdf"`)
408
+ - `options.multiple` — allow selecting multiple files (default: `false`)
409
+
410
+ ```ts
411
+ import { openFileDialog } from "@simplysm/core-browser";
412
+
413
+ const files = await openFileDialog({ accept: ".csv", multiple: true });
414
+ if (files) {
415
+ for (const file of files) {
416
+ console.log(file.name);
417
+ }
418
+ }
419
+ ```
420
+
421
+ **Signature:**
422
+
423
+ ```ts
424
+ function openFileDialog(options?: {
425
+ accept?: string;
426
+ multiple?: boolean;
427
+ }): Promise<File[] | undefined>
428
+ ```
429
+
430
+ ---
8
431
 
9
- **Peer Dependencies:** none
432
+ ## Types
10
433
 
11
- ## Source Index
434
+ ### `ElementBounds`
12
435
 
13
- ### Extensions
436
+ Bounding box information for an element, returned by `getBounds`.
14
437
 
15
- | Source | Exports | Description | Test |
16
- |--------|---------|-------------|------|
17
- | `src/extensions/element-ext.ts` | `ElementBounds`, `copyElement`, `pasteToElement`, `getBounds` | DOM clipboard operations and element bounds utilities | `element-ext.spec.ts` |
18
- | `src/extensions/html-element-ext.ts` | _(HTMLElement prototype augmentation only)_ | Repaint, relative offset, and scroll-into-view helpers | `html-element-ext.spec.ts` |
438
+ ```ts
439
+ interface ElementBounds {
440
+ /** Element that was measured */
441
+ target: Element;
442
+ /** Top position relative to the viewport */
443
+ top: number;
444
+ /** Left position relative to the viewport */
445
+ left: number;
446
+ /** Element width */
447
+ width: number;
448
+ /** Element height */
449
+ height: number;
450
+ }
451
+ ```
19
452
 
20
- ### Utils
453
+ ---
21
454
 
22
- | Source | Exports | Description | Test |
23
- |--------|---------|-------------|------|
24
- | `src/utils/download.ts` | `downloadBlob` | Trigger browser file download from a Blob | `download.spec.ts` |
25
- | `src/utils/fetch.ts` | `DownloadProgress`, `fetchUrlBytes` | Fetch URL as Uint8Array with progress callback | - |
26
- | `src/utils/file-dialog.ts` | `openFileDialog` | Open native file selection dialog programmatically | - |
455
+ ### `DownloadProgress`
27
456
 
28
- ## License
457
+ Progress information passed to the `onProgress` callback of `fetchUrlBytes`.
29
458
 
30
- Apache-2.0
459
+ ```ts
460
+ interface DownloadProgress {
461
+ /** Number of bytes received so far */
462
+ receivedLength: number;
463
+ /** Total content length in bytes (0 if unknown) */
464
+ contentLength: number;
465
+ }
466
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/core-browser",
3
- "version": "13.0.71",
3
+ "version": "13.0.74",
4
4
  "description": "Simplysm package - Core module (browser)",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "tabbable": "^6.4.0",
26
- "@simplysm/core-common": "13.0.71"
26
+ "@simplysm/core-common": "13.0.74"
27
27
  },
28
28
  "devDependencies": {
29
29
  "happy-dom": "^20.7.0"