diff --git a/src/project/FileTreeView.js b/src/project/FileTreeView.js index 6e366f726..11992ff87 100644 --- a/src/project/FileTreeView.js +++ b/src/project/FileTreeView.js @@ -1213,8 +1213,14 @@ define(function (require, exports, module) { }, handleDrop: function(e) { - var data = JSON.parse(e.dataTransfer.getData("text")); - this.props.actions.moveItem(data.path, this.props.parentPath); + try { + var data = JSON.parse(e.dataTransfer.getData("text")); + if (data && data.path) { + this.props.actions.moveItem(data.path, this.props.parentPath); + } + } catch (err) { + console.error("FileTreeView: drop handler error:", err); + } e.stopPropagation(); }, diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 87757e15a..b78a2d83c 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -2096,8 +2096,14 @@ define(function (require, exports, module) { // Add support for moving items to root directory $projectTreeContainer.on("drop", function(e) { - var data = JSON.parse(e.originalEvent.dataTransfer.getData("text")); - actionCreator.moveItem(data.path, getProjectRoot().fullPath); + try { + var data = JSON.parse(e.originalEvent.dataTransfer.getData("text")); + if (data && data.path) { + actionCreator.moveItem(data.path, getProjectRoot().fullPath); + } + } catch (err) { + console.error("ProjectManager: drop handler error:", err); + } e.stopPropagation(); }); diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less index afe16fabf..c9dc05160 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -180,6 +180,35 @@ img.panel-titlebar-icon { pointer-events: none; } +/* Overflow button: shown when tabs overflow even after collapsing to icons */ +.bottom-panel-overflow-btn { + display: flex; + align-items: center; + justify-content: center; + width: 1.9rem; + height: 2rem; + cursor: pointer; + color: #666; + font-size: 0.9rem; + flex: 0 0 auto; + transition: color 0.12s ease, background-color 0.12s ease; + + .dark & { + color: #aaa; + } + + &:hover { + background-color: #e0e0e0; + color: #333; + + .dark & { + background-color: #333; + color: #eee; + } + } + +} + /* Drag-and-drop tab reordering */ .bottom-panel-tab-dragging { opacity: 0.5; @@ -280,6 +309,7 @@ img.panel-titlebar-icon { color: #eee; } } + } .bottom-panel-tab-bar-actions { diff --git a/src/view/PanelView.js b/src/view/PanelView.js index 3b3134454..aeedd540e 100644 --- a/src/view/PanelView.js +++ b/src/view/PanelView.js @@ -28,6 +28,7 @@ define(function (require, exports, module) { const EventDispatcher = require("utils/EventDispatcher"), PreferencesManager = require("preferences/PreferencesManager"), Resizer = require("utils/Resizer"), + DropdownButton = require("widgets/DropdownButton"), Strings = require("strings"); /** @@ -185,11 +186,11 @@ define(function (require, exports, module) { _$tabsOverflow.append(_buildTab(panel, panelId === _activeId)); }); - // Re-append the "+" button at the end (after all tabs) + // Re-append the Tools button at the end if (_$addBtn) { _$tabsOverflow.append(_$addBtn); - _updateAddButtonVisibility(); } + _updateAddButtonVisibility(); _checkTabOverflow(); } @@ -226,8 +227,7 @@ define(function (require, exports, module) { return; } let $tab = _buildTab(panel, panelId === _activeId); - - // Insert before the "+" button so it stays at the end + // Insert before the Tools button so it stays at the end if (_$addBtn && _$addBtn.parent().length) { _$addBtn.before($tab); } else { @@ -295,7 +295,7 @@ define(function (require, exports, module) { _$tabBar.on("dragstart", ".bottom-panel-tab", function (e) { draggedTab = this; e.originalEvent.dataTransfer.effectAllowed = "move"; - e.originalEvent.dataTransfer.setData("text/plain", "panel-tab"); + e.originalEvent.dataTransfer.setData("application/x-phoenix-panel-tab", "1"); $(this).addClass("bottom-panel-tab-dragging"); }); @@ -354,6 +354,9 @@ define(function (require, exports, module) { * Only collapses tabs that have an icon available. * @private */ + /** @type {jQueryObject} Overflow dropdown button */ + let _$overflowBtn = null; + function _checkTabOverflow() { if (!_$tabBar) { return; @@ -362,6 +365,16 @@ define(function (require, exports, module) { _$tabBar.removeClass("bottom-panel-tabs-collapsed"); const isOverflowing = _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth; _$tabBar.toggleClass("bottom-panel-tabs-collapsed", isOverflowing); + + // Check if still overflowing after collapse + const stillOverflowing = isOverflowing && + _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth; + + // Show/hide overflow button + if (_$overflowBtn) { + _$overflowBtn.toggle(stillOverflowing); + } + // Show tooltip on hover only in collapsed mode (title text is hidden) _$tabBar.find(".bottom-panel-tab").each(function () { const $tab = $(this); @@ -373,6 +386,105 @@ define(function (require, exports, module) { }); } + /** + * Get the list of hidden (not fully visible) panel tabs. + * @return {Array<{panelId: string, title: string}>} + * @private + */ + function _getHiddenTabs() { + const hidden = []; + const barRect = _$tabsOverflow[0].getBoundingClientRect(); + _$tabsOverflow.find(".bottom-panel-tab").each(function () { + const tabRect = this.getBoundingClientRect(); + const isVisible = tabRect.left >= barRect.left && + tabRect.right <= (barRect.right + 2); + if (!isVisible) { + const $tab = $(this); + hidden.push({ + panelId: $tab.data("panel-id"), + title: $tab.find(".bottom-panel-tab-title").text() + }); + } + }); + return hidden; + } + + /** @type {DropdownButton.DropdownButton} */ + let _overflowDropdown = null; + + /** + * Show a dropdown menu listing hidden panel tabs. + * Uses the same DropdownButton widget as the file tab bar overflow. + * @private + */ + function _showOverflowMenu() { + // If dropdown is already open, close it (toggle behavior) + if (_overflowDropdown) { + _overflowDropdown.closeDropdown(); + _overflowDropdown = null; + return; + } + + const hidden = _getHiddenTabs(); + if (!hidden.length) { + return; + } + + _overflowDropdown = new DropdownButton.DropdownButton("", hidden, function (item) { + const panel = _panelMap[item.panelId]; + let iconHtml = ""; + if (panel && panel._options) { + if (panel._options.iconClass) { + iconHtml = ''; + } else if (panel._options.iconSvg) { + iconHtml = ''; + } + } + const activeClass = item.panelId === _activeId ? ' style="font-weight:600"' : ''; + return { + html: '', + enabled: true + }; + }); + + _overflowDropdown.dropdownExtraClasses = "dropdown-overflow-menu"; + + // Position at the overflow button + const btnRect = _$overflowBtn[0].getBoundingClientRect(); + $("body").append(_overflowDropdown.$button); + _overflowDropdown.$button.css({ + position: "absolute", + left: btnRect.left + "px", + top: (btnRect.top - 2) + "px", + zIndex: 1000 + }); + + _overflowDropdown.showDropdown(); + + _overflowDropdown.on("select", function (e, item) { + const panel = _panelMap[item.panelId]; + if (panel) { + panel.show(); + // Scroll the newly active tab into view + const $tab = _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + item.panelId + '"]'); + if ($tab.length) { + $tab[0].scrollIntoView({inline: "nearest"}); + } + } + }); + + // Clean up reference when dropdown closes + _overflowDropdown.on(DropdownButton.EVENT_DROPDOWN_CLOSED, function () { + if (_overflowDropdown) { + _overflowDropdown.$button.remove(); + _overflowDropdown = null; + } + }); + } + /** * Show or hide the "+" button based on whether the default panel is active. * The button is hidden when the default panel is the active tab (since @@ -664,8 +776,7 @@ define(function (require, exports, module) { _recomputeLayout = recomputeLayoutFn; _defaultPanelId = defaultPanelId; - // Create the "Tools" button inside the tabs overflow area (after all tabs) - // This opens the default/quick-access panel when clicked. + // Create the "Tools" button inside the scrollable tabs area. _$addBtn = $('' + '' @@ -692,10 +803,22 @@ define(function (require, exports, module) { panel.show(); } } + // Scroll clicked tab into view if partially hidden + this.scrollIntoView({inline: "nearest"}); }); _initDragAndDrop(); + // Overflow button for hidden tabs (inserted between tabs and action buttons) + _$overflowBtn = $('' + + ''); + _$overflowBtn.hide(); + _$tabBar.find(".bottom-panel-tab-bar-actions").before(_$overflowBtn); + _$overflowBtn.on("click", function (e) { + e.stopPropagation(); + _showOverflowMenu(); + }); + // "+" button opens the default/quick-access panel _$addBtn.on("click", function (e) { e.stopPropagation();