/* The WindowManager is responsible for creation, destruction, modification and tracking of game UI windows. */ class WindowManager { constructor(container) { this.windows = {} this.container = container } updateWindow(id, content, title = '') { if (id in this.windows) { let w = this.windows[id] w.querySelector('header .title').innerHTML = title w.querySelector('.body').innerHTML = content this.bringToFront(w) return } let w = this.createWindow(id, content, title) this.bringToFront(w) } createWindow(id, content, title = '') { // create window let w = document.createElement('div') w.id = `window-${id}` w.classList.add('window') // create header let h = document.createElement('header') w.appendChild(h) // create header title let ht = document.createElement('span') ht.classList.add('title') ht.innerHTML = title h.appendChild(ht) // create close button ht.insertAdjacentHTML('afterend', '') h.querySelector('a.close').addEventListener('click', () => { this.windows[id].remove() delete this.windows[id] }) // create body let b = document.createElement('div') b.classList.add('body') b.innerHTML = content w.appendChild(b) // track window and add it to the container this.makeWindowDraggable(w, this.container) this.windows[id] = w this.container.appendChild(w) return w } makeWindowDraggable(w, c) { const header = w.querySelector('header'); if (!header) return; let isDragging = false; let currentX; let currentY; let initialX; let initialY; header.addEventListener('mousedown', startDragging); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDragging); w.addEventListener('mousedown', () => this.bringToFront(w)); function startDragging(e) { isDragging = true; initialX = e.clientX - w.offsetLeft; initialY = e.clientY - w.offsetTop; } function drag(e) { if (!isDragging) return; e.preventDefault(); // Calculate new position currentX = e.clientX - initialX; currentY = e.clientY - initialY; // Get viewport dimensions const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Get window dimensions const windowRect = w.getBoundingClientRect(); // Constrain to viewport bounds // Left edge currentX = Math.max(0, currentX); // Right edge currentX = Math.min(viewportWidth - windowRect.width, currentX); // Top edge currentY = Math.max(0, currentY); // Bottom edge currentY = Math.min(viewportHeight - windowRect.height, currentY); // Apply the constrained position w.style.left = currentX + 'px'; w.style.top = currentY + 'px'; } function stopDragging() { isDragging = false; } // Handle window resize window.addEventListener('resize', () => { // Get current position and dimensions const rect = w.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Adjust position if window is outside viewport after resize let newX = parseInt(w.style.left); let newY = parseInt(w.style.top); // Constrain to new viewport bounds newX = Math.min(Math.max(0, newX), viewportWidth - rect.width); newY = Math.min(Math.max(0, newY), viewportHeight - rect.height); w.style.left = newX + 'px'; w.style.top = newY + 'px'; }); } normalizeZIndices() { const array = Array.from(this.windows); array.sort((a, b) => (parseInt(a.style.zIndex) || 0) - (parseInt(b.style.zIndex) || 0)); // Reassign z-indices starting from 1 array.forEach((win, index) => { win.style.zIndex = index + 1; }); return array.length + 1; // Return next available z-index } bringToFront(windowElement) { const currentMax = Math.max(...Object.values(this.windows) .map(w => parseInt(w.style.zIndex) || 0)); if (parseInt(windowElement.style.zIndex) >= currentMax) { return } // If z-index is getting too high, normalize all z-indices if (currentMax > 10000) { const newZ = this.normalizeZIndices(); windowElement.style.zIndex = newZ; } else { windowElement.style.zIndex = currentMax + 1; } } }