Skip to content

Commit 3cbc4ba

Browse files
committed
feat: overflow dropdown for hidden panel tabs, tools button always visible
Add chevron dropdown button using DropdownButton widget to access hidden tabs when tab bar overflows even after icon collapse. Tools button stays outside scrollable area, highlighted as active tab when selected. Scroll clicked/selected tabs into view. Guard drop handlers in ProjectManager and FileTreeView against non-file drags.
1 parent d463dc4 commit 3cbc4ba

4 files changed

Lines changed: 221 additions & 29 deletions

File tree

src/project/FileTreeView.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,8 +1213,14 @@ define(function (require, exports, module) {
12131213
},
12141214

12151215
handleDrop: function(e) {
1216-
var data = JSON.parse(e.dataTransfer.getData("text"));
1217-
this.props.actions.moveItem(data.path, this.props.parentPath);
1216+
try {
1217+
var data = JSON.parse(e.dataTransfer.getData("text"));
1218+
if (data && data.path) {
1219+
this.props.actions.moveItem(data.path, this.props.parentPath);
1220+
}
1221+
} catch (err) {
1222+
console.error("FileTreeView: drop handler error:", err);
1223+
}
12181224
e.stopPropagation();
12191225
},
12201226

src/project/ProjectManager.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2096,8 +2096,14 @@ define(function (require, exports, module) {
20962096

20972097
// Add support for moving items to root directory
20982098
$projectTreeContainer.on("drop", function(e) {
2099-
var data = JSON.parse(e.originalEvent.dataTransfer.getData("text"));
2100-
actionCreator.moveItem(data.path, getProjectRoot().fullPath);
2099+
try {
2100+
var data = JSON.parse(e.originalEvent.dataTransfer.getData("text"));
2101+
if (data && data.path) {
2102+
actionCreator.moveItem(data.path, getProjectRoot().fullPath);
2103+
}
2104+
} catch (err) {
2105+
console.error("ProjectManager: drop handler error:", err);
2106+
}
21012107
e.stopPropagation();
21022108
});
21032109

src/styles/Extn-BottomPanelTabs.less

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

183+
/* Overflow button: shown when tabs overflow even after collapsing to icons */
184+
.bottom-panel-overflow-btn {
185+
display: flex;
186+
align-items: center;
187+
justify-content: center;
188+
width: 1.9rem;
189+
height: 2rem;
190+
cursor: pointer;
191+
color: #666;
192+
font-size: 0.9rem;
193+
flex: 0 0 auto;
194+
transition: color 0.12s ease, background-color 0.12s ease;
195+
196+
.dark & {
197+
color: #aaa;
198+
}
199+
200+
&:hover {
201+
background-color: #e0e0e0;
202+
color: #333;
203+
204+
.dark & {
205+
background-color: #333;
206+
color: #eee;
207+
}
208+
}
209+
210+
}
211+
183212
/* Drag-and-drop tab reordering */
184213
.bottom-panel-tab-dragging {
185214
opacity: 0.5;
@@ -257,6 +286,11 @@ img.panel-titlebar-icon {
257286
align-items: center;
258287
justify-content: center;
259288
padding: 0 8px;
289+
border-left: 1px solid rgba(0, 0, 0, 0.08);
290+
291+
.dark & {
292+
border-left: 1px solid rgba(255, 255, 255, 0.08);
293+
}
260294
height: 2rem;
261295
line-height: 2rem;
262296
overflow: hidden;
@@ -280,6 +314,31 @@ img.panel-titlebar-icon {
280314
color: #eee;
281315
}
282316
}
317+
318+
&.active {
319+
color: #333;
320+
background-color: #fff;
321+
position: relative;
322+
323+
.dark & {
324+
color: #dedede;
325+
background-color: #1D1F21;
326+
}
327+
328+
&::after {
329+
content: "";
330+
position: absolute;
331+
top: 0;
332+
left: 0;
333+
right: 0;
334+
height: 0.1rem;
335+
background-color: #0078D7;
336+
337+
.dark & {
338+
background-color: #75BEFF;
339+
}
340+
}
341+
}
283342
}
284343

285344
.bottom-panel-tab-bar-actions {

src/view/PanelView.js

Lines changed: 146 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ define(function (require, exports, module) {
2828
const EventDispatcher = require("utils/EventDispatcher"),
2929
PreferencesManager = require("preferences/PreferencesManager"),
3030
Resizer = require("utils/Resizer"),
31+
DropdownButton = require("widgets/DropdownButton"),
3132
Strings = require("strings");
3233

3334
/**
@@ -171,25 +172,21 @@ define(function (require, exports, module) {
171172
if (!_$tabsOverflow) {
172173
return;
173174
}
174-
// Detach the add button before emptying to preserve its event handlers
175-
if (_$addBtn) {
176-
_$addBtn.detach();
177-
}
178175
_$tabsOverflow.empty();
179176

180177
_openIds.forEach(function (panelId) {
178+
// Default panel uses the external Tools button, not a tab
179+
if (panelId === _defaultPanelId) {
180+
return;
181+
}
181182
let panel = _panelMap[panelId];
182183
if (!panel) {
183184
return;
184185
}
185186
_$tabsOverflow.append(_buildTab(panel, panelId === _activeId));
186187
});
187188

188-
// Re-append the "+" button at the end (after all tabs)
189-
if (_$addBtn) {
190-
_$tabsOverflow.append(_$addBtn);
191-
_updateAddButtonVisibility();
192-
}
189+
_updateAddButtonVisibility();
193190
_checkTabOverflow();
194191
}
195192

@@ -221,18 +218,17 @@ define(function (require, exports, module) {
221218
if (!_$tabsOverflow) {
222219
return;
223220
}
221+
// Default panel uses the external Tools button, not a tab
222+
if (panelId === _defaultPanelId) {
223+
_updateAddButtonVisibility();
224+
return;
225+
}
224226
let panel = _panelMap[panelId];
225227
if (!panel) {
226228
return;
227229
}
228230
let $tab = _buildTab(panel, panelId === _activeId);
229-
230-
// Insert before the "+" button so it stays at the end
231-
if (_$addBtn && _$addBtn.parent().length) {
232-
_$addBtn.before($tab);
233-
} else {
234-
_$tabsOverflow.append($tab);
235-
}
231+
_$tabsOverflow.append($tab);
236232
_updateAddButtonVisibility();
237233
_checkTabOverflow();
238234
}
@@ -247,6 +243,10 @@ define(function (require, exports, module) {
247243
if (!_$tabsOverflow) {
248244
return;
249245
}
246+
if (panelId === _defaultPanelId) {
247+
_updateAddButtonVisibility();
248+
return;
249+
}
250250
_$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + panelId + '"]').remove();
251251
_updateAddButtonVisibility();
252252
_checkTabOverflow();
@@ -295,7 +295,7 @@ define(function (require, exports, module) {
295295
_$tabBar.on("dragstart", ".bottom-panel-tab", function (e) {
296296
draggedTab = this;
297297
e.originalEvent.dataTransfer.effectAllowed = "move";
298-
e.originalEvent.dataTransfer.setData("text/plain", "panel-tab");
298+
e.originalEvent.dataTransfer.setData("application/x-phoenix-panel-tab", "1");
299299
$(this).addClass("bottom-panel-tab-dragging");
300300
});
301301

@@ -354,6 +354,9 @@ define(function (require, exports, module) {
354354
* Only collapses tabs that have an icon available.
355355
* @private
356356
*/
357+
/** @type {jQueryObject} Overflow dropdown button */
358+
let _$overflowBtn = null;
359+
357360
function _checkTabOverflow() {
358361
if (!_$tabBar) {
359362
return;
@@ -362,6 +365,16 @@ define(function (require, exports, module) {
362365
_$tabBar.removeClass("bottom-panel-tabs-collapsed");
363366
const isOverflowing = _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth;
364367
_$tabBar.toggleClass("bottom-panel-tabs-collapsed", isOverflowing);
368+
369+
// Check if still overflowing after collapse
370+
const stillOverflowing = isOverflowing &&
371+
_$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth;
372+
373+
// Show/hide overflow button
374+
if (_$overflowBtn) {
375+
_$overflowBtn.toggle(stillOverflowing);
376+
}
377+
365378
// Show tooltip on hover only in collapsed mode (title text is hidden)
366379
_$tabBar.find(".bottom-panel-tab").each(function () {
367380
const $tab = $(this);
@@ -373,6 +386,105 @@ define(function (require, exports, module) {
373386
});
374387
}
375388

389+
/**
390+
* Get the list of hidden (not fully visible) panel tabs.
391+
* @return {Array<{panelId: string, title: string}>}
392+
* @private
393+
*/
394+
function _getHiddenTabs() {
395+
const hidden = [];
396+
const barRect = _$tabsOverflow[0].getBoundingClientRect();
397+
_$tabsOverflow.find(".bottom-panel-tab").each(function () {
398+
const tabRect = this.getBoundingClientRect();
399+
const isVisible = tabRect.left >= barRect.left &&
400+
tabRect.right <= (barRect.right + 2);
401+
if (!isVisible) {
402+
const $tab = $(this);
403+
hidden.push({
404+
panelId: $tab.data("panel-id"),
405+
title: $tab.find(".bottom-panel-tab-title").text()
406+
});
407+
}
408+
});
409+
return hidden;
410+
}
411+
412+
/** @type {DropdownButton.DropdownButton} */
413+
let _overflowDropdown = null;
414+
415+
/**
416+
* Show a dropdown menu listing hidden panel tabs.
417+
* Uses the same DropdownButton widget as the file tab bar overflow.
418+
* @private
419+
*/
420+
function _showOverflowMenu() {
421+
// If dropdown is already open, close it (toggle behavior)
422+
if (_overflowDropdown) {
423+
_overflowDropdown.closeDropdown();
424+
_overflowDropdown = null;
425+
return;
426+
}
427+
428+
const hidden = _getHiddenTabs();
429+
if (!hidden.length) {
430+
return;
431+
}
432+
433+
_overflowDropdown = new DropdownButton.DropdownButton("", hidden, function (item) {
434+
const panel = _panelMap[item.panelId];
435+
let iconHtml = "";
436+
if (panel && panel._options) {
437+
if (panel._options.iconClass) {
438+
iconHtml = '<i class="panel-titlebar-icon ' + panel._options.iconClass
439+
+ '" style="margin-right:6px"></i>';
440+
} else if (panel._options.iconSvg) {
441+
iconHtml = '<img class="panel-titlebar-icon" src="' + panel._options.iconSvg
442+
+ '" style="width:14px;height:14px;margin-right:6px;vertical-align:middle">';
443+
}
444+
}
445+
const activeClass = item.panelId === _activeId ? ' style="font-weight:600"' : '';
446+
return {
447+
html: '<div class="dropdown-tab-item"' + activeClass + '>'
448+
+ iconHtml + '<span>' + item.title + '</span></div>',
449+
enabled: true
450+
};
451+
});
452+
453+
_overflowDropdown.dropdownExtraClasses = "dropdown-overflow-menu";
454+
455+
// Position at the overflow button
456+
const btnRect = _$overflowBtn[0].getBoundingClientRect();
457+
$("body").append(_overflowDropdown.$button);
458+
_overflowDropdown.$button.css({
459+
position: "absolute",
460+
left: btnRect.left + "px",
461+
top: (btnRect.top - 2) + "px",
462+
zIndex: 1000
463+
});
464+
465+
_overflowDropdown.showDropdown();
466+
467+
_overflowDropdown.on("select", function (e, item) {
468+
const panel = _panelMap[item.panelId];
469+
if (panel) {
470+
panel.show();
471+
// Scroll the newly active tab into view
472+
const $tab = _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + item.panelId + '"]');
473+
if ($tab.length) {
474+
$tab[0].scrollIntoView({inline: "nearest"});
475+
}
476+
}
477+
});
478+
479+
// Clean up reference when dropdown closes
480+
_overflowDropdown.on(DropdownButton.EVENT_DROPDOWN_CLOSED, function () {
481+
if (_overflowDropdown) {
482+
_overflowDropdown.$button.remove();
483+
_overflowDropdown = null;
484+
}
485+
});
486+
}
487+
376488
/**
377489
* Show or hide the "+" button based on whether the default panel is active.
378490
* The button is hidden when the default panel is the active tab (since
@@ -383,11 +495,8 @@ define(function (require, exports, module) {
383495
if (!_$addBtn) {
384496
return;
385497
}
386-
if (_defaultPanelId && _activeId === _defaultPanelId) {
387-
_$addBtn.hide();
388-
} else {
389-
_$addBtn.show();
390-
}
498+
// Highlight the Tools button as active when the default panel is shown
499+
_$addBtn.toggleClass("active", _defaultPanelId && _activeId === _defaultPanelId);
391500
}
392501

393502
/**
@@ -664,13 +773,13 @@ define(function (require, exports, module) {
664773
_recomputeLayout = recomputeLayoutFn;
665774
_defaultPanelId = defaultPanelId;
666775

667-
// Create the "Tools" button inside the tabs overflow area (after all tabs)
668-
// This opens the default/quick-access panel when clicked.
776+
// Create the "Tools" button outside the scrollable tabs area
777+
// so it's always visible even when tabs overflow.
669778
_$addBtn = $('<span class="bottom-panel-add-btn" title="' + Strings.BOTTOM_PANEL_DEFAULT_TITLE + '">'
670779
+ '<img class="app-drawer-tab-icon" src="styles/images/app-drawer.svg"'
671780
+ ' style="width:12px;height:12px;vertical-align:middle;margin-right:4px">'
672781
+ Strings.BOTTOM_PANEL_DEFAULT_TITLE + '</span>');
673-
_$tabsOverflow.append(_$addBtn);
782+
_$tabBar.find(".bottom-panel-tab-bar-actions").before(_$addBtn);
674783

675784
// Tab bar click handlers
676785
_$tabBar.on("click", ".bottom-panel-tab-close-btn", function (e) {
@@ -692,10 +801,22 @@ define(function (require, exports, module) {
692801
panel.show();
693802
}
694803
}
804+
// Scroll clicked tab into view if partially hidden
805+
this.scrollIntoView({inline: "nearest"});
695806
});
696807

697808
_initDragAndDrop();
698809

810+
// Overflow button for hidden tabs (inserted between tabs and action buttons)
811+
_$overflowBtn = $('<span class="bottom-panel-overflow-btn" title="' + Strings.TABBAR_SHOW_HIDDEN_TABS + '">'
812+
+ '<i class="fa-solid fa-chevron-down"></i></span>');
813+
_$overflowBtn.hide();
814+
_$tabBar.find(".bottom-panel-tab-bar-actions").before(_$overflowBtn);
815+
_$overflowBtn.on("click", function (e) {
816+
e.stopPropagation();
817+
_showOverflowMenu();
818+
});
819+
699820
// "+" button opens the default/quick-access panel
700821
_$addBtn.on("click", function (e) {
701822
e.stopPropagation();

0 commit comments

Comments
 (0)