291 lines
9.3 KiB
JavaScript
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;
|
|
}
|