/* global jQuery */ (function($) { 'use strict'; var options = { propagateSupportedGesture: false }; function init(plot) { plot.hooks.processOptions.push(initTouchNavigation); } function initTouchNavigation(plot, options) { var gestureState = { twoTouches: false, currentTapStart: { x: 0, y: 0 }, currentTapEnd: { x: 0, y: 0 }, prevTap: { x: 0, y: 0 }, currentTap: { x: 0, y: 0 }, interceptedLongTap: false, isUnsupportedGesture: false, prevTapTime: null, tapStartTime: null, longTapTriggerId: null }, maxDistanceBetweenTaps = 20, maxIntervalBetweenTaps = 500, maxLongTapDistance = 20, minLongTapDuration = 1500, pressedTapDuration = 125, mainEventHolder; function interpretGestures(e) { var o = plot.getOptions(); if (!o.pan.active && !o.zoom.active) { return; } updateOnMultipleTouches(e); mainEventHolder.dispatchEvent(new CustomEvent('touchevent', { detail: e })); if (isPinchEvent(e)) { executeAction(e, 'pinch'); } else { executeAction(e, 'pan'); if (!wasPinchEvent(e)) { if (isDoubleTap(e)) { executeAction(e, 'doubleTap'); } executeAction(e, 'tap'); executeAction(e, 'longTap'); } } } function executeAction(e, gesture) { switch (gesture) { case 'pan': pan[e.type](e); break; case 'pinch': pinch[e.type](e); break; case 'doubleTap': doubleTap.onDoubleTap(e); break; case 'longTap': longTap[e.type](e); break; case 'tap': tap[e.type](e); break; } } function bindEvents(plot, eventHolder) { mainEventHolder = eventHolder[0]; eventHolder[0].addEventListener('touchstart', interpretGestures, false); eventHolder[0].addEventListener('touchmove', interpretGestures, false); eventHolder[0].addEventListener('touchend', interpretGestures, false); } function shutdown(plot, eventHolder) { eventHolder[0].removeEventListener('touchstart', interpretGestures); eventHolder[0].removeEventListener('touchmove', interpretGestures); eventHolder[0].removeEventListener('touchend', interpretGestures); if (gestureState.longTapTriggerId) { clearTimeout(gestureState.longTapTriggerId); gestureState.longTapTriggerId = null; } } var pan = { touchstart: function(e) { updatePrevForDoubleTap(); updateCurrentForDoubleTap(e); updateStateForLongTapStart(e); mainEventHolder.dispatchEvent(new CustomEvent('panstart', { detail: e })); }, touchmove: function(e) { preventEventBehaviors(e); updateCurrentForDoubleTap(e); updateStateForLongTapEnd(e); if (!gestureState.isUnsupportedGesture) { mainEventHolder.dispatchEvent(new CustomEvent('pandrag', { detail: e })); } }, touchend: function(e) { preventEventBehaviors(e); if (wasPinchEvent(e)) { mainEventHolder.dispatchEvent(new CustomEvent('pinchend', { detail: e })); mainEventHolder.dispatchEvent(new CustomEvent('panstart', { detail: e })); } else if (noTouchActive(e)) { mainEventHolder.dispatchEvent(new CustomEvent('panend', { detail: e })); } } }; var pinch = { touchstart: function(e) { mainEventHolder.dispatchEvent(new CustomEvent('pinchstart', { detail: e })); }, touchmove: function(e) { preventEventBehaviors(e); gestureState.twoTouches = isPinchEvent(e); if (!gestureState.isUnsupportedGesture) { mainEventHolder.dispatchEvent(new CustomEvent('pinchdrag', { detail: e })); } }, touchend: function(e) { preventEventBehaviors(e); } }; var doubleTap = { onDoubleTap: function(e) { preventEventBehaviors(e); mainEventHolder.dispatchEvent(new CustomEvent('doubletap', { detail: e })); } }; var longTap = { touchstart: function(e) { longTap.waitForLongTap(e); }, touchmove: function(e) { }, touchend: function(e) { if (gestureState.longTapTriggerId) { clearTimeout(gestureState.longTapTriggerId); gestureState.longTapTriggerId = null; } }, isLongTap: function(e) { var currentTime = new Date().getTime(), tapDuration = currentTime - gestureState.tapStartTime; if (tapDuration >= minLongTapDuration && !gestureState.interceptedLongTap) { if (distance(gestureState.currentTapStart.x, gestureState.currentTapStart.y, gestureState.currentTapEnd.x, gestureState.currentTapEnd.y) < maxLongTapDistance) { gestureState.interceptedLongTap = true; return true; } } return false; }, waitForLongTap: function(e) { var longTapTrigger = function() { if (longTap.isLongTap(e)) { mainEventHolder.dispatchEvent(new CustomEvent('longtap', { detail: e })); } gestureState.longTapTriggerId = null; }; if (!gestureState.longTapTriggerId) { gestureState.longTapTriggerId = setTimeout(longTapTrigger, minLongTapDuration); } } }; var tap = { touchstart: function(e) { gestureState.tapStartTime = new Date().getTime(); }, touchmove: function(e) { }, touchend: function(e) { if (tap.isTap(e)) { mainEventHolder.dispatchEvent(new CustomEvent('tap', { detail: e })); preventEventBehaviors(e); } }, isTap: function(e) { var currentTime = new Date().getTime(), tapDuration = currentTime - gestureState.tapStartTime; if (tapDuration <= pressedTapDuration) { if (distance(gestureState.currentTapStart.x, gestureState.currentTapStart.y, gestureState.currentTapEnd.x, gestureState.currentTapEnd.y) < maxLongTapDistance) { return true; } } return false; } }; if (options.pan.enableTouch === true || options.zoom.enableTouch) { plot.hooks.bindEvents.push(bindEvents); plot.hooks.shutdown.push(shutdown); }; function updatePrevForDoubleTap() { gestureState.prevTap = { x: gestureState.currentTap.x, y: gestureState.currentTap.y }; }; function updateCurrentForDoubleTap(e) { gestureState.currentTap = { x: e.touches[0].pageX, y: e.touches[0].pageY }; } function updateStateForLongTapStart(e) { gestureState.tapStartTime = new Date().getTime(); gestureState.interceptedLongTap = false; gestureState.currentTapStart = { x: e.touches[0].pageX, y: e.touches[0].pageY }; gestureState.currentTapEnd = { x: e.touches[0].pageX, y: e.touches[0].pageY }; }; function updateStateForLongTapEnd(e) { gestureState.currentTapEnd = { x: e.touches[0].pageX, y: e.touches[0].pageY }; }; function isDoubleTap(e) { var currentTime = new Date().getTime(), intervalBetweenTaps = currentTime - gestureState.prevTapTime; if (intervalBetweenTaps >= 0 && intervalBetweenTaps < maxIntervalBetweenTaps) { if (distance(gestureState.prevTap.x, gestureState.prevTap.y, gestureState.currentTap.x, gestureState.currentTap.y) < maxDistanceBetweenTaps) { e.firstTouch = gestureState.prevTap; e.secondTouch = gestureState.currentTap; return true; } } gestureState.prevTapTime = currentTime; return false; } function preventEventBehaviors(e) { if (!gestureState.isUnsupportedGesture) { e.preventDefault(); if (!plot.getOptions().propagateSupportedGesture) { e.stopPropagation(); } } } function distance(x1, y1, x2, y2) { return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } function noTouchActive(e) { return (e.touches && e.touches.length === 0); } function wasPinchEvent(e) { return (gestureState.twoTouches && e.touches.length === 1); } function updateOnMultipleTouches(e) { if (e.touches.length >= 3) { gestureState.isUnsupportedGesture = true; } else { gestureState.isUnsupportedGesture = false; } } function isPinchEvent(e) { if (e.touches && e.touches.length >= 2) { if (e.touches[0].target === plot.getEventHolder() && e.touches[1].target === plot.getEventHolder()) { return true; } } return false; } } $.plot.plugins.push({ init: init, options: options, name: 'navigateTouch', version: '0.3' }); })(jQuery);