@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.
Files changed (2) hide show
  1. package/README.md +115 -204
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,56 +1,63 @@
1
1
  # @simplysm/core-browser
2
2
 
3
- 브라우저 환경 전용 유틸리티. DOM 확장, 파일 다운로드, IndexedDB 추상화를 제공한다.
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
- **의존성:** `@simplysm/core-common`, `tabbable`
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
- ### Element 확장 메서드
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
- `import "@simplysm/core-browser"` (side-effect import) 시 `Element.prototype`에 추가된다.
18
+ These side effects run automatically when you import from `@simplysm/core-browser`.
18
19
 
19
- ```typescript
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
- el.isOffsetElement(); // position: relative/absolute/fixed/sticky 여부
36
- el.isVisible(); // 가시성 확인 (clientRects, visibility, opacity)
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
- #### getBounds -- 요소 크기/위치 일괄 조회
39
+ ### Extensions -- HTMLElement
40
40
 
41
- IntersectionObserver 기반으로 여러 요소의 뷰포트 기준 위치/크기를 비동기로 조회한다.
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
- ```typescript
44
- import { getBounds } from "@simplysm/core-browser";
45
- import type { ElementBounds } from "@simplysm/core-browser";
47
+ ### Utils
46
48
 
47
- const bounds: ElementBounds[] = await getBounds([el1, el2, el3], 5000);
48
- // 타임아웃 기본 5000ms, 초과 시 TimeoutError 발생
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
- bounds[0]; // { target: Element, top: number, left: number, width: number, height: number }
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
- - `pasteToElement`는 입력 요소의 전체 value를 교체하며, `input` 이벤트를 `bubbles: true`로 dispatch한다.
95
- - 대상 요소가 `Element`가 아니거나 `clipboardData`가 없으면 무시한다.
96
-
97
- ### HTMLElement 확장 메서드
78
+ Copy element content to clipboard. Use as a copy event handler.
98
79
 
99
- side-effect import 시 `HTMLElement.prototype`에 추가된다.
80
+ ## `pasteToElement`
100
81
 
101
82
  ```typescript
102
- el.repaint(); // 강제 리페인트 (reflow 트리거)
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
- - `getRelativeOffset`: 부모 요소를 찾을 없으면 `ArgumentError` (`@simplysm/core-common`)를 throw한다. 드롭다운/팝업을 `document.body`에 append한 위치를 잡을 때 유용하다.
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
- import { downloadBlob } from "@simplysm/core-browser";
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
- 내부적으로 `URL.createObjectURL`을 생성하고 1초 해제한다.
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
- function fetchUrlBytes(
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
- - HTTP 에러 응답 `Error("Download failed: {status} {statusText}")`를 throw한다.
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
- ### IndexedDB 스토어
133
+ Programmatically open a file selection dialog. Returns `undefined` if cancelled.
205
134
 
206
- IndexedDB를 간단한 key-value 스토어로 추상화한다. `open()` 중복 호출 시 기존 연결을 재사용하며, 버전 변경/연결 종료 시 자동으로 상태를 정리한다.
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
- get<TValue>(storeName: string, key: IDBValidKey): Promise<TValue | undefined>;
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
- - `open()`은 동시 호출 하나의 Promise만 생성하여 재사용한다.
259
- - 다른 연결에서 버전이 변경되면(`onversionchange`) 자동으로 연결을 닫고 상태를 초기화한다.
260
- - 다른 연결에 의해 blocked 상태가 되면 `Error("Database blocked by another connection")`를 throw한다.
261
- - `withStore`에서 `fn` 실행 중 에러 발생 시 트랜잭션을 abort한 뒤 원래 에러를 다시 throw한다.
163
+ IndexedDB wrapper that manages database connections, schema upgrades, and transactional CRUD operations.
164
+
165
+ ## `VirtualFsEntry`
262
166
 
263
- ### IndexedDB 가상 파일시스템
167
+ ```typescript
168
+ interface VirtualFsEntry {
169
+ kind: "file" | "dir";
170
+ dataBase64?: string;
171
+ }
172
+ ```
264
173
 
265
- IndexedDB 위에 계층적 파일/디렉토리 구조를 구현한다. `IndexedDbStore`를 내부 저장소로 사용한다.
174
+ ## `IndexedDbVirtualFs`
266
175
 
267
176
  ```typescript
268
- import { IndexedDbVirtualFs, IndexedDbStore } from "@simplysm/core-browser";
269
- import type { VirtualFsEntry } from "@simplysm/core-browser";
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
- const dbStore = new IndexedDbStore("vfs", 1, [{ name: "files", keyPath: "key" }]);
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
- const children = await vfs.listChildren("root/");
284
- // [{ name: "documents", isDirectory: true }, { name: "hello.txt", isDirectory: false }]
193
+ ```typescript
194
+ import { downloadBlob } from "@simplysm/core-browser";
285
195
 
286
- // 삭제 (키 자체 + 하위 항목 모두 삭제)
287
- const deleted: boolean = await vfs.deleteByPrefix("root/documents");
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
- interface VirtualFsEntry {
295
- kind: "file" | "dir";
296
- dataBase64?: string;
297
- }
203
+ import { openFileDialog } from "@simplysm/core-browser";
298
204
 
299
- class IndexedDbVirtualFs {
300
- constructor(db: IndexedDbStore, storeName: string, keyField: string);
301
- getEntry(fullKey: string): Promise<VirtualFsEntry | undefined>;
302
- putEntry(fullKey: string, kind: "file" | "dir", dataBase64?: string): Promise<void>;
303
- deleteByPrefix(keyPrefix: string): Promise<boolean>;
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
- - `deleteByPrefix`는 정확히 일치하는 키와 `keyPrefix + "/"` 로 시작하는 하위 항목을 모두 삭제한다.
310
- - `listChildren`은 직접 자식만 반환한다 (중첩된 하위 항목은 디렉토리로 표시).
311
- - `ensureDir`은 경로의 각 세그먼트에 대해 이미 존재하는지 확인 후 없으면 생성한다.
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.96",
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.96"
27
+ "@simplysm/core-common": "13.0.98"
28
28
  },
29
29
  "devDependencies": {
30
30
  "happy-dom": "^20.8.4"