@@ -1762,13 +1762,288 @@ define(function (require, exports, module) {
17621762 return cmenu ;
17631763 }
17641764
1765+ /**
1766+ * Hamburger menu: when the titlebar is too narrow to fit all menu items on one row,
1767+ * overflow items are hidden and a hamburger button appears with a dropdown listing them.
1768+ */
1769+ function _initHamburgerMenu ( ) {
1770+ const $menubar = $ ( "#titlebar .nav" ) ;
1771+ const $hamburger = $ ( `<li class="hamburger-menu" id="hamburger-menu" style="display:none;">
1772+ <a href="#" class="hamburger-toggle">
1773+ <i class="fa-solid fa-bars"></i>
1774+ </a>
1775+ <ul class="dropdown-menu hamburger-dropdown"></ul>
1776+ </li>` ) ;
1777+ $menubar . append ( $hamburger ) ;
1778+ const $hamburgerDropdown = $hamburger . find ( ".dropdown-menu" ) ;
1779+ const $hamburgerToggle = $hamburger . find ( ".hamburger-toggle" ) ;
1780+ let _activeSubmenuId = null ;
1781+
1782+ function _resetMenuItemStyles ( $menuItem ) {
1783+ const menu = menuMap [ $menuItem . attr ( "id" ) ] ;
1784+ if ( menu ) {
1785+ menu . closeSubMenu ( ) ;
1786+ }
1787+ $menuItem . removeClass ( "open" ) . css ( {
1788+ display : "none" ,
1789+ position : "" ,
1790+ visibility : "" ,
1791+ pointerEvents : "" ,
1792+ width : "" ,
1793+ height : "" ,
1794+ overflow : ""
1795+ } ) ;
1796+ $menuItem . find ( "> .dropdown-menu" ) . css ( {
1797+ display : "" ,
1798+ visibility : "" ,
1799+ pointerEvents : "" ,
1800+ position : "" ,
1801+ top : "" ,
1802+ left : "" ,
1803+ margin : ""
1804+ } ) ;
1805+ }
1806+
1807+ function _closeHamburgerSubmenus ( ) {
1808+ $hamburgerDropdown . find ( ".hamburger-submenu-open" ) . removeClass ( "hamburger-submenu-open" ) ;
1809+ // Reset the active flyout menu item
1810+ if ( _activeSubmenuId ) {
1811+ _resetMenuItemStyles ( $ ( `#${ _activeSubmenuId } ` ) ) ;
1812+ _activeSubmenuId = null ;
1813+ }
1814+ // Safety: also reset any overflow menu items that might still have
1815+ // inline styles from a flyout that wasn't properly closed
1816+ $menubar . children ( "li.dropdown:not(.hamburger-menu)" ) . each ( function ( ) {
1817+ const $item = $ ( this ) ;
1818+ if ( $item . css ( "display" ) !== "none" && $item . find ( "> .dropdown-menu" ) . css ( "position" ) === "fixed" ) {
1819+ _resetMenuItemStyles ( $item ) ;
1820+ }
1821+ } ) ;
1822+ }
1823+
1824+ function _closeHamburger ( ) {
1825+ $hamburger . removeClass ( "hamburger-open" ) ;
1826+ _closeHamburgerSubmenus ( ) ;
1827+ }
1828+
1829+ // Wire up hamburger click to toggle the dropdown
1830+ $hamburgerToggle . on ( "click" , function ( e ) {
1831+ e . preventDefault ( ) ;
1832+ e . stopPropagation ( ) ;
1833+ const wasOpen = $hamburger . hasClass ( "hamburger-open" ) ;
1834+ closeAll ( ) ;
1835+ _closeHamburger ( ) ;
1836+ if ( ! wasOpen ) {
1837+ $hamburger . addClass ( "hamburger-open" ) ;
1838+ }
1839+ } ) ;
1840+
1841+ // Close hamburger when clicking outside
1842+ $ ( document ) . on ( "mousedown" , function ( e ) {
1843+ if ( ! $hamburger . hasClass ( "hamburger-open" ) ) {
1844+ return ;
1845+ }
1846+ // Check if click is inside hamburger
1847+ if ( $ ( e . target ) . closest ( "#hamburger-menu" ) . length ) {
1848+ return ;
1849+ }
1850+ // Check if click is inside the active flyout menu
1851+ if ( _activeSubmenuId && $ ( e . target ) . closest ( `#${ _activeSubmenuId } ` ) . length ) {
1852+ return ;
1853+ }
1854+ // Check if click is inside any open context menu (sub-submenus
1855+ // live in #context-menu-bar, not inside the flyout menu)
1856+ if ( $ ( e . target ) . closest ( "#context-menu-bar .open" ) . length ) {
1857+ return ;
1858+ }
1859+ _closeHamburger ( ) ;
1860+ } ) ;
1861+
1862+ // Wire up hamburger toggle mouseenter like other menus
1863+ $hamburgerToggle . on ( "mouseenter" , function ( ) {
1864+ _closeAllSubMenus ( ) ;
1865+ const $this = $ ( this ) ;
1866+ if ( $ ( '#titlebar, #titlebar *' ) . is ( ':focus' ) ) {
1867+ $this . addClass ( 'selected' ) . focus ( ) ;
1868+ } else {
1869+ $this . addClass ( 'selected' ) ;
1870+ }
1871+ } ) ;
1872+ $hamburgerToggle . on ( "mouseleave" , function ( ) {
1873+ $ ( this ) . removeClass ( 'selected' ) ;
1874+ } ) ;
1875+
1876+ // Close hamburger when ESC is pressed
1877+ $ ( document ) . on ( "keydown" , function ( e ) {
1878+ if ( e . key === "Escape" && $hamburger . hasClass ( "hamburger-open" ) ) {
1879+ _closeHamburger ( ) ;
1880+ e . stopPropagation ( ) ;
1881+ }
1882+ } ) ;
1883+
1884+ // Close hamburger when window loses focus
1885+ $ ( window ) . on ( "blur" , _closeHamburger ) ;
1886+
1887+ // Close hamburger when a menu item in a flyout is clicked.
1888+ // Use setTimeout so the command executes before we hide the menu.
1889+ $menubar . on ( "click" , ".dropdown:not(.hamburger-menu) .menuAnchor" , function ( ) {
1890+ setTimeout ( _closeHamburger , 0 ) ;
1891+ } ) ;
1892+
1893+ // Also close on beforeExecuteCommand (e.g. keyboard shortcuts while open)
1894+ CommandManager . on ( "beforeExecuteCommand" , function ( ) {
1895+ _closeHamburger ( ) ;
1896+ } ) ;
1897+
1898+ let _updateScheduled = false ;
1899+
1900+ function _updateHamburgerMenu ( ) {
1901+ _updateScheduled = false ;
1902+ // Don't re-layout while a flyout submenu is active - showing the
1903+ // hidden menu li triggers ResizeObserver which would reset everything
1904+ if ( _activeSubmenuId ) {
1905+ return ;
1906+ }
1907+ _closeHamburgerSubmenus ( ) ;
1908+ const $items = $menubar . children ( "li.dropdown:not(.hamburger-menu)" ) ;
1909+ // First, show all items and hide hamburger to measure natural layout
1910+ $items . css ( { display : "" , position : "" , visibility : "" , pointerEvents : "" } ) ;
1911+ $hamburger . hide ( ) ;
1912+ $hamburgerDropdown . empty ( ) ;
1913+
1914+ if ( $items . length === 0 ) {
1915+ return ;
1916+ }
1917+
1918+ const firstItemTop = $items . first ( ) [ 0 ] . offsetTop ;
1919+ let overflowStartIndex = - 1 ;
1920+
1921+ for ( let i = 0 ; i < $items . length ; i ++ ) {
1922+ if ( $items [ i ] . offsetTop > firstItemTop ) {
1923+ overflowStartIndex = i ;
1924+ break ;
1925+ }
1926+ }
1927+
1928+ if ( overflowStartIndex === - 1 ) {
1929+ // Everything fits on one row
1930+ return ;
1931+ }
1932+
1933+ // Show hamburger, then re-check what fits with hamburger visible
1934+ $hamburger . css ( "display" , "" ) ;
1935+
1936+ // Re-measure: with hamburger visible, even more items might overflow
1937+ for ( let i = 0 ; i < $items . length ; i ++ ) {
1938+ if ( $items [ i ] . offsetTop > firstItemTop ) {
1939+ overflowStartIndex = i ;
1940+ break ;
1941+ }
1942+ }
1943+
1944+ function _openFlyout ( $entry , menuId ) {
1945+ if ( _activeSubmenuId && _activeSubmenuId !== menuId ) {
1946+ _closeHamburgerSubmenus ( ) ;
1947+ }
1948+ $hamburgerDropdown . find ( ".hamburger-submenu-open" ) . removeClass ( "hamburger-submenu-open" ) ;
1949+ $entry . addClass ( "hamburger-submenu-open" ) ;
1950+ _activeSubmenuId = menuId ;
1951+
1952+ const $menuItem = $ ( `#${ menuId } ` ) ;
1953+ // Add 'open' class so sub-submenus (ContextMenus) can open properly.
1954+ // Keep the li itself invisible and out of flow.
1955+ $menuItem . addClass ( "open" ) . css ( {
1956+ display : "block" ,
1957+ position : "absolute" ,
1958+ visibility : "hidden" ,
1959+ pointerEvents : "none" ,
1960+ width : "0" ,
1961+ height : "0" ,
1962+ overflow : "visible"
1963+ } ) ;
1964+
1965+ const $realDropdown = $menuItem . find ( "> .dropdown-menu" ) ;
1966+ const entryRect = $entry [ 0 ] . getBoundingClientRect ( ) ;
1967+ const hamburgerRect = $hamburgerDropdown [ 0 ] . getBoundingClientRect ( ) ;
1968+ let flyoutLeft = hamburgerRect . right - 2 ;
1969+ if ( flyoutLeft + 250 > window . innerWidth ) {
1970+ flyoutLeft = hamburgerRect . left - $realDropdown . outerWidth ( ) + 2 ;
1971+ }
1972+ $realDropdown . css ( {
1973+ display : "block" ,
1974+ visibility : "visible" ,
1975+ pointerEvents : "auto" ,
1976+ position : "fixed" ,
1977+ top : entryRect . top + "px" ,
1978+ left : flyoutLeft + "px" ,
1979+ margin : "0"
1980+ } ) ;
1981+ }
1982+
1983+ // Hide overflowing items and add them to hamburger dropdown as nested flyouts
1984+ for ( let i = overflowStartIndex ; i < $items . length ; i ++ ) {
1985+ const $item = $ ( $items [ i ] ) ;
1986+ const menuId = $item . attr ( "id" ) ;
1987+ const menuName = $item . find ( ".dropdown-toggle" ) . text ( ) ;
1988+ $item . css ( "display" , "none" ) ;
1989+
1990+ const $entry = $ ( `<li class="hamburger-submenu-item">
1991+ <a href="#" class="menuAnchor" data-menu-id="${ menuId } ">
1992+ <span class="menu-name">${ _ . escape ( menuName ) } </span>
1993+ <span class="hamburger-submenu-arrow">▸</span>
1994+ </a>
1995+ </li>` ) ;
1996+
1997+ $entry . on ( "mouseenter" , function ( ) {
1998+ _openFlyout ( $ ( this ) , menuId ) ;
1999+ } ) ;
2000+
2001+ $hamburgerDropdown . append ( $entry ) ;
2002+ }
2003+ }
2004+
2005+ function _scheduleUpdate ( ) {
2006+ if ( ! _updateScheduled ) {
2007+ _updateScheduled = true ;
2008+ requestAnimationFrame ( _updateHamburgerMenu ) ;
2009+ }
2010+ }
2011+
2012+ // Observe titlebar resizes
2013+ const titlebar = document . getElementById ( "titlebar" ) ;
2014+ if ( window . ResizeObserver ) {
2015+ const resizeObserver = new ResizeObserver ( _scheduleUpdate ) ;
2016+ resizeObserver . observe ( titlebar ) ;
2017+ }
2018+ $ ( window ) . on ( "resize" , _scheduleUpdate ) ;
2019+
2020+ // Also update when menus are added/removed
2021+ exports . on ( EVENT_MENU_ADDED , _scheduleUpdate ) ;
2022+
2023+ // Initial check
2024+ _scheduleUpdate ( ) ;
2025+ }
2026+
17652027 AppInit . htmlReady ( function ( ) {
17662028 $ ( '#titlebar' ) . on ( 'focusin' , function ( ) {
17672029 KeyBindingManager . addGlobalKeydownHook ( menuKeyboardNavigationHandler ) ;
17682030 } ) ;
17692031 $ ( '#titlebar' ) . on ( 'focusout' , function ( ) {
17702032 KeyBindingManager . removeGlobalKeydownHook ( menuKeyboardNavigationHandler ) ;
17712033 } ) ;
2034+ _initHamburgerMenu ( ) ;
2035+
2036+ // Close all menus, context menus, and popups when window loses focus
2037+ $ ( window ) . on ( "blur" , function ( ) {
2038+ closeAll ( ) ;
2039+ // Close all context menus (editor, file tree, working set, etc.)
2040+ _ . forEach ( contextMenuMap , function ( contextMenu ) {
2041+ if ( contextMenu . isOpen ( ) ) {
2042+ contextMenu . close ( ) ;
2043+ }
2044+ } ) ;
2045+ PopUpManager . closeAllPopups ( ) ;
2046+ } ) ;
17722047 } ) ;
17732048
17742049 EventDispatcher . makeEventDispatcher ( exports ) ;
0 commit comments