Skip to content

Commit 0ba61e0

Browse files
devvaannshabose
authored andcommitted
feat: enable drag drop between panes
1 parent 6b134ee commit 0ba61e0

File tree

2 files changed

+205
-9
lines changed

2 files changed

+205
-9
lines changed

src/extensionsIntegrated/TabBar/drag-drop.js

Lines changed: 201 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,22 @@
2323
/* eslint-disable no-invalid-this */
2424
define(function (require, exports, module) {
2525
const MainViewManager = require("view/MainViewManager");
26+
const CommandManager = require("command/CommandManager");
27+
const Commands = require("command/Commands");
2628

2729
/**
2830
* These variables track the drag and drop state of tabs
2931
* draggedTab: The tab that is currently being dragged
3032
* dragOverTab: The tab that is currently being hovered over
3133
* dragIndicator: Visual indicator showing where the tab will be dropped
3234
* scrollInterval: Used for automatic scrolling when dragging near edges
35+
* dragSourcePane: To track which pane the dragged tab originated from
3336
*/
3437
let draggedTab = null;
3538
let dragOverTab = null;
3639
let dragIndicator = null;
3740
let scrollInterval = null;
41+
let dragSourcePane = null;
3842

3943

4044
/**
@@ -47,11 +51,8 @@ define(function (require, exports, module) {
4751
* @param {String} secondPaneSelector - Selector for the second pane tab bar $("#phoenix-tab-bar-2")
4852
*/
4953
function init(firstPaneSelector, secondPaneSelector) {
50-
// setup both the tab bars
5154
setupDragForTabBar(firstPaneSelector);
5255
setupDragForTabBar(secondPaneSelector);
53-
54-
// setup the container-level drag events to catch drops in empty areas and enable auto-scroll
5556
setupContainerDrag(firstPaneSelector);
5657
setupContainerDrag(secondPaneSelector);
5758

@@ -60,6 +61,9 @@ define(function (require, exports, module) {
6061
dragIndicator = $('<div class="tab-drag-indicator"></div>');
6162
$('body').append(dragIndicator);
6263
}
64+
65+
// add initialization for empty panes
66+
initEmptyPaneDropTargets();
6367
}
6468

6569

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

164-
// mkae sure that the draggedTab exists and isn't the same as the target
168+
// make sure that the draggedTab exists and isn't the same as the target
165169
if (draggedTab && targetTab && draggedTab !== targetTab) {
166170
// check which pane the container belongs to
167171
const isSecondPane = $container.attr("id") === "phoenix-tab-bar-2";
168-
const paneId = isSecondPane ? "second-pane" : "first-pane";
172+
const targetPaneId = isSecondPane ? "second-pane" : "first-pane";
169173
const draggedPath = $(draggedTab).attr("data-path");
170174
const targetPath = $(targetTab).attr("data-path");
171-
moveWorkingSetItem(paneId, draggedPath, targetPath, onLeftSide);
175+
176+
// check if we're dropping in a different pane
177+
if (dragSourcePane !== targetPaneId) {
178+
// cross-pane drop
179+
moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, onLeftSide);
180+
} else {
181+
// same pane drop
182+
moveWorkingSetItem(targetPaneId, draggedPath, targetPath, onLeftSide);
183+
}
172184
}
173185
}
174186
});
@@ -228,6 +240,9 @@ define(function (require, exports, module) {
228240
e.originalEvent.dataTransfer.effectAllowed = 'move';
229241
e.originalEvent.dataTransfer.setData('text/html', this.innerHTML);
230242

243+
// Store which pane this tab came from
244+
dragSourcePane = $(this).closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane";
245+
231246
// Add dragging class for styling
232247
$(this).addClass('dragging');
233248

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

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

324-
// Move the tab in the working set
325-
moveWorkingSetItem(paneId, draggedPath, targetPath, onLeftSide);
339+
// Check if dragging between different panes
340+
if (dragSourcePane !== targetPaneId) {
341+
// Move the tab between panes
342+
moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, onLeftSide);
343+
} else {
344+
// Move within the same pane
345+
moveWorkingSetItem(targetPaneId, draggedPath, targetPath, onLeftSide);
346+
}
326347
}
327348
return false;
328349
}
@@ -339,6 +360,7 @@ define(function (require, exports, module) {
339360
updateDragIndicator(null);
340361
draggedTab = null;
341362
dragOverTab = null;
363+
dragSourcePane = null;
342364

343365
// Clear scroll interval if it exists
344366
if (scrollInterval) {
@@ -418,6 +440,176 @@ define(function (require, exports, module) {
418440
}
419441
}
420442

443+
/**
444+
* Move a tab from one pane to another
445+
* This function handles cross-pane drag and drop operations
446+
*
447+
* @param {String} sourcePaneId - The ID of the source pane ("first-pane" or "second-pane")
448+
* @param {String} targetPaneId - The ID of the target pane ("first-pane" or "second-pane")
449+
* @param {String} draggedPath - Path of the dragged file
450+
* @param {String} targetPath - Path of the drop target file (in the target pane)
451+
* @param {Boolean} beforeTarget - Whether to place before or after the target
452+
*/
453+
function moveTabBetweenPanes(sourcePaneId, targetPaneId, draggedPath, targetPath, beforeTarget) {
454+
const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId);
455+
const targetWorkingSet = MainViewManager.getWorkingSet(targetPaneId);
456+
457+
let draggedIndex = -1;
458+
let targetIndex = -1;
459+
let draggedFile = null;
460+
461+
// Find the dragged file and its index in the source pane
462+
for (let i = 0; i < sourceWorkingSet.length; i++) {
463+
if (sourceWorkingSet[i].fullPath === draggedPath) {
464+
draggedIndex = i;
465+
draggedFile = sourceWorkingSet[i];
466+
break;
467+
}
468+
}
469+
470+
// Find the target index in the target pane
471+
for (let i = 0; i < targetWorkingSet.length; i++) {
472+
if (targetWorkingSet[i].fullPath === targetPath) {
473+
targetIndex = i;
474+
break;
475+
}
476+
}
477+
478+
// Only continue if we found the dragged file
479+
if (draggedIndex !== -1 && draggedFile) {
480+
// Remove the file from source pane
481+
CommandManager.execute(
482+
Commands.FILE_CLOSE,
483+
{ file: draggedFile, paneId: sourcePaneId }
484+
);
485+
486+
// Calculate where to add it in the target pane
487+
let targetInsertIndex;
488+
489+
if (targetIndex !== -1) {
490+
// We have a specific target index to aim for
491+
targetInsertIndex = beforeTarget ? targetIndex : targetIndex + 1;
492+
} else {
493+
// No specific target, add to end of the working set
494+
targetInsertIndex = targetWorkingSet.length;
495+
}
496+
497+
// Add to the target pane at the calculated position
498+
MainViewManager.addToWorkingSet(targetPaneId, draggedFile, targetInsertIndex);
499+
500+
// If the tab was the active one in the source pane,
501+
// make it active in the target pane too
502+
const activeFile = MainViewManager.getCurrentlyViewedFile(sourcePaneId);
503+
if (activeFile && activeFile.fullPath === draggedPath) {
504+
// Open the file in the target pane and make it active
505+
CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: targetPaneId });
506+
}
507+
}
508+
}
509+
510+
/**
511+
* Initialize drop targets for empty panes
512+
* This creates invisible drop zones when a pane has no files and thus no tab bar
513+
*/
514+
function initEmptyPaneDropTargets() {
515+
// get the references to the editor holders (these are always present, even when empty)
516+
const $firstPaneHolder = $("#first-pane .pane-content");
517+
const $secondPaneHolder = $("#second-pane .pane-content");
518+
519+
// handle the drop events on empty panes
520+
setupEmptyPaneDropTarget($firstPaneHolder, "first-pane");
521+
setupEmptyPaneDropTarget($secondPaneHolder, "second-pane");
522+
}
523+
524+
525+
/**
526+
* sets up the whole pane as a drop target when it has no tabs
527+
*
528+
* @param {jQuery} $paneHolder - The jQuery object for the pane content area
529+
* @param {String} paneId - The ID of the pane ("first-pane" or "second-pane")
530+
*/
531+
function setupEmptyPaneDropTarget($paneHolder, paneId) {
532+
// remove if any existing handlers to prevent duplicates
533+
$paneHolder.off("dragover dragenter dragleave drop");
534+
535+
// Handle drag over empty pane
536+
$paneHolder.on("dragover dragenter", function (e) {
537+
// we only want to process if this pane is empty (has no tab bar or has hidden tab bar)
538+
const $tabBar = paneId === "first-pane" ? $("#phoenix-tab-bar") : $("#phoenix-tab-bar-2");
539+
const isEmptyPane = !$tabBar.length || $tabBar.is(":hidden") || $tabBar.children(".tab").length === 0;
540+
541+
if (isEmptyPane && draggedTab) {
542+
e.preventDefault();
543+
e.stopPropagation();
544+
545+
// add visual indicator that this is a drop target [refer to Extn-TabBar.less]
546+
$(this).addClass("empty-pane-drop-target");
547+
548+
// set the drop effect
549+
e.originalEvent.dataTransfer.dropEffect = 'move';
550+
}
551+
});
552+
553+
// handle leaving an empty pane drop target
554+
$paneHolder.on("dragleave", function (e) {
555+
$(this).removeClass("empty-pane-drop-target");
556+
});
557+
558+
// Handle drop on empty pane
559+
$paneHolder.on("drop", function (e) {
560+
const $tabBar = paneId === "first-pane" ? $("#phoenix-tab-bar") : $("#phoenix-tab-bar-2");
561+
const isEmptyPane = !$tabBar.length || $tabBar.is(":hidden") || $tabBar.children(".tab").length === 0;
562+
563+
if (isEmptyPane && draggedTab) {
564+
e.preventDefault();
565+
e.stopPropagation();
566+
567+
// remove the highlight
568+
$(this).removeClass("empty-pane-drop-target");
569+
570+
// get the dragged file path
571+
const draggedPath = $(draggedTab).attr("data-path");
572+
573+
// Determine source pane
574+
const sourcePaneId = $(draggedTab)
575+
.closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane";
576+
577+
// we don't want to do anything if dropping in the same pane
578+
if (sourcePaneId !== paneId) {
579+
const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId);
580+
let draggedFile = null;
581+
582+
// Find the dragged file in the source pane
583+
for (let i = 0; i < sourceWorkingSet.length; i++) {
584+
if (sourceWorkingSet[i].fullPath === draggedPath) {
585+
draggedFile = sourceWorkingSet[i];
586+
break;
587+
}
588+
}
589+
590+
if (draggedFile) {
591+
// close in the source pane
592+
CommandManager.execute(
593+
Commands.FILE_CLOSE,
594+
{ file: draggedFile, paneId: sourcePaneId }
595+
);
596+
597+
// and open in the target pane
598+
MainViewManager.addToWorkingSet(paneId, draggedFile);
599+
CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: paneId });
600+
}
601+
}
602+
603+
// reset all drag state stuff
604+
updateDragIndicator(null);
605+
draggedTab = null;
606+
dragOverTab = null;
607+
dragSourcePane = null;
608+
}
609+
});
610+
}
611+
612+
421613
module.exports = {
422614
init
423615
};

src/styles/Extn-TabBar.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,4 +364,8 @@
364364

365365
.dark .tab-close-icon-overflow .fa-solid.fa-times:hover {
366366
color: #FFF;
367+
}
368+
369+
.empty-pane-drop-target {
370+
border: 2px dashed #6db6ff !important;
367371
}

0 commit comments

Comments
 (0)