@@ -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
0 commit comments