@opendata-ai/openchart-vanilla 6.3.0 → 6.5.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/dist/index.d.ts +53 -5
- package/dist/index.js +897 -168
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -764
- package/package.json +3 -3
- package/src/__tests__/animation.test.ts +358 -0
- package/src/__tests__/edit-events.test.ts +35 -35
- package/src/__tests__/events.test.ts +7 -7
- package/src/__tests__/export.test.ts +1 -1
- package/src/__tests__/mount.test.ts +10 -10
- package/src/__tests__/selection-events.test.ts +869 -0
- package/src/__tests__/svg-renderer.test.ts +67 -67
- package/src/__tests__/table-keyboard.test.ts +18 -18
- package/src/__tests__/table-mount.test.ts +138 -17
- package/src/__tests__/tooltip.test.ts +12 -12
- package/src/animation.ts +75 -0
- package/src/graph/__tests__/graph-mount.test.ts +16 -16
- package/src/graph-mount.ts +18 -18
- package/src/index.ts +3 -1
- package/src/mount.ts +668 -30
- package/src/renderers/table-cells.ts +11 -9
- package/src/svg-renderer.ts +164 -54
- package/src/table-keyboard.ts +5 -5
- package/src/table-mount.ts +34 -11
- package/src/table-renderer.ts +70 -39
- package/src/text-edit-overlay.ts +255 -0
- package/src/tooltip.ts +8 -8
|
@@ -199,7 +199,7 @@ describe('createTable', () => {
|
|
|
199
199
|
const spec = makeSpec({ search: true });
|
|
200
200
|
const table = createTable(container, spec);
|
|
201
201
|
|
|
202
|
-
const input = container.querySelector('.
|
|
202
|
+
const input = container.querySelector('.oc-table-search input') as HTMLInputElement;
|
|
203
203
|
expect(input).not.toBeNull();
|
|
204
204
|
|
|
205
205
|
// Type in a search query
|
|
@@ -223,7 +223,7 @@ describe('createTable', () => {
|
|
|
223
223
|
const table = createTable(container, paginatedSpec);
|
|
224
224
|
|
|
225
225
|
// Should show page 1 of 5 (10 per page, 50 total)
|
|
226
|
-
const info = container.querySelector('.
|
|
226
|
+
const info = container.querySelector('.oc-table-pagination-info');
|
|
227
227
|
expect(info?.textContent).toContain('Showing 1-10 of 50');
|
|
228
228
|
|
|
229
229
|
const rows = container.querySelectorAll('tbody tr');
|
|
@@ -234,7 +234,7 @@ describe('createTable', () => {
|
|
|
234
234
|
expect(nextBtn).not.toBeNull();
|
|
235
235
|
nextBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
236
236
|
|
|
237
|
-
const infoAfter = container.querySelector('.
|
|
237
|
+
const infoAfter = container.querySelector('.oc-table-pagination-info');
|
|
238
238
|
expect(infoAfter?.textContent).toContain('Showing 11-20 of 50');
|
|
239
239
|
|
|
240
240
|
// Previous button should be enabled
|
|
@@ -248,7 +248,7 @@ describe('createTable', () => {
|
|
|
248
248
|
const table = createTable(container, stickySpec);
|
|
249
249
|
|
|
250
250
|
const tableEl = container.querySelector('table');
|
|
251
|
-
expect(tableEl?.classList.contains('
|
|
251
|
+
expect(tableEl?.classList.contains('oc-table--sticky')).toBe(true);
|
|
252
252
|
|
|
253
253
|
table.destroy();
|
|
254
254
|
});
|
|
@@ -272,7 +272,7 @@ describe('createTable', () => {
|
|
|
272
272
|
const table = createTable(container, sparklineSpec);
|
|
273
273
|
|
|
274
274
|
// Sparkline cells should be rendered with the sparkline class
|
|
275
|
-
const sparklineCells = container.querySelectorAll('.
|
|
275
|
+
const sparklineCells = container.querySelectorAll('.oc-table-sparkline');
|
|
276
276
|
expect(sparklineCells.length).toBeGreaterThan(0);
|
|
277
277
|
|
|
278
278
|
const svg = sparklineCells[0]?.querySelector('svg');
|
|
@@ -288,11 +288,11 @@ describe('createTable', () => {
|
|
|
288
288
|
it('bar cells have proportional fill div', () => {
|
|
289
289
|
const table = createTable(container, barSpec);
|
|
290
290
|
|
|
291
|
-
const barFills = container.querySelectorAll('.
|
|
291
|
+
const barFills = container.querySelectorAll('.oc-table-bar-fill');
|
|
292
292
|
expect(barFills.length).toBe(3);
|
|
293
293
|
|
|
294
294
|
// The bar values should have width proportional to their data
|
|
295
|
-
const barValues = container.querySelectorAll('.
|
|
295
|
+
const barValues = container.querySelectorAll('.oc-table-bar-value');
|
|
296
296
|
expect(barValues.length).toBe(3);
|
|
297
297
|
|
|
298
298
|
table.destroy();
|
|
@@ -331,14 +331,14 @@ describe('createTable', () => {
|
|
|
331
331
|
const table = createTable(container, spec);
|
|
332
332
|
|
|
333
333
|
// Search for something that doesn't match any data
|
|
334
|
-
const input = container.querySelector('.
|
|
334
|
+
const input = container.querySelector('.oc-table-search input') as HTMLInputElement;
|
|
335
335
|
input.value = 'zzzznonexistent';
|
|
336
336
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
337
337
|
|
|
338
338
|
// Advance past the 200ms debounce
|
|
339
339
|
vi.advanceTimersByTime(200);
|
|
340
340
|
|
|
341
|
-
const empty = container.querySelector('.
|
|
341
|
+
const empty = container.querySelector('.oc-table-empty');
|
|
342
342
|
expect(empty).not.toBeNull();
|
|
343
343
|
expect(empty?.textContent).toBe('No results found');
|
|
344
344
|
|
|
@@ -421,21 +421,21 @@ describe('createTable', () => {
|
|
|
421
421
|
table.destroy();
|
|
422
422
|
});
|
|
423
423
|
|
|
424
|
-
it('compact mode applies
|
|
424
|
+
it('compact mode applies oc-table--compact class', () => {
|
|
425
425
|
const spec = makeSpec({ compact: true });
|
|
426
426
|
const table = createTable(container, spec);
|
|
427
427
|
|
|
428
|
-
const wrapper = container.querySelector('.
|
|
429
|
-
expect(wrapper?.classList.contains('
|
|
428
|
+
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
429
|
+
expect(wrapper?.classList.contains('oc-table--compact')).toBe(true);
|
|
430
430
|
|
|
431
431
|
table.destroy();
|
|
432
432
|
});
|
|
433
433
|
|
|
434
|
-
it('dark mode applies
|
|
434
|
+
it('dark mode applies oc-dark class', () => {
|
|
435
435
|
const spec = makeSpec();
|
|
436
436
|
const table = createTable(container, spec, { darkMode: 'force' });
|
|
437
437
|
|
|
438
|
-
expect(container.classList.contains('
|
|
438
|
+
expect(container.classList.contains('oc-dark')).toBe(true);
|
|
439
439
|
|
|
440
440
|
table.destroy();
|
|
441
441
|
});
|
|
@@ -445,8 +445,8 @@ describe('createTable', () => {
|
|
|
445
445
|
const spec = makeSpec();
|
|
446
446
|
const table = createTable(container, spec, { onRowClick: onClick });
|
|
447
447
|
|
|
448
|
-
const wrapper = container.querySelector('.
|
|
449
|
-
expect(wrapper?.classList.contains('
|
|
448
|
+
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
449
|
+
expect(wrapper?.classList.contains('oc-table--clickable')).toBe(true);
|
|
450
450
|
|
|
451
451
|
// Click first row
|
|
452
452
|
const firstRow = container.querySelector('tbody tr');
|
|
@@ -462,7 +462,7 @@ describe('createTable', () => {
|
|
|
462
462
|
const spec = makeSpec();
|
|
463
463
|
const table = createTable(container, spec);
|
|
464
464
|
|
|
465
|
-
const title = container.querySelector('.
|
|
465
|
+
const title = container.querySelector('.oc-table-title');
|
|
466
466
|
expect(title).not.toBeNull();
|
|
467
467
|
expect(title?.textContent).toBe('People');
|
|
468
468
|
|
|
@@ -481,4 +481,125 @@ describe('createTable', () => {
|
|
|
481
481
|
|
|
482
482
|
table.destroy();
|
|
483
483
|
});
|
|
484
|
+
|
|
485
|
+
// -------------------------------------------------------------------------
|
|
486
|
+
// Animation lifecycle
|
|
487
|
+
// -------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
describe('animation', () => {
|
|
490
|
+
it('adds oc-animate class on first render when animation is enabled', () => {
|
|
491
|
+
const spec = makeSpec({ animation: true });
|
|
492
|
+
const table = createTable(container, spec);
|
|
493
|
+
|
|
494
|
+
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
495
|
+
expect(wrapper?.classList.contains('oc-animate')).toBe(true);
|
|
496
|
+
|
|
497
|
+
table.destroy();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('does not add oc-animate when animation is not set', () => {
|
|
501
|
+
const table = createTable(container, makeSpec());
|
|
502
|
+
|
|
503
|
+
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
504
|
+
expect(wrapper?.classList.contains('oc-animate')).toBe(false);
|
|
505
|
+
|
|
506
|
+
table.destroy();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('sets --oc-animation-duration CSS custom property', () => {
|
|
510
|
+
const spec = makeSpec({ animation: { enter: { duration: 800 } } });
|
|
511
|
+
const table = createTable(container, spec);
|
|
512
|
+
|
|
513
|
+
const wrapper = container.querySelector('.oc-table-wrapper') as HTMLElement;
|
|
514
|
+
expect(wrapper.style.getPropertyValue('--oc-animation-duration')).toBe('800ms');
|
|
515
|
+
|
|
516
|
+
table.destroy();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('sets --oc-animation-stagger CSS custom property', () => {
|
|
520
|
+
const spec = makeSpec({ animation: true });
|
|
521
|
+
const table = createTable(container, spec);
|
|
522
|
+
|
|
523
|
+
const wrapper = container.querySelector('.oc-table-wrapper') as HTMLElement;
|
|
524
|
+
const stagger = wrapper.style.getPropertyValue('--oc-animation-stagger');
|
|
525
|
+
expect(stagger).toMatch(/^\d+ms$/);
|
|
526
|
+
|
|
527
|
+
table.destroy();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('stamps --oc-row-index on each tbody tr', () => {
|
|
531
|
+
const spec = makeSpec({ animation: true });
|
|
532
|
+
const table = createTable(container, spec);
|
|
533
|
+
|
|
534
|
+
const rows = container.querySelectorAll('tbody tr');
|
|
535
|
+
for (let i = 0; i < rows.length; i++) {
|
|
536
|
+
const row = rows[i] as HTMLElement;
|
|
537
|
+
expect(row.style.getPropertyValue('--oc-row-index')).toBe(String(i));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
table.destroy();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('does not re-animate on sort (only first render)', () => {
|
|
544
|
+
const spec = makeSpec({ animation: true });
|
|
545
|
+
const table = createTable(container, spec);
|
|
546
|
+
|
|
547
|
+
// Trigger sort via click
|
|
548
|
+
const sortBtn = container.querySelector('[data-sort-column]');
|
|
549
|
+
if (sortBtn) {
|
|
550
|
+
sortBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
554
|
+
expect(wrapper?.classList.contains('oc-animate')).toBe(false);
|
|
555
|
+
|
|
556
|
+
table.destroy();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('does not re-animate on update()', () => {
|
|
560
|
+
const spec = makeSpec({ animation: true });
|
|
561
|
+
const table = createTable(container, spec);
|
|
562
|
+
|
|
563
|
+
table.update(makeSpec({ animation: true }));
|
|
564
|
+
|
|
565
|
+
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
566
|
+
expect(wrapper?.classList.contains('oc-animate')).toBe(false);
|
|
567
|
+
|
|
568
|
+
table.destroy();
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('removes oc-animate after cleanup timeout', () => {
|
|
572
|
+
vi.useFakeTimers();
|
|
573
|
+
try {
|
|
574
|
+
const spec = makeSpec({ animation: true });
|
|
575
|
+
const table = createTable(container, spec);
|
|
576
|
+
|
|
577
|
+
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
578
|
+
expect(wrapper?.classList.contains('oc-animate')).toBe(true);
|
|
579
|
+
|
|
580
|
+
// Advance past total animation time
|
|
581
|
+
vi.advanceTimersByTime(5000);
|
|
582
|
+
|
|
583
|
+
expect(wrapper?.classList.contains('oc-animate')).toBe(false);
|
|
584
|
+
|
|
585
|
+
table.destroy();
|
|
586
|
+
} finally {
|
|
587
|
+
vi.useRealTimers();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('destroy cancels animation cleanup without error', () => {
|
|
592
|
+
vi.useFakeTimers();
|
|
593
|
+
try {
|
|
594
|
+
const spec = makeSpec({ animation: true });
|
|
595
|
+
const table = createTable(container, spec);
|
|
596
|
+
table.destroy();
|
|
597
|
+
|
|
598
|
+
// Should not throw after timer fires
|
|
599
|
+
vi.advanceTimersByTime(5000);
|
|
600
|
+
} finally {
|
|
601
|
+
vi.useRealTimers();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
});
|
|
484
605
|
});
|
|
@@ -56,7 +56,7 @@ describe('createTooltipManager lifecycle', () => {
|
|
|
56
56
|
const container = createContainer();
|
|
57
57
|
createTooltipManager(container);
|
|
58
58
|
|
|
59
|
-
const tooltip = container.querySelector('.
|
|
59
|
+
const tooltip = container.querySelector('.oc-tooltip');
|
|
60
60
|
expect(tooltip).not.toBeNull();
|
|
61
61
|
expect(tooltip!.getAttribute('role')).toBe('tooltip');
|
|
62
62
|
});
|
|
@@ -67,7 +67,7 @@ describe('createTooltipManager lifecycle', () => {
|
|
|
67
67
|
|
|
68
68
|
manager.show({ title: 'Point A', fields: [{ label: 'Value', value: '42' }] }, 100, 100);
|
|
69
69
|
|
|
70
|
-
const tooltip = container.querySelector('.
|
|
70
|
+
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
71
71
|
expect(tooltip.style.display).toBe('block');
|
|
72
72
|
});
|
|
73
73
|
|
|
@@ -87,7 +87,7 @@ describe('createTooltipManager lifecycle', () => {
|
|
|
87
87
|
100,
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
-
const tooltip = container.querySelector('.
|
|
90
|
+
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
91
91
|
expect(tooltip.innerHTML).toContain('2021-Q1');
|
|
92
92
|
expect(tooltip.innerHTML).toContain('Revenue');
|
|
93
93
|
expect(tooltip.innerHTML).toContain('$1.2M');
|
|
@@ -108,7 +108,7 @@ describe('createTooltipManager lifecycle', () => {
|
|
|
108
108
|
50,
|
|
109
109
|
);
|
|
110
110
|
|
|
111
|
-
const dot = container.querySelector('.
|
|
111
|
+
const dot = container.querySelector('.oc-tooltip-dot') as HTMLElement;
|
|
112
112
|
expect(dot).not.toBeNull();
|
|
113
113
|
expect(dot.style.background).toBe('#ff0000');
|
|
114
114
|
});
|
|
@@ -120,7 +120,7 @@ describe('createTooltipManager lifecycle', () => {
|
|
|
120
120
|
manager.show({ title: 'Test', fields: [{ label: 'V', value: '1' }] }, 100, 100);
|
|
121
121
|
manager.hide();
|
|
122
122
|
|
|
123
|
-
const tooltip = container.querySelector('.
|
|
123
|
+
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
124
124
|
expect(tooltip.style.display).toBe('none');
|
|
125
125
|
});
|
|
126
126
|
|
|
@@ -129,11 +129,11 @@ describe('createTooltipManager lifecycle', () => {
|
|
|
129
129
|
const manager = createTooltipManager(container);
|
|
130
130
|
|
|
131
131
|
// Verify it exists
|
|
132
|
-
expect(container.querySelector('.
|
|
132
|
+
expect(container.querySelector('.oc-tooltip')).not.toBeNull();
|
|
133
133
|
|
|
134
134
|
manager.destroy();
|
|
135
135
|
|
|
136
|
-
expect(container.querySelector('.
|
|
136
|
+
expect(container.querySelector('.oc-tooltip')).toBeNull();
|
|
137
137
|
});
|
|
138
138
|
|
|
139
139
|
it('show() updates content when called again', () => {
|
|
@@ -142,12 +142,12 @@ describe('createTooltipManager lifecycle', () => {
|
|
|
142
142
|
|
|
143
143
|
manager.show({ title: 'First', fields: [{ label: 'A', value: '1' }] }, 50, 50);
|
|
144
144
|
|
|
145
|
-
let tooltip = container.querySelector('.
|
|
145
|
+
let tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
146
146
|
expect(tooltip.innerHTML).toContain('First');
|
|
147
147
|
|
|
148
148
|
manager.show({ title: 'Second', fields: [{ label: 'B', value: '2' }] }, 100, 100);
|
|
149
149
|
|
|
150
|
-
tooltip = container.querySelector('.
|
|
150
|
+
tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
151
151
|
expect(tooltip.innerHTML).toContain('Second');
|
|
152
152
|
// First content should be replaced
|
|
153
153
|
expect(tooltip.innerHTML).not.toContain('First');
|
|
@@ -190,7 +190,7 @@ describe('tooltip positioning', () => {
|
|
|
190
190
|
expect(rect.height).toBe(0);
|
|
191
191
|
|
|
192
192
|
// Position should be applied from computePosition result
|
|
193
|
-
const tooltip = container.querySelector('.
|
|
193
|
+
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
194
194
|
expect(tooltip.style.left).toContain('px');
|
|
195
195
|
expect(tooltip.style.top).toContain('px');
|
|
196
196
|
|
|
@@ -239,7 +239,7 @@ describe('tooltip positioning', () => {
|
|
|
239
239
|
});
|
|
240
240
|
await flushPositioning();
|
|
241
241
|
|
|
242
|
-
const tooltip = container.querySelector('.
|
|
242
|
+
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
243
243
|
const posAfterSecond = tooltip.style.left;
|
|
244
244
|
|
|
245
245
|
// Now resolve the first (stale) - should be discarded
|
|
@@ -297,7 +297,7 @@ describe('tooltip content escaping', () => {
|
|
|
297
297
|
50,
|
|
298
298
|
);
|
|
299
299
|
|
|
300
|
-
const tooltip = container.querySelector('.
|
|
300
|
+
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
301
301
|
// Should not contain raw HTML tags - the <script> should be escaped
|
|
302
302
|
expect(tooltip.innerHTML).not.toContain('<script>');
|
|
303
303
|
// Should contain escaped versions of angle brackets and ampersands
|
package/src/animation.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation runtime for entrance animations.
|
|
3
|
+
*
|
|
4
|
+
* All animations are CSS-driven (keyframes + clip-path + transforms + opacity).
|
|
5
|
+
* This module handles lifecycle: cleanup after completion, cancellation on update.
|
|
6
|
+
* No WAAPI needed since clip-path handles line/area drawing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Cancel entrance animations and clean up.
|
|
11
|
+
* Called when update() is invoked during animation, or on destroy.
|
|
12
|
+
*/
|
|
13
|
+
export function cancelAnimations(svg: SVGElement | null): void {
|
|
14
|
+
if (svg) {
|
|
15
|
+
svg.classList.remove('oc-animate');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set up animation cleanup that removes oc-animate after all animations complete.
|
|
21
|
+
*
|
|
22
|
+
* Uses the computed total animation time (duration + stagger * elementCount + annotation delay)
|
|
23
|
+
* rather than animationend events, because animationend fires per-element and the first
|
|
24
|
+
* element to finish would prematurely kill staggered animations still in progress.
|
|
25
|
+
*/
|
|
26
|
+
export function setupAnimationCleanup(svg: SVGElement): () => void {
|
|
27
|
+
// Read the animation timing from the CSS custom properties set by the renderer
|
|
28
|
+
const style = svg.style;
|
|
29
|
+
const duration = parseFloat(style.getPropertyValue('--oc-animation-duration')) || 600;
|
|
30
|
+
const stagger = parseFloat(style.getPropertyValue('--oc-animation-stagger')) || 0;
|
|
31
|
+
const annotationDelay = parseFloat(style.getPropertyValue('--oc-annotation-delay')) || 200;
|
|
32
|
+
|
|
33
|
+
// Count animated elements to compute total stagger span
|
|
34
|
+
const animatedElements = svg.querySelectorAll('[data-animation-index]').length;
|
|
35
|
+
const totalStagger = stagger * Math.max(0, animatedElements - 1);
|
|
36
|
+
|
|
37
|
+
// Total time: last element's stagger delay + its duration + annotation delay + buffer
|
|
38
|
+
const totalTime = totalStagger + duration + annotationDelay + 500;
|
|
39
|
+
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
svg.classList.remove('oc-animate');
|
|
42
|
+
}, totalTime);
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
cancelAnimations(svg);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set up animation cleanup for table entrance animations.
|
|
52
|
+
*
|
|
53
|
+
* Same timeout-based approach as chart animations: compute total time from
|
|
54
|
+
* CSS custom properties and row count, then remove oc-animate after completion.
|
|
55
|
+
*/
|
|
56
|
+
export function setupTableAnimationCleanup(wrapper: HTMLElement): () => void {
|
|
57
|
+
const style = wrapper.style;
|
|
58
|
+
const duration = parseFloat(style.getPropertyValue('--oc-animation-duration')) || 500;
|
|
59
|
+
const stagger = parseFloat(style.getPropertyValue('--oc-animation-stagger')) || 0;
|
|
60
|
+
|
|
61
|
+
const rows = wrapper.querySelectorAll('tbody tr').length;
|
|
62
|
+
const totalStagger = stagger * Math.max(0, rows - 1);
|
|
63
|
+
|
|
64
|
+
// Total: last row stagger + duration + buffer
|
|
65
|
+
const totalTime = totalStagger + duration + 300;
|
|
66
|
+
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
wrapper.classList.remove('oc-animate');
|
|
69
|
+
}, totalTime);
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
wrapper.classList.remove('oc-animate');
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -91,30 +91,30 @@ describe('createGraph', () => {
|
|
|
91
91
|
const graph = createGraph(container, basicSpec);
|
|
92
92
|
|
|
93
93
|
// Wrapper
|
|
94
|
-
const wrapper = container.querySelector('.
|
|
94
|
+
const wrapper = container.querySelector('.oc-graph-wrapper');
|
|
95
95
|
expect(wrapper).not.toBeNull();
|
|
96
96
|
|
|
97
97
|
// Canvas
|
|
98
|
-
const canvas = container.querySelector('.
|
|
98
|
+
const canvas = container.querySelector('.oc-graph-canvas');
|
|
99
99
|
expect(canvas).not.toBeNull();
|
|
100
100
|
expect(canvas?.tagName.toLowerCase()).toBe('canvas');
|
|
101
101
|
|
|
102
102
|
// Chrome
|
|
103
|
-
const chrome = container.querySelector('.
|
|
103
|
+
const chrome = container.querySelector('.oc-graph-chrome');
|
|
104
104
|
expect(chrome).not.toBeNull();
|
|
105
105
|
|
|
106
106
|
// Title
|
|
107
|
-
const title = container.querySelector('.
|
|
107
|
+
const title = container.querySelector('.oc-title');
|
|
108
108
|
expect(title).not.toBeNull();
|
|
109
109
|
expect(title?.textContent).toBe('Test Graph');
|
|
110
110
|
|
|
111
111
|
// Subtitle
|
|
112
|
-
const subtitle = container.querySelector('.
|
|
112
|
+
const subtitle = container.querySelector('.oc-subtitle');
|
|
113
113
|
expect(subtitle).not.toBeNull();
|
|
114
114
|
expect(subtitle?.textContent).toBe('A simple test graph');
|
|
115
115
|
|
|
116
116
|
// Legend exists (even if hidden for non-community graphs)
|
|
117
|
-
const legend = container.querySelector('.
|
|
117
|
+
const legend = container.querySelector('.oc-graph-legend');
|
|
118
118
|
expect(legend).not.toBeNull();
|
|
119
119
|
|
|
120
120
|
graph.destroy();
|
|
@@ -124,12 +124,12 @@ describe('createGraph', () => {
|
|
|
124
124
|
container = makeContainer();
|
|
125
125
|
const graph = createGraph(container, basicSpec);
|
|
126
126
|
|
|
127
|
-
expect(container.querySelector('.
|
|
127
|
+
expect(container.querySelector('.oc-graph-wrapper')).not.toBeNull();
|
|
128
128
|
|
|
129
129
|
graph.destroy();
|
|
130
130
|
|
|
131
|
-
expect(container.querySelector('.
|
|
132
|
-
expect(container.querySelector('.
|
|
131
|
+
expect(container.querySelector('.oc-graph-wrapper')).toBeNull();
|
|
132
|
+
expect(container.querySelector('.oc-graph-canvas')).toBeNull();
|
|
133
133
|
|
|
134
134
|
// Calling destroy again should not throw
|
|
135
135
|
expect(() => graph.destroy()).not.toThrow();
|
|
@@ -146,12 +146,12 @@ describe('createGraph', () => {
|
|
|
146
146
|
container = makeContainer();
|
|
147
147
|
const graph = createGraph(container, basicSpec);
|
|
148
148
|
|
|
149
|
-
const titleBefore = container.querySelector('.
|
|
149
|
+
const titleBefore = container.querySelector('.oc-title');
|
|
150
150
|
expect(titleBefore?.textContent).toBe('Test Graph');
|
|
151
151
|
|
|
152
152
|
graph.update(communitySpec);
|
|
153
153
|
|
|
154
|
-
const titleAfter = container.querySelector('.
|
|
154
|
+
const titleAfter = container.querySelector('.oc-title');
|
|
155
155
|
expect(titleAfter?.textContent).toBe('Community Graph');
|
|
156
156
|
|
|
157
157
|
graph.destroy();
|
|
@@ -161,10 +161,10 @@ describe('createGraph', () => {
|
|
|
161
161
|
container = makeContainer();
|
|
162
162
|
const graph = createGraph(container, communitySpec);
|
|
163
163
|
|
|
164
|
-
const legend = container.querySelector('.
|
|
164
|
+
const legend = container.querySelector('.oc-graph-legend');
|
|
165
165
|
expect(legend).not.toBeNull();
|
|
166
166
|
// Community graph should have visible legend items
|
|
167
|
-
const items = container.querySelectorAll('.
|
|
167
|
+
const items = container.querySelectorAll('.oc-graph-legend-item');
|
|
168
168
|
expect(items.length).toBeGreaterThan(0);
|
|
169
169
|
|
|
170
170
|
graph.destroy();
|
|
@@ -190,14 +190,14 @@ describe('createGraph', () => {
|
|
|
190
190
|
graph.destroy();
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
it('applies
|
|
193
|
+
it('applies oc-dark class in dark mode', () => {
|
|
194
194
|
container = makeContainer();
|
|
195
195
|
const graph = createGraph(container, basicSpec, { darkMode: 'force' });
|
|
196
196
|
|
|
197
|
-
expect(container.classList.contains('
|
|
197
|
+
expect(container.classList.contains('oc-dark')).toBe(true);
|
|
198
198
|
|
|
199
199
|
graph.destroy();
|
|
200
|
-
expect(container.classList.contains('
|
|
200
|
+
expect(container.classList.contains('oc-dark')).toBe(false);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
203
|
it('onSelectionChange callback fires on selectNode', () => {
|
package/src/graph-mount.ts
CHANGED
|
@@ -264,35 +264,35 @@ export function createGraph(
|
|
|
264
264
|
|
|
265
265
|
// Wrapper
|
|
266
266
|
wrapper = document.createElement('div');
|
|
267
|
-
wrapper.className = isDark ? '
|
|
267
|
+
wrapper.className = isDark ? 'oc-graph-wrapper oc-dark' : 'oc-graph-wrapper';
|
|
268
268
|
if (isDark) {
|
|
269
|
-
container.classList.add('
|
|
269
|
+
container.classList.add('oc-dark');
|
|
270
270
|
} else {
|
|
271
|
-
container.classList.remove('
|
|
271
|
+
container.classList.remove('oc-dark');
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
// Apply theme colors as CSS custom properties so chrome HTML picks them up.
|
|
275
275
|
// Without this, consumer-supplied theme.colors.text only affects canvas-drawn
|
|
276
|
-
// labels but not the HTML title/subtitle which read from --
|
|
276
|
+
// labels but not the HTML title/subtitle which read from --oc-text.
|
|
277
277
|
const resolvedTheme = compilation.theme;
|
|
278
278
|
if (resolvedTheme) {
|
|
279
279
|
const s = wrapper.style;
|
|
280
|
-
s.setProperty('--
|
|
281
|
-
s.setProperty('--
|
|
282
|
-
s.setProperty('--
|
|
283
|
-
s.setProperty('--
|
|
280
|
+
s.setProperty('--oc-bg', resolvedTheme.colors.background);
|
|
281
|
+
s.setProperty('--oc-text', resolvedTheme.colors.text);
|
|
282
|
+
s.setProperty('--oc-text-secondary', resolvedTheme.colors.axis ?? resolvedTheme.colors.text);
|
|
283
|
+
s.setProperty('--oc-font-family', resolvedTheme.fonts.family);
|
|
284
284
|
s.fontFamily = resolvedTheme.fonts.family;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
// Chrome (title, subtitle)
|
|
288
288
|
chromeEl = document.createElement('div');
|
|
289
|
-
chromeEl.className = '
|
|
289
|
+
chromeEl.className = 'oc-graph-chrome';
|
|
290
290
|
renderChrome();
|
|
291
291
|
wrapper.appendChild(chromeEl);
|
|
292
292
|
|
|
293
293
|
// Canvas
|
|
294
294
|
canvas = document.createElement('canvas');
|
|
295
|
-
canvas.className = '
|
|
295
|
+
canvas.className = 'oc-graph-canvas';
|
|
296
296
|
canvas.setAttribute('role', 'img');
|
|
297
297
|
if (compilation.a11y?.altText) {
|
|
298
298
|
canvas.setAttribute('aria-label', compilation.a11y.altText);
|
|
@@ -302,7 +302,7 @@ export function createGraph(
|
|
|
302
302
|
// Legend
|
|
303
303
|
if (options?.legend !== false) {
|
|
304
304
|
legendEl = document.createElement('div');
|
|
305
|
-
legendEl.className = '
|
|
305
|
+
legendEl.className = 'oc-graph-legend';
|
|
306
306
|
renderLegend();
|
|
307
307
|
wrapper.appendChild(legendEl);
|
|
308
308
|
}
|
|
@@ -321,10 +321,10 @@ export function createGraph(
|
|
|
321
321
|
let html = '';
|
|
322
322
|
|
|
323
323
|
if (compilation.chrome.title) {
|
|
324
|
-
html += `<h2 class="
|
|
324
|
+
html += `<h2 class="oc-title">${escapeHtml(compilation.chrome.title.text)}</h2>`;
|
|
325
325
|
}
|
|
326
326
|
if (compilation.chrome.subtitle) {
|
|
327
|
-
html += `<p class="
|
|
327
|
+
html += `<p class="oc-subtitle">${escapeHtml(compilation.chrome.subtitle.text)}</p>`;
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
chromeEl.innerHTML = html;
|
|
@@ -349,8 +349,8 @@ export function createGraph(
|
|
|
349
349
|
legendEl.style.display = '';
|
|
350
350
|
let html = '';
|
|
351
351
|
for (const entry of entries) {
|
|
352
|
-
html += '<div class="
|
|
353
|
-
html += `<span class="
|
|
352
|
+
html += '<div class="oc-graph-legend-item">';
|
|
353
|
+
html += `<span class="oc-graph-legend-swatch" style="background:${escapeHtml(entry.color)}"></span>`;
|
|
354
354
|
html += `<span>${escapeHtml(entry.label)}</span>`;
|
|
355
355
|
html += '</div>';
|
|
356
356
|
}
|
|
@@ -588,14 +588,14 @@ export function createGraph(
|
|
|
588
588
|
const x = node?.x ?? 0;
|
|
589
589
|
const y = node?.y ?? 0;
|
|
590
590
|
simulation?.pinNode(nodeId, x, y);
|
|
591
|
-
canvas?.classList.add('
|
|
591
|
+
canvas?.classList.add('oc-graph-canvas--dragging');
|
|
592
592
|
},
|
|
593
593
|
onNodeDrag(nodeId, x, y) {
|
|
594
594
|
simulation?.dragNode(nodeId, x, y);
|
|
595
595
|
},
|
|
596
596
|
onNodeDragEnd(nodeId) {
|
|
597
597
|
simulation?.unpinNode(nodeId);
|
|
598
|
-
canvas?.classList.remove('
|
|
598
|
+
canvas?.classList.remove('oc-graph-canvas--dragging');
|
|
599
599
|
},
|
|
600
600
|
onDoubleClick(nodeId) {
|
|
601
601
|
options?.onNodeDoubleClick?.(nodeDataById(nodeId));
|
|
@@ -821,7 +821,7 @@ export function createGraph(
|
|
|
821
821
|
legendEl = null;
|
|
822
822
|
renderer = null;
|
|
823
823
|
|
|
824
|
-
container.classList.remove('
|
|
824
|
+
container.classList.remove('oc-dark');
|
|
825
825
|
}
|
|
826
826
|
|
|
827
827
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { createSimulationWorker } from './graph/simulation-worker-url';
|
|
|
25
25
|
export type { GraphInstance, GraphMountOptions } from './graph-mount';
|
|
26
26
|
// Graph mount API
|
|
27
27
|
export { createGraph } from './graph-mount';
|
|
28
|
-
export type { ChartInstance, ExportOptions, MountOptions } from './mount';
|
|
28
|
+
export type { ChartInstance, ExportOptions, MountOptions, UpdateOptions } from './mount';
|
|
29
29
|
// Main mount API
|
|
30
30
|
export { createChart } from './mount';
|
|
31
31
|
// Cell renderers
|
|
@@ -51,6 +51,8 @@ export type { TableInstance, TableMountOptions, TableState } from './table-mount
|
|
|
51
51
|
export { createTable } from './table-mount';
|
|
52
52
|
// Table renderer (for advanced usage / custom rendering)
|
|
53
53
|
export { renderTable } from './table-renderer';
|
|
54
|
+
export type { TextEditOverlayConfig } from './text-edit-overlay';
|
|
55
|
+
export { createTextEditOverlay } from './text-edit-overlay';
|
|
54
56
|
export type { TooltipManager } from './tooltip';
|
|
55
57
|
// Tooltip
|
|
56
58
|
export { createTooltipManager } from './tooltip';
|