/* 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');
e.dataTransfer.effectAllowed = 'move';
cleanDragImage(el, e);
});
el.addEventListener('dragend', function () {
el.classList.remove('wt-dragging');
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;
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');
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();
})();