-
-
Notifications
You must be signed in to change notification settings - Fork 193
Expand file tree
/
Copy pathdrag-drop.js
More file actions
616 lines (524 loc) · 23.7 KB
/
drag-drop.js
File metadata and controls
616 lines (524 loc) · 23.7 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
/*
* 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;
/**
* 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();
}
/**
* 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);
// When dragging over the container but not directly over a tab element
$container.on("dragover", function (e) {
if (e.preventDefault) {
e.preventDefault();
}
// 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, e.originalEvent.clientX);
} 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);
}
}
});
// handle drop on the container (empty space)
$container.on("drop", function (e) {
if (e.preventDefault) {
e.preventDefault();
}
// hide the drag indicator
updateDragIndicator(null);
// 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);
}
}
}
});
}
/**
* 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 = 50; // teh threshold distance from the edge
// Calculate distance from edges
const distanceFromLeft = mouseX - rect.left;
const distanceFromRight = rect.right - mouseX;
// Determine scroll speed based on distance from edge (closer = faster scroll)
let scrollSpeed = 0;
if (distanceFromLeft < edgeThreshold) {
// exponential scroll speed: faster as you get closer to the edge
scrollSpeed = -Math.pow(1 - (distanceFromLeft / edgeThreshold), 2) * 15;
} else if (distanceFromRight < edgeThreshold) {
scrollSpeed = Math.pow(1 - (distanceFromRight / edgeThreshold), 2) * 15;
}
// 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) {
// 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(() => {
updateDragIndicator(null);
}, 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) {
if (e.stopPropagation) {
e.stopPropagation(); // Stops browser from redirecting
}
updateDragIndicator(null);
// 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);
}
}
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) {
$(".tab").removeClass('dragging drag-target');
updateDragIndicator(null);
draggedTab = null;
dragOverTab = null;
dragSourcePane = null;
// Clear scroll interval if it exists
if (scrollInterval) {
clearInterval(scrollInterval);
scrollInterval = null;
}
}
/**
* Update the drag indicator position and visibility
* The indicator shows where the tab will be dropped
*
* @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();
if (onLeftSide) {
// Position indicator at the left edge of the target tab
dragIndicator.css({
top: targetRect.top,
left: targetRect.left,
height: targetRect.height
});
} else {
// Position indicator at the right edge of the target tab
dragIndicator.css({
top: targetRect.top,
left: targetRect.right,
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) {
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
};
});