Files
GIA/core/static/js/workspace-shell.js

1183 lines
35 KiB
JavaScript

(function () {
const WIDGET_SHELL_SELECTOR = ".js-gia-widget-shell";
const WIDGET_SPAWN_SELECTOR = ".js-widget-spawn-trigger";
const WIDGET_ACTION_SELECTOR = ".js-gia-widget-action";
const TASKBAR_SELECTOR = "#gia-taskbar";
const TASKBAR_ITEMS_SELECTOR = "#gia-taskbar-items";
const WORKSPACE_STASH_SELECTOR = "#gia-workspace-stash";
const SNAP_ASSISTANT_SELECTOR = "#gia-snap-assistant";
const SNAP_ASSISTANT_OPTIONS_SELECTOR = "#gia-snap-assistant-options";
const SNAP_ASSISTANT_CLOSE_SELECTOR = ".js-gia-snap-assistant-close";
const MOBILE_MEDIA_QUERY = "(max-width: 768px)";
const DESKTOP_MEDIA_QUERY = "(min-width: 1216px)";
const GRID_COLUMNS = 12;
const GRID_ROWS = 12;
const MIN_QUARTER_TILE_HEIGHT = 340;
const MIN_QUARTER_TILE_WIDTH = 420;
const assetPromises = {
styles: {},
scripts: {},
};
const workspaceState = {
order: [],
minimized: new Map(),
activeWidgetId: "",
snapLeftId: "",
snapRightId: "",
snapAssistantSourceId: "",
};
function withDocumentRoot(root) {
return root && typeof root.querySelectorAll === "function" ? root : document;
}
function toArray(value) {
return Array.isArray(value) ? value : Array.from(value || []);
}
function getGridElement() {
return window.gridElement || document.getElementById("grid-stack-main");
}
function getTaskbar() {
return document.querySelector(TASKBAR_SELECTOR);
}
function getTaskbarItems() {
return document.querySelector(TASKBAR_ITEMS_SELECTOR);
}
function getWorkspaceStash() {
return document.querySelector(WORKSPACE_STASH_SELECTOR);
}
function getSnapAssistant() {
return document.querySelector(SNAP_ASSISTANT_SELECTOR);
}
function getSnapAssistantOptions() {
return document.querySelector(SNAP_ASSISTANT_OPTIONS_SELECTOR);
}
function getGridViewportMetrics() {
const gridElement = getGridElement();
const gridColumn = gridElement && gridElement.parentElement ? gridElement.parentElement : null;
const main = gridColumn && gridColumn.parentElement ? gridColumn.parentElement : null;
const source = gridColumn || main;
const rect = source && source.getBoundingClientRect ? source.getBoundingClientRect() : null;
return {
width: Math.max(0, Math.floor((rect && rect.width) || window.innerWidth || 0)),
height: Math.max(0, Math.floor((rect && rect.height) || window.innerHeight || 0)),
};
}
function getWidgetNode(widgetId) {
const id = String(widgetId || "").trim();
return id ? document.getElementById(id) : null;
}
function syncKnownWidgetOrder() {
const ordered = workspaceState.order.filter(function (widgetId, index, items) {
return !!widgetId && items.indexOf(widgetId) === index && hasWidget(widgetId);
});
toArray(document.querySelectorAll(".grid-stack-item[id]")).forEach(function (node) {
if (!ordered.includes(node.id)) {
ordered.push(node.id);
}
});
workspaceState.order = ordered;
return ordered;
}
function getVisibleWidgetNodes() {
const gridElement = getGridElement();
return syncKnownWidgetOrder()
.map(getWidgetNode)
.filter(function (node) {
return !!(node && node.parentElement === gridElement);
});
}
function getVisibleWidgetIds() {
return getVisibleWidgetNodes().map(function (node) {
return node.id;
});
}
function hasWidget(widgetId) {
return !!getWidgetNode(widgetId);
}
function executeWidgetScript(scriptNode) {
if (!scriptNode) {
return;
}
const replacement = document.createElement("script");
toArray(scriptNode.attributes).forEach(function (attribute) {
replacement.setAttribute(attribute.name, attribute.value);
});
replacement.text = scriptNode.textContent || "";
document.body.appendChild(replacement);
replacement.remove();
scriptNode.remove();
}
function parseAssetList(value) {
return String(value || "")
.split("|")
.map(function (item) {
return String(item || "").trim();
})
.filter(Boolean);
}
function findStylesheet(href) {
return toArray(document.querySelectorAll('link[rel="stylesheet"]')).find(
function (node) {
return node.getAttribute("href") === href;
}
);
}
function findScript(src) {
return toArray(document.querySelectorAll("script[src]")).find(function (node) {
return node.getAttribute("src") === src;
});
}
function ensureStylesheet(href) {
if (!href) {
return Promise.resolve(null);
}
if (assetPromises.styles[href]) {
return assetPromises.styles[href];
}
const existing = findStylesheet(href);
if (existing) {
assetPromises.styles[href] = Promise.resolve(existing);
return assetPromises.styles[href];
}
assetPromises.styles[href] = new Promise(function (resolve) {
const node = document.createElement("link");
node.rel = "stylesheet";
node.href = href;
node.addEventListener(
"load",
function () {
resolve(node);
},
{ once: true }
);
node.addEventListener(
"error",
function () {
resolve(node);
},
{ once: true }
);
document.head.appendChild(node);
});
return assetPromises.styles[href];
}
function ensureScript(src) {
if (!src) {
return Promise.resolve(null);
}
if (assetPromises.scripts[src]) {
return assetPromises.scripts[src];
}
const existing = findScript(src);
if (existing) {
assetPromises.scripts[src] = Promise.resolve(existing);
return assetPromises.scripts[src];
}
assetPromises.scripts[src] = new Promise(function (resolve) {
const node = document.createElement("script");
node.src = src;
node.async = false;
node.addEventListener(
"load",
function () {
resolve(node);
},
{ once: true }
);
node.addEventListener(
"error",
function () {
resolve(node);
},
{ once: true }
);
document.head.appendChild(node);
});
return assetPromises.scripts[src];
}
function ensureContainerAssets(container) {
if (!container) {
return Promise.resolve();
}
const styleHrefs = parseAssetList(container.getAttribute("data-gia-style-hrefs"));
const scriptSrcs = parseAssetList(container.getAttribute("data-gia-script-srcs"));
const styleTasks = styleHrefs.map(function (href) {
return ensureStylesheet(href);
});
const scriptChain = scriptSrcs.reduce(function (promise, src) {
return promise.then(function () {
return ensureScript(src);
});
}, Promise.resolve());
return Promise.all(styleTasks).then(function () {
return scriptChain;
});
}
function getWidgetTitle(widgetNode) {
if (!widgetNode) {
return "Window";
}
const heading = widgetNode.querySelector(".gia-widget-title");
const title = String(heading ? heading.textContent || "" : "").trim();
return title || "Window";
}
function getWidgetIconClass(widgetNode) {
if (!widgetNode) {
return "fa-solid fa-window-maximize";
}
const icon = widgetNode.querySelector(".gia-widget-heading-icon i");
const value = String(icon ? icon.className || "" : "").trim();
return value || "fa-solid fa-window-maximize";
}
function clearSnapAssistant() {
workspaceState.snapAssistantSourceId = "";
const assistant = getSnapAssistant();
if (assistant) {
assistant.classList.add("is-hidden");
}
const options = getSnapAssistantOptions();
if (options) {
options.innerHTML = "";
}
}
function normalizeSnapState() {
if (!hasWidget(workspaceState.snapLeftId)) {
workspaceState.snapLeftId = "";
}
if (!hasWidget(workspaceState.snapRightId)) {
workspaceState.snapRightId = "";
}
if (workspaceState.snapLeftId && workspaceState.snapLeftId === workspaceState.snapRightId) {
workspaceState.snapRightId = "";
}
if (
workspaceState.snapAssistantSourceId
&& workspaceState.snapAssistantSourceId !== workspaceState.snapLeftId
) {
workspaceState.snapAssistantSourceId = "";
}
}
function setActiveWidget(widgetId) {
const id = String(widgetId || "").trim();
if (!id) {
return;
}
workspaceState.activeWidgetId = id;
toArray(document.querySelectorAll(".grid-stack-item.is-gia-active")).forEach(
function (node) {
node.classList.remove("is-gia-active");
}
);
const widgetNode = getWidgetNode(id);
if (widgetNode) {
widgetNode.classList.add("is-gia-active");
}
renderTaskbar();
}
function bindWidgetLifecycle(widgetNode) {
if (!widgetNode || widgetNode.dataset.giaWidgetBound === "1") {
return;
}
widgetNode.dataset.giaWidgetBound = "1";
widgetNode.addEventListener("pointerdown", function () {
setActiveWidget(widgetNode.id);
});
}
function registerWidget(widgetNode, insertIndex) {
if (!widgetNode || !widgetNode.id) {
return;
}
if (!workspaceState.order.includes(widgetNode.id)) {
if (typeof insertIndex === "number" && insertIndex >= 0) {
const boundedIndex = Math.min(insertIndex, workspaceState.order.length);
workspaceState.order.splice(boundedIndex, 0, widgetNode.id);
} else {
workspaceState.order.push(widgetNode.id);
}
}
bindWidgetLifecycle(widgetNode);
setActiveWidget(widgetNode.id);
}
function renderTaskbar() {
const taskbar = getTaskbar();
const itemsNode = getTaskbarItems();
if (!taskbar || !itemsNode) {
return;
}
const widgetIds = syncKnownWidgetOrder();
itemsNode.innerHTML = "";
if (!widgetIds.length) {
taskbar.classList.add("is-hidden");
return;
}
widgetIds.forEach(function (widgetId) {
const widgetNode = getWidgetNode(widgetId);
const minimized = workspaceState.minimized.has(widgetId);
const meta = minimized
? (workspaceState.minimized.get(widgetId) || {})
: {
title: getWidgetTitle(widgetNode),
iconClass: getWidgetIconClass(widgetNode),
};
const item = document.createElement("li");
if (widgetId === workspaceState.activeWidgetId) {
item.classList.add("is-active");
}
item.classList.add(minimized ? "is-minimized" : "is-open");
const link = document.createElement("a");
link.href = "#";
link.className = "js-gia-taskbar-item";
link.dataset.giaWidgetId = widgetId;
link.dataset.giaWidgetState = minimized ? "minimized" : "open";
link.setAttribute(
"aria-label",
(minimized ? "Restore " : "Minimize ") + String(meta.title || "window")
);
const iconWrap = document.createElement("span");
iconWrap.className = "icon is-small";
const icon = document.createElement("i");
icon.className = String(meta.iconClass || "fa-solid fa-window-maximize");
iconWrap.appendChild(icon);
const label = document.createElement("span");
label.textContent = String(meta.title || "Window");
link.appendChild(iconWrap);
link.appendChild(label);
item.appendChild(link);
itemsNode.appendChild(item);
});
taskbar.classList.remove("is-hidden");
}
function buildSnapAssistantOption(widgetId, widgetNode) {
const button = document.createElement("button");
button.type = "button";
button.className = "button is-light js-gia-snap-target";
button.dataset.giaWidgetId = widgetId;
const leftGroup = document.createElement("span");
leftGroup.className = "is-inline-flex is-align-items-center";
const iconWrap = document.createElement("span");
iconWrap.className = "icon is-small mr-2";
const icon = document.createElement("i");
icon.className = getWidgetIconClass(widgetNode);
iconWrap.appendChild(icon);
const label = document.createElement("span");
label.textContent = getWidgetTitle(widgetNode);
leftGroup.appendChild(iconWrap);
leftGroup.appendChild(label);
const actionLabel = document.createElement("span");
actionLabel.textContent = "Use";
button.appendChild(leftGroup);
button.appendChild(actionLabel);
return button;
}
function renderSnapAssistant() {
const assistant = getSnapAssistant();
const options = getSnapAssistantOptions();
if (!assistant || !options) {
return;
}
normalizeSnapState();
const sourceId = workspaceState.snapAssistantSourceId;
if (!sourceId) {
clearSnapAssistant();
return;
}
const candidates = getVisibleWidgetNodes().filter(function (node) {
return node.id !== sourceId;
});
if (!candidates.length) {
clearSnapAssistant();
return;
}
options.innerHTML = "";
candidates.forEach(function (node) {
options.appendChild(buildSnapAssistantOption(node.id, node));
});
assistant.classList.remove("is-hidden");
}
function getLayoutProfile() {
const isMobile = window.matchMedia(MOBILE_MEDIA_QUERY).matches;
const isDesktop = window.matchMedia(DESKTOP_MEDIA_QUERY).matches;
const viewport = getGridViewportMetrics();
const canFitQuarterTiles = (
isDesktop
&& viewport.width >= MIN_QUARTER_TILE_WIDTH * 2
&& viewport.height >= MIN_QUARTER_TILE_HEIGHT * 2
);
return {
isMobile: isMobile,
isDesktop: isDesktop,
maxVisible: isMobile ? 1 : canFitQuarterTiles ? 4 : 2,
};
}
function syncGridMetrics() {
if (!window.grid || typeof window.grid.cellHeight !== "function") {
return;
}
const gridElement = getGridElement();
if (!gridElement) {
return;
}
const viewport = getGridViewportMetrics();
const height = Math.max(240, viewport.height || 0);
const cellHeight = Math.max(32, Math.floor(height / GRID_ROWS));
gridElement.style.height = height + "px";
window.grid.cellHeight(cellHeight);
if (typeof window.grid.column === "function") {
window.grid.column(GRID_COLUMNS);
}
}
function buildDefaultLayout(widgetNodes, profile) {
if (!widgetNodes.length) {
return [];
}
if (profile.isMobile) {
return [
{
node: widgetNodes[0],
x: 0,
y: 0,
w: GRID_COLUMNS,
h: GRID_ROWS,
},
];
}
if (widgetNodes.length === 1) {
return [
{
node: widgetNodes[0],
x: 0,
y: 0,
w: GRID_COLUMNS,
h: GRID_ROWS,
},
];
}
if (!profile.isDesktop) {
return widgetNodes.slice(0, 2).map(function (node, index) {
return {
node: node,
x: 0,
y: index * (GRID_ROWS / 2),
w: GRID_COLUMNS,
h: GRID_ROWS / 2,
};
});
}
if (widgetNodes.length === 2) {
return widgetNodes.map(function (node, index) {
return {
node: node,
x: index * (GRID_COLUMNS / 2),
y: 0,
w: GRID_COLUMNS / 2,
h: GRID_ROWS,
};
});
}
return widgetNodes.slice(0, 4).map(function (node, index) {
return {
node: node,
x: (index % 2) * (GRID_COLUMNS / 2),
y: Math.floor(index / 2) * (GRID_ROWS / 2),
w: GRID_COLUMNS / 2,
h: GRID_ROWS / 2,
};
});
}
function buildSnappedLayout(profile) {
if (profile.isMobile) {
return null;
}
const leftNode = getWidgetNode(workspaceState.snapLeftId);
const rightNode = getWidgetNode(workspaceState.snapRightId);
if (!leftNode || !rightNode) {
return null;
}
if (leftNode.parentElement !== getGridElement()) {
return null;
}
if (rightNode.parentElement !== getGridElement()) {
return null;
}
return [
{
node: leftNode,
x: 0,
y: 0,
w: GRID_COLUMNS / 2,
h: GRID_ROWS,
},
{
node: rightNode,
x: GRID_COLUMNS / 2,
y: 0,
w: GRID_COLUMNS / 2,
h: GRID_ROWS,
},
];
}
function detachWidgetFromGrid(widgetNode) {
if (
widgetNode
&& window.grid
&& typeof window.grid.removeWidget === "function"
&& widgetNode.parentElement === getGridElement()
) {
window.grid.removeWidget(widgetNode, false, false);
}
}
function attachWidgetToGrid(widgetNode) {
if (!widgetNode || !window.grid || !getGridElement()) {
return;
}
widgetNode.classList.remove("is-hidden");
if (widgetNode.parentElement !== getGridElement()) {
getGridElement().appendChild(widgetNode);
}
if (typeof window.grid.makeWidget === "function") {
window.grid.makeWidget(widgetNode);
}
if (window.htmx && typeof window.htmx.process === "function") {
window.htmx.process(widgetNode);
}
}
function chooseFallbackActiveWidget() {
const visibleIds = getVisibleWidgetIds();
if (!visibleIds.length) {
workspaceState.activeWidgetId = "";
return;
}
workspaceState.activeWidgetId = visibleIds[visibleIds.length - 1];
}
function minimizeWidget(widgetId, options) {
const config = options || {};
const widgetNode = getWidgetNode(widgetId);
if (!widgetNode || workspaceState.minimized.has(widgetId)) {
return;
}
workspaceState.minimized.set(widgetId, {
title: getWidgetTitle(widgetNode),
iconClass: getWidgetIconClass(widgetNode),
ts: Date.now(),
});
if (workspaceState.activeWidgetId === widgetId) {
chooseFallbackActiveWidget();
}
if (workspaceState.snapLeftId === widgetId) {
workspaceState.snapLeftId = "";
workspaceState.snapRightId = "";
clearSnapAssistant();
}
if (workspaceState.snapRightId === widgetId) {
workspaceState.snapRightId = "";
clearSnapAssistant();
}
detachWidgetFromGrid(widgetNode);
const stash = getWorkspaceStash();
if (stash) {
stash.appendChild(widgetNode);
}
widgetNode.classList.add("is-hidden");
renderTaskbar();
if (!config.skipLayout) {
layoutWorkspace();
}
}
function restoreWidget(widgetId, options) {
const config = options || {};
const widgetNode = getWidgetNode(widgetId);
if (!widgetNode || !workspaceState.minimized.has(widgetId)) {
return;
}
workspaceState.minimized.delete(widgetId);
attachWidgetToGrid(widgetNode);
registerWidget(widgetNode);
renderTaskbar();
if (!config.skipLayout) {
layoutWorkspace();
}
}
function clearWidgetState(widgetId) {
workspaceState.order = workspaceState.order.filter(function (item) {
return item !== widgetId;
});
workspaceState.minimized.delete(widgetId);
if (workspaceState.activeWidgetId === widgetId) {
workspaceState.activeWidgetId = "";
}
if (workspaceState.snapLeftId === widgetId) {
workspaceState.snapLeftId = "";
}
if (workspaceState.snapRightId === widgetId) {
workspaceState.snapRightId = "";
}
if (workspaceState.snapAssistantSourceId === widgetId) {
clearSnapAssistant();
}
}
function removeWidget(widgetId) {
const widgetNode = getWidgetNode(widgetId);
if (!widgetNode) {
return;
}
clearWidgetState(widgetId);
if (
window.grid
&& typeof window.grid.removeWidget === "function"
&& widgetNode.parentElement === getGridElement()
) {
window.grid.removeWidget(widgetNode, true, false);
} else {
widgetNode.remove();
}
chooseFallbackActiveWidget();
renderTaskbar();
layoutWorkspace();
}
function enforceVisibleLimit(profile) {
normalizeSnapState();
const snappedLayout = buildSnappedLayout(profile);
const visibleIds = getVisibleWidgetIds();
const protectedIds = new Set(
[
workspaceState.activeWidgetId,
workspaceState.snapLeftId,
workspaceState.snapRightId,
].filter(Boolean)
);
const maxVisible = snappedLayout ? 2 : profile.maxVisible;
if (visibleIds.length <= maxVisible) {
return;
}
const removableIds = visibleIds.filter(function (widgetId) {
return !protectedIds.has(widgetId);
});
while (getVisibleWidgetIds().length > maxVisible && removableIds.length) {
minimizeWidget(removableIds.shift(), { skipLayout: true });
}
const remainingIds = getVisibleWidgetIds();
while (remainingIds.length > maxVisible) {
const candidate = remainingIds.find(function (widgetId) {
return widgetId !== workspaceState.activeWidgetId;
});
if (!candidate) {
break;
}
minimizeWidget(candidate, { skipLayout: true });
remainingIds.splice(remainingIds.indexOf(candidate), 1);
}
}
function applyLayout(layoutItems) {
if (!window.grid || !layoutItems.length) {
return;
}
syncGridMetrics();
if (typeof window.grid.batchUpdate === "function") {
window.grid.batchUpdate(true);
}
layoutItems.forEach(function (item) {
window.grid.update(item.node, {
x: item.x,
y: item.y,
w: item.w,
h: item.h,
});
});
if (typeof window.grid.batchUpdate === "function") {
window.grid.batchUpdate(false);
}
}
function layoutWorkspace() {
if (!window.grid || !getGridElement()) {
return;
}
normalizeSnapState();
const profile = getLayoutProfile();
enforceVisibleLimit(profile);
renderSnapAssistant();
const snappedLayout = buildSnappedLayout(profile);
const layoutItems = snappedLayout || buildDefaultLayout(getVisibleWidgetNodes(), profile);
applyLayout(layoutItems);
if (!workspaceState.activeWidgetId && layoutItems.length) {
setActiveWidget(layoutItems[layoutItems.length - 1].node.id);
} else if (workspaceState.activeWidgetId) {
const activeNode = getWidgetNode(workspaceState.activeWidgetId);
if (activeNode) {
activeNode.classList.add("is-gia-active");
}
}
renderTaskbar();
}
function tileWidget(widgetId) {
if (!widgetId) {
return;
}
if (workspaceState.snapLeftId === widgetId || workspaceState.snapRightId === widgetId) {
workspaceState.snapLeftId = "";
workspaceState.snapRightId = "";
}
if (workspaceState.snapAssistantSourceId === widgetId) {
clearSnapAssistant();
}
setActiveWidget(widgetId);
layoutWorkspace();
}
function chooseSnapCandidate(excludeId) {
const candidates = getVisibleWidgetNodes().filter(function (node) {
return node.id !== excludeId;
});
if (!candidates.length) {
return "";
}
const active = candidates.find(function (node) {
return node.id === workspaceState.activeWidgetId;
});
return active ? active.id : candidates[candidates.length - 1].id;
}
function snapWidgetLeft(widgetId) {
const id = String(widgetId || "").trim();
if (!id) {
return;
}
workspaceState.snapLeftId = id;
workspaceState.snapRightId = "";
workspaceState.snapAssistantSourceId = id;
setActiveWidget(id);
layoutWorkspace();
}
function snapWidgetRight(widgetId) {
const id = String(widgetId || "").trim();
if (!id) {
return;
}
if (!workspaceState.snapLeftId || workspaceState.snapLeftId === id) {
workspaceState.snapLeftId = chooseSnapCandidate(id);
}
workspaceState.snapRightId = id;
clearSnapAssistant();
setActiveWidget(id);
layoutWorkspace();
}
function chooseSnapRight(widgetId) {
const id = String(widgetId || "").trim();
if (!id) {
return;
}
workspaceState.snapRightId = id;
clearSnapAssistant();
setActiveWidget(id);
layoutWorkspace();
}
function replaceExistingWidget(widgetNode) {
if (!widgetNode || !widgetNode.id) {
return -1;
}
const existingWidget = document.getElementById(widgetNode.id);
if (!existingWidget) {
return -1;
}
const previousIndex = workspaceState.order.indexOf(widgetNode.id);
clearWidgetState(widgetNode.id);
if (
window.grid
&& typeof window.grid.removeWidget === "function"
&& existingWidget.parentElement === getGridElement()
) {
window.grid.removeWidget(existingWidget, true, false);
} else {
existingWidget.remove();
}
return previousIndex;
}
function processWidgetShell(container) {
if (!container || container.dataset.giaWidgetProcessed === "1") {
return Promise.resolve();
}
container.dataset.giaWidgetProcessed = "1";
const widgetNode = container.firstElementChild
? container.firstElementChild.cloneNode(true)
: null;
if (!widgetNode) {
container.remove();
return Promise.resolve();
}
const scripts = toArray(widgetNode.querySelectorAll("script"));
const previousIndex = replaceExistingWidget(widgetNode);
container.remove();
if (!window.grid || !getGridElement()) {
return Promise.resolve();
}
getGridElement().appendChild(widgetNode);
if (typeof window.grid.makeWidget === "function") {
window.grid.makeWidget(widgetNode);
} else if (typeof window.grid.addWidget === "function") {
window.grid.addWidget(widgetNode);
}
if (window.htmx && typeof window.htmx.process === "function") {
window.htmx.process(widgetNode);
}
window.giaEnableWidgetSpawnButtons(widgetNode);
const liveWidget = widgetNode.id ? document.getElementById(widgetNode.id) : widgetNode;
registerWidget(liveWidget, previousIndex);
return ensureContainerAssets(container).then(function () {
scripts.forEach(executeWidgetScript);
if (window.GIAComposePanel && typeof window.GIAComposePanel.initAll === "function") {
window.GIAComposePanel.initAll(liveWidget);
}
layoutWorkspace();
});
}
function processPendingWidgetShells(root) {
const scope = withDocumentRoot(root);
const containers = [];
if (
scope !== document
&& typeof scope.matches === "function"
&& scope.matches(WIDGET_SHELL_SELECTOR)
) {
containers.push(scope);
}
scope.querySelectorAll(WIDGET_SHELL_SELECTOR).forEach(function (node) {
containers.push(node);
});
Promise.all(containers.map(processWidgetShell)).finally(function () {
layoutWorkspace();
window.giaEnableWidgetSpawnButtons(document);
});
}
function enableFloatingWindowInteractions(windowEl) {
if (!windowEl || windowEl.dataset.giaWindowInteractive === "1") {
return;
}
windowEl.dataset.giaWindowInteractive = "1";
windowEl.setAttribute("unmovable", "");
const heading = windowEl.querySelector(".panel-heading");
if (!heading) {
return;
}
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
const onMove = function (event) {
if (!dragging) {
return;
}
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
windowEl.style.left = startLeft + deltaX + "px";
windowEl.style.top = startTop + deltaY + "px";
windowEl.style.right = "auto";
windowEl.style.bottom = "auto";
};
const stopDrag = function () {
dragging = false;
document.removeEventListener("pointermove", onMove);
document.removeEventListener("pointerup", stopDrag);
};
heading.addEventListener("pointerdown", function (event) {
if (event.button !== 0) {
return;
}
const interactive = event.target.closest(
"button, a, input, textarea, select, label, .delete, .icon"
);
if (interactive) {
return;
}
const windowRect = windowEl.getBoundingClientRect();
windowEl.style.position = "fixed";
startLeft = windowRect.left;
startTop = windowRect.top;
startX = event.clientX;
startY = event.clientY;
dragging = true;
document.addEventListener("pointermove", onMove);
document.addEventListener("pointerup", stopDrag);
event.preventDefault();
});
}
function positionFloatingWindow(windowEl) {
if (!windowEl) {
return;
}
const margin = 12;
const rect = windowEl.getBoundingClientRect();
const anchor = window.giaWindowAnchor || null;
windowEl.style.position = "fixed";
if (!anchor || Date.now() - anchor.ts > 10000) {
const fallbackLeft = Math.max(margin, Math.round((window.innerWidth - rect.width) / 2));
const fallbackTop = Math.max(margin, Math.round((window.innerHeight - rect.height) / 2));
windowEl.style.left = fallbackLeft + "px";
windowEl.style.top = fallbackTop + "px";
return;
}
const desiredLeftViewport = anchor.left;
const desiredTopViewport = anchor.bottom + 6;
const maxLeftViewport = window.innerWidth - rect.width - margin;
const maxTopViewport = window.innerHeight - rect.height - margin;
const boundedLeftViewport = Math.max(
margin,
Math.min(desiredLeftViewport, maxLeftViewport)
);
const boundedTopViewport = Math.max(
margin,
Math.min(desiredTopViewport, maxTopViewport)
);
windowEl.style.left = boundedLeftViewport + "px";
windowEl.style.top = boundedTopViewport + "px";
windowEl.style.right = "auto";
windowEl.style.bottom = "auto";
windowEl.style.transform = "none";
window.giaWindowAnchor = null;
}
function handleWidgetAction(action, widgetId) {
if (!action || !widgetId) {
return;
}
if (action === "close") {
removeWidget(widgetId);
return;
}
if (action === "minimize") {
minimizeWidget(widgetId);
return;
}
if (action === "tile") {
tileWidget(widgetId);
return;
}
if (action === "snap-left") {
snapWidgetLeft(widgetId);
return;
}
if (action === "snap-right") {
snapWidgetRight(widgetId);
}
}
function initWorkspaceShell() {
if (window.giaWorkspaceShellInitialised) {
processPendingWidgetShells(document);
return;
}
const gridElement = document.getElementById("grid-stack-main");
if (!gridElement || !window.GridStack) {
return;
}
window.giaWorkspaceShellInitialised = true;
window.gridElement = gridElement;
window.giaWindowAnchor = null;
document.body.classList.add("gia-has-workspace");
document.documentElement.classList.add("gia-has-workspace-root");
window.giaPrepareWidgetTarget = function () {
return document.getElementById("widgets-here");
};
window.giaCanSpawnWidgets = function () {
return !!(
window.grid &&
typeof window.grid.addWidget === "function" &&
document.getElementById("widgets-here")
);
};
window.giaEnableWidgetSpawnButtons = function (root) {
const scope = withDocumentRoot(root);
const canSpawn = window.giaCanSpawnWidgets();
scope.querySelectorAll(WIDGET_SPAWN_SELECTOR).forEach(function (button) {
const widgetUrl = String(
button.getAttribute("data-widget-url")
|| button.getAttribute("hx-get")
|| ""
).trim();
const visible = canSpawn && !!widgetUrl;
button.classList.toggle("is-hidden", !visible);
button.setAttribute("aria-hidden", visible ? "false" : "true");
});
};
window.giaPrepareWindowAnchor = function (trigger) {
if (!trigger || !trigger.getBoundingClientRect) {
window.giaWindowAnchor = null;
return;
}
const rect = trigger.getBoundingClientRect();
window.giaWindowAnchor = {
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
ts: Date.now(),
};
};
window.giaPositionFloatingWindow = positionFloatingWindow;
window.giaEnableFloatingWindowInteractions = enableFloatingWindowInteractions;
window.giaCompactGrid = layoutWorkspace;
window.giaRemoveWidget = removeWidget;
window.giaMinimizeWidget = minimizeWidget;
window.giaRestoreWidget = restoreWidget;
window.giaProcessWidgetShells = processPendingWidgetShells;
window.grid = window.GridStack.init(
{
animate: true,
auto: false,
cellHeight: 40,
column: GRID_COLUMNS,
float: false,
margin: 8,
removable: false,
staticGrid: true,
},
gridElement
);
document.addEventListener("click", function (event) {
const spawnTrigger = event.target.closest(WIDGET_SPAWN_SELECTOR);
if (spawnTrigger) {
window.giaPrepareWidgetTarget();
}
const widgetAction = event.target.closest(WIDGET_ACTION_SELECTOR);
if (widgetAction) {
event.preventDefault();
event.stopPropagation();
handleWidgetAction(
String(widgetAction.dataset.giaAction || ""),
String(widgetAction.dataset.giaWidgetId || "")
);
return;
}
const taskbarItem = event.target.closest(".js-gia-taskbar-item");
if (taskbarItem) {
event.preventDefault();
const widgetId = String(taskbarItem.dataset.giaWidgetId || "");
if (workspaceState.minimized.has(widgetId)) {
restoreWidget(widgetId);
} else {
minimizeWidget(widgetId);
}
return;
}
const snapTarget = event.target.closest(".js-gia-snap-target");
if (snapTarget) {
event.preventDefault();
chooseSnapRight(String(snapTarget.dataset.giaWidgetId || ""));
return;
}
if (event.target.closest(SNAP_ASSISTANT_CLOSE_SELECTOR)) {
event.preventDefault();
clearSnapAssistant();
}
});
document.body.addEventListener("htmx:afterSwap", function (event) {
const target = (event && event.target) || document;
window.giaEnableWidgetSpawnButtons(target);
if (((target && target.id) || "") === "widgets-here") {
processPendingWidgetShells(target);
return;
}
if (((target && target.id) || "") !== "windows-here") {
return;
}
target.querySelectorAll(".floating-window").forEach(function (floatingWindow) {
window.setTimeout(function () {
positionFloatingWindow(floatingWindow);
enableFloatingWindowInteractions(floatingWindow);
}, 0);
});
});
document.addEventListener("load-widget", function (event) {
processPendingWidgetShells(event && event.detail && event.detail.root);
});
document.addEventListener("gia:load-widget", function (event) {
processPendingWidgetShells(event && event.detail && event.detail.root);
});
window.addEventListener("resize", function () {
window.requestAnimationFrame(layoutWorkspace);
});
window.giaEnableWidgetSpawnButtons(document);
processPendingWidgetShells(document);
window.requestAnimationFrame(layoutWorkspace);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initWorkspaceShell);
} else {
initWorkspaceShell();
}
})();