Initial QuestionGraph library and documentation
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user