@topgrid/grid-pro-range 0.1.0
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 +98 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +390 -0
- package/dist/index.d.ts +390 -0
- package/dist/index.mjs +8 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as _tanstack_react_table from '@tanstack/react-table';
|
|
2
|
+
import { ColumnDef, Table } from '@tanstack/react-table';
|
|
3
|
+
import { RefObject, ReactElement } from 'react';
|
|
4
|
+
|
|
5
|
+
/** 2D 셀 좌표 (0-based, row/col index). */
|
|
6
|
+
interface CellCoord {
|
|
7
|
+
row: number;
|
|
8
|
+
col: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 직사각형 셀 범위.
|
|
12
|
+
* start/end는 정규화 전 임의 방향 허용 — normalizeRange로 정규화.
|
|
13
|
+
*/
|
|
14
|
+
interface CellRange {
|
|
15
|
+
start: CellCoord;
|
|
16
|
+
end: CellCoord;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* RangeSelectGrid props (L0 backward-compat 포함 — AC-010).
|
|
20
|
+
*
|
|
21
|
+
* C-29 (exactOptionalPropertyTypes): optional 필드는 '?: T' 선언.
|
|
22
|
+
* 전달 시 spread-skip 패턴 사용 (Section 6.6).
|
|
23
|
+
*/
|
|
24
|
+
interface RangeSelectGridProps<TData extends object> {
|
|
25
|
+
data: TData[];
|
|
26
|
+
columns: ColumnDef<TData>[];
|
|
27
|
+
onRangeChange?: (range: CellRange | null) => void;
|
|
28
|
+
loading?: boolean;
|
|
29
|
+
emptyText?: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
/** 채우기 방향 (Excel 4방향, D5). */
|
|
33
|
+
type FillDirection = 'up' | 'down' | 'left' | 'right';
|
|
34
|
+
/**
|
|
35
|
+
* 단일 셀 업데이트 단위.
|
|
36
|
+
* 제네릭 <TCell>으로 any 미사용 (AC-001, AC-002).
|
|
37
|
+
*/
|
|
38
|
+
interface CellUpdate<TCell = unknown> {
|
|
39
|
+
row: number;
|
|
40
|
+
col: number;
|
|
41
|
+
value: TCell;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* DragFillHandle 컴포넌트 Props (AC-003).
|
|
45
|
+
*
|
|
46
|
+
* C-29 (exactOptionalPropertyTypes): optional 필드는 '?: T' 선언.
|
|
47
|
+
* 전달 시 spread-skip 패턴 사용 (spec Section 4.4).
|
|
48
|
+
*/
|
|
49
|
+
interface DragFillHandleProps<TCell = unknown> {
|
|
50
|
+
/** 현재 선택된 소스 범위 (G-001 CellRange). null이면 핸들 미표시. */
|
|
51
|
+
range: CellRange | null;
|
|
52
|
+
/** 소스 셀 값 getter — 드래그 시 fill 계산용. */
|
|
53
|
+
getCellValue: (row: number, col: number) => TCell;
|
|
54
|
+
/** 채우기 완료 콜백 (D3 MOD-GRID-10 분리). */
|
|
55
|
+
onFillComplete?: (cells: CellUpdate<TCell>[]) => void;
|
|
56
|
+
/** 드래그 중 fill target 범위 변경 알림 (시각적 점선 outline용). */
|
|
57
|
+
onFillTargetChange?: (target: CellRange | null) => void;
|
|
58
|
+
/** 그리드 전체 행 수 (경계 clamp). */
|
|
59
|
+
rowCount: number;
|
|
60
|
+
/** 그리드 전체 열 수 (경계 clamp). */
|
|
61
|
+
colCount: number;
|
|
62
|
+
/** 핸들이 렌더링될 컨테이너 ref (좌표 계산). */
|
|
63
|
+
containerRef: RefObject<HTMLElement>;
|
|
64
|
+
/** 셀 크기 getter (px) — 드래그 위치 → cell coord 변환용. */
|
|
65
|
+
getCellRect: (row: number, col: number) => {
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
width: number;
|
|
69
|
+
height: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 붙여넣기 결과 메타정보 (AC-002 보완 — D8).
|
|
74
|
+
* cells: 파싱된 CellUpdate 배열 (onPaste callback에 전달).
|
|
75
|
+
* truncated: true이면 grid 경계 초과로 일부 셀 클램프됨.
|
|
76
|
+
* rows: TSV 파싱 행 수.
|
|
77
|
+
* cols: TSV 파싱 열 수.
|
|
78
|
+
*/
|
|
79
|
+
interface PasteResult<TCell = unknown> {
|
|
80
|
+
cells: CellUpdate<TCell>[];
|
|
81
|
+
truncated: boolean;
|
|
82
|
+
rows: number;
|
|
83
|
+
cols: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* useClipboard hook props.
|
|
87
|
+
*
|
|
88
|
+
* C-29 (exactOptionalPropertyTypes): optional 필드는 '?: T' 선언.
|
|
89
|
+
* 전달 시 spread-skip 패턴 사용 (spec Section 3.4 예시 참조).
|
|
90
|
+
*/
|
|
91
|
+
interface UseClipboardProps<TData, TCell = unknown> {
|
|
92
|
+
/** 현재 선택 범위 (useCellRange의 range). null이면 Ctrl+C no-op. */
|
|
93
|
+
selection: CellRange | null;
|
|
94
|
+
/** 현재 활성 셀 좌표 (useKeyboardNav의 activeCell). null이면 Ctrl+V no-op. */
|
|
95
|
+
activeCell: CellCoord | null;
|
|
96
|
+
/** 그리드 전체 행 수 (경계 clamp). */
|
|
97
|
+
rowCount: number;
|
|
98
|
+
/** 그리드 전체 열 수 (경계 clamp). */
|
|
99
|
+
colCount: number;
|
|
100
|
+
/** 셀 값 getter — 복사 시 매트릭스 추출용. */
|
|
101
|
+
getCellValue: (row: number, col: number) => TCell;
|
|
102
|
+
/** 붙여넣기 결과 콜백 (D3 MOD-GRID-10 분리). 미제공 시 붙여넣기 파싱만 수행. */
|
|
103
|
+
onPaste?: (cells: CellUpdate<TCell>[]) => void;
|
|
104
|
+
/** 클립보드 API 에러 핸들러 (권한 거부 등). */
|
|
105
|
+
onError?: (error: Error) => void;
|
|
106
|
+
/** TanStack Table 인스턴스 — 사용 안 함, 향후 확장용 optional. */
|
|
107
|
+
table?: _tanstack_react_table.Table<TData>;
|
|
108
|
+
}
|
|
109
|
+
/** useClipboard hook 반환 타입. */
|
|
110
|
+
interface UseClipboardReturn {
|
|
111
|
+
/**
|
|
112
|
+
* Grid container에 부착할 keydown 핸들러 (D7).
|
|
113
|
+
* Ctrl+C → copyToClipboard, Ctrl+V → pasteFromClipboard 호출.
|
|
114
|
+
* G-002 useKeyboardNav.handleKeyDown과 합성하여 사용.
|
|
115
|
+
*/
|
|
116
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
117
|
+
/** Ctrl+C 프로그래매틱 복사. navigator.clipboard 비동기. */
|
|
118
|
+
copyToClipboard: () => Promise<void>;
|
|
119
|
+
/** Ctrl+V 프로그래매틱 붙여넣기. 명시적 tsvString 주입 가능 (Storybook/테스트용). */
|
|
120
|
+
pasteFromClipboard: (tsvString?: string) => Promise<PasteResult>;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* useKeyboardEdit hook props.
|
|
124
|
+
*
|
|
125
|
+
* C-29 (exactOptionalPropertyTypes): optional 필드는 '?: T' 선언.
|
|
126
|
+
* 전달 시 spread-skip 패턴 사용 (spec Section 10.1 예시 참조).
|
|
127
|
+
*/
|
|
128
|
+
interface UseKeyboardEditProps<TData, TCell = unknown> {
|
|
129
|
+
/** 현재 선택 범위 (useCellRange의 range). null이면 Delete/printable no-op. */
|
|
130
|
+
selection: CellRange | null;
|
|
131
|
+
/** 현재 활성 셀 좌표 (useKeyboardNav의 activeCell). null이면 F2/Enter no-op. */
|
|
132
|
+
activeCell: CellCoord | null;
|
|
133
|
+
/** 컬럼 편집 가능 여부 판별 함수. 미제공 시 모든 컬럼 편집 가능으로 취급. */
|
|
134
|
+
isEditableColumn?: (colIndex: number) => boolean;
|
|
135
|
+
/** Delete 키 범위 삭제 callback (D3 MOD-GRID-10 분리). */
|
|
136
|
+
onDeleteRange?: (cells: CellCoord[]) => void;
|
|
137
|
+
/** 범위 일괄 입력 callback (D3 MOD-GRID-10 분리). */
|
|
138
|
+
onBulkEdit?: (cells: CellCoord[], value: TCell) => void;
|
|
139
|
+
/** F2/Enter 단일 셀 편집 시작 callback (D4 MOD-GRID-05 분리). */
|
|
140
|
+
onEditStart?: (cell: CellCoord, initialValue?: TCell) => void;
|
|
141
|
+
/** TanStack Table 인스턴스 — 향후 확장용 optional. */
|
|
142
|
+
table?: _tanstack_react_table.Table<TData>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* useKeyboardEdit hook 반환 타입.
|
|
146
|
+
*/
|
|
147
|
+
interface UseKeyboardEditReturn {
|
|
148
|
+
/**
|
|
149
|
+
* Grid container에 부착할 keydown 핸들러 (D7).
|
|
150
|
+
* G-002 handleKeyDown / G-004 onKeyDown과 컴포저블 결합.
|
|
151
|
+
* Caller는 G-005 onKeyDown을 체인 앞에 배치 (D5 Enter 우선순위).
|
|
152
|
+
*/
|
|
153
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* G-006 확장 props — G-001 6-prop 유지 + 5개 enable 플래그 + 7개 callback (AC-001, C-6, C-29).
|
|
157
|
+
*
|
|
158
|
+
* enable* 플래그 설계 원칙 (D4, D5):
|
|
159
|
+
* - 모든 hook은 무조건 호출 (Rules of Hooks 준수)
|
|
160
|
+
* - enable* = false → hook 내부 early return (동작 게이팅)
|
|
161
|
+
* - DragFillHandle: 컴포넌트이므로 조건부 렌더 허용
|
|
162
|
+
*/
|
|
163
|
+
interface RangeSelectGridAllProps<TData extends object, TCell = unknown> extends RangeSelectGridProps<TData> {
|
|
164
|
+
/** 마우스 드래그 / Shift+Click 범위 선택 (default: true). */
|
|
165
|
+
enableRangeSelection?: boolean;
|
|
166
|
+
/** Arrow/Ctrl+Arrow 키보드 내비게이션 (default: true). */
|
|
167
|
+
enableKeyboardNav?: boolean;
|
|
168
|
+
/** Drag-fill 핸들 렌더링 + 채우기 기능 (default: false). */
|
|
169
|
+
enableDragFill?: boolean;
|
|
170
|
+
/** Ctrl+C/V 클립보드 (default: false). */
|
|
171
|
+
enableClipboard?: boolean;
|
|
172
|
+
/** Delete/F2/Enter/printable key 편집 트리거 (default: false). */
|
|
173
|
+
enableKeyboardEdit?: boolean;
|
|
174
|
+
/** @tanstack/react-virtual 가상화 (default: false, C-18). */
|
|
175
|
+
enableVirtualization?: boolean;
|
|
176
|
+
/** 셀 값 getter — drag-fill 계산 + clipboard 복사용 (AC-001). */
|
|
177
|
+
getCellValue?: (row: number, col: number) => TCell;
|
|
178
|
+
/** Drag-fill 완료 콜백 (D3 MOD-GRID-10 분리). */
|
|
179
|
+
onFillComplete?: (cells: CellUpdate<TCell>[]) => void;
|
|
180
|
+
/** Drag-fill target 범위 변경 알림 (점선 outline). */
|
|
181
|
+
onFillTargetChange?: (target: CellRange | null) => void;
|
|
182
|
+
/** 붙여넣기 결과 콜백 (D3 MOD-GRID-10 분리). */
|
|
183
|
+
onPaste?: (cells: CellUpdate<TCell>[]) => void;
|
|
184
|
+
/** 클립보드 API 에러 핸들러 (권한 거부 등). */
|
|
185
|
+
onClipboardError?: (error: Error) => void;
|
|
186
|
+
/** 컬럼 편집 가능 여부 판별. 미제공 시 전체 편집 가능. */
|
|
187
|
+
isEditableColumn?: (colIndex: number) => boolean;
|
|
188
|
+
/** Delete 키 범위 삭제 콜백 (D3 MOD-GRID-10 분리). */
|
|
189
|
+
onDeleteRange?: (cells: CellCoord[]) => void;
|
|
190
|
+
/** 범위 일괄 입력 콜백 (D3 MOD-GRID-10 분리). */
|
|
191
|
+
onBulkEdit?: (cells: CellCoord[], value: TCell) => void;
|
|
192
|
+
/** F2/Enter 단일 셀 편집 시작 콜백 (D4 MOD-GRID-05 분리). */
|
|
193
|
+
onEditStart?: (cell: CellCoord, initialValue?: TCell) => void;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @topgrid/grid-pro-range — 순수 범위 유틸 함수 (AC-002).
|
|
198
|
+
*
|
|
199
|
+
* normalizeRange: start ≤ end 방향으로 정규화 (역방향 드래그 지원).
|
|
200
|
+
* isInRange: 좌표가 범위 내 포함 여부 판별.
|
|
201
|
+
*
|
|
202
|
+
* 두 함수 모두 부수효과 없음 — 순수 함수 (pure functions).
|
|
203
|
+
*/
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* CellRange를 start ≤ end 방향으로 정규화.
|
|
207
|
+
*
|
|
208
|
+
* 드래그 방향과 무관하게 항상 정규화된 범위를 반환.
|
|
209
|
+
* 역방향(end < start) 입력도 올바르게 처리.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* normalizeRange({ start: {row:3, col:2}, end: {row:0, col:0} })
|
|
213
|
+
* // → { start: {row:0, col:0}, end: {row:3, col:2} }
|
|
214
|
+
*/
|
|
215
|
+
declare function normalizeRange(range: CellRange): CellRange;
|
|
216
|
+
/**
|
|
217
|
+
* 주어진 좌표(row, col)가 범위 내에 포함되는지 판별.
|
|
218
|
+
*
|
|
219
|
+
* range가 null이면 항상 false 반환.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* isInRange(1, 1, { start: {row:0,col:0}, end: {row:2,col:2} }) // → true
|
|
223
|
+
* isInRange(3, 3, { start: {row:0,col:0}, end: {row:2,col:2} }) // → false
|
|
224
|
+
* isInRange(0, 0, null) // → false
|
|
225
|
+
*/
|
|
226
|
+
declare function isInRange(row: number, col: number, range: CellRange | null): boolean;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @topgrid/grid-pro-range — Drag-fill pure functions (G-003).
|
|
230
|
+
*
|
|
231
|
+
* fillRange: 소스 범위 값을 지정 방향·개수만큼 CellUpdate 배열로 생성.
|
|
232
|
+
* detectSeriesStep: 숫자 배열에서 등차 step 감지.
|
|
233
|
+
*
|
|
234
|
+
* 두 함수 모두 부수효과 없음 — 순수 함수 (pure functions).
|
|
235
|
+
* 제네릭 <TCell> 사용 — any 미사용 (AC-001, AC-002).
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 숫자 배열에서 등차 step 감지.
|
|
240
|
+
* 요소가 1개이면 step = 0 (단순 복사).
|
|
241
|
+
* 요소가 2개 이상이고 모두 step 동일하면 해당 step 반환.
|
|
242
|
+
* step 불일치 시 null 반환 (단순 복사 모드).
|
|
243
|
+
*/
|
|
244
|
+
declare function detectSeriesStep(values: number[]): number | null;
|
|
245
|
+
/**
|
|
246
|
+
* 소스 범위 값을 채울 방향·개수만큼 CellUpdate 배열 생성.
|
|
247
|
+
* 제네릭 <TCell> — any 미사용 (AC-002).
|
|
248
|
+
*
|
|
249
|
+
* @param sourceRange 소스 CellRange (G-001 normalizeRange 보장된 값)
|
|
250
|
+
* @param direction 채울 방향 (FillDirection)
|
|
251
|
+
* @param fillCount 채울 셀 개수
|
|
252
|
+
* @param getCellValue 소스 셀 값 getter
|
|
253
|
+
*/
|
|
254
|
+
declare function fillRange<TCell>(sourceRange: CellRange, direction: FillDirection, fillCount: number, getCellValue: (row: number, col: number) => TCell): CellUpdate<TCell>[];
|
|
255
|
+
|
|
256
|
+
/** useCellRange 훅 반환 타입 (AC-003, AC-004, AC-006). */
|
|
257
|
+
interface UseCellRangeReturn {
|
|
258
|
+
/** 현재 선택된 셀 범위. 선택 없으면 null. */
|
|
259
|
+
range: CellRange | null;
|
|
260
|
+
/** 드래그 중 여부. */
|
|
261
|
+
dragging: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* 셀 mousedown 핸들러.
|
|
264
|
+
* @param row 0-based 행 인덱스
|
|
265
|
+
* @param col 0-based 열 인덱스
|
|
266
|
+
* @param shiftKey Shift 키 눌림 여부
|
|
267
|
+
*/
|
|
268
|
+
handleMouseDown: (row: number, col: number, shiftKey: boolean) => void;
|
|
269
|
+
/**
|
|
270
|
+
* 셀 mouseenter 핸들러 (드래그 범위 확장).
|
|
271
|
+
* @param row 0-based 행 인덱스
|
|
272
|
+
* @param col 0-based 열 인덱스
|
|
273
|
+
*/
|
|
274
|
+
handleMouseEnter: (row: number, col: number) => void;
|
|
275
|
+
/** mouseup 핸들러 (드래그 종료). */
|
|
276
|
+
handleMouseUp: () => void;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 마우스 드래그/Shift+Click 셀 범위 선택 훅.
|
|
280
|
+
*
|
|
281
|
+
* @param onRangeChange 범위 변경 시 호출되는 콜백 (AC-006).
|
|
282
|
+
* @returns 범위 state + 이벤트 핸들러 3종.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```tsx
|
|
286
|
+
* const { range, handleMouseDown, handleMouseEnter, handleMouseUp } =
|
|
287
|
+
* useCellRange((r) => console.log('range changed:', r));
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
declare function useCellRange(onRangeChange?: (range: CellRange | null) => void): UseCellRangeReturn;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* RangeSelectGrid — G-006 5-hook 완전 통합 (AC-001, D4, D5, D9).
|
|
294
|
+
*
|
|
295
|
+
* D5 Rules of Hooks: 5개 hook 전부 무조건 호출.
|
|
296
|
+
* D4 enable* = behavior gate (not hook invocation gate).
|
|
297
|
+
* D9 onKeyDown 합성: editKeyDown → navKeyDown → clipKeyDown.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```tsx
|
|
301
|
+
* // v0.1.x 그대로 동작 (C-6 backward compat)
|
|
302
|
+
* <RangeSelectGrid data={rows} columns={columns} />
|
|
303
|
+
*
|
|
304
|
+
* // v0.2.0 — Drag-fill + Clipboard 활성화
|
|
305
|
+
* <RangeSelectGrid<MyData, string>
|
|
306
|
+
* data={data}
|
|
307
|
+
* columns={columns}
|
|
308
|
+
* enableDragFill
|
|
309
|
+
* enableClipboard
|
|
310
|
+
* getCellValue={(row, col) => getValue(row, col)}
|
|
311
|
+
* onFillComplete={(cells) => apply(cells)}
|
|
312
|
+
* onPaste={(cells) => apply(cells)}
|
|
313
|
+
* />
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
declare function RangeSelectGrid<TData extends object, TCell = unknown>(props: RangeSelectGridAllProps<TData, TCell>): React.ReactElement;
|
|
317
|
+
|
|
318
|
+
interface UseKeyboardNavOptions<TData> {
|
|
319
|
+
/** TanStack table 인스턴스 (경계 계산용 — D5). */
|
|
320
|
+
table: Table<TData>;
|
|
321
|
+
/** 현재 활성 셀 좌표 (controlled). */
|
|
322
|
+
activeCell: CellCoord | null;
|
|
323
|
+
/** 활성 셀 변경 콜백. */
|
|
324
|
+
onActiveCellChange: (cell: CellCoord) => void;
|
|
325
|
+
/** 현재 선택 범위 (useCellRange에서 수신 — D4 controlled). */
|
|
326
|
+
range: CellRange | null;
|
|
327
|
+
/** 범위 변경 콜백 (useCellRange의 onRangeChange와 동일 시그니처 — D4). */
|
|
328
|
+
onRangeChange: (range: CellRange | null) => void;
|
|
329
|
+
/** Ctrl+Arrow data-edge 탐색 함수 (선택적). */
|
|
330
|
+
getCellValue?: (row: number, col: number) => unknown;
|
|
331
|
+
}
|
|
332
|
+
interface UseKeyboardNavReturn {
|
|
333
|
+
/** Grid container에 부착할 keydown 핸들러 (D3). */
|
|
334
|
+
handleKeyDown: (e: React.KeyboardEvent) => void;
|
|
335
|
+
}
|
|
336
|
+
declare function useKeyboardNav<TData>(options: UseKeyboardNavOptions<TData>): UseKeyboardNavReturn;
|
|
337
|
+
|
|
338
|
+
declare function DragFillHandle<TCell = unknown>({ range, getCellValue, onFillComplete, onFillTargetChange, rowCount, colCount, containerRef, getCellRect, }: DragFillHandleProps<TCell>): ReactElement | null;
|
|
339
|
+
|
|
340
|
+
declare function useClipboard<TData, TCell = unknown>(props: UseClipboardProps<TData, TCell>): UseClipboardReturn;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @topgrid/grid-pro-range — RFC 4180 호환 TSV 유틸리티 (G-004, AC-003).
|
|
344
|
+
*
|
|
345
|
+
* stringifyTsv: 2D 값 배열 → TSV 문자열 (탭/줄바꿈 포함 셀은 " 래핑)
|
|
346
|
+
* parseTsv: TSV 문자열 → 2D string 배열
|
|
347
|
+
*
|
|
348
|
+
* grid-export/copyToClipboard.ts의 공백 치환 escapeTsvValue와 다른 전략:
|
|
349
|
+
* - grid-export: 행 단위 복사, 특수문자 → 공백 (D6 역할 분리)
|
|
350
|
+
* - tsvUtils: 셀 범위 단위 복사, RFC 4180 완전 보존
|
|
351
|
+
*
|
|
352
|
+
* 두 함수 모두 부수효과 없음 — 순수 함수 (pure functions).
|
|
353
|
+
*/
|
|
354
|
+
/**
|
|
355
|
+
* 2D 값 배열을 RFC 4180 호환 TSV 문자열로 직렬화.
|
|
356
|
+
* 행 구분: \n, 셀 구분: \t.
|
|
357
|
+
*
|
|
358
|
+
* @param matrix row-major 2D 배열. 빈 배열이면 빈 문자열 반환.
|
|
359
|
+
*/
|
|
360
|
+
declare function stringifyTsv(matrix: readonly (readonly unknown[])[]): string;
|
|
361
|
+
/**
|
|
362
|
+
* RFC 4180 호환 TSV 문자열을 2D string 배열로 파싱.
|
|
363
|
+
* - " 래핑 셀: 언래핑 + "" → " 복원
|
|
364
|
+
* - 빈 문자열 또는 공백만: [['']] 반환
|
|
365
|
+
*
|
|
366
|
+
* @param tsv TSV 문자열 (Excel 복사 형식 포함).
|
|
367
|
+
* @returns row-major 2D string 배열. 행 수 = rows, 최대 열 수 = cols.
|
|
368
|
+
*/
|
|
369
|
+
declare function parseTsv(tsv: string): string[][];
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* useKeyboardEdit — Delete/F2/Enter/printable key 분기 hook.
|
|
373
|
+
*
|
|
374
|
+
* @returns `{ onKeyDown }` — Grid container에 부착할 keydown 핸들러 (D7).
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```tsx
|
|
378
|
+
* const { onKeyDown: editKeyDown } = useKeyboardEdit({ selection, activeCell, ... });
|
|
379
|
+
* // D7: G-005 앞에 배치 (D5 Enter 우선순위)
|
|
380
|
+
* const onKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
381
|
+
* editKeyDown(e);
|
|
382
|
+
* if (e.defaultPrevented) return;
|
|
383
|
+
* navKeyDown(e); // G-002
|
|
384
|
+
* clipKeyDown(e); // G-004
|
|
385
|
+
* }, [editKeyDown, navKeyDown, clipKeyDown]);
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
declare function useKeyboardEdit<TData, TCell = unknown>(props: UseKeyboardEditProps<TData, TCell>): UseKeyboardEditReturn;
|
|
389
|
+
|
|
390
|
+
export { type CellCoord, type CellRange, type CellUpdate, DragFillHandle, type DragFillHandleProps, type FillDirection, type PasteResult, RangeSelectGrid, type RangeSelectGridAllProps, type RangeSelectGridProps, type UseCellRangeReturn, type UseClipboardProps, type UseClipboardReturn, type UseKeyboardEditProps, type UseKeyboardEditReturn, type UseKeyboardNavOptions, type UseKeyboardNavReturn, detectSeriesStep, fillRange, isInRange, normalizeRange, parseTsv, stringifyTsv, useCellRange, useClipboard, useKeyboardEdit, useKeyboardNav };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {checkLicense,useLicenseStatus,Watermark}from'@topgrid/grid-license';import {useState,useRef,useCallback,useEffect}from'react';import {useReactTable,getSortedRowModel,getCoreRowModel,flexRender}from'@tanstack/react-table';import {useVirtualizer}from'@tanstack/react-virtual';import {jsx,jsxs}from'react/jsx-runtime';function K(e){return {start:{row:Math.min(e.start.row,e.end.row),col:Math.min(e.start.col,e.end.col)},end:{row:Math.max(e.start.row,e.end.row),col:Math.max(e.start.col,e.end.col)}}}function $(e,l,o){if(!o)return false;let s=K(o);return e>=s.start.row&&e<=s.end.row&&l>=s.start.col&&l<=s.end.col}function fe(e){if(e.length<2)return 0;let l=e[1]-e[0];for(let o=2;o<e.length;o++)if(e[o]-e[o-1]!==l)return null;return l}function O(e,l,o,s){if(o<=0)return [];let{start:u,end:d}=e,c=[],r=[];for(let i=u.row;i<=d.row;i++)c.push(i);for(let i=u.col;i<=d.col;i++)r.push(i);let m=c.map(i=>r.map(n=>s(i,n))),t=[];if(l==="down"){let i=r.length;for(let n=0;n<o;n++){let a=d.row+1+n;for(let f=0;f<i;f++){let v=c.map((g,C)=>m[C][f]);t.push({row:a,col:r[f],value:z(v,n+1)});}}}else if(l==="up"){let i=r.length;for(let n=0;n<o;n++){let a=u.row-o+n;for(let f=0;f<i;f++){let v=c.map((g,C)=>m[C][f]).reverse();t.push({row:a,col:r[f],value:z(v,o-n)});}}}else if(l==="right"){let i=c.length;for(let n=0;n<o;n++){let a=d.col+1+n;for(let f=0;f<i;f++){let v=r.map((g,C)=>m[f][C]);t.push({row:c[f],col:a,value:z(v,n+1)});}}}else {let i=c.length;for(let n=0;n<o;n++){let a=u.col-o+n;for(let f=0;f<i;f++){let v=r.map((g,C)=>m[f][C]).reverse();t.push({row:c[f],col:a,value:z(v,o-n)});}}}return t}function z(e,l){if(e.every(s=>typeof s=="number")&&e.length>0){let s=e,u=fe(s);if(u!==null)return s[s.length-1]+u*l}return e[l%e.length]}function j(e){let[l,o]=useState(null),[s,u]=useState(false),d=useRef(null),c=useCallback((t,i,n)=>{if(n&&l){let a=K({start:l.start,end:{row:t,col:i}});o(a),e?.(a);}else {d.current={row:t,col:i},u(true);let a={start:{row:t,col:i},end:{row:t,col:i}};o(a),e?.(a);}},[l,e]),r=useCallback((t,i)=>{if(!s||!d.current)return;let n=K({start:d.current,end:{row:t,col:i}});o(n),e?.(n);},[s,e]),m=useCallback(()=>{u(false);},[]);return {range:l,dragging:s,handleMouseDown:c,handleMouseEnter:r,handleMouseUp:m}}function q(e){let{table:l,activeCell:o,onActiveCellChange:s,range:u,onRangeChange:d,getCellValue:c}=e,r=useRef(null);return {handleKeyDown:useCallback(t=>{let i=l.getRowModel().rows.length,n=l.getAllColumns().filter(T=>T.getIsVisible()).length;if(i===0||n===0)return;let a=o??{row:0,col:0},f=T=>Math.max(0,Math.min(T,i-1)),v=T=>Math.max(0,Math.min(T,n-1)),g=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key),C=t.key==="Tab",y=t.key==="Enter";if(!(!g&&!C&&!y))if(t.preventDefault(),g){let p={ArrowUp:{row:-1,col:0},ArrowDown:{row:1,col:0},ArrowLeft:{row:0,col:-1},ArrowRight:{row:0,col:1}}[t.key];if(p===void 0)return;if(t.shiftKey){r.current===null&&(r.current=u?.start??a);let b=r.current,h=u?.end??a,w;t.ctrlKey?w=me(h,t.key,i,n,c):w={row:f(h.row+p.row),col:v(h.col+p.col)};let R=K({start:b,end:w});d(R),s(w);}else if(r.current=null,d(null),t.ctrlKey){let b=me(a,t.key,i,n,c);s(b);}else s({row:f(a.row+p.row),col:v(a.col+p.col)});}else C?(r.current=null,d(null),t.shiftKey?a.col>0?s({row:a.row,col:a.col-1}):a.row>0&&s({row:a.row-1,col:n-1}):a.col<n-1?s({row:a.row,col:a.col+1}):a.row<i-1&&s({row:a.row+1,col:0})):y&&(r.current=null,d(null),a.row<i-1&&s({row:a.row+1,col:a.col}));},[l,o,s,u,d,c])}}function me(e,l,o,s,u){if(u===void 0)switch(l){case "ArrowUp":return {row:0,col:e.col};case "ArrowDown":return {row:o-1,col:e.col};case "ArrowLeft":return {row:e.row,col:0};case "ArrowRight":return {row:e.row,col:s-1};default:return e}let{row:d,col:c}=e,r=0,m=0;switch(l){case "ArrowUp":r=-1;break;case "ArrowDown":r=1;break;case "ArrowLeft":m=-1;break;case "ArrowRight":m=1;break;default:return e}for(;d+r>=0&&d+r<o&&c+m>=0&&c+m<s;){let t=u(d+r,c+m);if(t==null||t==="")break;d+=r,c+=m;}return {row:d,col:c}}function Me(e){return e.includes(" ")||e.includes(`
|
|
2
|
+
`)||e.includes("\r")||e.includes('"')?'"'+e.replace(/"/g,'""')+'"':e}function X(e){return e.length===0?"":e.map(l=>l.map(o=>Me(o==null?"":String(o))).join(" ")).join(`
|
|
3
|
+
`)}function Y(e){if(e.trim()==="")return [[""]];let l=e.replace(/\r\n/g,`
|
|
4
|
+
`).replace(/\r/g,`
|
|
5
|
+
`),o=l.endsWith(`
|
|
6
|
+
`)?l.slice(0,-1):l,s=[],u=o.split(`
|
|
7
|
+
`);for(let d=0;d<u.length;d++){let c=[],r=u[d],m=0;for(;r.length>0||m===0;){if(r.startsWith('"')){let t=1,i="";for(;t<r.length;)if(r[t]==='"')if(t+1<r.length&&r[t+1]==='"')i+='"',t+=2;else {t++;break}else i+=r[t],t++;if(c.push(i),r=r.slice(t),r.startsWith(" "))r=r.slice(1);else break}else {let t=r.indexOf(" ");if(t===-1){c.push(r),r="";break}else c.push(r.slice(0,t)),r=r.slice(t+1);}m++;}s.push(c);}return s}function Q(e){let{selection:l,activeCell:o,rowCount:s,colCount:u,getCellValue:d,onPaste:c,onError:r}=e,m=useCallback(async()=>{if(l===null)return;let{start:n,end:a}=l,f=[];for(let g=n.row;g<=a.row;g++){let C=[];for(let y=n.col;y<=a.col;y++)C.push(d(g,y));f.push(C);}let v=X(f);try{if(typeof navigator<"u"&&navigator.clipboard&&typeof navigator.clipboard.writeText=="function")await navigator.clipboard.writeText(v);else {let g=document.createElement("textarea");g.value=v,g.style.cssText="position:fixed;opacity:0",document.body.appendChild(g),g.select();let C=document.execCommand("copy");if(document.body.removeChild(g),!C)throw new Error("[grid-pro-range] copyToClipboard: Clipboard API not supported")}}catch(g){let C=g instanceof Error?g:new Error(String(g));r!==void 0?r(C):console.warn("[grid-pro-range] copyToClipboard error:",C.message);}},[l,d,r]),t=useCallback(async n=>{if(o===null)return {cells:[],truncated:false,rows:0,cols:0};let a;if(n!==void 0)a=n;else try{if(typeof navigator<"u"&&navigator.clipboard&&typeof navigator.clipboard.readText=="function")a=await navigator.clipboard.readText();else {let p=new Error("[grid-pro-range] pasteFromClipboard: Clipboard read API not supported");return r!==void 0?r(p):console.warn(p.message),{cells:[],truncated:!1,rows:0,cols:0}}}catch(p){let b=p instanceof Error?p:new Error(String(p));return r!==void 0?r(b):console.warn("[grid-pro-range] pasteFromClipboard error:",b.message),{cells:[],truncated:false,rows:0,cols:0}}if(a.trim()==="")return {cells:[],truncated:false,rows:0,cols:0};let f=Y(a),v=f.length,g=Math.max(...f.map(p=>p.length),0),C=[],y=false;for(let p=0;p<v;p++){let b=o.row+p;if(b>=s){y=true;break}let h=f[p];for(let w=0;w<h.length;w++){let R=o.col+w;if(R>=u){y=true;continue}C.push({row:b,col:R,value:h[w]});}}let T={cells:C,truncated:y,rows:v,cols:g};return C.length>0&&c!==void 0&&c(C),T},[o,s,u,c,r]);return {onKeyDown:useCallback(n=>{let a=(n.ctrlKey||n.metaKey)&&n.key==="c",f=(n.ctrlKey||n.metaKey)&&n.key==="v";!a&&!f||(n.preventDefault(),a?m():t());},[m,t]),copyToClipboard:m,pasteFromClipboard:t}}function we(e){return e.start.row===e.end.row&&e.start.col===e.end.col}function be(e,l){let o=[];for(let s=e.start.row;s<=e.end.row;s++)for(let u=e.start.col;u<=e.end.col;u++)l(u)&&o.push({row:s,col:u});return o}function J(e){let{selection:l,activeCell:o,isEditableColumn:s,onDeleteRange:u,onBulkEdit:d,onEditStart:c}=e,r=useCallback(t=>s===void 0?true:s(t),[s]);return {onKeyDown:useCallback(t=>{if(t.key==="Delete"||t.key==="Backspace"){if(t.ctrlKey||t.metaKey||l===null)return;let n=be(l,r);if(n.length===0)return;t.preventDefault(),u!==void 0&&u(n);return}if(t.key==="F2"){if(o===null)return;t.preventDefault(),c!==void 0&&c(o);return}if(t.key==="Enter"){if(o===null||!(l!==null&&we(l)))return;t.preventDefault(),c!==void 0&&c(o);return}if(t.key.length===1&&!t.ctrlKey&&!t.metaKey&&!t.altKey&&!t.nativeEvent.isComposing){if(l===null)return;let n=be(l,r);if(n.length===0)return;if(we(l)&&o!==null){c!==void 0&&c(o,t.key);return}d!==void 0&&d(n,t.key);}},[l,o,r,u,d,c])}}function ee({range:e,getCellValue:l,onFillComplete:o,onFillTargetChange:s,rowCount:u,colCount:d,containerRef:c,getCellRect:r}){let m=useRef(false),t=useRef(null),i=useRef(null),n=useCallback(y=>Math.max(0,Math.min(y,u-1)),[u]),a=useCallback(y=>Math.max(0,Math.min(y,d-1)),[d]),f=useCallback((y,T)=>{let p=c.current;if(p===null)return null;let b=p.getBoundingClientRect(),h=y-b.left,w=T-b.top;for(let R=0;R<u;R++)for(let S=0;S<d;S++){let E=r(R,S);if(h>=E.x&&h<E.x+E.width&&w>=E.y&&w<E.y+E.height)return {row:n(R),col:a(S)}}return null},[c,u,d,r,n,a]),v=useCallback(y=>{y.preventDefault(),y.stopPropagation(),m.current=true,t.current={x:y.clientX,y:y.clientY},i.current=null;},[]);if(useEffect(()=>{let y=p=>{if(!m.current||e===null)return;let b=f(p.clientX,p.clientY);if(b===null)return;let{start:h,end:w}=e,R=null;b.row>w.row?R=K({start:{row:w.row+1,col:h.col},end:{row:b.row,col:w.col}}):b.row<h.row?R=K({start:{row:b.row,col:h.col},end:{row:h.row-1,col:w.col}}):b.col>w.col?R=K({start:{row:h.row,col:w.col+1},end:{row:w.row,col:b.col}}):b.col<h.col&&(R=K({start:{row:h.row,col:b.col},end:{row:w.row,col:h.col-1}})),i.current=R,s!==void 0&&s(R);},T=()=>{if(!m.current||e===null){m.current=false;return}m.current=false;let p=i.current;if(p===null)return;let{start:b,end:h}=e,w="down",R=0;if(p.start.row>h.row?(w="down",R=p.end.row-h.row):p.end.row<b.row?(w="up",R=b.row-p.start.row):p.start.col>h.col?(w="right",R=p.end.col-h.col):(w="left",R=b.col-p.start.col),R>0&&o!==void 0){let S=O(e,w,R,l);o(S);}i.current=null,s!==void 0&&s(null);};return window.addEventListener("mousemove",y),window.addEventListener("mouseup",T),()=>{window.removeEventListener("mousemove",y),window.removeEventListener("mouseup",T);}},[e,l,o,s,f]),e===null)return null;let g=r(e.end.row,e.end.col),C={position:"absolute",left:g.x+g.width-4,top:g.y+g.height-4};return jsx("div",{role:"presentation",style:C,className:"absolute w-2 h-2 bg-blue-500 cursor-crosshair border border-white z-10",onMouseDown:v})}function Ge(e){let l=useLicenseStatus(),{data:o,columns:s,onRangeChange:u,loading:d,emptyText:c="\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",className:r,enableRangeSelection:m=true,enableKeyboardNav:t=true,enableDragFill:i=false,enableClipboard:n=false,enableKeyboardEdit:a=false,enableVirtualization:f=false,getCellValue:v,onFillComplete:g,onFillTargetChange:C,onPaste:y,onClipboardError:T,isEditableColumn:p,onDeleteRange:b,onBulkEdit:h,onEditStart:w}=e,[R,S]=useState([]),E=useReactTable({data:o,columns:s,state:{sorting:R},onSortingChange:S,getCoreRowModel:getCoreRowModel(),getSortedRowModel:getSortedRowModel()}),F=E.getRowModel().rows,B=E.getAllLeafColumns(),_=F.length,re=B.length,U=useRef(null),he=useCallback((x,k)=>{if(U.current===null)return {x:0,y:0,width:0,height:0};let L=`[data-row="${x}"][data-col="${k}"]`,M=U.current.querySelector(L);if(M===null)return {x:0,y:0,width:0,height:0};let P=M.getBoundingClientRect(),V=U.current.getBoundingClientRect();return {x:P.left-V.left,y:P.top-V.top,width:P.width,height:P.height}},[]),{range:N,handleMouseDown:Re,handleMouseEnter:ve,handleMouseUp:ne}=j(m?u:void 0),[A,xe]=useState(null),oe=useCallback(x=>{},[]),{handleKeyDown:le}=q({table:E,activeCell:A,onActiveCellChange:xe,range:N,onRangeChange:t?u??oe:oe,...v!==void 0?{getCellValue:v}:{}}),De=useCallback(()=>{},[]),{onKeyDown:se}=Q({selection:N,activeCell:A,rowCount:_,colCount:re,getCellValue:v??De,...n&&y!==void 0?{onPaste:y}:{},...n&&T!==void 0?{onError:T}:{}}),{onKeyDown:ae}=J({selection:N,activeCell:A,...p!==void 0?{isEditableColumn:p}:{},...a&&b!==void 0?{onDeleteRange:b}:{},...a&&h!==void 0?{onBulkEdit:h}:{},...a&&w!==void 0?{onEditStart:w}:{}}),Te=useCallback(x=>{ae(x),!x.defaultPrevented&&(le(x),!x.defaultPrevented&&se(x));},[ae,le,se]),ie=useVirtualizer({count:f?_:0,getScrollElement:()=>U.current,estimateSize:()=>36}),ce=f?ie.getVirtualItems():null,ke=f?ie.getTotalSize():void 0;if(d===true)return jsx("div",{className:`flex flex-col ${r??""}`,children:jsx("div",{className:"h-40 flex items-center justify-center",children:jsx("div",{className:"animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500"})})});let ue=x=>F[x].getVisibleCells().map((L,M)=>{let P=$(x,M,N),V=A!==null&&A.row===x&&A.col===M;return jsx("td",{"data-row":x,"data-col":M,className:`px-4 py-3 whitespace-nowrap text-gray-700 cursor-cell border border-transparent transition-colors ${V?"bg-blue-50 ring-2 ring-blue-600":P?"bg-blue-100 ring-1 ring-blue-400":""}`,onMouseDown:de=>{m&&(de.preventDefault(),Re(x,M,de.shiftKey));},onMouseEnter:()=>{m&&ve(x,M);},children:flexRender(L.column.columnDef.cell,L.getContext())},L.id)});return jsxs("div",{ref:U,className:`flex flex-col ${r??""} relative`,tabIndex:0,onKeyDown:Te,onMouseUp:ne,onMouseLeave:ne,children:[jsx("div",{className:"overflow-x-auto rounded-lg border border-gray-200 select-none overflow-y-auto",children:jsxs("table",{className:"min-w-full text-sm divide-y divide-gray-200",children:[jsx("thead",{className:"bg-gray-50",children:E.getHeaderGroups().map(x=>jsx("tr",{children:x.headers.map(k=>jsx("th",{className:`px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap select-none ${k.column.getCanSort()?"cursor-pointer hover:bg-gray-100":""}`,onClick:k.column.getToggleSortingHandler(),children:jsxs("div",{className:"flex items-center gap-1",children:[k.isPlaceholder?null:flexRender(k.column.columnDef.header,k.getContext()),k.column.getCanSort()&&jsx("span",{className:"text-gray-400",children:{asc:"\u25B2",desc:"\u25BC"}[k.column.getIsSorted()]??"\u21C5"})]})},k.id))},x.id))}),jsx("tbody",{className:"bg-white divide-y divide-gray-100",children:F.length===0?jsx("tr",{children:jsx("td",{colSpan:B.length,className:"px-4 py-10 text-center text-gray-400",children:c})}):ce!==null?jsx("tr",{children:jsx("td",{colSpan:B.length,className:"p-0",children:jsx("div",{style:{height:ke,position:"relative"},children:ce.map(x=>jsx("table",{style:{position:"absolute",top:x.start,width:"100%",tableLayout:"fixed"},className:"min-w-full text-sm",children:jsx("tbody",{children:jsx("tr",{className:"hover:bg-gray-50",children:ue(x.index)})})},x.key))})})}):F.map((x,k)=>jsx("tr",{className:"hover:bg-gray-50",children:ue(k)},x.id))})]})}),i&&N!==null&&v!==void 0&&jsx(ee,{range:N,getCellValue:v,rowCount:_,colCount:re,containerRef:U,getCellRect:he,...g!==void 0?{onFillComplete:g}:{},...C!==void 0?{onFillTargetChange:C}:{}}),l.watermarkRequired&&jsx(Watermark,{required:true})]})}checkLicense();export{ee as DragFillHandle,Ge as RangeSelectGrid,fe as detectSeriesStep,O as fillRange,$ as isInRange,K as normalizeRange,Y as parseTsv,X as stringifyTsv,j as useCellRange,Q as useClipboard,J as useKeyboardEdit,q as useKeyboardNav};//# sourceMappingURL=index.mjs.map
|
|
8
|
+
//# sourceMappingURL=index.mjs.map
|