| /* |
| * 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 |
| } |
| |