27 * @param handlerIn function OR configuration object |
28 * @param handlerIn function OR configuration object |
28 * @param handlerOut function OR selector for delegation OR undefined |
29 * @param handlerOut function OR selector for delegation OR undefined |
29 * @param selector selector OR undefined |
30 * @param selector selector OR undefined |
30 * @author Brian Cherne <brian(at)cherne(dot)net> |
31 * @author Brian Cherne <brian(at)cherne(dot)net> |
31 */ |
32 */ |
32 (function($) { |
33 |
|
34 ;(function(factory) { |
|
35 'use strict'; |
|
36 if (typeof define === 'function' && define.amd) { |
|
37 define(['jquery'], factory); |
|
38 } else if (typeof module === 'object' && module.exports) { |
|
39 module.exports = factory(require('jquery')); |
|
40 } else if (jQuery && !jQuery.fn.hoverIntent) { |
|
41 factory(jQuery); |
|
42 } |
|
43 })(function($) { |
|
44 'use strict'; |
|
45 |
|
46 // default configuration values |
|
47 var _cfg = { |
|
48 interval: 100, |
|
49 sensitivity: 6, |
|
50 timeout: 0 |
|
51 }; |
|
52 |
|
53 // counter used to generate an ID for each instance |
|
54 var INSTANCE_COUNT = 0; |
|
55 |
|
56 // current X and Y position of mouse, updated during mousemove tracking (shared across instances) |
|
57 var cX, cY; |
|
58 |
|
59 // saves the current pointer position coordinates based on the given mousemove event |
|
60 var track = function(ev) { |
|
61 cX = ev.pageX; |
|
62 cY = ev.pageY; |
|
63 }; |
|
64 |
|
65 // compares current and previous mouse positions |
|
66 var compare = function(ev,$el,s,cfg) { |
|
67 // compare mouse positions to see if pointer has slowed enough to trigger `over` function |
|
68 if ( Math.sqrt( (s.pX-cX)*(s.pX-cX) + (s.pY-cY)*(s.pY-cY) ) < cfg.sensitivity ) { |
|
69 $el.off(s.event,track); |
|
70 delete s.timeoutId; |
|
71 // set hoverIntent state as active for this element (permits `out` handler to trigger) |
|
72 s.isActive = true; |
|
73 // overwrite old mouseenter event coordinates with most recent pointer position |
|
74 ev.pageX = cX; ev.pageY = cY; |
|
75 // clear coordinate data from state object |
|
76 delete s.pX; delete s.pY; |
|
77 return cfg.over.apply($el[0],[ev]); |
|
78 } else { |
|
79 // set previous coordinates for next comparison |
|
80 s.pX = cX; s.pY = cY; |
|
81 // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs) |
|
82 s.timeoutId = setTimeout( function(){compare(ev, $el, s, cfg);} , cfg.interval ); |
|
83 } |
|
84 }; |
|
85 |
|
86 // triggers given `out` function at configured `timeout` after a mouseleave and clears state |
|
87 var delay = function(ev,$el,s,out) { |
|
88 var data = $el.data('hoverIntent'); |
|
89 if (data) { |
|
90 delete data[s.id]; |
|
91 } |
|
92 return out.apply($el[0],[ev]); |
|
93 }; |
|
94 |
33 $.fn.hoverIntent = function(handlerIn,handlerOut,selector) { |
95 $.fn.hoverIntent = function(handlerIn,handlerOut,selector) { |
|
96 // instance ID, used as a key to store and retrieve state information on an element |
|
97 var instanceId = INSTANCE_COUNT++; |
34 |
98 |
35 // default configuration values |
99 // extend the default configuration and parse parameters |
36 var cfg = { |
100 var cfg = $.extend({}, _cfg); |
37 interval: 100, |
101 if ( $.isPlainObject(handlerIn) ) { |
38 sensitivity: 6, |
102 cfg = $.extend(cfg, handlerIn); |
39 timeout: 0 |
103 if ( !$.isFunction(cfg.out) ) { |
40 }; |
104 cfg.out = cfg.over; |
41 |
105 } |
42 if ( typeof handlerIn === "object" ) { |
106 } else if ( $.isFunction(handlerOut) ) { |
43 cfg = $.extend(cfg, handlerIn ); |
|
44 } else if ($.isFunction(handlerOut)) { |
|
45 cfg = $.extend(cfg, { over: handlerIn, out: handlerOut, selector: selector } ); |
107 cfg = $.extend(cfg, { over: handlerIn, out: handlerOut, selector: selector } ); |
46 } else { |
108 } else { |
47 cfg = $.extend(cfg, { over: handlerIn, out: handlerIn, selector: handlerOut } ); |
109 cfg = $.extend(cfg, { over: handlerIn, out: handlerIn, selector: handlerOut } ); |
48 } |
110 } |
49 |
111 |
50 // instantiate variables |
|
51 // cX, cY = current X and Y position of mouse, updated by mousemove event |
|
52 // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval |
|
53 var cX, cY, pX, pY; |
|
54 |
|
55 // A private function for getting mouse position |
|
56 var track = function(ev) { |
|
57 cX = ev.pageX; |
|
58 cY = ev.pageY; |
|
59 }; |
|
60 |
|
61 // A private function for comparing current and previous mouse position |
|
62 var compare = function(ev,ob) { |
|
63 ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); |
|
64 // compare mouse positions to see if they've crossed the threshold |
|
65 if ( Math.sqrt( (pX-cX)*(pX-cX) + (pY-cY)*(pY-cY) ) < cfg.sensitivity ) { |
|
66 $(ob).off("mousemove.hoverIntent",track); |
|
67 // set hoverIntent state to true (so mouseOut can be called) |
|
68 ob.hoverIntent_s = true; |
|
69 return cfg.over.apply(ob,[ev]); |
|
70 } else { |
|
71 // set previous coordinates for next time |
|
72 pX = cX; pY = cY; |
|
73 // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs) |
|
74 ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval ); |
|
75 } |
|
76 }; |
|
77 |
|
78 // A private function for delaying the mouseOut function |
|
79 var delay = function(ev,ob) { |
|
80 ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); |
|
81 ob.hoverIntent_s = false; |
|
82 return cfg.out.apply(ob,[ev]); |
|
83 }; |
|
84 |
|
85 // A private function for handling mouse 'hovering' |
112 // A private function for handling mouse 'hovering' |
86 var handleHover = function(e) { |
113 var handleHover = function(e) { |
87 // copy objects to be passed into t (required for event object to be passed in IE) |
114 // cloned event to pass to handlers (copy required for event object to be passed in IE) |
88 var ev = $.extend({},e); |
115 var ev = $.extend({},e); |
89 var ob = this; |
|
90 |
116 |
91 // cancel hoverIntent timer if it exists |
117 // the current target of the mouse event, wrapped in a jQuery object |
92 if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); } |
118 var $el = $(this); |
93 |
119 |
94 // if e.type === "mouseenter" |
120 // read hoverIntent data from element (or initialize if not present) |
95 if (e.type === "mouseenter") { |
121 var hoverIntentData = $el.data('hoverIntent'); |
|
122 if (!hoverIntentData) { $el.data('hoverIntent', (hoverIntentData = {})); } |
|
123 |
|
124 // read per-instance state from element (or initialize if not present) |
|
125 var state = hoverIntentData[instanceId]; |
|
126 if (!state) { hoverIntentData[instanceId] = state = { id: instanceId }; } |
|
127 |
|
128 // state properties: |
|
129 // id = instance ID, used to clean up data |
|
130 // timeoutId = timeout ID, reused for tracking mouse position and delaying "out" handler |
|
131 // isActive = plugin state, true after `over` is called just until `out` is called |
|
132 // pX, pY = previously-measured pointer coordinates, updated at each polling interval |
|
133 // event = string representing the namespaced event used for mouse tracking |
|
134 |
|
135 // clear any existing timeout |
|
136 if (state.timeoutId) { state.timeoutId = clearTimeout(state.timeoutId); } |
|
137 |
|
138 // namespaced event used to register and unregister mousemove tracking |
|
139 var mousemove = state.event = 'mousemove.hoverIntent.hoverIntent'+instanceId; |
|
140 |
|
141 // handle the event, based on its type |
|
142 if (e.type === 'mouseenter') { |
|
143 // do nothing if already active |
|
144 if (state.isActive) { return; } |
96 // set "previous" X and Y position based on initial entry point |
145 // set "previous" X and Y position based on initial entry point |
97 pX = ev.pageX; pY = ev.pageY; |
146 state.pX = ev.pageX; state.pY = ev.pageY; |
98 // update "current" X and Y position based on mousemove |
147 // update "current" X and Y position based on mousemove |
99 $(ob).on("mousemove.hoverIntent",track); |
148 $el.off(mousemove,track).on(mousemove,track); |
100 // start polling interval (self-calling timeout) to compare mouse coordinates over time |
149 // start polling interval (self-calling timeout) to compare mouse coordinates over time |
101 if (!ob.hoverIntent_s) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );} |
150 state.timeoutId = setTimeout( function(){compare(ev,$el,state,cfg);} , cfg.interval ); |
102 |
151 } else { // "mouseleave" |
103 // else e.type == "mouseleave" |
152 // do nothing if not already active |
104 } else { |
153 if (!state.isActive) { return; } |
105 // unbind expensive mousemove event |
154 // unbind expensive mousemove event |
106 $(ob).off("mousemove.hoverIntent",track); |
155 $el.off(mousemove,track); |
107 // if hoverIntent state is true, then call the mouseOut function after the specified delay |
156 // if hoverIntent state is true, then call the mouseOut function after the specified delay |
108 if (ob.hoverIntent_s) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );} |
157 state.timeoutId = setTimeout( function(){delay(ev,$el,state,cfg.out);} , cfg.timeout ); |
109 } |
158 } |
110 }; |
159 }; |
111 |
160 |
112 // listen for mouseenter and mouseleave |
161 // listen for mouseenter and mouseleave |
113 return this.on({'mouseenter.hoverIntent':handleHover,'mouseleave.hoverIntent':handleHover}, cfg.selector); |
162 return this.on({'mouseenter.hoverIntent':handleHover,'mouseleave.hoverIntent':handleHover}, cfg.selector); |
114 }; |
163 }; |
115 })(jQuery); |
164 }); |