2 * Jquery plugin for unified mouse and touch events
4 * Copyright (c) 2013 Michael S. Mikowski
5 * (mike[dot]mikowski[at]gmail[dotcom])
7 * Dual licensed under the MIT or GPL Version 2
8 * http://jquery.org/license
11 * 0.3.0 - Initial jQuery plugin site release
12 * - Replaced scrollwheel zoom with drag motion.
13 * This resolved a conflict with scrollable areas.
14 * 0.3.1 - Change for jQuery plugins site
15 * 0.3.2 - Updated to jQuery 1.9.1.
16 * Confirmed 1.7.0-1.9.1 compatibility.
17 * 0.4.2 - Updated documentation
18 * 0.4.3 - Removed fatal execption possibility if originalEvent
19 * is not defined on event object
23 /*jslint browser : true, continue : true,
24 devel : true, indent : 2, maxerr : 50,
25 newcap : true, plusplus : true, regexp : true,
26 sloppy : true, vars : true, white : true
28 /*global jQuery, sl */
31 //---------------- BEGIN MODULE SCOPE VARIABLES --------------
33 $Special = $.event.special, // shortcut for special event
34 motionMapMap = {}, // map of pointer motions by cursor
35 isMoveBound = false, // flag if move handlers bound
36 pxPinchZoom = -1, // distance between pinch-zoom points
37 optionKey = 'ue_bound', // data key for storing options
38 doDisableMouse = false, // flag to discard mouse input
39 defaultOptMap = { // Default option hash
40 bound_ns_map : {}, // namspace hash e.g. bound_ns_map.utap.fred
41 wheel_ratio : 15, // multiplier for mousewheel delta
42 px_radius : 3, // 'distance' dragged before dragstart
43 ignore_class : ':input', // 'not' suppress matching elements
44 tap_time : 200, // millisecond max time to consider tap
45 held_tap_time : 300 // millisecond min time to consider taphold
47 callbackList = [], // global callback stack
48 zoomMouseNum = 1, // multiplier for mouse zoom
49 zoomTouchNum = 4, // multiplier for touch zoom
52 motionDragId, motionHeldId, motionDzoomId,
53 motion1ZoomId, motion2ZoomId,
55 checkMatchVal, removeListVal, pushUniqVal, makeListPlus,
56 fnHeld, fnMotionStart, fnMotionMove,
57 fnMotionEnd, onMouse, onTouch,
60 //----------------- END MODULE SCOPE VARIABLES ---------------
62 //------------------- BEGIN UTILITY METHODS ------------------
63 // Begin utiltity /makeListPlus/
64 // Returns an array with much desired methods:
65 // * remove_val(value) : remove element that matches
66 // the provided value. Returns number of elements
68 // * match_val(value) : shows if a value exists
69 // * push_uniq(value) : pushes a value onto the stack
70 // iff it does not already exist there
71 // Note: the reason I need this is to compare objects to
72 // objects (perhaps jQuery has something similar?)
73 checkMatchVal = function ( data ) {
74 var match_count = 0, idx;
75 for ( idx = this.length; idx; 0 ) {
76 if ( this[--idx] === data ) { match_count++; }
80 removeListVal = function ( data ) {
81 var removed_count = 0, idx;
82 for ( idx = this.length; idx; 0 ) {
83 if ( this[--idx] === data ) {
91 pushUniqVal = function ( data ) {
92 if ( checkMatchVal.call(this, data ) ) { return false; }
97 makeListPlus = function ( input_list ) {
98 if ( input_list && $.isArray(input_list) ) {
99 if ( input_list.remove_val ) {
100 console.warn( 'The array appears to already have listPlus capabilities' );
107 input_list.remove_val = removeListVal;
108 input_list.match_val = checkMatchVal;
109 input_list.push_uniq = pushUniqVal;
113 // End utility /makeListPlus/
114 //-------------------- END UTILITY METHODS -------------------
116 //--------------- BEGIN JQUERY SPECIAL EVENTS ----------------
117 // Unique array for bound objects
118 boundList = makeListPlus();
120 // Begin define special event handlers
122 setup : function( data, a_names, fn_bind ) {
125 $to_bind = $(this_el),
127 option_map, idx, namespace_key, ue_namespace_code, namespace_list
130 // if previous related event bound do not rebind, but do add to
131 // type of event bound to this element, if not already noted
132 if ( $.data( this, optionKey ) ) { return; }
135 $.extend( true, option_map, defaultOptMap );
136 $.data( this_el, optionKey, option_map );
138 namespace_list = makeListPlus(a_names.slice(0));
139 if ( ! namespace_list.length
140 || namespace_list[0] === ""
141 ) { namespace_list = ["000"]; }
144 for ( idx = 0; idx < namespace_list.length; idx++ ) {
145 namespace_key = namespace_list[idx];
147 if ( ! namespace_key ) { continue NSPACE_00; }
148 if ( seen_map.hasOwnProperty(namespace_key) ) { continue NSPACE_00; }
150 seen_map[namespace_key] = true;
152 ue_namespace_code = '.__ue' + namespace_key;
154 $to_bind.bind( 'mousedown' + ue_namespace_code, onMouse );
155 $to_bind.bind( 'touchstart' + ue_namespace_code, onTouch );
156 $to_bind.bind( 'mousewheel' + ue_namespace_code, onMousewheel );
159 boundList.push_uniq( this_el ); // record as bound element
161 if ( ! isMoveBound ) {
162 // console.log('first element bound - adding global binds');
163 $(document).bind( 'mousemove.__ue', onMouse );
164 $(document).bind( 'touchmove.__ue', onTouch );
165 $(document).bind( 'mouseup.__ue' , onMouse );
166 $(document).bind( 'touchend.__ue' , onTouch );
167 $(document).bind( 'touchcancel.__ue', onTouch );
172 // arg_map.type = string - name of event to bind
173 // arg_map.data = poly - whatever (optional) data was passed when binding
174 // arg_map.namespace = string - A sorted, dot-delimited list of namespaces
175 // specified when binding the event
176 // arg_map.handler = fn - the event handler the developer wishes to be bound
177 // to the event. This function should be called whenever the event
179 // arg_map.guid = number - unique ID for event handler, provided by jQuery
180 // arg_map.selector = string - selector used by 'delegate' or 'live' jQuery
181 // methods. Only available when these methods are used.
183 // this - the element to which the event handler is being bound
184 // this always executes immediate after setup (if first binding)
185 add : function ( arg_map ) {
188 option_map = $.data( this_el, optionKey ),
189 namespace_str = arg_map.namespace,
190 event_type = arg_map.type,
191 bound_ns_map, namespace_list, idx, namespace_key
193 if ( ! option_map ) { return; }
195 bound_ns_map = option_map.bound_ns_map;
197 if ( ! bound_ns_map[event_type] ) {
198 // this indicates a non-namespaced entry
199 bound_ns_map[event_type] = {};
202 if ( ! namespace_str ) { return; }
204 namespace_list = namespace_str.split('.');
206 for ( idx = 0; idx < namespace_list.length; idx++ ) {
207 namespace_key = namespace_list[idx];
208 bound_ns_map[event_type][namespace_key] = true;
212 remove : function ( arg_map ) {
215 option_map = $.data( elem_bound, optionKey ),
216 bound_ns_map = option_map.bound_ns_map,
217 event_type = arg_map.type,
218 namespace_str = arg_map.namespace,
219 namespace_list, idx, namespace_key
222 if ( ! bound_ns_map[event_type] ) { return; }
224 // No namespace(s) provided:
225 // Remove complete record for custom event type (e.g. utap)
226 if ( ! namespace_str ) {
227 delete bound_ns_map[event_type];
231 // Namespace(s) provided:
232 // Remove namespace flags from each custom event typei (e.g. utap)
233 // record. If all claimed namespaces are removed, remove
235 namespace_list = namespace_str.split('.');
237 for ( idx = 0; idx < namespace_list.length; idx++ ) {
238 namespace_key = namespace_list[idx];
239 if (bound_ns_map[event_type][namespace_key]) {
240 delete bound_ns_map[event_type][namespace_key];
244 if ( $.isEmptyObject( bound_ns_map[event_type] ) ) {
245 delete bound_ns_map[event_type];
249 teardown : function( a_names ) {
252 $bound = $(elem_bound),
253 option_map = $.data( elem_bound, optionKey ),
254 bound_ns_map = option_map.bound_ns_map,
255 idx, namespace_key, ue_namespace_code, namespace_list
258 // do not tear down if related handlers are still bound
259 if ( ! $.isEmptyObject( bound_ns_map ) ) { return; }
261 namespace_list = makeListPlus(a_names);
262 namespace_list.push_uniq('000');
265 for ( idx = 0; idx < namespace_list.length; idx++ ) {
266 namespace_key = namespace_list[idx];
268 if ( ! namespace_key ) { continue NSPACE_01; }
270 ue_namespace_code = '.__ue' + namespace_key;
271 $bound.unbind( 'mousedown' + ue_namespace_code );
272 $bound.unbind( 'touchstart' + ue_namespace_code );
273 $bound.unbind( 'mousewheel' + ue_namespace_code );
276 $.removeData( elem_bound, optionKey );
278 // Unbind document events only after last element element is removed
279 boundList.remove_val(this);
280 if ( boundList.length === 0 ) {
281 // console.log('last bound element removed - removing global binds');
282 $(document).unbind( 'mousemove.__ue');
283 $(document).unbind( 'touchmove.__ue');
284 $(document).unbind( 'mouseup.__ue');
285 $(document).unbind( 'touchend.__ue');
286 $(document).unbind( 'touchcancel.__ue');
291 // End define special event handlers
292 //--------------- BEGIN JQUERY SPECIAL EVENTS ----------------
294 //------------------ BEGIN MOTION CONTROLS -------------------
295 // Begin motion control /fnHeld/
296 fnHeld = function ( arg_map ) {
298 timestamp = +new Date(),
299 motion_id = arg_map.motion_id,
300 motion_map = arg_map.motion_map,
301 bound_ns_map = arg_map.bound_ns_map,
305 delete motion_map.idto_tapheld;
307 if ( ! motion_map.do_allow_tap ) { return; }
309 motion_map.px_end_x = motion_map.px_start_x;
310 motion_map.px_end_y = motion_map.px_start_y;
311 motion_map.ms_timestop = timestamp;
312 motion_map.ms_elapsed = timestamp - motion_map.ms_timestart;
314 if ( bound_ns_map.uheld ) {
315 event_ue = $.Event('uheld');
316 $.extend( event_ue, motion_map );
317 $(motion_map.elem_bound).trigger(event_ue);
320 // remove tracking, as we want no futher action on this motion
321 if ( bound_ns_map.uheldstart ) {
322 event_ue = $.Event('uheldstart');
323 $.extend( event_ue, motion_map );
324 $(motion_map.elem_bound).trigger(event_ue);
325 motionHeldId = motion_id;
328 delete motionMapMap[motion_id];
331 // End motion control /fnHeld/
334 // Begin motion control /fnMotionStart/
335 fnMotionStart = function ( arg_map ) {
337 motion_id = arg_map.motion_id,
338 event_src = arg_map.event_src,
339 request_dzoom = arg_map.request_dzoom,
341 option_map = $.data( arg_map.elem, optionKey ),
342 bound_ns_map = option_map.bound_ns_map,
343 $target = $(event_src.target ),
344 do_zoomstart = false,
345 motion_map, cb_map, do_allow_tap, event_ue
348 // this should never happen, but it does
349 if ( motionMapMap[ motion_id ] ) { return; }
351 if ( request_dzoom && ! bound_ns_map.uzoomstart ) { return; }
353 // :input selector includes text areas
354 if ( $target.is( option_map.ignore_class ) ) { return; }
356 do_allow_tap = bound_ns_map.utap
357 || bound_ns_map.uheld || bound_ns_map.uheldstart
360 cb_map = callbackList.pop();
363 if ( $target.is( cb_map.selector_str )
364 || $( arg_map.elem ).is( cb_map.selector_str )
366 if ( cb_map.callback_match ) {
367 cb_map.callback_match( arg_map );
371 if ( cb_map.callback_nomatch ) {
372 cb_map.callback_nomatch( arg_map );
375 cb_map = callbackList.pop();
379 do_allow_tap : do_allow_tap,
380 elem_bound : arg_map.elem,
381 elem_target : event_src.target,
383 ms_timestart : event_src.timeStamp,
384 ms_timestop : undefined,
385 option_map : option_map,
386 orig_target : event_src.target,
387 px_current_x : event_src.clientX,
388 px_current_y : event_src.clientY,
389 px_end_x : undefined,
390 px_end_y : undefined,
391 px_start_x : event_src.clientX,
392 px_start_y : event_src.clientY,
393 timeStamp : event_src.timeStamp
396 motionMapMap[ motion_id ] = motion_map;
398 if ( bound_ns_map.uzoomstart ) {
399 if ( request_dzoom ) {
400 motionDzoomId = motion_id;
402 else if ( ! motion1ZoomId ) {
403 motion1ZoomId = motion_id;
405 else if ( ! motion2ZoomId ) {
406 motion2ZoomId = motion_id;
407 event_ue = $.Event('uzoomstart');
411 if ( do_zoomstart ) {
412 event_ue = $.Event( 'uzoomstart' );
413 motion_map.px_delta_zoom = 0;
414 $.extend( event_ue, motion_map );
415 $(motion_map.elem_bound).trigger(event_ue);
420 if ( bound_ns_map.uheld || bound_ns_map.uheldstart ) {
421 motion_map.idto_tapheld = setTimeout(
424 motion_id : motion_id,
425 motion_map : motion_map,
426 bound_ns_map : bound_ns_map
429 option_map.held_tap_time
433 // End motion control /fnMotionStart/
435 // Begin motion control /fnMotionMove/
436 fnMotionMove = function ( arg_map ) {
438 motion_id = arg_map.motion_id,
439 event_src = arg_map.event_src,
441 motion_map, option_map, bound_ns_map,
442 event_ue, px_pinch_zoom, px_delta_zoom,
443 mzoom1_map, mzoom2_map
446 if ( ! motionMapMap[motion_id] ) { return; }
448 motion_map = motionMapMap[motion_id];
449 option_map = motion_map.option_map;
450 bound_ns_map = option_map.bound_ns_map;
452 motion_map.timeStamp = event_src.timeStamp;
453 motion_map.elem_target = event_src.target;
454 motion_map.ms_elapsed = event_src.timeStamp - motion_map.ms_timestart;
456 motion_map.px_delta_x = event_src.clientX - motion_map.px_current_x;
457 motion_map.px_delta_y = event_src.clientY - motion_map.px_current_y;
459 motion_map.px_current_x = event_src.clientX;
460 motion_map.px_current_y = event_src.clientY;
462 // native event object override
463 motion_map.timeStamp = event_src.timeStamp;
465 // disallow tap if outside of zone or time elapsed
466 // we use this for other events, so we do it every time
467 if ( motion_map.do_allow_tap ) {
468 if ( Math.abs(motion_map.px_delta_x) > option_map.px_radius
469 || Math.abs(motion_map.pd_delta_y) > option_map.px_radius
470 || motion_map.ms_elapsed > option_map.tap_time
471 ) { motion_map.do_allow_tap = false; }
474 if ( motion1ZoomId && motion2ZoomId
475 && ( motion_id === motion1ZoomId
476 || motion_id === motion2ZoomId
478 motionMapMap[motion_id] = motion_map;
479 mzoom1_map = motionMapMap[motion1ZoomId];
480 mzoom2_map = motionMapMap[motion2ZoomId];
482 px_pinch_zoom = Math.floor(
484 Math.pow((mzoom1_map.px_current_x - mzoom2_map.px_current_x),2)
485 + Math.pow((mzoom1_map.px_current_y - mzoom2_map.px_current_y),2)
489 if ( pxPinchZoom === -1 ) { px_delta_zoom = 0; }
490 else { px_delta_zoom = ( px_pinch_zoom - pxPinchZoom ) * zoomTouchNum;}
492 // save value for next iteration delta comparison
493 pxPinchZoom = px_pinch_zoom;
496 else if ( motionDzoomId === motion_id ) {
497 if ( bound_ns_map.uzoommove ) {
498 px_delta_zoom = motion_map.px_delta_y * zoomMouseNum;
504 event_ue = $.Event('uzoommove');
505 motion_map.px_delta_zoom = px_delta_zoom;
506 $.extend( event_ue, motion_map );
507 $(motion_map.elem_bound).trigger(event_ue);
511 if ( motionHeldId === motion_id ) {
512 if ( bound_ns_map.uheldmove ) {
513 event_ue = $.Event('uheldmove');
514 $.extend( event_ue, motion_map );
515 $(motion_map.elem_bound).trigger(event_ue);
516 event_src.preventDefault();
519 else if ( motionDragId === motion_id ) {
520 if ( bound_ns_map.udragmove ) {
521 event_ue = $.Event('udragmove');
522 $.extend( event_ue, motion_map );
523 $(motion_map.elem_bound).trigger(event_ue);
524 event_src.preventDefault();
530 && bound_ns_map.udragstart
531 && motion_map.do_allow_tap === false
533 motionDragId = motion_id;
534 event_ue = $.Event('udragstart');
535 $.extend( event_ue, motion_map );
536 $(motion_map.elem_bound).trigger(event_ue);
537 event_src.preventDefault();
539 if ( motion_map.idto_tapheld ) {
540 clearTimeout(motion_map.idto_tapheld);
541 delete motion_map.idto_tapheld;
545 // End motion control /fnMotionMove/
547 // Begin motion control /fnMotionEnd/
548 fnMotionEnd = function ( arg_map ) {
550 motion_id = arg_map.motion_id,
551 event_src = arg_map.event_src,
553 motion_map, option_map, bound_ns_map, event_ue
556 doDisableMouse = false;
558 if ( ! motionMapMap[motion_id] ) { return; }
560 motion_map = motionMapMap[motion_id];
561 option_map = motion_map.option_map;
562 bound_ns_map = option_map.bound_ns_map;
564 motion_map.elem_target = event_src.target;
565 motion_map.ms_elapsed = event_src.timeStamp - motion_map.ms_timestart;
566 motion_map.ms_timestop = event_src.timeStamp;
568 if ( motion_map.px_current_x ) {
569 motion_map.px_delta_x = event_src.clientX - motion_map.px_current_x;
570 motion_map.px_delta_y = event_src.clientY - motion_map.px_current_y;
573 motion_map.px_current_x = event_src.clientX;
574 motion_map.px_current_y = event_src.clientY;
576 motion_map.px_end_x = event_src.clientX;
577 motion_map.px_end_y = event_src.clientY;
579 // native event object override
580 motion_map.timeStamp = event_src.timeStamp
583 // clear-out any long-hold tap timer
584 if ( motion_map.idto_tapheld ) {
585 clearTimeout(motion_map.idto_tapheld);
586 delete motion_map.idto_tapheld;
590 if ( bound_ns_map.utap
591 && motion_map.ms_elapsed <= option_map.tap_time
592 && motion_map.do_allow_tap
594 event_ue = $.Event('utap');
595 $.extend( event_ue, motion_map );
596 $(motion_map.elem_bound).trigger(event_ue);
600 if ( motion_id === motionDragId ) {
601 if ( bound_ns_map.udragend ) {
602 event_ue = $.Event('udragend');
603 $.extend( event_ue, motion_map );
604 $(motion_map.elem_bound).trigger(event_ue);
605 event_src.preventDefault();
607 motionDragId = undefined;
611 if ( motion_id === motionHeldId ) {
612 if ( bound_ns_map.uheldend ) {
613 event_ue = $.Event('uheldend');
614 $.extend( event_ue, motion_map );
615 $(motion_map.elem_bound).trigger(event_ue);
617 motionHeldId = undefined;
621 if ( motion_id === motionDzoomId ) {
623 motionDzoomId = undefined;
627 else if ( motion_id === motion1ZoomId ) {
628 if ( motion2ZoomId ) {
629 motion1ZoomId = motion2ZoomId;
630 motion2ZoomId = undefined;
633 else { motion1ZoomId = undefined; }
636 if ( motion_id === motion2ZoomId ) {
637 motion2ZoomId = undefined;
642 if ( do_zoomend && bound_ns_map.uzoomend ) {
643 event_ue = $.Event('uzoomend');
644 motion_map.px_delta_zoom = 0;
645 $.extend( event_ue, motion_map );
646 $(motion_map.elem_bound).trigger(event_ue);
648 // remove pointer from consideration
649 delete motionMapMap[motion_id];
651 // End motion control /fnMotionEnd/
652 //------------------ END MOTION CONTROLS -------------------
654 //------------------- BEGIN EVENT HANDLERS -------------------
655 // Begin event handler /onTouch/ for all touch events.
656 // We use the 'type' attribute to dispatch to motion control
657 onTouch = function ( event ) {
660 timestamp = +new Date(),
661 o_event = event.originalEvent,
662 touch_list = o_event ? o_event.changedTouches || [] : [],
663 touch_count = touch_list.length,
664 idx, touch_event, motion_id, handler_fn
667 doDisableMouse = true;
669 event.timeStamp = timestamp;
671 switch ( event.type ) {
673 handler_fn = fnMotionStart;
674 event.preventDefault();
677 handler_fn = fnMotionMove;
680 case 'touchcancel' : handler_fn = fnMotionEnd; break;
681 default : handler_fn = null;
684 if ( ! handler_fn ) { return; }
686 for ( idx = 0; idx < touch_count; idx++ ) {
687 touch_event = touch_list[idx];
689 motion_id = 'touch' + String(touch_event.identifier);
691 event.clientX = touch_event.clientX;
692 event.clientY = touch_event.clientY;
695 motion_id : motion_id,
700 // End event handler /onTouch/
703 // Begin event handler /onMouse/ for all mouse events
704 // We use the 'type' attribute to dispatch to motion control
705 onMouse = function ( event ) {
708 motion_id = 'mouse' + String(event.button),
709 request_dzoom = false,
713 if ( doDisableMouse ) {
714 event.stopImmediatePropagation();
718 if ( event.shiftKey ) { request_dzoom = true; }
720 // skip left or middle clicks
721 if ( event.type !== 'mousemove' ) {
722 if ( event.button !== 0 ) { return true; }
725 switch ( event.type ) {
727 handler_fn = fnMotionStart;
728 event.preventDefault();
731 handler_fn = fnMotionEnd;
734 handler_fn = fnMotionMove;
740 if ( ! handler_fn ) { return; }
745 request_dzoom : request_dzoom,
746 motion_id : motion_id
749 // End event handler /onMouse/
750 //-------------------- END EVENT HANDLERS --------------------
753 // Export special events through jQuery API
755 = $Special.utap = $Special.uheld
756 = $Special.uzoomstart = $Special.uzoommove = $Special.uzoomend
757 = $Special.udragstart = $Special.udragmove = $Special.udragend
758 = $Special.uheldstart = $Special.uheldmove = $Special.uheldend
761 $.ueSetGlobalCb = function ( selector_str, callback_match, callback_nomatch ) {
763 selector_str : selector_str || '',
764 callback_match : callback_match || null,
765 callback_nomatch : callback_nomatch || null