Skip to content

Commit 66ff648

Browse files
committed
feat: drag-and-drop tab reordering and tooltip in collapsed mode
Extract drag-and-drop into _initDragAndDrop() with vertical line indicator matching file tab bar UX. Fix Tools button losing click handler after tab rebuild by using detach() before empty(). Show tooltip on hover in collapsed icon mode only.
1 parent 6e1a55c commit 66ff648

2 files changed

Lines changed: 118 additions & 1 deletion

File tree

src/styles/Extn-BottomPanelTabs.less

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ img.panel-titlebar-icon {
180180
pointer-events: none;
181181
}
182182

183+
/* Drag-and-drop tab reordering */
184+
.bottom-panel-tab-dragging {
185+
opacity: 0.5;
186+
}
187+
183188
/* Collapsed tab bar: show icons, hide titles for tabs that have icons */
184189
.bottom-panel-tabs-collapsed {
185190
.bottom-panel-tab-icon {

src/view/PanelView.js

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ define(function (require, exports, module) {
148148
*/
149149
function _buildTab(panel, isActive) {
150150
let title = panel._tabTitle || _getPanelTitle(panel.panelID, panel.$panel);
151-
let $tab = $('<div class="bottom-panel-tab"></div>')
151+
let $tab = $('<div class="bottom-panel-tab" draggable="true"></div>')
152152
.toggleClass('active', isActive)
153153
.attr('data-panel-id', panel.panelID);
154154
const opts = panel._options;
@@ -171,6 +171,10 @@ define(function (require, exports, module) {
171171
if (!_$tabsOverflow) {
172172
return;
173173
}
174+
// Detach the add button before emptying to preserve its event handlers
175+
if (_$addBtn) {
176+
_$addBtn.detach();
177+
}
174178
_$tabsOverflow.empty();
175179

176180
_openIds.forEach(function (panelId) {
@@ -248,6 +252,103 @@ define(function (require, exports, module) {
248252
_checkTabOverflow();
249253
}
250254

255+
/**
256+
* Set up drag-and-drop tab reordering on the bottom panel tab bar.
257+
* Uses a vertical line indicator matching the file tab bar UX.
258+
* @private
259+
*/
260+
function _initDragAndDrop() {
261+
let draggedTab = null;
262+
let $indicator = $('<div class="tab-drag-indicator"></div>');
263+
$("body").append($indicator);
264+
265+
function getDropPosition(targetTab, mouseX) {
266+
const rect = targetTab.getBoundingClientRect();
267+
return mouseX < rect.left + rect.width / 2;
268+
}
269+
270+
function updateIndicator(targetTab, insertBefore) {
271+
if (!targetTab) {
272+
$indicator.hide();
273+
return;
274+
}
275+
const rect = targetTab.getBoundingClientRect();
276+
$indicator.css({
277+
position: "fixed",
278+
left: (insertBefore ? rect.left : rect.right) + "px",
279+
top: rect.top + "px",
280+
height: rect.height + "px",
281+
width: "2px",
282+
zIndex: 10001
283+
}).show();
284+
}
285+
286+
function cleanup() {
287+
if (draggedTab) {
288+
$(draggedTab).removeClass("bottom-panel-tab-dragging");
289+
}
290+
draggedTab = null;
291+
$indicator.hide();
292+
_$tabBar.find(".bottom-panel-tab").removeClass("drag-target");
293+
}
294+
295+
_$tabBar.on("dragstart", ".bottom-panel-tab", function (e) {
296+
draggedTab = this;
297+
e.originalEvent.dataTransfer.effectAllowed = "move";
298+
e.originalEvent.dataTransfer.setData("text/plain", "panel-tab");
299+
$(this).addClass("bottom-panel-tab-dragging");
300+
});
301+
302+
_$tabBar.on("dragend", ".bottom-panel-tab", function () {
303+
setTimeout(cleanup, 50);
304+
});
305+
306+
_$tabBar.on("dragover", ".bottom-panel-tab", function (e) {
307+
if (!draggedTab || this === draggedTab) {
308+
return;
309+
}
310+
e.preventDefault();
311+
e.originalEvent.dataTransfer.dropEffect = "move";
312+
_$tabBar.find(".bottom-panel-tab").removeClass("drag-target");
313+
$(this).addClass("drag-target");
314+
updateIndicator(this, getDropPosition(this, e.originalEvent.clientX));
315+
});
316+
317+
_$tabBar.on("dragleave", ".bottom-panel-tab", function (e) {
318+
const related = e.originalEvent.relatedTarget;
319+
if (!$(this).is(related) && !$(this).has(related).length) {
320+
$(this).removeClass("drag-target");
321+
}
322+
});
323+
324+
_$tabBar.on("drop", ".bottom-panel-tab", function (e) {
325+
e.preventDefault();
326+
e.stopPropagation();
327+
if (!draggedTab || this === draggedTab) {
328+
cleanup();
329+
return;
330+
}
331+
let draggedId = $(draggedTab).data("panel-id");
332+
let targetId = $(this).data("panel-id");
333+
let fromIdx = _openIds.indexOf(draggedId);
334+
let toIdx = _openIds.indexOf(targetId);
335+
if (fromIdx === -1 || toIdx === -1) {
336+
cleanup();
337+
return;
338+
}
339+
const insertBefore = getDropPosition(this, e.originalEvent.clientX);
340+
_openIds.splice(fromIdx, 1);
341+
let newIdx = _openIds.indexOf(targetId);
342+
if (!insertBefore) {
343+
newIdx++;
344+
}
345+
_openIds.splice(newIdx, 0, draggedId);
346+
cleanup();
347+
_updateBottomPanelTabBar();
348+
_updateActiveTabHighlight();
349+
});
350+
}
351+
251352
/**
252353
* Check if the tab bar is overflowing and collapse tabs to icons if so.
253354
* Only collapses tabs that have an icon available.
@@ -261,6 +362,15 @@ define(function (require, exports, module) {
261362
_$tabBar.removeClass("bottom-panel-tabs-collapsed");
262363
const isOverflowing = _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth;
263364
_$tabBar.toggleClass("bottom-panel-tabs-collapsed", isOverflowing);
365+
// Show tooltip on hover only in collapsed mode (title text is hidden)
366+
_$tabBar.find(".bottom-panel-tab").each(function () {
367+
const $tab = $(this);
368+
if (isOverflowing) {
369+
$tab.attr("title", $tab.find(".bottom-panel-tab-title").text());
370+
} else {
371+
$tab.removeAttr("title");
372+
}
373+
});
264374
}
265375

266376
/**
@@ -584,6 +694,8 @@ define(function (require, exports, module) {
584694
}
585695
});
586696

697+
_initDragAndDrop();
698+
587699
// "+" button opens the default/quick-access panel
588700
_$addBtn.on("click", function (e) {
589701
e.stopPropagation();

0 commit comments

Comments
 (0)