UNPKG

polen

Version:

A framework for delightful GraphQL developer portals

151 lines 7.8 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { React as ReactHooks } from '#dep/react/index'; import { useNavigate } from 'react-router'; import { analyze } from '../analysis.js'; import { useTooltipState } from '../hooks/use-tooltip-state.js'; import { createSimplePositionCalculator } from '../positioning-simple.js'; import { createPolenSchemaResolver } from '../schema-integration.js'; import { graphqlDocumentStyles } from './graphql-document-styles.js'; import { IdentifierLink } from './IdentifierLink.js'; /** * Interactive GraphQL document component * * Transforms static GraphQL code blocks into interactive documentation * with hyperlinks, tooltips, and schema validation. */ export const GraphQLDocument = ({ children, schema, options = {}, }) => { const { debug = false, plain = false, onNavigate, validate = true, className = ``, renderCode, } = options; const navigate = useNavigate(); const handleNavigate = onNavigate || ((url) => navigate(url)); // Container ref for positioning calculations const containerRef = ReactHooks.useRef(null); const [isReady, setIsReady] = ReactHooks.useState(false); // Use tooltip state management const tooltipState = useTooltipState({ showDelay: 300, hideDelay: 200, // Increased for smoother experience allowMultiplePins: true, }); // Handle escape key to unpin all ReactHooks.useEffect(() => { const handleKeyDown = (event) => { if (event.key === `Escape`) { tooltipState.unpinAll(); } }; document.addEventListener(`keydown`, handleKeyDown); return () => { document.removeEventListener(`keydown`, handleKeyDown); }; }, [tooltipState]); // Layer 1: Parse and analyze const analysisResult = ReactHooks.useMemo(() => { if (plain) return null; const result = analyze(children, { schema }); // Debug logging handled by debug prop return result; }, [children, plain, schema, debug]); // Layer 2: Schema resolution const resolver = ReactHooks.useMemo(() => { if (!schema || plain) return null; return createPolenSchemaResolver(schema); }, [schema, plain]); const resolutions = ReactHooks.useMemo(() => { if (!analysisResult || !resolver) { return new Map(); } const results = new Map(); for (const [position, identifier] of analysisResult.identifiers.byPosition) { const resolution = resolver.resolveIdentifier(identifier); if (resolution) { results.set(position, resolution); } } return results; }, [analysisResult, resolver]); // Layer 3: Position calculation const positionCalculator = ReactHooks.useMemo(() => { if (plain) return null; return createSimplePositionCalculator(); }, [plain]); const [positions, setPositions] = ReactHooks.useState(new Map()); // Prepare code block and calculate positions after render ReactHooks.useEffect(() => { if (!containerRef.current || !analysisResult || !positionCalculator || plain) { // Skip position calculation - debug handled by debug prop return; } // Get the code element within the container const codeElement = containerRef.current.querySelector(`pre.code-block code`) || containerRef.current.querySelector(`pre code`) || containerRef.current.querySelector(`code`); if (!codeElement) { // No code element found - skip return; } // Prepare the code block (wrap identifiers) const identifiers = Array.from(analysisResult.identifiers.byPosition.values()); // Prepare code block with identifiers positionCalculator.prepareCodeBlock(codeElement, identifiers); // Get positions after DOM update requestAnimationFrame(() => { // Pass containerRef.current as the reference element for positioning if (containerRef.current) { const newPositions = positionCalculator.getIdentifierPositions(codeElement, containerRef.current); // Position calculation complete setPositions(newPositions); setIsReady(true); } }); }, [analysisResult, positionCalculator, plain]); // Handle resize events with debouncing ReactHooks.useEffect(() => { if (!containerRef.current || !positionCalculator || plain) return; let resizeTimer; const handleResize = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { const codeElement = containerRef.current?.querySelector(`pre.code-block code`) || containerRef.current?.querySelector(`pre code`) || containerRef.current?.querySelector(`code`); if (codeElement && containerRef.current) { const newPositions = positionCalculator.getIdentifierPositions(codeElement, containerRef.current); setPositions(newPositions); } }, 100); // Debounce resize events }; window.addEventListener(`resize`, handleResize); return () => { clearTimeout(resizeTimer); window.removeEventListener(`resize`, handleResize); }; }, [positionCalculator, plain]); // Validation errors const validationErrors = ReactHooks.useMemo(() => { if (!validate || !analysisResult || !schema) return []; return analysisResult.errors; }, [validate, analysisResult, schema]); return (_jsxs(_Fragment, { children: [_jsx("style", { dangerouslySetInnerHTML: { __html: graphqlDocumentStyles } }), _jsxs("div", { ref: containerRef, className: `graphql-document ${className} ${debug ? `graphql-debug-mode` : ``} ${!isReady && !plain ? `graphql-loading` : ``}`, "data-testid": 'graphql-document', children: [renderCode ? (renderCode()) : (_jsx("pre", { className: 'code-block', children: _jsx("code", { children: children }) })), !plain && isReady && (_jsx("div", { className: 'graphql-interaction-layer', style: { pointerEvents: `none` }, children: Array.from(positions.entries()).map(([id, { position, identifier }]) => { const startPos = identifier.position.start; const resolution = resolutions.get(startPos); if (!resolution) return null; return (_jsx(IdentifierLink, { identifier: identifier, resolution: resolution, position: position, onNavigate: handleNavigate, debug: debug, isOpen: tooltipState.isOpen(id), isPinned: tooltipState.isPinned(id), onHoverStart: () => { tooltipState.onHoverStart(id); }, onHoverEnd: () => { tooltipState.onHoverEnd(id); }, onTogglePin: () => { tooltipState.onTogglePin(id); }, onTooltipHover: () => { tooltipState.onTooltipHover(id); } }, id)); }) })), validationErrors.length > 0 && (_jsx("div", { className: 'graphql-validation-errors', children: validationErrors.map((error, index) => (_jsx("div", { className: 'graphql-error', children: error.message }, index))) }))] })] })); }; //# sourceMappingURL=GraphQLDocument.js.map