@@ -517,9 +517,124 @@ This is a fully client-side application. Your content never leaves your browser
517517 } ;
518518 }
519519
520+ function closeTabMenus ( ) {
521+ document . querySelectorAll ( '.tab-menu-btn.open' ) . forEach ( function ( btn ) {
522+ btn . classList . remove ( 'open' ) ;
523+ btn . setAttribute ( 'aria-expanded' , 'false' ) ;
524+ } ) ;
525+ document . querySelectorAll ( '.tab-menu-dropdown.open' ) . forEach ( function ( dropdown ) {
526+ dropdown . classList . remove ( 'open' ) ;
527+ } ) ;
528+ }
529+
530+ function removeTabMenuDropdowns ( ) {
531+ document . querySelectorAll ( '.tab-menu-dropdown[data-tab-menu-dropdown="true"]' ) . forEach ( function ( dropdown ) {
532+ dropdown . remove ( ) ;
533+ } ) ;
534+ }
535+
536+ function positionTabMenu ( menuBtn , dropdown ) {
537+ const rect = menuBtn . getBoundingClientRect ( ) ;
538+ const margin = 8 ;
539+ const dropdownWidth = dropdown . offsetWidth || 130 ;
540+ const dropdownHeight = dropdown . offsetHeight || 110 ;
541+ let left = rect . right - dropdownWidth ;
542+ let top = rect . bottom + 4 ;
543+
544+ left = Math . max ( margin , Math . min ( left , window . innerWidth - dropdownWidth - margin ) ) ;
545+ if ( top + dropdownHeight > window . innerHeight - margin ) {
546+ top = Math . max ( margin , rect . top - dropdownHeight - 4 ) ;
547+ }
548+
549+ dropdown . style . top = top + 'px' ;
550+ dropdown . style . left = left + 'px' ;
551+ dropdown . style . right = 'auto' ;
552+ }
553+
554+ function runTabMenuAction ( tabId , action , isMobileMenu ) {
555+ if ( action === 'rename' ) {
556+ if ( isMobileMenu ) closeMobileMenu ( ) ;
557+ renameTab ( tabId ) ;
558+ } else if ( action === 'duplicate' ) {
559+ duplicateTab ( tabId ) ;
560+ if ( isMobileMenu ) closeMobileMenu ( ) ;
561+ } else if ( action === 'delete' ) {
562+ deleteTab ( tabId ) ;
563+ }
564+ }
565+
566+ function createTabActionMenu ( tab , options ) {
567+ const isMobileMenu = options && options . isMobileMenu ;
568+ const menuIdPrefix = options && options . menuIdPrefix ? options . menuIdPrefix : 'tab-menu' ;
569+ const menuId = menuIdPrefix + '-' + tab . id ;
570+
571+ const menuBtn = document . createElement ( 'button' ) ;
572+ menuBtn . type = 'button' ;
573+ menuBtn . className = 'tab-menu-btn' ;
574+ menuBtn . setAttribute ( 'aria-label' , 'File options for ' + ( tab . title || 'Untitled' ) ) ;
575+ menuBtn . setAttribute ( 'aria-haspopup' , 'menu' ) ;
576+ menuBtn . setAttribute ( 'aria-expanded' , 'false' ) ;
577+ menuBtn . setAttribute ( 'aria-controls' , menuId ) ;
578+ menuBtn . setAttribute ( 'draggable' , 'false' ) ;
579+ menuBtn . title = 'File options' ;
580+ menuBtn . innerHTML = '⋯' ;
581+
582+ const dropdown = document . createElement ( 'div' ) ;
583+ dropdown . id = menuId ;
584+ dropdown . className = 'tab-menu-dropdown' ;
585+ dropdown . setAttribute ( 'data-tab-menu-dropdown' , 'true' ) ;
586+ dropdown . setAttribute ( 'role' , 'menu' ) ;
587+ dropdown . innerHTML =
588+ '<button type="button" class="tab-menu-item" role="menuitem" data-action="rename"><i class="bi bi-pencil"></i> Rename</button>' +
589+ '<button type="button" class="tab-menu-item" role="menuitem" data-action="duplicate"><i class="bi bi-files"></i> Duplicate</button>' +
590+ '<button type="button" class="tab-menu-item tab-menu-item-danger" role="menuitem" data-action="delete"><i class="bi bi-trash"></i> Delete</button>' ;
591+
592+ menuBtn . addEventListener ( 'click' , function ( e ) {
593+ e . preventDefault ( ) ;
594+ e . stopPropagation ( ) ;
595+ const shouldOpen = ! menuBtn . classList . contains ( 'open' ) ;
596+ closeTabMenus ( ) ;
597+ if ( shouldOpen ) {
598+ menuBtn . classList . add ( 'open' ) ;
599+ menuBtn . setAttribute ( 'aria-expanded' , 'true' ) ;
600+ dropdown . classList . add ( 'open' ) ;
601+ positionTabMenu ( menuBtn , dropdown ) ;
602+ }
603+ } ) ;
604+
605+ menuBtn . addEventListener ( 'mousedown' , function ( e ) {
606+ e . stopPropagation ( ) ;
607+ } ) ;
608+
609+ menuBtn . addEventListener ( 'dragstart' , function ( e ) {
610+ e . preventDefault ( ) ;
611+ e . stopPropagation ( ) ;
612+ } ) ;
613+
614+ dropdown . addEventListener ( 'click' , function ( e ) {
615+ e . stopPropagation ( ) ;
616+ } ) ;
617+
618+ dropdown . querySelectorAll ( '.tab-menu-item' ) . forEach ( function ( actionBtn ) {
619+ actionBtn . addEventListener ( 'click' , function ( e ) {
620+ e . preventDefault ( ) ;
621+ e . stopPropagation ( ) ;
622+ const action = actionBtn . getAttribute ( 'data-action' ) ;
623+ closeTabMenus ( ) ;
624+ runTabMenuAction ( tab . id , action , isMobileMenu ) ;
625+ } ) ;
626+ } ) ;
627+
628+ document . body . appendChild ( dropdown ) ;
629+
630+ return { button : menuBtn , dropdown : dropdown } ;
631+ }
632+
520633 function renderTabBar ( tabsArr , currentActiveTabId ) {
521634 const tabList = document . getElementById ( 'tab-list' ) ;
522635 if ( ! tabList ) return ;
636+ closeTabMenus ( ) ;
637+ removeTabMenuDropdowns ( ) ;
523638 tabList . innerHTML = '' ;
524639 tabsArr . forEach ( function ( tab ) {
525640 const item = document . createElement ( 'div' ) ;
@@ -534,53 +649,10 @@ This is a fully client-side application. Your content never leaves your browser
534649 titleSpan . textContent = tab . title || 'Untitled' ;
535650 titleSpan . title = tab . title || 'Untitled' ;
536651
537- // Three-dot menu button
538- const menuBtn = document . createElement ( 'button' ) ;
539- menuBtn . className = 'tab-menu-btn' ;
540- menuBtn . setAttribute ( 'aria-label' , 'File options' ) ;
541- menuBtn . title = 'File options' ;
542- menuBtn . innerHTML = '⋯' ;
543-
544- // Dropdown
545- const dropdown = document . createElement ( 'div' ) ;
546- dropdown . className = 'tab-menu-dropdown' ;
547- dropdown . innerHTML =
548- '<button class="tab-menu-item" data-action="rename"><i class="bi bi-pencil"></i> Rename</button>' +
549- '<button class="tab-menu-item" data-action="duplicate"><i class="bi bi-files"></i> Duplicate</button>' +
550- '<button class="tab-menu-item tab-menu-item-danger" data-action="delete"><i class="bi bi-trash"></i> Delete</button>' ;
551-
552- menuBtn . appendChild ( dropdown ) ;
553-
554- menuBtn . addEventListener ( 'click' , function ( e ) {
555- e . stopPropagation ( ) ;
556- // Close all other open dropdowns first
557- document . querySelectorAll ( '.tab-menu-btn.open' ) . forEach ( function ( btn ) {
558- if ( btn !== menuBtn ) btn . classList . remove ( 'open' ) ;
559- } ) ;
560- menuBtn . classList . toggle ( 'open' ) ;
561- // Position the dropdown relative to the viewport so it escapes the
562- // overflow scroll container on .tab-list
563- if ( menuBtn . classList . contains ( 'open' ) ) {
564- var rect = menuBtn . getBoundingClientRect ( ) ;
565- dropdown . style . top = ( rect . bottom + 4 ) + 'px' ;
566- dropdown . style . right = ( window . innerWidth - rect . right ) + 'px' ;
567- dropdown . style . left = 'auto' ;
568- }
569- } ) ;
570-
571- dropdown . querySelectorAll ( '.tab-menu-item' ) . forEach ( function ( actionBtn ) {
572- actionBtn . addEventListener ( 'click' , function ( e ) {
573- e . stopPropagation ( ) ;
574- menuBtn . classList . remove ( 'open' ) ;
575- const action = actionBtn . getAttribute ( 'data-action' ) ;
576- if ( action === 'rename' ) renameTab ( tab . id ) ;
577- else if ( action === 'duplicate' ) duplicateTab ( tab . id ) ;
578- else if ( action === 'delete' ) deleteTab ( tab . id ) ;
579- } ) ;
580- } ) ;
652+ const tabMenu = createTabActionMenu ( tab , { menuIdPrefix : 'desktop-tab-menu' } ) ;
581653
582654 item . appendChild ( titleSpan ) ;
583- item . appendChild ( menuBtn ) ;
655+ item . appendChild ( tabMenu . button ) ;
584656
585657 item . addEventListener ( 'click' , function ( ) {
586658 switchTab ( tab . id ) ;
@@ -655,56 +727,13 @@ This is a fully client-side application. Your content never leaves your browser
655727 titleSpan . textContent = tab . title || 'Untitled' ;
656728 titleSpan . title = tab . title || 'Untitled' ;
657729
658- // Three-dot menu button (same as desktop)
659- const menuBtn = document . createElement ( 'button' ) ;
660- menuBtn . className = 'tab-menu-btn' ;
661- menuBtn . setAttribute ( 'aria-label' , 'File options' ) ;
662- menuBtn . title = 'File options' ;
663- menuBtn . innerHTML = '⋯' ;
664-
665- // Dropdown (same as desktop)
666- const dropdown = document . createElement ( 'div' ) ;
667- dropdown . className = 'tab-menu-dropdown' ;
668- dropdown . innerHTML =
669- '<button class="tab-menu-item" data-action="rename"><i class="bi bi-pencil"></i> Rename</button>' +
670- '<button class="tab-menu-item" data-action="duplicate"><i class="bi bi-files"></i> Duplicate</button>' +
671- '<button class="tab-menu-item tab-menu-item-danger" data-action="delete"><i class="bi bi-trash"></i> Delete</button>' ;
672-
673- menuBtn . appendChild ( dropdown ) ;
674-
675- menuBtn . addEventListener ( 'click' , function ( e ) {
676- e . stopPropagation ( ) ;
677- document . querySelectorAll ( '.tab-menu-btn.open' ) . forEach ( function ( btn ) {
678- if ( btn !== menuBtn ) btn . classList . remove ( 'open' ) ;
679- } ) ;
680- menuBtn . classList . toggle ( 'open' ) ;
681- if ( menuBtn . classList . contains ( 'open' ) ) {
682- const rect = menuBtn . getBoundingClientRect ( ) ;
683- dropdown . style . top = ( rect . bottom + 4 ) + 'px' ;
684- dropdown . style . right = ( window . innerWidth - rect . right ) + 'px' ;
685- dropdown . style . left = 'auto' ;
686- }
687- } ) ;
688-
689- dropdown . querySelectorAll ( '.tab-menu-item' ) . forEach ( function ( actionBtn ) {
690- actionBtn . addEventListener ( 'click' , function ( e ) {
691- e . stopPropagation ( ) ;
692- menuBtn . classList . remove ( 'open' ) ;
693- const action = actionBtn . getAttribute ( 'data-action' ) ;
694- if ( action === 'rename' ) {
695- closeMobileMenu ( ) ;
696- renameTab ( tab . id ) ;
697- } else if ( action === 'duplicate' ) {
698- duplicateTab ( tab . id ) ;
699- closeMobileMenu ( ) ;
700- } else if ( action === 'delete' ) {
701- deleteTab ( tab . id ) ;
702- }
703- } ) ;
730+ const tabMenu = createTabActionMenu ( tab , {
731+ isMobileMenu : true ,
732+ menuIdPrefix : 'mobile-tab-menu'
704733 } ) ;
705734
706735 item . appendChild ( titleSpan ) ;
707- item . appendChild ( menuBtn ) ;
736+ item . appendChild ( tabMenu . button ) ;
708737
709738 item . addEventListener ( 'click' , function ( ) {
710739 switchTab ( tab . id ) ;
@@ -717,9 +746,7 @@ This is a fully client-side application. Your content never leaves your browser
717746
718747 // Close any open tab dropdown when clicking elsewhere in the document
719748 document . addEventListener ( 'click' , function ( ) {
720- document . querySelectorAll ( '.tab-menu-btn.open' ) . forEach ( function ( btn ) {
721- btn . classList . remove ( 'open' ) ;
722- } ) ;
749+ closeTabMenus ( ) ;
723750 } ) ;
724751
725752 function saveCurrentTabState ( ) {
@@ -850,12 +877,18 @@ This is a fully client-side application. Your content never leaves your browser
850877 alert ( 'Maximum of 20 tabs reached. Please close an existing tab to open a new one.' ) ;
851878 return ;
852879 }
880+ const shouldSwitchToDuplicate = tabId === activeTabId ;
853881 saveCurrentTabState ( ) ;
854882 const dupTitle = tab . title + ' (copy)' ;
855883 const dup = createTab ( tab . content , dupTitle , tab . viewMode ) ;
856884 const idx = tabs . findIndex ( function ( t ) { return t . id === tabId ; } ) ;
857885 tabs . splice ( idx + 1 , 0 , dup ) ;
858- switchTab ( dup . id ) ;
886+ if ( shouldSwitchToDuplicate ) {
887+ switchTab ( dup . id ) ;
888+ } else {
889+ saveTabsToStorage ( tabs ) ;
890+ renderTabBar ( tabs , activeTabId ) ;
891+ }
859892 }
860893
861894 function resetAllTabs ( ) {
@@ -2862,6 +2895,7 @@ This is a fully client-side application. Your content never leaves your browser
28622895 }
28632896 // Close Mermaid zoom modal with Escape
28642897 if ( e . key === "Escape" ) {
2898+ closeTabMenus ( ) ;
28652899 closeMermaidModal ( ) ;
28662900 }
28672901 } ) ;
0 commit comments