/* weirsoe-ui.js — small, dependency-free UI behaviours for weirsoe-theme. * * • Sortable lists: mark a container with [data-wt-sortable] and a * data-order-url; its [data-item-id] children become drag-reorderable, and * on drop the new order is POSTed as urlencoded `order[]=…` to data-order-url. * Add a .drag-handle inside each child as the grab affordance. A clean, solid * drag image is used so translucent/blurred card surfaces don't smear. * *
*
⠿ …
*
* * • Copy to clipboard: any [data-copy-link] button copies its attribute value * and shows brief "Kopieret ✓" feedback. */ (function () { function afterElement(container, y) { var els = Array.prototype.slice.call( container.querySelectorAll('[data-item-id]:not(.wt-dragging)') ); var best = { offset: -Infinity, el: null }; els.forEach(function (child) { var box = child.getBoundingClientRect(); var offset = y - box.top - box.height / 2; if (offset < 0 && offset > best.offset) best = { offset: offset, el: child }; }); return best.el; } function cleanDragImage(el, e) { // A solid clone as the drag image — avoids the browser snapshotting the // card's translucent/blurred surface + page glow (which looks smeared). try { var g = el.cloneNode(true); g.classList.remove('wt-dragging'); var cs = getComputedStyle(el); var surface = getComputedStyle(document.documentElement) .getPropertyValue('--color-surface').trim(); g.style.position = 'fixed'; g.style.top = '-9999px'; g.style.left = '-9999px'; g.style.width = el.offsetWidth + 'px'; g.style.margin = '0'; g.style.opacity = '1'; g.style.boxShadow = 'none'; g.style.backdropFilter = 'none'; g.style.webkitBackdropFilter = 'none'; g.style.background = cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : surface || '#121a27'; document.body.appendChild(g); e.dataTransfer.setDragImage(g, 16, 16); setTimeout(function () { if (g.parentNode) g.parentNode.removeChild(g); }, 0); } catch (err) { /* setDragImage unsupported — fall back to default ghost */ } } function persist(container) { var url = container.getAttribute('data-order-url'); if (!url) return; var ids = Array.prototype.slice .call(container.querySelectorAll('[data-item-id]')) .map(function (el) { return el.getAttribute('data-item-id'); }); var body = ids.map(function (id) { return 'order[]=' + encodeURIComponent(id); }).join('&'); fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body, credentials: 'same-origin', }).catch(function () { /* non-fatal */ }); } function initSortable(container) { // ---- desktop: native HTML5 drag-and-drop ---- var dragEl = null; container.querySelectorAll('[data-item-id]').forEach(function (el) { el.setAttribute('draggable', 'true'); el.addEventListener('dragstart', function (e) { dragEl = el; el.classList.add('wt-dragging'); document.body.classList.add('wt-sorting'); e.dataTransfer.effectAllowed = 'move'; cleanDragImage(el, e); }); el.addEventListener('dragend', function () { el.classList.remove('wt-dragging'); document.body.classList.remove('wt-sorting'); dragEl = null; persist(container); }); }); container.addEventListener('dragover', function (e) { e.preventDefault(); if (!dragEl) return; var after = afterElement(container, e.clientY); if (after == null) container.appendChild(dragEl); else container.insertBefore(dragEl, after); }); // ---- mobile: long-press lifts the card into a floating clone that follows // the finger; the list reorders underneath; holding near a screen edge // auto-scrolls. (Native DnD doesn't fire on touch.) Hold ~220ms to lift; a // scroll-intent move before that cancels the lift so the page scrolls. ---- var holdTimer = null, lifted = false, touchEl = null, clone = null; var startY = 0, offX = 0, offY = 0, scrollSpeed = 0, raf = null; function startScroll() { if (raf) return; var tick = function () { if (scrollSpeed !== 0) window.scrollBy(0, scrollSpeed); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); } function stopScroll() { if (raf) { cancelAnimationFrame(raf); raf = null; } scrollSpeed = 0; } function makeClone(el, x, y) { var r = el.getBoundingClientRect(); offX = x - r.left; offY = y - r.top; clone = el.cloneNode(true); clone.classList.remove('wt-dragging'); clone.classList.add('wt-drag-clone'); clone.style.width = r.width + 'px'; clone.style.transform = 'translate(' + (x - offX) + 'px,' + (y - offY) + 'px) scale(1.04)'; document.body.appendChild(clone); } function moveClone(x, y) { if (clone) clone.style.transform = 'translate(' + (x - offX) + 'px,' + (y - offY) + 'px) scale(1.04)'; } function endTouch() { if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; } if (clone && clone.parentNode) clone.parentNode.removeChild(clone); clone = null; document.body.classList.remove('wt-sorting'); if (lifted && touchEl) { touchEl.classList.remove('wt-dragging'); persist(container); } lifted = false; touchEl = null; stopScroll(); } container.querySelectorAll('[data-item-id]').forEach(function (el) { el.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) return; if (e.target.closest('a, button, input, select, textarea, summary, label')) return; var t = e.touches[0]; startY = t.clientY; holdTimer = setTimeout(function () { lifted = true; touchEl = el; el.classList.add('wt-dragging'); document.body.classList.add('wt-sorting'); makeClone(el, t.clientX, t.clientY); if (navigator.vibrate) navigator.vibrate(12); startScroll(); }, 220); }, { passive: true }); }); container.addEventListener('touchmove', function (e) { if (!lifted) { if (holdTimer && Math.abs(e.touches[0].clientY - startY) > 12) { clearTimeout(holdTimer); holdTimer = null; // moved first → it's a scroll } return; } if (!touchEl) return; e.preventDefault(); // we own the gesture now var t = e.touches[0]; moveClone(t.clientX, t.clientY); var after = afterElement(container, t.clientY); if (after == null) container.appendChild(touchEl); else container.insertBefore(touchEl, after); // edge auto-scroll — generous zone so it triggers well before the edge var edge = 130, vh = window.innerHeight; if (t.clientY < edge) scrollSpeed = -Math.max(6, Math.ceil((edge - t.clientY) / 4)); else if (t.clientY > vh - edge) scrollSpeed = Math.max(6, Math.ceil((t.clientY - (vh - edge)) / 4)); else scrollSpeed = 0; }, { passive: false }); container.addEventListener('touchend', endTouch); container.addEventListener('touchcancel', endTouch); } function wire() { document.querySelectorAll('[data-wt-sortable]').forEach(initSortable); } document.addEventListener('click', function (e) { var btn = e.target.closest && e.target.closest('[data-copy-link]'); if (!btn) return; e.preventDefault(); var text = btn.getAttribute('data-copy-link'); var done = function () { var old = btn.textContent; btn.textContent = 'Kopieret ✓'; setTimeout(function () { btn.textContent = old; }, 1600); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(done, function () { window.prompt('Kopiér linket:', text); }); } else { window.prompt('Kopiér linket:', text); } }); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire); else wire(); })();