@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21

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 (145) hide show
  1. package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/chunk-DCODCLWI.js +234 -0
  3. package/dist/chunk-DCODCLWI.js.map +1 -0
  4. package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
  5. package/dist/chunk-EKAUQAAX.js.map +1 -0
  6. package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
  7. package/dist/chunk-KNT5AZ2O.js.map +1 -0
  8. package/dist/chunk-RM5XS6NR.js +113 -0
  9. package/dist/chunk-RM5XS6NR.js.map +1 -0
  10. package/dist/chunk-T32UHMZA.js +282 -0
  11. package/dist/chunk-T32UHMZA.js.map +1 -0
  12. package/dist/export-gif.d.ts +113 -0
  13. package/dist/export-gif.js +21 -0
  14. package/dist/export-gif.js.map +1 -0
  15. package/dist/index.d.ts +283 -24
  16. package/dist/index.js +2012 -763
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.js +7 -69
  21. package/dist/static.js.map +1 -1
  22. package/dist/story/index.d.ts +3 -2
  23. package/dist/story/index.js +5 -3
  24. package/dist/story/index.js.map +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/styles.css.map +1 -1
  27. package/package.json +14 -6
  28. package/dist/chunk-5EOV5JQZ.js.map +0 -1
  29. package/dist/chunk-ENXSUXC5.js.map +0 -1
  30. package/src/__test-fixtures__/dom.ts +0 -70
  31. package/src/__test-fixtures__/specs.ts +0 -214
  32. package/src/__tests__/animation.test.ts +0 -358
  33. package/src/__tests__/annotations-render.test.ts +0 -201
  34. package/src/__tests__/compound-labels.test.ts +0 -122
  35. package/src/__tests__/continuous-legend.test.ts +0 -91
  36. package/src/__tests__/crosshair.test.ts +0 -130
  37. package/src/__tests__/edit-events.test.ts +0 -902
  38. package/src/__tests__/events.test.ts +0 -387
  39. package/src/__tests__/export.test.ts +0 -241
  40. package/src/__tests__/font-race.test.ts +0 -199
  41. package/src/__tests__/gradient-ids.test.ts +0 -159
  42. package/src/__tests__/gradient-utils.test.ts +0 -125
  43. package/src/__tests__/known-bugs-render.test.ts +0 -183
  44. package/src/__tests__/map.test.ts +0 -293
  45. package/src/__tests__/mount.test.ts +0 -357
  46. package/src/__tests__/pattern-fills.test.ts +0 -105
  47. package/src/__tests__/resize-timing.test.ts +0 -218
  48. package/src/__tests__/sankey.test.ts +0 -133
  49. package/src/__tests__/selection-events.test.ts +0 -869
  50. package/src/__tests__/series-search.test.ts +0 -311
  51. package/src/__tests__/static.test.ts +0 -141
  52. package/src/__tests__/svg-renderer.test.ts +0 -895
  53. package/src/__tests__/table-keyboard.test.ts +0 -361
  54. package/src/__tests__/table-mount.test.ts +0 -639
  55. package/src/__tests__/tilemap.test.ts +0 -158
  56. package/src/__tests__/tooltip.test.ts +0 -337
  57. package/src/__tests__/transition.test.ts +0 -1777
  58. package/src/__tests__/you-draw-it.test.ts +0 -273
  59. package/src/animation-vars.ts +0 -42
  60. package/src/animation.ts +0 -76
  61. package/src/barlist-mount.ts +0 -383
  62. package/src/barlist-renderer.ts +0 -267
  63. package/src/dom-helpers.ts +0 -17
  64. package/src/export.ts +0 -482
  65. package/src/gradient-utils.ts +0 -138
  66. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  67. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  68. package/src/graph/__tests__/interaction.test.ts +0 -205
  69. package/src/graph/__tests__/keyboard.test.ts +0 -653
  70. package/src/graph/__tests__/search.test.ts +0 -88
  71. package/src/graph/__tests__/simulation.test.ts +0 -233
  72. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  73. package/src/graph/__tests__/zoom.test.ts +0 -203
  74. package/src/graph/canvas-renderer.ts +0 -747
  75. package/src/graph/interaction.ts +0 -367
  76. package/src/graph/keyboard.ts +0 -208
  77. package/src/graph/search.ts +0 -50
  78. package/src/graph/simulation-worker-url.ts +0 -12
  79. package/src/graph/simulation-worker.ts +0 -278
  80. package/src/graph/simulation.ts +0 -422
  81. package/src/graph/spatial-index.ts +0 -121
  82. package/src/graph/types.ts +0 -47
  83. package/src/graph/worker-protocol.ts +0 -70
  84. package/src/graph/zoom.ts +0 -134
  85. package/src/graph-mount.ts +0 -888
  86. package/src/index.ts +0 -76
  87. package/src/interactions/chart-events.ts +0 -139
  88. package/src/interactions/crosshair.ts +0 -233
  89. package/src/interactions/drag-handler.ts +0 -175
  90. package/src/interactions/editing-drags.ts +0 -515
  91. package/src/interactions/index.ts +0 -25
  92. package/src/interactions/keyboard-nav.ts +0 -111
  93. package/src/interactions/legend-interaction.ts +0 -46
  94. package/src/interactions/selection.ts +0 -280
  95. package/src/interactions/tooltip-events.ts +0 -72
  96. package/src/map-camera.ts +0 -88
  97. package/src/map-mount.ts +0 -701
  98. package/src/map-renderer.ts +0 -338
  99. package/src/map-transition.ts +0 -83
  100. package/src/measure-text.ts +0 -134
  101. package/src/mount.ts +0 -1382
  102. package/src/pattern-utils.ts +0 -110
  103. package/src/renderers/annotations.ts +0 -388
  104. package/src/renderers/axes.ts +0 -351
  105. package/src/renderers/brand.ts +0 -87
  106. package/src/renderers/chrome.ts +0 -196
  107. package/src/renderers/endpoint-labels.ts +0 -150
  108. package/src/renderers/legend.ts +0 -311
  109. package/src/renderers/marks.ts +0 -561
  110. package/src/renderers/metrics.ts +0 -63
  111. package/src/renderers/svg-dom.ts +0 -40
  112. package/src/renderers/table-cells.ts +0 -446
  113. package/src/resize-observer.ts +0 -46
  114. package/src/sankey-mount.ts +0 -622
  115. package/src/sankey-renderer.ts +0 -614
  116. package/src/series-search.ts +0 -392
  117. package/src/static.ts +0 -251
  118. package/src/story/__tests__/camera-math.test.ts +0 -145
  119. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  120. package/src/story/__tests__/progress-math.test.ts +0 -95
  121. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  122. package/src/story/__tests__/tween.test.ts +0 -159
  123. package/src/story/camera-math.ts +0 -125
  124. package/src/story/create-chart-story.ts +0 -353
  125. package/src/story/crossfade.ts +0 -70
  126. package/src/story/index.ts +0 -39
  127. package/src/story/progress-math.ts +0 -85
  128. package/src/story/resolve-camera-target.ts +0 -119
  129. package/src/story/scroll-driver.ts +0 -206
  130. package/src/story/story-camera.ts +0 -49
  131. package/src/story/tween.ts +0 -112
  132. package/src/story/types.ts +0 -76
  133. package/src/svg-ids.ts +0 -23
  134. package/src/svg-renderer.ts +0 -383
  135. package/src/table-keyboard.ts +0 -266
  136. package/src/table-mount.ts +0 -567
  137. package/src/table-renderer.ts +0 -414
  138. package/src/text-edit-overlay.ts +0 -255
  139. package/src/theme-tokens.ts +0 -51
  140. package/src/tilemap-mount.ts +0 -511
  141. package/src/tilemap-renderer.ts +0 -396
  142. package/src/tooltip.ts +0 -177
  143. package/src/transition.ts +0 -2816
  144. package/src/you-draw-it.ts +0 -480
  145. /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
@@ -1,567 +0,0 @@
1
- /**
2
- * Table mount API: the main entry point for vanilla JS table usage.
3
- *
4
- * createTable() takes a container, spec, and options, compiles the table,
5
- * renders it as HTML, sets up responsive resizing, sort/search/pagination
6
- * interactivity, and returns a TableInstance with update/resize/export/destroy.
7
- *
8
- * Supports both controlled and uncontrolled modes:
9
- * - Uncontrolled (default): manages sort/search/page internally
10
- * - Controlled: reads state from externalState, fires onStateChange
11
- */
12
-
13
- import type {
14
- CompileTableOptions,
15
- DarkMode,
16
- SortState,
17
- TableLayout,
18
- TableSpec,
19
- ThemeConfig,
20
- } from '@opendata-ai/openchart-core';
21
- import { getBreakpoint } from '@opendata-ai/openchart-core';
22
- import { compileTable } from '@opendata-ai/openchart-engine';
23
- import { setupTableAnimationCleanup } from './animation';
24
- import { observeResize } from './resize-observer';
25
- import { attachKeyboardNav } from './table-keyboard';
26
- import { renderTable } from './table-renderer';
27
-
28
- // ---------------------------------------------------------------------------
29
- // Types
30
- // ---------------------------------------------------------------------------
31
-
32
- export interface TableState {
33
- sort: SortState | null;
34
- search: string;
35
- page: number;
36
- }
37
-
38
- export interface TableMountOptions {
39
- theme?: ThemeConfig;
40
- darkMode?: DarkMode;
41
- responsive?: boolean;
42
- /** Show the tryOpenData.ai watermark. Defaults to true. */
43
- watermark?: boolean;
44
- onRowClick?: (row: Record<string, unknown>) => void;
45
- onStateChange?: (state: TableState) => void;
46
- externalState?: { sort?: SortState | null; search?: string; page?: number };
47
- }
48
-
49
- export interface TableInstance {
50
- update(spec: TableSpec): void;
51
- resize(): void;
52
- export(format: 'csv'): string;
53
- getState(): TableState;
54
- setState(partial: Partial<TableState>): void;
55
- destroy(): void;
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Dark mode resolution
60
- // ---------------------------------------------------------------------------
61
-
62
- function resolveDarkMode(mode?: DarkMode): boolean {
63
- if (mode === 'force') return true;
64
- if (mode === 'off' || mode === undefined) return false;
65
- if (typeof window !== 'undefined' && window.matchMedia) {
66
- return window.matchMedia('(prefers-color-scheme: dark)').matches;
67
- }
68
- return false;
69
- }
70
-
71
- // ---------------------------------------------------------------------------
72
- // CSV export
73
- // ---------------------------------------------------------------------------
74
-
75
- function csvEscape(value: string): string {
76
- if (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r')) {
77
- return `"${value.replace(/"/g, '""')}"`;
78
- }
79
- return value;
80
- }
81
-
82
- // ---------------------------------------------------------------------------
83
- // Sort cycling
84
- // ---------------------------------------------------------------------------
85
-
86
- /**
87
- * Cycle sort state: clicking same column cycles none -> asc -> desc -> none.
88
- * Clicking a different column resets to asc.
89
- */
90
- function cycleSort(current: SortState | null, column: string): SortState | null {
91
- if (!current || current.column !== column) {
92
- return { column, direction: 'asc' };
93
- }
94
- if (current.direction === 'asc') {
95
- return { column, direction: 'desc' };
96
- }
97
- // desc -> none
98
- return null;
99
- }
100
-
101
- // ---------------------------------------------------------------------------
102
- // Main API
103
- // ---------------------------------------------------------------------------
104
-
105
- /**
106
- * Create a table instance from a spec and mount it into a container.
107
- *
108
- * @param container - The DOM element to render into.
109
- * @param spec - The table spec.
110
- * @param options - Mount options.
111
- * @returns A TableInstance with update/resize/export/destroy methods.
112
- */
113
- export function createTable(
114
- container: HTMLElement,
115
- spec: TableSpec,
116
- options?: TableMountOptions,
117
- ): TableInstance {
118
- let currentSpec = spec;
119
- let currentLayout: TableLayout;
120
- let wrapperElement: HTMLElement | null = null;
121
- let disconnectResize: (() => void) | null = null;
122
- let cleanupKeyboard: (() => void) | null = null;
123
- let cleanupAnimations: (() => void) | null = null;
124
- let isFirstRender = true;
125
- let destroyed = false;
126
-
127
- // Internal state (used in uncontrolled mode)
128
- const internalState: TableState = {
129
- sort: null,
130
- search: '',
131
- page: 0,
132
- };
133
-
134
- // Debounce timers
135
- let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
136
- let resizeDebounceTimer: ReturnType<typeof setTimeout> | null = null;
137
-
138
- const isControlled = options?.externalState !== undefined;
139
-
140
- function getState(): TableState {
141
- if (isControlled && options?.externalState) {
142
- return {
143
- sort: options.externalState.sort ?? null,
144
- search: options.externalState.search ?? '',
145
- page: options.externalState.page ?? 0,
146
- };
147
- }
148
- return { ...internalState };
149
- }
150
-
151
- function updateState(partial: Partial<TableState>): void {
152
- if (isControlled) {
153
- // In controlled mode, notify parent
154
- const current = getState();
155
- const next: TableState = {
156
- sort: partial.sort !== undefined ? partial.sort : current.sort,
157
- search: partial.search !== undefined ? partial.search : current.search,
158
- page: partial.page !== undefined ? partial.page : current.page,
159
- };
160
- options?.onStateChange?.(next);
161
- } else {
162
- // In uncontrolled mode, update internal state
163
- if (partial.sort !== undefined) internalState.sort = partial.sort;
164
- if (partial.search !== undefined) internalState.search = partial.search;
165
- if (partial.page !== undefined) internalState.page = partial.page;
166
- options?.onStateChange?.({ ...internalState });
167
- }
168
- }
169
-
170
- function compile(): TableLayout {
171
- const state = getState();
172
- const darkMode = resolveDarkMode(options?.darkMode);
173
- const { width } = getContainerDimensions();
174
-
175
- const compileOpts: CompileTableOptions = {
176
- width,
177
- height: 600,
178
- theme: options?.theme,
179
- darkMode,
180
- watermark: options?.watermark,
181
- sort: state.sort ?? undefined,
182
- search: state.search || undefined,
183
- page: state.page,
184
- };
185
-
186
- return compileTable(currentSpec, compileOpts);
187
- }
188
-
189
- function getContainerDimensions(): { width: number; height: number } {
190
- const rect = container.getBoundingClientRect();
191
- return {
192
- width: Math.max(rect.width || 600, 100),
193
- height: Math.max(rect.height || 400, 100),
194
- };
195
- }
196
-
197
- /**
198
- * Announce a message to screen readers via the live region.
199
- */
200
- function announce(message: string): void {
201
- if (!wrapperElement) return;
202
- const liveRegion = wrapperElement.querySelector('.oc-table-live-region');
203
- if (liveRegion) {
204
- liveRegion.textContent = message;
205
- }
206
- }
207
-
208
- /**
209
- * Apply responsive breakpoint class based on container width.
210
- * Only auto-adds compact mode at small sizes. Never removes compact
211
- * if the spec explicitly requests it (layout.compact === true).
212
- */
213
- function applyBreakpointClass(): void {
214
- if (!wrapperElement) return;
215
- const { width } = getContainerDimensions();
216
- const bp = getBreakpoint(width);
217
-
218
- if (bp === 'compact' || bp === 'medium') {
219
- wrapperElement.classList.add('oc-table--compact');
220
- } else if (!currentLayout?.compact) {
221
- // Only remove compact if the spec didn't explicitly request it
222
- wrapperElement.classList.remove('oc-table--compact');
223
- }
224
- }
225
-
226
- function render(): void {
227
- if (destroyed) return;
228
-
229
- try {
230
- // Cancel any in-flight animations before re-rendering
231
- if (cleanupAnimations) {
232
- cleanupAnimations();
233
- cleanupAnimations = null;
234
- }
235
-
236
- // Clean up previous keyboard nav
237
- if (cleanupKeyboard) {
238
- cleanupKeyboard();
239
- cleanupKeyboard = null;
240
- }
241
-
242
- // Clean up previous render
243
- if (wrapperElement?.parentNode) {
244
- wrapperElement.parentNode.removeChild(wrapperElement);
245
- wrapperElement = null;
246
- }
247
-
248
- currentLayout = compile();
249
- const shouldAnimate = isFirstRender && !!currentLayout.animation?.enter;
250
- wrapperElement = renderTable(currentLayout, container, { animate: shouldAnimate });
251
-
252
- // Set up animation cleanup on first animated render
253
- if (shouldAnimate && wrapperElement) {
254
- cleanupAnimations = setupTableAnimationCleanup(wrapperElement);
255
- }
256
- if (isFirstRender) {
257
- isFirstRender = false;
258
- }
259
-
260
- // Apply dark mode class
261
- const isDark = resolveDarkMode(options?.darkMode);
262
- if (isDark) {
263
- container.classList.add('oc-dark');
264
- } else {
265
- container.classList.remove('oc-dark');
266
- }
267
-
268
- // Apply responsive breakpoint
269
- applyBreakpointClass();
270
-
271
- // Add clickable class if onRowClick is provided
272
- if (options?.onRowClick) {
273
- wrapperElement.classList.add('oc-table--clickable');
274
- }
275
-
276
- // Wire up event handlers
277
- wireEvents();
278
-
279
- // Wire up keyboard navigation
280
- if (wrapperElement) {
281
- cleanupKeyboard = attachKeyboardNav({
282
- wrapper: wrapperElement,
283
- onSort: (columnKey: string) => {
284
- const state = getState();
285
- const newSort = cycleSort(state.sort, columnKey);
286
- updateState({ sort: newSort, page: 0 });
287
-
288
- // Announce sort change
289
- if (newSort) {
290
- const dir = newSort.direction === 'asc' ? 'ascending' : 'descending';
291
- announce(`Sorted by ${columnKey} ${dir}`);
292
- } else {
293
- announce('Sort cleared');
294
- }
295
-
296
- if (!isControlled) {
297
- rerender();
298
- }
299
- },
300
- onClearSearch: () => {
301
- updateState({ search: '', page: 0 });
302
- if (!isControlled) {
303
- rerender();
304
- }
305
- },
306
- onAnnounce: announce,
307
- });
308
- }
309
- } catch (err) {
310
- console.error('[viz] Table render failed:', err);
311
- }
312
- }
313
-
314
- function wireEvents(): void {
315
- if (!wrapperElement) return;
316
-
317
- // Sort click handlers on thead buttons
318
- const sortBtns = wrapperElement.querySelectorAll('[data-sort-column]');
319
- for (const btn of sortBtns) {
320
- btn.addEventListener('click', handleSortClick);
321
- }
322
-
323
- // Search input
324
- const searchInput = wrapperElement.querySelector(
325
- '.oc-table-search input',
326
- ) as HTMLInputElement | null;
327
- if (searchInput) {
328
- searchInput.addEventListener('input', handleSearchInput);
329
- }
330
-
331
- // Pagination buttons
332
- const pageButtons = wrapperElement.querySelectorAll('[data-page-action]');
333
- for (const btn of pageButtons) {
334
- btn.addEventListener('click', handlePageClick);
335
- }
336
-
337
- // Row click
338
- if (options?.onRowClick) {
339
- const rows = wrapperElement.querySelectorAll('tbody tr');
340
- for (const row of rows) {
341
- row.addEventListener('click', handleRowClick);
342
- }
343
- }
344
- }
345
-
346
- function handleSortClick(e: Event): void {
347
- const btn = e.currentTarget as HTMLElement;
348
- const column = btn.getAttribute('data-sort-column');
349
- if (!column) return;
350
-
351
- const state = getState();
352
- const newSort = cycleSort(state.sort, column);
353
-
354
- updateState({ sort: newSort, page: 0 });
355
-
356
- // Announce sort change for screen readers
357
- if (newSort) {
358
- const dir = newSort.direction === 'asc' ? 'ascending' : 'descending';
359
- announce(`Sorted by ${column} ${dir}`);
360
- } else {
361
- announce('Sort cleared');
362
- }
363
-
364
- if (!isControlled) {
365
- rerender();
366
- }
367
- }
368
-
369
- function handleSearchInput(e: Event): void {
370
- const input = e.target as HTMLInputElement;
371
- const query = input.value;
372
-
373
- if (searchDebounceTimer !== null) {
374
- clearTimeout(searchDebounceTimer);
375
- }
376
-
377
- searchDebounceTimer = setTimeout(() => {
378
- searchDebounceTimer = null;
379
- updateState({ search: query, page: 0 });
380
-
381
- if (!isControlled) {
382
- rerender();
383
- // Announce search result count
384
- const rowCount = currentLayout?.rows?.length ?? 0;
385
- if (query) {
386
- announce(`${rowCount} result${rowCount !== 1 ? 's' : ''} found`);
387
- }
388
- }
389
- }, 200);
390
- }
391
-
392
- function handlePageClick(e: Event): void {
393
- const btn = e.currentTarget as HTMLElement;
394
- const action = btn.getAttribute('data-page-action');
395
- const state = getState();
396
-
397
- if (action === 'prev' && state.page > 0) {
398
- updateState({ page: state.page - 1 });
399
- } else if (action === 'next') {
400
- updateState({ page: state.page + 1 });
401
- }
402
-
403
- if (!isControlled) {
404
- rerender();
405
- }
406
- }
407
-
408
- function handleRowClick(e: Event): void {
409
- const tr = e.currentTarget as HTMLElement;
410
- const rowId = tr.getAttribute('data-row-id');
411
- if (!rowId || !currentLayout) return;
412
-
413
- const row = currentLayout.rows.find((r) => r.id === rowId);
414
- if (row) {
415
- options?.onRowClick?.(row.data);
416
- }
417
- }
418
-
419
- /**
420
- * Re-render the table, preserving search input focus across the DOM rebuild.
421
- */
422
- function rerender(): void {
423
- if (destroyed) return;
424
-
425
- // Capture current search input state before re-render
426
- const searchInput = wrapperElement?.querySelector(
427
- '.oc-table-search input',
428
- ) as HTMLInputElement | null;
429
- const hadFocus = searchInput && document.activeElement === searchInput;
430
- const selectionStart = searchInput?.selectionStart ?? 0;
431
- const selectionEnd = searchInput?.selectionEnd ?? 0;
432
-
433
- render();
434
-
435
- // Restore search focus after re-render
436
- if (hadFocus) {
437
- const newInput = wrapperElement?.querySelector(
438
- '.oc-table-search input',
439
- ) as HTMLInputElement | null;
440
- if (newInput) {
441
- newInput.focus();
442
- newInput.setSelectionRange(selectionStart, selectionEnd);
443
- }
444
- }
445
- }
446
-
447
- function update(newSpec: TableSpec): void {
448
- if (destroyed) return;
449
- currentSpec = newSpec;
450
- render();
451
- }
452
-
453
- function resize(): void {
454
- if (destroyed) return;
455
- if (cleanupAnimations) return; // Skip resize during entrance animation
456
- render();
457
- }
458
-
459
- function doExport(format: 'csv'): string {
460
- if (format !== 'csv') {
461
- throw new Error(`Unsupported export format: ${format}`);
462
- }
463
-
464
- // Export all filtered/sorted data (not just current page)
465
- // Re-compile without pagination
466
- const state = getState();
467
- const darkMode = resolveDarkMode(options?.darkMode);
468
- const { width } = getContainerDimensions();
469
-
470
- const fullLayout = compileTable(currentSpec, {
471
- width,
472
- height: 600,
473
- theme: options?.theme,
474
- darkMode,
475
- sort: state.sort ?? undefined,
476
- search: state.search || undefined,
477
- // No page/pageSize: get all rows
478
- });
479
-
480
- const headers = fullLayout.columns.map((c) => c.label);
481
- const csvRows = [headers.map(csvEscape).join(',')];
482
-
483
- for (const row of fullLayout.rows) {
484
- const values = row.cells.map((cell) => csvEscape(cell.formattedValue));
485
- csvRows.push(values.join(','));
486
- }
487
-
488
- return csvRows.join('\n');
489
- }
490
-
491
- function setState(partial: Partial<TableState>): void {
492
- if (destroyed) return;
493
-
494
- // In controlled mode compile() reads from options.externalState, so
495
- // updating internalState alone would be ignored. Push the new values onto
496
- // externalState (the source getState() reads) so the render reflects them.
497
- if (isControlled && options?.externalState) {
498
- if (partial.sort !== undefined) options.externalState.sort = partial.sort;
499
- if (partial.search !== undefined) options.externalState.search = partial.search;
500
- if (partial.page !== undefined) options.externalState.page = partial.page;
501
- } else {
502
- if (partial.sort !== undefined) internalState.sort = partial.sort;
503
- if (partial.search !== undefined) internalState.search = partial.search;
504
- if (partial.page !== undefined) internalState.page = partial.page;
505
- }
506
-
507
- render();
508
- }
509
-
510
- function destroy(): void {
511
- if (destroyed) return;
512
- destroyed = true;
513
-
514
- if (cleanupAnimations) {
515
- cleanupAnimations();
516
- cleanupAnimations = null;
517
- }
518
- if (cleanupKeyboard) {
519
- cleanupKeyboard();
520
- cleanupKeyboard = null;
521
- }
522
- if (searchDebounceTimer !== null) {
523
- clearTimeout(searchDebounceTimer);
524
- searchDebounceTimer = null;
525
- }
526
- if (resizeDebounceTimer !== null) {
527
- clearTimeout(resizeDebounceTimer);
528
- resizeDebounceTimer = null;
529
- }
530
- if (disconnectResize) {
531
- disconnectResize();
532
- disconnectResize = null;
533
- }
534
- if (wrapperElement?.parentNode) {
535
- wrapperElement.parentNode.removeChild(wrapperElement);
536
- wrapperElement = null;
537
- }
538
- container.classList.remove('oc-dark');
539
- }
540
-
541
- // Initial render
542
- render();
543
-
544
- // Set up responsive resize with breakpoint detection
545
- if (options?.responsive !== false) {
546
- disconnectResize = observeResize(container, () => {
547
- if (resizeDebounceTimer !== null) {
548
- clearTimeout(resizeDebounceTimer);
549
- }
550
- resizeDebounceTimer = setTimeout(() => {
551
- resizeDebounceTimer = null;
552
- // Update breakpoint class without full re-render when possible
553
- applyBreakpointClass();
554
- resize();
555
- }, 100);
556
- });
557
- }
558
-
559
- return {
560
- update,
561
- resize,
562
- export: doExport,
563
- getState,
564
- setState,
565
- destroy,
566
- };
567
- }