vendor: 12,369 bytes, lines 1-324
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 * MIT License 72*/ 73(function($) { 74 75 $.fn.menuAim = function(opts) { 76 // Initialize menu-aim for all elements in jQuery collection 77 this.each(function() { 78 init.call(this, opts); 79 }); 80 81 return this; 82 }; 83 84 function init(opts) { 85 var $menu = $(this), 86 activeRow = null, 87 mouseLocs = [], 88 lastDelayLoc = null, 89 timeoutId = null, 90 options = $.extend({ 91 rowSelector: "> li", 92 submenuSelector: "*", 93 submenuDirection: "right", 94 tolerance: 75, // bigger = more forgivey when entering submenu 95 enter: $.noop, 96 exit: $.noop, 97 activate: $.noop, 98 deactivate: $.noop, 99 exitMenu: $.noop 100 }, opts); 101 102 var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track 103 DELAY = 300; // ms delay when user appears to be entering submenu 104 105 /** 106 * Keep track of the last few locations of the mouse. 107 */ 108 var mousemoveDocument = function(e) { 109 mouseLocs.push({x: e.pageX, y: e.pageY}); 110 111 if (mouseLocs.length > MOUSE_LOCS_TRACKED) { 112 mouseLocs.shift(); 113 } 114 }; 115 116 /** 117 * Cancel possible row activations when leaving the menu entirely 118 */ 119 var mouseleaveMenu = function() { 120 if (timeoutId) { 121 clearTimeout(timeoutId); 122 } 123 124 // If exitMenu is supplied and returns true, deactivate the 125 // currently active row on menu exit. 126 if (options.exitMenu(this)) { 127 if (activeRow) { 128 options.deactivate(activeRow); 129 } 130 131 activeRow = null; 132 } 133 }; 134 135 /** 136 * Trigger a possible row activation whenever entering a new row. 137 */ 138 var mouseenterRow = function() { 139 if (timeoutId) { 140 // Cancel any previous activation delays 141 clearTimeout(timeoutId); 142 } 143 144 options.enter(this); 145 possiblyActivate(this); 146 }, 147 mouseleaveRow = function() { 148 options.exit(this); 149 }; 150 151 /* 152 * Immediately activate a row if the user clicks on it. 153 */ 154 var clickRow = function() { 155 activate(this); 156 }; 157 158 /** 159 * Activate a menu row. 160 */ 161 var activate = function(row) { 162 if (row == activeRow) { 163 return; 164 } 165 166 if (activeRow) { 167 options.deactivate(activeRow); 168 } 169 170 options.activate(row); 171 activeRow = row; 172 }; 173 174 /** 175 * Possibly activate a menu row. If mouse movement indicates that we 176 * shouldn't activate yet because user may be trying to enter 177 * a submenu's content, then delay and check again later. 178 */ 179 var possiblyActivate = function(row) { 180 var delay = activationDelay(); 181 182 if (delay) { 183 timeoutId = setTimeout(function() { 184 possiblyActivate(row); 185 }, delay); 186 } else { 187 activate(row); 188 } 189 }; 190 191 /** 192 * Return the amount of time that should be used as a delay before the 193 * currently hovered row is activated. 194 * 195 * Returns 0 if the activation should happen immediately. Otherwise, 196 * returns the number of milliseconds that should be delayed before 197 * checking again to see if the row should be activated. 198 */ 199 var activationDelay = function() { 200 if (!activeRow || !$(activeRow).is(options.submenuSelector)) { 201 // If there is no other submenu row already active, then 202 // go ahead and activate immediately. 203 return 0; 204 } 205 206 var offset = $menu.offset(), 207 upperLeft = { 208 x: offset.left, 209 y: offset.top - options.tolerance 210 }, 211 upperRight = { 212 x: offset.left + $menu.outerWidth(), 213 y: upperLeft.y 214 }, 215 lowerLeft = { 216 x: offset.left, 217 y: offset.top + $menu.outerHeight() + options.tolerance 218 }, 219 lowerRight = { 220 x: offset.left + $menu.outerWidth(), 221 y: lowerLeft.y 222 }, 223 loc = mouseLocs[mouseLocs.length - 1], 224 prevLoc = mouseLocs[0]; 225 226 if (!loc) { 227 return 0; 228 } 229 230 if (!prevLoc) { 231 prevLoc = loc; 232 } 233 234 if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || 235 prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { 236 // If the previous mouse location was outside of the entire 237 // menu's bounds, immediately activate. 238 return 0; 239 } 240 241 if (lastDelayLoc && 242 loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) { 243 // If the mouse hasn't moved since the last time we checked 244 // for activation status, immediately activate. 245 return 0; 246 } 247 248 // Detect if the user is moving towards the currently activated 249 // submenu. 250 // 251 // If the mouse is heading relatively clearly towards 252 // the submenu's content, we should wait and give the user more 253 // time before activating a new row. If the mouse is heading 254 // elsewhere, we can immediately activate a new row. 255 // 256 // We detect this by calculating the slope formed between the 257 // current mouse location and the upper/lower right points of 258 // the menu. We do the same for the previous mouse location. 259 // If the current mouse location's slopes are 260 // increasing/decreasing appropriately compared to the 261 // previous's, we know the user is moving toward the submenu. 262 // 263 // Note that since the y-axis increases as the cursor moves 264 // down the screen, we are looking for the slope between the 265 // cursor and the upper right corner to decrease over time, not 266 // increase (somewhat counterintuitively). 267 function slope(a, b) { 268 return (b.y - a.y) / (b.x - a.x); 269 }; 270 271 var decreasingCorner = upperRight, 272 increasingCorner = lowerRight; 273 274 // Our expectations for decreasing or increasing slope values 275 // depends on which direction the submenu opens relative to the 276 // main menu. By default, if the menu opens on the right, we 277 // expect the slope between the cursor and the upper right 278 // corner to decrease over time, as explained above. If the 279 // submenu opens in a different direction, we change our slope 280 // expectations. 281 if (options.submenuDirection == "left") { 282 decreasingCorner = lowerLeft; 283 increasingCorner = upperLeft; 284 } else if (options.submenuDirection == "below") { 285 decreasingCorner = lowerRight; 286 increasingCorner = lowerLeft; 287 } else if (options.submenuDirection == "above") { 288 decreasingCorner = upperLeft; 289 increasingCorner = upperRight; 290 } 291 292 var decreasingSlope = slope(loc, decreasingCorner), 293 increasingSlope = slope(loc, increasingCorner), 294 prevDecreasingSlope = slope(prevLoc, decreasingCorner), 295 prevIncreasingSlope = slope(prevLoc, increasingCorner); 296 297 if (decreasingSlope < prevDecreasingSlope && 298 increasingSlope > prevIncreasingSlope) { 299 // Mouse is moving from previous location towards the 300 // currently activated submenu. Delay before activating a 301 // new menu row, because user may be moving into submenu. 302 lastDelayLoc = loc; 303 return DELAY; 304 } 305 306 lastDelayLoc = null; 307 return 0; 308 }; 309 310 /** 311 * Hook up initial menu events 312 */ 313 $menu 314 .mouseleave(mouseleaveMenu) 315 .find(options.rowSelector) 316 .mouseenter(mouseenterRow) 317 .mouseleave(mouseleaveRow) 318 .click(clickRow); 319 320 $(document).mousemove(mousemoveDocument); 321 322 }; 323})(jQuery); 324
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.