1;(function ( $, window, document, undefined ) { 2 var pluginName = 'menuAim', 3 defaults = { 4 triggerEvent: "hover", // A means of activating submenu - either 'hover' or 'click' or 'both 5 rowSelector: "> li", // Selector for identifying which elements in the menu are rows 6 handle: "> a", // Handle for triggering mouse clicks on menu item 7 submenuSelector: "*", // You may have some menu rows that aren't submenus and therefore 8 // shouldn't ever need to "activate." If so, filter submenu rows w/ 9 // this selector. Defaults to "*" (all elements). 10 submenuDirection: "right", // Direction the submenu opens relative to the main menu. Can be 11 // left, right, above, or below. Defaults to "right". 12 openClassName: "open", // Class that will be applied to menu item when activated 13 tolerance: 75, // Bigger = more tolerant for mouse movement precision when entering submenu 14 activationDelay: 0, // Delay (ms) for first submenu opening 15 mouseLocsTracked: 3, // Number of past mouse locations to track direction 16 defaultDelay: 700, // Delay (ms) when user appears to be entering submenu 17 enterCallback: $.noop, // Function to call when mouse enters a menu row. Entering a row does not mean 18 // the row has been activated, as the user may be mousing over to a submenu. 19 activateCallback: $.noop, // Function to call when a row is purposefully activated. Use this 20 // to show a submenu's content for the activated row. 21 deactivateCallback: $.noop, // Function to call when a row is deactivated. 22 exitCallback: $.noop, // Function to call when mouse exits a menu row. 23 exitMenuCallback: $.noop // Function to call when mouse exits whole menu - for autoclosing submenu 24 }; 25 function Plugin( el, options ) { 26 this.el = el; 27 this.options = $.extend( {}, defaults, options); 28 this._defaults = defaults; 29 this._name = pluginName; 30 this.init(); 31 } 32 Plugin.prototype = { 33 init: function () { 34 this.activeRow = null, 35 this.mouseLocs = [], 36 this.lastDelayLoc = null, 37 this.timeoutId = null, 38 this.openDelayId = null, 39 this.isOnClick = $.inArray(this.options.triggerEvent, ['both', 'click']) > -1, 40 this.isOnHover = $.inArray(this.options.triggerEvent, ['both', 'hover']) > -1; 41 /** 42 * Hook up initial menu events 43 */ 44 if (this.isOnHover) { 45 this._hoverTriggerOn(); 46 } 47 if (this.isOnClick) { 48 this._clickTriggerOn(); 49 } 50 }, 51 // Private methods start with underscore 52 /** 53 * Keep track of the last few locations of the mouse. 54 */ 55 _mouseMoveDocument: function(e) { 56 obj = e.data.obj; 57 obj.mouseLocs.push({x: e.pageX, y: e.pageY}); 58 if (obj.mouseLocs.length > obj.options.mouseLocsTracked) { 59 obj.mouseLocs.shift(); 60 } 61 }, 62 /** 63 * Cancel possible row activations when leaving the menu entirely 64 */ 65 _mouseLeaveMenu: function(e) { 66 obj = e.data.obj; 67 if (obj.timeoutId) { 68 clearTimeout(obj.timeoutId); 69 } 70 if (obj.openDelayId) { 71 clearTimeout(obj.openDelayId); 72 } 73 obj._possiblyDeactivate(obj.activeRow); 74 obj.options.exitMenuCallback(this); 75 }, 76 /** 77 * Trigger a possible row activation whenever entering a new row. 78 */ 79 _mouseEnterRow: function(e) { 80 obj = e.data.obj; 81 if (obj.timeoutId) { 82 // Cancel any previous activation delays 83 clearTimeout(obj.timeoutId); 84 } 85 obj.options.enterCallback(this); 86 obj._possiblyActivate(this); 87 }, 88 _mouseLeaveRow: function(e) { 89 e.data.obj.options.exitCallback(this); 90 }, 91 /* 92 * Immediately activate a row if the user clicks on it. 93 */ 94 _clickRow: function(e) { 95 obj = e.data.obj; 96 obj._activate(this);
97 // bind close event when submenu content is rendered 98 $(obj.el) 99 .find(obj.options.rowSelector) 100 .find(obj.options.handle) 101 .on('click', { obj: obj }, obj._clickRowHandle); 102 }, 103 /* 104 * Close already opened submenu 105 */ 106 _clickRowHandle: function(e) { 107 obj = e.data.obj; 108 if ($(this).closest('li').hasClass(obj.options.openClassName)) { 109 obj._deactivate(); 110 e.stopPropagation(); 111 } 112 }, 113 /** 114 * Activate a menu row with possible delay 115 */ 116 _activate: function(row) { 117 var obj = this; 118 if (row == this.activeRow) { 119 return; 120 } 121 if (this.openDelayId) { 122 clearTimeout(this.openDelayId); 123 } 124 // check if delay should be placed before activation 125 if (parseInt(obj.options.activationDelay, 0) > 0 && obj.isOnHover) { 126 if (obj.activeRow) { 127 obj._activateWithoutDelay(row); 128 } else { 129 this.openDelayId = setTimeout(function() { 130 obj._activateWithoutDelay(row); 131 }, 132 obj.options.activationDelay); 133 } 134 } else { 135 obj._activateWithoutDelay(row); 136 } 137 }, 138 _activateWithoutDelay: function(row) { 139 if (this.activeRow) { 140 this.options.deactivateCallback(this.activeRow); 141 } 142 this.options.activateCallback(row); 143 this.activeRow = row; 144 }, 145 _deactivate: function() { 146 if (this.openDelayId) { 147 clearTimeout(this.openDelayId); 148 } 149 if (this.activeRow) { 150 this.options.deactivateCallback(this.activeRow); 151 this.activeRow = null; 152 } 153 }, 154 /** 155 * Possibly activate a menu row. If mouse movement indicates that we 156 * shouldn't activate yet because user may be trying to enter 157 * a submenu's content, then delay and check again later. 158 */ 159 _possiblyActivate: function(row) { 160 var delay = this._activationDelay(), 161 that = this; 162 if (delay) { 163 this.timeoutId = setTimeout(function() { 164 that._possiblyActivate(row); 165 }, delay); 166 } else { 167 this._activate(row); 168 } 169 }, 170 _possiblyDeactivate: function(row) { 171 var delay = this._activationDelay(), 172 that = this; 173 if (delay) { 174 this.timeoutId = setTimeout(function() { 175 that._possiblyDeactivate(row); 176 }, delay) 177 } else { 178 this.options.deactivateCallback(row); 179 this.activeRow = null; 180 } 181 }, 182 /** 183 * Return the amount of time that should be used as a delay before the 184 * currently hovered row is activated. 185 * 186 * Returns 0 if the activation should happen immediately. Otherwise, 187 * returns the number of milliseconds that should be delayed before 188 * checking again to see if the row should be activated. 189 */ 190 _activationDelay: function() { 191 if (!this.activeRow || !$(this.activeRow).is(this.options.submenuSelector)) { 192 // If there is no other submenu row already active, then 193 // go ahead and activate immediately. 194 return 0; 195 } 196 var offset = $(this.el).offset(), 197 upperLeft = { 198 x: offset.left, 199 y: offset.top - this.options.tolerance 200 }, 201 upperRight = { 202 x: offset.left + $(this.el).outerWidth(), 203 y: upperLeft.y 204 }, 205 lowerLeft = { 206 x: offset.left, 207 y: offset.top + $(this.el).outerHeight() + this.options.tolerance 208 }, 209 lowerRight = { 210 x: offset.left + $(this.el).outerWidth(), 211 y: lowerLeft.y 212 }, 213 loc = this.mouseLocs[this.mouseLocs.length - 1], 214 prevLoc = this.mouseLocs[0]; 215 if (!loc) { 216 return 0; 217 } 218 if (!prevLoc) { 219 prevLoc = loc; 220 } 221 if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || 222 prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { 223 // If the previous mouse location was outside of the entire 224 // menu's bounds, immediately activate. 225 return 0; 226 } 227 if (this.lastDelayLoc && loc.x == this.lastDelayLoc.x && loc.y == this.lastDelayLoc.y) { 228 // If the mouse hasn't moved since the last time we checked 229 // for activation status, immediately activate. 230 return 0; 231 } 232 // Detect if the user is moving towards the currently activated 233 // submenu. 234 // 235 // If the mouse is heading relatively clearly towards 236 // the submenu's content, we should wait and give the user more 237 // time before activating a new row. If the mouse is heading 238 // elsewhere, we can immediately activate a new row. 239 // 240 // We detect this by calculating the slope formed between the 241 // current mouse location and the upper/lower right points of 242 // the menu. We do the same for the previous mouse location. 243 // If the current mouse location's slopes are 244 // increasing/decreasing appropriately compared to the 245 // previous's, we know the user is moving toward the submenu. 246 // 247 // Note that since the y-axis increases as the cursor moves 248 // down the screen, we are looking for the slope between the 249 // cursor and the upper right corner to decrease over time, not 250 // increase (somewhat counterintuitively). 251 function slope(a, b) { 252 return (b.y - a.y) / (b.x - a.x); 253 }; 254 var decreasingCorner = upperRight, 255 increasingCorner = lowerRight; 256 // Our expectations for decreasing or increasing slope values 257 // depends on which direction the submenu opens relative to the 258 // main menu. By default, if the menu opens on the right, we 259 // expect the slope between the cursor and the upper right 260 // corner to decrease over time, as explained above. If the 261 // submenu opens in a different direction, we change our slope 262 // expectations. 263 if (this.options.submenuDirection == "left") { 264 decreasingCorner = lowerLeft; 265 increasingCorner = upperLeft; 266 } else if (this.options.submenuDirection == "below") { 267 decreasingCorner = lowerRight; 268 increasingCorner = lowerLeft; 269 } else if (this.options.submenuDirection == "above") { 270 decreasingCorner = upperLeft; 271 increasingCorner = upperRight; 272 } 273 var decreasingSlope = slope(loc, decreasingCorner), 274 increasingSlope = slope(loc, increasingCorner), 275 prevDecreasingSlope = slope(prevLoc, decreasingCorner), 276 prevIncreasingSlope = slope(prevLoc, increasingCorner); 277 if (decreasingSlope < prevDecreasingSlope && increasingSlope > prevIncreasingSlope) { 278 // Mouse is moving from previous location towards the 279 // currently activated submenu. Delay before activating a 280 // new menu row, because user may be moving into submenu. 281 this.lastDelayLoc = loc; 282 return this.options.defaultDelay; 283 } 284 this.lastDelayLoc = null; 285 return 0; 286 }, 287 // if the target isn't the container nor a descendant of the container 288 _outsideMenuClick: function(e) { 289 var obj = e.data.obj; 290 if ($(obj.el).not(e.target) && $(obj.el).has(e.target).length === 0) { 291 obj.options.deactivateCallback(obj.activeRow); 292 obj.activeRow = null; 293 } 294 }, 295 _hoverTriggerOn: function() { 296 $(this.el).on('mouseleave', { obj: this}, this._mouseLeaveMenu ) 297 .find(this.options.rowSelector) 298 .on('mouseenter', { obj: this}, this._mouseEnterRow) 299 .on('mouseleave', { obj: this}, this._mouseLeaveRow); 300 $(window).on('blur', { obj: this }, this._mouseLeaveMenu); 301 $(document).on('mousemove', { obj: this }, this._mouseMoveDocument); 302 }, 303 _hoverTriggerOff: function() { 304 $(this.el).off('mouseleave', this._mouseLeaveMenu) 305 .find(this.options.rowSelector) 306 .off('mouseenter', this._mouseEnterRow) 307 .off('mouseleave', this._mouseLeaveRow) 308 $(window).off('blur', this._mouseLeaveMenu); 309 $(document).off('mousemove', { obj: this }, this._mouseMoveDocument); 310 }, 311 _clickTriggerOn: function() { 312 $(this.el).find(this.options.rowSelector) 313 .on('click', { obj: this }, this._clickRow); 314 // hide menu if clicked elsewere 315 $(document).on('click', { obj: this }, this._outsideMenuClick); 316 }, 317 _clickTriggerOff: function() { 318 $(this.el) 319 .find(this.options.rowSelector) 320 .off('click', this._clickRow); 321 $(document).off('click', this._outsideMenuClick); 322 }, 323 // Public methods 324 switchToHover: function() { 325 this._clickTriggerOff(); 326 this._hoverTriggerOn(); 327 this.isOnHover = true; 328 this.isOnClick = false; 329 }, 330 switchToClick: function() { 331 this._hoverTriggerOff(); 332 this._clickTriggerOn(); 333 this.isOnHover = false; 334 this.isOnClick = true; 335 } 336 }; 337 // A lightweight plugin wrapper around the constructor, 338 // preventing against multiple instantiations and allowing any 339 // public function (ie. a function whose name doesn't start 340 // with an underscore) to be called via the jQuery plugin, 341 // e.g. $(element).defaultPluginName('functionName', arg1, arg2) 342 $.fn[pluginName] = function ( options ) { 343 var args = arguments; 344 // Is the first parameter an object (options), or was omitted, 345 // instantiate a new instance of the plugin. 346 if (options === undefined || typeof options === 'object') { 347 return this.each(function () { 348 // Only allow the plugin to be instantiated once, 349 // so we check that the element has no plugin instantiation yet 350 if (!$.data(this, 'plugin_' + pluginName)) { 351 // if it has no instance, create a new one, 352 // pass options to our plugin constructor, 353 // and store the plugin instance 354 // in the elements jQuery data object. 355 $.data(this, 'plugin_' + pluginName, new Plugin( this, options )); 356 } 357 }); 358 // If the first parameter is a string and it doesn't start 359 // with an underscore or "contains" the `init`-function, 360 // treat this as a call to a public method. 361 } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { 362 // Cache the method call 363 // to make it possible 364 // to return a value 365 var returns; 366 this.each(function () { 367 var instance = $.data(this, 'plugin_' + pluginName); 368 // Tests that there's already a plugin-instance 369 // and checks that the requested public method exists 370 if (instance instanceof Plugin && typeof instance[options] === 'function') { 371 // Call the method of our plugin instance, 372 // and pass it the supplied arguments. 373 returns = instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) ); 374 } 375 // Allow instances to be destroyed via the 'destroy' method 376 if (options === 'destroy') { 377 $.data(this, 'plugin_' + pluginName, null); 378 } 379 }); 380 // If the earlier cached method 381 // gives a value back return the value, 382 // otherwise return this to preserve chainability. 383 return returns !== undefined ? returns : this; 384 } 385 }; 386}(jQuery, window, document));
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.