@@ -16,6 +16,11 @@ class ProjectsBoardUI {
1616 this . projects = [ ] ;
1717 this . filter = '' ;
1818 this . dragProjectKey = null ;
19+ this . dragSourceColumnId = null ;
20+ this . dragCardEl = null ;
21+ this . dragPlaceholderEl = null ;
22+ this . _dragOverRaf = null ;
23+ this . _pendingDragOver = null ;
1924 this . hideForks = false ;
2025 this . githubRepos = [ ] ;
2126 this . _escHandler = null ;
@@ -463,14 +468,144 @@ class ProjectsBoardUI {
463468 }
464469 }
465470
471+ getColumnDropzone ( columnEl ) {
472+ if ( ! columnEl ) return null ;
473+ return columnEl . querySelector ?. ( '.projects-board-column-body[data-dropzone="true"]' ) || null ;
474+ }
475+
476+ ensureDragPlaceholder ( cardEl ) {
477+ if ( ! this . dragPlaceholderEl ) {
478+ const el = document . createElement ( 'div' ) ;
479+ el . className = 'projects-board-drop-placeholder' ;
480+ el . setAttribute ( 'data-drop-placeholder' , 'true' ) ;
481+ el . setAttribute ( 'aria-hidden' , 'true' ) ;
482+ this . dragPlaceholderEl = el ;
483+ }
484+
485+ if ( cardEl && this . dragPlaceholderEl ) {
486+ try {
487+ const rect = cardEl . getBoundingClientRect ( ) ;
488+ const h = Number ( rect ?. height || 0 ) ;
489+ if ( h > 0 ) {
490+ this . dragPlaceholderEl . style . height = `${ Math . round ( h ) } px` ;
491+ this . dragPlaceholderEl . style . minHeight = `${ Math . round ( h ) } px` ;
492+ }
493+ } catch { }
494+ }
495+
496+ return this . dragPlaceholderEl ;
497+ }
498+
499+ clearDragPlaceholder ( ) {
500+ if ( this . dragPlaceholderEl ?. parentElement ) {
501+ this . dragPlaceholderEl . parentElement . removeChild ( this . dragPlaceholderEl ) ;
502+ }
503+ }
504+
505+ computeClosestCardForPoint ( cards , x , y ) {
506+ const px = Number . isFinite ( Number ( x ) ) ? Number ( x ) : 0 ;
507+ const py = Number . isFinite ( Number ( y ) ) ? Number ( y ) : 0 ;
508+ let best = null ;
509+ let bestDist = Number . POSITIVE_INFINITY ;
510+
511+ for ( const card of cards ) {
512+ if ( ! card || card === this . dragPlaceholderEl ) continue ;
513+ let rect = null ;
514+ try {
515+ rect = card . getBoundingClientRect ( ) ;
516+ } catch {
517+ rect = null ;
518+ }
519+ if ( ! rect ) continue ;
520+
521+ const dx = ( px < rect . left ) ? ( rect . left - px ) : ( px > rect . right ? ( px - rect . right ) : 0 ) ;
522+ const dy = ( py < rect . top ) ? ( rect . top - py ) : ( py > rect . bottom ? ( py - rect . bottom ) : 0 ) ;
523+ const dist = ( dx * dx ) + ( dy * dy ) ;
524+ if ( dist < bestDist ) {
525+ bestDist = dist ;
526+ best = { card, rect } ;
527+ }
528+ }
529+
530+ return best ;
531+ }
532+
533+ positionDragPlaceholder ( dropzoneEl , { x, y } = { } ) {
534+ const dropzone = dropzoneEl ;
535+ if ( ! dropzone ) return null ;
536+ if ( ! this . dragProjectKey ) return null ;
537+
538+ const placeholder = this . ensureDragPlaceholder ( this . dragCardEl ) ;
539+ if ( ! placeholder ) return null ;
540+
541+ const empty = dropzone . querySelector ?. ( '.projects-board-empty' ) ;
542+ const cards = Array . from ( dropzone . querySelectorAll ( '.projects-board-card' ) ) . filter ( ( el ) => ! el . classList . contains ( 'dragging' ) ) ;
543+
544+ if ( ! cards . length ) {
545+ if ( empty ) dropzone . insertBefore ( placeholder , empty ) ;
546+ else dropzone . appendChild ( placeholder ) ;
547+ return placeholder ;
548+ }
549+
550+ const closest = this . computeClosestCardForPoint ( cards , x , y ) ;
551+ const target = closest ?. card || null ;
552+ const rect = closest ?. rect || null ;
553+ if ( ! target || ! rect ) {
554+ dropzone . appendChild ( placeholder ) ;
555+ return placeholder ;
556+ }
557+
558+ const cx = rect . left + rect . width / 2 ;
559+ const cy = rect . top + rect . height / 2 ;
560+ const dx = ( Number . isFinite ( Number ( x ) ) ? Number ( x ) : 0 ) - cx ;
561+ const dy = ( Number . isFinite ( Number ( y ) ) ? Number ( y ) : 0 ) - cy ;
562+ const useY = Math . abs ( dy ) >= Math . abs ( dx ) ;
563+ const after = useY ? ( dy > 0 ) : ( dx > 0 ) ;
564+ const insertBefore = after ? target . nextElementSibling : target ;
565+
566+ if ( insertBefore === placeholder ) return placeholder ;
567+ if ( placeholder . parentElement !== dropzone ) {
568+ dropzone . insertBefore ( placeholder , insertBefore ) ;
569+ return placeholder ;
570+ }
571+
572+ const currentNext = placeholder . nextElementSibling ;
573+ if ( ! after && target === currentNext ) return placeholder ;
574+ if ( after && target === placeholder . previousElementSibling ) return placeholder ;
575+
576+ dropzone . insertBefore ( placeholder , insertBefore ) ;
577+ return placeholder ;
578+ }
579+
580+ computeInsertIndexFromPlaceholder ( dropzoneEl , projectKey ) {
581+ const dropzone = dropzoneEl ;
582+ const placeholder = dropzone ?. querySelector ?. ( '.projects-board-drop-placeholder[data-drop-placeholder="true"]' ) || null ;
583+ if ( ! dropzone || ! placeholder ) return null ;
584+ const key = String ( projectKey || '' ) . trim ( ) . replace ( / \\ / g, '/' ) ;
585+
586+ let index = 0 ;
587+ const children = Array . from ( dropzone . children || [ ] ) ;
588+ for ( const child of children ) {
589+ if ( child === placeholder ) break ;
590+ if ( ! child ?. classList ?. contains ?. ( 'projects-board-card' ) ) continue ;
591+ const childKey = String ( child ?. dataset ?. projectKey || '' ) . trim ( ) . replace ( / \\ / g, '/' ) ;
592+ if ( ! childKey || childKey === key ) continue ;
593+ index += 1 ;
594+ }
595+ return index ;
596+ }
597+
466598 onDragStart ( event ) {
467599 if ( event . target ?. closest ?. ( '[data-no-drag="true"]' ) ) return ;
468600 const card = event . target ?. closest ?. ( '.projects-board-card' ) ;
469601 if ( ! card ) return ;
470602 const key = String ( card . dataset ?. projectKey || '' ) . trim ( ) ;
471603 if ( ! key ) return ;
472604 this . dragProjectKey = key ;
605+ this . dragSourceColumnId = this . getProjectColumn ( key ) ;
606+ this . dragCardEl = card ;
473607 card . classList . add ( 'dragging' ) ;
608+ this . ensureDragPlaceholder ( card ) ;
474609 try {
475610 event . dataTransfer ?. setData ?. ( 'text/plain' , key ) ;
476611 event . dataTransfer . effectAllowed = 'move' ;
@@ -482,17 +617,42 @@ class ProjectsBoardUI {
482617 if ( card ) card . classList . remove ( 'dragging' ) ;
483618 document . querySelectorAll ( '.projects-board-column.drag-over' ) . forEach ( ( el ) => el . classList . remove ( 'drag-over' ) ) ;
484619 this . dragProjectKey = null ;
620+ this . dragSourceColumnId = null ;
621+ this . dragCardEl = null ;
622+ this . clearDragPlaceholder ( ) ;
623+ if ( this . _dragOverRaf ) {
624+ window . cancelAnimationFrame ( this . _dragOverRaf ) ;
625+ this . _dragOverRaf = null ;
626+ }
627+ this . _pendingDragOver = null ;
485628 }
486629
487630 onDragOver ( event ) {
488631 const col = event . target ?. closest ?. ( '.projects-board-column' ) ;
489632 if ( ! col ) return ;
490633 if ( ! this . dragProjectKey ) return ;
491634 event . preventDefault ( ) ;
635+ document . querySelectorAll ( '.projects-board-column.drag-over' ) . forEach ( ( el ) => {
636+ if ( el !== col ) el . classList . remove ( 'drag-over' ) ;
637+ } ) ;
492638 col . classList . add ( 'drag-over' ) ;
493639 try {
494640 event . dataTransfer . dropEffect = 'move' ;
495641 } catch { }
642+
643+ if ( col . classList . contains ( 'is-collapsed' ) ) return ;
644+ const dropzone = this . getColumnDropzone ( col ) ;
645+ if ( ! dropzone ) return ;
646+
647+ this . _pendingDragOver = { dropzone, x : event . clientX , y : event . clientY } ;
648+ if ( this . _dragOverRaf ) return ;
649+ this . _dragOverRaf = window . requestAnimationFrame ( ( ) => {
650+ this . _dragOverRaf = null ;
651+ const pending = this . _pendingDragOver ;
652+ this . _pendingDragOver = null ;
653+ if ( ! pending ) return ;
654+ this . positionDragPlaceholder ( pending . dropzone , { x : pending . x , y : pending . y } ) ;
655+ } ) ;
496656 }
497657
498658 onDragLeave ( event ) {
@@ -511,35 +671,33 @@ class ProjectsBoardUI {
511671
512672 const columnId = String ( col . dataset ?. columnId || '' ) . trim ( ) ;
513673 const projectKey = String ( this . dragProjectKey || '' ) . trim ( ) || String ( event . dataTransfer ?. getData ?. ( 'text/plain' ) || '' ) . trim ( ) ;
674+ const sourceColumnId = this . dragSourceColumnId || this . getProjectColumn ( projectKey ) ;
514675 this . dragProjectKey = null ;
676+ this . dragSourceColumnId = null ;
515677 if ( ! projectKey || ! columnId ) return ;
516678
517- const targetCard = event . target ?. closest ?. ( '.projects-board-card' ) ;
518- const targetKey = targetCard ? String ( targetCard . dataset ?. projectKey || '' ) . trim ( ) : '' ;
519- const insertAfter = ( ( ) => {
520- if ( ! targetCard || ! targetKey || targetKey === projectKey ) return false ;
521- try {
522- const rect = targetCard . getBoundingClientRect ( ) ;
523- return ( event . clientY || 0 ) > rect . top + rect . height / 2 ;
524- } catch {
525- return false ;
526- }
527- } ) ( ) ;
679+ let insertIndex = null ;
680+ const dropzone = col . classList . contains ( 'is-collapsed' ) ? null : this . getColumnDropzone ( col ) ;
681+ if ( dropzone ) {
682+ this . positionDragPlaceholder ( dropzone , { x : event . clientX , y : event . clientY } ) ;
683+ insertIndex = this . computeInsertIndexFromPlaceholder ( dropzone , projectKey ) ;
684+ }
685+ this . clearDragPlaceholder ( ) ;
686+ this . dragCardEl = null ;
528687
529- const sourceColumnId = this . getProjectColumn ( projectKey ) ;
530688 const full = this . buildFullColumnModel ( ) ;
531689 const sourceKeys = ( full . get ( sourceColumnId ) || [ ] ) . map ( ( p ) => p . key ) . filter ( ( k ) => k !== projectKey ) ;
532690 const destinationKeysBase = sourceColumnId === columnId
533691 ? sourceKeys . slice ( )
534692 : ( full . get ( columnId ) || [ ] ) . map ( ( p ) => p . key ) . filter ( ( k ) => k !== projectKey ) ;
535693
536694 const destinationKeys = destinationKeysBase . slice ( ) ;
537- let insertIndex = destinationKeys . length ;
538- if ( targetKey && targetKey !== projectKey ) {
539- const idx = destinationKeys . indexOf ( targetKey ) ;
540- if ( idx !== - 1 ) insertIndex = insertAfter ? idx + 1 : idx ;
541- }
542- destinationKeys . splice ( insertIndex , 0 , projectKey ) ;
695+ const fallbackIndex = destinationKeys . length ;
696+ const desired = Number ( insertIndex ) ;
697+ const safeIndex = Number . isFinite ( desired )
698+ ? Math . min ( Math . max ( Math . round ( desired ) , 0 ) , destinationKeys . length )
699+ : fallbackIndex ;
700+ destinationKeys . splice ( safeIndex , 0 , projectKey ) ;
543701
544702 const orderByColumn = { [ columnId ] : destinationKeys } ;
545703 if ( sourceColumnId !== columnId ) orderByColumn [ sourceColumnId ] = sourceKeys ;
0 commit comments