Files
scrum-solitare/static/js/ui-controls.js
2026-03-06 11:11:08 +02:00

411 lines
14 KiB
JavaScript

(() => {
const THEME_KEY = 'scrumPoker.ui.theme';
const MODE_KEY = 'scrumPoker.ui.mode';
const WINDOW_LAYOUTS_KEY = 'scrumPoker.ui.windowLayouts.v1';
const DEFAULT_THEME = 'win98';
const MODE_ICON_LIGHT = '/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png';
const MODE_ICON_DARK = '/static/img/Windows Icons - PNG/desk.cpl_14_40-6.png';
const DEFAULT_WINDOW_LAYOUTS = {
'theme-tool-window': { left: 16, top: 88, width: 390, height: 250 },
'mode-tool-window': { left: 424, top: 88, width: 340, height: 190 },
};
let floatingWindowZ = 80;
let windowLayouts = {};
function isMobileViewport() {
return window.matchMedia('(max-width: 899px)').matches;
}
function applyTheme(theme) {
const normalized = theme || DEFAULT_THEME;
document.documentElement.setAttribute('data-ui-theme', normalized);
}
function applyMode(mode) {
if (mode === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
return;
}
document.documentElement.removeAttribute('data-theme');
}
function getCurrentMode() {
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
}
function isWindowOpen(id) {
const win = document.getElementById(id);
return Boolean(win && !win.classList.contains('hidden'));
}
function syncTaskButtons() {
document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
const target = button.dataset.target;
button.classList.toggle('is-active', isWindowOpen(target));
});
}
function syncControls() {
const theme = document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME;
const mode = getCurrentMode();
const modeLabel = mode === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode';
const modeIcon = mode === 'dark' ? MODE_ICON_DARK : MODE_ICON_LIGHT;
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
const selected = button.dataset.theme === theme;
button.classList.toggle('is-selected', selected);
button.setAttribute('aria-pressed', selected ? 'true' : 'false');
});
document.querySelectorAll('[data-role="mode-toggle-label"]').forEach((el) => {
el.textContent = modeLabel;
});
document.querySelectorAll('[data-role="mode-icon"]').forEach((el) => {
el.src = modeIcon;
});
document.querySelectorAll('#mode-status-text').forEach((el) => {
el.textContent = `Current mode: ${mode === 'dark' ? 'Dark' : 'Light'}`;
});
syncTaskButtons();
}
function bringWindowToFront(windowEl) {
floatingWindowZ += 1;
windowEl.style.zIndex = String(floatingWindowZ);
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function loadWindowLayouts() {
try {
const raw = localStorage.getItem(WINDOW_LAYOUTS_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return {};
}
return parsed;
} catch (_err) {
return {};
}
}
function saveWindowLayouts() {
localStorage.setItem(WINDOW_LAYOUTS_KEY, JSON.stringify(windowLayouts));
}
function normalizeLayout(raw, defaults) {
const fallback = defaults || { left: 16, top: 88, width: 360, height: 220 };
const minWidth = 260;
const minHeight = 160;
const width = Number(raw?.width);
const height = Number(raw?.height);
const left = Number(raw?.left);
const top = Number(raw?.top);
return {
width: Number.isFinite(width) ? Math.max(minWidth, width) : fallback.width,
height: Number.isFinite(height) ? Math.max(minHeight, height) : fallback.height,
left: Number.isFinite(left) ? left : fallback.left,
top: Number.isFinite(top) ? top : fallback.top,
};
}
function clampLayoutToViewport(layout) {
const margin = 8;
const minWidth = 260;
const minHeight = 160;
const maxWidth = Math.max(minWidth, window.innerWidth - margin * 2);
const maxHeight = Math.max(minHeight, window.innerHeight - margin * 2);
const width = clamp(layout.width, minWidth, maxWidth);
const height = clamp(layout.height, minHeight, maxHeight);
const left = clamp(layout.left, margin, Math.max(margin, window.innerWidth - width - margin));
const top = clamp(layout.top, margin, Math.max(margin, window.innerHeight - height - margin));
return { left, top, width, height };
}
function applyLayout(windowEl, layout) {
windowEl.style.left = `${layout.left}px`;
windowEl.style.top = `${layout.top}px`;
windowEl.style.width = `${layout.width}px`;
windowEl.style.height = `${layout.height}px`;
windowEl.style.right = 'auto';
windowEl.style.bottom = 'auto';
windowEl.style.transform = 'none';
}
function readLayoutFromDOM(windowEl) {
const rect = windowEl.getBoundingClientRect();
return {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
};
}
function persistWindowLayout(windowEl) {
const id = windowEl.id;
if (!id) {
return;
}
const next = clampLayoutToViewport(readLayoutFromDOM(windowEl));
windowLayouts[id] = next;
saveWindowLayouts();
}
function ensureWindowLayout(windowEl) {
const id = windowEl.id;
if (!id) {
return;
}
if (isMobileViewport()) {
windowEl.style.right = 'auto';
windowEl.style.bottom = 'auto';
windowEl.style.transform = 'none';
return;
}
const defaults = DEFAULT_WINDOW_LAYOUTS[id];
const saved = windowLayouts[id];
const normalized = normalizeLayout(saved, defaults);
const clamped = clampLayoutToViewport(normalized);
applyLayout(windowEl, clamped);
windowLayouts[id] = clamped;
}
function openToolWindow(id) {
const windowEl = document.getElementById(id);
if (!windowEl) {
return;
}
ensureWindowLayout(windowEl);
windowEl.classList.remove('hidden');
bringWindowToFront(windowEl);
syncTaskButtons();
}
function closeToolWindow(id) {
const windowEl = document.getElementById(id);
if (!windowEl) {
return;
}
windowEl.classList.add('hidden');
syncTaskButtons();
}
function initDraggableWindows() {
let dragState = null;
document.querySelectorAll('[data-role="drag-handle"]').forEach((handle) => {
handle.addEventListener('pointerdown', (event) => {
if (event.button !== 0) {
return;
}
if (event.target.closest('button')) {
return;
}
const windowEl = handle.closest('.ui-tool-window');
if (!windowEl || windowEl.classList.contains('hidden')) {
return;
}
if (isMobileViewport()) {
return;
}
bringWindowToFront(windowEl);
const rect = windowEl.getBoundingClientRect();
windowEl.style.left = `${rect.left}px`;
windowEl.style.top = `${rect.top}px`;
windowEl.style.right = 'auto';
windowEl.style.bottom = 'auto';
windowEl.style.transform = 'none';
dragState = {
pointerId: event.pointerId,
windowEl,
offsetX: event.clientX - rect.left,
offsetY: event.clientY - rect.top,
};
document.body.classList.add('is-dragging-window');
handle.setPointerCapture(event.pointerId);
event.preventDefault();
});
});
window.addEventListener('pointermove', (event) => {
if (!dragState || dragState.pointerId !== event.pointerId) {
return;
}
const windowEl = dragState.windowEl;
const maxLeft = Math.max(0, window.innerWidth - windowEl.offsetWidth);
const maxTop = Math.max(0, window.innerHeight - windowEl.offsetHeight);
const nextLeft = clamp(event.clientX - dragState.offsetX, 0, maxLeft);
const nextTop = clamp(event.clientY - dragState.offsetY, 0, maxTop);
windowEl.style.left = `${nextLeft}px`;
windowEl.style.top = `${nextTop}px`;
});
function finishDrag(event) {
if (!dragState || dragState.pointerId !== event.pointerId) {
return;
}
persistWindowLayout(dragState.windowEl);
dragState = null;
document.body.classList.remove('is-dragging-window');
}
window.addEventListener('pointerup', finishDrag);
window.addEventListener('pointercancel', finishDrag);
}
function initResizableWindows() {
if (typeof ResizeObserver !== 'function') {
return;
}
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const windowEl = entry.target;
if (windowEl.classList.contains('hidden')) {
return;
}
if (isMobileViewport()) {
return;
}
persistWindowLayout(windowEl);
});
});
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
observer.observe(windowEl);
});
}
function initWindowLayouts() {
windowLayouts = loadWindowLayouts();
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
ensureWindowLayout(windowEl);
});
window.addEventListener('resize', () => {
if (isMobileViewport()) {
return;
}
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
const id = windowEl.id;
if (!id) {
return;
}
const normalized = normalizeLayout(windowLayouts[id], DEFAULT_WINDOW_LAYOUTS[id]);
const clamped = clampLayoutToViewport(normalized);
applyLayout(windowEl, clamped);
windowLayouts[id] = clamped;
});
saveWindowLayouts();
});
}
function initUIControls() {
if (window.__uiControlsInitialized) {
syncControls();
return;
}
window.__uiControlsInitialized = true;
const savedTheme = localStorage.getItem(THEME_KEY) || DEFAULT_THEME;
const savedMode = localStorage.getItem(MODE_KEY) || 'light';
applyTheme(savedTheme);
applyMode(savedMode);
initWindowLayouts();
syncControls();
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
button.addEventListener('click', () => {
const value = button.dataset.theme || DEFAULT_THEME;
localStorage.setItem(THEME_KEY, value);
applyTheme(value);
syncControls();
});
});
document.querySelectorAll('[data-role="mode-toggle-action"]').forEach((button) => {
button.addEventListener('click', () => {
const next = getCurrentMode() === 'dark' ? 'light' : 'dark';
localStorage.setItem(MODE_KEY, next);
applyMode(next);
syncControls();
});
});
document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
button.addEventListener('click', () => {
const target = button.dataset.target;
if (!target) {
return;
}
if (isWindowOpen(target)) {
closeToolWindow(target);
return;
}
openToolWindow(target);
});
});
document.querySelectorAll('[data-role="close-window"]').forEach((button) => {
button.addEventListener('click', () => {
const target = button.dataset.target;
if (!target) {
return;
}
closeToolWindow(target);
});
});
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
windowEl.addEventListener('pointerdown', () => {
if (windowEl.classList.contains('hidden')) {
return;
}
bringWindowToFront(windowEl);
});
});
window.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') {
return;
}
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
if (windowEl.classList.contains('hidden')) {
return;
}
windowEl.classList.add('hidden');
});
syncTaskButtons();
});
initDraggableWindows();
initResizableWindows();
}
window.initUIControls = initUIControls;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUIControls, { once: true });
} else {
initUIControls();
}
})();