Line data Source code
1 : import * as React from 'react';
2 : import * as themes from '../editor/themes/index.js';
3 :
4 : import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
5 : import { json } from '@codemirror/lang-json';
6 : import { bracketMatching, foldGutter, foldKeymap } from '@codemirror/language';
7 : import { type Extension } from '@codemirror/state';
8 : import { EditorView, drawSelection, keymap, lineNumbers } from '@codemirror/view';
9 :
10 : import { CodeMirror } from '../editor/codemirror.js';
11 : import type { ChartSpec } from './chart_spec_paths.js';
12 : import * as styles from './chart_builder.module.css';
13 :
14 : /// Parse the text of the editor, a spec must be a JSON object
15 1 : export function parseChartSpec(text: string): { spec: ChartSpec } | { error: string } {
16 1 : let value: unknown;
17 1 : try {
18 1 : value = JSON.parse(text);
19 1 : } catch (e: unknown) {
20 1 : return { error: e instanceof Error ? e.message : String(e) };
21 1 : }
22 1 : if (value == null || typeof value !== 'object' || Array.isArray(value)) {
23 0 : return { error: 'The Vega-Lite spec must be a JSON object' };
24 : }
25 1 : return { spec: value as ChartSpec };
26 1 : }
27 :
28 : interface Props {
29 : /// The spec when the editor opens. The editor owns its text afterwards.
30 : initialSpec: ChartSpec;
31 : /// A valid edit of the spec
32 : onChange: (spec: ChartSpec) => void;
33 : }
34 :
35 : /// The JSON of the chart spec. Every edit that parses to an object updates the chart, others show the parse error.
36 1 : export function ChartSpecJsonEditor(props: Props): React.ReactElement {
37 1 : const [error, setError] = React.useState<string | null>(null);
38 1 : const onChangeRef = React.useRef(props.onChange);
39 1 : onChangeRef.current = props.onChange;
40 1 : const initialDoc = React.useMemo(() => JSON.stringify(props.initialSpec, null, 2), []);
41 :
42 1 : const extensions = React.useMemo<Extension[]>(() => [
43 1 : themes.xcode,
44 1 : json(),
45 1 : lineNumbers(),
46 1 : foldGutter(),
47 1 : bracketMatching(),
48 1 : drawSelection(),
49 1 : history(),
50 1 : keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap]),
51 1 : EditorView.contentAttributes.of({ 'aria-label': 'Vega-Lite spec JSON' }),
52 1 : EditorView.updateListener.of(update => {
53 1 : if (!update.docChanged) return;
54 1 : const parsed = parseChartSpec(update.state.doc.toString());
55 1 : if ('error' in parsed) {
56 1 : setError(parsed.error);
57 1 : } else {
58 1 : setError(null);
59 1 : onChangeRef.current(parsed.spec);
60 1 : }
61 1 : }),
62 1 : ], []);
63 :
64 1 : return (
65 1 : <div className={styles.json_pane}>
66 1 : <div className={styles.json_editor} data-testid="chart-json-editor">
67 1 : <CodeMirror extensions={extensions} initialDoc={initialDoc} />
68 1 : </div>
69 1 : {error != null && (
70 1 : <div className={styles.json_error} role="alert" data-testid="chart-json-error">{error}</div>
71 : )}
72 1 : </div>
73 : );
74 1 : }
|