Spaces:
Sleeping
Sleeping
Update static/index.html
Browse files- static/index.html +152 -103
static/index.html
CHANGED
|
@@ -206,6 +206,7 @@
|
|
| 206 |
const saveBadge = $('#saveState');
|
| 207 |
|
| 208 |
let hlsBg = null; // background HLS instance
|
|
|
|
| 209 |
|
| 210 |
const state = {
|
| 211 |
background: null, // {type:'image'|'video', src:'data/http(s) URL', fit:'cover'|'contain'}
|
|
@@ -325,6 +326,101 @@
|
|
| 325 |
}
|
| 326 |
}
|
| 327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
// ---------- Items ----------
|
| 329 |
$('#addItem').addEventListener('click', () => addOrPlace(false));
|
| 330 |
$('#addAndPlace').addEventListener('click', () => addOrPlace(true));
|
|
@@ -351,7 +447,14 @@
|
|
| 351 |
|
| 352 |
function removeItem(id){
|
| 353 |
const i = state.items.findIndex(x=>x.id===id);
|
| 354 |
-
if (i>=0){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
}
|
| 356 |
|
| 357 |
function startPlacing(id){
|
|
@@ -370,7 +473,16 @@
|
|
| 370 |
if (it){
|
| 371 |
it.x = Math.max(0, Math.min(100, xPct));
|
| 372 |
it.y = Math.max(0, Math.min(100, yPct));
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
}
|
| 375 |
stopPlacing();
|
| 376 |
});
|
|
@@ -379,8 +491,9 @@
|
|
| 379 |
function togglePopup(id){
|
| 380 |
const it = state.items.find(x=>x.id===id);
|
| 381 |
if (!it) return;
|
| 382 |
-
it.open
|
| 383 |
-
|
|
|
|
| 384 |
}
|
| 385 |
|
| 386 |
function adjustPopupPosition(el){
|
|
@@ -394,110 +507,34 @@
|
|
| 394 |
}
|
| 395 |
|
| 396 |
function renderItems(){
|
| 397 |
-
|
| 398 |
overlay.innerHTML = '';
|
| 399 |
-
|
| 400 |
for (const it of state.items){
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
pin.className = 'pin';
|
| 404 |
-
pin.setAttribute('role', 'button');
|
| 405 |
-
pin.setAttribute('tabindex', '0');
|
| 406 |
-
pin.style.left = it.x + '%';
|
| 407 |
-
pin.style.top = it.y + '%';
|
| 408 |
-
pin.title = it.title || it.type;
|
| 409 |
-
pin.addEventListener('click', (e) => { e.stopPropagation(); togglePopup(it.id); });
|
| 410 |
-
pin.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); togglePopup(it.id); } });
|
| 411 |
-
overlay.appendChild(pin);
|
| 412 |
-
|
| 413 |
-
// POPUP
|
| 414 |
-
if (it.open){
|
| 415 |
-
const pop = document.createElement('div');
|
| 416 |
-
pop.className = 'popup';
|
| 417 |
-
pop.style.left = it.x + '%';
|
| 418 |
-
pop.style.top = it.y + '%';
|
| 419 |
-
pop.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
|
| 420 |
-
|
| 421 |
-
const wrap = document.createElement('div');
|
| 422 |
-
wrap.className = 'media';
|
| 423 |
-
|
| 424 |
-
if (it.type === 'video') {
|
| 425 |
-
wrap.setAttribute('data-kind','video'); // reserve space
|
| 426 |
-
|
| 427 |
-
const v = document.createElement('video');
|
| 428 |
-
v.autoplay = true; v.loop = true; v.muted = true; v.playsInline = true;
|
| 429 |
-
v.crossOrigin = 'anonymous';
|
| 430 |
-
v.style.width = '100%'; v.style.height = 'auto';
|
| 431 |
-
|
| 432 |
-
let hlsInst = null;
|
| 433 |
-
if (window.Hls && window.Hls.isSupported() && /\.m3u8($|\?)/i.test(it.src)) {
|
| 434 |
-
hlsInst = new Hls();
|
| 435 |
-
hlsInst.loadSource(it.src);
|
| 436 |
-
hlsInst.attachMedia(v);
|
| 437 |
-
hlsInst.on(Hls.Events.MANIFEST_PARSED, () => { v.play().catch(()=>{}); });
|
| 438 |
-
} else {
|
| 439 |
-
const srcEl = document.createElement('source');
|
| 440 |
-
srcEl.src = it.src;
|
| 441 |
-
const mime = guessVideoMime(it.src); if (mime) srcEl.type = mime;
|
| 442 |
-
v.appendChild(srcEl);
|
| 443 |
-
v.load();
|
| 444 |
-
v.play().catch(()=>{});
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
v.addEventListener('loadedmetadata', () => {
|
| 448 |
-
if (v.videoWidth && v.videoHeight) wrap.style.aspectRatio = (v.videoWidth / v.videoHeight).toString();
|
| 449 |
-
});
|
| 450 |
-
|
| 451 |
-
// CLOSE THIS POPUP when user clicks the small video
|
| 452 |
-
const closeSelf = (e) => {
|
| 453 |
-
e.stopPropagation();
|
| 454 |
-
it.open = false;
|
| 455 |
-
try { v.pause(); } catch {}
|
| 456 |
-
if (hlsInst) { try { hlsInst.destroy(); } catch {} }
|
| 457 |
-
renderItems();
|
| 458 |
-
};
|
| 459 |
-
// Click anywhere on the video area to close
|
| 460 |
-
wrap.addEventListener('click', closeSelf);
|
| 461 |
-
v.addEventListener('click', closeSelf);
|
| 462 |
-
|
| 463 |
-
v.addEventListener('error', () => {
|
| 464 |
-
const note = document.createElement('div');
|
| 465 |
-
note.style.padding = '10px 12px';
|
| 466 |
-
note.style.color = '#ffd2d2';
|
| 467 |
-
note.textContent = 'Video failed to load. Use MP4 (H.264/AAC), WEBM, or HLS (.m3u8).';
|
| 468 |
-
wrap.innerHTML = ''; wrap.appendChild(note);
|
| 469 |
-
}, {once:true});
|
| 470 |
-
|
| 471 |
-
wrap.appendChild(v);
|
| 472 |
-
} else {
|
| 473 |
-
const img = document.createElement('img');
|
| 474 |
-
img.src = it.src; img.alt = it.title || '';
|
| 475 |
-
// Click image to close that popup
|
| 476 |
-
wrap.addEventListener('click', (e) => { e.stopPropagation(); it.open = false; renderItems(); });
|
| 477 |
-
wrap.appendChild(img);
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
pop.appendChild(wrap);
|
| 481 |
-
overlay.appendChild(pop);
|
| 482 |
-
requestAnimationFrame(() => adjustPopupPosition(pop));
|
| 483 |
-
}
|
| 484 |
}
|
|
|
|
|
|
|
| 485 |
|
| 486 |
-
|
|
|
|
| 487 |
const list = $('#items'); list.innerHTML='';
|
| 488 |
for (const it of state.items){
|
| 489 |
const row = document.createElement('div'); row.className='item';
|
| 490 |
const kind = document.createElement('div'); kind.className='badge'; kind.textContent = it.type.toUpperCase();
|
| 491 |
const info = document.createElement('div');
|
|
|
|
|
|
|
| 492 |
info.innerHTML = `<div class="name">${escapeHtml(it.title||'')}</div>
|
| 493 |
-
<div class="tiny"
|
| 494 |
const actions = document.createElement('div'); actions.className='actions';
|
|
|
|
| 495 |
|
| 496 |
const btnPlace = document.createElement('button'); btnPlace.textContent='Place';
|
| 497 |
btnPlace.addEventListener('click', ()=> startPlacing(it.id));
|
| 498 |
|
| 499 |
const btnPreview = document.createElement('button'); btnPreview.textContent = it.open ? 'Hide' : 'Preview';
|
| 500 |
-
btnPreview.addEventListener('click', ()=> togglePopup(it.id));
|
| 501 |
|
| 502 |
const widthIn = document.createElement('input');
|
| 503 |
widthIn.type = 'number'; widthIn.min = '10'; widthIn.max = '2000';
|
|
@@ -509,11 +546,21 @@ if (it.type === 'video') {
|
|
| 509 |
|
| 510 |
widthIn.addEventListener('change', () => {
|
| 511 |
it.width = Number(widthIn.value) || (it.widthUnit === 'px' ? 320 : 25);
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
});
|
| 514 |
unitSel.addEventListener('change', () => {
|
| 515 |
it.widthUnit = unitSel.value;
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
});
|
| 518 |
|
| 519 |
const btnDel = document.createElement('button'); btnDel.textContent='Delete'; btnDel.className='btn-danger';
|
|
@@ -563,8 +610,13 @@ if (it.type === 'video') {
|
|
| 563 |
// Clear / Download
|
| 564 |
$('#clearAll').addEventListener('click', async () => {
|
| 565 |
if (!confirm('Clear background and all items?')) return;
|
|
|
|
|
|
|
| 566 |
state.background = null; state.items = [];
|
| 567 |
-
|
|
|
|
|
|
|
|
|
|
| 568 |
});
|
| 569 |
|
| 570 |
$('#downloadJson').addEventListener('click', () => {
|
|
@@ -578,20 +630,17 @@ if (it.type === 'video') {
|
|
| 578 |
URL.revokeObjectURL(url);
|
| 579 |
});
|
| 580 |
|
| 581 |
-
//
|
| 582 |
window.addEventListener('resize', ()=>{
|
| 583 |
$$('.popup').forEach(el => {
|
| 584 |
el.style.transform='translate(-50%, -50%)';
|
| 585 |
requestAnimationFrame(()=>adjustPopupPosition(el));
|
| 586 |
});
|
| 587 |
});
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
// intentionally do nothing so outside clicks don't close anything
|
| 593 |
-
});
|
| 594 |
-
|
| 595 |
|
| 596 |
loadFromServer();
|
| 597 |
})();
|
|
|
|
| 206 |
const saveBadge = $('#saveState');
|
| 207 |
|
| 208 |
let hlsBg = null; // background HLS instance
|
| 209 |
+
const hlsMap = new Map(); // per-popup HLS instances
|
| 210 |
|
| 211 |
const state = {
|
| 212 |
background: null, // {type:'image'|'video', src:'data/http(s) URL', fit:'cover'|'contain'}
|
|
|
|
| 326 |
}
|
| 327 |
}
|
| 328 |
|
| 329 |
+
// ---------- Incremental DOM helpers for items ----------
|
| 330 |
+
const getPinEl = (id) => overlay.querySelector(`.pin[data-id="${id}"]`);
|
| 331 |
+
const getPopupEl = (id) => overlay.querySelector(`.popup[data-id="${id}"]`);
|
| 332 |
+
|
| 333 |
+
function createPin(it){
|
| 334 |
+
const pin = document.createElement('div');
|
| 335 |
+
pin.className = 'pin';
|
| 336 |
+
pin.dataset.id = it.id;
|
| 337 |
+
pin.setAttribute('role', 'button');
|
| 338 |
+
pin.setAttribute('tabindex', '0');
|
| 339 |
+
pin.style.left = it.x + '%';
|
| 340 |
+
pin.style.top = it.y + '%';
|
| 341 |
+
pin.title = it.title || it.type;
|
| 342 |
+
pin.addEventListener('click', (e) => { e.stopPropagation(); togglePopup(it.id); });
|
| 343 |
+
pin.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); togglePopup(it.id); } });
|
| 344 |
+
overlay.appendChild(pin);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
function createPopup(it){
|
| 348 |
+
if (getPopupEl(it.id)) return; // already open
|
| 349 |
+
const pop = document.createElement('div');
|
| 350 |
+
pop.className = 'popup';
|
| 351 |
+
pop.dataset.id = it.id;
|
| 352 |
+
pop.style.left = it.x + '%';
|
| 353 |
+
pop.style.top = it.y + '%';
|
| 354 |
+
pop.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
|
| 355 |
+
|
| 356 |
+
const wrap = document.createElement('div');
|
| 357 |
+
wrap.className = 'media';
|
| 358 |
+
|
| 359 |
+
if (it.type === 'video') {
|
| 360 |
+
wrap.setAttribute('data-kind','video'); // reserve space via CSS
|
| 361 |
+
const v = document.createElement('video');
|
| 362 |
+
v.autoplay = true; v.loop = true; v.muted = true; v.playsInline = true;
|
| 363 |
+
v.crossOrigin = 'anonymous';
|
| 364 |
+
v.style.width = '100%'; v.style.height = 'auto';
|
| 365 |
+
|
| 366 |
+
let hlsInst = null;
|
| 367 |
+
if (window.Hls && window.Hls.isSupported() && /\.m3u8($|\?)/i.test(it.src)) {
|
| 368 |
+
hlsInst = new Hls();
|
| 369 |
+
hlsInst.loadSource(it.src);
|
| 370 |
+
hlsInst.attachMedia(v);
|
| 371 |
+
hlsInst.on(Hls.Events.MANIFEST_PARSED, () => { v.play().catch(()=>{}); });
|
| 372 |
+
hlsMap.set(it.id, hlsInst);
|
| 373 |
+
} else {
|
| 374 |
+
const srcEl = document.createElement('source');
|
| 375 |
+
srcEl.src = it.src;
|
| 376 |
+
const mime = guessVideoMime(it.src); if (mime) srcEl.type = mime;
|
| 377 |
+
v.appendChild(srcEl);
|
| 378 |
+
v.load();
|
| 379 |
+
v.play().catch(()=>{});
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
v.addEventListener('loadedmetadata', () => {
|
| 383 |
+
if (v.videoWidth && v.videoHeight) wrap.style.aspectRatio = (v.videoWidth / v.videoHeight).toString();
|
| 384 |
+
});
|
| 385 |
+
|
| 386 |
+
// Close THIS popup on click (don’t touch others)
|
| 387 |
+
const closeSelf = (e) => { e.stopPropagation(); removePopup(it.id); it.open = false; };
|
| 388 |
+
wrap.addEventListener('click', closeSelf);
|
| 389 |
+
v.addEventListener('click', closeSelf);
|
| 390 |
+
|
| 391 |
+
v.addEventListener('error', () => {
|
| 392 |
+
const note = document.createElement('div');
|
| 393 |
+
note.style.padding = '10px 12px';
|
| 394 |
+
note.style.color = '#ffd2d2';
|
| 395 |
+
note.textContent = 'Video failed to load. Use MP4 (H.264/AAC), WEBM, or HLS (.m3u8).';
|
| 396 |
+
wrap.innerHTML = ''; wrap.appendChild(note);
|
| 397 |
+
}, {once:true});
|
| 398 |
+
|
| 399 |
+
wrap.appendChild(v);
|
| 400 |
+
} else {
|
| 401 |
+
const img = document.createElement('img');
|
| 402 |
+
img.src = it.src; img.alt = it.title || '';
|
| 403 |
+
// Close THIS popup on click
|
| 404 |
+
wrap.addEventListener('click', (e) => { e.stopPropagation(); removePopup(it.id); it.open = false; });
|
| 405 |
+
wrap.appendChild(img);
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
pop.appendChild(wrap);
|
| 409 |
+
overlay.appendChild(pop);
|
| 410 |
+
requestAnimationFrame(() => adjustPopupPosition(pop));
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
function removePopup(id){
|
| 414 |
+
const el = getPopupEl(id);
|
| 415 |
+
if (!el) return;
|
| 416 |
+
// stop video & destroy HLS if present
|
| 417 |
+
const v = el.querySelector('video');
|
| 418 |
+
if (v){ try{ v.pause(); }catch{} }
|
| 419 |
+
const hlsInst = hlsMap.get(id);
|
| 420 |
+
if (hlsInst){ try{ hlsInst.destroy(); }catch{} hlsMap.delete(id); }
|
| 421 |
+
el.remove();
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
// ---------- Items ----------
|
| 425 |
$('#addItem').addEventListener('click', () => addOrPlace(false));
|
| 426 |
$('#addAndPlace').addEventListener('click', () => addOrPlace(true));
|
|
|
|
| 447 |
|
| 448 |
function removeItem(id){
|
| 449 |
const i = state.items.findIndex(x=>x.id===id);
|
| 450 |
+
if (i>=0){
|
| 451 |
+
const it = state.items[i];
|
| 452 |
+
removePopup(id);
|
| 453 |
+
const pin = getPinEl(id); if (pin) pin.remove();
|
| 454 |
+
state.items.splice(i,1);
|
| 455 |
+
renderItemsList(); // refresh list & count only
|
| 456 |
+
markDirty();
|
| 457 |
+
}
|
| 458 |
}
|
| 459 |
|
| 460 |
function startPlacing(id){
|
|
|
|
| 473 |
if (it){
|
| 474 |
it.x = Math.max(0, Math.min(100, xPct));
|
| 475 |
it.y = Math.max(0, Math.min(100, yPct));
|
| 476 |
+
// update only that pin/popup
|
| 477 |
+
const pinEl = getPinEl(it.id);
|
| 478 |
+
if (pinEl){ pinEl.style.left = it.x + '%'; pinEl.style.top = it.y + '%'; }
|
| 479 |
+
const popEl = getPopupEl(it.id);
|
| 480 |
+
if (popEl){
|
| 481 |
+
popEl.style.left = it.x + '%';
|
| 482 |
+
popEl.style.top = it.y + '%';
|
| 483 |
+
requestAnimationFrame(()=>adjustPopupPosition(popEl));
|
| 484 |
+
}
|
| 485 |
+
markDirty();
|
| 486 |
}
|
| 487 |
stopPlacing();
|
| 488 |
});
|
|
|
|
| 491 |
function togglePopup(id){
|
| 492 |
const it = state.items.find(x=>x.id===id);
|
| 493 |
if (!it) return;
|
| 494 |
+
if (it.open){ removePopup(id); it.open = false; }
|
| 495 |
+
else { it.open = true; createPopup(it); }
|
| 496 |
+
// no global re-render here, so other videos keep playing
|
| 497 |
}
|
| 498 |
|
| 499 |
function adjustPopupPosition(el){
|
|
|
|
| 507 |
}
|
| 508 |
|
| 509 |
function renderItems(){
|
| 510 |
+
// Initial (or full) rebuild of pins/popups + list
|
| 511 |
overlay.innerHTML = '';
|
|
|
|
| 512 |
for (const it of state.items){
|
| 513 |
+
createPin(it);
|
| 514 |
+
if (it.open) createPopup(it);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
}
|
| 516 |
+
renderItemsList();
|
| 517 |
+
}
|
| 518 |
|
| 519 |
+
function renderItemsList(){
|
| 520 |
+
$('#count').textContent = String(state.items.length);
|
| 521 |
const list = $('#items'); list.innerHTML='';
|
| 522 |
for (const it of state.items){
|
| 523 |
const row = document.createElement('div'); row.className='item';
|
| 524 |
const kind = document.createElement('div'); kind.className='badge'; kind.textContent = it.type.toUpperCase();
|
| 525 |
const info = document.createElement('div');
|
| 526 |
+
const updateInfoTiny = () => info.querySelector('.tiny').textContent =
|
| 527 |
+
`${it.width}${it.widthUnit} • (${it.x.toFixed(1)}%, ${it.y.toFixed(1)}%)`;
|
| 528 |
info.innerHTML = `<div class="name">${escapeHtml(it.title||'')}</div>
|
| 529 |
+
<div class="tiny"></div>`;
|
| 530 |
const actions = document.createElement('div'); actions.className='actions';
|
| 531 |
+
updateInfoTiny();
|
| 532 |
|
| 533 |
const btnPlace = document.createElement('button'); btnPlace.textContent='Place';
|
| 534 |
btnPlace.addEventListener('click', ()=> startPlacing(it.id));
|
| 535 |
|
| 536 |
const btnPreview = document.createElement('button'); btnPreview.textContent = it.open ? 'Hide' : 'Preview';
|
| 537 |
+
btnPreview.addEventListener('click', ()=> { togglePopup(it.id); btnPreview.textContent = it.open ? 'Hide' : 'Preview'; });
|
| 538 |
|
| 539 |
const widthIn = document.createElement('input');
|
| 540 |
widthIn.type = 'number'; widthIn.min = '10'; widthIn.max = '2000';
|
|
|
|
| 546 |
|
| 547 |
widthIn.addEventListener('change', () => {
|
| 548 |
it.width = Number(widthIn.value) || (it.widthUnit === 'px' ? 320 : 25);
|
| 549 |
+
const popEl = getPopupEl(it.id);
|
| 550 |
+
if (popEl){
|
| 551 |
+
popEl.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
|
| 552 |
+
requestAnimationFrame(()=>adjustPopupPosition(popEl));
|
| 553 |
+
}
|
| 554 |
+
updateInfoTiny(); markDirty();
|
| 555 |
});
|
| 556 |
unitSel.addEventListener('change', () => {
|
| 557 |
it.widthUnit = unitSel.value;
|
| 558 |
+
const popEl = getPopupEl(it.id);
|
| 559 |
+
if (popEl){
|
| 560 |
+
popEl.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
|
| 561 |
+
requestAnimationFrame(()=>adjustPopupPosition(popEl));
|
| 562 |
+
}
|
| 563 |
+
updateInfoTiny(); markDirty();
|
| 564 |
});
|
| 565 |
|
| 566 |
const btnDel = document.createElement('button'); btnDel.textContent='Delete'; btnDel.className='btn-danger';
|
|
|
|
| 610 |
// Clear / Download
|
| 611 |
$('#clearAll').addEventListener('click', async () => {
|
| 612 |
if (!confirm('Clear background and all items?')) return;
|
| 613 |
+
// close all popups, clean HLS
|
| 614 |
+
state.items.forEach(it => removePopup(it.id));
|
| 615 |
state.background = null; state.items = [];
|
| 616 |
+
overlay.innerHTML = '';
|
| 617 |
+
renderItemsList();
|
| 618 |
+
renderBackground();
|
| 619 |
+
markDirty();
|
| 620 |
});
|
| 621 |
|
| 622 |
$('#downloadJson').addEventListener('click', () => {
|
|
|
|
| 630 |
URL.revokeObjectURL(url);
|
| 631 |
});
|
| 632 |
|
| 633 |
+
// Outside click: do nothing (don’t close popups)
|
| 634 |
window.addEventListener('resize', ()=>{
|
| 635 |
$$('.popup').forEach(el => {
|
| 636 |
el.style.transform='translate(-50%, -50%)';
|
| 637 |
requestAnimationFrame(()=>adjustPopupPosition(el));
|
| 638 |
});
|
| 639 |
});
|
| 640 |
+
stage.addEventListener('click', e => {
|
| 641 |
+
if (state.placingId) return; // placement handled on overlay
|
| 642 |
+
// intentionally no-op so outside clicks don't close anything
|
| 643 |
+
});
|
|
|
|
|
|
|
|
|
|
| 644 |
|
| 645 |
loadFromServer();
|
| 646 |
})();
|