polen
Version:
A framework for delightful GraphQL developer portals
151 lines • 7.8 kB
JavaScript
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