2025-08-09 05:25:35 +02:00
|
|
|
|
// WINDOW MANAGER
|
|
|
|
|
|
const desktop = document.getElementById('desktop');
|
|
|
|
|
|
const bglayer = document.getElementById('background-layer');
|
|
|
|
|
|
const newWindowBtn = document.getElementById('new-window-btn');
|
|
|
|
|
|
let zIndexCounter = 1;
|
|
|
|
|
|
|
|
|
|
|
|
let focusedWindow = null;
|
|
|
|
|
|
let windows = {};
|
|
|
|
|
|
let windowHooks = [];
|
|
|
|
|
|
|
|
|
|
|
|
function uniqueid() {
|
|
|
|
|
|
// always start with a letter (for DOM friendlyness)
|
|
|
|
|
|
var idstr = String.fromCharCode(Math.floor((Math.random() * 25) + 65));
|
|
|
|
|
|
do {
|
|
|
|
|
|
// between numbers and characters (48 is 0 and 90 is Z (42-48 = 90)
|
|
|
|
|
|
var ascicode = Math.floor((Math.random() * 42) + 48);
|
|
|
|
|
|
if (ascicode < 58 || ascicode > 64) {
|
|
|
|
|
|
// exclude all chars between : (58) and @ (64)
|
|
|
|
|
|
idstr += String.fromCharCode(ascicode);
|
|
|
|
|
|
}
|
|
|
|
|
|
} while (idstr.length < 32);
|
|
|
|
|
|
|
|
|
|
|
|
return (idstr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fadeOut(fadeTarget, speed, callback) {
|
|
|
|
|
|
let eee = 0;
|
|
|
|
|
|
var fadeEffect = setInterval(function () {
|
|
|
|
|
|
if (!fadeTarget.style.opacity) {
|
|
|
|
|
|
fadeTarget.style.opacity = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
fadeTarget.style.transform = `rotate3d(1, 0, 0, ${eee}deg)`;
|
|
|
|
|
|
if (fadeTarget.style.opacity > 0) {
|
|
|
|
|
|
fadeTarget.style.opacity -= 0.1;
|
|
|
|
|
|
eee += 10;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
clearInterval(fadeEffect);
|
|
|
|
|
|
callback();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, speed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function killWind(id) {
|
|
|
|
|
|
const windowDiv = windows[id]?.windElems?.windowDiv;
|
|
|
|
|
|
if (typeof (windowDiv) == "undefined") {
|
|
|
|
|
|
console.warn("Attempted to close undefined window..");
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
fadeOut(windowDiv, 20, function () {
|
|
|
|
|
|
windowDiv.remove();
|
|
|
|
|
|
delete windows[id];
|
|
|
|
|
|
windowHooks.forEach(element => {
|
|
|
|
|
|
element({ event: "windowDestroy", id: id })
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function focusWindow(id) {
|
|
|
|
|
|
const windData = windows[id];
|
|
|
|
|
|
const windowDiv = windData?.windElems?.windowDiv;
|
|
|
|
|
|
if (typeof (windData) == "undefined") {
|
|
|
|
|
|
console.warn("Attempted to focus undefined window..");
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
windowDiv.style.zIndex = zIndexCounter++;
|
|
|
|
|
|
if (windData.layer == "background") windowDiv.style.zIndex = windowDiv.style.zIndex * -1;
|
|
|
|
|
|
windowHooks.forEach(element => {
|
|
|
|
|
|
element({ event: "windowFocused", id: id })
|
|
|
|
|
|
});
|
|
|
|
|
|
Object.keys(windows).forEach(key => {
|
|
|
|
|
|
const element = windows[key];
|
|
|
|
|
|
if (element.alwaysOnTop) {
|
|
|
|
|
|
element.windElems.windowDiv.style.zIndex = zIndexCounter + 5;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
focusedWindow = id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createWindow(url, settings) {
|
|
|
|
|
|
const windID = uniqueid();
|
|
|
|
|
|
const name = settings.name;
|
|
|
|
|
|
const headerless = settings.headerless || false;
|
|
|
|
|
|
const resizable = settings.resizable !== undefined ? settings.resizable : true;
|
|
|
|
|
|
const shouldIndent = settings.indent !== undefined ? settings.indent : true;
|
|
|
|
|
|
const layer = settings.layer || "foreground";
|
|
|
|
|
|
|
|
|
|
|
|
const windowDiv = document.createElement('div');
|
|
|
|
|
|
windowDiv.classList.add('window');
|
2025-08-09 17:22:25 +02:00
|
|
|
|
if (shouldIndent) windowDiv.style.top = '50px';
|
2025-08-09 05:25:35 +02:00
|
|
|
|
if (shouldIndent) windowDiv.style.left = '50px';
|
|
|
|
|
|
windowDiv.style.zIndex = zIndexCounter++;
|
|
|
|
|
|
windowDiv.id = windID;
|
|
|
|
|
|
|
|
|
|
|
|
// Header
|
|
|
|
|
|
const header = document.createElement('div');
|
|
|
|
|
|
header.classList.add('window-header');
|
|
|
|
|
|
|
|
|
|
|
|
if (headerless) header.classList.add("window-hidden");
|
|
|
|
|
|
|
|
|
|
|
|
const title = document.createElement('div');
|
|
|
|
|
|
title.classList.add('window-title');
|
|
|
|
|
|
title.textContent = name || url;
|
|
|
|
|
|
|
|
|
|
|
|
const closeBtn = document.createElement('button');
|
|
|
|
|
|
closeBtn.classList.add('window-close-btn');
|
|
|
|
|
|
closeBtn.textContent = '×';
|
|
|
|
|
|
|
|
|
|
|
|
header.appendChild(title);
|
|
|
|
|
|
header.appendChild(closeBtn);
|
|
|
|
|
|
windowDiv.appendChild(header);
|
|
|
|
|
|
|
|
|
|
|
|
// iframe
|
|
|
|
|
|
const iframe = document.createElement('iframe');
|
|
|
|
|
|
iframe.classList.add('window-iframe');
|
|
|
|
|
|
iframe.src = url;
|
|
|
|
|
|
windowDiv.appendChild(iframe);
|
|
|
|
|
|
iframe.onload = function () {
|
|
|
|
|
|
try {
|
|
|
|
|
|
iframe.contentDocument.oncontextmenu = function () {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch { };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Add resize handle if resizable
|
|
|
|
|
|
let resizeHandle = null;
|
|
|
|
|
|
if (resizable) {
|
|
|
|
|
|
resizeHandle = document.createElement('div');
|
|
|
|
|
|
resizeHandle.style.width = '16px';
|
|
|
|
|
|
resizeHandle.style.height = '16px';
|
|
|
|
|
|
resizeHandle.style.position = 'absolute';
|
|
|
|
|
|
resizeHandle.style.right = '0';
|
|
|
|
|
|
resizeHandle.style.bottom = '0';
|
|
|
|
|
|
resizeHandle.style.cursor = 'se-resize';
|
|
|
|
|
|
// Optional: make it visible if you want
|
|
|
|
|
|
// resizeHandle.style.background = 'rgba(0,0,0,0.2)';
|
|
|
|
|
|
windowDiv.appendChild(resizeHandle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (layer == "foreground") desktop.appendChild(windowDiv);
|
|
|
|
|
|
else bglayer.appendChild(windowDiv);
|
|
|
|
|
|
|
|
|
|
|
|
// Dragging functionality
|
|
|
|
|
|
let isDragging = false;
|
|
|
|
|
|
let offsetX, offsetY;
|
|
|
|
|
|
|
|
|
|
|
|
header.addEventListener('mousedown', (e) => {
|
|
|
|
|
|
isDragging = true;
|
|
|
|
|
|
offsetX = e.clientX - windowDiv.getBoundingClientRect().left;
|
|
|
|
|
|
offsetY = e.clientY - windowDiv.getBoundingClientRect().top;
|
|
|
|
|
|
focusWindow(windID);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Resizing variables
|
|
|
|
|
|
let isResizing = false;
|
|
|
|
|
|
let startWidth, startHeight, startX, startY;
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('mouseup', () => {
|
|
|
|
|
|
isDragging = false;
|
|
|
|
|
|
isResizing = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
|
|
|
|
if (isDragging) {
|
|
|
|
|
|
let newX = e.clientX - offsetX;
|
|
|
|
|
|
let newY = e.clientY - offsetY;
|
|
|
|
|
|
|
|
|
|
|
|
transformWindow(windID, null, null, newX, newY);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} else if (isResizing) {
|
|
|
|
|
|
const dx = e.clientX - startX;
|
|
|
|
|
|
const dy = e.clientY - startY;
|
|
|
|
|
|
|
|
|
|
|
|
const newWidth = startWidth + dx;
|
|
|
|
|
|
const newHeight = startHeight + dy;
|
|
|
|
|
|
|
|
|
|
|
|
transformWindow(windID, newWidth, newHeight);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Resize handle mousedown
|
|
|
|
|
|
if (resizeHandle) {
|
|
|
|
|
|
resizeHandle.addEventListener('mousedown', (e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
isResizing = true;
|
|
|
|
|
|
startWidth = windowDiv.offsetWidth;
|
|
|
|
|
|
startHeight = windowDiv.offsetHeight;
|
|
|
|
|
|
startX = e.clientX;
|
|
|
|
|
|
startY = e.clientY;
|
|
|
|
|
|
focusWindow(windID);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Focus window on click
|
|
|
|
|
|
windowDiv.addEventListener('mousedown', () => {
|
|
|
|
|
|
focusWindow(windID);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Close window
|
|
|
|
|
|
closeBtn.addEventListener('click', () => {
|
|
|
|
|
|
killWind(windID);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const windObj = {
|
|
|
|
|
|
iframe: iframe,
|
|
|
|
|
|
windElems: {
|
|
|
|
|
|
closeBtn: closeBtn,
|
|
|
|
|
|
windowDiv: windowDiv,
|
|
|
|
|
|
header: header,
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
resizeHandle: resizeHandle
|
|
|
|
|
|
},
|
|
|
|
|
|
windID: windID,
|
|
|
|
|
|
minSizeX: 250,
|
|
|
|
|
|
minSizeY: 150,
|
|
|
|
|
|
alwaysOnTop: false,
|
|
|
|
|
|
layer: layer,
|
|
|
|
|
|
hideInTaskbar: layer == "background"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
windows[windID] = windObj;
|
|
|
|
|
|
windowHooks.forEach(element => {
|
|
|
|
|
|
element({ event: "windowCreate", id: windID, window: windObj })
|
|
|
|
|
|
});
|
|
|
|
|
|
return windObj;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function transformWindow(id, sizex, sizey, posx, posy, ignoreRects) {
|
|
|
|
|
|
const windData = windows[id];
|
|
|
|
|
|
const windowDiv = windData?.windElems?.windowDiv;
|
|
|
|
|
|
if (typeof (windData) == "undefined") {
|
|
|
|
|
|
console.warn("Attempted to transform undefined window..");
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
if (sizex) windowDiv.style.width = Math.max(windData.minSizeX, sizex) + 'px';
|
|
|
|
|
|
if (sizey) windowDiv.style.height = Math.max(windData.minSizeY, sizey) + 'px';
|
|
|
|
|
|
const desktopRect = desktop.getBoundingClientRect();
|
|
|
|
|
|
const winRect = windowDiv.getBoundingClientRect();
|
|
|
|
|
|
if (posx) {
|
|
|
|
|
|
const newX = ignoreRects ? posx : Math.max(desktopRect.left, Math.min(posx, desktopRect.right - winRect.width));
|
|
|
|
|
|
windowDiv.style.left = newX - desktopRect.left + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (posy) {
|
|
|
|
|
|
const newY = ignoreRects ? posy : Math.max(desktopRect.top, Math.min(posy, desktopRect.bottom - winRect.height));
|
|
|
|
|
|
windowDiv.style.top = newY - desktopRect.top + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTransform(id) {
|
|
|
|
|
|
const windData = windows[id];
|
|
|
|
|
|
const windowDiv = windData?.windElems?.windowDiv;
|
|
|
|
|
|
if (typeof windData === "undefined" || !windowDiv) {
|
|
|
|
|
|
console.warn("Attempted to transform undefined window..");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const desktopRect = desktop.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
// Extract numeric values from style or fallback to computed styles
|
|
|
|
|
|
const style = windowDiv.style;
|
|
|
|
|
|
|
|
|
|
|
|
// Helper to parse px values safely
|
|
|
|
|
|
function parsePx(val) {
|
|
|
|
|
|
if (!val) return 0;
|
|
|
|
|
|
return Math.floor(parseFloat(val));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use inline style if present, otherwise computed style
|
|
|
|
|
|
let topStr = style.top || window.getComputedStyle(windowDiv).top;
|
|
|
|
|
|
let leftStr = style.left || window.getComputedStyle(windowDiv).left;
|
|
|
|
|
|
let widthStr = style.width || window.getComputedStyle(windowDiv).width;
|
|
|
|
|
|
let heightStr = style.height || window.getComputedStyle(windowDiv).height;
|
|
|
|
|
|
|
|
|
|
|
|
// Parse numbers from strings like "50px"
|
|
|
|
|
|
const posy = parsePx(topStr);
|
|
|
|
|
|
const posx = parsePx(leftStr);
|
|
|
|
|
|
const sizex = parsePx(widthStr);
|
|
|
|
|
|
const sizey = parsePx(heightStr);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
x: posx,
|
|
|
|
|
|
y: posy,
|
|
|
|
|
|
sizex: sizex,
|
|
|
|
|
|
sizey: sizey
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateWindowMin(id) {
|
|
|
|
|
|
const windData = windows[id];
|
|
|
|
|
|
if (typeof (windData) == "undefined") {
|
|
|
|
|
|
console.warn("Attempted to update undefined window..");
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
const transform = getTransform(id);
|
|
|
|
|
|
transformWindow(id,transform.sizex,transform.sizey,transform.posx,transform.posy);
|
|
|
|
|
|
|
|
|
|
|
|
}
|