Skip to content

Commit 54f26a2

Browse files
committed
fix: restore projects board drag/drop reorder
1 parent 3e5302f commit 54f26a2

File tree

3 files changed

+257
-124
lines changed

3 files changed

+257
-124
lines changed

client/projects-board.js

Lines changed: 70 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ class ProjectsBoardUI {
1818
this.dragProjectKey = null;
1919
this.dragSourceColumnId = null;
2020
this.dragCardEl = null;
21-
this.dragPlaceholderEl = null;
22-
this.dragDropInFlight = false;
21+
this.dragInsertTargetEl = null;
22+
this.dragInsertBeforeKey = null;
23+
this.dragInsertColumnId = null;
2324
this._dragOverRaf = null;
2425
this._pendingDragOver = null;
2526
this.hideForks = false;
@@ -474,33 +475,13 @@ class ProjectsBoardUI {
474475
return columnEl.querySelector?.('.projects-board-column-body[data-dropzone="true"]') || null;
475476
}
476477

477-
ensureDragPlaceholder(cardEl) {
478-
if (!this.dragPlaceholderEl) {
479-
const el = document.createElement('div');
480-
el.className = 'projects-board-drop-placeholder';
481-
el.setAttribute('data-drop-placeholder', 'true');
482-
el.setAttribute('aria-hidden', 'true');
483-
this.dragPlaceholderEl = el;
484-
}
485-
486-
if (cardEl && this.dragPlaceholderEl) {
487-
try {
488-
const rect = cardEl.getBoundingClientRect();
489-
const h = Number(rect?.height || 0);
490-
if (h > 0) {
491-
this.dragPlaceholderEl.style.height = `${Math.round(h)}px`;
492-
this.dragPlaceholderEl.style.minHeight = `${Math.round(h)}px`;
493-
}
494-
} catch {}
495-
}
496-
497-
return this.dragPlaceholderEl;
498-
}
499-
500-
clearDragPlaceholder() {
501-
if (this.dragPlaceholderEl?.parentElement) {
502-
this.dragPlaceholderEl.parentElement.removeChild(this.dragPlaceholderEl);
478+
clearDragInsertTarget() {
479+
if (this.dragInsertTargetEl) {
480+
this.dragInsertTargetEl.classList.remove('is-drop-target-before', 'is-drop-target-after');
503481
}
482+
this.dragInsertTargetEl = null;
483+
this.dragInsertBeforeKey = null;
484+
this.dragInsertColumnId = null;
504485
}
505486

506487
computeClosestCardForPoint(cards, x, y) {
@@ -510,7 +491,7 @@ class ProjectsBoardUI {
510491
let bestDist = Number.POSITIVE_INFINITY;
511492

512493
for (const card of cards) {
513-
if (!card || card === this.dragPlaceholderEl) continue;
494+
if (!card) continue;
514495
let rect = null;
515496
try {
516497
rect = card.getBoundingClientRect();
@@ -531,69 +512,57 @@ class ProjectsBoardUI {
531512
return best;
532513
}
533514

534-
positionDragPlaceholder(dropzoneEl, { x, y } = {}) {
515+
computeInsertBeforeKey(dropzoneEl, { x, y } = {}) {
535516
const dropzone = dropzoneEl;
536-
if (!dropzone) return null;
537-
if (!this.dragProjectKey) return null;
538-
539-
const placeholder = this.ensureDragPlaceholder(this.dragCardEl);
540-
if (!placeholder) return null;
517+
if (!dropzone) return { beforeKey: null, targetEl: null, after: false };
541518

542-
const empty = dropzone.querySelector?.('.projects-board-empty');
543519
const cards = Array.from(dropzone.querySelectorAll('.projects-board-card')).filter((el) => !el.classList.contains('dragging'));
544-
545-
if (!cards.length) {
546-
if (empty) dropzone.insertBefore(placeholder, empty);
547-
else dropzone.appendChild(placeholder);
548-
return placeholder;
549-
}
520+
if (!cards.length) return { beforeKey: null, targetEl: null, after: false };
550521

551522
const closest = this.computeClosestCardForPoint(cards, x, y);
552523
const target = closest?.card || null;
553524
const rect = closest?.rect || null;
554-
if (!target || !rect) {
555-
dropzone.appendChild(placeholder);
556-
return placeholder;
557-
}
525+
if (!target || !rect) return { beforeKey: null, targetEl: null, after: false };
558526

559527
const cx = rect.left + rect.width / 2;
560528
const cy = rect.top + rect.height / 2;
561529
const dx = (Number.isFinite(Number(x)) ? Number(x) : 0) - cx;
562530
const dy = (Number.isFinite(Number(y)) ? Number(y) : 0) - cy;
563531
const useY = Math.abs(dy) >= Math.abs(dx);
564532
const after = useY ? (dy > 0) : (dx > 0);
565-
const insertBefore = after ? target.nextElementSibling : target;
566533

567-
if (insertBefore === placeholder) return placeholder;
568-
if (placeholder.parentElement !== dropzone) {
569-
dropzone.insertBefore(placeholder, insertBefore);
570-
return placeholder;
534+
const idx = cards.indexOf(target);
535+
if (after) {
536+
const next = idx >= 0 ? cards[idx + 1] : null;
537+
const beforeKey = String(next?.dataset?.projectKey || '').trim() || null;
538+
return { beforeKey, targetEl: target, after: true };
571539
}
572540

573-
const currentNext = placeholder.nextElementSibling;
574-
if (!after && target === currentNext) return placeholder;
575-
if (after && target === placeholder.previousElementSibling) return placeholder;
576-
577-
dropzone.insertBefore(placeholder, insertBefore);
578-
return placeholder;
541+
const beforeKey = String(target?.dataset?.projectKey || '').trim() || null;
542+
return { beforeKey, targetEl: target, after: false };
579543
}
580544

581-
computeInsertIndexFromPlaceholder(dropzoneEl, projectKey) {
545+
updateDragInsertTarget(dropzoneEl, { x, y } = {}) {
582546
const dropzone = dropzoneEl;
583-
const placeholder = dropzone?.querySelector?.('.projects-board-drop-placeholder[data-drop-placeholder="true"]') || null;
584-
if (!dropzone || !placeholder) return null;
585-
const key = String(projectKey || '').trim().replace(/\\/g, '/');
547+
const col = dropzone?.closest?.('.projects-board-column');
548+
const columnId = String(col?.dataset?.columnId || '').trim();
586549

587-
let index = 0;
588-
const children = Array.from(dropzone.children || []);
589-
for (const child of children) {
590-
if (child === placeholder) break;
591-
if (!child?.classList?.contains?.('projects-board-card')) continue;
592-
const childKey = String(child?.dataset?.projectKey || '').trim().replace(/\\/g, '/');
593-
if (!childKey || childKey === key) continue;
594-
index += 1;
550+
const { beforeKey, targetEl, after } = this.computeInsertBeforeKey(dropzone, { x, y });
551+
this.dragInsertBeforeKey = beforeKey;
552+
this.dragInsertColumnId = columnId || null;
553+
554+
if (this.dragInsertTargetEl && this.dragInsertTargetEl !== targetEl) {
555+
this.dragInsertTargetEl.classList.remove('is-drop-target-before', 'is-drop-target-after');
595556
}
596-
return index;
557+
558+
if (!targetEl) {
559+
this.dragInsertTargetEl = null;
560+
return;
561+
}
562+
563+
targetEl.classList.remove('is-drop-target-before', 'is-drop-target-after');
564+
targetEl.classList.add(after ? 'is-drop-target-after' : 'is-drop-target-before');
565+
this.dragInsertTargetEl = targetEl;
597566
}
598567

599568
onDragStart(event) {
@@ -605,38 +574,22 @@ class ProjectsBoardUI {
605574
this.dragProjectKey = key;
606575
this.dragSourceColumnId = this.getProjectColumn(key);
607576
this.dragCardEl = card;
608-
this.dragDropInFlight = false;
577+
this.clearDragInsertTarget();
609578
card.classList.add('dragging');
610-
const placeholder = this.ensureDragPlaceholder(card);
611-
const sourceDropzone = card.closest?.('.projects-board-column-body[data-dropzone="true"]') || null;
612-
if (sourceDropzone && placeholder) {
613-
try {
614-
sourceDropzone.insertBefore(placeholder, card);
615-
} catch {}
616-
}
617579
try {
618580
event.dataTransfer?.setData?.('text/plain', key);
619581
event.dataTransfer.effectAllowed = 'move';
620582
} catch {}
621-
622-
// Hide the dragged card from the grid layout so the placeholder doesn't create an extra
623-
// "half column" by adding one more grid item.
624-
setTimeout(() => {
625-
if (card.classList.contains('dragging')) card.classList.add('drag-hidden');
626-
}, 0);
627583
}
628584

629585
onDragEnd(event) {
630586
const card = event.target?.closest?.('.projects-board-card');
631587
if (card) card.classList.remove('dragging');
632-
if (card && !this.dragDropInFlight) {
633-
card.classList.remove('drag-hidden');
634-
}
635588
document.querySelectorAll('.projects-board-column.drag-over').forEach((el) => el.classList.remove('drag-over'));
589+
this.clearDragInsertTarget();
636590
this.dragProjectKey = null;
637591
this.dragSourceColumnId = null;
638592
this.dragCardEl = null;
639-
this.clearDragPlaceholder();
640593
if (this._dragOverRaf) {
641594
window.cancelAnimationFrame(this._dragOverRaf);
642595
this._dragOverRaf = null;
@@ -657,18 +610,24 @@ class ProjectsBoardUI {
657610
event.dataTransfer.dropEffect = 'move';
658611
} catch {}
659612

660-
if (col.classList.contains('is-collapsed')) return;
613+
const columnId = String(col.dataset?.columnId || '').trim();
614+
if (col.classList.contains('is-collapsed')) {
615+
this.clearDragInsertTarget();
616+
this.dragInsertBeforeKey = null;
617+
this.dragInsertColumnId = columnId || null;
618+
return;
619+
}
620+
661621
const dropzone = this.getColumnDropzone(col);
662622
if (!dropzone) return;
663-
664623
this._pendingDragOver = { dropzone, x: event.clientX, y: event.clientY };
665624
if (this._dragOverRaf) return;
666625
this._dragOverRaf = window.requestAnimationFrame(() => {
667626
this._dragOverRaf = null;
668627
const pending = this._pendingDragOver;
669628
this._pendingDragOver = null;
670629
if (!pending) return;
671-
this.positionDragPlaceholder(pending.dropzone, { x: pending.x, y: pending.y });
630+
this.updateDragInsertTarget(pending.dropzone, { x: pending.x, y: pending.y });
672631
});
673632
}
674633

@@ -678,6 +637,10 @@ class ProjectsBoardUI {
678637
const related = event.relatedTarget && col.contains(event.relatedTarget);
679638
if (related) return;
680639
col.classList.remove('drag-over');
640+
const columnId = String(col.dataset?.columnId || '').trim();
641+
if (columnId && columnId === this.dragInsertColumnId) {
642+
this.clearDragInsertTarget();
643+
}
681644
}
682645

683646
async onDrop(event) {
@@ -689,35 +652,34 @@ class ProjectsBoardUI {
689652
const columnId = String(col.dataset?.columnId || '').trim();
690653
const projectKey = String(this.dragProjectKey || '').trim() || String(event.dataTransfer?.getData?.('text/plain') || '').trim();
691654
const sourceColumnId = this.dragSourceColumnId || this.getProjectColumn(projectKey);
692-
this.dragProjectKey = null;
693-
this.dragSourceColumnId = null;
694655
if (!projectKey || !columnId) return;
695656

696-
const draggedEl = this.dragCardEl;
697-
this.dragDropInFlight = true;
698-
699-
let insertIndex = null;
657+
const normalizedProjectKey = String(projectKey || '').trim().replace(/\\/g, '/');
700658
const dropzone = col.classList.contains('is-collapsed') ? null : this.getColumnDropzone(col);
659+
let insertBeforeKey = null;
701660
if (dropzone) {
702-
this.positionDragPlaceholder(dropzone, { x: event.clientX, y: event.clientY });
703-
insertIndex = this.computeInsertIndexFromPlaceholder(dropzone, projectKey);
661+
insertBeforeKey = this.computeInsertBeforeKey(dropzone, { x: event.clientX, y: event.clientY })?.beforeKey || null;
704662
}
705-
this.clearDragPlaceholder();
663+
if (!insertBeforeKey && this.dragInsertColumnId === columnId) {
664+
insertBeforeKey = this.dragInsertBeforeKey;
665+
}
666+
667+
this.dragProjectKey = null;
668+
this.dragSourceColumnId = null;
706669
this.dragCardEl = null;
670+
this.clearDragInsertTarget();
707671

708672
const full = this.buildFullColumnModel();
709-
const sourceKeys = (full.get(sourceColumnId) || []).map((p) => p.key).filter((k) => k !== projectKey);
673+
const sourceKeys = (full.get(sourceColumnId) || []).map((p) => p.key).filter((k) => k !== normalizedProjectKey);
710674
const destinationKeysBase = sourceColumnId === columnId
711675
? sourceKeys.slice()
712-
: (full.get(columnId) || []).map((p) => p.key).filter((k) => k !== projectKey);
676+
: (full.get(columnId) || []).map((p) => p.key).filter((k) => k !== normalizedProjectKey);
713677

714678
const destinationKeys = destinationKeysBase.slice();
715-
const fallbackIndex = destinationKeys.length;
716-
const desired = Number(insertIndex);
717-
const safeIndex = Number.isFinite(desired)
718-
? Math.min(Math.max(Math.round(desired), 0), destinationKeys.length)
719-
: fallbackIndex;
720-
destinationKeys.splice(safeIndex, 0, projectKey);
679+
const normalizedBeforeKey = String(insertBeforeKey || '').trim().replace(/\\/g, '/');
680+
const anchorIndex = normalizedBeforeKey ? destinationKeys.indexOf(normalizedBeforeKey) : -1;
681+
const insertIndex = anchorIndex >= 0 ? anchorIndex : destinationKeys.length;
682+
destinationKeys.splice(insertIndex, 0, normalizedProjectKey);
721683

722684
const orderByColumn = { [columnId]: destinationKeys };
723685
if (sourceColumnId !== columnId) orderByColumn[sourceColumnId] = sourceKeys;
@@ -738,10 +700,7 @@ class ProjectsBoardUI {
738700
} catch {}
739701
this.render();
740702
} catch (error) {
741-
if (draggedEl) draggedEl.classList.remove('drag-hidden');
742703
this.orchestrator?.showToast?.(String(error?.message || error), 'error');
743-
} finally {
744-
this.dragDropInFlight = false;
745704
}
746705
}
747706

client/styles/projects-board.css

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -224,22 +224,32 @@
224224
opacity: 0.65;
225225
}
226226

227-
.projects-board-card.drag-hidden {
228-
position: fixed;
229-
left: -10000px;
230-
top: -10000px;
231-
width: 1px;
232-
height: 1px;
233-
overflow: hidden;
234-
opacity: 0;
227+
.projects-board-card.is-drop-target-before,
228+
.projects-board-card.is-drop-target-after {
229+
position: relative;
230+
}
231+
232+
.projects-board-card.is-drop-target-before::before {
233+
content: '';
234+
position: absolute;
235+
left: 8px;
236+
right: 8px;
237+
top: 0;
238+
height: 4px;
239+
border-radius: 999px;
240+
background: rgba(79, 70, 229, 0.85);
235241
pointer-events: none;
236242
}
237243

238-
.projects-board-drop-placeholder {
239-
border: 2px dashed rgba(79, 70, 229, 0.55);
240-
border-radius: var(--radius-sm);
241-
background: rgba(79, 70, 229, 0.08);
242-
min-height: 64px;
244+
.projects-board-card.is-drop-target-after::after {
245+
content: '';
246+
position: absolute;
247+
left: 8px;
248+
right: 8px;
249+
bottom: 0;
250+
height: 4px;
251+
border-radius: 999px;
252+
background: rgba(79, 70, 229, 0.85);
243253
pointer-events: none;
244254
}
245255

0 commit comments

Comments
 (0)