mirror of
https://github.com/openstf/stf
synced 2025-10-05 02:29:26 +02:00
922 lines
29 KiB
JavaScript
922 lines
29 KiB
JavaScript
var _ = require('lodash')
|
|
var rotator = require('./rotator')
|
|
|
|
module.exports = function DeviceScreenDirective(
|
|
$document
|
|
, ScalingService
|
|
, VendorUtil
|
|
, PageVisibilityService
|
|
, $timeout
|
|
, $window
|
|
) {
|
|
return {
|
|
restrict: 'E'
|
|
, template: require('./screen.jade')
|
|
, scope: {
|
|
control: '&'
|
|
, device: '&'
|
|
}
|
|
, link: function (scope, element) {
|
|
var URL = window.URL || window.webkitURL
|
|
var BLANK_IMG =
|
|
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
|
|
var cssTransform = VendorUtil.style(['transform', 'webkitTransform'])
|
|
|
|
var device = scope.device()
|
|
, control = scope.control()
|
|
|
|
var input = element.find('input')
|
|
|
|
var screen = scope.screen = {
|
|
rotation: 0
|
|
, bounds: {
|
|
x: 0
|
|
, y: 0
|
|
, w: 0
|
|
, h: 0
|
|
}
|
|
}
|
|
|
|
var scaler = ScalingService.coordinator(
|
|
device.display.width
|
|
, device.display.height
|
|
)
|
|
|
|
/**
|
|
* SCREEN HANDLING
|
|
*
|
|
* This section should deal with updating the screen ONLY.
|
|
*/
|
|
;(function() {
|
|
function stop() {
|
|
try {
|
|
ws.onerror = ws.onclose = ws.onmessage = ws.onopen = null
|
|
ws.close()
|
|
ws = null
|
|
}
|
|
catch (err) { /* noop */ }
|
|
}
|
|
|
|
var ws = new WebSocket(device.display.url)
|
|
ws.binaryType = 'blob'
|
|
|
|
var cleanupList = []
|
|
|
|
ws.onerror = function errorListener() {
|
|
// @todo Handle
|
|
}
|
|
|
|
ws.onclose = function closeListener() {
|
|
// @todo Maybe handle
|
|
}
|
|
|
|
ws.onopen = function openListener() {
|
|
var canvas = element.find('canvas')[0]
|
|
, g = canvas.getContext('2d')
|
|
, positioner = element.find('div')[0]
|
|
|
|
function vendorBackingStorePixelRatio(g) {
|
|
return g.webkitBackingStorePixelRatio ||
|
|
g.mozBackingStorePixelRatio ||
|
|
g.msBackingStorePixelRatio ||
|
|
g.oBackingStorePixelRatio ||
|
|
g.backingStorePixelRatio || 1
|
|
}
|
|
|
|
var devicePixelRatio = window.devicePixelRatio || 1
|
|
, backingStoreRatio = vendorBackingStorePixelRatio(g)
|
|
, frontBackRatio = devicePixelRatio / backingStoreRatio
|
|
|
|
var options = {
|
|
autoScaleForRetina: true
|
|
, density: Math.max(1, Math.min(1.5, devicePixelRatio || 1))
|
|
, minscale: 0.36
|
|
}
|
|
|
|
var adjustedBoundSize
|
|
var cachedEnabled = false
|
|
|
|
function updateBounds() {
|
|
function adjustBoundedSize(w, h) {
|
|
var sw = w * options.density
|
|
, sh = h * options.density
|
|
, f
|
|
|
|
if (sw < (f = device.display.width * options.minscale)) {
|
|
sw *= f / sw
|
|
sh *= f / sh
|
|
}
|
|
|
|
if (sh < (f = device.display.height * options.minscale)) {
|
|
sw *= f / sw
|
|
sh *= f / sh
|
|
}
|
|
|
|
return {
|
|
w: Math.ceil(sw)
|
|
, h: Math.ceil(sh)
|
|
}
|
|
}
|
|
|
|
// FIXME: element is an object HTMLUnknownElement in IE9
|
|
var w = screen.bounds.w = element[0].offsetWidth
|
|
, h = screen.bounds.h = element[0].offsetHeight
|
|
|
|
// Developer error, let's try to reduce debug time
|
|
if (!w || !h) {
|
|
throw new Error(
|
|
'Unable to read bounds; container must have dimensions'
|
|
)
|
|
}
|
|
|
|
var newAdjustedBoundSize = (function() {
|
|
switch (screen.rotation) {
|
|
case 90:
|
|
case 270:
|
|
return adjustBoundedSize(h, w)
|
|
case 0:
|
|
case 180:
|
|
/* falls through */
|
|
default:
|
|
return adjustBoundedSize(w, h)
|
|
}
|
|
})()
|
|
|
|
if (!adjustedBoundSize
|
|
|| newAdjustedBoundSize.w !== adjustedBoundSize.w
|
|
|| newAdjustedBoundSize.h !== adjustedBoundSize.h) {
|
|
adjustedBoundSize = newAdjustedBoundSize
|
|
onScreenInterestAreaChanged()
|
|
}
|
|
}
|
|
|
|
function shouldUpdateScreen() {
|
|
return (
|
|
// NO if the user has disabled the screen.
|
|
scope.$parent.showScreen &&
|
|
// NO if we're not even using the device anymore.
|
|
device.using &&
|
|
// NO if the page is not visible (e.g. background tab).
|
|
!PageVisibilityService.hidden
|
|
// YES otherwise
|
|
)
|
|
}
|
|
|
|
function checkEnabled() {
|
|
var newEnabled = shouldUpdateScreen()
|
|
|
|
if (newEnabled === cachedEnabled) {
|
|
updateBounds()
|
|
}
|
|
else if (newEnabled) {
|
|
updateBounds()
|
|
onScreenInterestGained()
|
|
}
|
|
else {
|
|
g.clearRect(0, 0, canvas.width, canvas.height)
|
|
onScreenInterestLost()
|
|
}
|
|
|
|
cachedEnabled = newEnabled
|
|
}
|
|
|
|
function onScreenInterestGained() {
|
|
ws.send('size ' + adjustedBoundSize.w + 'x' + adjustedBoundSize.h)
|
|
ws.send('on')
|
|
}
|
|
|
|
function onScreenInterestAreaChanged() {
|
|
ws.send('size ' + adjustedBoundSize.w + 'x' + adjustedBoundSize.h)
|
|
}
|
|
|
|
function onScreenInterestLost() {
|
|
ws.send('off')
|
|
}
|
|
|
|
ws.onmessage = (function() {
|
|
var cachedScreen = {
|
|
rotation: 0
|
|
, bounds: {
|
|
x: 0
|
|
, y: 0
|
|
, w: 0
|
|
, h: 0
|
|
}
|
|
}
|
|
|
|
var cachedImageWidth = 0
|
|
, cachedImageHeight = 0
|
|
, cssRotation = 0
|
|
, alwaysUpright = false
|
|
|
|
function applyQuirks(banner) {
|
|
element[0].classList.toggle(
|
|
'quirk-always-upright', alwaysUpright = banner.quirks.alwaysUpright)
|
|
}
|
|
|
|
function hasImageAreaChanged(img) {
|
|
return cachedScreen.bounds.w !== screen.bounds.w ||
|
|
cachedScreen.bounds.h !== screen.bounds.h ||
|
|
cachedImageWidth !== img.width ||
|
|
cachedImageHeight !== img.height ||
|
|
cachedScreen.rotation !== screen.rotation
|
|
}
|
|
|
|
function isRotated() {
|
|
return screen.rotation === 90 || screen.rotation === 270
|
|
}
|
|
|
|
function updateImageArea(img) {
|
|
if (!hasImageAreaChanged(img)) {
|
|
return
|
|
}
|
|
|
|
cachedImageWidth = img.width
|
|
cachedImageHeight = img.height
|
|
|
|
if (options.autoScaleForRetina) {
|
|
canvas.width = cachedImageWidth * frontBackRatio
|
|
canvas.height = cachedImageHeight * frontBackRatio
|
|
g.scale(frontBackRatio, frontBackRatio)
|
|
}
|
|
else {
|
|
canvas.width = cachedImageWidth
|
|
canvas.height = cachedImageHeight
|
|
}
|
|
|
|
cssRotation += rotator(cachedScreen.rotation, screen.rotation)
|
|
|
|
canvas.style[cssTransform] = 'rotate(' + cssRotation + 'deg)'
|
|
|
|
cachedScreen.bounds.h = screen.bounds.h
|
|
cachedScreen.bounds.w = screen.bounds.w
|
|
cachedScreen.rotation = screen.rotation
|
|
|
|
canvasAspect = canvas.width / canvas.height
|
|
|
|
if (isRotated() && !alwaysUpright) {
|
|
canvasAspect = img.height / img.width
|
|
element[0].classList.add('rotated')
|
|
}
|
|
else {
|
|
canvasAspect = img.width / img.height
|
|
element[0].classList.remove('rotated')
|
|
}
|
|
|
|
if (alwaysUpright) {
|
|
// If the screen image is always in upright position (but we
|
|
// still want the rotation animation), we need to cancel out
|
|
// the rotation by using another rotation.
|
|
positioner.style[cssTransform] = 'rotate(' + -cssRotation + 'deg)'
|
|
}
|
|
|
|
maybeFlipLetterbox()
|
|
}
|
|
|
|
return function messageListener(message) {
|
|
screen.rotation = device.display.rotation
|
|
|
|
if (message.data instanceof Blob) {
|
|
if (shouldUpdateScreen()) {
|
|
if (scope.displayError) {
|
|
scope.$apply(function () {
|
|
scope.displayError = false
|
|
})
|
|
}
|
|
|
|
var blob = new Blob([message.data], {
|
|
type: 'image/jpeg'
|
|
})
|
|
|
|
var img = new Image()
|
|
|
|
img.onload = function() {
|
|
updateImageArea(this)
|
|
|
|
g.drawImage(img, 0, 0, img.width, img.height)
|
|
|
|
// Try to forcefully clean everything to get rid of memory
|
|
// leaks. Note that despite this effort, Chrome will still
|
|
// leak huge amounts of memory when the developer tools are
|
|
// open, probably to save the resources for inspection. When
|
|
// the developer tools are closed no memory is leaked.
|
|
img.onload = img.onerror = null
|
|
img.src = BLANK_IMG
|
|
img = null
|
|
blob = null
|
|
|
|
URL.revokeObjectURL(url)
|
|
url = null
|
|
}
|
|
|
|
img.onerror = function() {
|
|
// Happily ignore. I suppose this shouldn't happen, but
|
|
// sometimes it does, presumably when we're loading images
|
|
// too quickly.
|
|
|
|
// Do the same cleanup here as in onload.
|
|
img.onload = img.onerror = null
|
|
img.src = BLANK_IMG
|
|
img = null
|
|
blob = null
|
|
|
|
URL.revokeObjectURL(url)
|
|
url = null
|
|
}
|
|
|
|
var url = URL.createObjectURL(blob)
|
|
img.src = url
|
|
}
|
|
}
|
|
else if (/^start /.test(message.data)) {
|
|
applyQuirks(JSON.parse(message.data.substr('start '.length)))
|
|
}
|
|
else if (message.data === 'secure_on') {
|
|
scope.$apply(function () {
|
|
scope.displayError = 'secure'
|
|
})
|
|
}
|
|
}
|
|
})()
|
|
|
|
// NOTE: instead of fa-pane-resize, a fa-child-pane-resize could be better
|
|
cleanupList.push(scope.$on('fa-pane-resize', _.debounce(updateBounds, 1000)))
|
|
cleanupList.push(scope.$watch('device.using', checkEnabled))
|
|
cleanupList.push(scope.$on('visibilitychange', checkEnabled))
|
|
cleanupList.push(scope.$watch('$parent.showScreen', checkEnabled))
|
|
}
|
|
|
|
scope.retryLoadingScreen = function () {
|
|
if (scope.displayError === 'secure') {
|
|
control.home()
|
|
}
|
|
}
|
|
|
|
scope.$on('guest-portrait', function () {
|
|
control.rotate(0)
|
|
})
|
|
|
|
scope.$on('guest-landscape', function () {
|
|
control.rotate(90)
|
|
})
|
|
|
|
var canvasAspect = 1
|
|
, parentAspect = 1
|
|
|
|
function resizeListener() {
|
|
parentAspect = element[0].offsetWidth / element[0].offsetHeight
|
|
maybeFlipLetterbox()
|
|
}
|
|
|
|
function maybeFlipLetterbox() {
|
|
element[0].classList.toggle(
|
|
'letterboxed', parentAspect < canvasAspect)
|
|
}
|
|
|
|
$window.addEventListener('beforeunload', stop, false)
|
|
$window.addEventListener('resize', resizeListener, false)
|
|
scope.$on('fa-pane-resize', resizeListener)
|
|
|
|
resizeListener()
|
|
|
|
scope.$on('$destroy', function() {
|
|
stop()
|
|
$window.removeEventListener('beforeunload', stop, false)
|
|
$window.removeEventListener('resize', resizeListener, false)
|
|
})
|
|
})()
|
|
|
|
/**
|
|
* KEYBOARD HANDLING
|
|
*
|
|
* This should be moved elsewhere, but due to shared dependencies and
|
|
* elements it's currently here. So basically due to laziness.
|
|
*
|
|
* For now, try to keep the whole section as a separate unit as much
|
|
* as possible.
|
|
*/
|
|
;(function() {
|
|
function isChangeCharsetKey(e) {
|
|
// Add any special key here for changing charset
|
|
//console.log('e', e)
|
|
|
|
// Chrome/Safari/Opera
|
|
if (
|
|
// Mac | Kinesis keyboard | Karabiner | Latin key, Kana key
|
|
e.keyCode === 0 && e.keyIdentifier === 'U+0010' ||
|
|
|
|
// Mac | MacBook Pro keyboard | Latin key, Kana key
|
|
e.keyCode === 0 && e.keyIdentifier === 'U+0020' ||
|
|
|
|
// Win | Lenovo X230 keyboard | Alt+Latin key
|
|
e.keyCode === 246 && e.keyIdentifier === 'U+00F6' ||
|
|
|
|
// Win | Lenovo X230 keyboard | Convert key
|
|
e.keyCode === 28 && e.keyIdentifier === 'U+001C'
|
|
) {
|
|
return true
|
|
}
|
|
|
|
// Firefox
|
|
switch (e.key) {
|
|
case 'Convert': // Windows | Convert key
|
|
case 'Alphanumeric': // Mac | Latin key
|
|
case 'RomanCharacters': // Windows/Mac | Latin key
|
|
case 'KanjiMode': // Windows/Mac | Kana key
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function handleSpecialKeys(e) {
|
|
if (isChangeCharsetKey(e)) {
|
|
e.preventDefault()
|
|
control.keyPress('switch_charset')
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function keydownListener(e) {
|
|
// Prevent tab from switching focus to the next element, we only want
|
|
// that to happen on the device side.
|
|
if (e.keyCode === 9) {
|
|
e.preventDefault()
|
|
}
|
|
control.keyDown(e.keyCode)
|
|
}
|
|
|
|
function keyupListener(e) {
|
|
if (!handleSpecialKeys(e)) {
|
|
control.keyUp(e.keyCode)
|
|
}
|
|
}
|
|
|
|
function pasteListener(e) {
|
|
// Prevent value change or the input event sees it. This way we get
|
|
// the real value instead of any "\n" -> " " conversions we might see
|
|
// in the input value.
|
|
e.preventDefault()
|
|
control.paste(e.clipboardData.getData('text/plain'))
|
|
}
|
|
|
|
function copyListener(e) {
|
|
e.preventDefault()
|
|
// This is asynchronous and by the time it returns we will no longer
|
|
// have access to setData(). In other words it doesn't work. Currently
|
|
// what happens is that on the first copy, it will attempt to fetch
|
|
// the clipboard contents. Only on the second copy will it actually
|
|
// copy that to the clipboard.
|
|
control.getClipboardContent()
|
|
if (control.clipboardContent) {
|
|
e.clipboardData.setData('text/plain', control.clipboardContent)
|
|
}
|
|
}
|
|
|
|
function inputListener() {
|
|
// Why use the input event if we don't let it handle pasting? The
|
|
// reason is that on latest Safari (Version 8.0 (10600.1.25)), if
|
|
// you use the "Romaji" Kotoeri input method, we'll never get any
|
|
// keypress events. It also causes us to lose the very first keypress
|
|
// on the page. Currently I'm not sure if we can fix that one.
|
|
control.type(this.value)
|
|
this.value = ''
|
|
}
|
|
|
|
input.bind('keydown', keydownListener)
|
|
input.bind('keyup', keyupListener)
|
|
input.bind('input', inputListener)
|
|
input.bind('paste', pasteListener)
|
|
input.bind('copy', copyListener)
|
|
})()
|
|
|
|
/**
|
|
* TOUCH HANDLING
|
|
*
|
|
* This should be moved elsewhere, but due to shared dependencies and
|
|
* elements it's currently here. So basically due to laziness.
|
|
*
|
|
* For now, try to keep the whole section as a separate unit as much
|
|
* as possible.
|
|
*/
|
|
;(function() {
|
|
var slots = []
|
|
, slotted = Object.create(null)
|
|
, fingers = []
|
|
, seq = -1
|
|
, cycle = 100
|
|
, fakePinch = false
|
|
, lastPossiblyBuggyMouseUpEvent = 0
|
|
|
|
function nextSeq() {
|
|
return ++seq >= cycle ? (seq = 0) : seq
|
|
}
|
|
|
|
function createSlots() {
|
|
// The reverse order is important because slots and fingers are in
|
|
// opposite sort order. Anyway don't change anything here unless
|
|
// you understand what it does and why.
|
|
for (var i = 9; i >= 0; --i) {
|
|
var finger = createFinger(i)
|
|
element.append(finger)
|
|
slots.push(i)
|
|
fingers.unshift(finger)
|
|
}
|
|
}
|
|
|
|
function activateFinger(index, x, y, pressure) {
|
|
var scale = 0.5 + pressure
|
|
fingers[index].classList.add('active')
|
|
fingers[index].style[cssTransform] =
|
|
'translate3d(' + x + 'px,' + y + 'px,0) ' +
|
|
'scale(' + scale + ',' + scale + ')'
|
|
}
|
|
|
|
function deactivateFinger(index) {
|
|
fingers[index].classList.remove('active')
|
|
}
|
|
|
|
function deactivateFingers() {
|
|
for (var i = 0, l = fingers.length; i < l; ++i) {
|
|
fingers[i].classList.remove('active')
|
|
}
|
|
}
|
|
|
|
function createFinger(index) {
|
|
var el = document.createElement('span')
|
|
el.className = 'finger finger-' + index
|
|
return el
|
|
}
|
|
|
|
function calculateBounds() {
|
|
var el = element[0]
|
|
|
|
screen.bounds.w = el.offsetWidth
|
|
screen.bounds.h = el.offsetHeight
|
|
screen.bounds.x = 0
|
|
screen.bounds.y = 0
|
|
|
|
while (el.offsetParent) {
|
|
screen.bounds.x += el.offsetLeft
|
|
screen.bounds.y += el.offsetTop
|
|
el = el.offsetParent
|
|
}
|
|
}
|
|
|
|
function mouseDownListener(e) {
|
|
if (e.originalEvent) {
|
|
e = e.originalEvent
|
|
}
|
|
|
|
// Skip secondary click
|
|
if (e.which === 3) {
|
|
return
|
|
}
|
|
|
|
e.preventDefault()
|
|
|
|
fakePinch = e.altKey
|
|
|
|
calculateBounds()
|
|
startMousing()
|
|
|
|
var x = e.pageX - screen.bounds.x
|
|
, y = e.pageY - screen.bounds.y
|
|
, pressure = 0.5
|
|
, scaled = scaler.coords(
|
|
screen.bounds.w
|
|
, screen.bounds.h
|
|
, x
|
|
, y
|
|
, screen.rotation
|
|
)
|
|
|
|
control.touchDown(nextSeq(), 0, scaled.xP, scaled.yP, pressure)
|
|
|
|
if (fakePinch) {
|
|
control.touchDown(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP,
|
|
pressure)
|
|
}
|
|
|
|
control.touchCommit(nextSeq())
|
|
|
|
activateFinger(0, x, y, pressure)
|
|
|
|
if (fakePinch) {
|
|
activateFinger(1, -e.pageX + screen.bounds.x + screen.bounds.w,
|
|
-e.pageY + screen.bounds.y + screen.bounds.h, pressure)
|
|
}
|
|
|
|
element.bind('mousemove', mouseMoveListener)
|
|
$document.bind('mouseup', mouseUpListener)
|
|
$document.bind('mouseleave', mouseUpListener)
|
|
|
|
if (lastPossiblyBuggyMouseUpEvent &&
|
|
lastPossiblyBuggyMouseUpEvent.timeStamp > e.timeStamp) {
|
|
// We got mouseup before mousedown. See mouseUpBugWorkaroundListener
|
|
// for details.
|
|
mouseUpListener(lastPossiblyBuggyMouseUpEvent)
|
|
}
|
|
else {
|
|
lastPossiblyBuggyMouseUpEvent = null
|
|
}
|
|
}
|
|
|
|
function mouseMoveListener(e) {
|
|
if (e.originalEvent) {
|
|
e = e.originalEvent
|
|
}
|
|
|
|
// Skip secondary click
|
|
if (e.which === 3) {
|
|
return
|
|
}
|
|
e.preventDefault()
|
|
|
|
var addGhostFinger = !fakePinch && e.altKey
|
|
var deleteGhostFinger = fakePinch && !e.altKey
|
|
|
|
fakePinch = e.altKey
|
|
|
|
var x = e.pageX - screen.bounds.x
|
|
, y = e.pageY - screen.bounds.y
|
|
, pressure = 0.5
|
|
, scaled = scaler.coords(
|
|
screen.bounds.w
|
|
, screen.bounds.h
|
|
, x
|
|
, y
|
|
, screen.rotation
|
|
)
|
|
|
|
control.touchMove(nextSeq(), 0, scaled.xP, scaled.yP, pressure)
|
|
|
|
if (addGhostFinger) {
|
|
control.touchDown(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure)
|
|
}
|
|
else if (deleteGhostFinger) {
|
|
control.touchUp(nextSeq(), 1)
|
|
}
|
|
else if (fakePinch) {
|
|
control.touchMove(nextSeq(), 1, 1 - scaled.xP, 1 - scaled.yP, pressure)
|
|
}
|
|
|
|
control.touchCommit(nextSeq())
|
|
|
|
activateFinger(0, x, y, pressure)
|
|
|
|
if (deleteGhostFinger) {
|
|
deactivateFinger(1)
|
|
}
|
|
else if (fakePinch) {
|
|
activateFinger(1, -e.pageX + screen.bounds.x + screen.bounds.w,
|
|
-e.pageY + screen.bounds.y + screen.bounds.h, pressure)
|
|
}
|
|
}
|
|
|
|
function mouseUpListener(e) {
|
|
if (e.originalEvent) {
|
|
e = e.originalEvent
|
|
}
|
|
|
|
// Skip secondary click
|
|
if (e.which === 3) {
|
|
return
|
|
}
|
|
e.preventDefault()
|
|
|
|
control.touchUp(nextSeq(), 0)
|
|
|
|
if (fakePinch) {
|
|
control.touchUp(nextSeq(), 1)
|
|
}
|
|
|
|
control.touchCommit(nextSeq())
|
|
|
|
deactivateFinger(0)
|
|
|
|
if (fakePinch) {
|
|
deactivateFinger(1)
|
|
}
|
|
|
|
stopMousing()
|
|
}
|
|
|
|
/**
|
|
* Do NOT remove under any circumstances. Currently, in the latest
|
|
* Safari (Version 8.0 (10600.1.25)), if an input field is focused
|
|
* while we do a tap click on an MBP trackpad ("Tap to click" in
|
|
* Settings), it sometimes causes the mouseup event to trigger before
|
|
* the mousedown event (but event.timeStamp will be correct). It
|
|
* doesn't happen in any other browser. The following minimal test
|
|
* case triggers the same behavior (although less frequently). Keep
|
|
* tapping and you'll eventually see see two mouseups in a row with
|
|
* the same counter value followed by a mousedown with a new counter
|
|
* value. Also, when the bug happens, the cursor in the input field
|
|
* stops blinking. It may take up to 300 attempts to spot the bug on
|
|
* a MacBook Pro (Retina, 15-inch, Mid 2014).
|
|
*
|
|
* <!doctype html>
|
|
*
|
|
* <div id="touchable"
|
|
* style="width: 100px; height: 100px; background: green"></div>
|
|
* <input id="focusable" type="text" />
|
|
*
|
|
* <script>
|
|
* var touchable = document.getElementById('touchable')
|
|
* , focusable = document.getElementById('focusable')
|
|
* , counter = 0
|
|
*
|
|
* function mousedownListener(e) {
|
|
* counter += 1
|
|
* console.log('mousedown', counter, e, e.timeStamp)
|
|
* e.preventDefault()
|
|
* }
|
|
*
|
|
* function mouseupListener(e) {
|
|
* e.preventDefault()
|
|
* console.log('mouseup', counter, e, e.timeStamp)
|
|
* focusable.focus()
|
|
* }
|
|
*
|
|
* touchable.addEventListener('mousedown', mousedownListener, false)
|
|
* touchable.addEventListener('mouseup', mouseupListener, false)
|
|
* </script>
|
|
*
|
|
* I believe that the bug is caused by some kind of a race condition
|
|
* in Safari. Using a textarea or a focused contenteditable does not
|
|
* get rid of the bug. The bug also happens if the text field is
|
|
* focused manually by the user (not with .focus()).
|
|
*
|
|
* It also doesn't help if you .blur() before .focus().
|
|
*
|
|
* So basically we'll just have to store the event on mouseup and check
|
|
* if we should do the browser's job in the mousedown handler.
|
|
*/
|
|
function mouseUpBugWorkaroundListener(e) {
|
|
lastPossiblyBuggyMouseUpEvent = e
|
|
}
|
|
|
|
function startMousing() {
|
|
control.gestureStart(nextSeq())
|
|
input[0].focus()
|
|
}
|
|
|
|
function stopMousing() {
|
|
element.unbind('mousemove', mouseMoveListener)
|
|
$document.unbind('mouseup', mouseUpListener)
|
|
$document.unbind('mouseleave', mouseUpListener)
|
|
deactivateFingers()
|
|
control.gestureStop(nextSeq())
|
|
}
|
|
|
|
function touchStartListener(e) {
|
|
e.preventDefault()
|
|
|
|
//Make it jQuery compatible also
|
|
if (e.originalEvent) {
|
|
e = e.originalEvent
|
|
}
|
|
|
|
calculateBounds()
|
|
|
|
if (e.touches.length === e.changedTouches.length) {
|
|
startTouching()
|
|
}
|
|
|
|
var currentTouches = Object.create(null)
|
|
var i, l
|
|
|
|
for (i = 0, l = e.touches.length; i < l; ++i) {
|
|
currentTouches[e.touches[i].identifier] = 1;
|
|
}
|
|
|
|
function maybeLostTouchEnd(id) {
|
|
return !(id in currentTouches)
|
|
}
|
|
|
|
// We might have lost a touchend event due to various edge cases
|
|
// (literally) such as dragging from the bottom of the screen so that
|
|
// the control center appears. If so, let's ask for a reset.
|
|
if (Object.keys(slotted).some(maybeLostTouchEnd)) {
|
|
Object.keys(slotted).forEach(function(id) {
|
|
slots.push(slotted[id])
|
|
delete slotted[id]
|
|
})
|
|
slots.sort().reverse()
|
|
control.touchReset(nextSeq())
|
|
deactivateFingers()
|
|
}
|
|
|
|
if (!slots.length) {
|
|
// This should never happen but who knows...
|
|
throw new Error('Ran out of multitouch slots')
|
|
}
|
|
|
|
for (i = 0, l = e.changedTouches.length; i < l; ++i) {
|
|
var touch = e.changedTouches[i]
|
|
, slot = slots.pop()
|
|
, x = touch.pageX - screen.bounds.x
|
|
, y = touch.pageY - screen.bounds.y
|
|
, pressure = touch.force || 0.5
|
|
, scaled = scaler.coords(
|
|
screen.bounds.w
|
|
, screen.bounds.h
|
|
, x
|
|
, y
|
|
, screen.rotation
|
|
)
|
|
|
|
slotted[touch.identifier] = slot
|
|
control.touchDown(nextSeq(), slot, scaled.xP, scaled.yP, pressure)
|
|
activateFinger(slot, x, y, pressure)
|
|
}
|
|
|
|
element.bind('touchmove', touchMoveListener)
|
|
$document.bind('touchend', touchEndListener)
|
|
$document.bind('touchleave', touchEndListener)
|
|
|
|
control.touchCommit(nextSeq())
|
|
}
|
|
|
|
function touchMoveListener(e) {
|
|
e.preventDefault()
|
|
|
|
if (e.originalEvent) {
|
|
e = e.originalEvent
|
|
}
|
|
|
|
for (var i = 0, l = e.changedTouches.length; i < l; ++i) {
|
|
var touch = e.changedTouches[i]
|
|
, slot = slotted[touch.identifier]
|
|
, x = touch.pageX - screen.bounds.x
|
|
, y = touch.pageY - screen.bounds.y
|
|
, pressure = touch.force || 0.5
|
|
, scaled = scaler.coords(
|
|
screen.bounds.w
|
|
, screen.bounds.h
|
|
, x
|
|
, y
|
|
, screen.rotation
|
|
)
|
|
|
|
control.touchMove(nextSeq(), slot, scaled.xP, scaled.yP, pressure)
|
|
activateFinger(slot, x, y, pressure)
|
|
}
|
|
|
|
control.touchCommit(nextSeq())
|
|
}
|
|
|
|
function touchEndListener(e) {
|
|
if (e.originalEvent) {
|
|
e = e.originalEvent
|
|
}
|
|
|
|
var foundAny = false
|
|
|
|
for (var i = 0, l = e.changedTouches.length; i < l; ++i) {
|
|
var touch = e.changedTouches[i]
|
|
, slot = slotted[touch.identifier]
|
|
if (slot === void 0) {
|
|
// We've already disposed of the contact. We may have gotten a
|
|
// touchend event for the same contact twice.
|
|
continue
|
|
}
|
|
delete slotted[touch.identifier]
|
|
slots.push(slot)
|
|
control.touchUp(nextSeq(), slot)
|
|
deactivateFinger(slot)
|
|
foundAny = true
|
|
}
|
|
|
|
if (foundAny) {
|
|
control.touchCommit(nextSeq())
|
|
if (!e.touches.length) {
|
|
stopTouching()
|
|
}
|
|
}
|
|
}
|
|
|
|
function startTouching() {
|
|
control.gestureStart(nextSeq())
|
|
}
|
|
|
|
function stopTouching() {
|
|
element.unbind('touchmove', touchMoveListener)
|
|
$document.unbind('touchend', touchEndListener)
|
|
$document.unbind('touchleave', touchEndListener)
|
|
deactivateFingers()
|
|
control.gestureStop(nextSeq())
|
|
}
|
|
|
|
element.on('touchstart', touchStartListener)
|
|
element.on('mousedown', mouseDownListener)
|
|
element.on('mouseup', mouseUpBugWorkaroundListener)
|
|
|
|
createSlots()
|
|
})()
|
|
}
|
|
}
|
|
}
|