@simplysm/core-browser 13.0.96 → 13.0.98
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 +115 -204
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,56 +1,63 @@
|
|
|
1
1
|
# @simplysm/core-browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Core module (browser) -- browser-only utilities including DOM extensions, file downloads, IndexedDB storage, and virtual file system.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @simplysm/core-browser
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Side-Effect Imports
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
This package includes side-effect imports that augment global prototypes when the module is loaded:
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
- `import "./extensions/element-ext"` -- Adds methods to `Element.prototype` (`findAll`, `findFirst`, `prependChild`, `getParents`, `findFocusableParent`, `findFirstFocusableChild`, `isOffsetElement`, `isVisible`).
|
|
16
|
+
- `import "./extensions/html-element-ext"` -- Adds methods to `HTMLElement.prototype` (`repaint`, `getRelativeOffset`, `scrollIntoViewIfNeeded`).
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
These side effects run automatically when you import from `@simplysm/core-browser`.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
import "@simplysm/core-browser";
|
|
21
|
-
|
|
22
|
-
// 요소 검색
|
|
23
|
-
el.findAll<HTMLDivElement>(".item"); // 자식 요소 전체 검색
|
|
24
|
-
el.findFirst<HTMLInputElement>("input"); // 첫 번째 매칭 요소
|
|
25
|
-
|
|
26
|
-
// DOM 조작
|
|
27
|
-
el.prependChild(newChild); // 첫 번째 자식으로 삽입
|
|
20
|
+
## API Overview
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
el.getParents(); // 모든 부모 요소 (가까운 순)
|
|
31
|
-
el.findFocusableParent(); // 포커스 가능한 부모 찾기
|
|
32
|
-
el.findFirstFocusableChild(); // 포커스 가능한 첫 자식 찾기
|
|
22
|
+
### Extensions -- Element
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
| API | Type | Description |
|
|
25
|
+
|-----|------|-------------|
|
|
26
|
+
| `ElementBounds` | interface | Element bounds info (`target`, `top`, `left`, `width`, `height`) |
|
|
27
|
+
| `copyElement` | function | Copy element content to clipboard via ClipboardEvent |
|
|
28
|
+
| `pasteToElement` | function | Paste clipboard content to element via ClipboardEvent |
|
|
29
|
+
| `getBounds` | function | Get bounds for multiple elements using IntersectionObserver |
|
|
30
|
+
| `Element.findAll` | prototype method | Find all child elements matching a CSS selector |
|
|
31
|
+
| `Element.findFirst` | prototype method | Find first element matching a CSS selector |
|
|
32
|
+
| `Element.prependChild` | prototype method | Insert element as first child |
|
|
33
|
+
| `Element.getParents` | prototype method | Get all parent elements (closest to farthest) |
|
|
34
|
+
| `Element.findFocusableParent` | prototype method | Find first focusable parent element |
|
|
35
|
+
| `Element.findFirstFocusableChild` | prototype method | Find first focusable child element |
|
|
36
|
+
| `Element.isOffsetElement` | prototype method | Check if element has offset positioning |
|
|
37
|
+
| `Element.isVisible` | prototype method | Check if element is visible on screen |
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
### Extensions -- HTMLElement
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
| API | Type | Description |
|
|
42
|
+
|-----|------|-------------|
|
|
43
|
+
| `HTMLElement.repaint` | prototype method | Force repaint (triggers reflow) |
|
|
44
|
+
| `HTMLElement.getRelativeOffset` | prototype method | Calculate position relative to a parent element |
|
|
45
|
+
| `HTMLElement.scrollIntoViewIfNeeded` | prototype method | Scroll to make target visible if obscured |
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
import { getBounds } from "@simplysm/core-browser";
|
|
45
|
-
import type { ElementBounds } from "@simplysm/core-browser";
|
|
47
|
+
### Utils
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
| API | Type | Description |
|
|
50
|
+
|-----|------|-------------|
|
|
51
|
+
| `downloadBlob` | function | Download a Blob as a file |
|
|
52
|
+
| `DownloadProgress` | interface | Download progress info (`receivedLength`, `contentLength`) |
|
|
53
|
+
| `fetchUrlBytes` | function | Download binary data from URL with progress callback |
|
|
54
|
+
| `openFileDialog` | function | Programmatically open file selection dialog |
|
|
55
|
+
| `StoreConfig` | interface | IndexedDB store configuration (`name`, `keyPath`) |
|
|
56
|
+
| `IndexedDbStore` | class | IndexedDB wrapper for key-value storage |
|
|
57
|
+
| `VirtualFsEntry` | interface | Virtual file system entry (`kind`, `dataBase64`) |
|
|
58
|
+
| `IndexedDbVirtualFs` | class | IndexedDB-backed virtual file system |
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
**시그니처:**
|
|
60
|
+
## `ElementBounds`
|
|
54
61
|
|
|
55
62
|
```typescript
|
|
56
63
|
interface ElementBounds {
|
|
@@ -60,139 +67,61 @@ interface ElementBounds {
|
|
|
60
67
|
width: number;
|
|
61
68
|
height: number;
|
|
62
69
|
}
|
|
63
|
-
|
|
64
|
-
function getBounds(els: Element[], timeout?: number): Promise<ElementBounds[]>;
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
- 빈 배열 입력 시 빈 배열을 즉시 반환한다.
|
|
69
|
-
- 타임아웃 초과 시 `TimeoutError` (`@simplysm/core-common`)를 throw한다.
|
|
70
|
-
|
|
71
|
-
#### 클립보드 -- copyElement / pasteToElement
|
|
72
|
-
|
|
73
|
-
copy/paste 이벤트 핸들러에서 사용한다. 대상 요소 내부의 첫 번째 `input`/`textarea`를 자동으로 찾아 처리한다.
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
import { copyElement, pasteToElement } from "@simplysm/core-browser";
|
|
77
|
-
|
|
78
|
-
document.addEventListener("copy", (event) => {
|
|
79
|
-
copyElement(event); // input/textarea의 value를 클립보드에 복사
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
document.addEventListener("paste", (event) => {
|
|
83
|
-
pasteToElement(event); // 클립보드 텍스트를 input/textarea에 붙여넣기
|
|
84
|
-
});
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
**시그니처:**
|
|
72
|
+
## `copyElement`
|
|
88
73
|
|
|
89
74
|
```typescript
|
|
90
75
|
function copyElement(event: ClipboardEvent): void;
|
|
91
|
-
function pasteToElement(event: ClipboardEvent): void;
|
|
92
76
|
```
|
|
93
77
|
|
|
94
|
-
|
|
95
|
-
- 대상 요소가 `Element`가 아니거나 `clipboardData`가 없으면 무시한다.
|
|
96
|
-
|
|
97
|
-
### HTMLElement 확장 메서드
|
|
78
|
+
Copy element content to clipboard. Use as a copy event handler.
|
|
98
79
|
|
|
99
|
-
|
|
80
|
+
## `pasteToElement`
|
|
100
81
|
|
|
101
82
|
```typescript
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// 상대 위치 계산 (transform 포함)
|
|
105
|
-
// parent: HTMLElement 또는 CSS 선택자 문자열
|
|
106
|
-
const offset = el.getRelativeOffset(parentEl);
|
|
107
|
-
const offset2 = el.getRelativeOffset(".container");
|
|
108
|
-
// { top: number, left: number } -- CSS top/left에 직접 사용 가능
|
|
109
|
-
// 뷰포트 좌표, 스크롤 위치, 보더, transform을 모두 반영
|
|
110
|
-
|
|
111
|
-
// 고정 영역을 고려한 스크롤
|
|
112
|
-
el.scrollIntoViewIfNeeded(
|
|
113
|
-
{ top: 100, left: 0 }, // 대상 위치 (offsetTop, offsetLeft)
|
|
114
|
-
{ top: 60, left: 0 }, // 오프셋 (고정 헤더 높이 등)
|
|
115
|
-
);
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
**시그니처:**
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
interface HTMLElement {
|
|
122
|
-
repaint(): void;
|
|
123
|
-
getRelativeOffset(parent: HTMLElement | string): { top: number; left: number };
|
|
124
|
-
scrollIntoViewIfNeeded(
|
|
125
|
-
target: { top: number; left: number },
|
|
126
|
-
offset?: { top: number; left: number },
|
|
127
|
-
): void;
|
|
128
|
-
}
|
|
83
|
+
function pasteToElement(event: ClipboardEvent): void;
|
|
129
84
|
```
|
|
130
85
|
|
|
131
|
-
|
|
132
|
-
- `scrollIntoViewIfNeeded`: 위쪽/왼쪽 방향 스크롤만 처리한다. 아래쪽/오른쪽은 브라우저 기본 포커스 스크롤에 위임한다.
|
|
86
|
+
Paste clipboard content to element. Finds the first `input`/`textarea` within the target and replaces its value.
|
|
133
87
|
|
|
134
|
-
|
|
88
|
+
## `getBounds`
|
|
135
89
|
|
|
136
90
|
```typescript
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
downloadBlob(blob, "report.xlsx");
|
|
91
|
+
async function getBounds(els: Element[], timeout?: number): Promise<ElementBounds[]>;
|
|
140
92
|
```
|
|
141
93
|
|
|
142
|
-
|
|
94
|
+
Get bounds information for elements using IntersectionObserver. Throws `TimeoutError` if no response within `timeout` ms (default: 5000).
|
|
95
|
+
|
|
96
|
+
## `downloadBlob`
|
|
143
97
|
|
|
144
98
|
```typescript
|
|
145
99
|
function downloadBlob(blob: Blob, fileName: string): void;
|
|
146
100
|
```
|
|
147
101
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
### 바이너리 다운로드 (진행률 지원)
|
|
151
|
-
|
|
152
|
-
URL에서 바이너리 데이터를 다운로드한다. Content-Length가 존재하면 미리 할당하여 메모리 효율적으로 처리하고, 없으면 chunked encoding 방식으로 수집 후 병합한다.
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
import { fetchUrlBytes } from "@simplysm/core-browser";
|
|
156
|
-
import type { DownloadProgress } from "@simplysm/core-browser";
|
|
157
|
-
|
|
158
|
-
const data: Uint8Array = await fetchUrlBytes("/api/file", {
|
|
159
|
-
onProgress: ({ receivedLength, contentLength }: DownloadProgress) => {
|
|
160
|
-
// contentLength가 0이면 Content-Length 헤더가 없는 경우
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
```
|
|
102
|
+
Download a Blob as a file by creating a temporary object URL and clicking a link.
|
|
164
103
|
|
|
165
|
-
|
|
104
|
+
## `DownloadProgress`
|
|
166
105
|
|
|
167
106
|
```typescript
|
|
168
107
|
interface DownloadProgress {
|
|
169
108
|
receivedLength: number;
|
|
170
109
|
contentLength: number;
|
|
171
110
|
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## `fetchUrlBytes`
|
|
172
114
|
|
|
173
|
-
|
|
115
|
+
```typescript
|
|
116
|
+
async function fetchUrlBytes(
|
|
174
117
|
url: string,
|
|
175
118
|
options?: { onProgress?: (progress: DownloadProgress) => void },
|
|
176
119
|
): Promise<Uint8Array>;
|
|
177
120
|
```
|
|
178
121
|
|
|
179
|
-
|
|
180
|
-
- response body를 읽을 수 없는 경우 `Error("Response body is not readable")`를 throw한다.
|
|
181
|
-
- Content-Length가 없는 경우 `onProgress` 콜백의 `contentLength`는 `0`이며, 콜백이 호출되지 않는다.
|
|
182
|
-
|
|
183
|
-
### 파일 선택 다이얼로그
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
import { openFileDialog } from "@simplysm/core-browser";
|
|
187
|
-
|
|
188
|
-
const files = await openFileDialog({
|
|
189
|
-
accept: ".json",
|
|
190
|
-
multiple: true,
|
|
191
|
-
});
|
|
192
|
-
// File[] | undefined (취소 시 undefined)
|
|
193
|
-
```
|
|
122
|
+
Download binary data from URL with optional progress callback. Pre-allocates memory when Content-Length is known.
|
|
194
123
|
|
|
195
|
-
|
|
124
|
+
## `openFileDialog`
|
|
196
125
|
|
|
197
126
|
```typescript
|
|
198
127
|
function openFileDialog(options?: {
|
|
@@ -201,111 +130,93 @@ function openFileDialog(options?: {
|
|
|
201
130
|
}): Promise<File[] | undefined>;
|
|
202
131
|
```
|
|
203
132
|
|
|
204
|
-
|
|
133
|
+
Programmatically open a file selection dialog. Returns `undefined` if cancelled.
|
|
205
134
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import { IndexedDbStore } from "@simplysm/core-browser";
|
|
210
|
-
import type { StoreConfig } from "@simplysm/core-browser";
|
|
211
|
-
|
|
212
|
-
const store = new IndexedDbStore("myApp", 1, [
|
|
213
|
-
{ name: "users", keyPath: "id" },
|
|
214
|
-
{ name: "settings", keyPath: "key" },
|
|
215
|
-
]);
|
|
216
|
-
|
|
217
|
-
await store.open();
|
|
218
|
-
|
|
219
|
-
// CRUD
|
|
220
|
-
await store.put("users", { id: "1", name: "Alice" });
|
|
221
|
-
const user = await store.get<User>("users", "1");
|
|
222
|
-
const all = await store.getAll<User>("users");
|
|
223
|
-
await store.delete("users", "1");
|
|
224
|
-
|
|
225
|
-
// 트랜잭션 스코프 (IDBObjectStore 직접 조작)
|
|
226
|
-
const result = await store.withStore("users", "readwrite", async (objStore) => {
|
|
227
|
-
// IDBObjectStore API 직접 사용
|
|
228
|
-
return someValue;
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
store.close();
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**시그니처:**
|
|
135
|
+
## `StoreConfig`
|
|
235
136
|
|
|
236
137
|
```typescript
|
|
237
138
|
interface StoreConfig {
|
|
238
139
|
name: string;
|
|
239
140
|
keyPath: string;
|
|
240
141
|
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## `IndexedDbStore`
|
|
241
145
|
|
|
146
|
+
```typescript
|
|
242
147
|
class IndexedDbStore {
|
|
243
148
|
constructor(dbName: string, dbVersion: number, storeConfigs: StoreConfig[]);
|
|
244
|
-
open(): Promise<IDBDatabase>;
|
|
245
|
-
|
|
246
|
-
put(storeName: string, value: unknown): Promise<void>;
|
|
247
|
-
delete(storeName: string, key: IDBValidKey): Promise<void>;
|
|
248
|
-
getAll<TItem>(storeName: string): Promise<TItem[]>;
|
|
249
|
-
withStore<TResult>(
|
|
149
|
+
async open(): Promise<IDBDatabase>;
|
|
150
|
+
async withStore<TResult>(
|
|
250
151
|
storeName: string,
|
|
251
152
|
mode: IDBTransactionMode,
|
|
252
153
|
fn: (store: IDBObjectStore) => Promise<TResult>,
|
|
253
154
|
): Promise<TResult>;
|
|
155
|
+
async get<TValue>(storeName: string, key: IDBValidKey): Promise<TValue | undefined>;
|
|
156
|
+
async put(storeName: string, value: unknown): Promise<void>;
|
|
157
|
+
async delete(storeName: string, key: IDBValidKey): Promise<void>;
|
|
158
|
+
async getAll<TItem>(storeName: string): Promise<TItem[]>;
|
|
254
159
|
close(): void;
|
|
255
160
|
}
|
|
256
161
|
```
|
|
257
162
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
- `withStore`에서 `fn` 실행 중 에러 발생 시 트랜잭션을 abort한 뒤 원래 에러를 다시 throw한다.
|
|
163
|
+
IndexedDB wrapper that manages database connections, schema upgrades, and transactional CRUD operations.
|
|
164
|
+
|
|
165
|
+
## `VirtualFsEntry`
|
|
262
166
|
|
|
263
|
-
|
|
167
|
+
```typescript
|
|
168
|
+
interface VirtualFsEntry {
|
|
169
|
+
kind: "file" | "dir";
|
|
170
|
+
dataBase64?: string;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
264
173
|
|
|
265
|
-
|
|
174
|
+
## `IndexedDbVirtualFs`
|
|
266
175
|
|
|
267
176
|
```typescript
|
|
268
|
-
|
|
269
|
-
|
|
177
|
+
class IndexedDbVirtualFs {
|
|
178
|
+
constructor(db: IndexedDbStore, storeName: string, keyField: string);
|
|
179
|
+
async getEntry(fullKey: string): Promise<VirtualFsEntry | undefined>;
|
|
180
|
+
async putEntry(fullKey: string, kind: "file" | "dir", dataBase64?: string): Promise<void>;
|
|
181
|
+
async deleteByPrefix(keyPrefix: string): Promise<boolean>;
|
|
182
|
+
async listChildren(prefix: string): Promise<{ name: string; isDirectory: boolean }[]>;
|
|
183
|
+
async ensureDir(fullKeyBuilder: (path: string) => string, dirPath: string): Promise<void>;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
270
186
|
|
|
271
|
-
|
|
272
|
-
const vfs = new IndexedDbVirtualFs(dbStore, "files", "key");
|
|
187
|
+
Virtual file system backed by IndexedDB. Stores files and directories as key-value entries with hierarchical path support.
|
|
273
188
|
|
|
274
|
-
|
|
275
|
-
await vfs.ensureDir((p) => `root${p}`, "documents/reports");
|
|
189
|
+
## Usage Examples
|
|
276
190
|
|
|
277
|
-
|
|
278
|
-
await vfs.putEntry("root/hello.txt", "file", btoa("Hello"));
|
|
279
|
-
const entry = await vfs.getEntry("root/hello.txt");
|
|
280
|
-
// { kind: "file", dataBase64: "SGVsbG8=" } | undefined
|
|
191
|
+
### Download a file
|
|
281
192
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// [{ name: "documents", isDirectory: true }, { name: "hello.txt", isDirectory: false }]
|
|
193
|
+
```typescript
|
|
194
|
+
import { downloadBlob } from "@simplysm/core-browser";
|
|
285
195
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// true: 삭제된 항목이 있음, false: 해당 키 prefix에 항목 없음
|
|
196
|
+
const blob = new Blob(["Hello"], { type: "text/plain" });
|
|
197
|
+
downloadBlob(blob, "hello.txt");
|
|
289
198
|
```
|
|
290
199
|
|
|
291
|
-
|
|
200
|
+
### Open file dialog and read files
|
|
292
201
|
|
|
293
202
|
```typescript
|
|
294
|
-
|
|
295
|
-
kind: "file" | "dir";
|
|
296
|
-
dataBase64?: string;
|
|
297
|
-
}
|
|
203
|
+
import { openFileDialog } from "@simplysm/core-browser";
|
|
298
204
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
listChildren(prefix: string): Promise<{ name: string; isDirectory: boolean }[]>;
|
|
305
|
-
ensureDir(fullKeyBuilder: (path: string) => string, dirPath: string): Promise<void>;
|
|
205
|
+
const files = await openFileDialog({ accept: ".csv", multiple: true });
|
|
206
|
+
if (files) {
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
const text = await file.text();
|
|
209
|
+
}
|
|
306
210
|
}
|
|
307
211
|
```
|
|
308
212
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
213
|
+
### Use IndexedDB store
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { IndexedDbStore } from "@simplysm/core-browser";
|
|
217
|
+
|
|
218
|
+
const store = new IndexedDbStore("myApp", 1, [{ name: "settings", keyPath: "key" }]);
|
|
219
|
+
await store.put("settings", { key: "theme", value: "dark" });
|
|
220
|
+
const item = await store.get<{ key: string; value: string }>("settings", "theme");
|
|
221
|
+
store.close();
|
|
222
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/core-browser",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.98",
|
|
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.
|
|
27
|
+
"@simplysm/core-common": "13.0.98"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"happy-dom": "^20.8.4"
|