@opendata-ai/openchart-vue 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/index.d.ts +153 -22
- package/dist/index.js +136 -16
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +5 -6
- package/src/BarList.ts +0 -155
- package/src/Chart.ts +0 -257
- package/src/ChartStory.ts +0 -184
- package/src/DataTable.ts +0 -194
- package/src/Graph.ts +0 -179
- package/src/Map.ts +0 -160
- package/src/Sankey.ts +0 -161
- package/src/ThemeProvider.ts +0 -41
- package/src/TileMap.ts +0 -164
- package/src/Visualization.ts +0 -84
- package/src/__tests__/Chart.test.ts +0 -139
- package/src/__tests__/ChartStory.test.ts +0 -70
- package/src/__tests__/DataTable.test.ts +0 -131
- package/src/__tests__/Graph.test.ts +0 -133
- package/src/__tests__/ThemeContext.test.ts +0 -266
- package/src/__tests__/composables.test.ts +0 -158
- package/src/__tests__/useTableState.test.ts +0 -256
- package/src/composables/useChart.ts +0 -96
- package/src/composables/useDarkMode.ts +0 -71
- package/src/composables/useGraph.ts +0 -89
- package/src/composables/useTable.ts +0 -88
- package/src/composables/useTableState.ts +0 -80
- package/src/context.ts +0 -33
- package/src/index.ts +0 -95
package/src/Chart.ts
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vue Chart component: thin wrapper around the vanilla adapter.
|
|
3
|
-
*
|
|
4
|
-
* Mounts a chart instance on render, updates when spec changes,
|
|
5
|
-
* and cleans up on unmount. All heavy lifting is done by the vanilla
|
|
6
|
-
* createChart() function.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
Annotation,
|
|
11
|
-
AnnotationOffset,
|
|
12
|
-
ChartSpec,
|
|
13
|
-
DarkMode,
|
|
14
|
-
ElementEdit,
|
|
15
|
-
ElementRef,
|
|
16
|
-
GraphSpec,
|
|
17
|
-
LayerSpec,
|
|
18
|
-
MarkEvent,
|
|
19
|
-
TextAnnotation,
|
|
20
|
-
ThemeConfig,
|
|
21
|
-
} from '@opendata-ai/openchart-core';
|
|
22
|
-
import { type ChartInstance, createChart, type MountOptions } from '@opendata-ai/openchart-vanilla';
|
|
23
|
-
import {
|
|
24
|
-
type CSSProperties,
|
|
25
|
-
defineComponent,
|
|
26
|
-
getCurrentInstance,
|
|
27
|
-
h,
|
|
28
|
-
inject,
|
|
29
|
-
onMounted,
|
|
30
|
-
onUnmounted,
|
|
31
|
-
type PropType,
|
|
32
|
-
ref,
|
|
33
|
-
watch,
|
|
34
|
-
} from 'vue';
|
|
35
|
-
import { VizDarkModeKey, VizThemeKey } from './context';
|
|
36
|
-
|
|
37
|
-
export interface ChartProps {
|
|
38
|
-
spec: ChartSpec | LayerSpec | GraphSpec;
|
|
39
|
-
theme?: ThemeConfig;
|
|
40
|
-
darkMode?: DarkMode;
|
|
41
|
-
class?: string;
|
|
42
|
-
style?: string | CSSProperties;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const Chart = defineComponent({
|
|
46
|
-
name: 'Chart',
|
|
47
|
-
props: {
|
|
48
|
-
spec: {
|
|
49
|
-
type: Object as PropType<ChartSpec | LayerSpec | GraphSpec>,
|
|
50
|
-
required: true,
|
|
51
|
-
},
|
|
52
|
-
theme: {
|
|
53
|
-
type: Object as PropType<ThemeConfig>,
|
|
54
|
-
default: undefined,
|
|
55
|
-
},
|
|
56
|
-
darkMode: {
|
|
57
|
-
type: String as PropType<DarkMode>,
|
|
58
|
-
default: undefined,
|
|
59
|
-
},
|
|
60
|
-
class: {
|
|
61
|
-
type: String,
|
|
62
|
-
default: undefined,
|
|
63
|
-
},
|
|
64
|
-
style: {
|
|
65
|
-
type: [String, Object] as PropType<string | CSSProperties>,
|
|
66
|
-
default: undefined,
|
|
67
|
-
},
|
|
68
|
-
selectedElement: {
|
|
69
|
-
type: Object as PropType<ElementRef>,
|
|
70
|
-
default: undefined,
|
|
71
|
-
},
|
|
72
|
-
highlight: {
|
|
73
|
-
type: [Array, null] as unknown as PropType<string[] | null>,
|
|
74
|
-
default: undefined,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
emits: {
|
|
78
|
-
'mark-click': (_event: MarkEvent) => true,
|
|
79
|
-
'mark-hover': (_event: MarkEvent) => true,
|
|
80
|
-
'mark-leave': () => true,
|
|
81
|
-
'legend-toggle': (_series: string, _visible: boolean) => true,
|
|
82
|
-
'annotation-click': (_annotation: Annotation, _event: MouseEvent) => true,
|
|
83
|
-
'annotation-edit': (_annotation: TextAnnotation, _updatedOffset: AnnotationOffset) => true,
|
|
84
|
-
edit: (_edit: ElementEdit) => true,
|
|
85
|
-
select: (_element: ElementRef) => true,
|
|
86
|
-
deselect: (_element: ElementRef) => true,
|
|
87
|
-
'text-edit': (_element: ElementRef, _oldText: string, _newText: string) => true,
|
|
88
|
-
'data-point-click': (_data: Record<string, unknown>) => true,
|
|
89
|
-
},
|
|
90
|
-
setup(props, { emit, expose }) {
|
|
91
|
-
const containerRef = ref<HTMLDivElement | null>(null);
|
|
92
|
-
let instance: ChartInstance | null = null;
|
|
93
|
-
let prevSpec = '';
|
|
94
|
-
|
|
95
|
-
// Inject theme/darkMode from provider as fallbacks
|
|
96
|
-
const contextTheme = inject(VizThemeKey, undefined);
|
|
97
|
-
const contextDarkMode = inject(VizDarkModeKey, undefined);
|
|
98
|
-
|
|
99
|
-
// Check which event listeners are bound by the parent.
|
|
100
|
-
// Vue 3 passes listeners as onXxx props on the component vnode.
|
|
101
|
-
// We cache this at setup time since it won't change.
|
|
102
|
-
const vm = getCurrentInstance();
|
|
103
|
-
const vnodeProps = vm?.vnode.props ?? {};
|
|
104
|
-
const hasAnnotationEditListener =
|
|
105
|
-
'onAnnotation-edit' in vnodeProps || 'onAnnotationEdit' in vnodeProps;
|
|
106
|
-
const hasEditListener = 'onEdit' in vnodeProps;
|
|
107
|
-
const hasSelectListener = 'onSelect' in vnodeProps;
|
|
108
|
-
const hasDeselectListener = 'onDeselect' in vnodeProps;
|
|
109
|
-
const hasTextEditListener = 'onText-edit' in vnodeProps || 'onTextEdit' in vnodeProps;
|
|
110
|
-
|
|
111
|
-
function resolveTheme(): ThemeConfig | undefined {
|
|
112
|
-
return props.theme ?? contextTheme?.value;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function resolveDarkMode(): DarkMode | undefined {
|
|
116
|
-
return props.darkMode ?? contextDarkMode?.value;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function mountChart() {
|
|
120
|
-
const container = containerRef.value;
|
|
121
|
-
if (!container) return;
|
|
122
|
-
|
|
123
|
-
const options: MountOptions = {
|
|
124
|
-
theme: resolveTheme(),
|
|
125
|
-
darkMode: resolveDarkMode(),
|
|
126
|
-
onDataPointClick: (data: Record<string, unknown>) => emit('data-point-click', data),
|
|
127
|
-
onMarkClick: (event: MarkEvent) => emit('mark-click', event),
|
|
128
|
-
onMarkHover: (event: MarkEvent) => emit('mark-hover', event),
|
|
129
|
-
onMarkLeave: () => emit('mark-leave'),
|
|
130
|
-
onLegendToggle: (series: string, visible: boolean) =>
|
|
131
|
-
emit('legend-toggle', series, visible),
|
|
132
|
-
onAnnotationClick: (annotation: Annotation, event: MouseEvent) =>
|
|
133
|
-
emit('annotation-click', annotation, event),
|
|
134
|
-
// Only include editing callbacks when the parent binds a listener.
|
|
135
|
-
// Without this gate, every chart gets drag editing wired up.
|
|
136
|
-
...(hasAnnotationEditListener
|
|
137
|
-
? {
|
|
138
|
-
onAnnotationEdit: (annotation: TextAnnotation, updatedOffset: AnnotationOffset) =>
|
|
139
|
-
emit('annotation-edit', annotation, updatedOffset),
|
|
140
|
-
}
|
|
141
|
-
: {}),
|
|
142
|
-
...(hasEditListener ? { onEdit: (edit: ElementEdit) => emit('edit', edit) } : {}),
|
|
143
|
-
...(hasSelectListener
|
|
144
|
-
? { onSelect: (element: ElementRef) => emit('select', element) }
|
|
145
|
-
: {}),
|
|
146
|
-
...(hasDeselectListener
|
|
147
|
-
? { onDeselect: (element: ElementRef) => emit('deselect', element) }
|
|
148
|
-
: {}),
|
|
149
|
-
...(hasTextEditListener
|
|
150
|
-
? {
|
|
151
|
-
onTextEdit: (element: ElementRef, oldText: string, newText: string) =>
|
|
152
|
-
emit('text-edit', element, oldText, newText),
|
|
153
|
-
}
|
|
154
|
-
: {}),
|
|
155
|
-
...(props.selectedElement ? { selectedElement: props.selectedElement } : {}),
|
|
156
|
-
responsive: true,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
instance = createChart(container, props.spec, options);
|
|
160
|
-
prevSpec = JSON.stringify(props.spec);
|
|
161
|
-
if (props.highlight !== undefined) {
|
|
162
|
-
instance.setHighlight(props.highlight ?? null);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function destroyChart() {
|
|
167
|
-
instance?.destroy();
|
|
168
|
-
instance = null;
|
|
169
|
-
prevSpec = '';
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Expose imperative methods for parent ref access
|
|
173
|
-
expose({
|
|
174
|
-
getSelectedElement(): ElementRef | null {
|
|
175
|
-
return instance?.getSelectedElement() ?? null;
|
|
176
|
-
},
|
|
177
|
-
select(elementRef: ElementRef): void {
|
|
178
|
-
instance?.select(elementRef);
|
|
179
|
-
},
|
|
180
|
-
deselect(): void {
|
|
181
|
-
instance?.deselect();
|
|
182
|
-
},
|
|
183
|
-
get instance() {
|
|
184
|
-
return instance;
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
onMounted(() => {
|
|
189
|
-
mountChart();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
onUnmounted(() => {
|
|
193
|
-
destroyChart();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// Watch spec changes: update if only spec changed, recreate if theme/darkMode changed
|
|
197
|
-
watch(
|
|
198
|
-
() => JSON.stringify(props.spec),
|
|
199
|
-
(newVal) => {
|
|
200
|
-
if (!instance) return;
|
|
201
|
-
if (newVal !== prevSpec) {
|
|
202
|
-
prevSpec = newVal;
|
|
203
|
-
instance.update(props.spec, { selectedElement: props.selectedElement });
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
// Watch selectedElement prop changes
|
|
209
|
-
watch(
|
|
210
|
-
() => props.selectedElement,
|
|
211
|
-
(newVal) => {
|
|
212
|
-
if (!instance) return;
|
|
213
|
-
if (newVal) {
|
|
214
|
-
instance.select(newVal);
|
|
215
|
-
} else {
|
|
216
|
-
instance.deselect();
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
// Watch highlight prop changes
|
|
222
|
-
watch(
|
|
223
|
-
() => props.highlight,
|
|
224
|
-
(newVal) => {
|
|
225
|
-
if (!instance) return;
|
|
226
|
-
instance.setHighlight(newVal ?? null);
|
|
227
|
-
},
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
// Recreate chart when theme or darkMode change
|
|
231
|
-
watch(
|
|
232
|
-
[
|
|
233
|
-
() => props.theme,
|
|
234
|
-
() => props.darkMode,
|
|
235
|
-
() => contextTheme?.value,
|
|
236
|
-
() => contextDarkMode?.value,
|
|
237
|
-
],
|
|
238
|
-
() => {
|
|
239
|
-
if (!containerRef.value) return;
|
|
240
|
-
destroyChart();
|
|
241
|
-
mountChart();
|
|
242
|
-
},
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
const rootClass = () => {
|
|
246
|
-
const base = 'oc-chart-root';
|
|
247
|
-
return props.class ? `${base} ${props.class}` : base;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
return () =>
|
|
251
|
-
h('div', {
|
|
252
|
-
ref: containerRef,
|
|
253
|
-
class: rootClass(),
|
|
254
|
-
style: props.style,
|
|
255
|
-
});
|
|
256
|
-
},
|
|
257
|
-
});
|
package/src/ChartStory.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vue scrollytelling shell around the vanilla `createChartStory`.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the React `<ChartStory>`: a sticky chart panel plus a scrolling
|
|
5
|
-
* narrative column whose blocks advance the story. The vanilla driver owns
|
|
6
|
-
* cumulative patch application, mark morphing, crossfade, and camera; this
|
|
7
|
-
* component owns only Vue lifecycle and the sticky layout.
|
|
8
|
-
*
|
|
9
|
-
* Narrative is provided as the default slot's children (one root node per
|
|
10
|
-
* step) or via the `narrative` prop (an array of vnodes/strings). Pass `step`
|
|
11
|
-
* to drive it externally; omit for self-driving scroll.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ChartSpec, GraphSpec, LayerSpec, MapSpec } from '@opendata-ai/openchart-core';
|
|
15
|
-
import type { MountOptions } from '@opendata-ai/openchart-vanilla';
|
|
16
|
-
import {
|
|
17
|
-
type ChartStoryInstance,
|
|
18
|
-
type ChartStoryOptions,
|
|
19
|
-
createChartStory,
|
|
20
|
-
} from '@opendata-ai/openchart-vanilla/story';
|
|
21
|
-
import {
|
|
22
|
-
type CSSProperties,
|
|
23
|
-
defineComponent,
|
|
24
|
-
h,
|
|
25
|
-
onMounted,
|
|
26
|
-
onUnmounted,
|
|
27
|
-
type PropType,
|
|
28
|
-
ref,
|
|
29
|
-
type VNode,
|
|
30
|
-
watch,
|
|
31
|
-
} from 'vue';
|
|
32
|
-
|
|
33
|
-
export interface ChartStoryProps {
|
|
34
|
-
spec: ChartSpec | LayerSpec | GraphSpec | MapSpec;
|
|
35
|
-
steps: ChartStoryOptions['steps'];
|
|
36
|
-
narrative?: VNode[];
|
|
37
|
-
step?: number;
|
|
38
|
-
triggerPosition?: number;
|
|
39
|
-
cameraMode?: 'step' | 'scrub';
|
|
40
|
-
mountOptions?: MountOptions;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const sectionStyle: CSSProperties = {
|
|
44
|
-
position: 'relative',
|
|
45
|
-
display: 'grid',
|
|
46
|
-
gridTemplateColumns: 'minmax(0, 26rem) minmax(0, 1fr)',
|
|
47
|
-
gap: '3rem',
|
|
48
|
-
alignItems: 'start',
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const stepStyle: CSSProperties = {
|
|
52
|
-
minHeight: '80vh',
|
|
53
|
-
display: 'flex',
|
|
54
|
-
alignItems: 'center',
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const panelStyle: CSSProperties = {
|
|
58
|
-
position: 'sticky',
|
|
59
|
-
top: '2rem',
|
|
60
|
-
height: 'calc(100vh - 4rem)',
|
|
61
|
-
display: 'flex',
|
|
62
|
-
alignItems: 'center',
|
|
63
|
-
justifyContent: 'center',
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export const ChartStory = defineComponent({
|
|
67
|
-
name: 'ChartStory',
|
|
68
|
-
props: {
|
|
69
|
-
spec: {
|
|
70
|
-
type: Object as PropType<ChartSpec | LayerSpec | GraphSpec | MapSpec>,
|
|
71
|
-
required: true,
|
|
72
|
-
},
|
|
73
|
-
steps: {
|
|
74
|
-
type: Array as PropType<ChartStoryOptions['steps']>,
|
|
75
|
-
required: true,
|
|
76
|
-
},
|
|
77
|
-
narrative: {
|
|
78
|
-
type: Array as PropType<VNode[]>,
|
|
79
|
-
default: undefined,
|
|
80
|
-
},
|
|
81
|
-
step: {
|
|
82
|
-
type: Number,
|
|
83
|
-
default: undefined,
|
|
84
|
-
},
|
|
85
|
-
triggerPosition: {
|
|
86
|
-
type: Number,
|
|
87
|
-
default: undefined,
|
|
88
|
-
},
|
|
89
|
-
cameraMode: {
|
|
90
|
-
type: String as PropType<'step' | 'scrub'>,
|
|
91
|
-
default: undefined,
|
|
92
|
-
},
|
|
93
|
-
mountOptions: {
|
|
94
|
-
type: Object as PropType<MountOptions>,
|
|
95
|
-
default: undefined,
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
setup(props, { slots, expose }) {
|
|
99
|
-
const sectionRef = ref<HTMLElement | null>(null);
|
|
100
|
-
const mountRef = ref<HTMLDivElement | null>(null);
|
|
101
|
-
const stepEls = ref<Array<HTMLElement | null>>([]);
|
|
102
|
-
let story: ChartStoryInstance | null = null;
|
|
103
|
-
|
|
104
|
-
onMounted(() => {
|
|
105
|
-
const mount = mountRef.value;
|
|
106
|
-
if (!mount) return;
|
|
107
|
-
|
|
108
|
-
const blocks = props.narrative ?? [];
|
|
109
|
-
if (blocks.length > 0 && blocks.length !== props.steps.length) {
|
|
110
|
-
console.warn(
|
|
111
|
-
`[openchart] ChartStory: narrative length (${blocks.length}) does not match steps length (${props.steps.length}). Each step should have a corresponding narrative element.`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
story = createChartStory(
|
|
116
|
-
mount,
|
|
117
|
-
{
|
|
118
|
-
spec: props.spec,
|
|
119
|
-
steps: props.steps,
|
|
120
|
-
triggerPosition: props.triggerPosition,
|
|
121
|
-
cameraMode: props.cameraMode,
|
|
122
|
-
},
|
|
123
|
-
props.mountOptions,
|
|
124
|
-
);
|
|
125
|
-
if (sectionRef.value) story.setContainer(sectionRef.value);
|
|
126
|
-
stepEls.value.forEach((el, i) => {
|
|
127
|
-
if (el) story?.registerStep(i, el);
|
|
128
|
-
});
|
|
129
|
-
if (props.step !== undefined) story.goTo(props.step);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
watch(
|
|
133
|
-
() => props.step,
|
|
134
|
-
(next) => {
|
|
135
|
-
if (next !== undefined) story?.goTo(next);
|
|
136
|
-
},
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
onUnmounted(() => {
|
|
140
|
-
story?.destroy();
|
|
141
|
-
story = null;
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
expose({
|
|
145
|
-
goTo(index: number) {
|
|
146
|
-
story?.goTo(index);
|
|
147
|
-
},
|
|
148
|
-
get instance() {
|
|
149
|
-
return story;
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return () => {
|
|
154
|
-
// Prefer the `narrative` prop; fall back to default slot children so
|
|
155
|
-
// consumers can author steps as template markup.
|
|
156
|
-
const blocks = props.narrative ?? slots.default?.() ?? [];
|
|
157
|
-
return h('section', { ref: sectionRef, style: sectionStyle }, [
|
|
158
|
-
h(
|
|
159
|
-
'div',
|
|
160
|
-
{ style: { position: 'relative' } },
|
|
161
|
-
blocks.map((content, i) =>
|
|
162
|
-
h(
|
|
163
|
-
'div',
|
|
164
|
-
{
|
|
165
|
-
// Steps are a fixed ordered list, so the index is the stable
|
|
166
|
-
// identity. Do not reorder or splice narrative entries at runtime
|
|
167
|
-
// (that would remap step refs to the wrong content); rebuild the
|
|
168
|
-
// whole story instead.
|
|
169
|
-
key: i,
|
|
170
|
-
ref: (el: unknown) => {
|
|
171
|
-
stepEls.value[i] = el as HTMLElement | null;
|
|
172
|
-
},
|
|
173
|
-
'data-oc-story-step': i,
|
|
174
|
-
style: stepStyle,
|
|
175
|
-
},
|
|
176
|
-
[h('div', { style: { maxWidth: '36rem' } }, [content])],
|
|
177
|
-
),
|
|
178
|
-
),
|
|
179
|
-
),
|
|
180
|
-
h('div', { style: panelStyle }, [h('div', { ref: mountRef, style: { width: '100%' } })]),
|
|
181
|
-
]);
|
|
182
|
-
};
|
|
183
|
-
},
|
|
184
|
-
});
|
package/src/DataTable.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DataTable component: Vue wrapper around the vanilla table adapter.
|
|
3
|
-
*
|
|
4
|
-
* Mounts a table instance on render, updates when spec changes,
|
|
5
|
-
* and cleans up on unmount. Supports both controlled and uncontrolled modes
|
|
6
|
-
* for sort, search, and pagination state.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { DarkMode, SortState, TableSpec, ThemeConfig } from '@opendata-ai/openchart-core';
|
|
10
|
-
import {
|
|
11
|
-
createTable,
|
|
12
|
-
type TableInstance,
|
|
13
|
-
type TableMountOptions,
|
|
14
|
-
} from '@opendata-ai/openchart-vanilla';
|
|
15
|
-
import {
|
|
16
|
-
type CSSProperties,
|
|
17
|
-
defineComponent,
|
|
18
|
-
h,
|
|
19
|
-
inject,
|
|
20
|
-
onMounted,
|
|
21
|
-
onUnmounted,
|
|
22
|
-
type PropType,
|
|
23
|
-
ref,
|
|
24
|
-
watch,
|
|
25
|
-
} from 'vue';
|
|
26
|
-
import { VizDarkModeKey, VizThemeKey } from './context';
|
|
27
|
-
|
|
28
|
-
export interface DataTableProps {
|
|
29
|
-
spec: TableSpec;
|
|
30
|
-
theme?: ThemeConfig;
|
|
31
|
-
darkMode?: DarkMode;
|
|
32
|
-
class?: string;
|
|
33
|
-
style?: string | CSSProperties;
|
|
34
|
-
sort?: SortState | null;
|
|
35
|
-
search?: string;
|
|
36
|
-
page?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const DataTable = defineComponent({
|
|
40
|
-
name: 'DataTable',
|
|
41
|
-
props: {
|
|
42
|
-
spec: {
|
|
43
|
-
type: Object as PropType<TableSpec>,
|
|
44
|
-
required: true,
|
|
45
|
-
},
|
|
46
|
-
theme: {
|
|
47
|
-
type: Object as PropType<ThemeConfig>,
|
|
48
|
-
default: undefined,
|
|
49
|
-
},
|
|
50
|
-
darkMode: {
|
|
51
|
-
type: String as PropType<DarkMode>,
|
|
52
|
-
default: undefined,
|
|
53
|
-
},
|
|
54
|
-
class: {
|
|
55
|
-
type: String,
|
|
56
|
-
default: undefined,
|
|
57
|
-
},
|
|
58
|
-
style: {
|
|
59
|
-
type: [String, Object] as PropType<string | CSSProperties>,
|
|
60
|
-
default: undefined,
|
|
61
|
-
},
|
|
62
|
-
sort: {
|
|
63
|
-
type: [Object, null] as PropType<SortState | null>,
|
|
64
|
-
default: undefined,
|
|
65
|
-
},
|
|
66
|
-
search: {
|
|
67
|
-
type: String,
|
|
68
|
-
default: undefined,
|
|
69
|
-
},
|
|
70
|
-
page: {
|
|
71
|
-
type: Number,
|
|
72
|
-
default: undefined,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
emits: {
|
|
76
|
-
'row-click': (_row: Record<string, unknown>) => true,
|
|
77
|
-
'update:sort': (_sort: SortState | null) => true,
|
|
78
|
-
'update:search': (_query: string) => true,
|
|
79
|
-
'update:page': (_page: number) => true,
|
|
80
|
-
},
|
|
81
|
-
setup(props, { emit }) {
|
|
82
|
-
const containerRef = ref<HTMLDivElement | null>(null);
|
|
83
|
-
let instance: TableInstance | null = null;
|
|
84
|
-
|
|
85
|
-
// Inject theme/darkMode from provider as fallbacks
|
|
86
|
-
const contextTheme = inject(VizThemeKey, undefined);
|
|
87
|
-
const contextDarkMode = inject(VizDarkModeKey, undefined);
|
|
88
|
-
|
|
89
|
-
function resolveTheme(): ThemeConfig | undefined {
|
|
90
|
-
return props.theme ?? contextTheme?.value;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function resolveDarkMode(): DarkMode | undefined {
|
|
94
|
-
return props.darkMode ?? contextDarkMode?.value;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function isControlled(): boolean {
|
|
98
|
-
return props.sort !== undefined || props.search !== undefined || props.page !== undefined;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let prevSpec = '';
|
|
102
|
-
|
|
103
|
-
function mountTable() {
|
|
104
|
-
const container = containerRef.value;
|
|
105
|
-
if (!container) return;
|
|
106
|
-
|
|
107
|
-
const mountOptions: TableMountOptions = {
|
|
108
|
-
theme: resolveTheme(),
|
|
109
|
-
darkMode: resolveDarkMode(),
|
|
110
|
-
onRowClick: (row: Record<string, unknown>) => emit('row-click', row),
|
|
111
|
-
responsive: true,
|
|
112
|
-
onStateChange: (state) => {
|
|
113
|
-
if (state.sort !== undefined) emit('update:sort', state.sort);
|
|
114
|
-
if (state.search !== undefined) emit('update:search', state.search);
|
|
115
|
-
if (state.page !== undefined) emit('update:page', state.page);
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
if (isControlled()) {
|
|
120
|
-
mountOptions.externalState = {
|
|
121
|
-
sort: props.sort ?? null,
|
|
122
|
-
search: props.search ?? '',
|
|
123
|
-
page: props.page ?? 0,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
instance = createTable(container, props.spec, mountOptions);
|
|
128
|
-
prevSpec = JSON.stringify(props.spec);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function destroyTable() {
|
|
132
|
-
instance?.destroy();
|
|
133
|
-
instance = null;
|
|
134
|
-
prevSpec = '';
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
onMounted(() => {
|
|
138
|
-
mountTable();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
onUnmounted(() => {
|
|
142
|
-
destroyTable();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Watch spec changes via JSON.stringify comparison (consistent with Chart/Graph)
|
|
146
|
-
watch(
|
|
147
|
-
() => JSON.stringify(props.spec),
|
|
148
|
-
(newVal) => {
|
|
149
|
-
if (!instance) return;
|
|
150
|
-
if (newVal !== prevSpec) {
|
|
151
|
-
prevSpec = newVal;
|
|
152
|
-
instance.update(props.spec);
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
// Recreate when theme or darkMode change
|
|
158
|
-
watch(
|
|
159
|
-
[
|
|
160
|
-
() => props.theme,
|
|
161
|
-
() => props.darkMode,
|
|
162
|
-
() => contextTheme?.value,
|
|
163
|
-
() => contextDarkMode?.value,
|
|
164
|
-
],
|
|
165
|
-
() => {
|
|
166
|
-
if (!containerRef.value) return;
|
|
167
|
-
destroyTable();
|
|
168
|
-
mountTable();
|
|
169
|
-
},
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Sync controlled state without remounting
|
|
173
|
-
watch([() => props.sort, () => props.search, () => props.page], () => {
|
|
174
|
-
if (!instance || !isControlled()) return;
|
|
175
|
-
instance.setState({
|
|
176
|
-
sort: props.sort ?? null,
|
|
177
|
-
search: props.search ?? '',
|
|
178
|
-
page: props.page ?? 0,
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const rootClass = () => {
|
|
183
|
-
const base = 'oc-table-root';
|
|
184
|
-
return props.class ? `${base} ${props.class}` : base;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
return () =>
|
|
188
|
-
h('div', {
|
|
189
|
-
ref: containerRef,
|
|
190
|
-
class: rootClass(),
|
|
191
|
-
style: props.style,
|
|
192
|
-
});
|
|
193
|
-
},
|
|
194
|
-
});
|