@simplysm/core-browser 13.0.82 → 13.0.83

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.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @simplysm/core-browser
2
+
3
+ > Simplysm package - Core module (browser)
4
+
5
+ Browser-specific utilities for DOM manipulation, file handling, HTTP fetching, and IndexedDB storage. This package extends native `Element` and `HTMLElement` prototypes with convenience methods and provides standalone utility functions for common browser tasks.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @simplysm/core-browser
11
+ ```
12
+
13
+ **Peer dependency:** `@simplysm/core-common`
14
+
15
+ ## Side Effects
16
+
17
+ This package includes side-effect imports that augment global prototypes. The following modules execute on import:
18
+
19
+ - `extensions/element-ext` -- extends `Element.prototype`
20
+ - `extensions/html-element-ext` -- extends `HTMLElement.prototype`
21
+
22
+ ## Documentation
23
+
24
+ | Category | Description | File |
25
+ |----------|-------------|------|
26
+ | Element Extensions | `Element` prototype methods and clipboard/bounds helpers | [docs/element-extensions.md](docs/element-extensions.md) |
27
+ | HTMLElement Extensions | `HTMLElement` prototype methods for layout and scrolling | [docs/html-element-extensions.md](docs/html-element-extensions.md) |
28
+ | Utilities | File download, fetch with progress, file dialog | [docs/utilities.md](docs/utilities.md) |
29
+ | IndexedDB | IndexedDB store wrapper and virtual filesystem | [docs/indexed-db.md](docs/indexed-db.md) |
@@ -0,0 +1,151 @@
1
+ # Element Extensions
2
+
3
+ Side-effect module that extends `Element.prototype` with DOM traversal, focus management, and visibility helpers. Also exports standalone functions for clipboard operations and element bounds measurement.
4
+
5
+ ## Prototype Methods
6
+
7
+ ### Element.findAll
8
+
9
+ ```typescript
10
+ findAll<TEl extends Element = Element>(selector: string): TEl[]
11
+ ```
12
+
13
+ Find all child elements matching a CSS selector. Returns an empty array if the selector is empty.
14
+
15
+ ### Element.findFirst
16
+
17
+ ```typescript
18
+ findFirst<TEl extends Element = Element>(selector: string): TEl | undefined
19
+ ```
20
+
21
+ Find the first child element matching a CSS selector. Returns `undefined` if the selector is empty or no match is found.
22
+
23
+ ### Element.prependChild
24
+
25
+ ```typescript
26
+ prependChild<TEl extends Element>(child: TEl): TEl
27
+ ```
28
+
29
+ Insert an element as the first child. Returns the inserted child element.
30
+
31
+ ### Element.getParents
32
+
33
+ ```typescript
34
+ getParents(): Element[]
35
+ ```
36
+
37
+ Get all parent elements ordered from closest to farthest (up to the root).
38
+
39
+ ### Element.findFocusableParent
40
+
41
+ ```typescript
42
+ findFocusableParent(): HTMLElement | undefined
43
+ ```
44
+
45
+ Find the nearest focusable ancestor element. Uses the `tabbable` library to determine focusability.
46
+
47
+ ### Element.findFirstFocusableChild
48
+
49
+ ```typescript
50
+ findFirstFocusableChild(): HTMLElement | undefined
51
+ ```
52
+
53
+ Find the first focusable descendant element using a tree walker. Uses the `tabbable` library to determine focusability.
54
+
55
+ ### Element.isOffsetElement
56
+
57
+ ```typescript
58
+ isOffsetElement(): boolean
59
+ ```
60
+
61
+ Check whether the element is an offset parent. Returns `true` if the computed `position` is `relative`, `absolute`, `fixed`, or `sticky`.
62
+
63
+ ### Element.isVisible
64
+
65
+ ```typescript
66
+ isVisible(): boolean
67
+ ```
68
+
69
+ Check whether the element is visible on screen. Checks for the existence of client rects, `visibility: hidden`, and `opacity: 0`.
70
+
71
+ ---
72
+
73
+ ## Exported Interfaces
74
+
75
+ ### ElementBounds
76
+
77
+ ```typescript
78
+ interface ElementBounds {
79
+ target: Element;
80
+ top: number;
81
+ left: number;
82
+ width: number;
83
+ height: number;
84
+ }
85
+ ```
86
+
87
+ Bounds information for an element, with positions relative to the viewport.
88
+
89
+ ---
90
+
91
+ ## Exported Functions
92
+
93
+ ### copyElement
94
+
95
+ ```typescript
96
+ function copyElement(event: ClipboardEvent): void
97
+ ```
98
+
99
+ Copy element content to clipboard. Intended for use as a `copy` event handler. Finds the first `input` or `textarea` within the event target and writes its value to the clipboard.
100
+
101
+ ### pasteToElement
102
+
103
+ ```typescript
104
+ function pasteToElement(event: ClipboardEvent): void
105
+ ```
106
+
107
+ Paste clipboard content into an element. Intended for use as a `paste` event handler. Finds the first `input` or `textarea` within the event target and replaces its value with clipboard text, dispatching an `input` event afterward.
108
+
109
+ ### getBounds
110
+
111
+ ```typescript
112
+ function getBounds(els: Element[], timeout?: number): Promise<ElementBounds[]>
113
+ ```
114
+
115
+ Get bounds information for multiple elements using `IntersectionObserver`. Results are returned in the same order as the input array, with duplicates removed.
116
+
117
+ | Parameter | Type | Default | Description |
118
+ |-----------|------|---------|-------------|
119
+ | `els` | `Element[]` | -- | Target elements |
120
+ | `timeout` | `number` | `5000` | Timeout in milliseconds |
121
+
122
+ Throws `TimeoutError` (from `@simplysm/core-common`) if the observer does not respond within the timeout duration.
123
+
124
+ ---
125
+
126
+ ## Usage Examples
127
+
128
+ ```typescript
129
+ import "@simplysm/core-browser"; // activate side effects
130
+
131
+ // Find elements
132
+ const buttons = container.findAll<HTMLButtonElement>("button.primary");
133
+ const firstInput = form.findFirst<HTMLInputElement>("input[type=text]");
134
+
135
+ // DOM traversal
136
+ const parents = element.getParents();
137
+ const focusable = element.findFirstFocusableChild();
138
+
139
+ // Visibility check
140
+ if (element.isVisible()) {
141
+ // element is rendered and visible
142
+ }
143
+
144
+ // Clipboard handlers
145
+ document.addEventListener("copy", copyElement);
146
+ document.addEventListener("paste", pasteToElement);
147
+
148
+ // Measure element bounds
149
+ const bounds = await getBounds([el1, el2, el3]);
150
+ // bounds[0].top, bounds[0].left, bounds[0].width, bounds[0].height
151
+ ```
@@ -0,0 +1,86 @@
1
+ # HTMLElement Extensions
2
+
3
+ Side-effect module that extends `HTMLElement.prototype` with layout and scrolling utilities.
4
+
5
+ ## Prototype Methods
6
+
7
+ ### HTMLElement.repaint
8
+
9
+ ```typescript
10
+ repaint(): void
11
+ ```
12
+
13
+ Force a synchronous repaint by triggering a reflow. Internally accesses `offsetHeight` to flush pending style changes.
14
+
15
+ ---
16
+
17
+ ### HTMLElement.getRelativeOffset
18
+
19
+ ```typescript
20
+ getRelativeOffset(parent: HTMLElement | string): { top: number; left: number }
21
+ ```
22
+
23
+ Calculate the element's position relative to a parent element, returning document-based coordinates suitable for CSS `top`/`left` properties.
24
+
25
+ | Parameter | Type | Description |
26
+ |-----------|------|-------------|
27
+ | `parent` | `HTMLElement \| string` | Parent element or CSS selector to use as reference |
28
+
29
+ **Returns:** `{ top: number; left: number }` -- coordinates usable in CSS positioning.
30
+
31
+ **Throws:** `ArgumentError` (from `@simplysm/core-common`) if the parent element cannot be found.
32
+
33
+ The calculation accounts for:
34
+ - Viewport-relative position (`getBoundingClientRect`)
35
+ - Document scroll position (`window.scrollX/Y`)
36
+ - Parent element internal scroll (`parentEl.scrollTop/Left`)
37
+ - Border thickness of intermediate elements
38
+ - CSS `transform` transformations
39
+
40
+ Common use cases include positioning dropdowns and popups after appending them to `document.body`.
41
+
42
+ ---
43
+
44
+ ### HTMLElement.scrollIntoViewIfNeeded
45
+
46
+ ```typescript
47
+ scrollIntoViewIfNeeded(
48
+ target: { top: number; left: number },
49
+ offset?: { top: number; left: number },
50
+ ): void
51
+ ```
52
+
53
+ Scroll the element so that a target position is not obscured by a fixed offset area (e.g., a sticky header or fixed column).
54
+
55
+ | Parameter | Type | Default | Description |
56
+ |-----------|------|---------|-------------|
57
+ | `target` | `{ top: number; left: number }` | -- | Target position within the container (`offsetTop`, `offsetLeft`) |
58
+ | `offset` | `{ top: number; left: number }` | `{ top: 0, left: 0 }` | Size of the area that must remain unobscured |
59
+
60
+ Only handles cases where the target extends beyond the top/left boundaries of the scroll area. For downward/rightward scrolling, the browser's default focus scroll behavior is relied upon. Typically used with focus events on tables that have fixed headers or columns.
61
+
62
+ ---
63
+
64
+ ## Usage Examples
65
+
66
+ ```typescript
67
+ import "@simplysm/core-browser"; // activate side effects
68
+
69
+ // Force repaint after style changes
70
+ element.style.transform = "scale(1.1)";
71
+ element.repaint();
72
+
73
+ // Position a dropdown relative to document.body
74
+ const pos = trigger.getRelativeOffset(document.body);
75
+ dropdown.style.top = `${pos.top + trigger.offsetHeight}px`;
76
+ dropdown.style.left = `${pos.left}px`;
77
+
78
+ // Position relative to a container found by selector
79
+ const pos2 = cell.getRelativeOffset(".scroll-container");
80
+
81
+ // Scroll table so focused cell is visible past fixed header
82
+ tableContainer.scrollIntoViewIfNeeded(
83
+ { top: cell.offsetTop, left: cell.offsetLeft },
84
+ { top: headerHeight, left: fixedColumnWidth },
85
+ );
86
+ ```
@@ -0,0 +1,193 @@
1
+ # IndexedDB
2
+
3
+ Lightweight wrappers around the browser IndexedDB API. `IndexedDbStore` provides a simplified interface for opening databases and performing CRUD operations. `IndexedDbVirtualFs` builds on top of it to implement a virtual filesystem stored in IndexedDB.
4
+
5
+ ## IndexedDbStore
6
+
7
+ ```typescript
8
+ class IndexedDbStore {
9
+ constructor(dbName: string, dbVersion: number, storeConfigs: StoreConfig[]);
10
+ }
11
+ ```
12
+
13
+ A wrapper that manages an IndexedDB database with automatic store creation on version upgrades.
14
+
15
+ | Parameter | Type | Description |
16
+ |-----------|------|-------------|
17
+ | `dbName` | `string` | Database name |
18
+ | `dbVersion` | `number` | Database version (triggers `onupgradeneeded` when increased) |
19
+ | `storeConfigs` | `StoreConfig[]` | Object store definitions |
20
+
21
+ ### StoreConfig
22
+
23
+ ```typescript
24
+ interface StoreConfig {
25
+ name: string;
26
+ keyPath: string;
27
+ }
28
+ ```
29
+
30
+ ### Methods
31
+
32
+ #### open
33
+
34
+ ```typescript
35
+ open(): Promise<IDBDatabase>
36
+ ```
37
+
38
+ Open the database, creating any missing object stores defined in `storeConfigs`. Rejects if the database is blocked by another connection.
39
+
40
+ #### withStore
41
+
42
+ ```typescript
43
+ withStore<TResult>(
44
+ storeName: string,
45
+ mode: IDBTransactionMode,
46
+ fn: (store: IDBObjectStore) => Promise<TResult>,
47
+ ): Promise<TResult>
48
+ ```
49
+
50
+ Execute a callback within a transaction on the specified store. The database connection is automatically opened and closed. If the callback throws, the transaction is aborted.
51
+
52
+ #### get
53
+
54
+ ```typescript
55
+ get<TValue>(storeName: string, key: IDBValidKey): Promise<TValue | undefined>
56
+ ```
57
+
58
+ Retrieve a single record by key. Returns `undefined` if not found.
59
+
60
+ #### put
61
+
62
+ ```typescript
63
+ put(storeName: string, value: unknown): Promise<void>
64
+ ```
65
+
66
+ Insert or update a record. The key is extracted from the value using the store's `keyPath`.
67
+
68
+ #### getAll
69
+
70
+ ```typescript
71
+ getAll<TItem>(storeName: string): Promise<TItem[]>
72
+ ```
73
+
74
+ Retrieve all records from a store.
75
+
76
+ ---
77
+
78
+ ## IndexedDbVirtualFs
79
+
80
+ ```typescript
81
+ class IndexedDbVirtualFs {
82
+ constructor(db: IndexedDbStore, storeName: string, keyField: string);
83
+ }
84
+ ```
85
+
86
+ A virtual filesystem built on `IndexedDbStore`. Each entry is stored as a record with a full path key, a kind (`"file"` or `"dir"`), and optional Base64-encoded data.
87
+
88
+ | Parameter | Type | Description |
89
+ |-----------|------|-------------|
90
+ | `db` | `IndexedDbStore` | The underlying IndexedDB store instance |
91
+ | `storeName` | `string` | Name of the object store to use |
92
+ | `keyField` | `string` | The key property name in stored records |
93
+
94
+ ### VirtualFsEntry
95
+
96
+ ```typescript
97
+ interface VirtualFsEntry {
98
+ kind: "file" | "dir";
99
+ dataBase64?: string;
100
+ }
101
+ ```
102
+
103
+ ### Methods
104
+
105
+ #### getEntry
106
+
107
+ ```typescript
108
+ getEntry(fullKey: string): Promise<VirtualFsEntry | undefined>
109
+ ```
110
+
111
+ Get a single filesystem entry by its full key path.
112
+
113
+ #### putEntry
114
+
115
+ ```typescript
116
+ putEntry(fullKey: string, kind: "file" | "dir", dataBase64?: string): Promise<void>
117
+ ```
118
+
119
+ Create or update a filesystem entry.
120
+
121
+ #### deleteByPrefix
122
+
123
+ ```typescript
124
+ deleteByPrefix(keyPrefix: string): Promise<boolean>
125
+ ```
126
+
127
+ Delete all entries whose key matches the prefix or starts with `prefix + "/"`. Returns `true` if any entries were deleted.
128
+
129
+ #### listChildren
130
+
131
+ ```typescript
132
+ listChildren(prefix: string): Promise<{ name: string; isDirectory: boolean }[]>
133
+ ```
134
+
135
+ List immediate children under a path prefix. Returns each child's name and whether it is a directory.
136
+
137
+ #### ensureDir
138
+
139
+ ```typescript
140
+ ensureDir(
141
+ fullKeyBuilder: (path: string) => string,
142
+ dirPath: string,
143
+ ): Promise<void>
144
+ ```
145
+
146
+ Ensure that a directory and all its ancestor directories exist. Walks through each segment of `dirPath` and creates missing directory entries.
147
+
148
+ | Parameter | Type | Description |
149
+ |-----------|------|-------------|
150
+ | `fullKeyBuilder` | `(path: string) => string` | Function to convert a path into a full key |
151
+ | `dirPath` | `string` | Directory path to ensure (e.g., `"/data/images"`) |
152
+
153
+ ---
154
+
155
+ ## Usage Examples
156
+
157
+ ```typescript
158
+ import { IndexedDbStore, IndexedDbVirtualFs } from "@simplysm/core-browser";
159
+
160
+ // Set up a database with one store
161
+ const store = new IndexedDbStore("my-app-db", 1, [
162
+ { name: "files", keyPath: "path" },
163
+ ]);
164
+
165
+ // Basic CRUD
166
+ await store.put("files", { path: "/config.json", data: "{}" });
167
+ const record = await store.get<{ path: string; data: string }>("files", "/config.json");
168
+ const allRecords = await store.getAll("files");
169
+
170
+ // Use withStore for custom transactions
171
+ await store.withStore("files", "readwrite", async (objStore) => {
172
+ return new Promise((resolve, reject) => {
173
+ const req = objStore.delete("/old-file.txt");
174
+ req.onsuccess = () => resolve(undefined);
175
+ req.onerror = () => reject(req.error);
176
+ });
177
+ });
178
+
179
+ // Virtual filesystem
180
+ const vfs = new IndexedDbVirtualFs(store, "files", "path");
181
+
182
+ await vfs.ensureDir((p) => p, "/data/images");
183
+ await vfs.putEntry("/data/images/photo.png", "file", btoa("...binary data..."));
184
+
185
+ const children = await vfs.listChildren("/data/");
186
+ // [{ name: "images", isDirectory: true }]
187
+
188
+ const entry = await vfs.getEntry("/data/images/photo.png");
189
+ // { kind: "file", dataBase64: "..." }
190
+
191
+ const deleted = await vfs.deleteByPrefix("/data/images");
192
+ // true
193
+ ```
@@ -0,0 +1,94 @@
1
+ # Utilities
2
+
3
+ Standalone utility functions for file downloads, HTTP fetching with progress tracking, and programmatic file selection dialogs.
4
+
5
+ ## downloadBlob
6
+
7
+ ```typescript
8
+ function downloadBlob(blob: Blob, fileName: string): void
9
+ ```
10
+
11
+ Download a Blob as a file. Creates a temporary object URL, triggers a download via an anchor click, and revokes the URL after 1 second.
12
+
13
+ | Parameter | Type | Description |
14
+ |-----------|------|-------------|
15
+ | `blob` | `Blob` | Blob object to download |
16
+ | `fileName` | `string` | File name for the downloaded file |
17
+
18
+ ---
19
+
20
+ ## fetchUrlBytes
21
+
22
+ ```typescript
23
+ function fetchUrlBytes(
24
+ url: string,
25
+ options?: { onProgress?: (progress: DownloadProgress) => void },
26
+ ): Promise<Uint8Array>
27
+ ```
28
+
29
+ Download binary data from a URL as a `Uint8Array`, with optional progress reporting.
30
+
31
+ | Parameter | Type | Description |
32
+ |-----------|------|-------------|
33
+ | `url` | `string` | URL to download from |
34
+ | `options.onProgress` | `(progress: DownloadProgress) => void` | Callback invoked as chunks are received |
35
+
36
+ When the server provides a `Content-Length` header, memory is pre-allocated for efficiency. For chunked transfers without `Content-Length`, chunks are collected and concatenated at the end.
37
+
38
+ Throws an `Error` if the response status is not OK or the response body is not readable.
39
+
40
+ ### DownloadProgress
41
+
42
+ ```typescript
43
+ interface DownloadProgress {
44
+ receivedLength: number;
45
+ contentLength: number;
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## openFileDialog
52
+
53
+ ```typescript
54
+ function openFileDialog(options?: {
55
+ accept?: string;
56
+ multiple?: boolean;
57
+ }): Promise<File[] | undefined>
58
+ ```
59
+
60
+ Programmatically open a file selection dialog without requiring a visible `<input type="file">` in the DOM.
61
+
62
+ | Parameter | Type | Default | Description |
63
+ |-----------|------|---------|-------------|
64
+ | `options.accept` | `string` | -- | Accepted file types (e.g., `".png,.jpg"`, `"image/*"`) |
65
+ | `options.multiple` | `boolean` | `false` | Allow selecting multiple files |
66
+
67
+ **Returns:** A `File[]` if the user selected files, or `undefined` if the dialog was cancelled.
68
+
69
+ ---
70
+
71
+ ## Usage Examples
72
+
73
+ ```typescript
74
+ import { downloadBlob, fetchUrlBytes, openFileDialog } from "@simplysm/core-browser";
75
+
76
+ // Download a text file
77
+ const blob = new Blob(["Hello, world!"], { type: "text/plain" });
78
+ downloadBlob(blob, "hello.txt");
79
+
80
+ // Fetch binary data with progress
81
+ const data = await fetchUrlBytes("https://example.com/archive.zip", {
82
+ onProgress: ({ receivedLength, contentLength }) => {
83
+ console.log(`${Math.round((receivedLength / contentLength) * 100)}%`);
84
+ },
85
+ });
86
+
87
+ // Open file dialog for images
88
+ const files = await openFileDialog({ accept: "image/*", multiple: true });
89
+ if (files) {
90
+ for (const file of files) {
91
+ // process each selected file
92
+ }
93
+ }
94
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/core-browser",
3
- "version": "13.0.82",
3
+ "version": "13.0.83",
4
4
  "description": "Simplysm package - Core module (browser)",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "tabbable": "^6.4.0",
27
- "@simplysm/core-common": "13.0.82"
27
+ "@simplysm/core-common": "13.0.83"
28
28
  },
29
29
  "devDependencies": {
30
30
  "happy-dom": "^20.8.3"