framework7
Version:
Full featured mobile HTML framework for building iOS & Android apps
482 lines (478 loc) • 15.9 kB
JavaScript
/* eslint-disable no-nested-ternary */
import { getWindow, getDocument } from 'ssr-window';
import $ from '../../shared/dom7.js';
import { getSupport } from '../../shared/get-support.js';
import { getDevice } from '../../shared/get-device.js';
import { extend } from '../../shared/utils.js';
function initTouch() {
const app = this;
const device = getDevice();
const support = getSupport();
const window = getWindow();
const document = getDocument();
const params = app.params.touch;
const useRipple = params[`${app.theme}TouchRipple`];
if (device.ios && device.webView) {
// Strange hack required for iOS 8 webview to work on inputs
window.addEventListener('touchstart', () => {});
}
let touchStartX;
let touchStartY;
let targetElement;
let isMoved;
let tapHoldFired;
let tapHoldTimeout;
let preventClick;
let activableElement;
let activeTimeout;
let rippleWave;
let rippleTarget;
let rippleTimeout;
function findActivableElement(el) {
const target = $(el);
const parents = target.parents(params.activeStateElements);
if (target.closest('.no-active-state').length) {
return null;
}
let activable;
if (target.is(params.activeStateElements)) {
activable = target;
}
if (parents.length > 0) {
activable = activable ? activable.add(parents) : parents;
}
if (activable && activable.length > 1) {
const newActivable = [];
let preventPropagation;
for (let i = 0; i < activable.length; i += 1) {
if (!preventPropagation) {
newActivable.push(activable[i]);
if (activable.eq(i).hasClass('prevent-active-state-propagation') || activable.eq(i).hasClass('no-active-state-propagation')) {
preventPropagation = true;
}
}
}
activable = $(newActivable);
}
return activable || target;
}
function isInsideScrollableView(el) {
const pageContent = el.parents('.page-content');
return pageContent.length > 0;
}
function addActive() {
if (!activableElement) return;
activableElement.addClass('active-state');
}
function removeActive() {
if (!activableElement) return;
activableElement.removeClass('active-state');
activableElement = null;
}
// Ripple handlers
function findRippleElement(el) {
const rippleElements = params.touchRippleElements;
const $el = $(el);
if ($el.is(rippleElements)) {
if ($el.hasClass('no-ripple')) {
return false;
}
return $el;
}
if ($el.parents(rippleElements).length > 0) {
const rippleParent = $el.parents(rippleElements).eq(0);
if (rippleParent.hasClass('no-ripple')) {
return false;
}
return rippleParent;
}
return false;
}
function createRipple($el, x, y) {
if (!$el) return;
rippleWave = app.touchRipple.create(app, $el, x, y);
}
function removeRipple() {
if (!rippleWave) return;
rippleWave.remove();
rippleWave = undefined;
rippleTarget = undefined;
}
function rippleTouchStart(el) {
rippleTarget = findRippleElement(el);
if (!rippleTarget || rippleTarget.length === 0) {
rippleTarget = undefined;
return;
}
const inScrollable = isInsideScrollableView(rippleTarget);
if (!inScrollable) {
removeRipple();
createRipple(rippleTarget, touchStartX, touchStartY);
} else {
clearTimeout(rippleTimeout);
rippleTimeout = setTimeout(() => {
removeRipple();
createRipple(rippleTarget, touchStartX, touchStartY);
}, 80);
}
}
function rippleTouchMove() {
clearTimeout(rippleTimeout);
removeRipple();
}
function rippleTouchEnd() {
if (!rippleWave && rippleTarget && !isMoved) {
clearTimeout(rippleTimeout);
createRipple(rippleTarget, touchStartX, touchStartY);
setTimeout(removeRipple, 0);
} else {
removeRipple();
}
}
// Mouse Handlers
function handleMouseDown(e) {
const $activableEl = findActivableElement(e.target);
if ($activableEl) {
$activableEl.addClass('active-state');
if ('which' in e && e.which === 3) {
setTimeout(() => {
$('.active-state').removeClass('active-state');
}, 0);
}
}
if (useRipple) {
touchStartX = e.pageX;
touchStartY = e.pageY;
rippleTouchStart(e.target, e.pageX, e.pageY);
}
}
function handleMouseMove() {
if (!params.activeStateOnMouseMove) {
$('.active-state').removeClass('active-state');
}
if (useRipple) {
rippleTouchMove();
}
}
function handleMouseUp() {
$('.active-state').removeClass('active-state');
if (useRipple) {
rippleTouchEnd();
}
}
function handleTouchCancel() {
targetElement = null;
// Remove Active State
clearTimeout(activeTimeout);
clearTimeout(tapHoldTimeout);
if (params.activeState) {
removeActive();
}
// Remove Ripple
if (useRipple) {
rippleTouchEnd();
}
}
let isScrolling;
let isSegmentedStrong = false;
let segmentedStrongEl = null;
const touchMoveActivableIos = '.dialog-button, .actions-button';
let isTouchMoveActivable = false;
let touchmoveActivableEl = null;
function handleTouchStart(e) {
if (!e.isTrusted) return true;
isMoved = false;
tapHoldFired = false;
preventClick = false;
isScrolling = undefined;
if (e.targetTouches.length > 1) {
if (activableElement) removeActive();
return true;
}
if (e.touches.length > 1 && activableElement) {
removeActive();
}
if (params.tapHold) {
if (tapHoldTimeout) clearTimeout(tapHoldTimeout);
tapHoldTimeout = setTimeout(() => {
if (e && e.touches && e.touches.length > 1) return;
tapHoldFired = true;
e.preventDefault();
preventClick = true;
$(e.target).trigger('taphold', e);
app.emit('taphold', e);
}, params.tapHoldDelay);
}
targetElement = e.target;
touchStartX = e.targetTouches[0].pageX;
touchStartY = e.targetTouches[0].pageY;
isSegmentedStrong = e.target.closest('.segmented-strong .button-active, .segmented-strong .tab-link-active');
isTouchMoveActivable = app.theme === 'ios' && e.target.closest(touchMoveActivableIos);
if (isSegmentedStrong) {
segmentedStrongEl = isSegmentedStrong.closest('.segmented-strong');
}
if (params.activeState) {
activableElement = findActivableElement(targetElement);
if (activableElement && !isInsideScrollableView(activableElement)) {
addActive();
} else if (activableElement) {
activeTimeout = setTimeout(addActive, 80);
}
}
if (useRipple) {
rippleTouchStart(targetElement, touchStartX, touchStartY);
}
return true;
}
function handleTouchMove(e) {
if (!e.isTrusted) return;
let touch;
let distance;
let shouldRemoveActive = true;
if (e.type === 'touchmove') {
touch = e.targetTouches[0];
distance = params.touchClicksDistanceThreshold;
}
const touchCurrentX = e.targetTouches[0].pageX;
const touchCurrentY = e.targetTouches[0].pageY;
if (typeof isScrolling === 'undefined') {
isScrolling = !!(isScrolling || Math.abs(touchCurrentY - touchStartY) > Math.abs(touchCurrentX - touchStartX));
}
if (isTouchMoveActivable || !isScrolling && isSegmentedStrong && segmentedStrongEl) {
if (e.cancelable) e.preventDefault();
}
if (!isScrolling && isSegmentedStrong && segmentedStrongEl) {
const elementFromPoint = document.elementFromPoint(e.targetTouches[0].clientX, e.targetTouches[0].clientY);
const buttonEl = elementFromPoint.closest('.segmented-strong .button:not(.button-active):not(.tab-link-active)');
if (buttonEl && segmentedStrongEl.contains(buttonEl)) {
$(buttonEl).trigger('click', 'f7Segmented');
targetElement = buttonEl;
}
}
if (distance && touch) {
const pageX = touch.pageX;
const pageY = touch.pageY;
if (Math.abs(pageX - touchStartX) > distance || Math.abs(pageY - touchStartY) > distance) {
isMoved = true;
}
} else {
isMoved = true;
}
if (isMoved) {
preventClick = true;
// Keep active state on touchMove (for dialog and actions buttons)
if (isTouchMoveActivable) {
const elementFromPoint = document.elementFromPoint(e.targetTouches[0].clientX, e.targetTouches[0].clientY);
touchmoveActivableEl = elementFromPoint.closest(touchMoveActivableIos);
if (touchmoveActivableEl && activableElement && activableElement[0] === touchmoveActivableEl) {
shouldRemoveActive = false;
} else if (touchmoveActivableEl) {
setTimeout(() => {
activableElement = findActivableElement(touchmoveActivableEl);
addActive();
});
}
}
if (params.tapHold) {
clearTimeout(tapHoldTimeout);
}
if (params.activeState && shouldRemoveActive) {
clearTimeout(activeTimeout);
removeActive();
}
if (useRipple) {
rippleTouchMove();
}
}
}
function handleTouchEnd(e) {
if (!e.isTrusted) return true;
isScrolling = undefined;
isSegmentedStrong = false;
segmentedStrongEl = null;
isTouchMoveActivable = false;
clearTimeout(activeTimeout);
clearTimeout(tapHoldTimeout);
if (touchmoveActivableEl) {
$(touchmoveActivableEl).trigger('click', 'f7TouchMoveActivable');
touchmoveActivableEl = null;
}
if (document.activeElement === e.target) {
if (params.activeState) removeActive();
if (useRipple) {
rippleTouchEnd();
}
return true;
}
if (params.activeState) {
addActive();
setTimeout(removeActive, 0);
}
if (useRipple) {
rippleTouchEnd();
}
if (params.tapHoldPreventClicks && tapHoldFired || preventClick) {
if (e.cancelable) e.preventDefault();
preventClick = true;
return false;
}
return true;
}
function handleClick(e) {
const isOverswipe = e && e.detail && e.detail === 'f7Overswipe';
const isSegmented = e && e.detail && e.detail === 'f7Segmented';
// eslint-disable-next-line
const isTouchMoveActivable = e && e.detail && e.detail === 'f7TouchMoveActivable';
let localPreventClick = preventClick;
if (targetElement && e.target !== targetElement) {
if (isOverswipe || isSegmented || isTouchMoveActivable) {
localPreventClick = false;
} else {
localPreventClick = true;
}
} else if (isTouchMoveActivable) {
localPreventClick = false;
}
if (params.tapHold && params.tapHoldPreventClicks && tapHoldFired) {
localPreventClick = true;
}
if (localPreventClick) {
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
}
if (params.tapHold) {
tapHoldTimeout = setTimeout(() => {
tapHoldFired = false;
}, device.ios || device.androidChrome ? 100 : 400);
}
preventClick = false;
targetElement = null;
return !localPreventClick;
}
function emitAppTouchEvent(name, e) {
app.emit({
events: name,
data: [e]
});
}
function appClick(e) {
emitAppTouchEvent('click', e);
}
function appTouchStartActive(e) {
emitAppTouchEvent('touchstart touchstart:active', e);
}
function appTouchMoveActive(e) {
emitAppTouchEvent('touchmove touchmove:active', e);
}
function appTouchEndActive(e) {
emitAppTouchEvent('touchend touchend:active', e);
}
function appTouchStartPassive(e) {
emitAppTouchEvent('touchstart:passive', e);
}
function appTouchMovePassive(e) {
emitAppTouchEvent('touchmove:passive', e);
}
function appTouchEndPassive(e) {
emitAppTouchEvent('touchend:passive', e);
}
const passiveListener = support.passiveListener ? {
passive: true
} : false;
const passiveListenerCapture = support.passiveListener ? {
passive: true,
capture: true
} : true;
const activeListener = support.passiveListener ? {
passive: false
} : false;
const activeListenerCapture = support.passiveListener ? {
passive: false,
capture: true
} : true;
document.addEventListener('click', appClick, true);
if (support.passiveListener) {
document.addEventListener(app.touchEvents.start, appTouchStartActive, activeListenerCapture);
document.addEventListener(app.touchEvents.move, appTouchMoveActive, activeListener);
document.addEventListener(app.touchEvents.end, appTouchEndActive, activeListener);
document.addEventListener(app.touchEvents.start, appTouchStartPassive, passiveListenerCapture);
document.addEventListener(app.touchEvents.move, appTouchMovePassive, passiveListener);
document.addEventListener(app.touchEvents.end, appTouchEndPassive, passiveListener);
} else {
document.addEventListener(app.touchEvents.start, e => {
appTouchStartActive(e);
appTouchStartPassive(e);
}, true);
document.addEventListener(app.touchEvents.move, e => {
appTouchMoveActive(e);
appTouchMovePassive(e);
}, false);
document.addEventListener(app.touchEvents.end, e => {
appTouchEndActive(e);
appTouchEndPassive(e);
}, false);
}
if (support.touch) {
app.on('click', handleClick);
app.on('touchstart', handleTouchStart);
app.on('touchmove', handleTouchMove);
app.on('touchend', handleTouchEnd);
document.addEventListener('touchcancel', handleTouchCancel, {
passive: true
});
} else if (params.activeState) {
app.on('touchstart', handleMouseDown);
app.on('touchmove', handleMouseMove);
app.on('touchend', handleMouseUp);
document.addEventListener('pointercancel', handleMouseUp, {
passive: true
});
}
document.addEventListener('contextmenu', e => {
if (params.disableContextMenu && (device.ios || device.android || device.cordova || window.Capacitor && window.Capacitor.isNative)) {
e.preventDefault();
}
if (useRipple) {
if (activableElement) removeActive();
rippleTouchEnd();
}
});
}
export default {
name: 'touch',
params: {
touch: {
// Clicks
touchClicksDistanceThreshold: 5,
// ContextMenu
disableContextMenu: false,
// Tap Hold
tapHold: false,
tapHoldDelay: 750,
tapHoldPreventClicks: true,
// Active State
activeState: true,
activeStateElements: 'a, button, label, span, .actions-button, .stepper-button, .stepper-button-plus, .stepper-button-minus, .card-expandable, .link, .item-link, .accordion-item-toggle',
activeStateOnMouseMove: false,
mdTouchRipple: true,
iosTouchRipple: false,
touchRippleElements: '.ripple, .link, .item-link, .list label.item-content, .list-button, .links-list a, .button, button, .input-clear-button, .dialog-button, .tab-link, .item-radio, .item-checkbox, .actions-button, .searchbar-disable-button, .fab a, .checkbox, .radio, .data-table .sortable-cell:not(.input-cell), .notification-close-button, .stepper-button, .stepper-button-minus, .stepper-button-plus, .list.accordion-list .accordion-item-toggle',
touchRippleInsetElements: '.ripple-inset, .icon-only, .searchbar-disable-button, .input-clear-button, .notification-close-button, .md .navbar .link.back'
}
},
create() {
const app = this;
const support = getSupport();
extend(app, {
touchEvents: {
start: support.touch ? 'touchstart' : support.pointerEvents ? 'pointerdown' : 'mousedown',
move: support.touch ? 'touchmove' : support.pointerEvents ? 'pointermove' : 'mousemove',
end: support.touch ? 'touchend' : support.pointerEvents ? 'pointerup' : 'mouseup'
}
});
},
on: {
init: initTouch
}
};