Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 201 additions & 9 deletions src/extensionsIntegrated/TabBar/drag-drop.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,22 @@
/* eslint-disable no-invalid-this */
define(function (require, exports, module) {
const MainViewManager = require("view/MainViewManager");
const CommandManager = require("command/CommandManager");
const Commands = require("command/Commands");

/**
* 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;


/**
Expand All @@ -47,11 +51,8 @@ define(function (require, exports, module) {
* @param {String} secondPaneSelector - Selector for the second pane tab bar $("#phoenix-tab-bar-2")
*/
function init(firstPaneSelector, secondPaneSelector) {
// setup both the tab bars
setupDragForTabBar(firstPaneSelector);
setupDragForTabBar(secondPaneSelector);

// setup the container-level drag events to catch drops in empty areas and enable auto-scroll
setupContainerDrag(firstPaneSelector);
setupContainerDrag(secondPaneSelector);

Expand All @@ -60,6 +61,9 @@ define(function (require, exports, module) {
dragIndicator = $('<div class="tab-drag-indicator"></div>');
$('body').append(dragIndicator);
}

// add initialization for empty panes
initEmptyPaneDropTargets();
}


Expand Down Expand Up @@ -161,14 +165,22 @@ define(function (require, exports, module) {
// If dropping on left half, target the first tab; otherwise, target the last tab
const targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0];

// mkae sure that the draggedTab exists and isn't the same as the target
// 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 paneId = isSecondPane ? "second-pane" : "first-pane";
const targetPaneId = isSecondPane ? "second-pane" : "first-pane";
const draggedPath = $(draggedTab).attr("data-path");
const targetPath = $(targetTab).attr("data-path");
moveWorkingSetItem(paneId, draggedPath, targetPath, onLeftSide);

// 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);
}
}
}
});
Expand Down Expand Up @@ -228,6 +240,9 @@ define(function (require, exports, module) {
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
$(this).addClass('dragging');

Expand Down Expand Up @@ -311,7 +326,7 @@ define(function (require, exports, module) {
if (draggedTab !== this) {
// Determine which pane the drop target belongs to
const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0;
const paneId = isSecondPane ? "second-pane" : "first-pane";
const targetPaneId = isSecondPane ? "second-pane" : "first-pane";
const draggedPath = $(draggedTab).attr("data-path");
const targetPath = $(this).attr("data-path");

Expand All @@ -321,8 +336,14 @@ define(function (require, exports, module) {
const midPoint = targetRect.left + (targetRect.width / 2);
const onLeftSide = mouseX < midPoint;

// Move the tab in the working set
moveWorkingSetItem(paneId, draggedPath, targetPath, onLeftSide);
// 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);
}
}
return false;
}
Expand All @@ -339,6 +360,7 @@ define(function (require, exports, module) {
updateDragIndicator(null);
draggedTab = null;
dragOverTab = null;
dragSourcePane = null;

// Clear scroll interval if it exists
if (scrollInterval) {
Expand Down Expand Up @@ -418,6 +440,176 @@ define(function (require, exports, module) {
}
}

/**
* 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
*/
function moveTabBetweenPanes(sourcePaneId, targetPaneId, draggedPath, targetPath, beforeTarget) {
const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId);
const targetWorkingSet = MainViewManager.getWorkingSet(targetPaneId);

let draggedIndex = -1;
let targetIndex = -1;
let draggedFile = null;

// 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;
}
}

// Find the target index in the target pane
for (let i = 0; i < targetWorkingSet.length; i++) {
if (targetWorkingSet[i].fullPath === targetPath) {
targetIndex = i;
break;
}
}

// Only continue if we found the dragged file
if (draggedIndex !== -1 && draggedFile) {
// Remove the file from 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);

// If the tab was the active one in the source pane,
// make it active in the target pane too
const activeFile = MainViewManager.getCurrentlyViewedFile(sourcePaneId);
if (activeFile && activeFile.fullPath === draggedPath) {
// Open the file in the target pane and make it active
CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: targetPaneId });
}
}
}

/**
* Initialize drop targets for empty panes
* This creates invisible drop zones when a pane has no files and thus no tab bar
*/
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");
}


/**
* 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")
*/
function setupEmptyPaneDropTarget($paneHolder, paneId) {
// remove if any existing handlers to prevent duplicates
$paneHolder.off("dragover dragenter dragleave drop");

// Handle drag over empty pane
$paneHolder.on("dragover dragenter", function (e) {
// we only want to process if this pane is empty (has no tab bar or has hidden tab bar)
const $tabBar = paneId === "first-pane" ? $("#phoenix-tab-bar") : $("#phoenix-tab-bar-2");
const isEmptyPane = !$tabBar.length || $tabBar.is(":hidden") || $tabBar.children(".tab").length === 0;

if (isEmptyPane && draggedTab) {
e.preventDefault();
e.stopPropagation();

// add visual indicator that this is a drop target [refer to Extn-TabBar.less]
$(this).addClass("empty-pane-drop-target");

// set the drop effect
e.originalEvent.dataTransfer.dropEffect = 'move';
}
});

// handle leaving an empty pane drop target
$paneHolder.on("dragleave", function (e) {
$(this).removeClass("empty-pane-drop-target");
});

// 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;

if (isEmptyPane && draggedTab) {
e.preventDefault();
e.stopPropagation();

// remove the highlight
$(this).removeClass("empty-pane-drop-target");

// get the dragged file path
const draggedPath = $(draggedTab).attr("data-path");

// Determine source pane
const sourcePaneId = $(draggedTab)
.closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane";

// we don't want to do anything if dropping in the same pane
if (sourcePaneId !== paneId) {
const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId);
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];
break;
}
}

if (draggedFile) {
// close in the source pane
CommandManager.execute(
Commands.FILE_CLOSE,
{ file: draggedFile, paneId: sourcePaneId }
);

// and open in the target pane
MainViewManager.addToWorkingSet(paneId, draggedFile);
CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: paneId });
}
}

// reset all drag state stuff
updateDragIndicator(null);
draggedTab = null;
dragOverTab = null;
dragSourcePane = null;
}
});
}


module.exports = {
init
};
Expand Down
73 changes: 57 additions & 16 deletions src/extensionsIntegrated/TabBar/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,26 +123,29 @@ define(function (require, exports, module) {
return;
}

// set up all the necessary properties
const activeEditor = EditorManager.getActiveEditor();
const activePath = activeEditor ? activeEditor.document.file.fullPath : null;
// get the current active file for this specific pane
const activeFileInPane = MainViewManager.getCurrentlyViewedFile(paneId);
const activePathInPane = activeFileInPane ? activeFileInPane.fullPath : null;

// Check if this file is active in its pane
const isActive = (entry.path === activePathInPane);

// Current active pane (used to determine whether to add the blue underline)
const currentActivePane = MainViewManager.getActivePaneId();
// if the file is the currently active file
// also verify that the tab belongs to the active pane
const isActive = (entry.path === activePath && paneId === currentActivePane);
const isDirty = Helper._isFileModified(FileSystem.getFileForPath(entry.path)); // if the file is dirty
const isPaneActive = (paneId === currentActivePane);

const isDirty = Helper._isFileModified(FileSystem.getFileForPath(entry.path));

// Create the tab element with the structure we need
// tab name is written as a separate div because it may include directory info which we style differently
// create tab with active class
const $tab = $(
`<div class="tab ${isActive ? 'active' : ''} ${isDirty ? 'dirty' : ''}"
data-path="${entry.path}"
title="${entry.path}">
<div class="tab-icon"></div>
<div class="tab-name"></div>
<div class="tab-close"><i class="fa-solid fa-times"></i></div>
</div>`);
`<div class="tab
${isActive ? 'active' : ''}
${isDirty ? 'dirty' : ''}" data-path="${entry.path}" title="${entry.path}">
<div class="tab-icon"></div>
<div class="tab-name"></div>
<div class="tab-close"><i class="fa-solid fa-times"></i></div>
</div>`
);

// Add the file icon
const $icon = Helper._getFileIcon(entry);
Expand All @@ -163,6 +166,13 @@ define(function (require, exports, module) {
$tabName.text(entry.name);
}

// only add the underline class if this is both active AND in the active pane
if (isActive && !isPaneActive) {
// if it's active but in a non-active pane, we add a special class
// to style differently in CSS to indicate that it's active but not in the active pane
$tab.addClass('active-in-inactive-pane');
}

return $tab;
}

Expand Down Expand Up @@ -517,6 +527,34 @@ define(function (require, exports, module) {
);
}

/**
* this function sets up mouse wheel scrolling functionality for the tab bars
* when the mouse wheel is scrolled up, the tab bar will scroll to the left
* when its scrolled down, the tab bar will scroll to the right
*/
function setupTabBarScrolling() {

// common handler for both the tab bars
function handleMouseWheel(e) {
// get the tab bar element that is being scrolled
const $scrolledTabBar = $(this);

// A negative deltaY means scrolling up so we need to scroll to the left,
// positive means scrolling down so we need to scroll to the right
// here we calculate the scroll amount (pixels)
// and multiply by 2.5 for increasing the scroll amount
const scrollAmount = e.originalEvent.deltaY * 2.5;

// calculate the new scroll position
const newScrollLeft = $scrolledTabBar.scrollLeft() + scrollAmount;

// apply the new scroll position
$scrolledTabBar.scrollLeft(newScrollLeft);
}

// attach the wheel event handler to both tab bars
$(document).on('wheel', '#phoenix-tab-bar, #phoenix-tab-bar-2', handleMouseWheel);
}

AppInit.appReady(function () {
_registerCommands();
Expand All @@ -538,5 +576,8 @@ define(function (require, exports, module) {

Overflow.init();
DragDrop.init($('#phoenix-tab-bar'), $('#phoenix-tab-bar-2'));

// setup the mouse wheel scrolling
setupTabBarScrolling();
});
});
Loading
Loading