1
+ /**
2
+ * Menu Widget
3
+ *
4
+ * @author Michael van Engelshoven <mve@brainbits.net>
5
+ * @copyright 2011 Brainbits GmbH
6
+ * @version $id$
7
+ */
8
+ ( function ( $ , undefined ) {
9
+
10
+ $ . widget ( 'phlex.navigation' , {
11
+
12
+ /**
13
+ * Widget options
14
+ */
15
+ options : {
16
+ position : {
17
+ my : 'left top' ,
18
+ at : 'left bottom'
19
+ } ,
20
+ timeout : 350 ,
21
+ sensity : 10 ,
22
+ itemSelector : 'li' ,
23
+ submenuSelector : 'ul'
24
+ } ,
25
+
26
+ /**
27
+ * Indicates that the menu is open
28
+ */
29
+ _isOpen : false ,
30
+
31
+ /**
32
+ * jQuery object wich contains all navigation items
33
+ */
34
+ _items : [ ] ,
35
+
36
+ /**
37
+ * ID returned by setTimeoput for the close timer
38
+ */
39
+ _closeTimer : null ,
40
+
41
+ /**
42
+ * ID returned by setInterval for the mouse tracking
43
+ */
44
+ _trackTimer : null ,
45
+
46
+ /**
47
+ * Inititializes the navigation widget
48
+ */
49
+ _create : function ( ) {
50
+
51
+ var self = this ,
52
+ options = this . options
53
+ menu = this . element ;
54
+ items = this . _items = menu . find ( options . itemSelector ) ;
55
+
56
+ self . _setCurrent ( items . first ( ) ) ;
57
+
58
+ menu . bind ( 'mouseenter.' + self . widgetEventPrefix , $ . proxy ( self . _handleHover , self ) )
59
+ . bind ( 'mouseleave.' + self . widgetEventPrefix , $ . proxy ( self . _handleLeave , self ) )
60
+ . bind ( 'keydown.' + self . widgetEventPrefix , function ( event ) {
61
+ switch ( event . keyCode ) {
62
+ case $ . ui . keyCode . UP :
63
+ self . _handleToPrevious ( event ) ;
64
+ event . preventDefault ( ) ;
65
+ break ;
66
+ case $ . ui . keyCode . DOWN :
67
+ self . _handleToNext ( event ) ;
68
+ event . preventDefault ( ) ;
69
+ break ;
70
+ case $ . ui . keyCode . LEFT :
71
+ self . _handleToParent ( event ) ;
72
+ event . preventDefault ( ) ;
73
+ break ;
74
+ case $ . ui . keyCode . RIGHT :
75
+ self . _handleToSubmenu ( event ) ;
76
+ event . preventDefault ( ) ;
77
+ break ;
78
+ } ;
79
+ } ) ;
80
+
81
+ items . each ( function ( ) {
82
+
83
+ var item = $ ( this ) ,
84
+ button = item . children ( 'a' ) . first ( ) ,
85
+ submenu = item . children ( options . submenuSelector ) ;
86
+
87
+ button . button ( ) ;
88
+
89
+ button . attr ( 'role' , 'menuitem' ) ;
90
+
91
+ button . bind ( 'mouseenter.' + self . widgetEventPrefix + ' focus.' + self . widgetEventPrefix , function ( event ) {
92
+ self . _setCurrent ( item ) ;
93
+ if ( event . type === 'focus' ) {
94
+ self . open ( ) ;
95
+ }
96
+ } ) ;
97
+
98
+ // This is a workaroud for touch devices. The button can only be clicked, if the submenu is visible
99
+ button . bind ( 'click.' + self . widgetEventPrefix , function ( ) {
100
+ if ( submenu . length && ! submenu . is ( ':visible' ) ) {
101
+ return false ;
102
+ }
103
+ } ) ;
104
+
105
+ if ( submenu . length ) {
106
+ button . button ( 'option' , 'icons' , { secondary : "icon-arrow-down" } )
107
+ . attr ( 'aria-haspopup' , 'true' ) ;
108
+ }
109
+
110
+ } ) ;
111
+ } ,
112
+
113
+ /**
114
+ * Create option object for specified depth
115
+ *
116
+ * Some options can be configured as array, with differen values for different nestings. This method
117
+ * resolved the actual option values for the given depth. If given depth is not configured in array,
118
+ * the most recent one will be used.
119
+ *
120
+ * @param {integer } depth Nesting depth of item
121
+ * @return {object } Option object
122
+ */
123
+ _getOptionsForDepth : function ( depth ) {
124
+
125
+ var depthOptions = $ . extend ( { } , this . options ) ,
126
+ index ;
127
+
128
+ if ( $ . isArray ( depthOptions . position ) ) {
129
+ index = Math . min ( depth , depthOptions . position . length ) - 1 ;
130
+ depthOptions . position = depthOptions . position [ index ] ;
131
+ }
132
+
133
+ return depthOptions ;
134
+ } ,
135
+
136
+ _setCurrent : function ( elem ) {
137
+
138
+ link = elem . children ( 'a' ) . removeAttr ( 'tabindex' ) ;
139
+
140
+ this . _items . children ( 'a' )
141
+ . not ( link )
142
+ . attr ( 'tabindex' , '-1' ) ;
143
+
144
+ this . _current = elem ;
145
+ this . _refresh ( ) ;
146
+ } ,
147
+
148
+ /**
149
+ * Refreshes the display status of sub menues
150
+ */
151
+ _refresh : function ( ) {
152
+
153
+ var options = this . options ,
154
+ items = this . _items ,
155
+ menu = this . element ,
156
+ current = this . _current ,
157
+ link = current . children ( 'a' ) . first ( ) ,
158
+ depth = current . parents ( options . submenuSelector ) . not ( menu . parents ( ) ) . length
159
+ currentPath = current . parents ( options . itemSelector ) . andSelf ( ) ;
160
+
161
+ options = this . _getOptionsForDepth ( depth ) ;
162
+
163
+ this . _trigger ( 'refresh' ) ;
164
+
165
+ if ( this . _isOpen ) {
166
+ current . children ( options . submenuSelector )
167
+ . show ( )
168
+ . position ( $ . extend ( {
169
+ of : link ,
170
+ collision : 'none'
171
+ } , options . position ) ) ;
172
+
173
+ items . not ( currentPath )
174
+ . children ( options . submenuSelector )
175
+ . hide ( ) ;
176
+ } else {
177
+ items . children ( options . submenuSelector ) . hide ( ) ;
178
+ }
179
+ } ,
180
+
181
+ /**
182
+ * Handles action when mouse enters the menu
183
+ *
184
+ * @param {eventObject } event jQuery event object
185
+ */
186
+ _handleHover : function ( event ) {
187
+
188
+ clearTimeout ( this . _closeTimer ) ;
189
+
190
+ if ( ! this . _isOpen ) {
191
+ this . _trackMousemove ( event ) ;
192
+ }
193
+ } ,
194
+
195
+ /**
196
+ * Handles action when mouse leave the menu
197
+ *
198
+ * @param {eventObject } event jQuery event object
199
+ */
200
+ _handleLeave : function ( event ) {
201
+
202
+ var self = this ;
203
+
204
+ // Reset mouse tracking for opening
205
+ self . element . unbind ( 'mousemove.' + self . widgetEventPefix ) ;
206
+ clearInterval ( self . _trackTimer ) ;
207
+
208
+ if ( self . _isOpen ) {
209
+ self . _closeTimer = setTimeout ( function ( ) {
210
+ self . close ( ) ;
211
+ } , self . options . timeout ) ;
212
+ }
213
+ } ,
214
+
215
+ /**
216
+ * Handles keydown for the up key
217
+ */
218
+ _handleToPrevious : function ( event ) {
219
+ this . _current
220
+ . prev ( )
221
+ . children ( 'a' )
222
+ . focus ( ) ;
223
+ } ,
224
+
225
+ /**
226
+ * Handles keydown for the down key
227
+ */
228
+ _handleToNext : function ( event ) {
229
+ this . _current
230
+ . next ( )
231
+ . children ( 'a' )
232
+ . focus ( ) ;
233
+ } ,
234
+
235
+ /**
236
+ * Handles keydown for the left key
237
+ */
238
+ _handleToParent : function ( event ) {
239
+ this . _current
240
+ . parent ( this . options . submenuSelector )
241
+ . prev ( 'a' )
242
+ . focus ( ) ;
243
+ } ,
244
+
245
+ /**
246
+ * Handles keydown for the right key
247
+ */
248
+ _handleToSubmenu : function ( event ) {
249
+ this . open ( ) ;
250
+ this . _current
251
+ . children ( this . options . submenuSelector )
252
+ . children ( this . options . itemSelector )
253
+ . first ( )
254
+ . children ( 'a' )
255
+ . focus ( ) ;
256
+ } ,
257
+
258
+ /**
259
+ * Checks if user intend to open the menu.
260
+ *
261
+ * @param {eventObject } event jQuery event object
262
+ */
263
+ _trackMousemove : function ( event ) {
264
+
265
+ var self = this ,
266
+ menu = self . element ,
267
+ start = { x : event . clientX , y : event . clientY } ,
268
+ current = { x : event . clientX , y : event . clientY } ;
269
+
270
+ menu . bind ( 'mousemove.' + self . widgetEventPefix , function ( event ) {
271
+ current . x = event . clientX ;
272
+ current . y = event . clientY ;
273
+ } ) ;
274
+
275
+ self . _trackTimer = setInterval ( function ( ) {
276
+
277
+ var distance = Math . sqrt ( Math . pow ( start . x - current . x , 2 ) + Math . pow ( start . y - current . y , 2 ) ) ,
278
+ mouseTooSlow = self . options . sensity > distance ;
279
+
280
+ if ( ! mouseTooSlow ) {
281
+ start . x = current . x ;
282
+ start . y = current . y ;
283
+ } else {
284
+ // Stop tracking
285
+ clearInterval ( self . _trackTimer ) ;
286
+ menu . unbind ( 'mousemove.' + self . widgetEventPefix ) ;
287
+ // Open menu
288
+ self . open ( ) ;
289
+ }
290
+
291
+ } , 100 ) ;
292
+ } ,
293
+
294
+ /**
295
+ * Opens the menu
296
+ */
297
+ open : function ( ) {
298
+
299
+ if ( this . _trigger ( 'beforeopen' ) === false ) {
300
+ return ;
301
+ }
302
+
303
+ this . _trigger ( 'open' ) ;
304
+ this . _isOpen = true ;
305
+ this . _refresh ( ) ;
306
+ } ,
307
+
308
+ /**
309
+ * Closes the menu
310
+ */
311
+ close : function ( ) {
312
+
313
+ if ( this . _trigger ( 'beforeclose' ) === false ) {
314
+ return ;
315
+ }
316
+
317
+ this . _setCurrent ( this . _items . first ( ) ) ;
318
+ this . _trigger ( 'close' ) ;
319
+ this . _isOpen = false ;
320
+ this . _refresh ( ) ;
321
+ }
322
+
323
+ } ) ;
324
+
325
+ } ( jQuery ) ) ;
0 commit comments