blob: 24ef581c90e064880d044926693688d1f4307606 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable */
'use strict'
var isInitialized = false
// major events supported:
// panstart
// panmove
// panend
// swipe
// longpress
// extra events supported:
// dualtouchstart
// dualtouch
// dualtouchend
// tap
// doubletap
// pressend
var doc = window.document
var docEl = doc.documentElement
var slice = Array.prototype.slice
var gestures = {}
var lastTap = null
/**
* find the closest common ancestor
* if there's no one, return null
*
* @param {Element} el1 first element
* @param {Element} el2 second element
* @return {Element} common ancestor
*/
function getCommonAncestor(el1, el2) {
var el = el1
while (el) {
if (el.contains(el2) || el == el2) {
return el
}
el = el.parentNode
}
return null
}
/**
* fire a HTMLEvent
*
* @param {Element} element which element to fire a event on
* @param {string} type type of event
* @param {object} extra extra data for the event object
*/
function fireEvent(element, type, extra) {
var event = doc.createEvent('HTMLEvents')
event.initEvent(type, true, true)
if (typeof extra === 'object') {
for (var p in extra) {
event[p] = extra[p]
}
}
element.dispatchEvent(event)
}
/**
* calc the transform
* assume 4 points ABCD on the coordinate system
* > rotate:angle rotating from AB to CD
* > scale:scale ratio from AB to CD
* > translate:translate shift from A to C
*
* @param {number} x1 abscissa of A
* @param {number} y1 ordinate of A
* @param {number} x2 abscissa of B
* @param {number} y2 ordinate of B
* @param {number} x3 abscissa of C
* @param {number} y3 ordinate of C
* @param {number} x4 abscissa of D
* @param {number} y4 ordinate of D
* @return {object} transform object like
* {rotate, scale, translate[2], matrix[3][3]}
*/
function calc(x1, y1, x2, y2, x3, y3, x4, y4) {
var rotate = Math.atan2(y4 - y3, x4 - x3) - Math.atan2(y2 - y1, x2 - x1)
var scale = Math.sqrt((Math.pow(y4 - y3, 2)
+ Math.pow(x4 - x3, 2)) / (Math.pow(y2 - y1, 2)
+ Math.pow(x2 - x1, 2)))
var translate = [
x3
- scale * x1 * Math.cos(rotate)
+ scale * y1 * Math.sin(rotate),
y3
- scale * y1 * Math.cos(rotate)
- scale * x1 * Math.sin(rotate)]
return {
rotate: rotate,
scale: scale,
translate: translate,
matrix: [
[scale * Math.cos(rotate), -scale * Math.sin(rotate), translate[0]],
[scale * Math.sin(rotate), scale * Math.cos(rotate), translate[1]],
[0, 0, 1]
]
}
}
/**
* take over the touchstart events. Add new touches to the gestures.
* If there is no previous records, then bind touchmove, tochend
* and touchcancel events.
* new touches initialized with state 'tapping', and within 500 milliseconds
* if the state is still tapping, then trigger gesture 'press'.
* If there are two touche points, then the 'dualtouchstart' is triggerd. The
* node of the touch gesture is their cloest common ancestor.
*
* @event
* @param {event} event
*/
function touchstartHandler(event) {
if (Object.keys(gestures).length === 0) {
docEl.addEventListener('touchmove', touchmoveHandler, false)
docEl.addEventListener('touchend', touchendHandler, false)
docEl.addEventListener('touchcancel', touchcancelHandler, false)
}
// record every touch
for (var i = 0; i < event.changedTouches.length; i++) {
var touch = event.changedTouches[i]
var touchRecord = {}
for (var p in touch) {
touchRecord[p] = touch[p]
}
var gesture = {
startTouch: touchRecord,
startTime: Date.now(),
status: 'tapping',
element: event.srcElement || event.target,
pressingHandler: setTimeout(function (element, touch) {
return function () {
if (gesture.status === 'tapping') {
gesture.status = 'pressing'
fireEvent(element, 'longpress', {
// add touch data for weex
touch: touch,
touches: event.touches,
changedTouches: event.changedTouches,
touchEvent: event
})
}
clearTimeout(gesture.pressingHandler)
gesture.pressingHandler = null
}
}(event.srcElement || event.target, event.changedTouches[i]), 500)
}
gestures[touch.identifier] = gesture
}
if (Object.keys(gestures).length == 2) {
var elements = []
for (var p in gestures) {
elements.push(gestures[p].element)
}
fireEvent(getCommonAncestor(elements[0], elements[1]), 'dualtouchstart', {
touches: slice.call(event.touches),
touchEvent: event
})
}
}
/**
* take over touchmove events, and handle pan and dual related gestures.
*
* 1. traverse every touch point:
* > if 'tapping' and the shift is over 10 pixles, then it's a 'panning'.
* 2. if there are two touch points, then calc the tranform and trigger
* 'dualtouch'.
*
* @event
* @param {event} event
*/
function touchmoveHandler(event) {
for (var i = 0; i < event.changedTouches.length; i++) {
var touch = event.changedTouches[i]
var gesture = gestures[touch.identifier]
if (!gesture) {
return
}
if (!gesture.lastTouch) {
gesture.lastTouch = gesture.startTouch
}
if (!gesture.lastTime) {
gesture.lastTime = gesture.startTime
}
if (!gesture.velocityX) {
gesture.velocityX = 0
}
if (!gesture.velocityY) {
gesture.velocityY = 0
}
if (!gesture.duration) {
gesture.duration = 0
}
var time = Date.now() - gesture.lastTime
var vx = (touch.clientX - gesture.lastTouch.clientX) / time
var vy = (touch.clientY - gesture.lastTouch.clientY) / time
var RECORD_DURATION = 70
if (time > RECORD_DURATION) {
time = RECORD_DURATION
}
if (gesture.duration + time > RECORD_DURATION) {
gesture.duration = RECORD_DURATION - time
}
gesture.velocityX = (gesture.velocityX * gesture.duration + vx * time)
/ (gesture.duration + time)
gesture.velocityY = (gesture.velocityY * gesture.duration + vy * time)
/ (gesture.duration + time)
gesture.duration += time
gesture.lastTouch = {}
for (var p in touch) {
gesture.lastTouch[p] = touch[p]
}
gesture.lastTime = Date.now()
var displacementX = touch.clientX - gesture.startTouch.clientX
var displacementY = touch.clientY - gesture.startTouch.clientY
var distance = Math.sqrt(Math.pow(displacementX, 2)
+ Math.pow(displacementY, 2))
var isVertical = !(Math.abs(displacementX) > Math.abs(displacementY))
var direction = isVertical
? displacementY >= 0 ? 'down' : 'up'
: displacementX >= 0 ? 'right' : 'left'
// magic number 10: moving 10px means pan, not tap
if ((gesture.status === 'tapping' || gesture.status === 'pressing')
&& distance > 10) {
gesture.status = 'panning'
gesture.isVertical = isVertical
gesture.direction = direction
fireEvent(gesture.element, 'panstart', {
touch: touch,
touches: event.touches,
changedTouches: event.changedTouches,
touchEvent: event,
isVertical: gesture.isVertical,
direction: direction
})
}
if (gesture.status === 'panning') {
gesture.panTime = Date.now()
fireEvent(gesture.element, 'panmove', {
displacementX: displacementX,
displacementY: displacementY,
touch: touch,
touches: event.touches,
changedTouches: event.changedTouches,
touchEvent: event,
isVertical: gesture.isVertical,
direction: direction
})
}
}
if (Object.keys(gestures).length == 2) {
var position = []
var current = []
var elements = []
var transform
for (var i = 0; i < event.touches.length; i++) {
var touch = event.touches[i]
var gesture = gestures[touch.identifier]
position.push([gesture.startTouch.clientX, gesture.startTouch.clientY])
current.push([touch.clientX, touch.clientY])
}
for (var p in gestures) {
elements.push(gestures[p].element)
}
transform = calc(
position[0][0],
position[0][1],
position[1][0],
position[1][1],
current[0][0],
current[0][1],
current[1][0],
current[1][1]
)
fireEvent(getCommonAncestor(elements[0], elements[1]), 'dualtouch', {
transform: transform,
touches: event.touches,
touchEvent: event
})
}
}
/**
* handle touchend event
*
* 1. if there are tow touch points, then trigger 'dualtouchend'如
*
* 2. traverse every touch piont:
* > if tapping, then trigger 'tap'.
* If there is a tap 300 milliseconds before, then it's a 'doubletap'.
* > if padding, then decide to trigger 'panend' or 'swipe'
* > if pressing, then trigger 'pressend'.
*
* 3. remove listeners.
*
* @event
* @param {event} event
*/
function touchendHandler(event) {
if (Object.keys(gestures).length == 2) {
var elements = []
for (var p in gestures) {
elements.push(gestures[p].element)
}
fireEvent(getCommonAncestor(elements[0], elements[1]), 'dualtouchend', {
touches: slice.call(event.touches),
touchEvent: event
})
}
for (var i = 0; i < event.changedTouches.length; i++) {
var touch = event.changedTouches[i]
var id = touch.identifier
var gesture = gestures[id]
if (!gesture) {
continue
}
if (gesture.pressingHandler) {
clearTimeout(gesture.pressingHandler)
gesture.pressingHandler = null
}
if (gesture.status === 'tapping') {
gesture.timestamp = Date.now()
fireEvent(gesture.element, 'tap', {
touch: touch,
touchEvent: event
})
if (lastTap && gesture.timestamp - lastTap.timestamp < 300) {
fireEvent(gesture.element, 'doubletap', {
touch: touch,
touchEvent: event
})
}
lastTap = gesture
}
if (gesture.status === 'panning') {
var now = Date.now()
var duration = now - gesture.startTime
var displacementX = touch.clientX - gesture.startTouch.clientX
var displacementY = touch.clientY - gesture.startTouch.clientY
var velocity = Math.sqrt(gesture.velocityY * gesture.velocityY
+ gesture.velocityX * gesture.velocityX)
var isSwipe = velocity > 0.5 && (now - gesture.lastTime) < 100
var extra = {
duration: duration,
isSwipe: isSwipe,
velocityX: gesture.velocityX,
velocityY: gesture.velocityY,
displacementX: displacementX,
displacementY: displacementY,
touch: touch,
touches: event.touches,
changedTouches: event.changedTouches,
touchEvent: event,
isVertical: gesture.isVertical,
direction: gesture.direction
}
fireEvent(gesture.element, 'panend', extra)
if (isSwipe) {
fireEvent(gesture.element, 'swipe', extra)
}
}
if (gesture.status === 'pressing') {
fireEvent(gesture.element, 'pressend', {
touch: touch,
touchEvent: event
})
}
delete gestures[id]
}
if (Object.keys(gestures).length === 0) {
docEl.removeEventListener('touchmove', touchmoveHandler, false)
docEl.removeEventListener('touchend', touchendHandler, false)
docEl.removeEventListener('touchcancel', touchcancelHandler, false)
}
}
/**
* handle touchcancel
*
* 1. if there are two touch points, then trigger 'dualtouchend'
*
* 2. traverse everty touch point:
* > if pannnig, then trigger 'panend'
* > if pressing, then trigger 'pressend'
*
* 3. remove listeners
*
* @event
* @param {event} event
*/
function touchcancelHandler(event) {
if (Object.keys(gestures).length == 2) {
var elements = []
for (var p in gestures) {
elements.push(gestures[p].element)
}
fireEvent(getCommonAncestor(elements[0], elements[1]), 'dualtouchend', {
touches: slice.call(event.touches),
touchEvent: event
})
}
for (var i = 0; i < event.changedTouches.length; i++) {
var touch = event.changedTouches[i]
var id = touch.identifier
var gesture = gestures[id]
if (!gesture) {
continue
}
if (gesture.pressingHandler) {
clearTimeout(gesture.pressingHandler)
gesture.pressingHandler = null
}
if (gesture.status === 'panning') {
fireEvent(gesture.element, 'panend', {
touch: touch,
touches: event.touches,
changedTouches: event.changedTouches,
touchEvent: event
})
}
if (gesture.status === 'pressing') {
fireEvent(gesture.element, 'pressend', {
touch: touch,
touchEvent: event
})
}
delete gestures[id]
}
if (Object.keys(gestures).length === 0) {
docEl.removeEventListener('touchmove', touchmoveHandler, false)
docEl.removeEventListener('touchend', touchendHandler, false)
docEl.removeEventListener('touchcancel', touchcancelHandler, false)
}
}
if (!isInitialized) {
docEl.addEventListener('touchstart', touchstartHandler, false)
isInitialized = true
}