Lightningbeam/src/layoutmanager.js

291 lines
9.3 KiB
JavaScript

// 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;
}