77
88import 'vs/css!./menu' ;
99import { IDisposable } from 'vs/base/common/lifecycle' ;
10- import { IActionRunner , IAction } from 'vs/base/common/actions' ;
11- import { ActionBar , IActionItemProvider , ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar' ;
12- import { ResolvedKeybinding } from 'vs/base/common/keyCodes' ;
10+ import { IActionRunner , IAction , Action } from 'vs/base/common/actions' ;
11+ import { ActionBar , IActionItemProvider , ActionsOrientation , Separator , ActionItem , IActionItemOptions } from 'vs/base/browser/ui/actionbar/actionbar' ;
12+ import { ResolvedKeybinding , KeyCode } from 'vs/base/common/keyCodes' ;
1313import { Event } from 'vs/base/common/event' ;
14- import { addClass } from 'vs/base/browser/dom' ;
14+ import { addClass , EventType , EventHelper , EventLike } from 'vs/base/browser/dom' ;
15+ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent' ;
16+ import { $ } from 'vs/base/browser/builder' ;
1517
1618export interface IMenuOptions {
1719 context ?: any ;
@@ -21,6 +23,18 @@ export interface IMenuOptions {
2123 ariaLabel ?: string ;
2224}
2325
26+
27+ export class SubmenuAction extends Action {
28+ constructor ( label : string , public entries : ( SubmenuAction | IAction ) [ ] , cssClass ?: string ) {
29+ super ( ! ! cssClass ? cssClass : 'submenu' , label , '' , true ) ;
30+ }
31+ }
32+
33+ interface ISubMenuData {
34+ parent : Menu ;
35+ submenu ?: Menu ;
36+ }
37+
2438export class Menu {
2539
2640 private actionBar : ActionBar ;
@@ -35,9 +49,31 @@ export class Menu {
3549 menuContainer . setAttribute ( 'role' , 'presentation' ) ;
3650 container . appendChild ( menuContainer ) ;
3751
52+ let parentData : ISubMenuData = {
53+ parent : this
54+ } ;
55+
56+ const getActionItem = ( action : IAction ) => {
57+ if ( action instanceof Separator ) {
58+ return new ActionItem ( options . context , action , { icon : true } ) ;
59+ } else if ( action instanceof SubmenuAction ) {
60+ return new SubmenuActionItem ( action , action . entries , parentData , options ) ;
61+ } else {
62+ const menuItemOptions : IActionItemOptions = { } ;
63+ if ( options . getKeyBinding ) {
64+ const keybinding = options . getKeyBinding ( action ) ;
65+ if ( keybinding ) {
66+ menuItemOptions . keybinding = keybinding . getLabel ( ) ;
67+ }
68+ }
69+
70+ return new MenuActionItem ( options . context , action , menuItemOptions ) ;
71+ }
72+ } ;
73+
3874 this . actionBar = new ActionBar ( menuContainer , {
3975 orientation : ActionsOrientation . VERTICAL ,
40- actionItemProvider : options . actionItemProvider ,
76+ actionItemProvider : options . actionItemProvider ? options . actionItemProvider : getActionItem ,
4177 context : options . context ,
4278 actionRunner : options . actionRunner ,
4379 isMenu : true ,
@@ -70,4 +106,139 @@ export class Menu {
70106 this . listener = null ;
71107 }
72108 }
109+ }
110+
111+ class MenuActionItem extends ActionItem {
112+ static MNEMONIC_REGEX : RegExp = / & & ( .) / g;
113+
114+ constructor ( ctx : any , action : IAction , options : IActionItemOptions = { } ) {
115+ options . isMenu = true ;
116+ super ( action , action , options ) ;
117+ }
118+
119+ private _addMnemonic ( action : IAction , actionItemElement : HTMLElement ) : void {
120+ let matches = MenuActionItem . MNEMONIC_REGEX . exec ( action . label ) ;
121+ if ( matches && matches . length === 2 ) {
122+ let mnemonic = matches [ 1 ] ;
123+
124+ let ariaLabel = action . label . replace ( MenuActionItem . MNEMONIC_REGEX , mnemonic ) ;
125+
126+ actionItemElement . accessKey = mnemonic . toLocaleLowerCase ( ) ;
127+ this . $e . attr ( 'aria-label' , ariaLabel ) ;
128+ } else {
129+ this . $e . attr ( 'aria-label' , action . label ) ;
130+ }
131+ }
132+
133+ public render ( container : HTMLElement ) : void {
134+ super . render ( container ) ;
135+
136+ this . _addMnemonic ( this . getAction ( ) , container ) ;
137+ this . $e . attr ( 'role' , 'menuitem' ) ;
138+ }
139+
140+ public _updateLabel ( ) : void {
141+ if ( this . options . label ) {
142+ let label = this . getAction ( ) . label ;
143+ if ( label && this . options . isMenu ) {
144+ label = label . replace ( MenuActionItem . MNEMONIC_REGEX , '$1\u0332' ) ;
145+ }
146+ this . $e . text ( label ) ;
147+ }
148+ }
149+ }
150+
151+ class SubmenuActionItem extends MenuActionItem {
152+ private mysubmenu : Menu ;
153+
154+ constructor (
155+ action : IAction ,
156+ private submenuActions : IAction [ ] ,
157+ private parentData : ISubMenuData ,
158+ private submenuOptions ?: IMenuOptions
159+ ) {
160+ super ( action , action , { label : true , isMenu : true } ) ;
161+ }
162+
163+ public render ( container : HTMLElement ) : void {
164+ super . render ( container ) ;
165+
166+ this . builder = $ ( container ) ;
167+ $ ( this . builder ) . addClass ( 'monaco-submenu-item' ) ;
168+ $ ( 'span.submenu-indicator' ) . text ( '\u25B6' ) . appendTo ( this . builder ) ;
169+ this . $e . attr ( 'role' , 'menu' ) ;
170+
171+ $ ( this . builder ) . on ( EventType . KEY_UP , ( e ) => {
172+ let event = new StandardKeyboardEvent ( e as KeyboardEvent ) ;
173+ if ( event . equals ( KeyCode . RightArrow ) ) {
174+ EventHelper . stop ( e , true ) ;
175+
176+ this . createSubmenu ( ) ;
177+ }
178+ } ) ;
179+
180+ $ ( this . builder ) . on ( EventType . KEY_DOWN , ( e ) => {
181+ let event = new StandardKeyboardEvent ( e as KeyboardEvent ) ;
182+ if ( event . equals ( KeyCode . RightArrow ) ) {
183+ EventHelper . stop ( e , true ) ;
184+ }
185+ } ) ;
186+
187+ $ ( this . builder ) . on ( EventType . MOUSE_OVER , ( e ) => {
188+ this . cleanupExistingSubmenu ( false ) ;
189+ this . createSubmenu ( ) ;
190+ } ) ;
191+
192+
193+ $ ( this . builder ) . on ( EventType . MOUSE_LEAVE , ( e ) => {
194+ this . parentData . parent . focus ( ) ;
195+ this . cleanupExistingSubmenu ( true ) ;
196+ } ) ;
197+ }
198+
199+ public onClick ( e : EventLike ) {
200+ // stop clicking from trying to run an action
201+ EventHelper . stop ( e , true ) ;
202+ }
203+
204+ private cleanupExistingSubmenu ( force : boolean ) {
205+ if ( this . parentData . submenu && ( force || ( this . parentData . submenu !== this . mysubmenu ) ) ) {
206+ this . parentData . submenu . dispose ( ) ;
207+ this . parentData . submenu = null ;
208+ }
209+ }
210+
211+ private createSubmenu ( ) {
212+ if ( ! this . parentData . submenu ) {
213+ const submenuContainer = $ ( this . builder ) . div ( { class : 'monaco-submenu menubar-menu-items-holder context-view' } ) ;
214+
215+ $ ( submenuContainer ) . style ( {
216+ 'left' : `${ $ ( this . builder ) . getClientArea ( ) . width } px`
217+ } ) ;
218+
219+ $ ( submenuContainer ) . on ( EventType . KEY_UP , ( e ) => {
220+ let event = new StandardKeyboardEvent ( e as KeyboardEvent ) ;
221+ if ( event . equals ( KeyCode . LeftArrow ) ) {
222+ EventHelper . stop ( e , true ) ;
223+
224+ this . parentData . parent . focus ( ) ;
225+ this . parentData . submenu . dispose ( ) ;
226+ this . parentData . submenu = null ;
227+ }
228+ } ) ;
229+
230+ $ ( submenuContainer ) . on ( EventType . KEY_DOWN , ( e ) => {
231+ let event = new StandardKeyboardEvent ( e as KeyboardEvent ) ;
232+ if ( event . equals ( KeyCode . LeftArrow ) ) {
233+ EventHelper . stop ( e , true ) ;
234+ }
235+ } ) ;
236+
237+
238+ this . parentData . submenu = new Menu ( submenuContainer . getHTMLElement ( ) , this . submenuActions , this . submenuOptions ) ;
239+ this . parentData . submenu . focus ( ) ;
240+
241+ this . mysubmenu = this . parentData . submenu ;
242+ }
243+ }
73244}
0 commit comments