@@ -586,3 +586,191 @@ export function setupQuickPinListener(moveTabToSpace, moveTabToPinned, moveTabTo
586586 }
587587 } ) ;
588588}
589+
590+ // ============================================================================
591+ // Unified Drag Helper Functions
592+ // ============================================================================
593+
594+ /**
595+ * Unified function to find the element to insert after during drag-and-drop.
596+ * Replaces getDragAfterElementSwitcher, getDragAfterElement, and getDragAfterElementFavicon.
597+ *
598+ * @param {HTMLElement } container - The container element
599+ * @param {number } position - The mouse position (clientX for horizontal, clientY for vertical)
600+ * @param {Object } options - Configuration options
601+ * @param {string } options.axis - 'x' for horizontal, 'y' for vertical (default: 'y')
602+ * @param {string } options.selector - CSS selector for draggable elements
603+ * @param {string } options.placeholderSelector - CSS selector for placeholder element
604+ * @returns {HTMLElement|null } - The element to insert after, or null
605+ */
606+ export function getDragAfterElement ( container , position , options = { } ) {
607+ const { axis = 'y' , selector, placeholderSelector } = options ;
608+ const draggableElements = [ ...container . querySelectorAll ( selector ) ] ;
609+
610+ // If no draggable elements exist, return the placeholder as a reference for empty containers
611+ if ( draggableElements . length === 0 ) {
612+ return placeholderSelector ? container . querySelector ( placeholderSelector ) : null ;
613+ }
614+
615+ return draggableElements . reduce ( ( closest , child ) => {
616+ const box = child . getBoundingClientRect ( ) ;
617+ const offset = axis === 'x'
618+ ? position - box . left - box . width / 2
619+ : position - box . top - box . height / 2 ;
620+
621+ if ( offset < 0 && offset > closest . offset ) {
622+ return { offset, element : child } ;
623+ }
624+ return closest ;
625+ } , { offset : Number . NEGATIVE_INFINITY } ) . element ;
626+ }
627+
628+ // ============================================================================
629+ // Container Query Helpers
630+ // ============================================================================
631+
632+ /**
633+ * Get a space element by its ID
634+ * @param {number|string } spaceId - The space ID
635+ * @returns {HTMLElement|null }
636+ */
637+ export function getSpaceElement ( spaceId ) {
638+ return document . querySelector ( `[data-space-id="${ spaceId } "]` ) ;
639+ }
640+
641+ /**
642+ * Get both pinned and temporary containers for a space element
643+ * @param {HTMLElement } spaceElement - The space element
644+ * @returns {{pinned: HTMLElement|null, temp: HTMLElement|null} }
645+ */
646+ export function getContainers ( spaceElement ) {
647+ return {
648+ pinned : spaceElement ?. querySelector ( '[data-tab-type="pinned"]' ) ?? null ,
649+ temp : spaceElement ?. querySelector ( '[data-tab-type="temporary"]' ) ?? null
650+ } ;
651+ }
652+
653+ /**
654+ * Get a tab element by its ID
655+ * @param {number|string } tabId - The tab ID
656+ * @returns {HTMLElement|null }
657+ */
658+ export function getTabElement ( tabId ) {
659+ return document . querySelector ( `[data-tab-id="${ tabId } "]` ) ;
660+ }
661+
662+ /**
663+ * Get pinned container for a space element
664+ * @param {HTMLElement } spaceElement - The space element
665+ * @returns {HTMLElement|null }
666+ */
667+ export function getPinnedContainer ( spaceElement ) {
668+ return spaceElement ?. querySelector ( '[data-tab-type="pinned"]' ) ?? null ;
669+ }
670+
671+ /**
672+ * Get temporary container for a space element
673+ * @param {HTMLElement } spaceElement - The space element
674+ * @returns {HTMLElement|null }
675+ */
676+ export function getTempContainer ( spaceElement ) {
677+ return spaceElement ?. querySelector ( '[data-tab-type="temporary"]' ) ?? null ;
678+ }
679+
680+ // ============================================================================
681+ // Active State Management
682+ // ============================================================================
683+
684+ /**
685+ * Clear active state from all tabs and pinned favicons
686+ */
687+ export function clearAllActiveStates ( ) {
688+ document . querySelectorAll ( '.tab, .pinned-favicon' )
689+ . forEach ( el => el . classList . remove ( 'active' ) ) ;
690+ }
691+
692+ // ============================================================================
693+ // Drop Indicator Functions
694+ // ============================================================================
695+
696+ /**
697+ * Hide all drop indicators in the document
698+ */
699+ export function hideAllDropIndicators ( ) {
700+ document . querySelectorAll ( '.drop-indicator-horizontal, .drop-indicator-vertical' ) . forEach ( element => {
701+ element . classList . remove ( 'drop-indicator-horizontal' , 'drop-indicator-vertical' , 'above' , 'below' , 'left' , 'right' ) ;
702+ } ) ;
703+ }
704+
705+ /**
706+ * Show a drop indicator on the target element
707+ * @param {HTMLElement } targetElement - The element to show the indicator on
708+ * @param {string } position - Position: 'above', 'below', 'left', or 'right'
709+ * @param {boolean } isHorizontal - True for horizontal layout (favicons), false for vertical (tabs)
710+ */
711+ export function showDropIndicator ( targetElement , position , isHorizontal = false ) {
712+ // First, hide all existing indicators
713+ hideAllDropIndicators ( ) ;
714+
715+ if ( ! targetElement ) return ;
716+
717+ if ( isHorizontal ) {
718+ // For horizontal favicons (left/right positioning)
719+ targetElement . classList . add ( 'drop-indicator-vertical' ) ;
720+ targetElement . classList . add ( position ) ; // 'left' or 'right'
721+ } else {
722+ // For vertical sidebar tabs (above/below positioning)
723+ targetElement . classList . add ( 'drop-indicator-horizontal' ) ;
724+ targetElement . classList . add ( position ) ; // 'above' or 'below'
725+ }
726+ }
727+
728+ /**
729+ * Get the drop position relative to an element
730+ * @param {HTMLElement } element - The target element
731+ * @param {number } clientX - Mouse X position
732+ * @param {number } clientY - Mouse Y position
733+ * @param {boolean } isHorizontal - True for horizontal layout, false for vertical
734+ * @returns {string|null } - Position: 'above', 'below', 'left', 'right', or null
735+ */
736+ export function getDropPosition ( element , clientX , clientY , isHorizontal = false ) {
737+ if ( ! element ) return null ;
738+
739+ const rect = element . getBoundingClientRect ( ) ;
740+
741+ if ( isHorizontal ) {
742+ // For horizontal favicons, use X position to determine left/right
743+ const centerX = rect . left + rect . width / 2 ;
744+ return clientX < centerX ? 'left' : 'right' ;
745+ } else {
746+ // For vertical tabs, use Y position to determine above/below
747+ const centerY = rect . top + rect . height / 2 ;
748+ return clientY < centerY ? 'above' : 'below' ;
749+ }
750+ }
751+
752+ /**
753+ * Handle empty container drops consistently
754+ * @param {HTMLElement } container - The container element
755+ * @param {HTMLElement } draggingElement - The element being dragged
756+ * @param {HTMLElement } placeholder - The placeholder element
757+ * @returns {boolean } - True if handled successfully
758+ */
759+ export function handleEmptyContainerDrop ( container , draggingElement , placeholder ) {
760+ if ( ! container || ! draggingElement || ! placeholder ) return false ;
761+
762+ // Append element to container
763+ container . appendChild ( draggingElement ) ;
764+
765+ // Hide placeholder appropriately based on type
766+ if ( placeholder . classList . contains ( 'pinned-placeholder-container' ) ) {
767+ // For favorites area - use display none
768+ placeholder . style . display = 'none' ;
769+ } else if ( placeholder . classList . contains ( 'tab-placeholder' ) ) {
770+ // For space containers - use hidden class
771+ placeholder . classList . add ( 'hidden' ) ;
772+ }
773+
774+ Logger . log ( 'Handled empty container drop, hiding placeholder' ) ;
775+ return true ;
776+ }
0 commit comments