@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.
- package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
- package/dist/chunk-KNT5AZ2O.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
package/src/table-renderer.ts
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Table renderer: produces semantic HTML from a TableLayout.
|
|
3
|
-
*
|
|
4
|
-
* renderTable() creates the full DOM structure: chrome, search bar,
|
|
5
|
-
* scrollable table with sticky column support, pagination, and footer.
|
|
6
|
-
* The returned element replaces or appends to the given container.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { ResolvedColumn, TableLayout, TableRow } from '@opendata-ai/openchart-core';
|
|
10
|
-
import { BRAND_FONT_SIZE } from '@opendata-ai/openchart-core';
|
|
11
|
-
import { clampStaggerDelay } from '@opendata-ai/openchart-engine';
|
|
12
|
-
import { stampAnimationVars } from './animation-vars';
|
|
13
|
-
import { applySrOnlyStyles } from './dom-helpers';
|
|
14
|
-
import { renderCell } from './renderers/table-cells';
|
|
15
|
-
|
|
16
|
-
/** Options for renderTable(). */
|
|
17
|
-
export interface TableRenderOptions {
|
|
18
|
-
/** Whether to apply entrance animation on this render. */
|
|
19
|
-
animate?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Constants
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
const BRAND_URL = 'https://tryopendata.ai';
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Chrome rendering
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/** Create chrome (title/subtitle or source/footer) block. */
|
|
33
|
-
function renderChromeBlock(
|
|
34
|
-
layout: TableLayout,
|
|
35
|
-
position: 'header' | 'footer',
|
|
36
|
-
): HTMLDivElement | null {
|
|
37
|
-
const chrome = layout.chrome;
|
|
38
|
-
|
|
39
|
-
if (position === 'header') {
|
|
40
|
-
if (!chrome.title && !chrome.subtitle) return null;
|
|
41
|
-
|
|
42
|
-
const div = document.createElement('div');
|
|
43
|
-
div.className = 'oc-chrome';
|
|
44
|
-
|
|
45
|
-
if (chrome.title) {
|
|
46
|
-
const h = document.createElement('div');
|
|
47
|
-
h.className = 'oc-table-title';
|
|
48
|
-
h.textContent = chrome.title.text;
|
|
49
|
-
div.appendChild(h);
|
|
50
|
-
}
|
|
51
|
-
if (chrome.subtitle) {
|
|
52
|
-
const sub = document.createElement('div');
|
|
53
|
-
sub.className = 'oc-table-subtitle';
|
|
54
|
-
sub.textContent = chrome.subtitle.text;
|
|
55
|
-
div.appendChild(sub);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return div;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Footer position
|
|
62
|
-
if (!chrome.source && !chrome.footer) return null;
|
|
63
|
-
|
|
64
|
-
const div = document.createElement('div');
|
|
65
|
-
div.className = 'oc-chrome oc-chrome-footer';
|
|
66
|
-
|
|
67
|
-
if (chrome.source) {
|
|
68
|
-
const src = document.createElement('div');
|
|
69
|
-
src.className = 'oc-table-source';
|
|
70
|
-
src.textContent = chrome.source.text;
|
|
71
|
-
div.appendChild(src);
|
|
72
|
-
}
|
|
73
|
-
if (chrome.footer) {
|
|
74
|
-
const foot = document.createElement('div');
|
|
75
|
-
foot.className = 'oc-table-footer-text';
|
|
76
|
-
foot.textContent = chrome.footer.text;
|
|
77
|
-
div.appendChild(foot);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return div;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Table head
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
function renderThead(
|
|
88
|
-
columns: ResolvedColumn[],
|
|
89
|
-
sort: TableLayout['sort'],
|
|
90
|
-
): HTMLTableSectionElement {
|
|
91
|
-
const thead = document.createElement('thead');
|
|
92
|
-
const tr = document.createElement('tr');
|
|
93
|
-
tr.setAttribute('role', 'row');
|
|
94
|
-
|
|
95
|
-
for (const col of columns) {
|
|
96
|
-
const th = document.createElement('th');
|
|
97
|
-
th.setAttribute('scope', 'col');
|
|
98
|
-
th.setAttribute('role', 'columnheader');
|
|
99
|
-
th.style.textAlign = col.align;
|
|
100
|
-
th.style.width = `${col.width}px`;
|
|
101
|
-
|
|
102
|
-
// Sort state: use 'ascending'/'descending' per WAI-ARIA spec
|
|
103
|
-
let ariaSortValue: string = 'none';
|
|
104
|
-
if (sort && sort.column === col.key) {
|
|
105
|
-
ariaSortValue = sort.direction === 'asc' ? 'ascending' : 'descending';
|
|
106
|
-
}
|
|
107
|
-
th.setAttribute('aria-sort', ariaSortValue);
|
|
108
|
-
th.setAttribute('data-column', col.key);
|
|
109
|
-
|
|
110
|
-
// Label
|
|
111
|
-
const labelSpan = document.createTextNode(col.label);
|
|
112
|
-
th.appendChild(labelSpan);
|
|
113
|
-
|
|
114
|
-
// Sort button
|
|
115
|
-
if (col.sortable) {
|
|
116
|
-
const btn = document.createElement('button');
|
|
117
|
-
btn.className = 'oc-table-sort-btn';
|
|
118
|
-
btn.setAttribute('aria-label', `Sort by ${col.label}`);
|
|
119
|
-
btn.setAttribute('data-sort-column', col.key);
|
|
120
|
-
btn.type = 'button';
|
|
121
|
-
th.appendChild(btn);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
tr.appendChild(th);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
thead.appendChild(tr);
|
|
128
|
-
return thead;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
// Table body
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
function renderTbody(rows: TableRow[], columns: ResolvedColumn[]): HTMLTableSectionElement {
|
|
136
|
-
const tbody = document.createElement('tbody');
|
|
137
|
-
|
|
138
|
-
for (let r = 0; r < rows.length; r++) {
|
|
139
|
-
const row = rows[r];
|
|
140
|
-
const tr = document.createElement('tr');
|
|
141
|
-
tr.setAttribute('role', 'row');
|
|
142
|
-
tr.setAttribute('data-row-id', row.id);
|
|
143
|
-
tr.style.setProperty('--oc-row-index', String(r));
|
|
144
|
-
|
|
145
|
-
for (let c = 0; c < columns.length; c++) {
|
|
146
|
-
const cell = row.cells[c];
|
|
147
|
-
if (!cell) continue;
|
|
148
|
-
|
|
149
|
-
const td = renderCell(cell);
|
|
150
|
-
td.setAttribute('role', 'gridcell');
|
|
151
|
-
td.style.textAlign = columns[c].align;
|
|
152
|
-
tr.appendChild(td);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
tbody.appendChild(tr);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return tbody;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
// Search bar
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
|
|
165
|
-
function renderSearchBar(layout: TableLayout): HTMLDivElement | null {
|
|
166
|
-
if (!layout.search.enabled) return null;
|
|
167
|
-
|
|
168
|
-
const div = document.createElement('div');
|
|
169
|
-
div.className = 'oc-table-search';
|
|
170
|
-
|
|
171
|
-
const input = document.createElement('input');
|
|
172
|
-
input.type = 'search';
|
|
173
|
-
input.placeholder = layout.search.placeholder;
|
|
174
|
-
input.setAttribute('aria-label', 'Search table');
|
|
175
|
-
input.value = layout.search.query;
|
|
176
|
-
|
|
177
|
-
div.appendChild(input);
|
|
178
|
-
return div;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
// Pagination
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
|
|
185
|
-
function renderPagination(layout: TableLayout): HTMLDivElement | null {
|
|
186
|
-
if (!layout.pagination) return null;
|
|
187
|
-
|
|
188
|
-
const { page, pageSize, totalRows, totalPages } = layout.pagination;
|
|
189
|
-
|
|
190
|
-
const div = document.createElement('div');
|
|
191
|
-
div.className = 'oc-table-pagination';
|
|
192
|
-
|
|
193
|
-
const info = document.createElement('span');
|
|
194
|
-
info.className = 'oc-table-pagination-info';
|
|
195
|
-
|
|
196
|
-
if (totalRows === 0) {
|
|
197
|
-
info.textContent = 'No results';
|
|
198
|
-
} else {
|
|
199
|
-
const start = page * pageSize + 1;
|
|
200
|
-
const end = Math.min((page + 1) * pageSize, totalRows);
|
|
201
|
-
info.textContent = `Showing ${start}-${end} of ${totalRows}`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
div.appendChild(info);
|
|
205
|
-
|
|
206
|
-
const btnGroup = document.createElement('span');
|
|
207
|
-
btnGroup.className = 'oc-table-pagination-btns';
|
|
208
|
-
|
|
209
|
-
const prevBtn = document.createElement('button');
|
|
210
|
-
prevBtn.setAttribute('aria-label', 'Previous page');
|
|
211
|
-
prevBtn.setAttribute('data-page-action', 'prev');
|
|
212
|
-
prevBtn.textContent = 'Prev';
|
|
213
|
-
prevBtn.disabled = page <= 0;
|
|
214
|
-
btnGroup.appendChild(prevBtn);
|
|
215
|
-
|
|
216
|
-
const nextBtn = document.createElement('button');
|
|
217
|
-
nextBtn.setAttribute('aria-label', 'Next page');
|
|
218
|
-
nextBtn.setAttribute('data-page-action', 'next');
|
|
219
|
-
nextBtn.textContent = 'Next';
|
|
220
|
-
nextBtn.disabled = page >= totalPages - 1;
|
|
221
|
-
btnGroup.appendChild(nextBtn);
|
|
222
|
-
|
|
223
|
-
div.appendChild(btnGroup);
|
|
224
|
-
return div;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
// Empty state
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
function renderEmptyState(message: string): HTMLDivElement {
|
|
232
|
-
const div = document.createElement('div');
|
|
233
|
-
div.className = 'oc-table-empty';
|
|
234
|
-
div.setAttribute('aria-live', 'polite');
|
|
235
|
-
div.textContent = message;
|
|
236
|
-
return div;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
// Main render
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Render a TableLayout into a full DOM structure.
|
|
245
|
-
*
|
|
246
|
-
* @param layout - The compiled table layout.
|
|
247
|
-
* @param container - The container element to render into.
|
|
248
|
-
* @returns The wrapper element that was created.
|
|
249
|
-
*/
|
|
250
|
-
export function renderTable(
|
|
251
|
-
layout: TableLayout,
|
|
252
|
-
container: HTMLElement,
|
|
253
|
-
opts?: TableRenderOptions,
|
|
254
|
-
): HTMLElement {
|
|
255
|
-
const wrapper = document.createElement('div');
|
|
256
|
-
wrapper.className = 'oc-table-wrapper';
|
|
257
|
-
|
|
258
|
-
// Apply theme colors as CSS custom properties so table CSS picks them up.
|
|
259
|
-
// Without this, dark-background themes show invisible text since the
|
|
260
|
-
// CSS defaults (--oc-text etc.) are light-mode values.
|
|
261
|
-
const { theme, chrome } = layout;
|
|
262
|
-
if (theme) {
|
|
263
|
-
const s = wrapper.style;
|
|
264
|
-
s.setProperty('--oc-bg', theme.colors.background);
|
|
265
|
-
s.setProperty('--oc-text', theme.colors.text);
|
|
266
|
-
s.setProperty('--oc-text-secondary', theme.colors.axis ?? theme.colors.text);
|
|
267
|
-
s.setProperty('--oc-text-muted', theme.colors.axis ?? theme.colors.text);
|
|
268
|
-
s.setProperty('--oc-gridline', theme.colors.gridline);
|
|
269
|
-
s.setProperty('--oc-border', theme.colors.gridline);
|
|
270
|
-
s.setProperty('--oc-font-family', theme.fonts.family);
|
|
271
|
-
s.fontFamily = theme.fonts.family;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Set computed chrome CSS custom properties so chrome elements pick up
|
|
275
|
-
// theme-resolved values via CSS fallbacks (e.g. --oc-title-computed-size).
|
|
276
|
-
{
|
|
277
|
-
const s = wrapper.style;
|
|
278
|
-
if (chrome.title) {
|
|
279
|
-
s.setProperty('--oc-title-computed-size', `${chrome.title.style.fontSize}px`);
|
|
280
|
-
s.setProperty('--oc-title-computed-weight', String(chrome.title.style.fontWeight));
|
|
281
|
-
s.setProperty('--oc-title-computed-color', chrome.title.style.fill);
|
|
282
|
-
}
|
|
283
|
-
if (chrome.subtitle) {
|
|
284
|
-
s.setProperty('--oc-subtitle-computed-size', `${chrome.subtitle.style.fontSize}px`);
|
|
285
|
-
s.setProperty('--oc-subtitle-computed-weight', String(chrome.subtitle.style.fontWeight));
|
|
286
|
-
s.setProperty('--oc-subtitle-computed-color', chrome.subtitle.style.fill);
|
|
287
|
-
}
|
|
288
|
-
if (chrome.source) {
|
|
289
|
-
s.setProperty('--oc-source-computed-size', `${chrome.source.style.fontSize}px`);
|
|
290
|
-
s.setProperty('--oc-source-computed-color', chrome.source.style.fill);
|
|
291
|
-
}
|
|
292
|
-
if (chrome.footer) {
|
|
293
|
-
s.setProperty('--oc-footer-computed-size', `${chrome.footer.style.fontSize}px`);
|
|
294
|
-
s.setProperty('--oc-footer-computed-color', chrome.footer.style.fill);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Apply class modifiers
|
|
299
|
-
if (layout.compact) {
|
|
300
|
-
wrapper.classList.add('oc-table--compact');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Header chrome
|
|
304
|
-
const headerChrome = renderChromeBlock(layout, 'header');
|
|
305
|
-
if (headerChrome) {
|
|
306
|
-
wrapper.appendChild(headerChrome);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Search bar
|
|
310
|
-
const searchBar = renderSearchBar(layout);
|
|
311
|
-
if (searchBar) {
|
|
312
|
-
wrapper.appendChild(searchBar);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Handle empty data
|
|
316
|
-
if (layout.rows.length === 0) {
|
|
317
|
-
const message = layout.search.query ? 'No results found' : 'No data';
|
|
318
|
-
wrapper.appendChild(renderEmptyState(message));
|
|
319
|
-
} else {
|
|
320
|
-
// Scroll container
|
|
321
|
-
const scroll = document.createElement('div');
|
|
322
|
-
scroll.className = 'oc-table-scroll';
|
|
323
|
-
|
|
324
|
-
// Table
|
|
325
|
-
const table = document.createElement('table');
|
|
326
|
-
table.setAttribute('role', 'grid');
|
|
327
|
-
table.setAttribute('aria-label', layout.a11y.caption);
|
|
328
|
-
|
|
329
|
-
if (layout.stickyFirstColumn) {
|
|
330
|
-
table.classList.add('oc-table--sticky');
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Caption (screen reader only – inline styles ensure hiding even without
|
|
334
|
-
// the external stylesheet, e.g. CDN / esm.sh usage)
|
|
335
|
-
const caption = document.createElement('caption');
|
|
336
|
-
caption.className = 'oc-sr-only';
|
|
337
|
-
applySrOnlyStyles(caption);
|
|
338
|
-
caption.textContent = layout.a11y.summary;
|
|
339
|
-
table.appendChild(caption);
|
|
340
|
-
|
|
341
|
-
// Thead
|
|
342
|
-
table.appendChild(renderThead(layout.columns, layout.sort));
|
|
343
|
-
|
|
344
|
-
// Tbody
|
|
345
|
-
table.appendChild(renderTbody(layout.rows, layout.columns));
|
|
346
|
-
|
|
347
|
-
scroll.appendChild(table);
|
|
348
|
-
wrapper.appendChild(scroll);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Pagination
|
|
352
|
-
const pagination = renderPagination(layout);
|
|
353
|
-
if (pagination) {
|
|
354
|
-
wrapper.appendChild(pagination);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Footer row: source/footer chrome on the left, brand watermark on the
|
|
358
|
-
// right, laid out on a single line via flex. Built up here and appended
|
|
359
|
-
// after the live region so source text and watermark share a baseline.
|
|
360
|
-
const footerRow = document.createElement('div');
|
|
361
|
-
footerRow.className = 'oc-table-footer-row';
|
|
362
|
-
|
|
363
|
-
const footerChrome = renderChromeBlock(layout, 'footer');
|
|
364
|
-
if (footerChrome) {
|
|
365
|
-
footerRow.appendChild(footerChrome);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Live region for screen reader announcements (sort changes, search results)
|
|
369
|
-
const liveRegion = document.createElement('div');
|
|
370
|
-
liveRegion.className = 'oc-table-live-region oc-sr-only';
|
|
371
|
-
applySrOnlyStyles(liveRegion);
|
|
372
|
-
liveRegion.setAttribute('aria-live', 'polite');
|
|
373
|
-
liveRegion.setAttribute('aria-atomic', 'true');
|
|
374
|
-
liveRegion.setAttribute('role', 'status');
|
|
375
|
-
wrapper.appendChild(liveRegion);
|
|
376
|
-
|
|
377
|
-
// Brand watermark — joins the footer row, pushed to the right by the
|
|
378
|
-
// flex container so it sits on the same line as the source text.
|
|
379
|
-
if (layout.watermark) {
|
|
380
|
-
const brandColor = theme ? theme.colors.axis : '#999999';
|
|
381
|
-
const brand = document.createElement('div');
|
|
382
|
-
brand.className = 'oc-table-ref';
|
|
383
|
-
const brandLink = document.createElement('a');
|
|
384
|
-
brandLink.href = BRAND_URL;
|
|
385
|
-
brandLink.target = '_blank';
|
|
386
|
-
brandLink.rel = 'noopener';
|
|
387
|
-
brandLink.style.cssText = `font-size: ${BRAND_FONT_SIZE}px; font-weight: 600; color: ${brandColor}; opacity: 0.55; text-decoration: none; font-family: ${theme ? theme.fonts.family : 'sans-serif'};`;
|
|
388
|
-
brandLink.textContent = 'tryOpenData.ai';
|
|
389
|
-
brand.appendChild(brandLink);
|
|
390
|
-
footerRow.appendChild(brand);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Append the footer row only when it has content (source/footer/watermark).
|
|
394
|
-
if (footerRow.childElementCount > 0) {
|
|
395
|
-
wrapper.appendChild(footerRow);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Animation: stamp CSS custom properties and add oc-animate class BEFORE
|
|
399
|
-
// DOM insertion to avoid a flash of final state.
|
|
400
|
-
if (opts?.animate && layout.animation?.enter) {
|
|
401
|
-
const enterPhase = layout.animation.enter;
|
|
402
|
-
const rowCount = layout.rows.length;
|
|
403
|
-
const stagger = clampStaggerDelay(enterPhase.staggerDelay, rowCount);
|
|
404
|
-
stampAnimationVars(wrapper, {
|
|
405
|
-
duration: enterPhase.duration,
|
|
406
|
-
stagger,
|
|
407
|
-
ease: enterPhase.ease,
|
|
408
|
-
});
|
|
409
|
-
wrapper.classList.add('oc-animate');
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
container.appendChild(wrapper);
|
|
413
|
-
return wrapper;
|
|
414
|
-
}
|
package/src/text-edit-overlay.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text edit overlay: creates a positioned textarea over an SVG text element
|
|
3
|
-
* for inline editing. Handles commit (Enter), cancel (Escape), and
|
|
4
|
-
* click-outside-to-commit behavior.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface TextEditOverlayConfig {
|
|
8
|
-
/** The container div that holds the SVG. */
|
|
9
|
-
container: HTMLElement;
|
|
10
|
-
/** The root SVG element. */
|
|
11
|
-
svg: SVGSVGElement;
|
|
12
|
-
/** The SVG text element being edited. */
|
|
13
|
-
targetElement: SVGElement;
|
|
14
|
-
/** Current text content to populate the textarea. */
|
|
15
|
-
currentText: string;
|
|
16
|
-
/** Called when the user commits the edit (Enter or click outside). */
|
|
17
|
-
onCommit: (newText: string) => void;
|
|
18
|
-
/** Called when the user cancels the edit (Escape). */
|
|
19
|
-
onCancel: () => void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get the viewBox-to-pixel scale factors for an SVG element.
|
|
24
|
-
* Same approach used by the drag handlers in mount.ts.
|
|
25
|
-
*/
|
|
26
|
-
function getScale(svg: SVGSVGElement): { scaleX: number; scaleY: number } {
|
|
27
|
-
const viewBox = svg.viewBox?.baseVal;
|
|
28
|
-
const svgRect = svg.getBoundingClientRect();
|
|
29
|
-
return {
|
|
30
|
-
scaleX: viewBox?.width && svgRect.width ? svgRect.width / viewBox.width : 1,
|
|
31
|
-
scaleY: viewBox?.height && svgRect.height ? svgRect.height / viewBox.height : 1,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Read computed font styles from an SVG text element and map them to CSS.
|
|
37
|
-
*/
|
|
38
|
-
function getTextStyles(
|
|
39
|
-
targetElement: SVGElement,
|
|
40
|
-
scale: { scaleX: number; scaleY: number },
|
|
41
|
-
): {
|
|
42
|
-
fontFamily: string;
|
|
43
|
-
fontSize: string;
|
|
44
|
-
fontWeight: string;
|
|
45
|
-
color: string;
|
|
46
|
-
textAlign: string;
|
|
47
|
-
lineHeight: string;
|
|
48
|
-
} {
|
|
49
|
-
const computed = window.getComputedStyle(targetElement);
|
|
50
|
-
|
|
51
|
-
// Font size needs to be scaled from SVG viewBox units to pixel units
|
|
52
|
-
const svgFontSize = parseFloat(
|
|
53
|
-
targetElement.getAttribute('font-size') ?? computed.fontSize ?? '14',
|
|
54
|
-
);
|
|
55
|
-
const pixelFontSize = svgFontSize * scale.scaleY;
|
|
56
|
-
|
|
57
|
-
const fontFamily = targetElement.getAttribute('font-family') ?? computed.fontFamily ?? 'inherit';
|
|
58
|
-
const fontWeight = targetElement.getAttribute('font-weight') ?? computed.fontWeight ?? '400';
|
|
59
|
-
|
|
60
|
-
// Get fill color from inline style or attribute
|
|
61
|
-
const fill =
|
|
62
|
-
(targetElement as SVGElement & ElementCSSInlineStyle).style.getPropertyValue('fill') ||
|
|
63
|
-
targetElement.getAttribute('fill') ||
|
|
64
|
-
computed.color ||
|
|
65
|
-
'#000';
|
|
66
|
-
|
|
67
|
-
// Map SVG text-anchor to CSS text-align
|
|
68
|
-
const textAnchor = targetElement.getAttribute('text-anchor') ?? 'start';
|
|
69
|
-
let textAlign = 'left';
|
|
70
|
-
if (textAnchor === 'middle') textAlign = 'center';
|
|
71
|
-
else if (textAnchor === 'end') textAlign = 'right';
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
fontFamily,
|
|
75
|
-
fontSize: `${pixelFontSize}px`,
|
|
76
|
-
fontWeight,
|
|
77
|
-
color: fill,
|
|
78
|
-
textAlign,
|
|
79
|
-
lineHeight: '1.3',
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Compute the pixel position and size for the textarea based on the
|
|
85
|
-
* target SVG element's bounding box.
|
|
86
|
-
*/
|
|
87
|
-
function computePosition(
|
|
88
|
-
targetElement: SVGElement,
|
|
89
|
-
svg: SVGSVGElement,
|
|
90
|
-
container: HTMLElement,
|
|
91
|
-
): { top: number; left: number; width: number; height: number } {
|
|
92
|
-
const bbox = (targetElement as SVGGraphicsElement).getBBox();
|
|
93
|
-
const scale = getScale(svg);
|
|
94
|
-
const svgRect = svg.getBoundingClientRect();
|
|
95
|
-
const containerRect = container.getBoundingClientRect();
|
|
96
|
-
|
|
97
|
-
// SVG viewBox coords -> pixel coords relative to the container
|
|
98
|
-
const padding = 4;
|
|
99
|
-
const left = bbox.x * scale.scaleX + (svgRect.left - containerRect.left) - padding;
|
|
100
|
-
const top = bbox.y * scale.scaleY + (svgRect.top - containerRect.top) - padding;
|
|
101
|
-
const width = bbox.width * scale.scaleX + padding * 2;
|
|
102
|
-
const height = bbox.height * scale.scaleY + padding * 2;
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
top: Math.max(0, top),
|
|
106
|
-
left: Math.max(0, left),
|
|
107
|
-
width: Math.max(width, 60),
|
|
108
|
-
height: Math.max(height, 24),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Create an inline text editing overlay positioned over an SVG text element.
|
|
114
|
-
* Returns an object with a `destroy()` method to clean up.
|
|
115
|
-
*/
|
|
116
|
-
export function createTextEditOverlay(config: TextEditOverlayConfig): { destroy: () => void } {
|
|
117
|
-
const { container, svg, targetElement, currentText, onCommit, onCancel } = config;
|
|
118
|
-
|
|
119
|
-
let destroyed = false;
|
|
120
|
-
|
|
121
|
-
// Hide the underlying SVG text element (not display:none to preserve layout)
|
|
122
|
-
const originalOpacity = targetElement.getAttribute('opacity');
|
|
123
|
-
targetElement.setAttribute('opacity', '0');
|
|
124
|
-
|
|
125
|
-
// Compute position and styles
|
|
126
|
-
const scale = getScale(svg);
|
|
127
|
-
const styles = getTextStyles(targetElement, scale);
|
|
128
|
-
const position = computePosition(targetElement, svg, container);
|
|
129
|
-
|
|
130
|
-
// Create the textarea
|
|
131
|
-
const textarea = document.createElement('textarea');
|
|
132
|
-
textarea.value = currentText;
|
|
133
|
-
|
|
134
|
-
// Ensure container is positioned so absolute children work
|
|
135
|
-
const containerPosition = window.getComputedStyle(container).position;
|
|
136
|
-
const containerWasStatic = containerPosition === 'static';
|
|
137
|
-
if (containerWasStatic) {
|
|
138
|
-
container.style.position = 'relative';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Style the textarea to match the SVG text
|
|
142
|
-
Object.assign(textarea.style, {
|
|
143
|
-
position: 'absolute',
|
|
144
|
-
top: `${position.top}px`,
|
|
145
|
-
left: `${position.left}px`,
|
|
146
|
-
width: `${position.width}px`,
|
|
147
|
-
minHeight: `${position.height}px`,
|
|
148
|
-
fontFamily: styles.fontFamily,
|
|
149
|
-
fontSize: styles.fontSize,
|
|
150
|
-
fontWeight: styles.fontWeight,
|
|
151
|
-
color: styles.color,
|
|
152
|
-
textAlign: styles.textAlign,
|
|
153
|
-
lineHeight: styles.lineHeight,
|
|
154
|
-
padding: '2px 4px',
|
|
155
|
-
margin: '0',
|
|
156
|
-
border: '1px solid rgba(79, 70, 229, 0.4)', // intentionally 0.4, not --oc-editable-hover (0.35)
|
|
157
|
-
borderRadius: '3px',
|
|
158
|
-
background: 'rgba(255, 255, 255, 0.95)',
|
|
159
|
-
outline: 'none',
|
|
160
|
-
resize: 'none',
|
|
161
|
-
overflow: 'hidden',
|
|
162
|
-
boxSizing: 'border-box',
|
|
163
|
-
zIndex: '10000',
|
|
164
|
-
// Remove default textarea appearance
|
|
165
|
-
WebkitAppearance: 'none',
|
|
166
|
-
appearance: 'none',
|
|
167
|
-
} as Record<string, string>);
|
|
168
|
-
|
|
169
|
-
container.appendChild(textarea);
|
|
170
|
-
|
|
171
|
-
// Auto-select all text
|
|
172
|
-
textarea.focus();
|
|
173
|
-
textarea.select();
|
|
174
|
-
|
|
175
|
-
// Cleanup function
|
|
176
|
-
function destroy(): void {
|
|
177
|
-
if (destroyed) return;
|
|
178
|
-
destroyed = true;
|
|
179
|
-
|
|
180
|
-
// Restore SVG text visibility
|
|
181
|
-
if (originalOpacity !== null) {
|
|
182
|
-
targetElement.setAttribute('opacity', originalOpacity);
|
|
183
|
-
} else {
|
|
184
|
-
targetElement.removeAttribute('opacity');
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Remove event listeners
|
|
188
|
-
textarea.removeEventListener('keydown', handleKeyDown);
|
|
189
|
-
document.removeEventListener('mousedown', handleClickOutside);
|
|
190
|
-
resizeObserver.disconnect();
|
|
191
|
-
|
|
192
|
-
// Restore container position if we changed it
|
|
193
|
-
if (containerWasStatic) {
|
|
194
|
-
container.style.position = '';
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Remove the textarea
|
|
198
|
-
if (textarea.parentNode) {
|
|
199
|
-
textarea.parentNode.removeChild(textarea);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function commit(): void {
|
|
204
|
-
if (destroyed) return;
|
|
205
|
-
const newText = textarea.value;
|
|
206
|
-
destroy();
|
|
207
|
-
onCommit(newText);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function cancel(): void {
|
|
211
|
-
if (destroyed) return;
|
|
212
|
-
destroy();
|
|
213
|
-
onCancel();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Keyboard handling
|
|
217
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
218
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
219
|
-
e.preventDefault();
|
|
220
|
-
commit();
|
|
221
|
-
} else if (e.key === 'Escape') {
|
|
222
|
-
e.preventDefault();
|
|
223
|
-
cancel();
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
textarea.addEventListener('keydown', handleKeyDown);
|
|
228
|
-
|
|
229
|
-
// Click outside to commit
|
|
230
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
231
|
-
if (!textarea.contains(e.target as Node)) {
|
|
232
|
-
commit();
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
// Delay attaching click-outside so the current click event doesn't trigger it
|
|
237
|
-
requestAnimationFrame(() => {
|
|
238
|
-
if (!destroyed) {
|
|
239
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Reposition on container resize
|
|
244
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
245
|
-
if (destroyed) return;
|
|
246
|
-
const newPosition = computePosition(targetElement, svg, container);
|
|
247
|
-
textarea.style.top = `${newPosition.top}px`;
|
|
248
|
-
textarea.style.left = `${newPosition.left}px`;
|
|
249
|
-
textarea.style.width = `${newPosition.width}px`;
|
|
250
|
-
textarea.style.minHeight = `${newPosition.height}px`;
|
|
251
|
-
});
|
|
252
|
-
resizeObserver.observe(container);
|
|
253
|
-
|
|
254
|
-
return { destroy };
|
|
255
|
-
}
|