-
-
Notifications
You must be signed in to change notification settings - Fork 193
Expand file tree
/
Copy pathdrag-drop.js
More file actions
919 lines (795 loc) · 38.8 KB
/
drag-drop.js
File metadata and controls
919 lines (795 loc) · 38.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/
/* This file houses the functionality for dragging and dropping tabs */
/* 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;
/**
* this function is responsible to make sure that all the drag state is properly cleaned up
* it is needed to make sure that the tab bar doesn't get unresponsive
* because of handlers not being attached properly
*/
function cleanupDragState() {
$(".tab").removeClass("dragging drag-target");
$(".empty-pane-drop-target").removeClass("empty-pane-drop-target");
// this is to make sure that the drag indicator is hidden and remove any inline styles
if (dragIndicator) {
dragIndicator.hide().css({
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);
}
/**
* 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")
*/
function init(firstPaneSelector, secondPaneSelector) {
setupDragForTabBar(firstPaneSelector);
setupDragForTabBar(secondPaneSelector);
setupContainerDrag(firstPaneSelector);
setupContainerDrag(secondPaneSelector);
// Create drag indicator element if it doesn't exist
if (!dragIndicator) {
dragIndicator = $('<div class="tab-drag-indicator"></div>');
$("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
*/
function setupDragForTabBar(tabBarSelector) {
const $tabs = $(tabBarSelector).find(".tab");
// 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);
}
/**
* 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
*/
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 = $('<div id="tab-drag-extended-zone"></div>').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();
}
}
/**
* 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
*/
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;
}
// Apply scrolling if needed
if (scrollSpeed !== 0) {
container.scrollLeft += scrollSpeed;
// 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;
}
}
}
/**
* Handle the start of a drag operation
* Stores the tab being dragged and adds visual styling
*
* @param {Event} e - The event object
*/
function handleDragStart(e) {
if (draggedTab) {
cleanupDragState();
}
// store reference to the dragged tab
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
$(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
setTimeout(() => {
// Ensure the drag indicator is properly hidden at the start
if (dragIndicator) {
dragIndicator.hide();
}
}, 0);
}
/**
* Handle the dragover event to enable drop
* Updates the visual indicator showing where the tab will be dropped
*
* @param {Event} e - The event object
*/
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault(); // Allows us to drop
}
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;
updateDragIndicator(this, onLeftSide);
return false;
}
/**
* 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");
}
/**
* Handle leaving a potential drop target
* Removes styling when no longer hovering over a drop target
*
* @param {Event} e - The event object
*/
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;
}
}
}
/**
* 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
}
// 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);
}
}
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;
}
}
/**
* Handle the end of a drag operation
* Cleans up classes and resets state variables
*
* @param {Event} e - The event object
*/
function handleDragEnd(e) {
setTimeout(() => {
cleanupDragState();
}, 10);
}
/**
* 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
*/
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);
}
});
// 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);
}
});
// Listen for ESC key to cancel drag operations
$(document).on('keydown', function(e) {
if (e.key === 'Escape' && draggedTab) {
cleanupDragState();
}
});
// Listen for page visibility changes (like alt-tab) to clean up
$(document).on('visibilitychange', function() {
if (document.hidden && draggedTab) {
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
*/
function updateDragIndicator(targetTab, onLeftSide) {
if (!targetTab) {
dragIndicator.hide();
return;
}
// 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
});
} 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
});
}
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
*/
function moveWorkingSetItem(paneId, draggedPath, targetPath, beforeTarget) {
const workingSet = MainViewManager.getWorkingSet(paneId);
let draggedIndex = -1;
let targetIndex = -1;
// Find the indices of both the dragged item and the target item
for (let i = 0; i < workingSet.length; i++) {
if (workingSet[i].fullPath === draggedPath) {
draggedIndex = i;
}
if (workingSet[i].fullPath === targetPath) {
targetIndex = i;
}
}
// 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
*/
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;
// 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) {
// 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
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);
// we always need to make the dragged tab active in the target pane when moving between panes
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
*/
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) {
if (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
if (e.originalEvent && e.originalEvent.dataTransfer) {
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 (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 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);
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];
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
});
}
}
}
// 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;
}
}
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);
}
} else {
// No active file, add to the end
MainViewManager.addToWorkingSet(paneId, draggedFile);
}
} else {
// Empty pane, just add it
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 });
}
}
}
cleanupDragState();
}
});
}
module.exports = {
init
};
});