1891 lines
57 KiB
JavaScript
1891 lines
57 KiB
JavaScript
(function () {
|
|
const WIDGET_SHELL_SELECTOR = ".js-gia-widget-shell";
|
|
const WIDGET_SPAWN_SELECTOR = ".js-widget-spawn-trigger";
|
|
const WIDGET_LOAD_TARGET_SELECTOR = '[hx-target="#widgets-here"], [data-widget-url]';
|
|
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: {},
|
|
};
|
|
let widgetShellProcessingChain = Promise.resolve();
|
|
const workspaceState = {
|
|
order: [],
|
|
minimized: new Map(),
|
|
pendingWidgetIds: new Set(),
|
|
activeWidgetId: "",
|
|
anchorWidgetId: "",
|
|
pendingSpawnSourceId: "",
|
|
pendingSpawnTs: 0,
|
|
lastSpawnedId: "",
|
|
snapLeftId: "",
|
|
snapRightId: "",
|
|
snapTopId: "",
|
|
snapBottomId: "",
|
|
snapAssistantSourceId: "",
|
|
snapAssistantMode: "",
|
|
};
|
|
|
|
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 writeGridPositionAttrs(widgetNode, layoutItem) {
|
|
if (!widgetNode || !layoutItem) {
|
|
return;
|
|
}
|
|
widgetNode.setAttribute("gs-id", String(widgetNode.id || ""));
|
|
["x", "y", "w", "h"].forEach(function (key) {
|
|
const value = Number(layoutItem[key]);
|
|
if (!Number.isFinite(value)) {
|
|
return;
|
|
}
|
|
const attr = "gs-" + key;
|
|
if ((key === "w" || key === "h") && value <= 1) {
|
|
widgetNode.removeAttribute(attr);
|
|
return;
|
|
}
|
|
widgetNode.setAttribute(attr, String(value));
|
|
});
|
|
}
|
|
|
|
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 getAnchorWidgetId(visibleIds) {
|
|
const ids = Array.isArray(visibleIds) && visibleIds.length
|
|
? visibleIds.map(function (value) {
|
|
return String(value || "").trim();
|
|
})
|
|
: getVisibleWidgetIds();
|
|
const explicitAnchorId = String(workspaceState.anchorWidgetId || "").trim();
|
|
if (
|
|
explicitAnchorId
|
|
&& ids.includes(explicitAnchorId)
|
|
&& !workspaceState.minimized.has(explicitAnchorId)
|
|
) {
|
|
return explicitAnchorId;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
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 getWidgetMeta(widgetId) {
|
|
const node = getWidgetNode(widgetId);
|
|
const minimizedMeta = workspaceState.minimized.get(widgetId) || null;
|
|
return {
|
|
id: widgetId,
|
|
node: node,
|
|
minimized: workspaceState.minimized.has(widgetId),
|
|
title: minimizedMeta && minimizedMeta.title
|
|
? String(minimizedMeta.title)
|
|
: getWidgetTitle(node),
|
|
iconClass: minimizedMeta && minimizedMeta.iconClass
|
|
? String(minimizedMeta.iconClass)
|
|
: getWidgetIconClass(node),
|
|
};
|
|
}
|
|
|
|
function normalizeComposeWidgetSegment(value) {
|
|
const cleaned = String(value || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
return cleaned || "none";
|
|
}
|
|
|
|
function buildComposeWidgetIdFromUrl(urlValue) {
|
|
const raw = String(urlValue || "").trim();
|
|
if (!raw) {
|
|
return "";
|
|
}
|
|
try {
|
|
const parsed = new URL(raw, window.location.origin);
|
|
if (!/\/compose\/widget\/?$/.test(parsed.pathname)) {
|
|
return "";
|
|
}
|
|
const service = normalizeComposeWidgetSegment(parsed.searchParams.get("service"));
|
|
const identifier = normalizeComposeWidgetSegment(
|
|
String(parsed.searchParams.get("identifier") || "").split("@", 1)[0]
|
|
);
|
|
const person = normalizeComposeWidgetSegment(parsed.searchParams.get("person"));
|
|
return ["widget", "compose-widget", service, identifier, person].join("-");
|
|
} catch (_err) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function getWidgetUrlFromTrigger(trigger) {
|
|
if (!trigger || typeof trigger.getAttribute !== "function") {
|
|
return "";
|
|
}
|
|
return String(
|
|
trigger.getAttribute("data-widget-url")
|
|
|| trigger.getAttribute("hx-get")
|
|
|| ""
|
|
).trim();
|
|
}
|
|
|
|
function getRequestedWidgetIdFromTrigger(trigger) {
|
|
if (!trigger || typeof trigger.getAttribute !== "function") {
|
|
return "";
|
|
}
|
|
const explicitId = String(trigger.getAttribute("data-gia-widget-id") || "").trim();
|
|
if (explicitId) {
|
|
return explicitId;
|
|
}
|
|
return buildComposeWidgetIdFromUrl(getWidgetUrlFromTrigger(trigger));
|
|
}
|
|
|
|
function clearPendingWidgetRequest(trigger) {
|
|
if (!trigger || !trigger.dataset) {
|
|
return;
|
|
}
|
|
const widgetId = String(trigger.dataset.giaPendingWidgetId || "").trim();
|
|
if (widgetId) {
|
|
workspaceState.pendingWidgetIds.delete(widgetId);
|
|
}
|
|
delete trigger.dataset.giaPendingWidgetId;
|
|
}
|
|
|
|
function activateExistingWidget(widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id || !hasWidget(id)) {
|
|
return false;
|
|
}
|
|
if (workspaceState.minimized.has(id)) {
|
|
restoreWidget(id);
|
|
} else {
|
|
setActiveWidget(id);
|
|
syncWidgetChrome();
|
|
renderTaskbar();
|
|
}
|
|
if (window.GIAComposePanel && typeof window.GIAComposePanel.scrollWidgetToLatest === "function") {
|
|
window.GIAComposePanel.scrollWidgetToLatest(id);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function clearSnapAssistant() {
|
|
workspaceState.snapAssistantSourceId = "";
|
|
workspaceState.snapAssistantMode = "";
|
|
const assistant = getSnapAssistant();
|
|
if (assistant) {
|
|
assistant.classList.add("is-hidden");
|
|
}
|
|
const options = getSnapAssistantOptions();
|
|
if (options) {
|
|
options.innerHTML = "";
|
|
}
|
|
}
|
|
|
|
function normalizeSnapState() {
|
|
if (
|
|
workspaceState.anchorWidgetId
|
|
&& (
|
|
!hasWidget(workspaceState.anchorWidgetId)
|
|
|| workspaceState.minimized.has(workspaceState.anchorWidgetId)
|
|
)
|
|
) {
|
|
workspaceState.anchorWidgetId = "";
|
|
}
|
|
if (!hasWidget(workspaceState.snapLeftId)) {
|
|
workspaceState.snapLeftId = "";
|
|
}
|
|
if (!hasWidget(workspaceState.snapRightId)) {
|
|
workspaceState.snapRightId = "";
|
|
}
|
|
if (!hasWidget(workspaceState.snapTopId)) {
|
|
workspaceState.snapTopId = "";
|
|
}
|
|
if (!hasWidget(workspaceState.snapBottomId)) {
|
|
workspaceState.snapBottomId = "";
|
|
}
|
|
if (workspaceState.snapLeftId && workspaceState.snapLeftId === workspaceState.snapRightId) {
|
|
workspaceState.snapRightId = "";
|
|
}
|
|
if (workspaceState.snapTopId && workspaceState.snapTopId === workspaceState.snapBottomId) {
|
|
workspaceState.snapBottomId = "";
|
|
}
|
|
if (workspaceState.snapTopId || workspaceState.snapBottomId) {
|
|
workspaceState.snapLeftId = "";
|
|
workspaceState.snapRightId = "";
|
|
}
|
|
if (workspaceState.snapLeftId || workspaceState.snapRightId) {
|
|
workspaceState.snapTopId = "";
|
|
workspaceState.snapBottomId = "";
|
|
}
|
|
if (
|
|
workspaceState.snapAssistantSourceId
|
|
&& workspaceState.snapAssistantSourceId !== workspaceState.snapLeftId
|
|
&& workspaceState.snapAssistantSourceId !== workspaceState.snapTopId
|
|
) {
|
|
workspaceState.snapAssistantSourceId = "";
|
|
workspaceState.snapAssistantMode = "";
|
|
}
|
|
}
|
|
|
|
function getSnapAssistantMeta() {
|
|
if (workspaceState.snapAssistantMode === "vertical") {
|
|
return {
|
|
title: "Snap Bottom",
|
|
message: "Choose a second window for the bottom side.",
|
|
};
|
|
}
|
|
return {
|
|
title: "Snap Right",
|
|
message: "Choose a second window for the right side.",
|
|
};
|
|
}
|
|
|
|
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");
|
|
}
|
|
syncWidgetChrome();
|
|
renderTaskbar();
|
|
}
|
|
|
|
function syncWidgetChrome() {
|
|
const anchorId = getAnchorWidgetId(syncKnownWidgetOrder());
|
|
toArray(document.querySelectorAll(".grid-stack-item[id]")).forEach(function (node) {
|
|
const widgetId = String(node.id || "").trim();
|
|
const spawned = widgetId && widgetId === workspaceState.lastSpawnedId;
|
|
const gridNode = node.gridstackNode || null;
|
|
const atLeftEdge = !!(
|
|
gridNode
|
|
&& Number(gridNode.x || 0) === 0
|
|
&& Number(gridNode.w || GRID_COLUMNS) < GRID_COLUMNS
|
|
);
|
|
const atRightEdge = !!(
|
|
gridNode
|
|
&& Number((gridNode.x || 0) + (gridNode.w || GRID_COLUMNS)) === GRID_COLUMNS
|
|
&& Number(gridNode.w || GRID_COLUMNS) < GRID_COLUMNS
|
|
);
|
|
node.classList.toggle("is-gia-anchor", widgetId === anchorId);
|
|
node.classList.toggle("is-gia-spawned", spawned);
|
|
node.querySelectorAll('.js-gia-widget-action[data-gia-action="snap-left"]').forEach(
|
|
function (button) {
|
|
const icon = button.querySelector("i");
|
|
const label = atLeftEdge ? "Choose window for right side" : "Snap window left";
|
|
button.setAttribute("aria-label", label);
|
|
button.setAttribute("title", label);
|
|
button.classList.toggle("is-link", atLeftEdge);
|
|
if (icon) {
|
|
icon.className = atLeftEdge
|
|
? "fa-solid fa-table-columns"
|
|
: "fa-solid fa-arrow-left";
|
|
}
|
|
}
|
|
);
|
|
node.querySelectorAll('.js-gia-widget-action[data-gia-action="snap-right"]').forEach(
|
|
function (button) {
|
|
const icon = button.querySelector("i");
|
|
const label = atRightEdge ? "Choose window for left side" : "Snap window right";
|
|
button.setAttribute("aria-label", label);
|
|
button.setAttribute("title", label);
|
|
button.classList.toggle("is-link", atRightEdge);
|
|
if (icon) {
|
|
icon.className = atRightEdge
|
|
? "fa-solid fa-table-columns"
|
|
: "fa-solid fa-arrow-right";
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
function flashSpawnedWidget(widgetNode) {
|
|
if (!widgetNode || !widgetNode.id) {
|
|
return;
|
|
}
|
|
workspaceState.lastSpawnedId = widgetNode.id;
|
|
syncWidgetChrome();
|
|
window.setTimeout(function () {
|
|
if (workspaceState.lastSpawnedId === widgetNode.id) {
|
|
workspaceState.lastSpawnedId = "";
|
|
syncWidgetChrome();
|
|
}
|
|
}, 1800);
|
|
}
|
|
|
|
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 rememberPendingSpawn(trigger) {
|
|
const sourceWidget = trigger && trigger.closest
|
|
? trigger.closest(".grid-stack-item[id]")
|
|
: null;
|
|
const sourceId = String(sourceWidget && sourceWidget.id ? sourceWidget.id : "").trim();
|
|
workspaceState.pendingSpawnSourceId = sourceId;
|
|
workspaceState.pendingSpawnTs = Date.now();
|
|
if (sourceId) {
|
|
workspaceState.anchorWidgetId = sourceId;
|
|
syncWidgetChrome();
|
|
renderTaskbar();
|
|
}
|
|
}
|
|
|
|
function consumePendingSpawnContext() {
|
|
const sourceId = String(workspaceState.pendingSpawnSourceId || "").trim();
|
|
const spawnTs = Number(workspaceState.pendingSpawnTs || 0);
|
|
workspaceState.pendingSpawnSourceId = "";
|
|
workspaceState.pendingSpawnTs = 0;
|
|
if (!sourceId || (spawnTs > 0 && Date.now() - spawnTs > 15000)) {
|
|
return { sourceId: "" };
|
|
}
|
|
return { sourceId: sourceId };
|
|
}
|
|
|
|
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);
|
|
});
|
|
syncWidgetChrome();
|
|
taskbar.classList.remove("is-hidden");
|
|
}
|
|
|
|
function buildSnapAssistantOption(widgetMeta) {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "button is-light js-gia-snap-target";
|
|
button.dataset.giaWidgetId = widgetMeta.id;
|
|
if (widgetMeta.minimized) {
|
|
button.dataset.giaWidgetState = "minimized";
|
|
}
|
|
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 = widgetMeta.iconClass;
|
|
iconWrap.appendChild(icon);
|
|
const label = document.createElement("span");
|
|
label.textContent = widgetMeta.title;
|
|
leftGroup.appendChild(iconWrap);
|
|
leftGroup.appendChild(label);
|
|
const actionLabel = document.createElement("span");
|
|
actionLabel.textContent = widgetMeta.minimized ? "Restore" : "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 = syncKnownWidgetOrder().filter(function (widgetId) {
|
|
return widgetId !== sourceId && hasWidget(widgetId);
|
|
});
|
|
if (!candidates.length) {
|
|
clearSnapAssistant();
|
|
return;
|
|
}
|
|
options.innerHTML = "";
|
|
candidates.forEach(function (widgetId) {
|
|
options.appendChild(buildSnapAssistantOption(getWidgetMeta(widgetId)));
|
|
});
|
|
const meta = getSnapAssistantMeta();
|
|
const headingText = assistant.querySelector(".gia-snap-assistant-title");
|
|
if (headingText) {
|
|
headingText.textContent = meta.title;
|
|
}
|
|
const bodyText = assistant.querySelector(".gia-snap-assistant-message");
|
|
if (bodyText) {
|
|
bodyText.textContent = meta.message;
|
|
}
|
|
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.setAnimation === "function") {
|
|
window.grid.setAnimation(!window.matchMedia(MOBILE_MEDIA_QUERY).matches);
|
|
}
|
|
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,
|
|
};
|
|
});
|
|
}
|
|
if (widgetNodes.length === 3) {
|
|
const dominantNode = widgetNodes.find(function (node) {
|
|
return node.id === workspaceState.activeWidgetId;
|
|
}) || widgetNodes[widgetNodes.length - 1];
|
|
const secondaryNodes = widgetNodes.filter(function (node) {
|
|
return node !== dominantNode;
|
|
});
|
|
return [
|
|
{
|
|
node: dominantNode,
|
|
x: 0,
|
|
y: 0,
|
|
w: GRID_COLUMNS / 2,
|
|
h: GRID_ROWS,
|
|
},
|
|
{
|
|
node: secondaryNodes[0],
|
|
x: GRID_COLUMNS / 2,
|
|
y: 0,
|
|
w: GRID_COLUMNS / 2,
|
|
h: GRID_ROWS / 2,
|
|
},
|
|
{
|
|
node: secondaryNodes[1],
|
|
x: GRID_COLUMNS / 2,
|
|
y: GRID_ROWS / 2,
|
|
w: GRID_COLUMNS / 2,
|
|
h: GRID_ROWS / 2,
|
|
},
|
|
];
|
|
}
|
|
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 buildWorkspaceLayoutItems(widgetNodes, profile) {
|
|
const snappedLayout = buildSnappedLayout(profile);
|
|
const anchoredLayout = snappedLayout ? null : buildAnchoredLayout(widgetNodes, profile);
|
|
return snappedLayout || anchoredLayout || buildDefaultLayout(widgetNodes, profile);
|
|
}
|
|
|
|
function buildIncomingLayout(widgetNode, spawnSourceId) {
|
|
if (!widgetNode) {
|
|
return null;
|
|
}
|
|
const sourceId = String(spawnSourceId || "").trim();
|
|
const profile = getLayoutProfile();
|
|
const existingNodes = getVisibleWidgetNodes().filter(function (node) {
|
|
return node.id !== widgetNode.id;
|
|
});
|
|
let widgetNodes = existingNodes.slice();
|
|
if (sourceId) {
|
|
const sourceIndex = widgetNodes.findIndex(function (node) {
|
|
return node.id === sourceId;
|
|
});
|
|
if (sourceIndex >= 0) {
|
|
widgetNodes.splice(sourceIndex + 1, 0, widgetNode);
|
|
} else {
|
|
widgetNodes.push(widgetNode);
|
|
}
|
|
} else {
|
|
widgetNodes.push(widgetNode);
|
|
}
|
|
const previousSpawnedId = workspaceState.lastSpawnedId;
|
|
if (sourceId && sourceId !== widgetNode.id) {
|
|
workspaceState.lastSpawnedId = widgetNode.id;
|
|
}
|
|
const layoutItems = buildWorkspaceLayoutItems(widgetNodes, profile);
|
|
workspaceState.lastSpawnedId = previousSpawnedId;
|
|
return layoutItems.find(function (item) {
|
|
return item && item.node === widgetNode;
|
|
}) || null;
|
|
}
|
|
|
|
function buildAnchoredLayout(widgetNodes, profile) {
|
|
if (!widgetNodes.length || widgetNodes.length < 2) {
|
|
return null;
|
|
}
|
|
const anchorId = getAnchorWidgetId(
|
|
widgetNodes.map(function (node) {
|
|
return node.id;
|
|
})
|
|
);
|
|
if (!anchorId) {
|
|
return null;
|
|
}
|
|
const anchorNode = widgetNodes.find(function (node) {
|
|
return node.id === anchorId;
|
|
});
|
|
if (!anchorNode) {
|
|
return null;
|
|
}
|
|
let secondaryNodes = widgetNodes.filter(function (node) {
|
|
return node.id !== anchorId;
|
|
});
|
|
const spawnedIndex = secondaryNodes.findIndex(function (node) {
|
|
return node.id === workspaceState.lastSpawnedId;
|
|
});
|
|
if (spawnedIndex > 0) {
|
|
secondaryNodes = [secondaryNodes[spawnedIndex]].concat(
|
|
secondaryNodes.filter(function (_node, index) {
|
|
return index !== spawnedIndex;
|
|
})
|
|
);
|
|
}
|
|
if (!secondaryNodes.length) {
|
|
return null;
|
|
}
|
|
if (profile.isMobile || !profile.isDesktop) {
|
|
return [
|
|
{
|
|
node: anchorNode,
|
|
x: 0,
|
|
y: 0,
|
|
w: GRID_COLUMNS,
|
|
h: GRID_ROWS / 2,
|
|
},
|
|
{
|
|
node: secondaryNodes[0],
|
|
x: 0,
|
|
y: GRID_ROWS / 2,
|
|
w: GRID_COLUMNS,
|
|
h: GRID_ROWS / 2,
|
|
},
|
|
];
|
|
}
|
|
const stackedNodes = secondaryNodes.slice(0, 3);
|
|
const layoutItems = [
|
|
{
|
|
node: anchorNode,
|
|
x: 0,
|
|
y: 0,
|
|
w: GRID_COLUMNS / 2,
|
|
h: GRID_ROWS,
|
|
},
|
|
];
|
|
const baseHeight = Math.floor(GRID_ROWS / stackedNodes.length);
|
|
let offsetY = 0;
|
|
stackedNodes.forEach(function (node, index) {
|
|
const height = index === stackedNodes.length - 1
|
|
? GRID_ROWS - offsetY
|
|
: baseHeight;
|
|
layoutItems.push({
|
|
node: node,
|
|
x: GRID_COLUMNS / 2,
|
|
y: offsetY,
|
|
w: GRID_COLUMNS / 2,
|
|
h: height,
|
|
});
|
|
offsetY += height;
|
|
});
|
|
return layoutItems;
|
|
}
|
|
|
|
function buildSnappedLayout(profile) {
|
|
const gridElement = getGridElement();
|
|
const leftNode = getWidgetNode(workspaceState.snapLeftId);
|
|
const rightNode = getWidgetNode(workspaceState.snapRightId);
|
|
if (
|
|
leftNode
|
|
&& rightNode
|
|
&& leftNode.parentElement === gridElement
|
|
&& rightNode.parentElement === gridElement
|
|
&& !profile.isMobile
|
|
) {
|
|
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,
|
|
},
|
|
];
|
|
}
|
|
const topNode = getWidgetNode(workspaceState.snapTopId);
|
|
const bottomNode = getWidgetNode(workspaceState.snapBottomId);
|
|
if (
|
|
!topNode
|
|
|| !bottomNode
|
|
|| topNode.parentElement !== gridElement
|
|
|| bottomNode.parentElement !== gridElement
|
|
) {
|
|
return null;
|
|
}
|
|
return [
|
|
{
|
|
node: topNode,
|
|
x: 0,
|
|
y: 0,
|
|
w: GRID_COLUMNS,
|
|
h: GRID_ROWS / 2,
|
|
},
|
|
{
|
|
node: bottomNode,
|
|
x: 0,
|
|
y: GRID_ROWS / 2,
|
|
w: GRID_COLUMNS,
|
|
h: GRID_ROWS / 2,
|
|
},
|
|
];
|
|
}
|
|
|
|
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;
|
|
}
|
|
const anchorId = getAnchorWidgetId(visibleIds);
|
|
workspaceState.activeWidgetId = anchorId || 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.lastSpawnedId === widgetId) {
|
|
workspaceState.lastSpawnedId = "";
|
|
}
|
|
if (workspaceState.snapLeftId === widgetId) {
|
|
workspaceState.snapLeftId = "";
|
|
workspaceState.snapRightId = "";
|
|
clearSnapAssistant();
|
|
}
|
|
if (workspaceState.snapRightId === widgetId) {
|
|
workspaceState.snapRightId = "";
|
|
clearSnapAssistant();
|
|
}
|
|
if (workspaceState.snapTopId === widgetId) {
|
|
workspaceState.snapTopId = "";
|
|
workspaceState.snapBottomId = "";
|
|
clearSnapAssistant();
|
|
}
|
|
if (workspaceState.snapBottomId === widgetId) {
|
|
workspaceState.snapBottomId = "";
|
|
clearSnapAssistant();
|
|
}
|
|
if (workspaceState.anchorWidgetId === widgetId) {
|
|
workspaceState.anchorWidgetId = "";
|
|
}
|
|
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.anchorWidgetId === widgetId) {
|
|
workspaceState.anchorWidgetId = "";
|
|
}
|
|
if (workspaceState.lastSpawnedId === widgetId) {
|
|
workspaceState.lastSpawnedId = "";
|
|
}
|
|
if (workspaceState.snapLeftId === widgetId) {
|
|
workspaceState.snapLeftId = "";
|
|
}
|
|
if (workspaceState.snapRightId === widgetId) {
|
|
workspaceState.snapRightId = "";
|
|
}
|
|
if (workspaceState.snapTopId === widgetId) {
|
|
workspaceState.snapTopId = "";
|
|
}
|
|
if (workspaceState.snapBottomId === widgetId) {
|
|
workspaceState.snapBottomId = "";
|
|
}
|
|
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 anchorId = getAnchorWidgetId(syncKnownWidgetOrder());
|
|
const visibleIds = getVisibleWidgetIds();
|
|
const hardProtectedIds = new Set([anchorId].filter(Boolean));
|
|
const softProtectedIds = new Set(
|
|
[
|
|
workspaceState.activeWidgetId,
|
|
workspaceState.snapLeftId,
|
|
workspaceState.snapRightId,
|
|
workspaceState.snapTopId,
|
|
workspaceState.snapBottomId,
|
|
].filter(Boolean)
|
|
);
|
|
const anchoredMobileSplit = !snappedLayout && !!anchorId && profile.isMobile;
|
|
const maxVisible = snappedLayout ? 2 : anchoredMobileSplit ? 2 : profile.maxVisible;
|
|
if (visibleIds.length <= maxVisible) {
|
|
return;
|
|
}
|
|
const removableIds = visibleIds.filter(function (widgetId) {
|
|
return !hardProtectedIds.has(widgetId) && !softProtectedIds.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 !hardProtectedIds.has(widgetId) && !softProtectedIds.has(widgetId);
|
|
}) || remainingIds.find(function (widgetId) {
|
|
return !hardProtectedIds.has(widgetId);
|
|
});
|
|
if (!candidate) {
|
|
break;
|
|
}
|
|
minimizeWidget(candidate, { skipLayout: true });
|
|
remainingIds.splice(remainingIds.indexOf(candidate), 1);
|
|
}
|
|
}
|
|
|
|
function applyLayout(layoutItems) {
|
|
if (!window.grid || !layoutItems.length) {
|
|
return;
|
|
}
|
|
syncGridMetrics();
|
|
layoutItems.forEach(function (item) {
|
|
writeGridPositionAttrs(item.node, item);
|
|
});
|
|
const canLoadAtomically = typeof window.grid.load === "function"
|
|
&& layoutItems.every(function (item) {
|
|
return !!(
|
|
item
|
|
&& item.node
|
|
&& item.node.id
|
|
&& item.node.gridstackNode
|
|
&& String(item.node.gridstackNode.id || "") === String(item.node.id || "")
|
|
);
|
|
});
|
|
if (canLoadAtomically) {
|
|
window.grid.load(
|
|
layoutItems.map(function (item) {
|
|
return {
|
|
id: item.node.id,
|
|
x: item.x,
|
|
y: item.y,
|
|
w: item.w,
|
|
h: item.h,
|
|
};
|
|
}),
|
|
false
|
|
);
|
|
return;
|
|
}
|
|
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 visibleNodes = getVisibleWidgetNodes();
|
|
const layoutItems = buildWorkspaceLayoutItems(visibleNodes, 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.snapTopId === widgetId || workspaceState.snapBottomId === widgetId) {
|
|
workspaceState.snapTopId = "";
|
|
workspaceState.snapBottomId = "";
|
|
}
|
|
if (workspaceState.snapAssistantSourceId === widgetId) {
|
|
clearSnapAssistant();
|
|
}
|
|
setActiveWidget(widgetId);
|
|
layoutWorkspace();
|
|
}
|
|
|
|
function getSnapCandidateIds(excludeId, options) {
|
|
const config = options || {};
|
|
const candidates = [];
|
|
const pushCandidate = function (widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id || id === excludeId || candidates.includes(id) || !hasWidget(id)) {
|
|
return;
|
|
}
|
|
candidates.push(id);
|
|
};
|
|
const visibleIds = getVisibleWidgetIds().filter(function (widgetId) {
|
|
return widgetId !== excludeId;
|
|
});
|
|
pushCandidate(getAnchorWidgetId(visibleIds));
|
|
pushCandidate(workspaceState.activeWidgetId);
|
|
visibleIds.forEach(pushCandidate);
|
|
if (config.includeMinimized) {
|
|
getLockedWidgetIds().forEach(pushCandidate);
|
|
syncKnownWidgetOrder().forEach(function (widgetId) {
|
|
if (workspaceState.minimized.has(widgetId)) {
|
|
pushCandidate(widgetId);
|
|
}
|
|
});
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function chooseSnapCandidate(excludeId, options) {
|
|
const candidates = getSnapCandidateIds(excludeId, options);
|
|
return candidates.length ? candidates[0] : "";
|
|
}
|
|
|
|
function ensureWidgetVisible(widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id) {
|
|
return "";
|
|
}
|
|
if (workspaceState.minimized.has(id)) {
|
|
restoreWidget(id, { skipLayout: true });
|
|
}
|
|
return id;
|
|
}
|
|
|
|
function snapWidgetLeft(widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id) {
|
|
return;
|
|
}
|
|
const candidates = getSnapCandidateIds(id, { includeMinimized: true });
|
|
workspaceState.snapTopId = "";
|
|
workspaceState.snapBottomId = "";
|
|
workspaceState.snapLeftId = id;
|
|
if (candidates.length === 1) {
|
|
workspaceState.snapRightId = ensureWidgetVisible(candidates[0]);
|
|
clearSnapAssistant();
|
|
} else {
|
|
workspaceState.snapRightId = "";
|
|
workspaceState.snapAssistantSourceId = id;
|
|
workspaceState.snapAssistantMode = "horizontal";
|
|
}
|
|
setActiveWidget(id);
|
|
layoutWorkspace();
|
|
}
|
|
|
|
function snapWidgetRight(widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id) {
|
|
return;
|
|
}
|
|
if (!workspaceState.snapLeftId || workspaceState.snapLeftId === id) {
|
|
workspaceState.snapLeftId = ensureWidgetVisible(
|
|
chooseSnapCandidate(id, { includeMinimized: true })
|
|
);
|
|
}
|
|
workspaceState.snapTopId = "";
|
|
workspaceState.snapBottomId = "";
|
|
workspaceState.snapRightId = id;
|
|
clearSnapAssistant();
|
|
setActiveWidget(id);
|
|
layoutWorkspace();
|
|
}
|
|
|
|
function chooseSnapRight(widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id) {
|
|
return;
|
|
}
|
|
ensureWidgetVisible(id);
|
|
workspaceState.snapTopId = "";
|
|
workspaceState.snapBottomId = "";
|
|
workspaceState.snapRightId = id;
|
|
clearSnapAssistant();
|
|
setActiveWidget(id);
|
|
layoutWorkspace();
|
|
}
|
|
|
|
function snapWidgetTop(widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id) {
|
|
return;
|
|
}
|
|
const partnerId = ensureWidgetVisible(
|
|
chooseSnapCandidate(id, { includeMinimized: true })
|
|
);
|
|
workspaceState.snapLeftId = "";
|
|
workspaceState.snapRightId = "";
|
|
workspaceState.snapTopId = id;
|
|
workspaceState.snapBottomId = partnerId;
|
|
clearSnapAssistant();
|
|
setActiveWidget(id);
|
|
layoutWorkspace();
|
|
}
|
|
|
|
function snapWidgetBottom(widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id) {
|
|
return;
|
|
}
|
|
const partnerId = ensureWidgetVisible(
|
|
chooseSnapCandidate(id, { includeMinimized: true })
|
|
);
|
|
workspaceState.snapLeftId = "";
|
|
workspaceState.snapRightId = "";
|
|
workspaceState.snapTopId = partnerId;
|
|
workspaceState.snapBottomId = id;
|
|
clearSnapAssistant();
|
|
setActiveWidget(id);
|
|
layoutWorkspace();
|
|
}
|
|
|
|
function chooseSnapBottom(widgetId) {
|
|
const id = String(widgetId || "").trim();
|
|
if (!id) {
|
|
return;
|
|
}
|
|
ensureWidgetVisible(id);
|
|
workspaceState.snapLeftId = "";
|
|
workspaceState.snapRightId = "";
|
|
workspaceState.snapBottomId = 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 spawnContext = consumePendingSpawnContext();
|
|
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();
|
|
}
|
|
const initialLayout = buildIncomingLayout(widgetNode, spawnContext.sourceId);
|
|
if (initialLayout) {
|
|
writeGridPositionAttrs(widgetNode, initialLayout);
|
|
}
|
|
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;
|
|
if (liveWidget && liveWidget.id) {
|
|
workspaceState.pendingWidgetIds.delete(liveWidget.id);
|
|
}
|
|
let insertIndex = previousIndex;
|
|
if (spawnContext.sourceId && spawnContext.sourceId !== liveWidget.id) {
|
|
const sourceIndex = workspaceState.order.indexOf(spawnContext.sourceId);
|
|
if (sourceIndex >= 0) {
|
|
insertIndex = sourceIndex + 1;
|
|
}
|
|
workspaceState.anchorWidgetId = spawnContext.sourceId;
|
|
liveWidget.dataset.giaSpawnSourceId = spawnContext.sourceId;
|
|
} else {
|
|
delete liveWidget.dataset.giaSpawnSourceId;
|
|
}
|
|
registerWidget(liveWidget, insertIndex);
|
|
return ensureContainerAssets(container).then(function () {
|
|
scripts.forEach(executeWidgetScript);
|
|
if (window.GIAComposePanel && typeof window.GIAComposePanel.initAll === "function") {
|
|
window.GIAComposePanel.initAll(liveWidget);
|
|
}
|
|
if (spawnContext.sourceId && spawnContext.sourceId !== liveWidget.id) {
|
|
flashSpawnedWidget(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);
|
|
});
|
|
widgetShellProcessingChain = widgetShellProcessingChain
|
|
.catch(function () {
|
|
return null;
|
|
})
|
|
.then(function () {
|
|
return containers.reduce(function (promise, containerNode) {
|
|
return promise.then(function () {
|
|
return processWidgetShell(containerNode);
|
|
});
|
|
}, Promise.resolve());
|
|
})
|
|
.finally(function () {
|
|
layoutWorkspace();
|
|
window.giaEnableWidgetSpawnButtons(document);
|
|
});
|
|
return widgetShellProcessingChain;
|
|
}
|
|
|
|
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-top") {
|
|
snapWidgetTop(widgetId);
|
|
return;
|
|
}
|
|
if (action === "snap-right") {
|
|
snapWidgetRight(widgetId);
|
|
return;
|
|
}
|
|
if (action === "snap-bottom") {
|
|
snapWidgetBottom(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)
|
|
|| event.target.closest(WIDGET_LOAD_TARGET_SELECTOR);
|
|
if (spawnTrigger) {
|
|
rememberPendingSpawn(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();
|
|
if (workspaceState.snapAssistantMode === "vertical") {
|
|
chooseSnapBottom(String(snapTarget.dataset.giaWidgetId || ""));
|
|
} else {
|
|
chooseSnapRight(String(snapTarget.dataset.giaWidgetId || ""));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event.target.closest(SNAP_ASSISTANT_CLOSE_SELECTOR)) {
|
|
event.preventDefault();
|
|
clearSnapAssistant();
|
|
}
|
|
});
|
|
|
|
document.body.addEventListener("htmx:beforeRequest", function (event) {
|
|
const trigger = event && event.detail ? event.detail.elt : null;
|
|
if (!trigger || typeof trigger.getAttribute !== "function") {
|
|
return;
|
|
}
|
|
const target = event.detail ? event.detail.target : null;
|
|
const targetId = String((target && target.id) || trigger.getAttribute("hx-target") || "")
|
|
.replace(/^#/, "")
|
|
.trim();
|
|
if (targetId !== "widgets-here") {
|
|
return;
|
|
}
|
|
const widgetId = getRequestedWidgetIdFromTrigger(trigger);
|
|
if (!widgetId) {
|
|
return;
|
|
}
|
|
if (workspaceState.pendingWidgetIds.has(widgetId) || hasWidget(widgetId)) {
|
|
event.preventDefault();
|
|
activateExistingWidget(widgetId);
|
|
return;
|
|
}
|
|
workspaceState.pendingWidgetIds.add(widgetId);
|
|
trigger.dataset.giaPendingWidgetId = widgetId;
|
|
});
|
|
|
|
document.body.addEventListener("htmx:afterRequest", function (event) {
|
|
const trigger = event && event.detail ? event.detail.elt : null;
|
|
if (!trigger || !trigger.dataset || !trigger.dataset.giaPendingWidgetId) {
|
|
return;
|
|
}
|
|
if (event.detail && event.detail.successful) {
|
|
return;
|
|
}
|
|
clearPendingWidgetRequest(trigger);
|
|
});
|
|
|
|
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();
|
|
}
|
|
})();
|