17
17
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18
18
*/
19
19
20
+
21
+ /**
22
+ * menu-aim is a jQuery plugin for dropdown menus that can differentiate
23
+ * between a user trying hover over a dropdown item vs trying to navigate into
24
+ * a submenu's contents.
25
+ *
26
+ * menu-aim assumes that you have are using a menu with submenus that expand
27
+ * to the menu's right. It will fire events when the user's mouse enters a new
28
+ * dropdown item *and* when that item is being intentionally hovered over.
29
+ *
30
+ * __________________________
31
+ * | Monkeys >| Gorilla |
32
+ * | Gorillas >| Content |
33
+ * | Chimps >| Here |
34
+ * |___________|____________|
35
+ *
36
+ * In the above example, "Gorillas" is selected and its submenu content is
37
+ * being shown on the right. Imagine that the user's cursor is hovering over
38
+ * "Gorillas." When they move their mouse into the "Gorilla Content" area, they
39
+ * may briefly hover over "Chimps." This shouldn't close the "Gorilla Content"
40
+ * area.
41
+ *
42
+ * This problem is normally solved using timeouts and delays. menu-aim tries to
43
+ * solve this by detecting the direction of the user's mouse movement. This can
44
+ * make for quicker transitions when navigating up and down the menu. The
45
+ * experience is hopefully similar to amazon.com/'s "Shop by Department"
46
+ * dropdown.
47
+ *
48
+ * Use like so:
49
+ *
50
+ * $("#menu").menuAim({
51
+ * activate: $.noop, // fired on row activation
52
+ * deactivate: $.noop // fired on row deactivation
53
+ * });
54
+ *
55
+ * ...to receive events when a menu's row has been purposefully (de)activated.
56
+ *
57
+ * The following options can be passed to menuAim. All functions execute with
58
+ * the relevant row's HTML element as the execution context ('this'):
59
+ *
60
+ * .menuAim({
61
+ * // Function to call when a row is purposefully activated. Use this
62
+ * // to show a submenu's content for the activated row.
63
+ * activate: function() {},
64
+ *
65
+ * // Function to call when a row is deactivated.
66
+ * deactivate: function() {},
67
+ *
68
+ * // Function to call when mouse enters a menu row. Entering a row
69
+ * // does not mean the row has been activated, as the user may be
70
+ * // mousing over to a submenu.
71
+ * enter: function() {},
72
+ *
73
+ * // Function to call when mouse exits a menu row.
74
+ * exit: function() {},
75
+ *
76
+ * // Selector for identifying which elements in the menu are rows
77
+ * // that can trigger the above events. Defaults to "> li".
78
+ * rowSelector: "> li",
79
+ *
80
+ * // You may have some menu rows that aren't submenus and therefore
81
+ * // shouldn't ever need to "activate." If so, filter submenu rows w/
82
+ * // this selector. Defaults to "*" (all elements).
83
+ * submenuSelector: "*",
84
+ *
85
+ * // Direction the submenu opens relative to the main menu. Can be
86
+ * // left, right, above, or below. Defaults to "right".
87
+ * submenuDirection: "right"
88
+ * });
89
+ *
90
+ * https://github.com/kamens/jQuery-menu-aim
91
+ */
92
+ ( function ( $ ) {
93
+
94
+ $ . fn . menuAim = function ( opts ) {
95
+ // Initialize menu-aim for all elements in jQuery collection
96
+ this . each ( function ( ) {
97
+ init . call ( this , opts ) ;
98
+ } ) ;
99
+
100
+ return this ;
101
+ } ;
102
+
103
+ function init ( opts ) {
104
+ var $menu = $ ( this ) ,
105
+ activeRow = null ,
106
+ mouseLocs = [ ] ,
107
+ lastDelayLoc = null ,
108
+ timeoutId = null ,
109
+ options = $ . extend ( {
110
+ rowSelector : "> li" ,
111
+ submenuSelector : "*" ,
112
+ submenuDirection : $ . noop ,
113
+ tolerance : 75 , // bigger = more forgivey when entering submenu
114
+ enter : $ . noop ,
115
+ exit : $ . noop ,
116
+ activate : $ . noop ,
117
+ deactivate : $ . noop ,
118
+ exitMenu : $ . noop
119
+ } , opts ) ;
120
+
121
+ var MOUSE_LOCS_TRACKED = 3 , // number of past mouse locations to track
122
+ DELAY = 300 ; // ms delay when user appears to be entering submenu
123
+
124
+ /**
125
+ * Keep track of the last few locations of the mouse.
126
+ */
127
+ var mousemoveDocument = function ( e ) {
128
+ mouseLocs . push ( { x : e . pageX , y : e . pageY } ) ;
129
+
130
+ if ( mouseLocs . length > MOUSE_LOCS_TRACKED ) {
131
+ mouseLocs . shift ( ) ;
132
+ }
133
+ } ;
134
+
135
+ /**
136
+ * Cancel possible row activations when leaving the menu entirely
137
+ */
138
+ var mouseleaveMenu = function ( ) {
139
+ if ( timeoutId ) {
140
+ clearTimeout ( timeoutId ) ;
141
+ }
142
+
143
+ // If exitMenu is supplied and returns true, deactivate the
144
+ // currently active row on menu exit.
145
+ if ( options . exitMenu ( this ) ) {
146
+ if ( activeRow ) {
147
+ options . deactivate ( activeRow ) ;
148
+ }
149
+
150
+ activeRow = null ;
151
+ }
152
+ } ;
153
+
154
+ /**
155
+ * Trigger a possible row activation whenever entering a new row.
156
+ */
157
+ var mouseenterRow = function ( ) {
158
+ if ( timeoutId ) {
159
+ // Cancel any previous activation delays
160
+ clearTimeout ( timeoutId ) ;
161
+ }
162
+
163
+ options . enter ( this ) ;
164
+ possiblyActivate ( this ) ;
165
+ } ,
166
+ mouseleaveRow = function ( ) {
167
+ options . exit ( this ) ;
168
+ } ;
169
+
170
+ /*
171
+ * Immediately activate a row if the user clicks on it.
172
+ */
173
+ var clickRow = function ( ) {
174
+ activate ( this ) ;
175
+ } ;
176
+
177
+ /**
178
+ * Activate a menu row.
179
+ */
180
+ var activate = function ( row ) {
181
+ if ( row == activeRow ) {
182
+ return ;
183
+ }
184
+
185
+ if ( activeRow ) {
186
+ options . deactivate ( activeRow ) ;
187
+ }
188
+
189
+
190
+ options . activate ( row ) ;
191
+ activeRow = row ;
192
+ } ;
193
+
194
+ /**
195
+ * Possibly activate a menu row. If mouse movement indicates that we
196
+ * shouldn't activate yet because user may be trying to enter
197
+ * a submenu's content, then delay and check again later.
198
+ */
199
+ var possiblyActivate = function ( row ) {
200
+ var delay = activationDelay ( ) ;
201
+
202
+ if ( delay ) {
203
+ timeoutId = setTimeout ( function ( ) {
204
+ possiblyActivate ( row ) ;
205
+ } , delay ) ;
206
+ } else {
207
+ activate ( row ) ;
208
+ }
209
+ } ;
210
+
211
+ /**
212
+ * Return the amount of time that should be used as a delay before the
213
+ * currently hovered row is activated.
214
+ *
215
+ * Returns 0 if the activation should happen immediately. Otherwise,
216
+ * returns the number of milliseconds that should be delayed before
217
+ * checking again to see if the row should be activated.
218
+ */
219
+ var activationDelay = function ( ) {
220
+ if ( ! activeRow || ! $ ( activeRow ) . is ( options . submenuSelector ) ) {
221
+ // If there is no other submenu row already active, then
222
+ // go ahead and activate immediately.
223
+ return 0 ;
224
+ }
225
+
226
+ var offset = $menu . offset ( ) ,
227
+ upperLeft = {
228
+ x : offset . left ,
229
+ y : offset . top - options . tolerance
230
+ } ,
231
+ upperRight = {
232
+ x : offset . left + $menu . outerWidth ( ) ,
233
+ y : upperLeft . y
234
+ } ,
235
+ lowerLeft = {
236
+ x : offset . left ,
237
+ y : offset . top + $menu . outerHeight ( ) + options . tolerance
238
+ } ,
239
+ lowerRight = {
240
+ x : offset . left + $menu . outerWidth ( ) ,
241
+ y : lowerLeft . y
242
+ } ,
243
+ loc = mouseLocs [ mouseLocs . length - 1 ] ,
244
+ prevLoc = mouseLocs [ 0 ] ;
245
+
246
+ if ( ! loc ) {
247
+ return 0 ;
248
+ }
249
+
250
+ if ( ! prevLoc ) {
251
+ prevLoc = loc ;
252
+ }
253
+
254
+ if ( prevLoc . x < offset . left || prevLoc . x > lowerRight . x ||
255
+ prevLoc . y < offset . top || prevLoc . y > lowerRight . y ) {
256
+ // If the previous mouse location was outside of the entire
257
+ // menu's bounds, immediately activate.
258
+ return 0 ;
259
+ }
260
+
261
+ if ( lastDelayLoc &&
262
+ loc . x == lastDelayLoc . x && loc . y == lastDelayLoc . y ) {
263
+ // If the mouse hasn't moved since the last time we checked
264
+ // for activation status, immediately activate.
265
+ return 0 ;
266
+ }
267
+
268
+ // Detect if the user is moving towards the currently activated
269
+ // submenu.
270
+ //
271
+ // If the mouse is heading relatively clearly towards
272
+ // the submenu's content, we should wait and give the user more
273
+ // time before activating a new row. If the mouse is heading
274
+ // elsewhere, we can immediately activate a new row.
275
+ //
276
+ // We detect this by calculating the slope formed between the
277
+ // current mouse location and the upper/lower right points of
278
+ // the menu. We do the same for the previous mouse location.
279
+ // If the current mouse location's slopes are
280
+ // increasing/decreasing appropriately compared to the
281
+ // previous's, we know the user is moving toward the submenu.
282
+ //
283
+ // Note that since the y-axis increases as the cursor moves
284
+ // down the screen, we are looking for the slope between the
285
+ // cursor and the upper right corner to decrease over time, not
286
+ // increase (somewhat counterintuitively).
287
+ function slope ( a , b ) {
288
+ return ( b . y - a . y ) / ( b . x - a . x ) ;
289
+ } ;
290
+
291
+ var decreasingCorner = upperRight ,
292
+ increasingCorner = lowerRight ;
293
+
294
+ // Our expectations for decreasing or increasing slope values
295
+ // depends on which direction the submenu opens relative to the
296
+ // main menu. By default, if the menu opens on the right, we
297
+ // expect the slope between the cursor and the upper right
298
+ // corner to decrease over time, as explained above. If the
299
+ // submenu opens in a different direction, we change our slope
300
+ // expectations.
301
+ if ( options . submenuDirection ( ) == "left" ) {
302
+ decreasingCorner = lowerLeft ;
303
+ increasingCorner = upperLeft ;
304
+ } else if ( options . submenuDirection ( ) == "below" ) {
305
+ decreasingCorner = lowerRight ;
306
+ increasingCorner = lowerLeft ;
307
+ } else if ( options . submenuDirection ( ) == "above" ) {
308
+ decreasingCorner = upperLeft ;
309
+ increasingCorner = upperRight ;
310
+ }
311
+
312
+ var decreasingSlope = slope ( loc , decreasingCorner ) ,
313
+ increasingSlope = slope ( loc , increasingCorner ) ,
314
+ prevDecreasingSlope = slope ( prevLoc , decreasingCorner ) ,
315
+ prevIncreasingSlope = slope ( prevLoc , increasingCorner ) ;
316
+
317
+ if ( decreasingSlope < prevDecreasingSlope &&
318
+ increasingSlope > prevIncreasingSlope ) {
319
+ // Mouse is moving from previous location towards the
320
+ // currently activated submenu. Delay before activating a
321
+ // new menu row, because user may be moving into submenu.
322
+ lastDelayLoc = loc ;
323
+ return DELAY ;
324
+ }
325
+
326
+ lastDelayLoc = null ;
327
+ return 0 ;
328
+ } ;
329
+
330
+ $menu . on ( 'mouseenter' , function ( e ) {
331
+ if ( $menu . find ( '.context-menu-item-active' ) . length === 0 && $menu . find ( '.has-open-context-menu-submenu' ) . length === 0 )
332
+ activeRow = null ;
333
+ } )
334
+ /**
335
+ * Hook up initial menu events
336
+ */
337
+ $menu
338
+ . mouseleave ( mouseleaveMenu )
339
+ . find ( options . rowSelector )
340
+ . mouseenter ( mouseenterRow )
341
+ . mouseleave ( mouseleaveRow )
342
+ . click ( clickRow ) ;
343
+
344
+ $ ( document ) . mousemove ( mousemoveDocument ) ;
345
+
346
+ } ;
347
+ } ) ( jQuery ) ;
348
+
20
349
function UIContextMenu ( options ) {
21
350
$ ( '.window-active .window-app-iframe' ) . css ( 'pointer-events' , 'none' ) ;
22
351
@@ -184,8 +513,9 @@ function UIContextMenu(options){
184
513
// just passing over the item, the submenu doesn't open immediately.
185
514
let submenu_delay_timer ;
186
515
187
- // Initialize the menuAim plugin (../libs/jquery.menu-aim.js)
516
+ // Initialize the menuAim plugin
188
517
$ ( contextMenu ) . menuAim ( {
518
+ rowSelector : ".context-menu-item" ,
189
519
submenuSelector : ".context-menu-item-submenu" ,
190
520
submenuDirection : function ( ) {
191
521
// If not submenu
@@ -198,6 +528,10 @@ function UIContextMenu(options){
198
528
}
199
529
}
200
530
} ,
531
+ enter : function ( e ) {
532
+ // activate items
533
+ // this.activate(e);
534
+ } ,
201
535
// activates item when mouse enters depending on mouse position and direction
202
536
activate : function ( e ) {
203
537
// activate items
@@ -258,6 +592,7 @@ function UIContextMenu(options){
258
592
// remove `has-open-context-menu-submenu` class from the parent menu item
259
593
$ ( e ) . removeClass ( 'has-open-context-menu-submenu' ) ;
260
594
}
595
+
261
596
}
262
597
} ) ;
263
598
@@ -307,6 +642,14 @@ function UIContextMenu(options){
307
642
return false ;
308
643
} )
309
644
645
+ $ ( contextMenu ) . on ( "mouseleave" , function ( e ) {
646
+ $ ( contextMenu ) . find ( '.context-menu-item' ) . removeClass ( 'context-menu-item-active' ) ;
647
+ clearTimeout ( submenu_delay_timer ) ;
648
+ } )
649
+
650
+ $ ( contextMenu ) . on ( "mouseenter" , function ( e ) {
651
+ } )
652
+
310
653
return {
311
654
cancel : ( cancel_options ) => {
312
655
cancel_options_ = cancel_options ;
@@ -344,7 +687,14 @@ $(document).on('mouseenter', '.context-menu', function(e){
344
687
} )
345
688
346
689
$ ( document ) . on ( 'mouseenter' , '.context-menu-item' , function ( e ) {
347
- select_ctxmenu_item ( this ) ;
690
+ // select_ctxmenu_item(this);
348
691
} )
349
692
693
+ $ ( document ) . on ( 'mouseenter' , '.context-menu-divider' , function ( e ) {
694
+ // unselect all items
695
+ $ ( this ) . siblings ( '.context-menu-item:not(.has-open-context-menu-submenu)' ) . removeClass ( 'context-menu-item-active' ) ;
696
+ } )
697
+
698
+ $ ( document )
699
+
350
700
export default UIContextMenu ;
0 commit comments