build-container / build (push) Successful in 3m3s
Sheets (including Now Playing) get a close button, an animated close and swipe-down on phones; lyrics and the visualizer close on swipe-down too. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
70 lines
5.7 KiB
JavaScript
70 lines
5.7 KiB
JavaScript
'use strict';
|
|
// ---- Bottom sheet (dialog on tablet/desktop). Body content is built by the caller from trusted/escaped HTML.
|
|
function openSheet(title, sub, bodyHtml) {
|
|
closeSheet();
|
|
const el = document.createElement('div');
|
|
el.id = 'sheet';
|
|
el.innerHTML = `<div class="scrim"></div><div class="sheet" role="dialog" aria-modal="true" aria-label="${esc(title)}">
|
|
<button type="button" class="ibtn sheetx" aria-label="Close">${icon('x')}</button><div class="grip"></div><h3>${esc(title)}</h3>${sub ? `<p class="sub">${esc(sub)}</p>` : ''}${bodyHtml}</div>`;
|
|
document.body.appendChild(el);
|
|
el.querySelector('.scrim').onclick = dismissSheet; el.querySelector('.sheetx').onclick = dismissSheet;
|
|
swipeToDismiss(el.querySelector('.sheet'), () => closeSheet(), { handle: '.grip, h3, .sheetx', scrim: el.querySelector('.scrim') });
|
|
requestAnimationFrame(() => el.classList.add('on'));
|
|
return el;
|
|
}
|
|
function closeSheet() { document.getElementById('sheet')?.remove(); }
|
|
/** Close with the slide-down (the close button, a tap outside); closeSheet() is the instant one other code calls before opening something else. */
|
|
function dismissSheet() { const s = document.getElementById('sheet'); if (!s || s._closing) return; s._closing = true; s.classList.remove('on'); setTimeout(() => { if (s.isConnected) s.remove(); }, 230); }
|
|
/** Swipe down to dismiss: `el` follows the finger and `close` runs when it is let go far enough or fast enough. Touches starting in `handle` always count; elsewhere only when nothing under the finger is scrolled down, and never on sliders or text fields. On a phone-sized screen only, unless `always` (full-screen views). */
|
|
function swipeToDismiss(el, close, { handle = '', scrim = null, always = false } = {}) {
|
|
let y0 = 0, x0 = 0, t0 = 0, on = false, dead = true;
|
|
const reset = () => { el.style.transition = 'transform .2s ease'; el.style.transform = ''; if (scrim) { scrim.style.transition = 'opacity .2s'; scrim.style.opacity = ''; } setTimeout(() => { el.style.transition = ''; if (scrim) scrim.style.transition = ''; }, 220); };
|
|
el.addEventListener('touchstart', e => {
|
|
on = false; dead = true; if (e.touches.length !== 1 || (!always && !matchMedia('(max-width: 767px)').matches)) return;
|
|
const t = e.target; if (t.closest && t.closest('input, textarea, select, [contenteditable], [data-nodrag]')) return;
|
|
if (!(handle && t.closest && t.closest(handle))) for (let n = t; n; n = n.parentElement) { if (n.scrollTop > 0) return; if (n === el) break; }
|
|
y0 = e.touches[0].clientY; x0 = e.touches[0].clientX; t0 = Date.now(); dead = false;
|
|
}, { passive: true });
|
|
el.addEventListener('touchmove', e => {
|
|
if (dead) return; const dy = e.touches[0].clientY - y0, dx = e.touches[0].clientX - x0;
|
|
if (!on) { if (dy > 10 && dy > Math.abs(dx) * 1.5) { on = true; el.style.transition = 'none'; if (scrim) scrim.style.transition = 'none'; } else if (Math.abs(dx) > 10 || dy < -6) { dead = true; return; } else return; }
|
|
if (e.cancelable) e.preventDefault();
|
|
const d = Math.max(0, dy); el.style.transform = 'translateY(' + d + 'px)'; if (scrim) scrim.style.opacity = String(Math.max(0, 1 - d / 420));
|
|
}, { passive: false });
|
|
const end = e => {
|
|
if (!on) return; on = false; dead = true; const dy = (e.changedTouches && e.changedTouches[0] ? e.changedTouches[0].clientY : y0) - y0, fast = dy / Math.max(1, Date.now() - t0) > 0.6;
|
|
if (e.type !== 'touchcancel' && (dy > 110 || (fast && dy > 30))) { el.style.transition = 'transform .18s ease'; el.style.transform = 'translateY(105%)'; if (scrim) { scrim.style.transition = 'opacity .18s'; scrim.style.opacity = '0'; } setTimeout(close, 170); } else reset();
|
|
};
|
|
el.addEventListener('touchend', end); el.addEventListener('touchcancel', end);
|
|
}
|
|
addEventListener('keydown', e => { if (e.key === 'Escape') closeSheet(); });
|
|
|
|
function actionSheet(title, sub, actions) {
|
|
const el = openSheet(title, sub, `<div class="list">${actions.map((a, i) => `
|
|
<button class="row ${a.danger ? 'dng' : ''}" data-i="${i}">${icon(a.icon)}<span class="grow"><b>${esc(a.label)}</b></span></button>`).join('')}</div>
|
|
<div class="bar"><button class="btn ghost" id="sx" style="flex:1;justify-content:center">Cancel</button></div>`);
|
|
el.querySelector('#sx').onclick = closeSheet;
|
|
el.querySelectorAll('button.row').forEach(b => b.onclick = () => { closeSheet(); actions[b.dataset.i].run(); });
|
|
}
|
|
|
|
function textSheet(title, label, okLabel, onOk, initial = '', validate) {
|
|
const el = openSheet(title, '', `<form><label>${esc(label)}</label><input type="text" id="tv" autocapitalize="off" autocomplete="off" required value="${esc(initial)}">
|
|
<div class="err" id="te" style="margin-top:6px"></div>
|
|
<div class="bar"><button class="btn ghost" type="button" id="sx" style="flex:1;justify-content:center">Cancel</button>
|
|
<button class="btn" style="flex:1;justify-content:center">${esc(okLabel)}</button></div></form>`);
|
|
el.querySelector('#sx').onclick = closeSheet;
|
|
el.querySelector('form').onsubmit = e => {
|
|
e.preventDefault(); const v = el.querySelector('#tv').value.trim(); if (!v) return;
|
|
const bad = validate && validate(v); if (bad) { el.querySelector('#te').textContent = bad; return; }
|
|
closeSheet(); onOk(v);
|
|
};
|
|
setTimeout(() => { const t = el.querySelector('#tv'); t.focus(); const d = initial.lastIndexOf('.'); t.setSelectionRange(0, d > 0 ? d : initial.length); }, 50);
|
|
}
|
|
|
|
function confirmSheet(title, sub, okLabel, onOk) {
|
|
const el = openSheet(title, sub, `<div class="bar"><button class="btn ghost" id="sx" style="flex:1;justify-content:center">Cancel</button>
|
|
<button class="btn" id="so" style="flex:1;justify-content:center;background:var(--danger);color:#fff">${esc(okLabel)}</button></div>`);
|
|
el.querySelector('#sx').onclick = closeSheet;
|
|
el.querySelector('#so').onclick = () => { closeSheet(); onOk(); };
|
|
}
|