From 12f9766b40f9276c661137a939259c3b16f33f11 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 4 Sep 2025 13:35:21 +0530 Subject: [PATCH 1/8] fix: right clicks on tab bar sometimes opens up editor's context menu --- src/command/DefaultMenus.js | 6 +++++- src/extensionsIntegrated/TabBar/main.js | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index 5a1e8febaa..e62c756d2e 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -407,6 +407,10 @@ define(function (require, exports, module) { * an existing selection */ $("#editor-holder").on("contextmenu", function (e) { + // make sure that the click was not made inside a tab bar container + // if it is, then we don't show editor context menu as tab bar has its own + if($(e.target).closest('.tab-bar-container').length) { return; } + require(["editor/EditorManager"], function (EditorManager) { if ($(e.target).parents(".CodeMirror-gutter").length !== 0) { return; @@ -477,4 +481,4 @@ define(function (require, exports, module) { Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU).on("beforeContextMenuOpen", _setMenuItemsVisible); Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU).on("beforeContextMenuOpen", _setMenuItemsVisible); }); -}); \ No newline at end of file +}); diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index e6f6494d46..2e571fb5d7 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -511,6 +511,9 @@ define(function (require, exports, module) { // delegate event handling for both tab bars $(document).on("mousedown", ".phoenix-tab-bar .tab", function (event) { + // to prevent right clicks activate the mousedown event + if (event.button === 2) { return; } + if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { return; } From f330a263103e61860f7c09cd7521d8a468c22b45 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 4 Sep 2025 15:35:30 +0530 Subject: [PATCH 2/8] fix: drag indicators not coming at expected places --- src/extensionsIntegrated/TabBar/drag-drop.js | 998 ++++++------------- 1 file changed, 313 insertions(+), 685 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js index 578ce516a9..53bcb50062 100644 --- a/src/extensionsIntegrated/TabBar/drag-drop.js +++ b/src/extensionsIntegrated/TabBar/drag-drop.js @@ -28,15 +28,11 @@ define(function (require, exports, module) { /** * These variables track the drag and drop state of tabs * draggedTab: The tab that is currently being dragged - * dragOverTab: The tab that is currently being hovered over * dragIndicator: Visual indicator showing where the tab will be dropped - * scrollInterval: Used for automatic scrolling when dragging near edges * dragSourcePane: To track which pane the dragged tab originated from */ let draggedTab = null; - let dragOverTab = null; let dragIndicator = null; - let scrollInterval = null; let dragSourcePane = null; /** @@ -51,586 +47,244 @@ define(function (require, exports, module) { // this is to make sure that the drag indicator is hidden and remove any inline styles if (dragIndicator) { dragIndicator.hide().css({ - top: '', - left: '', - height: '' + top: "", + left: "", + height: "" }); } // Reset all drag state variables draggedTab = null; - dragOverTab = null; dragSourcePane = null; - if (scrollInterval) { - clearInterval(scrollInterval); - scrollInterval = null; - } - - $("#tab-drag-extended-zone").remove(); - - // this is needed to make sure that all the drag-active styling are properly hidden - // it is required because noticed a bug where sometimes some styles remain when drop fails - $(".phoenix-tab-bar").removeClass("drag-active"); - - setTimeout(() => { - // a double check just to make sure that the drag indicator is still hidden - if (dragIndicator && dragIndicator.is(':visible')) { - dragIndicator.hide(); - } - }, 5); + // remove any temporary elements + $("#tab-drag-helper").remove(); } /** - * Initialize drag and drop functionality for tab bars - * This is called from `main.js` - * This function sets up event listeners for both panes' tab bars - * and creates the visual drag indicator - * - * @param {String} firstPaneSelector - Selector for the first pane tab bar $("#phoenix-tab-bar") - * @param {String} secondPaneSelector - Selector for the second pane tab bar $("#phoenix-tab-bar-2") + * create the drag indicator if it doesn't exist */ - function init(firstPaneSelector, secondPaneSelector) { - setupDragForTabBar(firstPaneSelector); - setupDragForTabBar(secondPaneSelector); - setupContainerDrag(firstPaneSelector); - setupContainerDrag(secondPaneSelector); - - // Create drag indicator element if it doesn't exist - if (!dragIndicator) { + function ensureDragIndicator() { + if (!dragIndicator || !dragIndicator.length) { dragIndicator = $('
'); $("body").append(dragIndicator); } - - // add initialization for empty panes - initEmptyPaneDropTargets(); - - // Set up global drag cleanup handlers to ensure drag state is always cleaned up - setupGlobalDragCleanup(); } /** - * Setup drag and drop for a specific tab bar - * Makes tabs draggable and adds all the necessary event listeners - * - * @param {String} tabBarSelector - The selector for the tab bar + * this function is to update the drag indicator position + * @param {Element} targetTab - The target tab element + * @param {boolean} insertBefore - Whether to insert before the target tab */ - function setupDragForTabBar(tabBarSelector) { - const $tabs = $(tabBarSelector).find(".tab"); + function updateDragIndicator(targetTab, insertBefore) { + if (!targetTab) { + dragIndicator.hide(); + return; + } - // Make tabs draggable - $tabs.attr("draggable", "true"); - - // Remove any existing event listeners first to prevent duplicates - // This is important when the tab bar is recreated or updated - $tabs.off("dragstart dragover dragenter dragleave drop dragend"); - - // Add drag event listeners to each tab - // Each event has its own handler function for better organization - $tabs.on("dragstart", handleDragStart); - $tabs.on("dragover", handleDragOver); - $tabs.on("dragenter", handleDragEnter); - $tabs.on("dragleave", handleDragLeave); - $tabs.on("drop", handleDrop); - $tabs.on("dragend", handleDragEnd); + const rect = targetTab.getBoundingClientRect(); + const x = insertBefore ? rect.left : rect.right; + + dragIndicator + .css({ + position: "fixed", + left: x + "px", + top: rect.top + "px", + height: rect.height + "px", + width: "2px", + zIndex: 10001 + }) + .show(); } /** - * Setup container-level drag events - * This enables dropping tabs in empty spaces and auto-scrolling - * when dragging near the container's edges - * - * @param {String} containerSelector - The selector for the tab bar container + * get the drop position relative to a target tab + * @param {Element} targetTab - The target tab element + * @param {number} mouseX - The mouse X coordinate + * @returns {boolean} True if should insert before, false if after */ - function setupContainerDrag(containerSelector) { - const $container = $(containerSelector); - let lastKnownMousePosition = { x: 0 }; - const boundaryTolerance = 50; // px tolerance outside the container that still allows dropping - - // create a larger drop zone around the container - // this is done to make sure that even if the tab is not exactly over the tab bar, we still allow drag-drop - const createOuterDropZone = () => { - if (draggedTab && !$("#tab-drag-extended-zone").length) { - // an invisible larger zone around the container that can still receive drops - const containerRect = $container[0].getBoundingClientRect(); - const $outerZone = $('
').css({ - position: "fixed", - top: containerRect.top - boundaryTolerance, - left: containerRect.left - boundaryTolerance, - width: containerRect.width + boundaryTolerance * 2, - height: containerRect.height + boundaryTolerance * 2, - zIndex: 9999, - pointerEvents: "all" - }); - - $("body").append($outerZone); - - $outerZone.on("dragover", function (e) { - e.preventDefault(); - e.stopPropagation(); - lastKnownMousePosition.x = e.originalEvent.clientX; - - autoScrollContainer($container[0], lastKnownMousePosition.x); - - updateDragIndicatorFromOuterZone($container, lastKnownMousePosition.x); - - return false; - }); - - $outerZone.on("drop", function (e) { - e.preventDefault(); - e.stopPropagation(); - - // to handle drop the same way as if it happened in the container - handleOuterZoneDrop($container, lastKnownMousePosition.x); - - return false; - }); - } - }; - - // When dragging over the container but not directly over a tab element - $container.on("dragover", function (e) { - if (e.preventDefault) { - e.preventDefault(); - } - - lastKnownMousePosition.x = e.originalEvent.clientX; - - // Clear any existing scroll interval - if (scrollInterval) { - clearInterval(scrollInterval); - } - - // auto-scroll if near container edge - autoScrollContainer(this, e.originalEvent.clientX); - - // Set up interval for continuous scrolling while dragging near the edge - scrollInterval = setInterval(() => { - if (draggedTab) { - // Only continue scrolling if still dragging - autoScrollContainer(this, lastKnownMousePosition.x); - } else { - clearInterval(scrollInterval); - scrollInterval = null; - } - }, 16); // this is almost about 60fps - - // if the target is not a tab, update the drag indicator using the container bounds - if ($(e.target).closest(".tab").length === 0) { - const containerRect = this.getBoundingClientRect(); - const mouseX = e.originalEvent.clientX; - - // determine if dropping on left or right half of container - const onLeftSide = mouseX < containerRect.left + containerRect.width / 2; - - const $tabs = $container.find(".tab"); - if ($tabs.length) { - // choose the first tab for left drop, last tab for right drop - const targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0]; - updateDragIndicator(targetTab, onLeftSide); - } - } - - // Create the extended drop zone if we're actively dragging - if (draggedTab) { - createOuterDropZone(); - } - }); - - // handle drop on the container (empty space) - $container.on("drop", function (e) { - if (e.preventDefault) { - e.preventDefault(); - } - - // get container dimensions to determine drop position - const containerRect = this.getBoundingClientRect(); - const mouseX = e.originalEvent.clientX; - // determine if dropping on left or right half of container - const onLeftSide = mouseX < containerRect.left + containerRect.width / 2; - - const $tabs = $container.find(".tab"); - if ($tabs.length) { - // If dropping on left half, target the first tab; otherwise, target the last tab - const targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0]; - - // make sure that the draggedTab exists and isn't the same as the target - if (draggedTab && targetTab && draggedTab !== targetTab) { - // check which pane the container belongs to - const isSecondPane = $container.attr("id") === "phoenix-tab-bar-2"; - const targetPaneId = isSecondPane ? "second-pane" : "first-pane"; - const draggedPath = $(draggedTab).attr("data-path"); - const targetPath = $(targetTab).attr("data-path"); - - // check if we're dropping in a different pane - if (dragSourcePane !== targetPaneId) { - // cross-pane drop - moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, onLeftSide); - } else { - // same pane drop - moveWorkingSetItem(targetPaneId, draggedPath, targetPath, onLeftSide); - } - } - } - - // ensure all drag state is cleaned up - cleanupDragState(); - }); - - /** - * Updates the drag indicator when mouse is in the extended zone (outside actual tab bar) - * @param {jQuery} $container - The tab bar container - * @param {number} mouseX - Current mouse X position - */ - function updateDragIndicatorFromOuterZone($container, mouseX) { - const containerRect = $container[0].getBoundingClientRect(); - const $tabs = $container.find(".tab"); - - if ($tabs.length) { - // Determine if dropping on left half or right half - let onLeftSide = true; - let targetTab; - - // If beyond the right edge, use the last tab - if (mouseX > containerRect.right) { - targetTab = $tabs.last()[0]; - onLeftSide = false; - } else if (mouseX < containerRect.left) { // If beyond the left edge, use the first tab - targetTab = $tabs.first()[0]; - onLeftSide = true; - } else { // If within bounds, find the closest tab - onLeftSide = mouseX < containerRect.left + containerRect.width / 2; - targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0]; - } - - updateDragIndicator(targetTab, onLeftSide); - } - } - - /** - * Handles drops that occur in the extended drop zone - * @param {jQuery} $container - The tab bar container - * @param {number} mouseX - Current mouse X position - */ - function handleOuterZoneDrop($container, mouseX) { - const containerRect = $container[0].getBoundingClientRect(); - const $tabs = $container.find(".tab"); - - if ($tabs.length && draggedTab) { - // Determine drop position similar to updateDragIndicatorFromOuterZone - let onLeftSide = true; - let targetTab; - - if (mouseX > containerRect.right) { - targetTab = $tabs.last()[0]; - onLeftSide = false; - } else if (mouseX < containerRect.left) { - targetTab = $tabs.first()[0]; - onLeftSide = true; - } else { - onLeftSide = mouseX < containerRect.left + containerRect.width / 2; - targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0]; - } - - // Process the drop - const isSecondPane = $container.attr("id") === "phoenix-tab-bar-2"; - const targetPaneId = isSecondPane ? "second-pane" : "first-pane"; - const draggedPath = $(draggedTab).attr("data-path"); - const targetPath = $(targetTab).attr("data-path"); - - if (dragSourcePane !== targetPaneId) { - // cross-pane drop - moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, onLeftSide); - } else { - // same pane drop - moveWorkingSetItem(targetPaneId, draggedPath, targetPath, onLeftSide); - } - } - - cleanupDragState(); - } + function getDropPosition(targetTab, mouseX) { + const rect = targetTab.getBoundingClientRect(); + const midpoint = rect.left + rect.width / 2; + return mouseX < midpoint; } /** - * enhanced auto-scroll function for container when the mouse is near its left or right edge - * creates a smooth scrolling effect with speed based on proximity to the edge - * - * @param {HTMLElement} container - The scrollable container element - * @param {Number} mouseX - The current mouse X coordinate + * find the closest tab to mouse position + * @param {jQuery} container - The tab container element + * @param {number} mouseX - The mouse X coordinate + * @returns {Element|null} The closest tab element or null */ - function autoScrollContainer(container, mouseX) { - const rect = container.getBoundingClientRect(); - const edgeThreshold = 100; // Increased threshold for edge detection (was 50) - const outerThreshold = 50; // Distance outside the container that still triggers scrolling - - // Calculate distance from edges, allowing for mouse to be slightly outside bounds - const distanceFromLeft = mouseX - (rect.left - outerThreshold); - const distanceFromRight = rect.right + outerThreshold - mouseX; - - // Determine scroll speed based on distance from edge (closer = faster scroll) - let scrollSpeed = 0; - - // Only activate scrolling when within the threshold (including the outer buffer) - if (distanceFromLeft < edgeThreshold + outerThreshold && mouseX < rect.right) { - // Non-linear scroll speed: faster as you get closer to the edge - scrollSpeed = -Math.pow(1 - distanceFromLeft / (edgeThreshold + outerThreshold), 2) * 25; - } else if (distanceFromRight < edgeThreshold + outerThreshold && mouseX > rect.left) { - scrollSpeed = Math.pow(1 - distanceFromRight / (edgeThreshold + outerThreshold), 2) * 25; - } + function findClosestTab(container, mouseX) { + const tabs = container.find(".tab").get(); + let closestTab = null; + let closestDistance = Infinity; + + for (let tab of tabs) { + if (tab === draggedTab) { + continue; + } - // Apply scrolling if needed - if (scrollSpeed !== 0) { - container.scrollLeft += scrollSpeed; + const rect = tab.getBoundingClientRect(); + const tabCenter = rect.left + rect.width / 2; + const distance = Math.abs(mouseX - tabCenter); - // If we're already at the edge, don't keep trying to scroll - if ( - (scrollSpeed < 0 && container.scrollLeft <= 0) || - (scrollSpeed > 0 && container.scrollLeft >= container.scrollWidth - container.clientWidth) - ) { - return; + if (distance < closestDistance) { + closestDistance = distance; + closestTab = tab; } } + + return closestTab; } /** - * Handle the start of a drag operation - * Stores the tab being dragged and adds visual styling - * - * @param {Event} e - The event object + * this function handles the drag start + * @param {Event} event - the event instance */ - function handleDragStart(e) { - if (draggedTab) { - cleanupDragState(); - } - - // store reference to the dragged tab + function handleDragStart(event) { draggedTab = this; - - // set data transfer (required for Firefox) - // Firefox requires data to be set for the drag operation to work - e.originalEvent.dataTransfer.effectAllowed = "move"; - e.originalEvent.dataTransfer.setData("text/html", this.innerHTML); - - // Store which pane this tab came from dragSourcePane = $(this).closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane"; - // Add dragging class for styling + // Set up drag data + event.originalEvent.dataTransfer.effectAllowed = "move"; + event.originalEvent.dataTransfer.setData("application/x-phoenix-tab", "tab-drag"); + + // Add visual styling $(this).addClass("dragging"); - // Use a timeout to let the dragging class apply before taking measurements - // This ensures visual updates are applied before we calculate positions + ensureDragIndicator(); + + // Small delay to let the dragging class take effect setTimeout(() => { - // Ensure the drag indicator is properly hidden at the start - if (dragIndicator) { - dragIndicator.hide(); - } - }, 0); + dragIndicator.hide(); + }, 10); } /** - * Handle the dragover event to enable drop - * Updates the visual indicator showing where the tab will be dropped - * - * @param {Event} e - The event object + * this function handles the drag over + * @param {Event} event - the event instance */ - function handleDragOver(e) { - if (e.preventDefault) { - e.preventDefault(); // Allows us to drop + function handleDragOver(event) { + if (!draggedTab) { + return; } - e.originalEvent.dataTransfer.dropEffect = "move"; - // Update the drag indicator position - // We need to determine if it should be on the left or right side of the target tab - const targetRect = this.getBoundingClientRect(); - const mouseX = e.originalEvent.clientX; - const midPoint = targetRect.left + targetRect.width / 2; - const onLeftSide = mouseX < midPoint; + event.preventDefault(); + event.originalEvent.dataTransfer.dropEffect = "move"; - updateDragIndicator(this, onLeftSide); + const targetTab = event.currentTarget; + if (targetTab === draggedTab) { + return; + } - return false; - } + const insertBefore = getDropPosition(targetTab, event.originalEvent.clientX); + updateDragIndicator(targetTab, insertBefore); - /** - * Handle entering a potential drop target - * Applies styling to indicate the current drop target - * - * @param {Event} e - The event object - */ - function handleDragEnter(e) { - dragOverTab = this; - $(this).addClass("drag-target"); + // Clear any existing drag-target classes + $(".tab").removeClass("drag-target"); + $(targetTab).addClass("drag-target"); } /** - * Handle leaving a potential drop target - * Removes styling when no longer hovering over a drop target - * - * @param {Event} e - The event object + * handles the container drag over (for empty space drops) + * @param {Event} event */ - function handleDragLeave(e) { - const relatedTarget = e.originalEvent.relatedTarget; - // Only remove the class if we're truly leaving this tab - // This prevents flickering when moving over child elements - if (!$(this).is(relatedTarget) && !$(this).has(relatedTarget).length) { - $(this).removeClass("drag-target"); - if (dragOverTab === this) { - dragOverTab = null; - } + function handleContainerDragOver(event) { + if (!draggedTab) { + return; } - } - /** - * Handle dropping a tab onto a target - * Moves the file in the working set to the new position - * - * @param {Event} e - The event object - */ - function handleDrop(e) { - try { - if (e.stopPropagation) { - e.stopPropagation(); // Stops browser from redirecting - } + event.preventDefault(); - // Only process the drop if the dragged tab is different from the drop target - if (draggedTab !== this) { - // Determine which pane the drop target belongs to - const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; - const targetPaneId = isSecondPane ? "second-pane" : "first-pane"; - const draggedPath = $(draggedTab).attr("data-path"); - const targetPath = $(this).attr("data-path"); - - // Determine if we're dropping to the left or right of the target - const targetRect = this.getBoundingClientRect(); - const mouseX = e.originalEvent.clientX; - const midPoint = targetRect.left + targetRect.width / 2; - const onLeftSide = mouseX < midPoint; - - // Check if dragging between different panes - if (dragSourcePane !== targetPaneId) { - // Move the tab between panes - moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, onLeftSide); - } else { - // Move within the same pane - moveWorkingSetItem(targetPaneId, draggedPath, targetPath, onLeftSide); - } - } + const container = $(this); + const mouseX = event.originalEvent.clientX; + const closestTab = findClosestTab(container, mouseX); - cleanupDragState(); - return false; - } catch (error) { - console.error("Error during tab drop operation:", error); - // Ensure cleanup happens even if there's an error - cleanupDragState(); - return false; + if (closestTab) { + const insertBefore = getDropPosition(closestTab, mouseX); + updateDragIndicator(closestTab, insertBefore); + + // Clear existing classes and add to closest + $(".tab").removeClass("drag-target"); + $(closestTab).addClass("drag-target"); + } else { + dragIndicator.hide(); } } /** - * Handle the end of a drag operation - * Cleans up classes and resets state variables - * - * @param {Event} e - The event object + * this handles the drop + * @param {Event} event */ - function handleDragEnd(e) { - setTimeout(() => { + function handleDrop(event) { + event.preventDefault(); + event.stopPropagation(); + + if (!draggedTab || this === draggedTab) { cleanupDragState(); - }, 10); + return; + } + + const targetTab = this; + const targetPaneId = $(targetTab).closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane"; + const insertBefore = getDropPosition(targetTab, event.originalEvent.clientX); + + performTabMove(targetTab, targetPaneId, insertBefore); + cleanupDragState(); } /** - * Global document event listeners to ensure drag state is always cleaned up - * This handles cases where drag operations fail or are cancelled outside - * the normal tab bar drop zones + * this function handles the drop on container (empty space) + * @param {Event} event */ - function setupGlobalDragCleanup() { - // Listen for drags ending anywhere on the document - $(document).on('dragend', function(e) { - // Only clean up if we were tracking a drag operation - if (draggedTab) { - setTimeout(() => { - cleanupDragState(); - }, 10); - } - }); + function handleContainerDrop(event) { + event.preventDefault(); - // Listen for global mouse up events to catch cancelled drags - $(document).on('mouseup', function(e) { - // If we have an active drag but mouse is released, clean up - if (draggedTab && !e.originalEvent.dataTransfer) { - setTimeout(() => { - cleanupDragState(); - }, 10); - } - }); + if (!draggedTab) { + cleanupDragState(); + return; + } - // Listen for ESC key to cancel drag operations - $(document).on('keydown', function(e) { - if (e.key === 'Escape' && draggedTab) { - cleanupDragState(); - } - }); + const container = $(this); + const mouseX = event.originalEvent.clientX; + const closestTab = findClosestTab(container, mouseX); - // Listen for page visibility changes (like alt-tab) to clean up - $(document).on('visibilitychange', function() { - if (document.hidden && draggedTab) { - cleanupDragState(); - } - }); + if (closestTab) { + const targetPaneId = container.attr("id") === "phoenix-tab-bar-2" ? "second-pane" : "first-pane"; + const insertBefore = getDropPosition(closestTab, mouseX); + performTabMove(closestTab, targetPaneId, insertBefore); + } + + cleanupDragState(); } /** - * Update the drag indicator position and visibility - * The indicator shows where the tab will be dropped - * Ensures the indicator stays within the bounds of the tab bar - * - * @param {HTMLElement} targetTab - The tab being dragged over, or null to hide - * @param {Boolean} onLeftSide - Whether the indicator should be on the left or right side + * this function performs the actual tab move + * it can be of two types: tab move in same pane, tab move between different panes + * @param {Element} targetTab - The target tab element + * @param {string} targetPaneId - The target pane ID + * @param {boolean} insertBefore - Whether to insert before the target */ - function updateDragIndicator(targetTab, onLeftSide) { - if (!targetTab) { - dragIndicator.hide(); - return; - } + function performTabMove(targetTab, targetPaneId, insertBefore) { + const draggedPath = $(draggedTab).attr("data-path"); + const targetPath = $(targetTab).attr("data-path"); - // Get the target tab's position and size - const targetRect = targetTab.getBoundingClientRect(); - - // Find the containing tab bar to ensure the indicator stays within bounds - const $tabBar = $(targetTab).closest("#phoenix-tab-bar, #phoenix-tab-bar-2"); - const tabBarRect = $tabBar[0] ? $tabBar[0].getBoundingClientRect() : null; - - if (onLeftSide) { - // Position indicator at the left edge of the target tab - // Ensure it doesn't go beyond the tab bar's left edge - const leftPos = tabBarRect ? Math.max(targetRect.left, tabBarRect.left) : targetRect.left; - dragIndicator.css({ - top: targetRect.top, - left: leftPos, - height: targetRect.height - }); + if (dragSourcePane === targetPaneId) { + // same pane move + moveWorkingSetItem(targetPaneId, draggedPath, targetPath, insertBefore); } else { - // Position indicator at the right edge of the target tab - // Ensure it doesn't go beyond the tab bar's right edge - const rightPos = tabBarRect ? Math.min(targetRect.right, tabBarRect.right) : targetRect.right; - dragIndicator.css({ - top: targetRect.top, - left: rightPos, - height: targetRect.height - }); + // different pane move + moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, insertBefore); } - dragIndicator.show(); } /** - * Move an item in the working set - * This function actually performs the reordering of tabs - * - * @param {String} paneId - The ID of the pane ("first-pane" or "second-pane") - * @param {String} draggedPath - Path of the dragged file - * @param {String} targetPath - Path of the drop target file - * @param {Boolean} beforeTarget - Whether to place before or after the target + * this function is responsible to move the tab within the pane + * @param {string} paneId - The pane ID + * @param {string} draggedPath - The path of the dragged file + * @param {string} targetPath - The path of the target file + * @param {boolean} beforeTarget - Whether to insert before the target */ function moveWorkingSetItem(paneId, draggedPath, targetPath, beforeTarget) { const workingSet = MainViewManager.getWorkingSet(paneId); @@ -647,43 +301,34 @@ define(function (require, exports, module) { } } - // Only move if we found both items if (draggedIndex !== -1 && targetIndex !== -1) { - // Calculate the new position based on whether we're inserting before or after the target let newPosition = beforeTarget ? targetIndex : targetIndex + 1; - // Adjust position if the dragged item is before the target - // This is necessary because removing the dragged item will shift all following items if (draggedIndex < newPosition) { newPosition--; } - // Perform the actual move in the MainViewManager MainViewManager._moveWorkingSetItem(paneId, draggedIndex, newPosition); } } /** - * Move a tab from one pane to another - * This function handles cross-pane drag and drop operations - * - * @param {String} sourcePaneId - The ID of the source pane ("first-pane" or "second-pane") - * @param {String} targetPaneId - The ID of the target pane ("first-pane" or "second-pane") - * @param {String} draggedPath - Path of the dragged file - * @param {String} targetPath - Path of the drop target file (in the target pane) - * @param {Boolean} beforeTarget - Whether to place before or after the target + * this function is responsible to move the tab in between different panes + * @param {string} sourcePaneId - The source pane ID + * @param {string} targetPaneId - The target pane ID + * @param {string} draggedPath - The path of the dragged file + * @param {string} targetPath - The path of the target file + * @param {boolean} beforeTarget - Whether to insert before the target */ function moveTabBetweenPanes(sourcePaneId, targetPaneId, draggedPath, targetPath, beforeTarget) { try { const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId); const targetWorkingSet = MainViewManager.getWorkingSet(targetPaneId); - let draggedIndex = -1; - let targetIndex = -1; let draggedFile = null; + let targetIndex = -1; // Find the dragged file and its index in the source pane for (let i = 0; i < sourceWorkingSet.length; i++) { if (sourceWorkingSet[i].fullPath === draggedPath) { - draggedIndex = i; draggedFile = sourceWorkingSet[i]; break; } @@ -697,222 +342,205 @@ define(function (require, exports, module) { } } - // Only continue if we found the dragged file - if (draggedIndex !== -1 && draggedFile) { - // Check if the dragged file is currently active in the source pane - const currentActiveFileInSource = MainViewManager.getCurrentlyViewedFile(sourcePaneId); - const isActiveFileBeingMoved = currentActiveFileInSource && - currentActiveFileInSource.fullPath === draggedPath; - - // If the active file is being moved and there are other files in the source pane, - // switch to another file first to prevent placeholder creation - if (isActiveFileBeingMoved && sourceWorkingSet.length > 1) { - // Find another file to make active (prefer the next file, or previous if this is the last) - let newActiveIndex = draggedIndex + 1; - if (newActiveIndex >= sourceWorkingSet.length) { - newActiveIndex = draggedIndex - 1; - } - - if (newActiveIndex >= 0 && newActiveIndex < sourceWorkingSet.length) { - const newActiveFile = sourceWorkingSet[newActiveIndex]; - // Open the new active file in the source pane before removing the dragged file - CommandManager.execute(Commands.FILE_OPEN, { - fullPath: newActiveFile.fullPath, - paneId: sourcePaneId - }); - } - } - - // Remove the file from source pane + if (draggedFile) { + // Close in source pane CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: sourcePaneId }); - // Calculate where to add it in the target pane - let targetInsertIndex; - - if (targetIndex !== -1) { - // We have a specific target index to aim for - targetInsertIndex = beforeTarget ? targetIndex : targetIndex + 1; - } else { - // No specific target, add to end of the working set - targetInsertIndex = targetWorkingSet.length; - } - - // Add to the target pane at the calculated position - MainViewManager.addToWorkingSet(targetPaneId, draggedFile, targetInsertIndex); + // calculate where to insert it in the target pane + const insertIndex = + targetIndex !== -1 ? (beforeTarget ? targetIndex : targetIndex + 1) : targetWorkingSet.length; - // we always need to make the dragged tab active in the target pane when moving between panes + // Add to target pane + MainViewManager.addToWorkingSet(targetPaneId, draggedFile, insertIndex); CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: targetPaneId }); } } catch (error) { console.error("Error during cross-pane tab move:", error); - // Even if there's an error, ensure the drag state is cleaned up - cleanupDragState(); } } /** - * Initialize drop targets for empty panes - * This creates invisible drop zones when a pane has no files and thus no tab bar + * this function handles the drag end event + * @param {Event} event - the event instance */ - function initEmptyPaneDropTargets() { - // get the references to the editor holders (these are always present, even when empty) - const $firstPaneHolder = $("#first-pane .pane-content"); - const $secondPaneHolder = $("#second-pane .pane-content"); - - // handle the drop events on empty panes - setupEmptyPaneDropTarget($firstPaneHolder, "first-pane"); - setupEmptyPaneDropTarget($secondPaneHolder, "second-pane"); + function handleDragEnd(event) { + // Small delay to ensure other events finish + setTimeout(() => { + cleanupDragState(); + }, 50); } /** - * sets up the whole pane as a drop target when it has no tabs - * - * @param {jQuery} $paneHolder - The jQuery object for the pane content area - * @param {String} paneId - The ID of the pane ("first-pane" or "second-pane") + * this function handles drag leave + * @param {Event} event - the event instance */ - function setupEmptyPaneDropTarget($paneHolder, paneId) { - // remove if any existing handlers to prevent duplicates - $paneHolder.off("dragover dragenter dragleave drop"); + function handleDragLeave(event) { + // Only remove styling if truly leaving the element + const relatedTarget = event.originalEvent.relatedTarget; + if (!$(this).is(relatedTarget) && !$(this).has(relatedTarget).length) { + $(this).removeClass("drag-target"); + } + } - // Handle drag over empty pane - $paneHolder.on("dragover dragenter", function (e) { - if (draggedTab) { - e.preventDefault(); - e.stopPropagation(); + /** + * Setup drag handlers for a tab bar + * @param {string} tabBarSelector - The jQuery selector for the tab bar + */ + function setupTabBarDragHandlers(tabBarSelector) { + const $tabBar = $(tabBarSelector); - // add visual indicator that this is a drop target [refer to Extn-TabBar.less] - $(this).addClass("empty-pane-drop-target"); + // Remove existing handlers to prevent duplicates + $tabBar.off("dragstart dragover dragenter dragleave drop dragend"); - // set the drop effect - if (e.originalEvent && e.originalEvent.dataTransfer) { - e.originalEvent.dataTransfer.dropEffect = "move"; - } - } - }); + // add the handlers + $tabBar.on("dragstart", ".tab", handleDragStart); + $tabBar.on("dragover", ".tab", handleDragOver); + $tabBar.on("dragleave", ".tab", handleDragLeave); + $tabBar.on("drop", ".tab", handleDrop); + $tabBar.on("dragend", ".tab", handleDragEnd); - // handle leaving an empty pane drop target - $paneHolder.on("dragleave", function (e) { - $(this).removeClass("empty-pane-drop-target"); - }); + // container level handlers (for empty space) + $tabBar.on("dragover", handleContainerDragOver); + $tabBar.on("drop", handleContainerDrop); - // Handle drop on empty pane - $paneHolder.on("drop", function (e) { - const $tabBar = paneId === "first-pane" ? $("#phoenix-tab-bar") : $("#phoenix-tab-bar-2"); - const isEmptyPane = !$tabBar.length || $tabBar.is(":hidden") || $tabBar.children(".tab").length === 0; + // Make tabs draggable + $tabBar.find(".tab").attr("draggable", true); + } - if (draggedTab) { - e.preventDefault(); - e.stopPropagation(); + /** + * setup the pane drop targets + */ + function setupPaneDropTargets() { + const panes = ["#first-pane .pane-content", "#second-pane .pane-content"]; + + panes.forEach((paneSelector, index) => { + const paneId = index === 0 ? "first-pane" : "second-pane"; + const $pane = $(paneSelector); + + $pane.off("dragover dragenter dragleave drop"); - // remove the highlight - $(this).removeClass("empty-pane-drop-target"); + $pane.on("dragover dragenter", function (event) { + if (draggedTab && dragSourcePane !== paneId) { + // Only allow cross-pane drops + event.preventDefault(); - // get the dragged file path - const draggedPath = $(draggedTab).attr("data-path"); + // Check if this pane is empty for visual styling + const tabBarId = paneId === "first-pane" ? "#phoenix-tab-bar" : "#phoenix-tab-bar-2"; + const $tabBar = $(tabBarId); + const isEmptyPane = + !$tabBar.length || $tabBar.is(":hidden") || $tabBar.children(".tab").length === 0; - // Determine source pane - const sourcePaneId = - $(draggedTab).closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane"; + if (isEmptyPane) { + $(this).addClass("empty-pane-drop-target"); + } else { + // Add a different class for non-empty panes + $(this).addClass("pane-drop-target"); + } + } + }); + + $pane.on("dragleave", function (event) { + $(this).removeClass("empty-pane-drop-target pane-drop-target"); + }); - // we only want to proceed if we're not dropping in the same pane or, - // allow if it's the same pane with existing tabs - if (sourcePaneId !== paneId || !isEmptyPane) { - const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId); + $pane.on("drop", function (event) { + if (draggedTab && dragSourcePane !== paneId) { + event.preventDefault(); + $(this).removeClass("empty-pane-drop-target pane-drop-target"); + + const draggedPath = $(draggedTab).attr("data-path"); + const sourceWorkingSet = MainViewManager.getWorkingSet(dragSourcePane); const targetWorkingSet = MainViewManager.getWorkingSet(paneId); let draggedFile = null; - // Find the dragged file in the source pane - for (let i = 0; i < sourceWorkingSet.length; i++) { - if (sourceWorkingSet[i].fullPath === draggedPath) { - draggedFile = sourceWorkingSet[i]; + // Find dragged file + for (let file of sourceWorkingSet) { + if (file.fullPath === draggedPath) { + draggedFile = file; break; } } if (draggedFile) { - if (sourcePaneId !== paneId) { - // Check if the dragged file is currently active in the source pane - const currentActiveFileInSource = MainViewManager.getCurrentlyViewedFile(sourcePaneId); - const isActiveFileBeingMoved = currentActiveFileInSource && - currentActiveFileInSource.fullPath === draggedPath; - - // If the active file is being moved and there are other files in the source pane, - // switch to another file first to prevent placeholder creation - if (isActiveFileBeingMoved && sourceWorkingSet.length > 1) { - // Find another file to make active - let draggedIndex = -1; - for (let i = 0; i < sourceWorkingSet.length; i++) { - if (sourceWorkingSet[i].fullPath === draggedPath) { - draggedIndex = i; - break; - } - } - - if (draggedIndex !== -1) { - let newActiveIndex = draggedIndex + 1; - if (newActiveIndex >= sourceWorkingSet.length) { - newActiveIndex = draggedIndex - 1; - } - - if (newActiveIndex >= 0 && newActiveIndex < sourceWorkingSet.length) { - const newActiveFile = sourceWorkingSet[newActiveIndex]; - // Open the new active file in the source pane before removing the dragged file - CommandManager.execute(Commands.FILE_OPEN, { - fullPath: newActiveFile.fullPath, - paneId: sourcePaneId - }); - } - } - } + // Close in source pane + CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: dragSourcePane }); - // If different panes, close in source pane - CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: sourcePaneId }); - - // For non-empty panes, find current active file to place tab after it - if (!isEmptyPane && targetWorkingSet.length > 0) { - const currentActiveFile = MainViewManager.getCurrentlyViewedFile(paneId); - - if (currentActiveFile) { - // Find index of current active file - let targetIndex = -1; - for (let i = 0; i < targetWorkingSet.length; i++) { - if (targetWorkingSet[i].fullPath === currentActiveFile.fullPath) { - targetIndex = i; - break; - } - } + // Check if target pane is empty or has files + const tabBarId = paneId === "first-pane" ? "#phoenix-tab-bar" : "#phoenix-tab-bar-2"; + const $tabBar = $(tabBarId); + const isEmptyPane = + !$tabBar.length || $tabBar.is(":hidden") || $tabBar.children(".tab").length === 0; - if (targetIndex !== -1) { - // Add after current active file - MainViewManager.addToWorkingSet(paneId, draggedFile, targetIndex + 1); - } else { - // Fallback to adding at the end - MainViewManager.addToWorkingSet(paneId, draggedFile); + if (isEmptyPane) { + // Empty pane: just add the file + MainViewManager.addToWorkingSet(paneId, draggedFile); + } else { + // Non-empty pane: add after the currently active file + const currentActiveFile = MainViewManager.getCurrentlyViewedFile(paneId); + if (currentActiveFile) { + // Find index of current active file and insert after it + let targetIndex = -1; + for (let i = 0; i < targetWorkingSet.length; i++) { + if (targetWorkingSet[i].fullPath === currentActiveFile.fullPath) { + targetIndex = i; + break; } - } else { - // No active file, add to the end - MainViewManager.addToWorkingSet(paneId, draggedFile); } + MainViewManager.addToWorkingSet(paneId, draggedFile, targetIndex + 1); } else { - // Empty pane, just add it + // Fallback: add to end MainViewManager.addToWorkingSet(paneId, draggedFile); } - - // Open file in target pane - CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: paneId }); - } else if (isEmptyPane) { - // Same pane, empty pane case (should never happen but kept for safety) - MainViewManager.addToWorkingSet(paneId, draggedFile); - CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: paneId }); } + + // Open file in target pane + CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: paneId }); } + + cleanupDragState(); } + }); + }); + } + + /** + * clean up the handlers + */ + function setupGlobalCleanup() { + // Clean up on document level events + $(document).on("dragend mouseup", function () { + if (draggedTab) { + setTimeout(() => cleanupDragState(), 100); + } + }); + // Clean up on escape key + $(document).on("keydown", function (event) { + if (event.key === "Escape" && draggedTab) { cleanupDragState(); } }); } + /** + * Initialize drag and drop functionality + * @param {string} firstPaneSelector - The selector for the first pane tab bar + * @param {string} secondPaneSelector - The selector for the second pane tab bar + */ + function init(firstPaneSelector, secondPaneSelector) { + // setup drag handlers for both tab bars + if (firstPaneSelector) { + setupTabBarDragHandlers(firstPaneSelector); + } + if (secondPaneSelector) { + setupTabBarDragHandlers(secondPaneSelector); + } + + setupPaneDropTargets(); + setupGlobalCleanup(); + ensureDragIndicator(); + } + module.exports = { init }; From 485787377afc77dd22bbfbf81a00796b72498170 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 4 Sep 2025 15:46:36 +0530 Subject: [PATCH 3/8] fix: typo in comment --- src/extensionsIntegrated/TabBar/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 2e571fb5d7..264c890ade 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -511,7 +511,7 @@ define(function (require, exports, module) { // delegate event handling for both tab bars $(document).on("mousedown", ".phoenix-tab-bar .tab", function (event) { - // to prevent right clicks activate the mousedown event + // to prevent right-clicks from activating the mousedown event if (event.button === 2) { return; } if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { From c946970c981fd010dfb7c410252592ec32e0b219 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 4 Sep 2025 19:33:42 +0530 Subject: [PATCH 4/8] fix: clicking twice on overflow button doesn't hide the dropdown --- src/extensionsIntegrated/TabBar/overflow.js | 73 +++++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js index 169e6e63fd..77f1e04c76 100644 --- a/src/extensionsIntegrated/TabBar/overflow.js +++ b/src/extensionsIntegrated/TabBar/overflow.js @@ -35,6 +35,8 @@ define(function (require, exports, module) { const EditorManager = require("editor/EditorManager"); const FileSystem = require("filesystem/FileSystem"); + // holds the dropdown instance + let $dropdown = null; /** * This function determines which tabs are hidden in the tab bar due to overflow @@ -130,15 +132,12 @@ define(function (require, exports, module) { function showOverflowMenu(paneId, x, y) { const hiddenTabs = _getListOfHiddenTabs(paneId); - // first, remove any existing dropdown menus to prevent duplicates - $(".dropdown-overflow-menu").remove(); - // Create a map to track tabs that are being closed // Using paths as keys for quick lookup const closingTabPaths = {}; // create the dropdown - const dropdown = new DropdownButton.DropdownButton("", hiddenTabs, function (item, index) { + $dropdown = new DropdownButton.DropdownButton("", hiddenTabs, function (item, index) { const iconHtml = item.$icon[0].outerHTML; // the file icon const dirtyHtml = item.isDirty ? '' @@ -169,15 +168,14 @@ define(function (require, exports, module) { }; }); - // add the custom classes for styling the dropdown - dropdown.dropdownExtraClasses = "dropdown-overflow-menu"; - dropdown.$button.addClass("btn-overflow-tabs"); + // add custom class to separate overflow dropdown from regular ones + $dropdown.dropdownExtraClasses = "dropdown-overflow-menu"; // appending to document body. we'll position this with absolute positioning - $("body").append(dropdown.$button); + $("body").append($dropdown.$button); // position the dropdown where the user clicked - dropdown.$button.css({ + $dropdown.$button.css({ position: "absolute", left: x + "px", top: y + "px", @@ -203,10 +201,10 @@ define(function (require, exports, module) { e.preventDefault(); }); - dropdown.showDropdown(); + $dropdown.showDropdown(); // handle the option selection - dropdown.on("select", function (e, item, index) { + $dropdown.on("select", function (e, item, index) { // check if this tab was marked for closing if (closingTabPaths[item.path]) { // this tab is being closed, so handle the close operation @@ -240,15 +238,9 @@ define(function (require, exports, module) { } }); - // clean up when the dropdown is closed - dropdown.$button.on("dropdown-closed", function () { - $(document).off("mousedown", ".tab-close-icon-overflow"); - dropdown.$button.remove(); - }); - // a button was getting displayed on the screen wherever a click was made. not sure why // but this fixes it - dropdown.$button.css({ + $dropdown.$button.css({ display: "none" }); } @@ -335,29 +327,52 @@ define(function (require, exports, module) { } } + /** + * to close the overflow button's dropdown + */ + function _closeDropdown() { + if ($dropdown) { + if ($dropdown.$button) { + $dropdown.$button.remove(); + } + $dropdown = null; + } + } + + /** + * this function gets called when the overflow button gets clicked + * it shows/closes the dropdown as required + * @param {Event} e - the event instance + * @param {String} paneId - the pane id "first-pane" or "second-pane" + */ + function _handleOverflowButtonClick(e, paneId) { + e.stopPropagation(); + $dropdown ? _closeDropdown() : showOverflowMenu(paneId, e.pageX, e.pageY); + } /** - * To setup the handlers for the overflow menu + * initialize the handling of the overflow buttons + * this also registers the event handlers */ - function setupOverflowHandlers() { + function init() { + // when clicked anywhere on the page we want to close the dropdown + // except the overflow-buttons + $("html").on("click", function (e) { + if ($(e.target).closest("#overflow-button, #overflow-button-2").length) { return; } + _closeDropdown(); + }); + // handle when the overflow button is clicked for the first pane $(document).on("click", "#overflow-button", function (e) { - e.stopPropagation(); - showOverflowMenu("first-pane", e.pageX, e.pageY); + _handleOverflowButtonClick(e, "first-pane"); }); // for second pane $(document).on("click", "#overflow-button-2", function (e) { - e.stopPropagation(); - showOverflowMenu("second-pane", e.pageX, e.pageY); + _handleOverflowButtonClick(e, "second-pane"); }); } - // initialize the handling of the overflow buttons - function init() { - setupOverflowHandlers(); - } - module.exports = { init, toggleOverflowVisibility, From 6e13d7790d81c48fc987d899bc16f7cd0f92879c Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 4 Sep 2025 20:16:00 +0530 Subject: [PATCH 5/8] fix: mousedown event not getting trigged when dropdown exists, moved everything to click handler --- src/extensionsIntegrated/TabBar/main.js | 56 +++++++------------------ 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 264c890ade..8bea1d4fdf 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -487,60 +487,36 @@ define(function (require, exports, module) { function handleTabClick() { // delegate event handling for both tab bars $(document).on("click", ".phoenix-tab-bar .tab", function (event) { - // check if the clicked element is the close button - if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { - // Get the file path from the data-path attribute of the parent tab - const filePath = $(this).attr("data-path"); - - if (filePath) { - // determine the pane inside which the tab belongs - const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; - const paneId = isSecondPane ? "second-pane" : "first-pane"; - - // get the file object - const fileObj = FileSystem.getFileForPath(filePath); - // close the file - CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); + // Get the file path from the data-path attribute of the parent tab + const filePath = $(this).attr("data-path"); + if (!filePath) { return; } - // Prevent default behavior - event.preventDefault(); - event.stopPropagation(); - } - } - }); + // determine the pane inside which the tab belongs + const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; + const paneId = isSecondPane ? "second-pane" : "first-pane"; - // delegate event handling for both tab bars - $(document).on("mousedown", ".phoenix-tab-bar .tab", function (event) { - // to prevent right-clicks from activating the mousedown event - if (event.button === 2) { return; } + // get the file object + const fileObj = FileSystem.getFileForPath(filePath); + // check if the clicked element is the close button if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { - return; - } - // Get the file path from the data-path attribute - const filePath = $(this).attr("data-path"); + event.preventDefault(); + event.stopPropagation(); - if (filePath) { - // determine the pane inside which the tab belongs - const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; - const paneId = isSecondPane ? "second-pane" : "first-pane"; + CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); // close the file + } else { // open the clicked tab const currentActivePane = MainViewManager.getActivePaneId(); const isPaneActive = paneId === currentActivePane; const currentFile = MainViewManager.getCurrentlyViewedFile(currentActivePane); - // Check if this is a placeholder tab + // if the clicked tab is a placeholder tab, we add it to the working set if ($(this).hasClass("placeholder")) { - // Add the file to the working set when placeholder tab is clicked - const fileObj = FileSystem.getFileForPath(filePath); MainViewManager.addToWorkingSet(paneId, fileObj); } - if (isPaneActive && currentFile && currentFile.fullPath === filePath) { - return; - } + // clicked tab is already active, don't do anything + if (isPaneActive && currentFile && currentFile.fullPath === filePath) { return; } CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath, paneId: paneId }); - - // We dont prevent default behavior here to enable drag and drop of this tab } }); From e94ea3f96092aae81321ac10b58ec17484a8e532 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 4 Sep 2025 20:22:11 +0530 Subject: [PATCH 6/8] fix: tests failing as changed tab open handler from mousedown to click --- test/spec/Extn-Tabbar-integ-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/Extn-Tabbar-integ-test.js b/test/spec/Extn-Tabbar-integ-test.js index 9292bce749..bd5840875d 100644 --- a/test/spec/Extn-Tabbar-integ-test.js +++ b/test/spec/Extn-Tabbar-integ-test.js @@ -1193,7 +1193,7 @@ define(function (require, exports, module) { async function clickTabAndVerify(filePath, description) { const $tab = getTab(filePath); expect($tab.length).toBe(1); - $tab.trigger("mousedown"); + $tab.trigger("click"); // Wait for the file to become active await awaitsFor( From 687bf0f3abc47f124107f71f96b3801c47475694 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 4 Sep 2025 21:17:04 +0530 Subject: [PATCH 7/8] fix: add localization for overflow button tooltips --- src/extensionsIntegrated/TabBar/html/tabbar-pane.html | 2 +- src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html | 2 +- src/extensionsIntegrated/TabBar/main.js | 3 +++ src/nls/root/strings.js | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html index c48160c2f7..2f9a0e0a74 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html @@ -3,7 +3,7 @@ -
+
diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html index a0a8ab597d..795598e47e 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html @@ -3,7 +3,7 @@ -
+
diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 8bea1d4fdf..241baf5eeb 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -229,6 +229,7 @@ define(function (require, exports, module) { $tabBar = $(TabBarHTML); // since we need to add the tab bar before the editor which has .not-editor class $(".pane-header").after($tabBar); + $("#overflow-button").attr("title", Strings.TABBAR_SHOW_HIDDEN_TABS); WorkspaceManager.recomputeLayout(true); updateTabs(); } else if ($paneHeader.length === 2) { @@ -239,6 +240,8 @@ define(function (require, exports, module) { // TODO: Fix bug where the tab bar gets hidden inside the editor in horizontal split $paneHeader.eq(0).after($tabBar); $paneHeader.eq(1).after($tabBar2); + $("#overflow-button").attr("title", Strings.TABBAR_SHOW_HIDDEN_TABS); + $("#overflow-button-2").attr("title", Strings.TABBAR_SHOW_HIDDEN_TABS); WorkspaceManager.recomputeLayout(true); updateTabs(); } diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index abb3b0e46c..23b764535c 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1309,6 +1309,7 @@ define({ "DESCRIPTION_TABBAR": "Set the tab bar settings.", "DESCRIPTION_SHOW_TABBAR": "true to show the tab bar, else false.", "DESCRIPTION_NUMBER_OF_TABS": "The number of tabs to show in the tab bar. Set to -1 to show all tabs", + "TABBAR_SHOW_HIDDEN_TABS": "Show hidden tabs", // Git extension "ENABLE_GIT": "Enable Git", From 33723c9d29405c0ce6cb183d77b53d0aeaeed334 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 4 Sep 2025 21:32:56 +0530 Subject: [PATCH 8/8] fix: tab clicks when switching panes not marking correct tab as active --- src/extensionsIntegrated/TabBar/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 241baf5eeb..c4d9971380 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -595,7 +595,7 @@ define(function (require, exports, module) { MainViewManager.on("paneCreate paneDestroy paneLayoutChange", createTabBar); // For active pane changes, update only the tabs - MainViewManager.on("activePaneChange", updateTabs); + MainViewManager.on("activePaneChange", debounceUpdateTabs); // For editor changes, update only the tabs. MainViewManager.on(MainViewManager.EVENT_CURRENT_FILE_CHANGE, debounceUpdateTabs);