@simplysm/core-browser 13.0.99 → 14.0.1
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/dist/extensions/element-ext.d.ts +36 -36
- package/dist/extensions/element-ext.d.ts.map +1 -1
- package/dist/extensions/element-ext.js +132 -111
- package/dist/extensions/element-ext.js.map +1 -6
- package/dist/extensions/html-element-ext.d.ts +22 -22
- package/dist/extensions/html-element-ext.js +50 -45
- package/dist/extensions/html-element-ext.js.map +1 -6
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -6
- package/dist/utils/IndexedDbStore.js +115 -112
- package/dist/utils/IndexedDbStore.js.map +1 -6
- package/dist/utils/IndexedDbVirtualFs.js +81 -83
- package/dist/utils/IndexedDbVirtualFs.js.map +1 -6
- package/dist/utils/download.d.ts +3 -3
- package/dist/utils/download.js +18 -14
- package/dist/utils/download.js.map +1 -6
- package/dist/utils/fetch.d.ts +1 -1
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +46 -36
- package/dist/utils/fetch.js.map +1 -6
- package/dist/utils/file-dialog.d.ts +1 -1
- package/dist/utils/file-dialog.js +19 -19
- package/dist/utils/file-dialog.js.map +1 -6
- package/package.json +7 -10
- package/src/extensions/element-ext.ts +40 -40
- package/src/extensions/html-element-ext.ts +24 -24
- package/src/index.ts +3 -3
- package/src/utils/IndexedDbStore.ts +3 -3
- package/src/utils/download.ts +3 -3
- package/src/utils/fetch.ts +17 -5
- package/src/utils/file-dialog.ts +1 -1
- package/README.md +0 -106
- package/docs/classes.md +0 -184
- package/docs/element-extensions.md +0 -134
- package/docs/html-element-extensions.md +0 -56
- package/docs/utilities.md +0 -71
- package/tests/extensions/element-ext.spec.ts +0 -693
- package/tests/extensions/html-element-ext.spec.ts +0 -175
- package/tests/utils/IndexedDbStore.spec.ts +0 -103
- package/tests/utils/IndexedDbVirtualFs.spec.ts +0 -171
- package/tests/utils/download.spec.ts +0 -66
- package/tests/utils/fetch.spec.ts +0 -154
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 프로그래밍 방식으로 파일 선택 대화상자 열기
|
|
3
|
+
*/
|
|
4
|
+
export function openFileDialog(options) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const input = document.createElement("input");
|
|
7
|
+
input.type = "file";
|
|
8
|
+
input.multiple = options?.multiple ?? false;
|
|
9
|
+
if (options?.accept != null) {
|
|
10
|
+
input.accept = options.accept;
|
|
11
|
+
}
|
|
12
|
+
input.onchange = () => {
|
|
13
|
+
resolve(input.files != null && input.files.length > 0 ? [...input.files] : undefined);
|
|
14
|
+
};
|
|
15
|
+
input.addEventListener("cancel", () => {
|
|
16
|
+
resolve(undefined);
|
|
17
|
+
});
|
|
18
|
+
input.click();
|
|
14
19
|
});
|
|
15
|
-
input.click();
|
|
16
|
-
});
|
|
17
20
|
}
|
|
18
|
-
|
|
19
|
-
openFileDialog
|
|
20
|
-
};
|
|
21
|
-
//# sourceMappingURL=file-dialog.js.map
|
|
21
|
+
//# sourceMappingURL=file-dialog.js.map
|
|
@@ -1,6 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/utils/file-dialog.ts"],
|
|
4
|
-
"mappings": "AAGO,SAAS,eAAe,SAGC;AAC9B,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,OAAO;AACb,UAAM,YAAW,mCAAS,aAAY;AACtC,SAAI,mCAAS,WAAU,MAAM;AAC3B,YAAM,SAAS,QAAQ;AAAA,IACzB;AACA,UAAM,WAAW,MAAM;AACrB,cAAQ,MAAM,SAAS,QAAQ,MAAM,MAAM,SAAS,IAAI,CAAC,GAAG,MAAM,KAAK,IAAI,MAAS;AAAA,IACtF;AACA,UAAM,iBAAiB,UAAU,MAAM;AACrC,cAAQ,MAAS;AAAA,IACnB,CAAC;AACD,UAAM,MAAM;AAAA,EACd,CAAC;AACH;",
|
|
5
|
-
"names": []
|
|
6
|
-
}
|
|
1
|
+
{"version":3,"file":"file-dialog.js","sourceRoot":"","sources":["..\\..\\src\\utils\\file-dialog.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,OAG9B;IACC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC9C,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC;QACpB,KAAK,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,KAAK,CAAC;QAC5C,IAAI,OAAO,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;YAC5B,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAChC,CAAC;QACD,KAAK,CAAC,QAAQ,GAAG,GAAG,EAAE;YACpB,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACxF,CAAC,CAAC;QACF,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE;YACpC,OAAO,CAAC,SAAS,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/core-browser",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "
|
|
3
|
+
"version": "14.0.1",
|
|
4
|
+
"description": "심플리즘 패키지 - 코어 (browser)",
|
|
5
|
+
"author": "심플리즘",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -14,19 +14,16 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
-
"
|
|
18
|
-
"src",
|
|
19
|
-
"tests"
|
|
17
|
+
"src"
|
|
20
18
|
],
|
|
21
19
|
"sideEffects": [
|
|
20
|
+
"./src/extensions/element-ext.ts",
|
|
21
|
+
"./src/extensions/html-element-ext.ts",
|
|
22
22
|
"./dist/extensions/element-ext.js",
|
|
23
23
|
"./dist/extensions/html-element-ext.js"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"tabbable": "^6.4.0",
|
|
27
|
-
"@simplysm/core-common": "
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"happy-dom": "^20.8.4"
|
|
27
|
+
"@simplysm/core-common": "14.0.1"
|
|
31
28
|
}
|
|
32
29
|
}
|
|
@@ -2,82 +2,82 @@ import { isFocusable } from "tabbable";
|
|
|
2
2
|
import { TimeoutError } from "@simplysm/core-common";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* 요소 경계 정보 타입
|
|
6
6
|
*/
|
|
7
7
|
export interface ElementBounds {
|
|
8
|
-
/**
|
|
8
|
+
/** 측정 대상 요소 */
|
|
9
9
|
target: Element;
|
|
10
|
-
/**
|
|
10
|
+
/** 뷰포트 기준 상단 위치 */
|
|
11
11
|
top: number;
|
|
12
|
-
/**
|
|
12
|
+
/** 뷰포트 기준 좌측 위치 */
|
|
13
13
|
left: number;
|
|
14
|
-
/**
|
|
14
|
+
/** 요소 너비 */
|
|
15
15
|
width: number;
|
|
16
|
-
/**
|
|
16
|
+
/** 요소 높이 */
|
|
17
17
|
height: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
declare global {
|
|
21
21
|
interface Element {
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* 선택자와 일치하는 모든 하위 요소 검색
|
|
24
24
|
*
|
|
25
|
-
* @param selector - CSS
|
|
26
|
-
* @returns
|
|
25
|
+
* @param selector - CSS 선택자
|
|
26
|
+
* @returns 일치하는 요소 배열 (빈 선택자는 빈 배열 반환)
|
|
27
27
|
*/
|
|
28
28
|
findAll<TEl extends Element = Element>(selector: string): TEl[];
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* 선택자와 일치하는 첫 번째 요소 검색
|
|
32
32
|
*
|
|
33
|
-
* @param selector - CSS
|
|
34
|
-
* @returns
|
|
33
|
+
* @param selector - CSS 선택자
|
|
34
|
+
* @returns 첫 번째 일치 요소 또는 undefined (빈 선택자는 undefined 반환)
|
|
35
35
|
*/
|
|
36
36
|
findFirst<TEl extends Element = Element>(selector: string): TEl | undefined;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* 요소를 첫 번째 자식으로 삽입
|
|
40
40
|
*
|
|
41
|
-
* @param child -
|
|
42
|
-
* @returns
|
|
41
|
+
* @param child - 삽입할 자식 요소
|
|
42
|
+
* @returns 삽입된 자식 요소
|
|
43
43
|
*/
|
|
44
44
|
prependChild<TEl extends Element>(child: TEl): TEl;
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* 모든 부모 요소 조회 (가까운 순서)
|
|
48
48
|
*
|
|
49
|
-
* @returns
|
|
49
|
+
* @returns 부모 요소 배열 (가장 가까운 것부터 먼 것 순)
|
|
50
50
|
*/
|
|
51
51
|
getParents(): Element[];
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* 첫 번째 포커스 가능한 부모 요소 검색 (tabbable 사용)
|
|
55
55
|
*
|
|
56
|
-
* @returns
|
|
56
|
+
* @returns 첫 번째 포커스 가능한 부모 요소 또는 undefined
|
|
57
57
|
*/
|
|
58
58
|
findFocusableParent(): HTMLElement | undefined;
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* 첫 번째 포커스 가능한 자식 요소 검색 (tabbable 사용)
|
|
62
62
|
*
|
|
63
|
-
* @returns
|
|
63
|
+
* @returns 첫 번째 포커스 가능한 자식 요소 또는 undefined
|
|
64
64
|
*/
|
|
65
65
|
findFirstFocusableChild(): HTMLElement | undefined;
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* 요소가 offset parent인지 확인 (position: relative/absolute/fixed/sticky)
|
|
69
69
|
*
|
|
70
|
-
* @returns
|
|
70
|
+
* @returns position 속성이 relative, absolute, fixed, sticky 중 하나이면 true
|
|
71
71
|
*/
|
|
72
72
|
isOffsetElement(): boolean;
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* 요소가 화면에 보이는지 확인
|
|
76
76
|
*
|
|
77
77
|
* @remarks
|
|
78
|
-
*
|
|
78
|
+
* clientRects 존재 여부, visibility: hidden, opacity: 0을 확인합니다.
|
|
79
79
|
*
|
|
80
|
-
* @returns
|
|
80
|
+
* @returns 요소가 화면에 보이면 true
|
|
81
81
|
*/
|
|
82
82
|
isVisible(): boolean;
|
|
83
83
|
}
|
|
@@ -144,13 +144,13 @@ Element.prototype.isVisible = function (): boolean {
|
|
|
144
144
|
};
|
|
145
145
|
|
|
146
146
|
// ============================================================================
|
|
147
|
-
//
|
|
147
|
+
// 정적 함수 (이벤트 핸들러 또는 다중 요소용)
|
|
148
148
|
// ============================================================================
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
*
|
|
151
|
+
* 요소 내용을 클립보드에 복사 (copy 이벤트 핸들러와 함께 사용)
|
|
152
152
|
*
|
|
153
|
-
* @param event - copy
|
|
153
|
+
* @param event - copy 이벤트 객체
|
|
154
154
|
*/
|
|
155
155
|
export function copyElement(event: ClipboardEvent): void {
|
|
156
156
|
const clipboardData = event.clipboardData;
|
|
@@ -167,13 +167,13 @@ export function copyElement(event: ClipboardEvent): void {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
*
|
|
170
|
+
* 클립보드 내용을 요소에 붙여넣기 (paste 이벤트 핸들러와 함께 사용)
|
|
171
171
|
*
|
|
172
172
|
* @remarks
|
|
173
|
-
*
|
|
174
|
-
*
|
|
173
|
+
* 대상 요소 내의 첫 번째 input/textarea를 찾아 전체 값을 클립보드 내용으로 교체합니다.
|
|
174
|
+
* 커서 위치나 선택 영역은 고려하지 않습니다.
|
|
175
175
|
*
|
|
176
|
-
* @param event - paste
|
|
176
|
+
* @param event - paste 이벤트 객체
|
|
177
177
|
*/
|
|
178
178
|
export function pasteToElement(event: ClipboardEvent): void {
|
|
179
179
|
const clipboardData = event.clipboardData;
|
|
@@ -191,20 +191,20 @@ export function pasteToElement(event: ClipboardEvent): void {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
|
-
*
|
|
194
|
+
* IntersectionObserver를 사용하여 요소의 경계 정보 조회
|
|
195
195
|
*
|
|
196
|
-
* @param els -
|
|
197
|
-
* @param timeout -
|
|
198
|
-
* @throws {TimeoutError}
|
|
196
|
+
* @param els - 대상 요소 배열
|
|
197
|
+
* @param timeout - 타임아웃 밀리초 (기본값: 5000)
|
|
198
|
+
* @throws {TimeoutError} 타임아웃 시간 내에 응답이 없는 경우
|
|
199
199
|
*/
|
|
200
200
|
export async function getBounds(els: Element[], timeout: number = 5000): Promise<ElementBounds[]> {
|
|
201
|
-
//
|
|
201
|
+
// 중복 제거 및 입력 순서대로 결과 정렬을 위한 인덱스 맵
|
|
202
202
|
const indexMap = new Map(els.map((el, i) => [el, i] as const));
|
|
203
203
|
if (indexMap.size === 0) {
|
|
204
204
|
return [];
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
//
|
|
207
|
+
// 남은 요소를 추적하기 위한 Set
|
|
208
208
|
const remaining = new Set(indexMap.keys());
|
|
209
209
|
|
|
210
210
|
let observer: IntersectionObserver | undefined;
|
|
@@ -231,7 +231,7 @@ export async function getBounds(els: Element[], timeout: number = 5000): Promise
|
|
|
231
231
|
|
|
232
232
|
if (remaining.size === 0) {
|
|
233
233
|
observer?.disconnect();
|
|
234
|
-
//
|
|
234
|
+
// 입력 순서대로 정렬
|
|
235
235
|
resolve(
|
|
236
236
|
results.sort((a, b) => indexMap.get(a.target)! - indexMap.get(b.target)!),
|
|
237
237
|
);
|
|
@@ -3,44 +3,44 @@ import { ArgumentError } from "@simplysm/core-common";
|
|
|
3
3
|
declare global {
|
|
4
4
|
interface HTMLElement {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* 강제 리페인트 (reflow 트리거)
|
|
7
7
|
*/
|
|
8
8
|
repaint(): void;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* 부모 요소 기준 상대 위치 계산 (CSS 포지셔닝용)
|
|
12
12
|
*
|
|
13
13
|
* @remarks
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* 부모 요소 기준으로 요소 위치를 계산하며, CSS `top`/`left` 속성에 바로 사용할 수 있는
|
|
15
|
+
* `window.scrollX/Y`를 포함한 문서 기반 좌표를 반환합니다.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
17
|
+
* 주요 사용 사례:
|
|
18
|
+
* - `document.body`에 append한 후 드롭다운, 팝업 위치 지정
|
|
19
|
+
* - 스크롤된 페이지에서도 정상 동작
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
25
|
-
* -
|
|
26
|
-
* - CSS transform
|
|
21
|
+
* 계산에 포함되는 요소:
|
|
22
|
+
* - 뷰포트 기준 위치 (getBoundingClientRect)
|
|
23
|
+
* - 문서 스크롤 위치 (window.scrollX/Y)
|
|
24
|
+
* - 부모 요소 내부 스크롤 (parentEl.scrollTop/Left)
|
|
25
|
+
* - 중간 요소의 border 두께
|
|
26
|
+
* - CSS transform 변환
|
|
27
27
|
*
|
|
28
|
-
* @param parent -
|
|
29
|
-
* @returns
|
|
30
|
-
* @throws {ArgumentError}
|
|
28
|
+
* @param parent - 기준이 되는 부모 요소 또는 선택자 (예: document.body, ".container")
|
|
29
|
+
* @returns CSS top/left 속성에 사용 가능한 좌표
|
|
30
|
+
* @throws {ArgumentError} 부모 요소를 찾을 수 없는 경우
|
|
31
31
|
*/
|
|
32
32
|
getRelativeOffset(parent: HTMLElement | string): { top: number; left: number };
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* offset 영역(예: 고정 헤더/컬럼)에 가려진 경우 대상이 보이도록 스크롤
|
|
36
36
|
*
|
|
37
37
|
* @remarks
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* 대상이 스크롤 영역의 상단/좌측 경계를 벗어나는 경우만 처리합니다.
|
|
39
|
+
* 하단/우측 방향 스크롤이 필요한 경우는 브라우저의 기본 포커스 스크롤 동작에 의존합니다.
|
|
40
|
+
* 주로 고정 헤더나 컬럼이 있는 테이블의 포커스 이벤트에서 사용됩니다.
|
|
41
41
|
*
|
|
42
|
-
* @param target -
|
|
43
|
-
* @param offset -
|
|
42
|
+
* @param target - 컨테이너 내 대상 위치 (offsetTop, offsetLeft)
|
|
43
|
+
* @param offset - 가려지면 안 되는 영역의 크기 (예: 고정 헤더 높이, 고정 컬럼 너비)
|
|
44
44
|
*/
|
|
45
45
|
scrollIntoViewIfNeeded(
|
|
46
46
|
target: { top: number; left: number },
|
|
@@ -50,8 +50,8 @@ declare global {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
HTMLElement.prototype.repaint = function (): void {
|
|
53
|
-
//
|
|
54
|
-
//
|
|
53
|
+
// offsetHeight에 접근하면 브라우저에서 강제 동기 레이아웃이 트리거되어,
|
|
54
|
+
// 현재 레이아웃의 스타일 변경이 즉시 적용되고 리페인트가 발생합니다.
|
|
55
55
|
void this.offsetHeight;
|
|
56
56
|
};
|
|
57
57
|
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// core-browser:
|
|
1
|
+
// core-browser: 브라우저 전용 유틸리티
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// 확장 (사이드 이펙트)
|
|
4
4
|
import "./extensions/element-ext";
|
|
5
5
|
import "./extensions/html-element-ext";
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// 재내보내기
|
|
8
8
|
export * from "./extensions/element-ext";
|
|
9
9
|
export * from "./extensions/html-element-ext";
|
|
10
10
|
export * from "./utils/download";
|
|
@@ -53,7 +53,7 @@ export class IndexedDbStore {
|
|
|
53
53
|
};
|
|
54
54
|
req.onblocked = () => {
|
|
55
55
|
this._opening = undefined;
|
|
56
|
-
reject(new Error("
|
|
56
|
+
reject(new Error("다른 연결에 의해 데이터베이스가 차단되었습니다"));
|
|
57
57
|
};
|
|
58
58
|
});
|
|
59
59
|
|
|
@@ -69,7 +69,7 @@ export class IndexedDbStore {
|
|
|
69
69
|
const tx = db.transaction(storeName, mode);
|
|
70
70
|
const store = tx.objectStore(storeName);
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// 먼저 fn 결과를 대기
|
|
73
73
|
let result: TResult;
|
|
74
74
|
let fnError: unknown;
|
|
75
75
|
let hasFnError = false;
|
|
@@ -81,7 +81,7 @@ export class IndexedDbStore {
|
|
|
81
81
|
tx.abort();
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// 트랜잭션 완료 대기
|
|
85
85
|
return new Promise<TResult>((resolve, reject) => {
|
|
86
86
|
if (hasFnError) {
|
|
87
87
|
tx.onabort = () => {
|
package/src/utils/download.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Blob을 파일로 다운로드
|
|
3
3
|
*
|
|
4
|
-
* @param blob - Blob
|
|
5
|
-
* @param fileName -
|
|
4
|
+
* @param blob - 다운로드할 Blob 객체
|
|
5
|
+
* @param fileName - 저장할 파일명
|
|
6
6
|
*/
|
|
7
7
|
export function downloadBlob(blob: Blob, fileName: string): void {
|
|
8
8
|
const url = URL.createObjectURL(blob);
|
package/src/utils/fetch.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface DownloadProgress {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* URL에서 바이너리 데이터 다운로드 (진행 콜백 지원)
|
|
10
10
|
*/
|
|
11
11
|
export async function fetchUrlBytes(
|
|
12
12
|
url: string,
|
|
@@ -14,17 +14,17 @@ export async function fetchUrlBytes(
|
|
|
14
14
|
): Promise<Uint8Array> {
|
|
15
15
|
const response = await fetch(url);
|
|
16
16
|
if (!response.ok) {
|
|
17
|
-
throw new Error(
|
|
17
|
+
throw new Error(`다운로드 실패: ${response.status} ${response.statusText}`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const contentLength = Number(response.headers.get("Content-Length") ?? 0);
|
|
21
21
|
const reader = response.body?.getReader();
|
|
22
22
|
if (!reader) {
|
|
23
|
-
throw new Error("
|
|
23
|
+
throw new Error("응답 본문을 읽을 수 없습니다");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
try {
|
|
27
|
-
//
|
|
27
|
+
// Content-Length를 알 수 있는 경우, 메모리 효율을 위해 사전 할당
|
|
28
28
|
if (contentLength > 0) {
|
|
29
29
|
const result = new Uint8Array(contentLength);
|
|
30
30
|
let receivedLength = 0;
|
|
@@ -33,15 +33,27 @@ export async function fetchUrlBytes(
|
|
|
33
33
|
const { done, value } = await reader.read();
|
|
34
34
|
if (done) break;
|
|
35
35
|
|
|
36
|
+
if (receivedLength + value.length > contentLength) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`수신 데이터가 Content-Length를 초과했습니다 (Content-Length: ${contentLength}, 수신: ${receivedLength + value.length}+)`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
result.set(value, receivedLength);
|
|
37
43
|
receivedLength += value.length;
|
|
38
44
|
options?.onProgress?.({ receivedLength, contentLength });
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
if (receivedLength < contentLength) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`수신 데이터가 Content-Length보다 부족합니다 (Content-Length: ${contentLength}, 수신: ${receivedLength})`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
return result;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
//
|
|
56
|
+
// Content-Length를 알 수 없는 경우, 청크를 수집 후 병합 (chunked encoding)
|
|
45
57
|
const chunks: Uint8Array[] = [];
|
|
46
58
|
|
|
47
59
|
while (true) {
|
package/src/utils/file-dialog.ts
CHANGED
package/README.md
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# @simplysm/core-browser
|
|
2
|
-
|
|
3
|
-
Simplysm package - Core module (browser). Browser-only utilities including DOM extensions, file downloads, IndexedDB storage, and virtual file system.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @simplysm/core-browser
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Side-Effect Imports
|
|
12
|
-
|
|
13
|
-
This package includes side-effect imports that augment global prototypes when the module is loaded:
|
|
14
|
-
|
|
15
|
-
- `Element.prototype` -- Adds `findAll`, `findFirst`, `prependChild`, `getParents`, `findFocusableParent`, `findFirstFocusableChild`, `isOffsetElement`, `isVisible`.
|
|
16
|
-
- `HTMLElement.prototype` -- Adds `repaint`, `getRelativeOffset`, `scrollIntoViewIfNeeded`.
|
|
17
|
-
|
|
18
|
-
These side effects run automatically when you import from `@simplysm/core-browser`.
|
|
19
|
-
|
|
20
|
-
## API Overview
|
|
21
|
-
|
|
22
|
-
### Element Extensions
|
|
23
|
-
|
|
24
|
-
| API | Type | Description |
|
|
25
|
-
|-----|------|-------------|
|
|
26
|
-
| `ElementBounds` | interface | Element bounds info (`target`, `top`, `left`, `width`, `height`) |
|
|
27
|
-
| `Element.findAll` | prototype method | Find all child elements matching a CSS selector |
|
|
28
|
-
| `Element.findFirst` | prototype method | Find first element matching a CSS selector |
|
|
29
|
-
| `Element.prependChild` | prototype method | Insert element as first child |
|
|
30
|
-
| `Element.getParents` | prototype method | Get all parent elements (closest to farthest) |
|
|
31
|
-
| `Element.findFocusableParent` | prototype method | Find first focusable parent element |
|
|
32
|
-
| `Element.findFirstFocusableChild` | prototype method | Find first focusable child element |
|
|
33
|
-
| `Element.isOffsetElement` | prototype method | Check if element has offset positioning |
|
|
34
|
-
| `Element.isVisible` | prototype method | Check if element is visible on screen |
|
|
35
|
-
| `copyElement` | function | Copy element content to clipboard via ClipboardEvent |
|
|
36
|
-
| `pasteToElement` | function | Paste clipboard content to element via ClipboardEvent |
|
|
37
|
-
| `getBounds` | function | Get bounds for multiple elements using IntersectionObserver |
|
|
38
|
-
|
|
39
|
-
-> See [docs/element-extensions.md](./docs/element-extensions.md) for details.
|
|
40
|
-
|
|
41
|
-
### HTMLElement Extensions
|
|
42
|
-
|
|
43
|
-
| API | Type | Description |
|
|
44
|
-
|-----|------|-------------|
|
|
45
|
-
| `HTMLElement.repaint` | prototype method | Force repaint (triggers reflow) |
|
|
46
|
-
| `HTMLElement.getRelativeOffset` | prototype method | Calculate position relative to a parent element |
|
|
47
|
-
| `HTMLElement.scrollIntoViewIfNeeded` | prototype method | Scroll to make target visible if obscured |
|
|
48
|
-
|
|
49
|
-
-> See [docs/html-element-extensions.md](./docs/html-element-extensions.md) for details.
|
|
50
|
-
|
|
51
|
-
### Utilities
|
|
52
|
-
|
|
53
|
-
| API | Type | Description |
|
|
54
|
-
|-----|------|-------------|
|
|
55
|
-
| `downloadBlob` | function | Download a Blob as a file |
|
|
56
|
-
| `DownloadProgress` | interface | Download progress info (`receivedLength`, `contentLength`) |
|
|
57
|
-
| `fetchUrlBytes` | function | Download binary data from URL with progress callback |
|
|
58
|
-
| `openFileDialog` | function | Programmatically open file selection dialog |
|
|
59
|
-
|
|
60
|
-
-> See [docs/utilities.md](./docs/utilities.md) for details.
|
|
61
|
-
|
|
62
|
-
### Classes
|
|
63
|
-
|
|
64
|
-
| API | Type | Description |
|
|
65
|
-
|-----|------|-------------|
|
|
66
|
-
| `StoreConfig` | interface | IndexedDB store configuration (`name`, `keyPath`) |
|
|
67
|
-
| `IndexedDbStore` | class | IndexedDB wrapper for key-value storage |
|
|
68
|
-
| `VirtualFsEntry` | interface | Virtual file system entry (`kind`, `dataBase64`) |
|
|
69
|
-
| `IndexedDbVirtualFs` | class | IndexedDB-backed virtual file system |
|
|
70
|
-
|
|
71
|
-
-> See [docs/classes.md](./docs/classes.md) for details.
|
|
72
|
-
|
|
73
|
-
## Usage Examples
|
|
74
|
-
|
|
75
|
-
### Download a file
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
import { downloadBlob } from "@simplysm/core-browser";
|
|
79
|
-
|
|
80
|
-
const blob = new Blob(["Hello"], { type: "text/plain" });
|
|
81
|
-
downloadBlob(blob, "hello.txt");
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Open file dialog and read files
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
import { openFileDialog } from "@simplysm/core-browser";
|
|
88
|
-
|
|
89
|
-
const files = await openFileDialog({ accept: ".csv", multiple: true });
|
|
90
|
-
if (files) {
|
|
91
|
-
for (const file of files) {
|
|
92
|
-
const text = await file.text();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Use IndexedDB store
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
import { IndexedDbStore } from "@simplysm/core-browser";
|
|
101
|
-
|
|
102
|
-
const store = new IndexedDbStore("myApp", 1, [{ name: "settings", keyPath: "key" }]);
|
|
103
|
-
await store.put("settings", { key: "theme", value: "dark" });
|
|
104
|
-
const item = await store.get<{ key: string; value: string }>("settings", "theme");
|
|
105
|
-
store.close();
|
|
106
|
-
```
|