Skip to content

Instantly share code, notes, and snippets.

@stakach
Created April 8, 2013 08:57
Show Gist options
  • Save stakach/5335327 to your computer and use it in GitHub Desktop.
Save stakach/5335327 to your computer and use it in GitHub Desktop.
AngularJS Mobile + Desktop Events
(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