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