mirror of https://github.com/twbs/ratchet.git
Build mobile apps with simple HTML, CSS, and JS components.
http://goratchet.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
273 lines
7.7 KiB
273 lines
7.7 KiB
// FINGERBLAST.js |
|
// -------------- |
|
// Adapted from phantom limb by Brian Cartensen |
|
|
|
/* jshint bitwise: false */ |
|
/* global GLOBAL: true */ |
|
|
|
(function () { |
|
|
|
'use strict'; |
|
|
|
function FingerBlast (element) { |
|
this.element = typeof element === 'string' ? document.querySelector(element) : element; |
|
|
|
if (this.element) { |
|
this.listen(); |
|
} |
|
} |
|
|
|
FingerBlast.prototype = { |
|
x: NaN, |
|
y: NaN, |
|
|
|
startDistance: NaN, |
|
startAngle: NaN, |
|
|
|
mouseIsDown: false, |
|
|
|
listen: function () { |
|
var activate = this.activate.bind(this); |
|
var deactivate = this.deactivate.bind(this); |
|
|
|
function contains (element, ancestor) { |
|
var descendants; |
|
var index; |
|
var descendant; |
|
|
|
if (!element) { |
|
return false; |
|
} |
|
|
|
if ('compareDocumentPosition' in ancestor) { |
|
return !!(ancestor.compareDocumentPosition(element) & 16); |
|
} else if ('contains' in ancestor) { |
|
return ancestor !== element && ancestor.contains(element); |
|
} else { |
|
for ((descendants = ancestor.getElementsByTagName('*')), index = 0; (descendant = descendants[index++]);) { |
|
if (descendant === element) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
} |
|
|
|
this.element.addEventListener('mouseover', function (e) { |
|
var target = e.relatedTarget; |
|
if (target !== this && !contains(target, this)) { |
|
activate(); |
|
} |
|
}); |
|
|
|
this.element.addEventListener('mouseout', function (e) { |
|
var target = e.relatedTarget; |
|
if (target !== this && !contains(target, this)) { |
|
deactivate(e); |
|
} |
|
}); |
|
}, |
|
|
|
activate: function () { |
|
if (this.active) { |
|
return; |
|
} |
|
this.element.addEventListener('mousedown', (this.touchStart = this.touchStart.bind(this)), true); |
|
this.element.addEventListener('mousemove', (this.touchMove = this.touchMove.bind(this)), true); |
|
this.element.addEventListener('mouseup', (this.touchEnd = this.touchEnd.bind(this)), true); |
|
this.element.addEventListener('click', (this.click = this.click.bind(this)), true); |
|
this.active = true; |
|
}, |
|
|
|
deactivate: function (e) { |
|
this.active = false; |
|
if (this.mouseIsDown) { |
|
this.touchEnd(e); |
|
} |
|
this.element.removeEventListener('mousedown', this.touchStart, true); |
|
this.element.removeEventListener('mousemove', this.touchMove, true); |
|
this.element.removeEventListener('mouseup', this.touchEnd, true); |
|
this.element.removeEventListener('click', this.click, true); |
|
}, |
|
|
|
click: function (e) { |
|
if (e.synthetic) { |
|
return; |
|
} |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
}, |
|
|
|
touchStart: function (e) { |
|
if (e.synthetic || /input|textarea/.test(e.target.tagName.toLowerCase())) { |
|
return; |
|
} |
|
|
|
this.mouseIsDown = true; |
|
|
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
|
|
this.fireTouchEvents('touchstart', e); |
|
}, |
|
|
|
touchMove: function (e) { |
|
if (e.synthetic) { |
|
return; |
|
} |
|
|
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
|
|
this.move(e.clientX, e.clientY); |
|
|
|
if (this.mouseIsDown) { |
|
this.fireTouchEvents('touchmove', e); |
|
} |
|
}, |
|
|
|
touchEnd: function (e) { |
|
if (e.synthetic) { |
|
return; |
|
} |
|
|
|
this.mouseIsDown = false; |
|
|
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
|
|
this.fireTouchEvents('touchend', e); |
|
|
|
if (!this.target) { |
|
return; |
|
} |
|
|
|
// Mobile Safari moves all the mouse events to fire after the touchend event. |
|
this.target.dispatchEvent(this.createMouseEvent('mouseover', e)); |
|
this.target.dispatchEvent(this.createMouseEvent('mousemove', e)); |
|
this.target.dispatchEvent(this.createMouseEvent('mousedown', e)); |
|
}, |
|
|
|
fireTouchEvents: function (eventName, originalEvent) { |
|
var events = []; |
|
var gestures = []; |
|
|
|
if (!this.target) { |
|
return; |
|
} |
|
|
|
// Convert 'ontouch*' properties and attributes to listeners. |
|
var onEventName = 'on' + eventName; |
|
|
|
if (onEventName in this.target) { |
|
console.warn('Converting `' + onEventName + '` property to event listener.', this.target); |
|
this.target.addEventListener(eventName, this.target[onEventName], false); |
|
delete this.target[onEventName]; |
|
} |
|
|
|
if (this.target.hasAttribute(onEventName)) { |
|
console.warn('Converting `' + onEventName + '` attribute to event listener.', this.target); |
|
var handler = new GLOBAL.Function('event', this.target.getAttribute(onEventName)); |
|
this.target.addEventListener(eventName, handler, false); |
|
this.target.removeAttribute(onEventName); |
|
} |
|
|
|
// Set up a new event with the coordinates of the finger. |
|
var touch = this.createMouseEvent(eventName, originalEvent); |
|
|
|
events.push(touch); |
|
|
|
// Figure out scale and rotation. |
|
if (events.length > 1) { |
|
var x = events[0].pageX - events[1].pageX; |
|
var y = events[0].pageY - events[1].pageY; |
|
|
|
var distance = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); |
|
var angle = Math.atan2(x, y) * (180 / Math.PI); |
|
|
|
var gestureName = 'gesturechange'; |
|
|
|
if (eventName === 'touchstart') { |
|
gestureName = 'gesturestart'; |
|
this.startDistance = distance; |
|
this.startAngle = angle; |
|
} |
|
|
|
if (eventName === 'touchend') { |
|
gestureName = 'gestureend'; |
|
} |
|
|
|
events.forEach(function (event) { |
|
var gesture = this.createMouseEvent.call(event._finger, gestureName, event); |
|
gestures.push(gesture); |
|
}.bind(this)); |
|
|
|
events.concat(gestures).forEach(function (event) { |
|
event.scale = distance / this.startDistance; |
|
event.rotation = this.startAngle - angle; |
|
}); |
|
} |
|
|
|
// Loop through the events array and fill in each touch array. |
|
events.forEach(function (touch) { |
|
touch.touches = events.filter(function (e) { |
|
return ~e.type.indexOf('touch') && e.type !== 'touchend'; |
|
}); |
|
|
|
touch.changedTouches = events.filter(function (e) { |
|
return ~e.type.indexOf('touch') && e._finger.target === touch._finger.target; |
|
}); |
|
|
|
touch.targetTouches = touch.changedTouches.filter(function (e) { |
|
return ~e.type.indexOf('touch') && e.type !== 'touchend'; |
|
}); |
|
}); |
|
|
|
// Then fire the events. |
|
events.concat(gestures).forEach(function (event, i) { |
|
event.identifier = i; |
|
event._finger.target.dispatchEvent(event); |
|
}); |
|
}, |
|
|
|
createMouseEvent: function (eventName, originalEvent) { |
|
var e = new MouseEvent(eventName, { |
|
view : window, |
|
detail : originalEvent.detail, |
|
bubbles : true, |
|
cancelable : true, |
|
target : this.target || originalEvent.relatedTarget, |
|
clientX : this.x || originalEvent.clientX, |
|
clientY : this.y || originalEvent.clientY, |
|
screenX : this.x || originalEvent.screenX, |
|
screenY : this.y || originalEvent.screenY, |
|
ctrlKey : originalEvent.ctrlKey, |
|
shiftKey : originalEvent.shiftKey, |
|
altKey : originalEvent.altKey, |
|
metaKey : originalEvent.metaKey, |
|
button : originalEvent.button |
|
}); |
|
|
|
e.synthetic = true; |
|
e._finger = this; |
|
|
|
return e; |
|
}, |
|
|
|
move: function (x, y) { |
|
if (isNaN(x) || isNaN(y)) { |
|
this.target = null; |
|
} else { |
|
this.x = x; |
|
this.y = y; |
|
|
|
if (!this.mouseIsDown) { |
|
this.target = document.elementFromPoint(x, y); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
window.FingerBlast = FingerBlast; |
|
|
|
}());
|
|
|