// Layout Manager - Handles layout serialization and loading import { getLayout, getLayoutByName } from "./layouts.js"; import { config } from "./state.js"; /** * Builds a UI layout from a layout definition using the same approach as DOMContentLoaded * @param {HTMLElement} rootElement - The root container element * @param {Object} layoutDef - Layout definition object * @param {Object} panes - Panes registry with pane functions * @param {Function} createPane - Function to create a pane element * @param {Function} splitPane - Function to create a split pane */ export function buildLayout(rootElement, layoutDef, panes, createPane, splitPane) { if (!layoutDef || !layoutDef.layout) { throw new Error("Invalid layout definition"); } // Start by creating the first pane and adding it to root const firstPane = buildLayoutNode(layoutDef.layout, panes, createPane); rootElement.appendChild(firstPane); // Then recursively split it according to the layout definition splitLayoutNode(rootElement, layoutDef.layout, panes, createPane, splitPane); } /** * Creates a pane element for a leaf node (doesn't split anything, just creates the pane) * @private */ function buildLayoutNode(node, panes, createPane) { if (node.type === "pane") { if (!node.name || !panes[node.name]) { console.warn(`Pane "${node.name}" not found, using placeholder`); return createPlaceholderPane(node.name); } return createPane(panes[node.name]); } // For grid nodes, find the leftmost/topmost leaf pane if (node.type === "horizontal-grid" || node.type === "vertical-grid") { return buildLayoutNode(node.children[0], panes, createPane); } throw new Error(`Unknown node type: ${node.type}`); } /** * Recursively splits panes according to the layout definition * @private */ function splitLayoutNode(container, node, panes, createPane, splitPane) { if (node.type === "pane") { // Leaf node - nothing to split return; } if (node.type === "horizontal-grid" || node.type === "vertical-grid") { const isHorizontal = node.type === "horizontal-grid"; const percent = node.percent || 50; // Build the second child pane const child2Pane = buildLayoutNode(node.children[1], panes, createPane); // Split the container const [container1, container2] = splitPane(container, percent, isHorizontal, child2Pane); // Recursively split both children splitLayoutNode(container1, node.children[0], panes, createPane, splitPane); splitLayoutNode(container2, node.children[1], panes, createPane, splitPane); } } /** * Creates a placeholder pane for missing pane types * @private */ function createPlaceholderPane(paneName) { const div = document.createElement("div"); div.className = "pane panecontainer"; div.style.display = "flex"; div.style.alignItems = "center"; div.style.justifyContent = "center"; div.style.flexDirection = "column"; div.style.color = "#888"; div.style.fontSize = "14px"; const title = document.createElement("div"); title.textContent = paneName || "Unknown Pane"; title.style.fontSize = "18px"; title.style.marginBottom = "8px"; const message = document.createElement("div"); message.textContent = "Coming Soon"; div.appendChild(title); div.appendChild(message); return div; } /** * Serializes the current layout to a layout definition * @param {HTMLElement} rootElement - The root element containing the layout * @returns {Object} Layout definition object */ export function serializeLayout(rootElement) { if (!rootElement.firstChild) { throw new Error("No layout to serialize"); } console.log('[serializeLayout] rootElement has', rootElement.children.length, 'children:'); for (let i = 0; i < rootElement.children.length; i++) { console.log(` [${i}]:`, rootElement.children[i].className); } let layoutNode; // Check if rootElement itself acts as a grid (has 2 children from a split) if (rootElement.children.length === 2) { // rootElement is acting as a grid container // Check if it has grid attributes const isHorizontal = rootElement.classList.contains("horizontal-grid"); const isVertical = rootElement.classList.contains("vertical-grid"); const percent = parseFloat(rootElement.getAttribute("lb-percent")) || 50; if (isHorizontal || isVertical) { console.log('[serializeLayout] rootElement is a grid, serializing both children'); layoutNode = { type: isHorizontal ? "horizontal-grid" : "vertical-grid", percent: percent, children: [ serializeLayoutNode(rootElement.children[0]), serializeLayoutNode(rootElement.children[1]) ] }; } else { // No grid classes, but has 2 children - this shouldn't happen but handle it console.warn('[serializeLayout] rootElement has 2 children but no grid classes, serializing first child only'); layoutNode = serializeLayoutNode(rootElement.firstChild); } } else { // Single child, serialize it directly console.log('[serializeLayout] Starting from element:', rootElement.firstChild.className); layoutNode = serializeLayoutNode(rootElement.firstChild); } console.log('[serializeLayout] Serialized layout:', JSON.stringify(layoutNode, null, 2)); return { name: "Custom Layout", description: "User-created layout", layout: layoutNode }; } /** * Recursively serializes a layout node * @private */ function serializeLayoutNode(element, depth = 0) { const indent = ' '.repeat(depth); console.log(`${indent}[serializeLayoutNode depth=${depth}] element:`, element.className, 'children:', element.children.length); if (!element) { throw new Error("Cannot serialize null element"); } // Check if this is a pane (has data-pane-name attribute) // This check must come first, as panes may also have grid classes for internal layout if (element.hasAttribute("data-pane-name")) { // The data-pane-name is kebab-case, but we need to save the camelCase key // that matches the panes object keys, not the name property const dataName = element.getAttribute("data-pane-name"); // Convert kebab-case to camelCase (e.g., "preset-browser" -> "presetBrowser") const camelCaseName = dataName.replace(/-([a-z0-9])/g, (g) => g[1].toUpperCase()); console.log(`${indent} -> Found pane: ${camelCaseName}`); return { type: "pane", name: camelCaseName }; } // Check if this is a grid (split pane structure) if (element.classList.contains("horizontal-grid") || element.classList.contains("vertical-grid")) { const isHorizontal = element.classList.contains("horizontal-grid"); const percent = parseFloat(element.getAttribute("lb-percent")) || 50; console.log(`${indent} -> Found ${isHorizontal ? 'horizontal' : 'vertical'} grid with ${percent}% split`); if (element.children.length !== 2) { throw new Error("Grid must have exactly 2 children"); } return { type: isHorizontal ? "horizontal-grid" : "vertical-grid", percent: percent, children: [ serializeLayoutNode(element.children[0], depth + 1), serializeLayoutNode(element.children[1], depth + 1) ] }; } // Check if this is a panecontainer wrapper - recurse into it if (element.classList.contains("panecontainer")) { console.log(`${indent} -> Found panecontainer, recursing into child`); if (element.children.length === 1) { return serializeLayoutNode(element.children[0], depth); } } // If element has only one child, recurse into it if (element.children.length === 1) { console.log(`${indent} -> Element has 1 child, recursing`); return serializeLayoutNode(element.children[0], depth); } throw new Error(`Cannot serialize element: ${element.className}`); } /** * Loads a layout by key or name * @param {string} keyOrName - Layout key or name * @returns {Object|null} Layout definition or null if not found */ export function loadLayoutByKeyOrName(keyOrName) { // First try as a key let layout = getLayout(keyOrName); // If not found, try as a name if (!layout) { layout = getLayoutByName(keyOrName); } // If still not found, check custom layouts if (!layout && config.customLayouts) { layout = config.customLayouts.find(l => l.name === keyOrName); } return layout; } /** * Saves a custom layout * @param {string} name - Name for the custom layout * @param {Object} layoutDef - Layout definition */ export function saveCustomLayout(name, layoutDef) { if (!config.customLayouts) { config.customLayouts = []; } // Check if layout with this name already exists const existingIndex = config.customLayouts.findIndex(l => l.name === name); const customLayout = { ...layoutDef, name: name, custom: true }; if (existingIndex >= 0) { // Update existing config.customLayouts[existingIndex] = customLayout; } else { // Add new config.customLayouts.push(customLayout); } } /** * Deletes a custom layout * @param {string} name - Name of the layout to delete * @returns {boolean} True if deleted, false if not found */ export function deleteCustomLayout(name) { if (!config.customLayouts) { return false; } const index = config.customLayouts.findIndex(l => l.name === name); if (index >= 0) { config.customLayouts.splice(index, 1); return true; } return false; }