453 lines
18 KiB
JavaScript
453 lines
18 KiB
JavaScript
|
|
/**
|
|||
|
|
* pc-live | Desktop – boot, icon grid, clock, message router
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
/* ── NUI fetch helper ───────────────────────────────────── */
|
|||
|
|
async function fetchNui(event, data = {}) {
|
|||
|
|
try {
|
|||
|
|
const resp = await fetch(`https://pc-live/${event}`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify(data),
|
|||
|
|
});
|
|||
|
|
return resp.ok ? await resp.json() : null;
|
|||
|
|
} catch {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── State ──────────────────────────────────────────────── */
|
|||
|
|
const Desktop = (() => {
|
|||
|
|
let _session = null;
|
|||
|
|
let _clockTimer = null;
|
|||
|
|
let _installedApps = [];
|
|||
|
|
|
|||
|
|
const APP_META = {
|
|||
|
|
'pc.browser': { name: 'Browser', icon: '🌐', open: () => BrowserApp.open() },
|
|||
|
|
'pc.mail': { name: 'Mail', icon: '✉', open: () => MailApp.open() },
|
|||
|
|
'pc.addressbook': { name: 'Adressbuch', icon: '👤', open: () => AddressBookApp.open() },
|
|||
|
|
'pc.store': { name: 'Software Store', icon: '🛒', open: () => StoreApp.open() },
|
|||
|
|
'pc.parkuhr': { name: 'Parkuhr Dashboard', icon: '🅿', open: () => ParkuhrApp.open() },
|
|||
|
|
'pc.ic_verwaltung': { name: 'IC-Verwaltung', icon: '🏛', open: () => ICVerwaltungApp.open() },
|
|||
|
|
'pc.gesetzbuch': { name: 'Gesetzbuch', icon: '📖', open: () => GesetzbuchApp.open() },
|
|||
|
|
'pbs.dashboard': { name: 'PBS Dashboard', icon: '📞', open: () => PBSDashboardApp.open() },
|
|||
|
|
'pc.webhosting': { name: 'Webhosting', icon: '🌍', open: () => WebhostingApp.open() },
|
|||
|
|
'pc.bleeter': { name: 'Bleeter', icon: '🐦', open: () => BleeterApp.open() },
|
|||
|
|
'pc.bleeteradmin': { name: 'Bleeter-Verwaltung', icon: '🛠', open: () => BleeterAdminApp.open() },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/* ── Boot sequence ────────────────────────────────────── */
|
|||
|
|
function boot(session) {
|
|||
|
|
_session = session;
|
|||
|
|
MailApp.initMailboxes(session.sharedMailboxes || [], session.personalAddress || '',
|
|||
|
|
session.personalSignature || '');
|
|||
|
|
// Freigeschaltete Postfächer nachladen, damit die ✕-Knöpfe stimmen.
|
|||
|
|
MailApp.refreshMailboxes();
|
|||
|
|
|
|||
|
|
// Apply accent color
|
|||
|
|
if (session.accentColor) {
|
|||
|
|
document.documentElement.style.setProperty('--accent', session.accentColor);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const app = document.getElementById('app');
|
|||
|
|
const bootEl = document.getElementById('boot-screen');
|
|||
|
|
const fill = document.getElementById('boot-bar-fill');
|
|||
|
|
const desktopEl = document.getElementById('desktop');
|
|||
|
|
|
|||
|
|
// Set labels in titlebar and boot screen
|
|||
|
|
const label = session.pcLabel || 'Terminal';
|
|||
|
|
const pcLabel = document.getElementById('pc-label');
|
|||
|
|
const pcBootLabel = document.getElementById('pc-boot-label');
|
|||
|
|
if (pcLabel) pcLabel.textContent = label;
|
|||
|
|
if (pcBootLabel) pcBootLabel.textContent = label;
|
|||
|
|
|
|||
|
|
app.classList.add('visible');
|
|||
|
|
|
|||
|
|
// Animate progress bar
|
|||
|
|
const duration = 2500;
|
|||
|
|
fill.style.transition = `width ${duration}ms linear`;
|
|||
|
|
requestAnimationFrame(() => { fill.style.width = '100%'; });
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
bootEl.classList.add('fade-out');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
bootEl.style.display = 'none';
|
|||
|
|
desktopEl.classList.add('visible');
|
|||
|
|
startClock();
|
|||
|
|
renderIcons(session.installedApps || []);
|
|||
|
|
// Startseite automatisch öffnen falls konfiguriert
|
|||
|
|
if (session.startPage) {
|
|||
|
|
BrowserApp.open(session.startPage);
|
|||
|
|
}
|
|||
|
|
// Auto-Open-App (z.B. mobiler Streifenwagen-Terminal → IC-Verwaltung/Personalakte)
|
|||
|
|
if (session.autoOpenApp) {
|
|||
|
|
openApp(session.autoOpenApp);
|
|||
|
|
}
|
|||
|
|
}, 600);
|
|||
|
|
}, duration);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Clock ────────────────────────────────────────────── */
|
|||
|
|
function startClock() {
|
|||
|
|
const el = document.getElementById('clock');
|
|||
|
|
const tick = () => {
|
|||
|
|
if (!el) return;
|
|||
|
|
const now = new Date();
|
|||
|
|
el.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|||
|
|
};
|
|||
|
|
tick();
|
|||
|
|
_clockTimer = setInterval(tick, 1000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function stopClock() {
|
|||
|
|
clearInterval(_clockTimer);
|
|||
|
|
_clockTimer = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Desktop Icons ────────────────────────────────────── */
|
|||
|
|
function renderIcons(apps) {
|
|||
|
|
_installedApps = apps || [];
|
|||
|
|
const grid = document.getElementById('icon-grid');
|
|||
|
|
if (!grid) return;
|
|||
|
|
grid.innerHTML = '';
|
|||
|
|
|
|||
|
|
_installedApps.forEach(app => {
|
|||
|
|
const meta = APP_META[app.app_id] || {};
|
|||
|
|
const icon = meta.icon || app.icon || '📦';
|
|||
|
|
const name = meta.name || app.name || app.app_id;
|
|||
|
|
|
|||
|
|
const div = document.createElement('div');
|
|||
|
|
div.className = 'desktop-icon';
|
|||
|
|
div.innerHTML = `
|
|||
|
|
<div class="icon-glyph">${icon}</div>
|
|||
|
|
<div class="icon-label">${name}</div>`;
|
|||
|
|
div.addEventListener('dblclick', () => openApp(app.app_id));
|
|||
|
|
grid.appendChild(div);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openApp(appId) {
|
|||
|
|
const meta = APP_META[appId];
|
|||
|
|
if (meta && meta.open) {
|
|||
|
|
meta.open();
|
|||
|
|
} else {
|
|||
|
|
showNotification('⚠ App not yet available');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Close ────────────────────────────────────────────── */
|
|||
|
|
function close() {
|
|||
|
|
WindowManager.closeAll();
|
|||
|
|
stopClock();
|
|||
|
|
|
|||
|
|
const app = document.getElementById('app');
|
|||
|
|
const bootEl = document.getElementById('boot-screen');
|
|||
|
|
const desktopEl = document.getElementById('desktop');
|
|||
|
|
const fill = document.getElementById('boot-bar-fill');
|
|||
|
|
const grid = document.getElementById('icon-grid');
|
|||
|
|
const taskbar = document.getElementById('taskbar-apps');
|
|||
|
|
const container = document.getElementById('pc-container');
|
|||
|
|
const minBtn = document.getElementById('pc-minimize-btn');
|
|||
|
|
|
|||
|
|
desktopEl.classList.remove('visible');
|
|||
|
|
bootEl.style.display = 'flex';
|
|||
|
|
bootEl.classList.remove('fade-out');
|
|||
|
|
fill.style.transition = '';
|
|||
|
|
fill.style.width = '0%';
|
|||
|
|
if (grid) grid.innerHTML = '';
|
|||
|
|
if (taskbar) taskbar.innerHTML = '';
|
|||
|
|
|
|||
|
|
// Reset PC container position and minimized state
|
|||
|
|
if (container) {
|
|||
|
|
container.classList.remove('minimized');
|
|||
|
|
container.style.left = '50%';
|
|||
|
|
container.style.top = '50%';
|
|||
|
|
container.style.transform = 'translate(-50%, -50%)';
|
|||
|
|
}
|
|||
|
|
if (minBtn) { minBtn.textContent = '─'; minBtn.title = 'Minimize'; }
|
|||
|
|
|
|||
|
|
app.classList.remove('visible');
|
|||
|
|
app.classList.remove('interact-mode');
|
|||
|
|
_session = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Message handler ───────────────────────────────────── */
|
|||
|
|
window.addEventListener('message', (e) => {
|
|||
|
|
const { action, data } = e.data || {};
|
|||
|
|
switch (action) {
|
|||
|
|
case 'boot':
|
|||
|
|
boot(data);
|
|||
|
|
break;
|
|||
|
|
case 'close':
|
|||
|
|
close();
|
|||
|
|
break;
|
|||
|
|
case 'exitInteract':
|
|||
|
|
exitInteractMode();
|
|||
|
|
break;
|
|||
|
|
case 'setInstalledApps':
|
|||
|
|
renderIcons(data);
|
|||
|
|
break;
|
|||
|
|
case 'setAccentColor':
|
|||
|
|
document.documentElement.style.setProperty('--accent', data.color);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
// Mail
|
|||
|
|
case 'setInbox': MailApp.onInbox(data); break;
|
|||
|
|
case 'setMailContent': MailApp.onMailContent(data); break;
|
|||
|
|
case 'mailSent': MailApp.onMailSent(); break;
|
|||
|
|
case 'mailDeleted': MailApp.onMailDeleted(data.id); break;
|
|||
|
|
case 'newMailNotify':
|
|||
|
|
// Show unread badge on desktop icon (quick visual cue)
|
|||
|
|
showNotification('📧 New mail received');
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
// Store
|
|||
|
|
case 'setStoreList': StoreApp.onStoreData(data); break;
|
|||
|
|
|
|||
|
|
// Mail extended
|
|||
|
|
case 'setSent': MailApp.onSetSent(data); break;
|
|||
|
|
case 'folderCreated': MailApp.onFolderCreated(data); break;
|
|||
|
|
case 'folderDeleted': MailApp.onFolderDeleted(data.id, data.address); break;
|
|||
|
|
case 'foldersData': MailApp.onFoldersData(data.address, data.folders); break;
|
|||
|
|
case 'folderMailsData': MailApp.onFolderMailsData(data.folderId, data.rows); break;
|
|||
|
|
case 'mailMoved': MailApp.onMailMoved(data.id, data.folderId); break;
|
|||
|
|
|
|||
|
|
// Geteilte Postfächer
|
|||
|
|
case 'sharedMailboxes': MailApp.onSharedMailboxes(data.mailboxes, data.personalAddress,
|
|||
|
|
data.personalSignature); break;
|
|||
|
|
case 'signatureSaved': MailApp.onSignatureSaved(data); break;
|
|||
|
|
|
|||
|
|
// Mail-Einrichtung
|
|||
|
|
case 'mailAddressStatus': MailApp.onMailAddressStatus(data); break;
|
|||
|
|
case 'mailCreateResult': MailApp.onMailCreateResult(data); break;
|
|||
|
|
|
|||
|
|
// Calendar
|
|||
|
|
case 'calendarData': MailApp.onCalendarData(data); break;
|
|||
|
|
case 'calendarEventAdded': MailApp.onCalendarEventAdded(data); break;
|
|||
|
|
case 'calendarEventUpdated': MailApp.onCalendarEventUpdated(data); break;
|
|||
|
|
case 'calendarEventDeleted': MailApp.onCalendarEventDeleted(data.id); break;
|
|||
|
|
|
|||
|
|
// Contacts
|
|||
|
|
case 'contactsData': AddressBookApp.onContactsData(data); break;
|
|||
|
|
case 'contactAdded': AddressBookApp.onContactAdded(data); break;
|
|||
|
|
case 'contactUpdated': AddressBookApp.onContactUpdated(data); break;
|
|||
|
|
case 'contactDeleted': AddressBookApp.onContactDeleted(data.id); break;
|
|||
|
|
|
|||
|
|
// IC-Verwaltung relay: forward Lua SendNUIMessage into the iframe
|
|||
|
|
case 'ic_verwaltung_relay': ICVerwaltungApp.onRelay(data); break;
|
|||
|
|
|
|||
|
|
// Webhosting (ic-web): Antwort auf eine Anfrage mit Anfrage-Id
|
|||
|
|
case 'icweb_response': ICWebNet.onResponse(data); break;
|
|||
|
|
// Bleeter schickt alles über einen Kanal: Zustand und Verwaltungsantworten.
|
|||
|
|
case 'bleeter_data': BleeterApp.onData(data); break;
|
|||
|
|
case 'bleeter_notify': BleeterApp.onNotify(data); break;
|
|||
|
|
case 'bleeter_uploaded': BleeterApp.onUploaded(data); break;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/* ── Keyboard: Escape closes PC ────────────────────────── */
|
|||
|
|
document.addEventListener('keydown', (e) => {
|
|||
|
|
if (e.key === 'Escape' && document.getElementById('desktop')?.classList.contains('visible')) {
|
|||
|
|
doClosePC();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/* ── Notification toast ─────────────────────────────────── */
|
|||
|
|
function showNotification(msg) {
|
|||
|
|
const el = document.createElement('div');
|
|||
|
|
el.style.cssText = `
|
|||
|
|
position:fixed;top:50px;right:16px;z-index:9000;
|
|||
|
|
background:var(--bg-window);border:1px solid var(--border);
|
|||
|
|
border-left:3px solid var(--accent);
|
|||
|
|
border-radius:var(--radius);padding:10px 14px;
|
|||
|
|
font-size:.78rem;color:var(--text);
|
|||
|
|
box-shadow:var(--shadow);
|
|||
|
|
animation:fadeIn .2s ease;
|
|||
|
|
`;
|
|||
|
|
el.textContent = msg;
|
|||
|
|
document.body.appendChild(el);
|
|||
|
|
setTimeout(() => el.remove(), 4000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { boot, close, openApp, showNotification,
|
|||
|
|
get session() { return _session; } };
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ── Close button ───────────────────────────────────────── */
|
|||
|
|
function doClosePC() {
|
|||
|
|
fetchNui('closePC', {});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── PC Container: Minimize / Maximize ─────────────────── */
|
|||
|
|
function toggleMinimizePC() {
|
|||
|
|
const container = document.getElementById('pc-container');
|
|||
|
|
const btn = document.getElementById('pc-minimize-btn');
|
|||
|
|
if (!container) return;
|
|||
|
|
const isMin = container.classList.contains('minimized');
|
|||
|
|
container.classList.toggle('minimized', !isMin);
|
|||
|
|
if (btn) {
|
|||
|
|
btn.textContent = isMin ? '─' : '□';
|
|||
|
|
btn.title = isMin ? 'Minimize' : 'Restore';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Interaktionsmodus: PC transparent + Fokus zurück ins Spiel ── */
|
|||
|
|
function toggleInteractMode() {
|
|||
|
|
const app = document.getElementById('app');
|
|||
|
|
const btn = document.getElementById('pc-interact-btn');
|
|||
|
|
if (!app) return;
|
|||
|
|
const on = !app.classList.contains('interact-mode');
|
|||
|
|
app.classList.toggle('interact-mode', on);
|
|||
|
|
if (btn) btn.classList.toggle('active', on);
|
|||
|
|
// interact AN -> NUI-Fokus AUS (man interagiert mit dem Spiel); AUS -> Fokus zurück auf den PC
|
|||
|
|
fetchNui('setFocus', { focus: !on });
|
|||
|
|
}
|
|||
|
|
function exitInteractMode() {
|
|||
|
|
const app = document.getElementById('app');
|
|||
|
|
const btn = document.getElementById('pc-interact-btn');
|
|||
|
|
if (app) app.classList.remove('interact-mode');
|
|||
|
|
if (btn) btn.classList.remove('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── PC Size (viewport %) ────────────────────────────────── */
|
|||
|
|
const PCScale = (() => {
|
|||
|
|
const STEP = 5;
|
|||
|
|
const MIN = 40;
|
|||
|
|
const MAX = 98;
|
|||
|
|
const KEY = 'pc_live_vp';
|
|||
|
|
|
|||
|
|
let _vp = parseInt(localStorage.getItem(KEY) || '88', 10);
|
|||
|
|
|
|||
|
|
function apply() {
|
|||
|
|
_vp = Math.max(MIN, Math.min(MAX, _vp));
|
|||
|
|
document.documentElement.style.setProperty('--pc-vp', _vp);
|
|||
|
|
localStorage.setItem(KEY, _vp);
|
|||
|
|
const c = document.getElementById('pc-container');
|
|||
|
|
if (c) {
|
|||
|
|
c.style.left = '50%';
|
|||
|
|
c.style.top = '50%';
|
|||
|
|
c.style.transform = 'translate(-50%, -50%)';
|
|||
|
|
}
|
|||
|
|
const disp = document.getElementById('pc-vp-display');
|
|||
|
|
const slider = document.getElementById('pc-vp-slider');
|
|||
|
|
if (disp) disp.textContent = _vp + '%';
|
|||
|
|
if (slider) slider.value = _vp;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function step(dir) { _vp += dir * STEP; apply(); }
|
|||
|
|
function setVP(v) { _vp = parseInt(v, 10); apply(); }
|
|||
|
|
|
|||
|
|
document.addEventListener('wheel', (e) => {
|
|||
|
|
if (!e.ctrlKey) return;
|
|||
|
|
e.preventDefault();
|
|||
|
|
step(e.deltaY < 0 ? 1 : -1);
|
|||
|
|
}, { passive: false });
|
|||
|
|
|
|||
|
|
apply();
|
|||
|
|
return { step, setVP };
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ── PC Font Size ─────────────────────────────────────────── */
|
|||
|
|
const PCFontSize = (() => {
|
|||
|
|
const STEP = 1;
|
|||
|
|
const MIN = 11;
|
|||
|
|
const MAX = 20;
|
|||
|
|
const KEY = 'pc_live_fs';
|
|||
|
|
|
|||
|
|
let _fs = parseInt(localStorage.getItem(KEY) || '13', 10);
|
|||
|
|
|
|||
|
|
function apply() {
|
|||
|
|
_fs = Math.max(MIN, Math.min(MAX, _fs));
|
|||
|
|
document.documentElement.style.setProperty('--pc-fs', _fs);
|
|||
|
|
localStorage.setItem(KEY, _fs);
|
|||
|
|
const disp = document.getElementById('pc-fs-display');
|
|||
|
|
const slider = document.getElementById('pc-fs-slider');
|
|||
|
|
if (disp) disp.textContent = _fs + 'px';
|
|||
|
|
if (slider) slider.value = _fs;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function step(dir) { _fs += dir * STEP; apply(); }
|
|||
|
|
function set(v) { _fs = parseInt(v, 10); apply(); }
|
|||
|
|
|
|||
|
|
apply();
|
|||
|
|
return { step, set };
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ── Settings Panel ───────────────────────────────────────── */
|
|||
|
|
const PCSettings = (() => {
|
|||
|
|
let _open = false;
|
|||
|
|
|
|||
|
|
function toggle() {
|
|||
|
|
_open = !_open;
|
|||
|
|
const panel = document.getElementById('pc-settings-panel');
|
|||
|
|
const btn = document.getElementById('pc-settings-btn');
|
|||
|
|
if (panel) panel.classList.toggle('open', _open);
|
|||
|
|
if (btn) btn.classList.toggle('active', _open);
|
|||
|
|
// Sync slider values when opening
|
|||
|
|
if (_open) {
|
|||
|
|
const fsSlider = document.getElementById('pc-fs-slider');
|
|||
|
|
const vpSlider = document.getElementById('pc-vp-slider');
|
|||
|
|
if (fsSlider) fsSlider.value = parseInt(localStorage.getItem('pc_live_fs') || '13', 10);
|
|||
|
|
if (vpSlider) vpSlider.value = parseInt(localStorage.getItem('pc_live_vp') || '88', 10);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function close() {
|
|||
|
|
_open = false;
|
|||
|
|
const panel = document.getElementById('pc-settings-panel');
|
|||
|
|
const btn = document.getElementById('pc-settings-btn');
|
|||
|
|
if (panel) panel.classList.remove('open');
|
|||
|
|
if (btn) btn.classList.remove('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Schließen bei Klick außerhalb
|
|||
|
|
document.addEventListener('click', (e) => {
|
|||
|
|
if (!_open) return;
|
|||
|
|
const panel = document.getElementById('pc-settings-panel');
|
|||
|
|
const btn = document.getElementById('pc-settings-btn');
|
|||
|
|
if (panel && !panel.contains(e.target) && btn && !btn.contains(e.target)) {
|
|||
|
|
close();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return { toggle, close };
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ── PC Container: Draggable via Titelleiste ────────────── */
|
|||
|
|
(function initPCDrag() {
|
|||
|
|
const container = document.getElementById('pc-container');
|
|||
|
|
const titlebar = document.getElementById('pc-titlebar');
|
|||
|
|
if (!container || !titlebar) return;
|
|||
|
|
|
|||
|
|
let startX, startY, startL, startT;
|
|||
|
|
|
|||
|
|
titlebar.addEventListener('mousedown', (e) => {
|
|||
|
|
// Ignore clicks on buttons
|
|||
|
|
if (e.target.closest('#pc-minimize-btn, #close-pc-btn, #pc-settings-btn, #pc-settings-panel')) return;
|
|||
|
|
|
|||
|
|
// Convert centered transform to explicit pixel position
|
|||
|
|
const rect = container.getBoundingClientRect();
|
|||
|
|
container.style.left = rect.left + 'px';
|
|||
|
|
container.style.top = rect.top + 'px';
|
|||
|
|
container.style.transform = 'none';
|
|||
|
|
|
|||
|
|
startX = e.clientX; startY = e.clientY;
|
|||
|
|
startL = rect.left; startT = rect.top;
|
|||
|
|
|
|||
|
|
const onMove = (e2) => {
|
|||
|
|
const newL = startL + e2.clientX - startX;
|
|||
|
|
const newT = startT + e2.clientY - startY;
|
|||
|
|
const maxL = window.innerWidth - container.offsetWidth;
|
|||
|
|
const maxT = window.innerHeight - container.offsetHeight;
|
|||
|
|
container.style.left = Math.max(0, Math.min(maxL, newL)) + 'px';
|
|||
|
|
container.style.top = Math.max(0, Math.min(maxT, newT)) + 'px';
|
|||
|
|
};
|
|||
|
|
const onUp = () => {
|
|||
|
|
document.removeEventListener('mousemove', onMove);
|
|||
|
|
document.removeEventListener('mouseup', onUp);
|
|||
|
|
};
|
|||
|
|
document.addEventListener('mousemove', onMove);
|
|||
|
|
document.addEventListener('mouseup', onUp);
|
|||
|
|
});
|
|||
|
|
})();
|