virtual-excel-table 2.2.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 +410 -0
- package/demo.html +624 -0
- package/package.json +39 -0
- package/src/main/webapp/WEB-INF/views/virtual-excel-demo.jsp +233 -0
- package/src/main/webapp/resources/js/virtual-excel-table.js +3391 -0
package/README.md
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# VirtualExcelTable
|
|
2
|
+
|
|
3
|
+
레거시 Spring MVC/JSP의 일반 HTML `<table>`에 엑셀처럼 범위 선택, 붙여넣기, 삭제, 되돌리기/다시 실행을 추가하는 의존성 없는 JavaScript입니다. ES5 문법으로 작성되어 jQuery가 필요하지 않습니다.
|
|
4
|
+
|
|
5
|
+
## npm 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install virtual-excel-table
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
CommonJS:
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
var VirtualExcelTable = require('virtual-excel-table');
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
브라우저 번들 또는 정적 리소스 복사 시 진입 파일:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
node_modules/virtual-excel-table/src/main/webapp/resources/js/virtual-excel-table.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 설치
|
|
24
|
+
|
|
25
|
+
JavaScript 파일을 웹 리소스 경로에 둡니다.
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
src/main/webapp/resources/js/virtual-excel-table.js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
JSP의 `</body>` 직전에 스크립트를 불러오고 테이블 셀렉터로 초기화합니다.
|
|
32
|
+
|
|
33
|
+
```jsp
|
|
34
|
+
<script src="${pageContext.request.contextPath}/resources/js/virtual-excel-table.js"></script>
|
|
35
|
+
<script>var excelTable = VirtualExcelTable('#excelTable');</script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
기존 XML 기반 Spring MVC에서 `/resources/**` 정적 리소스 매핑이 없다면 servlet context 설정에 다음을 추가합니다.
|
|
39
|
+
|
|
40
|
+
```xml
|
|
41
|
+
<mvc:resources mapping="/resources/**" location="/resources/" />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
완성된 JSP 예제는 `src/main/webapp/WEB-INF/views/virtual-excel-demo.jsp`에 있습니다. 컨트롤러에서 해당 뷰 이름(`virtual-excel-demo`)을 반환해 확인할 수 있습니다.
|
|
45
|
+
|
|
46
|
+
## 기본 마크업
|
|
47
|
+
|
|
48
|
+
기본 `cellSelector`는 `tbody td`입니다. 각 셀 안에서 `data-excel-target` 요소를 먼저 찾고, 없으면 첫 번째 `input`, `textarea`, `select` 또는 `contenteditable` 요소를 사용합니다. 편집 요소와 자식 태그가 없는 단순 `td`는 셀의 텍스트 자체를 읽고 씁니다. 링크·버튼·아이콘처럼 복합 마크업만 있는 셀은 DOM 손상을 막기 위해 기본적으로 읽기 전용 처리되므로, 수정할 요소에 `data-excel-target`을 붙이거나 `getValue`/`setValue`를 제공하십시오.
|
|
49
|
+
|
|
50
|
+
```jsp
|
|
51
|
+
<form method="post" action="${pageContext.request.contextPath}/orders/save">
|
|
52
|
+
<table id="excelTable">
|
|
53
|
+
<thead>
|
|
54
|
+
<tr>
|
|
55
|
+
<th data-excel-corner>#</th>
|
|
56
|
+
<th data-excel-column="0" data-excel-key="productCode">상품 코드</th>
|
|
57
|
+
<th data-excel-column="1" data-excel-key="quantity">수량</th>
|
|
58
|
+
</tr>
|
|
59
|
+
</thead>
|
|
60
|
+
<tbody>
|
|
61
|
+
<tr>
|
|
62
|
+
<th data-excel-row-header>1</th>
|
|
63
|
+
<td><input name="items[0].productCode" data-excel-target></td>
|
|
64
|
+
<td><input name="items[0].quantity" data-excel-target></td>
|
|
65
|
+
</tr>
|
|
66
|
+
<tr>
|
|
67
|
+
<th data-excel-row-header>2</th>
|
|
68
|
+
<td><input name="items[1].productCode" data-excel-target></td>
|
|
69
|
+
<td><input name="items[1].quantity" data-excel-target></td>
|
|
70
|
+
</tr>
|
|
71
|
+
</tbody>
|
|
72
|
+
</table>
|
|
73
|
+
</form>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`items[0].productCode` 같은 0 기반 이름은 Spring MVC 커맨드 객체의 `List` 속성 바인딩에 사용할 수 있습니다. 행을 서버/JSTL로 렌더링할 때도 인덱스가 중복되거나 빠지지 않게 만듭니다.
|
|
77
|
+
|
|
78
|
+
`data-excel-row-header`는 행 전체 선택, `data-excel-column`은 열 전체 선택, `data-excel-corner`는 좌상단 전체 선택 헤더입니다. 행 헤더로 선택한 뒤 Delete를 누르면 행이 제거됩니다. 열/좌상단 전체 선택에서 Delete는 값만 비우며 열이나 행 DOM을 제거하지 않습니다.
|
|
79
|
+
|
|
80
|
+
읽기 전용 셀은 `td` 또는 입력 요소에 `data-excel-readonly`, `aria-readonly="true"`, `readonly`, `disabled` 중 하나를 지정합니다. 읽기 전용 셀은 선택할 수 있지만 붙여넣기와 삭제에서는 건너뜁니다.
|
|
81
|
+
|
|
82
|
+
## 사용법
|
|
83
|
+
|
|
84
|
+
- 한 번 클릭: input 위에서도 한 셀 선택
|
|
85
|
+
- 드래그 또는 Shift+클릭: input 위에서 시작해도 사각 범위 선택
|
|
86
|
+
- 방향키: 활성 셀 이동, Shift+방향키: 범위 확장
|
|
87
|
+
- 선택 상태에서 문자·숫자 입력: 기존 값을 해당 글자로 바꾸고 바로 입력 시작
|
|
88
|
+
- F2 또는 더블클릭: 기존 값을 유지한 수동 커서 편집, Escape: 편집 전 값으로 취소
|
|
89
|
+
- Tab/Shift+Tab: 오른쪽/왼쪽 셀 이동(행 끝에서 다음/이전 행으로 이동)
|
|
90
|
+
- Enter/Shift+Enter: 아래/위 셀 이동. 편집 중에는 현재 값을 확정한 뒤 이동
|
|
91
|
+
- Ctrl/Cmd+Enter: 현재 활성 행 바로 아래에 빈 행 삽입. 편집 중이면 값을 확정한 뒤 삽입
|
|
92
|
+
- 바로 입력한 편집 모드의 방향키: 값을 확정하고 인접 셀 이동
|
|
93
|
+
- F2/더블클릭 편집 모드의 방향키: input 내부 텍스트 커서만 이동
|
|
94
|
+
- Ctrl/Cmd+C: 선택 범위를 Excel 호환 TSV 형식으로 복사
|
|
95
|
+
- Ctrl/Cmd+V: Excel, LibreOffice, Google Sheets의 탭/줄바꿈 형식 데이터를 붙여넣기. 복사 블록이 선택 범위에 정확히 반복되는 크기라면 전체 범위에 타일처럼 반복
|
|
96
|
+
- Delete/Backspace: 선택 값 삭제. 단, 행 헤더 선택의 Delete는 행 자체 삭제
|
|
97
|
+
- Ctrl/Cmd+- 또는 `deleteRows()`: 선택된 셀이 포함된 `tbody` 행 삭제
|
|
98
|
+
- Ctrl/Cmd+Z: 되돌리기, Ctrl/Cmd+Y 또는 Ctrl/Cmd+Shift+Z: 다시 실행
|
|
99
|
+
|
|
100
|
+
기본 `interactionMode: 'spreadsheet'`에서는 한 번 클릭이 셀 선택이므로 Delete가 셀 전체를 비웁니다. F2/더블클릭 수동 편집 모드에서는 방향키가 input 내부 커서를 움직이고, 바로 타이핑해 진입한 편집 모드에서는 방향키가 값을 확정하고 셀을 이동합니다. 기존 폼처럼 input을 한 번 클릭해 즉시 편집하려면 `interactionMode: 'form'`을 지정하십시오.
|
|
101
|
+
|
|
102
|
+
붙여넣기, 선택 값 삭제, 행 삭제와 확정된 직접 입력은 모두 같은 되돌리기 기록에 저장됩니다. 아직 input을 편집 중일 때의 Ctrl/Cmd+Z는 브라우저의 문자 편집을 따르고, Tab/Enter/외부 클릭으로 확정한 뒤에는 테이블 단위 Ctrl/Cmd+Z로 되돌릴 수 있습니다.
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<button type="button" onclick="excelTable.undo();">되돌리기</button>
|
|
106
|
+
<button type="button" onclick="excelTable.redo();">다시 실행</button>
|
|
107
|
+
<button type="button" onclick="excelTable.deleteSelection();">선택 값 삭제</button>
|
|
108
|
+
<button type="button" onclick="excelTable.deleteRows();">선택 행 삭제</button>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`Delete` 키는 엑셀과 동일하게 셀 값만 비웁니다. `<tr>` 자체를 제거하려면 행 안의 셀을 하나 이상 선택하고 `deleteRows()`를 호출합니다. 행 삭제도 같은 Undo/Redo 기록에 포함됩니다.
|
|
112
|
+
|
|
113
|
+
## 옵션
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
var excelTable = VirtualExcelTable('#excelTable', {
|
|
117
|
+
cellSelector: 'tbody td.editable',
|
|
118
|
+
historyLimit: 100,
|
|
119
|
+
overflow: 'reject',
|
|
120
|
+
pasteMode: 'grid',
|
|
121
|
+
interactionMode: 'spreadsheet',
|
|
122
|
+
afterChange: function (changes, meta) {
|
|
123
|
+
// changes: [{ cell: td, oldValue: '기존값', newValue: '새값' }]
|
|
124
|
+
// meta.type: paste, delete, undo, redo
|
|
125
|
+
},
|
|
126
|
+
onError: function (error) {
|
|
127
|
+
alert(error.message);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| 옵션 | 기본값 | 설명 |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `cellSelector` | `'tbody td'` | 표 안에서 편집 셀로 취급할 CSS 셀렉터 |
|
|
135
|
+
| `rowSelector` | `'tbody tr'` | `deleteRows()`가 제거할 수 있는 데이터 행 셀렉터 |
|
|
136
|
+
| `rowHeaderSelector` | `'tbody th'` | 클릭·드래그로 행 전체를 선택하는 헤더 셀렉터 |
|
|
137
|
+
| `columnHeaderSelector` | `'thead th'` | 클릭·드래그로 열 전체를 선택하는 헤더 셀렉터 |
|
|
138
|
+
| `minRows` | `1` | 행 삭제 후 반드시 남겨 둘 최소 데이터 행 수 |
|
|
139
|
+
| `historyLimit` | `50` | 보관할 실행 취소 항목 수. `0` 이하는 기록하지 않음 |
|
|
140
|
+
| `overflow` | `'clip'` | 붙여넣기가 표를 넘을 때 초과분만 자르는 `'clip'` 또는 전체를 거부하는 `'reject'` |
|
|
141
|
+
| `autoAddRowsOnPaste` | `true` | 붙여넣기 행 수가 부족하면 마지막 행 구조를 자동 복제 |
|
|
142
|
+
| `pasteMode` | `'grid'` | 항상 표 붙여넣기인 `'grid'`, 단일 입력값은 브라우저에 맡기는 `'auto'`, 표 붙여넣기를 끄는 `'native'` |
|
|
143
|
+
| `interactionMode` | `'spreadsheet'` | 한 번 클릭/드래그로 셀을 선택하는 `'spreadsheet'` 또는 input 기본 편집을 우선하는 `'form'` |
|
|
144
|
+
| `pasteSingleToSelection` | `true` | 한 값을 여러 선택 셀 전체에 채울지 여부 |
|
|
145
|
+
| `pasteRepeatToSelection` | `true` | 복사한 직사각형 블록이 더 큰 선택 범위의 행·열 크기를 정확히 나눌 때 반복해서 채울지 여부 |
|
|
146
|
+
| `dispatchEvents` | `true` | 값 변경 후 대상 요소에 버블링되는 `input`, `change` 이벤트 발생 여부 |
|
|
147
|
+
| `injectStyles` | `true` | 선택/활성/읽기 전용 표시용 기본 CSS 자동 삽입 여부 |
|
|
148
|
+
| `readOnly(cell, target)` | `null` | `true`를 반환한 셀을 읽기 전용으로 처리 |
|
|
149
|
+
| `rowReadOnly(row)` | `null` | `true`를 반환한 행의 삭제를 차단 |
|
|
150
|
+
| `getValue(cell, target)` | `null` | 사용자 정의 값 읽기 함수 |
|
|
151
|
+
| `setValue(cell, value, target)` | `null` | 사용자 정의 값 쓰기 함수 |
|
|
152
|
+
| `beforeChange(changes, meta)` | `null` | 변경 직전 호출. `false` 반환 시 해당 작업 취소 |
|
|
153
|
+
| `afterChange(changes, meta)` | `null` | 변경 완료 후 호출 |
|
|
154
|
+
| `beforeRowsDelete(rows, meta)` | `null` | 행 삭제 직전 호출. `false` 반환 시 전체 삭제 취소 |
|
|
155
|
+
| `beforeRowAdd(row, meta)` | `null` | 새 행을 DOM에 추가하기 직전 호출. `false` 반환 시 취소 |
|
|
156
|
+
| `rowFactory(table, index, values, referenceRow)` | `null` | 복제 대신 프로젝트 전용 새 `tr`을 반환하는 함수. 중간 삽입이면 `referenceRow` 제공 |
|
|
157
|
+
| `reindexRows(table, meta)` | `null` | 행 삭제·복원·다시 삭제 후 Spring 입력 이름 등을 재정렬하는 훅 |
|
|
158
|
+
| `afterRowsChange(rows, meta)` | `null` | 행 추가·자동 확장·삭제·Undo/Redo로 DOM 행 구성이 바뀐 뒤 호출 |
|
|
159
|
+
| `onSelectionChange(selection)` | `null` | 선택 변경 후 호출. 선택 정보는 0 기반 `start`, `end`, `cells` 포함 |
|
|
160
|
+
| `onError(error)` | `null` | 붙여넣기 범위 초과, 값 쓰기 또는 콜백 오류 등을 받을 함수 |
|
|
161
|
+
|
|
162
|
+
`injectStyles: false`를 사용하면 `.vet-cell`, `.vet-selected`, `.vet-active`, `.vet-readonly` 클래스를 애플리케이션 CSS에서 직접 꾸며야 합니다.
|
|
163
|
+
|
|
164
|
+
## API
|
|
165
|
+
|
|
166
|
+
### 생성 함수와 정적 API
|
|
167
|
+
|
|
168
|
+
| API | 반환/설명 |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `VirtualExcelTable(selectorOrTable, options)` | 정확히 한 개의 테이블에 연결하고 인스턴스 반환 |
|
|
171
|
+
| `VirtualExcelTable.attach(...)` | 위 생성 함수의 별칭 |
|
|
172
|
+
| `VirtualExcelTable.attachAll(selectorOrList, options)` | 여러 테이블에 연결하고 인스턴스 배열 반환 |
|
|
173
|
+
| `VirtualExcelTable.parseClipboardText(text)` | 탭/줄바꿈 클립보드 문자열을 2차원 배열로 변환 |
|
|
174
|
+
| `VirtualExcelTable.version` | 라이브러리 버전 문자열 |
|
|
175
|
+
|
|
176
|
+
같은 테이블에 다시 연결하면 기존 인스턴스를 반환합니다. 단일 생성 함수의 셀렉터가 여러 테이블과 일치하면 오류가 발생하므로 그 경우 `attachAll`을 사용합니다.
|
|
177
|
+
|
|
178
|
+
### 인스턴스 API
|
|
179
|
+
|
|
180
|
+
| 메서드 | 설명 |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `select(startRow, startColumn[, endRow, endColumn])` | 0 기반 좌표 범위 선택. 인스턴스를 반환 |
|
|
183
|
+
| `selectAll()` | 편집 대상 전체 선택 |
|
|
184
|
+
| `getSelection()` | `{ start, end, cells }` 또는 선택이 없으면 `null` 반환 |
|
|
185
|
+
| `getCopyText()` | 현재 선택 범위를 Excel 호환 TSV 문자열로 반환 |
|
|
186
|
+
| `copy()` | 버튼 클릭 같은 사용자 동작 안에서 선택 범위를 시스템 클립보드로 복사하고 성공 여부 반환 |
|
|
187
|
+
| `paste(text)` | 문자열을 현재 선택 위치에 넣고 실제 변경된 셀 수 반환 |
|
|
188
|
+
| `clear()` / `deleteSelection()` | 선택 값을 비우고 실제 변경된 셀 수 반환 |
|
|
189
|
+
| `deleteRows()` / `removeSelectedRows()` | 선택 셀이 속한 고유 `tbody` 행을 제거하고 삭제 행 수 반환 |
|
|
190
|
+
| `addRow([values])` | 마지막 행 구조를 복제하거나 `rowFactory`로 행을 추가하고 새 `tr` 반환 |
|
|
191
|
+
| `insertRowBelow([values])` / `addRowBelow([values])` | 현재 활성 행 구조를 복제해 바로 아래에 삽입하고 새 `tr` 반환 |
|
|
192
|
+
| `getJsonData([keys], [options])` | 행 데이터를 객체 배열로 반환. `options.skipEmptyRows` 지원 |
|
|
193
|
+
| `toJSON([keys], [options])` | `getJsonData()` 별칭 |
|
|
194
|
+
| `undo()` / `redo()` | 작업 실행 후 성공 여부 반환 |
|
|
195
|
+
| `canUndo()` / `canRedo()` | 실행 가능한 기록 존재 여부 반환 |
|
|
196
|
+
| `refresh()` | 동적으로 행/셀을 바꾼 뒤 셀 목록과 선택 상태 갱신 |
|
|
197
|
+
| `destroy()` | 이벤트와 상태/클래스를 제거하고 테이블을 원래 상태로 복원 |
|
|
198
|
+
|
|
199
|
+
행을 Ajax로 추가하거나 제거했다면 마크업을 갱신한 직후 `excelTable.refresh()`를 호출합니다. 삭제된 DOM 셀을 가리키는 과거 기록은 되돌리기에서 적용할 수 없으므로, 대규모 재렌더링 뒤 기록까지 초기화하려면 `destroy()` 후 다시 생성하는 편이 안전합니다.
|
|
200
|
+
|
|
201
|
+
Spring 목록 이름은 프로젝트마다 다르므로 코어가 임의로 변경하지 않습니다. 다음처럼 `reindexRows`에서 `items[n]`을 다시 맞출 수 있습니다.
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
var excelTable = VirtualExcelTable('#excelTable', {
|
|
205
|
+
reindexRows: function (table) {
|
|
206
|
+
var rows = table.querySelectorAll('tbody tr');
|
|
207
|
+
var fields;
|
|
208
|
+
var i;
|
|
209
|
+
var j;
|
|
210
|
+
|
|
211
|
+
for (i = 0; i < rows.length; i += 1) {
|
|
212
|
+
fields = rows[i].querySelectorAll('[name]');
|
|
213
|
+
for (j = 0; j < fields.length; j += 1) {
|
|
214
|
+
fields[j].name = fields[j].name.replace(
|
|
215
|
+
/^items\[\d+\]/,
|
|
216
|
+
'items[' + i + ']'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Select와 JSON 값
|
|
225
|
+
|
|
226
|
+
`select`는 스프레드시트 input 모드로 가로채지 않고 일반 HTML select처럼 클릭·방향키·Enter·Tab이 동작합니다. 화면 복사에는 선택된 option의 라벨을 사용하지만, JSON과 폼 전송에는 실제 `select.value`를 사용합니다. 붙여넣기는 option value를 먼저 찾고, 없으면 라벨이 같은 option을 찾아 실제 value를 선택합니다.
|
|
227
|
+
|
|
228
|
+
```html
|
|
229
|
+
<th data-excel-column="2" data-excel-key="status">상태</th>
|
|
230
|
+
<td>
|
|
231
|
+
<select name="items[0].status">
|
|
232
|
+
<option value="A">사용</option>
|
|
233
|
+
<option value="I">미사용</option>
|
|
234
|
+
</select>
|
|
235
|
+
</td>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
var data = excelTable.getJsonData(
|
|
240
|
+
['code', 'name', 'status'],
|
|
241
|
+
{ skipEmptyRows: true }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// [{ code: 'A001', name: '사과', status: 'A' }]
|
|
245
|
+
console.log(JSON.stringify(data));
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
키 배열을 생략하면 `data-excel-key`, input/select의 `name` 마지막 필드명, `key1`, `key2` 순서로 자동 추론합니다.
|
|
249
|
+
|
|
250
|
+
## 행 추가와 자동 확장
|
|
251
|
+
|
|
252
|
+
기본값에서는 마지막 한 행(`minRows: 1`)을 삭제할 수 없습니다. `addRow()`는 마지막 행을, `insertRowBelow()`와 Ctrl/Cmd+Enter는 현재 활성 행을 복제합니다. 중복 `id`와 label `for`를 제거하고 input 값을 비우며 select를 첫 option으로 초기화합니다. 붙여넣기에 더 많은 행이 필요할 때는 항상 테이블 끝에 자동 확장됩니다.
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
excelTable.addRow();
|
|
256
|
+
excelTable.addRow({
|
|
257
|
+
productCode: 'A001',
|
|
258
|
+
quantity: '10',
|
|
259
|
+
status: 'A'
|
|
260
|
+
});
|
|
261
|
+
excelTable.insertRowBelow();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
복잡한 행은 복제 대신 팩토리를 제공할 수 있습니다.
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
var excelTable = VirtualExcelTable('#excelTable', {
|
|
268
|
+
minRows: 1,
|
|
269
|
+
autoAddRowsOnPaste: true,
|
|
270
|
+
rowFactory: function (table, index) {
|
|
271
|
+
var row = document.createElement('tr');
|
|
272
|
+
row.innerHTML = '<th data-excel-row-header></th>' +
|
|
273
|
+
'<td><input name="items[' + index + '].code"></td>';
|
|
274
|
+
return row;
|
|
275
|
+
},
|
|
276
|
+
reindexRows: reindexItemRows
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## jQuery UI Datepicker 열
|
|
281
|
+
|
|
282
|
+
코어 라이브러리는 jQuery에 의존하지 않지만, 특정 input에 `data-excel-native-editor`를 붙이면 첫 클릭을 가로채지 않으므로 jQuery UI Datepicker 같은 기존 편집기를 그대로 열 수 있습니다.
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
<td>
|
|
286
|
+
<input type="text"
|
|
287
|
+
class="js-datepicker"
|
|
288
|
+
data-excel-native-editor
|
|
289
|
+
name="items[0].receivedDate">
|
|
290
|
+
</td>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
행 복제 시 원본 Datepicker의 `hasDatepicker` 표시 클래스는 복제되지만 jQuery 내부 인스턴스 데이터는 복제되지 않습니다. 새 행을 붙이기 전에 표시 클래스를 제거하고, 행 구성이 바뀐 뒤 현재 표 안의 미초기화 input만 다시 초기화하십시오. 이 콜백은 `addRow()`뿐 아니라 여러 행 붙여넣기의 자동 행 추가와 Undo/Redo에도 호출됩니다.
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
function prepareDatepickerRow(row) {
|
|
297
|
+
var $row = jQuery(row);
|
|
298
|
+
|
|
299
|
+
$row.find('.ui-datepicker-trigger, .ui-datepicker-append').remove();
|
|
300
|
+
$row.find('.js-datepicker').each(function () {
|
|
301
|
+
jQuery.removeData(this, 'datepicker');
|
|
302
|
+
jQuery(this)
|
|
303
|
+
.removeClass('hasDatepicker')
|
|
304
|
+
.removeAttr(
|
|
305
|
+
'aria-haspopup aria-expanded aria-controls aria-describedby aria-owns'
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function initializeDatepickers(table) {
|
|
311
|
+
jQuery(table).find('.js-datepicker').each(function () {
|
|
312
|
+
if (!jQuery.data(this, 'datepicker')) {
|
|
313
|
+
jQuery(this).removeClass('hasDatepicker').datepicker({
|
|
314
|
+
dateFormat: 'yy-mm-dd',
|
|
315
|
+
onSelect: function () {
|
|
316
|
+
jQuery(this).trigger('input').trigger('change');
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
var tableElement = document.getElementById('excelTable');
|
|
324
|
+
var excelTable = VirtualExcelTable(tableElement, {
|
|
325
|
+
beforeRowAdd: function (row) {
|
|
326
|
+
prepareDatepickerRow(row);
|
|
327
|
+
},
|
|
328
|
+
reindexRows: reindexItemRows,
|
|
329
|
+
afterRowsChange: function () {
|
|
330
|
+
initializeDatepickers(tableElement);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
initializeDatepickers(tableElement);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`demo.html`은 jQuery UI 1.14.2를 CDN에서 불러오는 실행 예제를 포함합니다. 실제 Spring/JSP 프로젝트에서는 이미 로드한 jQuery/jQuery UI 파일을 그대로 사용하면 됩니다.
|
|
338
|
+
|
|
339
|
+
## 셀별 유효성 검사 데모
|
|
340
|
+
|
|
341
|
+
`demo.html`의 `등록(유효성 검사)` 버튼은 업무 화면에서 검증을 트리거하는 예시입니다. 실제 control에는 `data-excel-target`과 `data-field`를 붙이고, 오류 메시지는 같은 `td`의 `.vet-field-error`에 표시합니다. 따라서 오류 문구가 복사·붙여넣기·JSON 값에 섞이지 않습니다.
|
|
342
|
+
|
|
343
|
+
```html
|
|
344
|
+
<td>
|
|
345
|
+
<div class="vet-field-control">
|
|
346
|
+
<input type="text"
|
|
347
|
+
data-excel-target
|
|
348
|
+
data-field="code"
|
|
349
|
+
name="items[0].code">
|
|
350
|
+
</div>
|
|
351
|
+
<div class="vet-field-error" role="alert" aria-live="polite"></div>
|
|
352
|
+
</td>
|
|
353
|
+
|
|
354
|
+
<button type="button" id="registerButton">등록(유효성 검사)</button>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```css
|
|
358
|
+
.vet-field-error {
|
|
359
|
+
display: none;
|
|
360
|
+
color: #dc2626;
|
|
361
|
+
font-size: 12px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
tr.has-validation-errors .vet-field-error {
|
|
365
|
+
display: block;
|
|
366
|
+
visibility: hidden;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
tr.has-validation-errors td.is-invalid > .vet-field-error {
|
|
370
|
+
visibility: visible;
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
데모는 input/select 자체에 CSS를 적용하지 않습니다. 바깥 `.vet-field-control`과 오류 영역만 정렬하므로 프로젝트의 기존 control 스타일이 그대로 유지됩니다. 상품 코드·상품명·상태 필수, 수량은 1 이상의 정수, 단가는 0 이상의 숫자, 입고일은 실제 존재하는 `YYYY-MM-DD` 날짜를 검사합니다. 완전히 빈 행은 무시하지만 모든 행이 비어 있으면 첫 행에 필수 오류를 보여줍니다. `input` 또는 `change`가 발생하면 해당 셀 오류만 지우며, 새 행을 복제할 때는 `beforeRowAdd`에서 기존 오류 문구와 `aria-invalid`를 제거합니다.
|
|
375
|
+
|
|
376
|
+
## 테이블 구조 제약
|
|
377
|
+
|
|
378
|
+
- `rowspan`과 `colspan`은 시각적인 병합 좌표로 계산하지 않습니다. 각 `<tr>`에 실제로 존재하는 셀 순서만 사용하므로 병합 셀이 있는 표에서는 선택/붙여넣기 좌표가 어긋날 수 있습니다. 편집 영역에는 사용하지 마십시오.
|
|
379
|
+
- 모든 편집 행은 같은 수의 `td`를 가진 직사각형 구조를 권장합니다. 행별 셀 수가 다르면 없는 위치는 `overflow` 규칙의 영향을 받습니다.
|
|
380
|
+
- 기본값에서는 `thead th`와 `tbody th`가 편집 대상이 아닙니다. 헤더까지 포함하려면 `cellSelector`를 명시하되, 데이터 행과 열 좌표가 달라진다는 점을 고려해야 합니다.
|
|
381
|
+
- 중첩 테이블의 셀/입력 요소는 바깥 테이블의 편집 대상으로 섞이지 않습니다.
|
|
382
|
+
- 체크박스, 라디오, 파일 및 버튼 타입 `input`은 자동 값 대상으로 취급하지 않습니다. 필요하면 `getValue`/`setValue`를 제공하십시오.
|
|
383
|
+
|
|
384
|
+
## 브라우저 호환
|
|
385
|
+
|
|
386
|
+
코드는 ES5 문법과 표준 DOM 이벤트를 사용하며 현재 버전의 Chrome, Edge, Firefox, Safari를 기준으로 사용할 수 있습니다. `addEventListener`, `querySelectorAll`, `matches` 계열 API가 없는 IE8 이하는 지원하지 않습니다. IE9~11 같은 구형 브라우저는 문법상 동작 가능하도록 작성되어 있지만 클립보드 이벤트와 보안 정책이 브라우저/사내 설정마다 다르므로 실제 운영 대상 환경에서 반드시 확인하십시오.
|
|
387
|
+
|
|
388
|
+
브라우저 보안상 시스템 클립보드를 임의로 읽지 않습니다. 사용자가 테이블을 선택하고 Ctrl/Cmd+V를 눌러 발생시킨 `paste` 이벤트의 텍스트만 처리합니다. 테스트 자동화나 서버 데이터 적용처럼 이미 문자열이 있다면 `excelTable.paste(text)`를 사용합니다.
|
|
389
|
+
|
|
390
|
+
## 테스트 방법
|
|
391
|
+
|
|
392
|
+
1. `demo.html`을 브라우저로 열거나 Spring 애플리케이션에서 `virtual-excel-demo.jsp`를 반환하는 URL을 엽니다.
|
|
393
|
+
2. Excel에 `상품 코드, 상품명, 수량, 단가, 입고일, 상태` 순서의 3행 x 6열 데이터를 만든 뒤 복사하고, 웹 표의 첫 셀을 클릭해 Ctrl/Cmd+V를 누릅니다.
|
|
394
|
+
3. 값이 행/열 순서대로 들어가고 각 input의 개발자 도구 `value`가 바뀌었는지 확인합니다.
|
|
395
|
+
4. 여러 셀을 선택해 Delete, Ctrl/Cmd+Z, Ctrl/Cmd+Y(또는 Cmd+Shift+Z)를 차례로 시험합니다.
|
|
396
|
+
5. 표 오른쪽/아래 경계를 넘는 데이터를 붙여 `overflow: 'clip'`과 `'reject'`를 각각 확인합니다.
|
|
397
|
+
6. 읽기 전용 셀, 한 값의 범위 채우기, 따옴표 안 탭/줄바꿈 데이터도 확인합니다.
|
|
398
|
+
7. 폼을 전송하는 화면에서는 서버가 `items[n].fieldName` 값을 예상한 목록 객체로 바인딩하는지 통합 테스트합니다.
|
|
399
|
+
8. 자동 회귀 검사는 `tests/browser-test.html`, Datepicker 행 추가 통합 검사는 `tests/demo-datepicker-test.html`을 브라우저로 엽니다.
|
|
400
|
+
|
|
401
|
+
간단한 콘솔 확인 예시는 다음과 같습니다.
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
excelTable.select(0, 0);
|
|
405
|
+
excelTable.paste('A001\t사과\t10\t1200\r\nA002\t배\t5\t2500');
|
|
406
|
+
console.log(excelTable.getSelection());
|
|
407
|
+
console.log(excelTable.canUndo());
|
|
408
|
+
excelTable.undo();
|
|
409
|
+
excelTable.redo();
|
|
410
|
+
```
|