Skip to content

Commit 73d74bd

Browse files
committed
feat: v0.14.0 — built-in context menu + XLSX export
- Built-in context menu (show-context-menu attribute) - Cell right-click: copy, insert row above/below, delete row, hide column, sort asc/desc, filter by value / clear filter - Viewport boundary auto-correction - context-menu CustomEvent is now cancelable (preventDefault suppresses menu) - XLSX Export (zero external dependencies) - Pure TypeScript ZIP/OOXML implementation (CRC32 + ZIP STORE) - exportToFile("xlsx") / exportToString("xlsx") -> Uint8Array - Header row with bold style, number/date/boolean cell types - Date uses Excel serial number with yyyy-mm-dd format - ExportFormat now includes "xlsx" - 253 tests (was 246, +7)
1 parent 8ddafec commit 73d74bd

9 files changed

Lines changed: 596 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.14.0] - 2026-05-19
11+
12+
### Added
13+
- **내장 컨텍스트 메뉴 (`show-context-menu`)**: 셀 우클릭 시 기본 메뉴 표시 (복사, 행 삽입·삭제, 컬럼 숨기기, 정렬, 필터)
14+
- `context-menu` 이벤트에서 `preventDefault()` 호출 시 내장 메뉴 억제
15+
- 화면 경계 자동 위치 보정
16+
- **XLSX Export**: `exportToFile('xlsx', ...)` / `exportToString('xlsx')` 지원
17+
- 외부 의존성 없는 순수 TypeScript 구현 (ZIP STORE + OOXML)
18+
- 헤더 행 포함, 숫자/날짜/불리언 셀 타입 지원, 헤더 bold 스타일
19+
20+
### Changed
21+
- `ExportFormat``'xlsx'` 추가
22+
- `exportToString` 반환 타입: `string | Uint8Array` (xlsx는 Uint8Array)
23+
- `context-menu` CustomEvent가 이제 `cancelable: true`
24+
25+
---
26+
1027
## [0.13.0] - 2026-05-19
1128

1229
### Added

ROADMAP.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Roadmap
22

33
> Last updated: 2026-05-19
4-
> Current version: **v0.13.0** (246 tests)
4+
> Current version: **v0.14.0** (253 tests)
55
66
flex-table 개발 로드맵. 완료된 기능과 향후 계획을 추적한다.
77

@@ -141,6 +141,19 @@ inline whitespace gap. 최종 해결: 모든 prefix/pinned 셀을 `position: abs
141141

142142
---
143143

144+
## Phase 14: Export/Import & 고급 — v0.14.0 (부분 완료)
145+
146+
> **완료**: 내장 컨텍스트 메뉴 + XLSX Export.
147+
148+
| ID | Task | Status | Description |
149+
|----|------|--------|-------------|
150+
| EX-01 | 내장 컨텍스트 메뉴 || `show-context-menu` 속성, 우클릭 메뉴 |
151+
| EX-02 | XLSX Export || 순수 TS 구현, zero deps |
152+
| EX-03 | 행 고정 (Freeze Rows) || 복잡도 상 |
153+
| EX-04 | 고급 필터 || 복잡도 중 |
154+
155+
---
156+
144157
## Phase 13: 서식 & 표시 — v0.13.0 ✅ COMPLETE
145158

146159
> **완료**: 셀 포맷, 조건부 서식, 자동완성 편집기, 컬럼 숨기기/표시 UI.
@@ -206,7 +219,8 @@ inline whitespace gap. 최종 해결: 모든 prefix/pinned 셀을 `position: abs
206219
| **v0.10.0**| 179 | 74.62 KB | 16.60 KB | React wrapper (`@iyulab/flex-table/react`) |
207220
| **v0.11.0**| 189 | TBD | TBD | 마우스 드래그 선택, Fill Down/Right |
208221
| **v0.12.0**| 213 | TBD | TBD | 비연속 선택, 컬럼 드래그, select 편집기, 찾기/바꾸기, 행 드래그, Fill Handle |
209-
| **v0.13.0** | 246 | TBD | TBD | 셀 포맷, 조건부 서식, 자동완성, 컬럼 숨기기/표시 UI |
222+
| **v0.13.0**| 246 | TBD | TBD | 셀 포맷, 조건부 서식, 자동완성, 컬럼 숨기기/표시 UI |
223+
| **v0.14.0** | 253 | TBD | TBD | 내장 컨텍스트 메뉴, XLSX Export |
210224

211225
---
212226

demo/index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ <h2 style="margin-top: 32px;">v0.13 New Features</h2>
7777
Format (number/date) &middot; Conditional Rules &middot; Autocomplete editor &middot;
7878
Column Hide/Show (right-click header)
7979
</p>
80-
<flex-table id="new-features-table" row-height="32" show-row-numbers editable></flex-table>
80+
<flex-table id="new-features-table" row-height="32" show-row-numbers editable show-context-menu></flex-table>
81+
<div class="toolbar" style="margin-top: 8px;">
82+
<button class="btn" id="export-xlsx-btn">Export XLSX</button>
83+
</div>
8184

8285
<h2 style="margin-top: 32px;">Horizontal Virtual Scroll Test</h2>
8386
<p class="info"><span id="wide-col-count"></span> columns &middot; <span id="wide-row-count"></span> rows &middot; Column 0 pinned left</p>
@@ -267,6 +270,10 @@ <h2 style="margin-top: 32px;">Horizontal Virtual Scroll Test</h2>
267270
status: ['Active', 'Inactive', 'Pending'][i % 3],
268271
}));
269272

273+
document.getElementById('export-xlsx-btn').addEventListener('click', () => {
274+
newFeaturesTable.exportToFile('xlsx', 'flex-table-export.xlsx');
275+
});
276+
270277
// --- 100-Column Wide Table ---
271278
const wideTable = document.getElementById('wide-table');
272279
const WIDE_COLS = 100;

src/export/export.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const data: DataRow[] = [
1616
describe('exportData', () => {
1717
describe('csv', () => {
1818
it('should export with headers', () => {
19-
const result = exportData(data, cols, 'csv');
19+
const result = exportData(data, cols, 'csv') as string;
2020
const lines = result.split('\n');
2121
expect(lines[0]).toBe('Name,Age,Active');
2222
expect(lines[1]).toBe('Alice,30,true');
@@ -25,32 +25,32 @@ describe('exportData', () => {
2525

2626
it('should escape commas in values', () => {
2727
const d: DataRow[] = [{ name: 'Doe, Jane', age: 20, active: true }];
28-
const result = exportData(d, cols, 'csv');
28+
const result = exportData(d, cols, 'csv') as string;
2929
expect(result).toContain('"Doe, Jane"');
3030
});
3131

3232
it('should escape quotes in values', () => {
3333
const d: DataRow[] = [{ name: 'He said "hi"', age: 20, active: true }];
34-
const result = exportData(d, cols, 'csv');
34+
const result = exportData(d, cols, 'csv') as string;
3535
expect(result).toContain('"He said ""hi"""');
3636
});
3737

3838
it('should handle null values', () => {
3939
const d: DataRow[] = [{ name: null, age: undefined, active: true }];
40-
const result = exportData(d, cols, 'csv');
40+
const result = exportData(d, cols, 'csv') as string;
4141
const lines = result.split('\n');
4242
expect(lines[1]).toBe(',,true');
4343
});
4444

4545
it('should handle empty data', () => {
46-
const result = exportData([], cols, 'csv');
46+
const result = exportData([], cols, 'csv') as string;
4747
expect(result).toBe('Name,Age,Active');
4848
});
4949
});
5050

5151
describe('tsv', () => {
5252
it('should use tab delimiter', () => {
53-
const result = exportData(data, cols, 'tsv');
53+
const result = exportData(data, cols, 'tsv') as string;
5454
const lines = result.split('\n');
5555
expect(lines[0]).toBe('Name\tAge\tActive');
5656
expect(lines[1]).toBe('Alice\t30\ttrue');
@@ -59,7 +59,7 @@ describe('exportData', () => {
5959

6060
describe('json', () => {
6161
it('should export as JSON array', () => {
62-
const result = exportData(data, cols, 'json');
62+
const result = exportData(data, cols, 'json') as string;
6363
const parsed = JSON.parse(result);
6464
expect(parsed).toEqual([
6565
{ name: 'Alice', age: 30, active: true },
@@ -69,14 +69,14 @@ describe('exportData', () => {
6969

7070
it('should only include column keys', () => {
7171
const d: DataRow[] = [{ name: 'Alice', age: 30, active: true, extra: 'hidden' }];
72-
const result = exportData(d, cols, 'json');
72+
const result = exportData(d, cols, 'json') as string;
7373
const parsed = JSON.parse(result);
7474
expect(parsed[0]).not.toHaveProperty('extra');
7575
});
7676

7777
it('should handle null values', () => {
7878
const d: DataRow[] = [{ name: null, age: undefined, active: true }];
79-
const result = exportData(d, cols, 'json');
79+
const result = exportData(d, cols, 'json') as string;
8080
const parsed = JSON.parse(result);
8181
expect(parsed[0].name).toBeNull();
8282
expect(parsed[0].age).toBeNull();
@@ -91,23 +91,23 @@ describe('exportData', () => {
9191

9292
it('should export Date objects as ISO 8601 in CSV', () => {
9393
const d: DataRow[] = [{ created: new Date('2024-03-15'), updated: new Date('2024-03-15T10:30:00Z') }];
94-
const result = exportData(d, dateCols, 'csv');
94+
const result = exportData(d, dateCols, 'csv') as string;
9595
const lines = result.split('\n');
9696
expect(lines[1]).toContain('2024-03-15');
9797
expect(lines[1]).toContain('T');
9898
});
9999

100100
it('should export Date objects as ISO 8601 in JSON', () => {
101101
const d: DataRow[] = [{ created: new Date('2024-03-15'), updated: new Date('2024-03-15T10:30:00Z') }];
102-
const result = exportData(d, dateCols, 'json');
102+
const result = exportData(d, dateCols, 'json') as string;
103103
const parsed = JSON.parse(result);
104104
expect(parsed[0].created).toMatch(/^\d{4}-\d{2}-\d{2}T/);
105105
expect(parsed[0].updated).toMatch(/^\d{4}-\d{2}-\d{2}T/);
106106
});
107107

108108
it('should handle string date values as-is', () => {
109109
const d: DataRow[] = [{ created: '2024-03-15', updated: '2024-03-15T10:30:00' }];
110-
const result = exportData(d, dateCols, 'csv');
110+
const result = exportData(d, dateCols, 'csv') as string;
111111
const lines = result.split('\n');
112112
expect(lines[1]).toBe('2024-03-15,2024-03-15T10:30:00');
113113
});

src/export/export.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import type { ColumnDefinition, DataRow } from '../models/types.js';
2+
import { buildXlsx } from './xlsx-writer.js';
23

3-
export type ExportFormat = 'csv' | 'tsv' | 'json';
4+
export type ExportFormat = 'csv' | 'tsv' | 'json' | 'xlsx';
45

56
/**
67
* Export data to the specified format.
8+
* Returns string for text formats (csv/tsv/json) or Uint8Array for xlsx.
79
*/
810
export function exportData(
911
data: DataRow[],
1012
columns: ColumnDefinition[],
1113
format: ExportFormat
12-
): string {
14+
): string | Uint8Array {
1315
switch (format) {
1416
case 'csv':
1517
return exportDelimited(data, columns, ',');
1618
case 'tsv':
1719
return exportDelimited(data, columns, '\t');
1820
case 'json':
1921
return exportJson(data, columns);
22+
case 'xlsx':
23+
return buildXlsx(data, columns);
2024
}
2125
}
2226

@@ -70,7 +74,7 @@ function exportJson(data: DataRow[], columns: ColumnDefinition[]): string {
7074
/**
7175
* Trigger a file download in the browser.
7276
*/
73-
export function downloadFile(content: string, filename: string, mimeType: string): void {
77+
export function downloadFile(content: string | Uint8Array, filename: string, mimeType: string): void {
7478
const blob = new Blob([content], { type: mimeType });
7579
const url = URL.createObjectURL(blob);
7680
const a = document.createElement('a');
@@ -87,12 +91,14 @@ const MIME_TYPES: Record<ExportFormat, string> = {
8791
csv: 'text/csv;charset=utf-8',
8892
tsv: 'text/tab-separated-values;charset=utf-8',
8993
json: 'application/json;charset=utf-8',
94+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
9095
};
9196

9297
const EXTENSIONS: Record<ExportFormat, string> = {
9398
csv: '.csv',
9499
tsv: '.tsv',
95100
json: '.json',
101+
xlsx: '.xlsx',
96102
};
97103

98104
export function getExportMimeType(format: ExportFormat): string {

0 commit comments

Comments
 (0)