diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css index ce7a1a5..624fc2b 100644 --- a/server/src/wled_controller/static/css/cards.css +++ b/server/src/wled_controller/static/css/cards.css @@ -73,7 +73,7 @@ section { } .card-enter { - animation: cardEnter 0.25s ease-out both; + animation: cardEnter 0.25s ease-out backwards; } /* ── Card drag-and-drop reordering ── */ @@ -135,14 +135,22 @@ section { transition: none; } -/* Suppress hover effects during drag */ -.cs-dragging .card, -.cs-dragging .template-card { +/* Suppress hover effects globally during drag */ +body.cs-drag-active .card, +body.cs-drag-active .template-card, +body.cs-drag-active .add-template-card { transition: none !important; transform: none !important; } -.cs-dragging .card-drag-handle { +body.cs-drag-active .add-template-card { + pointer-events: none; + border-color: var(--border-color) !important; + background: transparent !important; + box-shadow: none !important; +} + +body.cs-drag-active .card-drag-handle { opacity: 0 !important; } diff --git a/server/src/wled_controller/static/js/core/card-sections.js b/server/src/wled_controller/static/js/core/card-sections.js index 591ffb1..bb639f8 100644 --- a/server/src/wled_controller/static/js/core/card-sections.js +++ b/server/src/wled_controller/static/js/core/card-sections.js @@ -178,6 +178,12 @@ export class CardSection { * @returns {{added: Set, replaced: Set, removed: Set}} */ reconcile(items) { + // Skip DOM mutations while a drag is in progress — would destroy drag state + if (this._dragState) { + this._pendingReconcile = items; + return { added: new Set(), replaced: new Set(), removed: new Set() }; + } + const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`); if (!content) return { added: new Set(), replaced: new Set(), removed: new Set() }; @@ -240,6 +246,7 @@ export class CardSection { if (card) { card.style.animationDelay = `${delay}ms`; card.classList.add('card-enter'); + card.addEventListener('animationend', () => card.classList.remove('card-enter'), { once: true }); delay += 30; } } @@ -347,6 +354,7 @@ export class CardSection { cards.forEach((card, i) => { card.style.animationDelay = `${i * 30}ms`; card.classList.add('card-enter'); + card.addEventListener('animationend', () => card.classList.remove('card-enter'), { once: true }); }); } @@ -502,19 +510,16 @@ export class CardSection { ds.clone.style.left = (e.clientX - ds.offsetX) + 'px'; ds.clone.style.top = (e.clientY - ds.offsetY) + 'px'; - // Find drop target - const target = this._getDropTarget(e.clientX, e.clientY, ds.content); - if (target && target !== ds.placeholder) { - const rect = target.getBoundingClientRect(); - const midX = rect.left + rect.width / 2; - const midY = rect.top + rect.height / 2; - // For grid: use combined horizontal+vertical check - const insertBefore = (e.clientY < midY) || (e.clientY < midY + rect.height * 0.3 && e.clientX < midX); - if (insertBefore) { - ds.content.insertBefore(ds.placeholder, target); - } else { - ds.content.insertBefore(ds.placeholder, target.nextSibling); - } + // Only move placeholder when cursor enters a card's rect + const { card: target, before } = this._getDropTarget(e.clientX, e.clientY, ds.content); + if (!target) return; // cursor is in a gap — keep placeholder where it is + if (target === ds.lastTarget && before === ds.lastBefore) return; // same position + ds.lastTarget = target; + ds.lastBefore = before; + if (before) { + ds.content.insertBefore(ds.placeholder, target); + } else { + ds.content.insertBefore(ds.placeholder, target.nextSibling); } // Auto-scroll near viewport edges @@ -548,6 +553,7 @@ export class CardSection { // Hide original ds.card.style.display = 'none'; ds.content.classList.add('cs-dragging'); + document.body.classList.add('cs-drag-active'); } _onDragEnd() { @@ -564,21 +570,30 @@ export class CardSection { ds.placeholder.remove(); ds.clone.remove(); ds.content.classList.remove('cs-dragging'); + document.body.classList.remove('cs-drag-active'); // Save new order from DOM const keys = this._readDomOrder(ds.content); this._saveOrder(keys); + + // Flush any reconcile that was deferred during drag + if (this._pendingReconcile) { + const items = this._pendingReconcile; + this._pendingReconcile = null; + this.reconcile(items); + } } _getDropTarget(x, y, content) { - // Temporarily show all cards for hit testing - const els = document.elementsFromPoint(x, y); - for (const el of els) { - if (el === content) break; - const card = el.closest(`[${this.keyAttr}]`); - if (card && card.style.display !== 'none' && content.contains(card)) return card; + const cards = content.querySelectorAll(`[${this.keyAttr}]`); + for (const card of cards) { + if (card.style.display === 'none') continue; + const r = card.getBoundingClientRect(); + if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) { + return { card, before: x < r.left + r.width / 2 }; + } } - return null; + return { card: null, before: false }; } _readDomOrder(content) {