This commit is contained in:
2026-09-28 23:24:50 +02:00
parent 2cc768f1fd
commit 73c0659d4a
29 changed files with 820 additions and 117 deletions
+81 -5
View File
@@ -1,7 +1,8 @@
// Pages whose content depends on the rating/mime filter and must reload when it changes.
// Profile pages count even when both previews are empty (then no .posts grid is rendered).
// Elements whose horizontal drags belong to them (sliders, sideways-scrolling rows): swipe gestures ignore touches starting here.
window.F0CK_NO_SWIPE_SELECTOR = 'input[type="range"], .f0ck-tuner-subtabs-nav, .f0ck-tuner-mode-bar, [data-no-swipe]';
// .f0ck-tuner-row: on touch devices sliders are locked (pointer-events none), so the touch lands on the row
window.F0CK_NO_SWIPE_SELECTOR = 'input[type="range"], .f0ck-tuner-row, .f0ck-tuner-subtabs-nav, .f0ck-tuner-mode-bar, [data-no-swipe]';
window.isFilterReloadableView = function () {
return !!document.querySelector('.posts, .tags-grid') || /^\/user\/[^/]+\/?$/.test(window.location.pathname);
@@ -2925,8 +2926,9 @@ window.cancelAnimFrame = (function () {
if (registerBtn && registerModal) {
registerBtn.addEventListener('click', (e) => {
// The nav "Register" link is only rendered while registration is open; the modal logic must not depend on it
if (registerModal) {
if (registerBtn) registerBtn.addEventListener('click', (e) => {
e.preventDefault();
openModal(registerModal);
});
@@ -3059,7 +3061,7 @@ window.cancelAnimFrame = (function () {
// Delegate help button click (since it's in a partial)
document.addEventListener('click', (e) => {
if (e.target.id === 'help-button') {
if (e.target.closest?.('#help-button')) {
openModal(shortcutsModal);
}
});
@@ -22291,7 +22293,29 @@ document.addEventListener('click', (e) => {
// ── Mobile scroll-to-top button ───────────────────────────────────────────────
// Visible only on mobile (≤999px), appears below #comments-container so users
// can easily get back to the top after reading comments.
// can easily get back to the top after reading comments. Only shown once the relevant scroller is
// scrolled far enough that getting back up by hand is a chore (1.5 screens, at least 600px).
(() => {
const scrollerFor = (btn) => btn.closest('#onara-modal') || document.querySelector('.index-container') || null;
const update = () => {
document.querySelectorAll('.mobile-scroll-to-top').forEach((btn) => {
const sc = scrollerFor(btn);
const top = sc ? sc.scrollTop : (window.scrollY || document.documentElement.scrollTop);
const viewH = sc ? sc.clientHeight : window.innerHeight;
btn.classList.toggle('is-visible', top > Math.max(600, viewH * 1.5));
});
};
let queued = false;
const schedule = () => {
if (queued) return;
queued = true;
requestAnimationFrame(() => { queued = false; update(); });
};
// Capture: scroll events don't bubble, and the scroller may be the window, .index-container or the Onara modal
document.addEventListener('scroll', schedule, { capture: true, passive: true });
window.addEventListener('resize', schedule, { passive: true });
})();
document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-scroll-to-top')) return;
e.preventDefault();
@@ -24357,3 +24381,55 @@ window.BulkSelection = (() => {
};
})();
// ─── Tap-to-activate range sliders on touch devices ───────────────────────────
// A finger scrolling a panel full of sliders easily grabs one by accident. On coarse pointers, sliders
// ignore touches (CSS: pointer-events none) until tapped; the tapped slider unlocks and relocks when the
// user touches elsewhere or leaves it alone for a few seconds. Player controls are exempt (see LOCKABLE).
(() => {
if (!window.matchMedia || !window.matchMedia('(pointer: coarse)').matches) return;
const LOCKABLE = 'input[type="range"]:not(.v0ck input):not(#volume-slider):not([data-no-tap-lock])';
const RELOCK_MS = 4000;
const HIT_PAD = 12;
let active = null;
let timer = null;
const lock = () => {
if (active) active.classList.remove('range-unlocked');
active = null;
clearTimeout(timer);
};
const armTimer = () => {
clearTimeout(timer);
timer = setTimeout(lock, RELOCK_MS);
};
const hits = (el, x, y) => {
const r = el.getBoundingClientRect();
return r.width > 0 && x >= r.left - HIT_PAD && x <= r.right + HIT_PAD && y >= r.top - HIT_PAD && y <= r.bottom + HIT_PAD;
};
document.documentElement.classList.add('range-tap-lock');
// A tap (no click fires after a scroll) lands on the slider's parent because the slider has no pointer events
document.addEventListener('click', (e) => {
const target = Array.from(document.querySelectorAll(LOCKABLE)).find(el => el !== active && hits(el, e.clientX, e.clientY));
if (!target) return;
lock();
active = target;
active.classList.add('range-unlocked');
armTimer();
e.preventDefault();
e.stopPropagation();
}, true);
// Touching anywhere outside the unlocked slider locks it again
document.addEventListener('touchstart', (e) => {
if (!active) return;
const t = e.touches[0];
if (t && !hits(active, t.clientX, t.clientY)) lock();
else armTimer();
}, { capture: true, passive: true });
// Keep it unlocked while it's being used
document.addEventListener('input', (e) => { if (e.target === active) armTimer(); }, true);
})();