/* ============================================= HELLION NEWTAB — drag.js Drag & Drop via Pointer Events Boards: Reihenfolge per Handle Bookmarks: Reihenfolge innerhalb eines Boards ============================================= */ // ---- BOARD FREE-MOVE (Pointer Events) ---- // Neugebaut fuer v2.3 (frueher Reorder mit Ghost/Placeholder). Vorbild: // widgets.js _initDrag — setPointerCapture, offX/offY, onMove mit Clamping // gegen window.innerWidth/Height, onUp schreibt board.pos + saveBoards(). // Gebunden am .board-drag-handle, NICHT am ganzen .board, damit Bookmark-Drag, // Klick-Delegation und Action-Buttons frei bleiben. function initBoardDragDrop() { const wrapper = document.getElementById('boardsWrapper'); wrapper.querySelectorAll('.board').forEach(boardEl => { const handle = boardEl.querySelector('.board-drag-handle'); if (!handle) return; handle.addEventListener('pointerdown', function onDown(e) { // Auf Mobil ist .board position:static (Stapel) -> kein Free-Move. if (getComputedStyle(boardEl).position !== 'absolute') return; e.preventDefault(); handle.setPointerCapture(e.pointerId); // .board.dragging hebt das Board per CSS nach vorne (z-index) UND ist das Signal fuer den // Live-Sync-Guard in app.js (bindStorageSync verwirft ein onChanged-Re-Render, das diesen // Drag sonst abreissen wuerde). Der Guard prueft genau diese Klasse (Phase-4-Review 2a). boardEl.classList.add('dragging'); const rect = boardEl.getBoundingClientRect(); const offX = e.clientX - rect.left; const offY = e.clientY - rect.top; const startCX = e.clientX, startCY = e.clientY; // Erst eine echte Bewegung (> 3px) zaehlt als Drag. Ein reiner Klick/Tap auf den Handle darf // board.pos NICHT ueberschreiben: renderBoards() schreibt in --board-x/y den gegen die Viewport // GECLAMPTEN Wert, board.pos bleibt absichtlich der wahre (evtl. off-screen) Wert. onUp liest // --board-x/y zurueck — bei einem No-Move-Klick waere das der Clamp und wuerde die wahre // Position zerstoeren (Phase-5-Review, HIGH/data-loss). let moved = false; function onMove(ev) { if (Math.abs(ev.clientX - startCX) > 3 || Math.abs(ev.clientY - startCY) > 3) moved = true; const maxX = window.innerWidth - boardEl.offsetWidth; const maxY = window.innerHeight - boardEl.offsetHeight; const x = Math.max(0, Math.min(maxX, ev.clientX - offX)); const y = Math.max(48, Math.min(maxY, ev.clientY - offY)); // 48px = Header-Hoehe boardEl.style.setProperty('--board-x', x + 'px'); boardEl.style.setProperty('--board-y', y + 'px'); } // Gemeinsames Aufraeumen: Pointer-Capture freigeben, ALLE Listener entfernen, // .board.dragging entfernen. MUSS auch im Cancel-Pfad laufen — sonst klebt die Klasse // und der app.js-Sync-Guard unterdrueckt dauerhaft Quick-Save-Renders (Phase-5-Review). function cleanup() { try { handle.releasePointerCapture(e.pointerId); } catch (_) { /* schon freigegeben */ } handle.removeEventListener('pointermove', onMove); handle.removeEventListener('pointerup', onUp); handle.removeEventListener('pointercancel', onCancel); boardEl.classList.remove('dragging'); } async function onUp() { cleanup(); // Nur bei echtem Verschieben persistieren — sonst board.pos unangetastet lassen. if (moved) { const id = boardEl.dataset.boardId; const board = boards.find(b => b.id === id); if (board) { board.pos = { x: parseFloat(boardEl.style.getPropertyValue('--board-x')), y: parseFloat(boardEl.style.getPropertyValue('--board-y')) }; await saveBoards(); } } // Einen waehrend des Drags ausgelassenen Quick-Save-Render nachholen (app.js). if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred(); } // pointercancel feuert STATT pointerup bei Touch-Interrupt, Browser-Geste oder wenn das // captured Element aus dem DOM faellt -> nur aufraeumen (cleanup), pos NICHT ueberschreiben. function onCancel() { cleanup(); if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred(); } handle.addEventListener('pointermove', onMove); handle.addEventListener('pointerup', onUp); handle.addEventListener('pointercancel', onCancel); }); }); } // ---- BOOKMARK DRAG (innerhalb eines Boards) ---- function initBookmarkDragDrop(listEl, board) { let dragSrcBmId = null; listEl.addEventListener('dragstart', e => { const item = e.target.closest('.bm-item'); if (!item) return; dragSrcBmId = item.dataset.bmId; e.dataTransfer.effectAllowed = 'move'; setTimeout(() => item.classList.add('dragging-source'), 0); }); listEl.addEventListener('dragend', e => { const item = e.target.closest('.bm-item'); if (item) item.classList.remove('dragging-source'); // Blieb ein Quick-Save-Render waehrend des Bookmark-Drags aus (Drop ausgeblieben), jetzt nachholen. if (typeof flushQuickSaveRenderIfDeferred === 'function') flushQuickSaveRenderIfDeferred(); }); listEl.addEventListener('dragover', e => { e.preventDefault(); const item = e.target.closest('.bm-item'); if (item) item.classList.add('drag-over'); }); listEl.addEventListener('dragleave', e => { const item = e.target.closest('.bm-item'); if (item) item.classList.remove('drag-over'); }); listEl.addEventListener('drop', async e => { e.preventDefault(); e.stopPropagation(); const item = e.target.closest('.bm-item'); if (!item) return; item.classList.remove('drag-over'); const targetBmId = item.dataset.bmId; if (!dragSrcBmId || dragSrcBmId === targetBmId) return; const srcIdx = board.bookmarks.findIndex(b => b.id === dragSrcBmId); const tgtIdx = board.bookmarks.findIndex(b => b.id === targetBmId); const [moved] = board.bookmarks.splice(srcIdx, 1); board.bookmarks.splice(tgtIdx, 0, moved); await saveBoards(); renderBoards(); }); }