polen
Version:
A framework for delightful GraphQL developer portals
185 lines • 8.81 kB
JavaScript
/**
* 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