UNPKG

polen

Version:

A framework for delightful GraphQL developer portals

185 lines 8.81 kB
/** * Layer 3: Simplified Positioning & Layout Engine * * Maps GraphQL AST positions to DOM coordinates for overlay placement. * This simplified version focuses on working with Polen's existing infrastructure. */ /** * Simplified position calculator for syntax-highlighted code * * This version uses a more straightforward approach: * 1. Find the line element by line number * 2. Search for the identifier text within that line * 3. Create a span around the identifier for positioning * * This approach modifies the DOM but is more reliable for testing * and works well with React's reconciliation. */ export class SimplePositionCalculator { /** * Prepare a code block for positioning by wrapping identifiers in spans */ prepareCodeBlock(containerElement, identifiers) { // Get the full text content of the container const fullText = containerElement.textContent || ``; const lines = fullText.split(`\n`); // Build a map of line start positions in the full text const lineStartPositions = [0]; for (let i = 0; i < lines.length - 1; i++) { const lineLength = lines[i]?.length ?? 0; lineStartPositions.push(lineStartPositions[i] + lineLength + 1); // +1 for newline } // Process identifiers by line for (const identifier of identifiers) { const lineIndex = identifier.position.line - 1; if (lineIndex >= lines.length || lineIndex < 0) continue; const lineText = lines[lineIndex]; if (!lineText) continue; const columnIndex = identifier.position.column - 1; // Check if the identifier exists at the expected position if (lineText.substring(columnIndex).startsWith(identifier.name)) { // Calculate the absolute position in the full text const lineStartPosition = lineStartPositions[lineIndex] ?? 0; const absolutePosition = lineStartPosition + columnIndex; // Check if already wrapped at this specific position const existingWrapped = containerElement.querySelectorAll(`[data-graphql-id]`); let alreadyWrapped = false; for (const wrapped of existingWrapped) { if (wrapped.textContent === identifier.name) { const startPos = parseInt(wrapped.getAttribute(`data-graphql-start`) || `0`); if (startPos === identifier.position.start) { alreadyWrapped = true; break; } } } if (alreadyWrapped) continue; // Create wrapper span const wrapper = document.createElement(`span`); const id = `${identifier.position.start}-${identifier.name}-${identifier.kind}`; wrapper.setAttribute(`data-graphql-id`, id); wrapper.setAttribute(`data-graphql-name`, identifier.name); wrapper.setAttribute(`data-graphql-kind`, identifier.kind); wrapper.setAttribute(`data-graphql-start`, String(identifier.position.start)); wrapper.setAttribute(`data-graphql-end`, String(identifier.position.end)); wrapper.setAttribute(`data-graphql-line`, String(identifier.position.line)); wrapper.setAttribute(`data-graphql-column`, String(identifier.position.column)); wrapper.setAttribute(`data-graphql-path`, identifier.schemaPath.join(`,`)); // Find the position in the container and wrap the text const walker = document.createTreeWalker(containerElement, NodeFilter.SHOW_TEXT, null); let currentPos = 0; let node; while (node = walker.nextNode()) { const textNode = node; const text = textNode.textContent || ``; // Check if this text node contains our identifier if (currentPos <= absolutePosition && absolutePosition < currentPos + text.length) { const relativePos = absolutePosition - currentPos; if (text.substring(relativePos).startsWith(identifier.name)) { // Split the text node const before = text.substring(0, relativePos); const identifierText = identifier.name; const after = text.substring(relativePos + identifierText.length); const parent = textNode.parentNode; if (before) { parent.insertBefore(document.createTextNode(before), textNode); } wrapper.textContent = identifierText; parent.insertBefore(wrapper, textNode); if (after) { parent.insertBefore(document.createTextNode(after), textNode); } parent.removeChild(textNode); break; } } currentPos += text.length; } } } } /** * Get positions of all wrapped identifiers */ getIdentifierPositions(containerElement, relativeToElement) { const results = new Map(); // Find all wrapped identifiers const wrappedIdentifiers = containerElement.querySelectorAll(`[data-graphql-id]`); // Use the provided element for relative positioning, or the container itself const referenceElement = relativeToElement || containerElement; for (const element of wrappedIdentifiers) { const id = element.getAttribute(`data-graphql-id`); if (!id) continue; // Get position relative to the reference element const referenceRect = referenceElement.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); const position = { top: elementRect.top - referenceRect.top, left: elementRect.left - referenceRect.left, width: elementRect.width, height: elementRect.height, }; // Reconstruct identifier from data attributes const identifier = { name: element.getAttribute(`data-graphql-name`) || ``, kind: (element.getAttribute(`data-graphql-kind`) || `Field`), position: { start: parseInt(element.getAttribute(`data-graphql-start`) || `0`), end: parseInt(element.getAttribute(`data-graphql-end`) || `0`), line: parseInt(element.getAttribute(`data-graphql-line`) || `1`), column: parseInt(element.getAttribute(`data-graphql-column`) || `1`), }, schemaPath: (element.getAttribute(`data-graphql-path`) || ``).split(`,`).filter(Boolean), context: { selectionPath: [] }, }; results.set(id, { position, identifier }); } return results; } } /** * Create invisible overlay for a position */ export const createSimpleOverlay = (position, identifier, options) => { const overlay = document.createElement(`div`); // Base styles for positioning overlay.style.position = `absolute`; overlay.style.top = `${position.top}px`; overlay.style.left = `${position.left}px`; overlay.style.width = `${position.width}px`; overlay.style.height = `${position.height}px`; overlay.style.cursor = `pointer`; overlay.style.zIndex = `10`; // Add custom class if provided if (options?.className) { overlay.className = options.className; } // Data attributes overlay.setAttribute(`data-graphql-overlay`, `true`); overlay.setAttribute(`data-graphql-name`, identifier.name); overlay.setAttribute(`data-graphql-kind`, identifier.kind); // Event handlers if (options?.onClick) { overlay.addEventListener(`click`, (e) => { e.preventDefault(); options.onClick(identifier); }); } if (options?.onHover) { overlay.addEventListener(`mouseenter`, (e) => { options.onHover(identifier, e); }); } return overlay; }; /** * Factory function for position calculator */ export const createSimplePositionCalculator = () => { return new SimplePositionCalculator(); }; //# sourceMappingURL=positioning-simple.js.map