58 lines
2.9 KiB
TypeScript
58 lines
2.9 KiB
TypeScript
const jsonToken = /("(?:\\.|[^"\\])*"\s*:)|("(?:\\.|[^"\\])*")|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false)\b|\b(null)\b|([{}\[\],:])/g;
|
|
|
|
import { useEffect, useMemo, useRef } from "react";
|
|
|
|
export function highlightJson(source: string) {
|
|
const parts = [];
|
|
let cursor = 0;
|
|
|
|
for (const match of source.matchAll(jsonToken)) {
|
|
const index = match.index;
|
|
if (index > cursor) parts.push(source.slice(cursor, index));
|
|
const kind = match[1] ? "key" : match[2] ? "string" : match[3] ? "number" : match[4] ? "boolean" : match[5] ? "null" : "punctuation";
|
|
parts.push(<span className={`json-${kind}`} key={index}>{match[0]}</span>);
|
|
cursor = index + match[0].length;
|
|
}
|
|
parts.push(source.slice(cursor));
|
|
|
|
return parts;
|
|
}
|
|
|
|
export function JsonViewer({ value, activeNode, activeQuestion, isOpen = true }: { value: unknown; activeNode?: string; activeQuestion?: string; isOpen?: boolean }) {
|
|
const containerRef = useRef<HTMLPreElement>(null);
|
|
const activeRef = useRef<HTMLSpanElement>(null);
|
|
const lines = useMemo(() => (JSON.stringify(value, null, 2) ?? "null").split("\n"), [value]);
|
|
// IDs are globally unique for fields and unique within pages. Match the object containing its id.
|
|
const rangeFor = (id: string | undefined, page = false): [number, number] => {
|
|
if (!id) return [-1, -1];
|
|
const idLine = lines.findIndex(line => line.trim() === `"id": ${JSON.stringify(id)},` && (!page || line.startsWith(' "id"')));
|
|
if (idLine < 0) return [-1, -1];
|
|
const indent = lines[idLine].length - lines[idLine].trimStart().length - 2;
|
|
const start = idLine - 1;
|
|
const end = lines.findIndex((line, i) => i > idLine && (line === " ".repeat(indent) + "}" || line === " ".repeat(indent) + "},"));
|
|
return [start, end];
|
|
};
|
|
const [start, end] = rangeFor(activeNode, true);
|
|
const [questionStart, questionEnd] = rangeFor(activeQuestion);
|
|
const scrollTarget = questionStart >= 0 ? questionStart : start;
|
|
|
|
useEffect(() => {
|
|
const container = containerRef.current;
|
|
const active = activeRef.current;
|
|
if (isOpen && container && active) {
|
|
container.scrollTop += active.getBoundingClientRect().top - container.getBoundingClientRect().top - 16;
|
|
}
|
|
}, [activeNode, activeQuestion, isOpen, lines]);
|
|
|
|
return (
|
|
<pre ref={containerRef} className="json-code" tabIndex={0} aria-label="Questionnaire JSON source">
|
|
<code>{lines.map((line, index) => (
|
|
<span key={index} ref={index === scrollTarget ? activeRef : undefined}
|
|
className={`json-line${start >= 0 && index >= start && index <= end ? " json-active-node" : ""}${questionStart >= 0 && index >= questionStart && index <= questionEnd ? " json-active-question" : ""}`}
|
|
aria-current={index === scrollTarget ? "location" : undefined}
|
|
>{highlightJson(line)}{"\n"}</span>
|
|
))}</code>
|
|
</pre>
|
|
);
|
|
}
|