1183 lines
35 KiB
JavaScript
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();
|
|
}
|
|
})();
|