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