@@ -135,11 +135,11 @@ angular.module('material.components.menu', [
135
135
*
136
136
*/
137
137
138
- function MenuDirective ( ) {
138
+ function MenuDirective ( $mdMenu , $mdUtil , $timeout ) {
139
139
var INVALID_PREFIX = 'Invalid HTML for md-menu: ' ;
140
140
return {
141
141
restrict : 'E' ,
142
- require : 'mdMenu' ,
142
+ require : [ 'mdMenu' , '?mdMenuBar' ] ,
143
143
controller : 'mdMenuCtrl' , // empty function to be built by link
144
144
scope : true ,
145
145
compile : compile
@@ -149,25 +149,51 @@ function MenuDirective() {
149
149
templateElement . addClass ( 'md-menu' ) ;
150
150
var triggerElement = templateElement . children ( ) [ 0 ] ;
151
151
if ( ! triggerElement . hasAttribute ( 'ng-click' ) ) {
152
- triggerElement = triggerElement . querySelector ( '[ng-click],[ng-mouseenter]' ) ;
152
+ triggerElement = triggerElement . querySelector ( '[ng-click],[ng-mouseenter]' ) || triggerElement ;
153
153
}
154
+ if ( triggerElement && (
155
+ triggerElement . nodeName == 'MD-BUTTON' ||
156
+ triggerElement . nodeName == 'BUTTON'
157
+ ) && ! triggerElement . hasAttribute ( 'type' ) ) {
158
+ triggerElement . setAttribute ( 'type' , 'button' ) ;
159
+ }
160
+
154
161
if ( templateElement . children ( ) . length != 2 ) {
155
162
throw Error ( INVALID_PREFIX + 'Expected two children elements.' ) ;
156
163
}
157
164
158
165
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
159
166
triggerElement && triggerElement . setAttribute ( 'aria-haspopup' , 'true' ) ;
167
+
168
+ var nestedMenus = templateElement [ 0 ] . querySelectorAll ( 'md-menu' ) ;
169
+ var nestingDepth = parseInt ( templateElement [ 0 ] . getAttribute ( 'md-nest-level' ) , 10 ) || 0 ;
170
+ if ( nestedMenus ) {
171
+ angular . forEach ( $mdUtil . nodesToArray ( nestedMenus ) , function ( menuEl ) {
172
+ if ( ! menuEl . hasAttribute ( 'md-position-mode' ) ) {
173
+ menuEl . setAttribute ( 'md-position-mode' , 'cascade' ) ;
174
+ }
175
+ menuEl . classList . add ( 'md-nested-menu' ) ;
176
+ menuEl . setAttribute ( 'md-nest-level' , nestingDepth + 1 ) ;
177
+ menuEl . setAttribute ( 'role' , 'menu' ) ;
178
+ } ) ;
179
+ }
160
180
return link ;
161
181
}
162
182
163
- function link ( scope , element , attrs , mdMenuCtrl ) {
183
+ function link ( scope , element , attrs , ctrls ) {
184
+ var mdMenuCtrl = ctrls [ 0 ] ;
185
+ var isInMenuBar = ctrls [ 1 ] != undefined ;
164
186
// Move everything into a md-menu-container and pass it to the controller
165
187
var menuContainer = angular . element (
166
188
'<div class="md-open-menu-container md-whiteframe-z2"></div>'
167
189
) ;
168
190
var menuContents = element . children ( ) [ 1 ] ;
169
191
menuContainer . append ( menuContents ) ;
170
- mdMenuCtrl . init ( menuContainer ) ;
192
+ if ( isInMenuBar ) {
193
+ element . append ( menuContainer ) ;
194
+ menuContainer [ 0 ] . style . display = 'none' ;
195
+ }
196
+ mdMenuCtrl . init ( menuContainer , { isInMenuBar : isInMenuBar } ) ;
171
197
172
198
scope . $on ( '$destroy' , function ( ) {
173
199
menuContainer . remove ( ) ;
@@ -177,69 +203,151 @@ function MenuDirective() {
177
203
}
178
204
}
179
205
180
- function MenuController ( $mdMenu , $attrs , $element , $scope ) {
206
+ function MenuController ( $mdMenu , $attrs , $element , $scope , $mdUtil , $timeout ) {
207
+
181
208
var menuContainer ;
182
- var ctrl = this ;
209
+ var self = this ;
183
210
var triggerElement ;
184
211
185
- this . init = angular . bind ( this , init ) ;
186
- this . open = angular . bind ( this , openMenu ) ;
187
- this . close = angular . bind ( this , closeMenu ) ;
188
-
189
- this . positionMode = angular . bind ( this , positionMode ) ;
190
- this . offsets = angular . bind ( this , offsets ) ;
191
-
192
- // Expose a open function to the child scope for html to use
193
- $scope . $mdOpenMenu = this . open ;
212
+ this . nestLevel = parseInt ( $attrs . mdNestLevel , 10 ) || 0 ;
194
213
195
214
/**
196
215
* Called by our linking fn to provide access to the menu-content
197
216
* element removed during link
198
217
*/
199
- function init ( setMenuContainer ) {
218
+ this . init = function init ( setMenuContainer , opts ) {
219
+ opts = opts || { } ;
200
220
menuContainer = setMenuContainer ;
201
221
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
202
222
triggerElement = $element [ 0 ] . querySelector ( '[ng-click],[ng-mouseenter]' ) ;
203
- }
223
+
224
+ this . isInMenuBar = opts . isInMenuBar ;
225
+ this . nestedMenus = $mdUtil . nodesToArray ( menuContainer [ 0 ] . querySelectorAll ( '.md-nested-menu' ) ) ;
226
+ this . enableHoverListener ( ) ;
227
+
228
+ menuContainer . on ( '$mdInterimElementRemove' , function ( ) {
229
+ self . isOpen = false ;
230
+ } ) ;
231
+ } ;
232
+
233
+ this . enableHoverListener = function ( ) {
234
+ $scope . $on ( '$mdMenuOpen' , function ( event , el ) {
235
+ if ( menuContainer [ 0 ] . contains ( el [ 0 ] ) ) {
236
+ self . currentlyOpenMenu = el . controller ( 'mdMenu' ) ;
237
+ self . isAlreadyOpening = false ;
238
+ self . currentlyOpenMenu . registerContainerProxy ( self . triggerContainerProxy . bind ( self ) ) ;
239
+ }
240
+ } ) ;
241
+ $scope . $on ( '$mdMenuClose' , function ( event , el ) {
242
+ if ( menuContainer [ 0 ] . contains ( el [ 0 ] ) ) {
243
+ self . currentlyOpenMenu = undefined ;
244
+ }
245
+ } ) ;
246
+
247
+ var menuItems = angular . element ( $mdUtil . nodesToArray ( menuContainer [ 0 ] . querySelectorAll ( 'md-menu-item' ) ) ) ;
248
+
249
+ var openMenuTimeout ;
250
+ menuItems . on ( 'mouseenter' , function ( event ) {
251
+ if ( self . isAlreadyOpening ) return ;
252
+ var nestedMenu = (
253
+ event . target . querySelector ( 'md-menu' )
254
+ || $mdUtil . getClosest ( event . target , 'MD-MENU' )
255
+ ) ;
256
+ openMenuTimeout = $timeout ( function ( ) {
257
+ if ( nestedMenu ) {
258
+ nestedMenu = angular . element ( nestedMenu ) . controller ( 'mdMenu' ) ;
259
+ }
260
+
261
+ if ( self . currentlyOpenMenu && self . currentlyOpenMenu != nestedMenu ) {
262
+ var closeTo = self . nestLevel + 1 ;
263
+ self . currentlyOpenMenu . close ( true , { closeTo : closeTo } ) ;
264
+ } else if ( nestedMenu && ! nestedMenu . isOpen && nestedMenu . open ) {
265
+ self . isAlreadyOpening = true ;
266
+ nestedMenu . open ( ) ;
267
+ }
268
+ } , nestedMenu ? 100 : 250 ) ;
269
+ var focusableTarget = event . currentTarget . querySelector ( '[tabindex]' ) ;
270
+ focusableTarget && focusableTarget . focus ( ) ;
271
+ } ) ;
272
+ menuItems . on ( 'mouseleave' , function ( event ) {
273
+ if ( openMenuTimeout ) {
274
+ $timeout . cancel ( openMenuTimeout ) ;
275
+ openMenuTimeout = undefined ;
276
+ }
277
+ } ) ;
278
+ } ;
204
279
205
280
/**
206
281
* Uses the $mdMenu interim element service to open the menu contents
207
282
*/
208
- function openMenu ( ev ) {
283
+ this . open = function openMenu ( ev ) {
209
284
ev && ev . stopPropagation ( ) ;
210
-
285
+ ev && ev . preventDefault ( ) ;
286
+ if ( self . isOpen ) return ;
287
+ self . isOpen = true ;
211
288
triggerElement = triggerElement || ( ev ? ev . target : $element [ 0 ] ) ;
212
- triggerElement . setAttribute ( 'aria-expanded' , 'true' ) ;
213
-
214
- ctrl . isOpen = true ;
289
+ $scope . $emit ( '$mdMenuOpen' , $element ) ;
215
290
$mdMenu . show ( {
216
291
scope : $scope ,
217
- mdMenuCtrl : ctrl ,
292
+ mdMenuCtrl : self ,
293
+ nestLevel : self . nestLevel ,
218
294
element : menuContainer ,
219
- target : triggerElement
295
+ target : triggerElement ,
296
+ preserveElement : self . isInMenuBar || self . nestedMenus . length > 0 ,
297
+ parent : self . isInMenuBar ? $element : 'body'
220
298
} ) ;
221
299
}
222
300
223
- /**
224
- * Use the $mdMenu interim element service to close the menu contents
225
- */
226
- function closeMenu ( skipFocus ) {
227
- if ( ! ctrl . isOpen ) return ;
301
+ // Expose a open function to the child scope for html to use
302
+ $scope . $mdOpenMenu = this . open ;
303
+
304
+ $scope . $watch ( function ( ) { return self . isOpen ; } , function ( isOpen ) {
305
+ if ( isOpen ) {
306
+ triggerElement . setAttribute ( 'aria-expanded' , 'true' ) ;
307
+ $element [ 0 ] . classList . add ( 'md-open' ) ;
308
+ angular . forEach ( self . nestedMenus , function ( el ) {
309
+ el . classList . remove ( 'md-open' ) ;
310
+ } ) ;
311
+ } else {
312
+ triggerElement && triggerElement . setAttribute ( 'aria-expanded' , 'false' ) ;
313
+ $element [ 0 ] . classList . remove ( 'md-open' ) ;
314
+ }
315
+ $scope . $mdMenuIsOpen = self . isOpen ;
316
+ } ) ;
317
+
318
+ this . focusMenuContainer = function focusMenuContainer ( ) {
319
+ var focusTarget = menuContainer [ 0 ] . querySelector ( '[md-menu-focus-target]' ) ;
320
+ if ( ! focusTarget ) focusTarget = menuContainer [ 0 ] . querySelector ( '.md-button' ) ;
321
+ focusTarget . focus ( ) ;
322
+ } ;
323
+
324
+ this . registerContainerProxy = function registerContainerProxy ( handler ) {
325
+ this . containerProxy = handler ;
326
+ } ;
327
+
328
+ this . triggerContainerProxy = function triggerContainerProxy ( ev ) {
329
+ this . containerProxy && this . containerProxy ( ev ) ;
330
+ } ;
228
331
229
- ctrl . isOpen = false ;
230
- triggerElement && triggerElement . setAttribute ( 'aria-expanded' , 'false' ) ;
231
- $mdMenu . hide ( ) ;
332
+ // Use the $mdMenu interim element service to close the menu contents
333
+ this . close = function closeMenu ( skipFocus , closeOpts ) {
334
+ if ( ! self . isOpen ) return ;
335
+ self . isOpen = false ;
232
336
337
+ $scope . $emit ( '$mdMenuClose' , $element ) ;
338
+ $mdMenu . hide ( null , closeOpts ) ;
233
339
if ( ! skipFocus ) {
234
- $element . children ( ) [ 0 ] . focus ( ) ;
340
+ var el = self . restoreFocusTo || $element . find ( 'button' ) [ 0 ] ;
341
+ if ( el instanceof angular . element ) el = el [ 0 ] ;
342
+ el . focus ( ) ;
235
343
}
236
344
}
237
345
238
346
/**
239
347
* Build a nice object out of our string attribute which specifies the
240
348
* target mode for left and top positioning
241
349
*/
242
- function positionMode ( ) {
350
+ this . positionMode = function positionMode ( ) {
243
351
var attachment = ( $attrs . mdPositionMode || 'target' ) . split ( ' ' ) ;
244
352
245
353
// If attachment is a single item, duplicate it for our second value.
@@ -258,7 +366,7 @@ function MenuController($mdMenu, $attrs, $element, $scope) {
258
366
* Build a nice object out of our string attribute which specifies
259
367
* the offset of top and left in pixels.
260
368
*/
261
- function offsets ( ) {
369
+ this . offsets = function offsets ( ) {
262
370
var offsets = ( $attrs . mdOffset || '0 0' ) . split ( ' ' ) . map ( parseFloat ) ;
263
371
if ( offsets . length == 2 ) {
264
372
return {
0 commit comments