Created
April 8, 2013 08:57
-
-
Save stakach/5335327 to your computer and use it in GitHub Desktop.
AngularJS Mobile + Desktop Events
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(angular, undefined) { | |
'use strict'; | |
angular.module('Core.SafeApply', []). | |
service('Core.SafeApply', function() { | |
this.do = function(scope, fn) { | |
var phase = scope.$root.$$phase; | |
if(phase == '$apply' || phase == '$digest') { | |
fn(); | |
} else { | |
scope.$apply(fn); | |
} | |
}; | |
}); | |
})(angular); | |
// Referencing: | |
// http://ie.microsoft.com/testdrive/ieblog/2011/oct/PointerDraw.js.source.html | |
(function(angular, undefined) { | |
'use strict'; | |
angular.module('Core.TouchMouse', ['Core.SafeApply']) | |
.factory('Core.TouchMouse', ['$window', function(window) { | |
// | |
// Opera doesn't have Object.keys so we use this wrapper | |
// | |
var NumberOfKeys = function(theObject) { | |
if (Object.keys) | |
return Object.keys(theObject).length; | |
var n = 0; | |
for (var key in theObject) | |
++n; | |
return n; | |
}, | |
$document = angular.element(window.document), | |
// IE10's implementation in the Windows Developer Preview requires doing all of this | |
// Not all of these methods remain in the Windows Consumer Preview, hence the tests for method existence. | |
PreventDefaults = function(evtObj) { | |
if (evtObj.preventDefault) | |
evtObj.preventDefault(); | |
if (evtObj.preventManipulation) | |
evtObj.preventManipulation(); | |
if (evtObj.preventMouseEvent) | |
evtObj.preventMouseEvent(); | |
}, | |
pointerEvents = function(scope, element, callbacks) { | |
var tracker = {}, | |
// common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events | |
doEvent = function(event) { | |
// | |
// Optimise rejecting clicks (iOS) that are most likely triggered by a touch | |
// | |
if (event.originalEvent.type == "click" && NumberOfKeys(tracker) == 0) | |
return; | |
var theEvtObj = event.originalEvent, | |
pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj]; | |
PreventDefaults(theEvtObj); | |
for (var i = 0; i < pointerList.length; ++i) { | |
var pointerObj = pointerList[i], | |
pointerId = (typeof pointerObj.identifier != 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId != 'undefined') ? pointerObj.pointerId : 1; | |
if (theEvtObj.type.match(/(start|down)$/i)) { | |
// clause for processing MSPointerDown, touchstart, and mousedown | |
// protect against failing to get an up or end on this pointerId | |
if (tracker[pointerId]) { | |
try { | |
if (callbacks['cancel']) { | |
callbacks['cancel'](pointerId); | |
} else if (callbacks['up']) { | |
callbacks['up'](pointerId, pointerObj); | |
} | |
} finally { | |
delete tracker[pointerId]; | |
if (this.releasePointerCapture) { | |
this.releasePointerCapture(pointerId); | |
} else { | |
$document.off('.p' + pointerId); | |
} | |
} | |
} | |
// | |
// Track the element the event started on and if we should execute the attached action | |
// | |
tracker[pointerId] = this; // TODO:: maybe this should be this[0] | |
// | |
// in the Microsoft pointer model, set the capture for this pointer | |
// nothing is required for the iOS touch model because capture is implied on touchstart | |
// | |
try { | |
this.setPointerCapture = this.setPointerCapture || this.msSetPointerCapture; | |
this.releasePointerCapture = this.releasePointerCapture || this.msReleasePointerCapture; | |
if (this.setPointerCapture) { | |
this.setPointerCapture(pointerId); | |
} else { | |
$document.on('mousemove.p' + pointerId + ' mouseup.p' + pointerId, doEvent); | |
} | |
} catch(e) {} // was getting an 'SCRIPT16389: Unspecified error' on IE10 Win7 Preview @ 16/11/2012 | |
try { | |
if(callbacks['down']) { | |
callbacks['down'](pointerId, pointerObj); | |
} | |
} catch(e) {} | |
} else if (theEvtObj.type.match(/move$/i)) { | |
// clause handles MSPointerMove and touchmove | |
try { | |
if(tracker[pointerId] && callbacks['move']) { | |
callbacks['move'](pointerId, pointerObj); | |
} | |
} catch(e) {} | |
} else if (tracker[pointerId] && theEvtObj.type.match(/(up|end|cancel|click)$/i)) { | |
// clause handles up/end/cancel/click | |
var target = tracker[pointerId]; | |
try { | |
if (theEvtObj.type.match(/cancel$/i) && callbacks['cancel']) { | |
callbacks['cancel'](pointerId); // Cancel the gesture | |
} else if (callbacks['up']) { | |
callbacks['up'](pointerId, pointerObj); // Apply the click, touch, point event | |
} | |
} finally { | |
delete tracker[pointerId]; | |
// | |
// in the Microsoft pointer model, release the capture for this pointer | |
// in the mouse model, release the capture or remove document-level event handlers if there are no down points | |
// nothing is required for the iOS touch model because capture is implied on touchstart | |
// | |
if (target.releasePointerCapture) { | |
target.releasePointerCapture(pointerId); | |
} else { | |
$document.off('.p' + pointerId); | |
} | |
} | |
} | |
} | |
}; | |
if (window.navigator.pointerEnabled) { | |
// Pointer model | |
element.on('pointerdown.coPoint pointermove.coPoint pointerup.coPoint pointercancel.coPoint', doEvent); | |
} else if (window.navigator.msPointerEnabled) { | |
// Microsoft pointer model | |
element.on('MSPointerDown.coPoint MSPointerMove.coPoint MSPointerUp.coPoint MSPointerCancel.coPoint', doEvent); | |
} else { | |
// iOS touch model & mouse model | |
element.on('touchstart.coPoint touchmove.coPoint touchend.coPoint touchcancel.coPoint mousedown.coPoint', doEvent); | |
} | |
// | |
// Clean up any event handlers | |
// | |
scope.$on('$destroy', function() { | |
element.off('.coPoint'); | |
}); | |
return { | |
current: function() { | |
return tracker; | |
} | |
}; | |
}; | |
return { | |
getPointerEvents: pointerEvents | |
}; | |
}]) | |
// | |
// Effectively the same functionality as ng-click="" for click, pointer and touch tap | |
// | |
.directive('coTap', ['Core.SafeApply', 'Core.TouchMouse', function(safeApply, touchMouse) { | |
return function(scope, element, attrs) { | |
var tracker = {}, | |
events = touchMouse.getPointerEvents(scope, element, { | |
down: function(id, event) { | |
tracker[id] = true // execute callback == true | |
}, | |
move: function(id, event) { | |
tracker[id] = false | |
}, | |
up: function(id, event) { | |
try { | |
if(tracker[id]) { | |
safeApply.do(scope, attrs['coTap']); // Apply the click, touch, point event | |
} | |
} finally { | |
delete tracker[id]; | |
} | |
}, | |
cancel: function(id, event) { | |
delete tracker[id]; | |
} | |
}); | |
}; | |
}]) | |
// | |
// Provides object dragging functionality where the first touch point takes control of the drag | |
// Based on: https://github.com/jquerytools/jquerytools/blob/master/src/rangeinput/rangeinput.js#L46 | |
// | |
.directive('coDraggable', ['Core.SafeApply', 'Core.TouchMouse', function(safeApply, touchMouse) { | |
return { | |
restrict: 'A', // Attribute i.e. <div co-draggable="true" min="0" max="23" /> | |
transclude: false, // Contents of the tag are discarded | |
replace: false, // Remove the actual Range tag | |
scope: { | |
onDragStart:'&', // func_name(new_val, by_user) | |
onDrag:'&', // create a delegate onChange function | |
onDragEnd:'&' // create a delegate onChange function | |
}, | |
link: function(scope, element, attrs) { | |
var offset, // Actual offset of element | |
x0, // X 0ffset of drag start | |
y0, // Y 0ffset of drag start | |
x, // Current X offset during drag | |
y, // Current Y offset during drag | |
props, // Stores the change in position | |
firstId, // Touch id performing the drag | |
start, // Is this the first move | |
events = touchMouse.getPointerEvents(scope, element, { | |
down: function(id, e) { | |
if(!firstId) { | |
firstId = id; | |
offset = element.position(); | |
x0 = e.pageX - offset.left; | |
y0 = e.pageY - offset.top; | |
start = true; | |
} | |
}, | |
move: function(id, e) { | |
if(firstId == id) { | |
props = {}; | |
x = e.pageX - x0; | |
y = e.pageY - y0; | |
if (scope.moveX) { props.left = x; } | |
if (scope.moveY) { props.top = y; } | |
if (start === true && attrs['onDragStart']) { | |
safeApply.do(scope, function() { | |
scope.onDragStart({event: e}); | |
}); | |
} | |
if (scope.drag) { element.css(props); } // If we want the drag to occur | |
if (attrs['onDrag']) { | |
safeApply.do(scope, function() { | |
scope.onDrag({event: e, position: props}); | |
}); | |
} | |
} | |
}, | |
up: function(id, e) { | |
if(firstId == id) { | |
firstId = undefined; | |
if (attrs['onDragEnd']) { | |
safeApply.do(scope, function() { | |
scope.onDragEnd({event: e}); | |
}); | |
} | |
} | |
} | |
}); | |
scope.moveX = attrs['moveX'] != 'false'; | |
scope.moveY = attrs['moveY'] != 'false'; | |
scope.drag = attrs['coDraggable'] == 'true'; | |
} | |
}; | |
}]); | |
})(angular); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment