web-apps/vendor/framework7/src/js/fast-clicks.js
2016-11-11 16:24:21 +03:00

538 lines
18 KiB
JavaScript

/*===============================================================================
************ Fast Clicks ************
************ Inspired by https://github.com/ftlabs/fastclick ************
===============================================================================*/
app.initFastClicks = function () {
if (app.params.activeState) {
$('html').addClass('watch-active-state');
}
if (app.device.ios && app.device.webView) {
// Strange hack required for iOS 8 webview to work on inputs
window.addEventListener('touchstart', function () {});
}
var touchStartX, touchStartY, touchStartTime, targetElement, trackClick, activeSelection, scrollParent, lastClickTime, isMoved, tapHoldFired, tapHoldTimeout;
var activableElement, activeTimeout, needsFastClick, needsFastClickTimeOut;
var rippleWave, rippleTarget, rippleTransform, rippleTimeout;
function findActivableElement(el) {
var target = $(el);
var parents = target.parents(app.params.activeStateElements);
var activable;
if (target.is(app.params.activeStateElements)) {
activable = target;
}
if (parents.length > 0) {
activable = activable ? activable.add(parents) : parents;
}
return activable ? activable : target;
}
function isInsideScrollableView(el) {
var pageContent = el.parents('.page-content, .panel');
if (pageContent.length === 0) {
return false;
}
// This event handler covers the "tap to stop scrolling".
if (pageContent.prop('scrollHandlerSet') !== 'yes') {
pageContent.on('scroll', function() {
clearTimeout(activeTimeout);
clearTimeout(rippleTimeout);
});
pageContent.prop('scrollHandlerSet', 'yes');
}
return true;
}
function addActive() {
if (!activableElement) return;
activableElement.addClass('active-state');
}
function removeActive(el) {
if (!activableElement) return;
activableElement.removeClass('active-state');
activableElement = null;
}
function isFormElement(el) {
var nodes = ('input select textarea label').split(' ');
if (el.nodeName && nodes.indexOf(el.nodeName.toLowerCase()) >= 0) return true;
return false;
}
function androidNeedsBlur(el) {
var noBlur = ('button input textarea select').split(' ');
if (document.activeElement && el !== document.activeElement && document.activeElement !== document.body) {
if (noBlur.indexOf(el.nodeName.toLowerCase()) >= 0) {
return false;
}
else {
return true;
}
}
else {
return false;
}
}
function targetNeedsFastClick(el) {
var $el = $(el);
if (el.nodeName.toLowerCase() === 'input' && el.type === 'file') return false;
if (el.nodeName.toLowerCase() === 'select' && app.device.android) return false;
if ($el.hasClass('no-fastclick') || $el.parents('.no-fastclick').length > 0) return false;
if (app.params.fastClicksExclude && $el.is(app.params.fastClicksExclude)) return false;
return true;
}
function targetNeedsFocus(el) {
if (document.activeElement === el) {
return false;
}
var tag = el.nodeName.toLowerCase();
var skipInputs = ('button checkbox file image radio submit').split(' ');
if (el.disabled || el.readOnly) return false;
if (tag === 'textarea') return true;
if (tag === 'select') {
if (app.device.android) return false;
else return true;
}
if (tag === 'input' && skipInputs.indexOf(el.type) < 0) return true;
}
function targetNeedsPrevent(el) {
el = $(el);
var prevent = true;
if (el.is('label') || el.parents('label').length > 0) {
if (app.device.android) {
prevent = false;
}
else if (app.device.ios && el.is('input')) {
prevent = true;
}
else prevent = false;
}
return prevent;
}
// Mouse Handlers
function handleMouseDown (e) {
findActivableElement(e.target).addClass('active-state');
if ('which' in e && e.which === 3) {
setTimeout(function () {
$('.active-state').removeClass('active-state');
}, 0);
}
if (app.params.material && app.params.materialRipple) {
touchStartX = e.pageX;
touchStartY = e.pageY;
rippleTouchStart(e.target, e.pageX, e.pageY);
}
}
function handleMouseMove (e) {
$('.active-state').removeClass('active-state');
if (app.params.material && app.params.materialRipple) {
rippleTouchMove();
}
}
function handleMouseUp (e) {
$('.active-state').removeClass('active-state');
if (app.params.material && app.params.materialRipple) {
rippleTouchEnd();
}
}
// Material Touch Ripple Effect
function findRippleElement(el) {
var needsRipple = app.params.materialRippleElements;
var $el = $(el);
if ($el.is(needsRipple)) {
if ($el.hasClass('no-ripple')) {
return false;
}
return $el;
}
else if ($el.parents(needsRipple).length > 0) {
var rippleParent = $el.parents(needsRipple).eq(0);
if (rippleParent.hasClass('no-ripple')) {
return false;
}
return rippleParent;
}
else return false;
}
function createRipple(x, y, el) {
var box = el[0].getBoundingClientRect();
var center = {
x: x - box.left,
y: y - box.top
},
height = box.height,
width = box.width;
var diameter = Math.max(Math.pow((Math.pow(height, 2) + Math.pow(width, 2)), 0.5), 48);
rippleWave = $(
'<div class="ripple-wave" style="width: ' + diameter + 'px; height: '+diameter+'px; margin-top:-'+diameter/2+'px; margin-left:-'+diameter/2+'px; left:'+center.x+'px; top:'+center.y+'px;"></div>'
);
el.prepend(rippleWave);
var clientLeft = rippleWave[0].clientLeft;
rippleTransform = 'translate3d('+(-center.x + width/2)+'px, '+(-center.y + height/2)+'px, 0) scale(1)';
rippleWave.transform(rippleTransform);
}
function removeRipple() {
if (!rippleWave) return;
var toRemove = rippleWave;
var removeTimeout = setTimeout(function () {
toRemove.remove();
}, 400);
rippleWave
.addClass('ripple-wave-fill')
.transform(rippleTransform.replace('scale(1)', 'scale(1.01)'))
.transitionEnd(function () {
clearTimeout(removeTimeout);
var rippleWave = $(this)
.addClass('ripple-wave-out')
.transform(rippleTransform.replace('scale(1)', 'scale(1.01)'));
removeTimeout = setTimeout(function () {
rippleWave.remove();
}, 700);
setTimeout(function () {
rippleWave.transitionEnd(function(){
clearTimeout(removeTimeout);
$(this).remove();
});
}, 0);
});
rippleWave = rippleTarget = undefined;
}
function rippleTouchStart (el, x, y) {
rippleTarget = findRippleElement(el);
if (!rippleTarget || rippleTarget.length === 0) {
rippleTarget = undefined;
return;
}
if (!isInsideScrollableView(rippleTarget)) {
createRipple(touchStartX, touchStartY, rippleTarget);
}
else {
rippleTimeout = setTimeout(function () {
createRipple(touchStartX, touchStartY, rippleTarget);
}, 80);
}
}
function rippleTouchMove() {
clearTimeout(rippleTimeout);
removeRipple();
}
function rippleTouchEnd() {
if (rippleWave) {
removeRipple();
}
else if (rippleTarget && !isMoved) {
clearTimeout(rippleTimeout);
createRipple(touchStartX, touchStartY, rippleTarget);
setTimeout(removeRipple, 0);
}
else {
removeRipple();
}
}
// Send Click
function sendClick(e) {
var touch = e.changedTouches[0];
var evt = document.createEvent('MouseEvents');
var eventType = 'click';
if (app.device.android && targetElement.nodeName.toLowerCase() === 'select') {
eventType = 'mousedown';
}
evt.initMouseEvent(eventType, true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
evt.forwardedTouchEvent = true;
targetElement.dispatchEvent(evt);
}
// Touch Handlers
function handleTouchStart(e) {
isMoved = false;
tapHoldFired = false;
if (e.targetTouches.length > 1) {
if (activableElement) removeActive();
return true;
}
if (e.touches.length > 1 && activableElement) {
removeActive();
}
if (app.params.tapHold) {
if (tapHoldTimeout) clearTimeout(tapHoldTimeout);
tapHoldTimeout = setTimeout(function () {
if (e && e.touches && e.touches.length > 1) return;
tapHoldFired = true;
e.preventDefault();
$(e.target).trigger('taphold');
}, app.params.tapHoldDelay);
}
if (needsFastClickTimeOut) clearTimeout(needsFastClickTimeOut);
needsFastClick = targetNeedsFastClick(e.target);
if (!needsFastClick) {
trackClick = false;
return true;
}
if (app.device.ios || (app.device.android && 'getSelection' in window)) {
var selection = window.getSelection();
if (selection.rangeCount && selection.focusNode !== document.body && (!selection.isCollapsed || document.activeElement === selection.focusNode)) {
activeSelection = true;
return true;
}
else {
activeSelection = false;
}
}
if (app.device.android) {
if (androidNeedsBlur(e.target)) {
document.activeElement.blur();
}
}
trackClick = true;
targetElement = e.target;
touchStartTime = (new Date()).getTime();
touchStartX = e.targetTouches[0].pageX;
touchStartY = e.targetTouches[0].pageY;
// Detect scroll parent
if (app.device.ios) {
scrollParent = undefined;
$(targetElement).parents().each(function () {
var parent = this;
if (parent.scrollHeight > parent.offsetHeight && !scrollParent) {
scrollParent = parent;
scrollParent.f7ScrollTop = scrollParent.scrollTop;
}
});
}
if ((e.timeStamp - lastClickTime) < app.params.fastClicksDelayBetweenClicks) {
e.preventDefault();
}
if (app.params.activeState) {
activableElement = findActivableElement(targetElement);
// If it's inside a scrollable view, we don't trigger active-state yet,
// because it can be a scroll instead. Based on the link:
// http://labnote.beedesk.com/click-scroll-and-pseudo-active-on-mobile-webk
if (!isInsideScrollableView(activableElement)) {
addActive();
} else {
activeTimeout = setTimeout(addActive, 80);
}
}
if (app.params.material && app.params.materialRipple) {
rippleTouchStart(targetElement, touchStartX, touchStartY);
}
}
function handleTouchMove(e) {
if (!trackClick) return;
var _isMoved = false;
var distance = app.params.fastClicksDistanceThreshold;
if (distance) {
var pageX = e.targetTouches[0].pageX;
var pageY = e.targetTouches[0].pageY;
if (Math.abs(pageX - touchStartX) > distance || Math.abs(pageY - touchStartY) > distance) {
_isMoved = true;
}
}
else {
_isMoved = true;
}
if (_isMoved) {
trackClick = false;
targetElement = null;
isMoved = true;
if (app.params.tapHold) {
clearTimeout(tapHoldTimeout);
}
if (app.params.activeState) {
clearTimeout(activeTimeout);
removeActive();
}
if (app.params.material && app.params.materialRipple) {
rippleTouchMove();
}
}
}
function handleTouchEnd(e) {
clearTimeout(activeTimeout);
clearTimeout(tapHoldTimeout);
if (!trackClick) {
if (!activeSelection && needsFastClick) {
if (!(app.device.android && !e.cancelable)) {
e.preventDefault();
}
}
return true;
}
if (document.activeElement === e.target) {
if (app.params.activeState) removeActive();
if (app.params.material && app.params.materialRipple) {
rippleTouchEnd();
}
return true;
}
if (!activeSelection) {
e.preventDefault();
}
if ((e.timeStamp - lastClickTime) < app.params.fastClicksDelayBetweenClicks) {
setTimeout(removeActive, 0);
return true;
}
lastClickTime = e.timeStamp;
trackClick = false;
if (app.device.ios && scrollParent) {
if (scrollParent.scrollTop !== scrollParent.f7ScrollTop) {
return false;
}
}
// Add active-state here because, in a very fast tap, the timeout didn't
// have the chance to execute. Removing active-state in a timeout gives
// the chance to the animation execute.
if (app.params.activeState) {
addActive();
setTimeout(removeActive, 0);
}
// Remove Ripple
if (app.params.material && app.params.materialRipple) {
rippleTouchEnd();
}
// Trigger focus when required
if (targetNeedsFocus(targetElement)) {
if (app.device.ios && app.device.webView) {
if ((event.timeStamp - touchStartTime) > 159) {
targetElement = null;
return false;
}
targetElement.focus();
return false;
}
else {
targetElement.focus();
}
}
// Blur active elements
if (document.activeElement && targetElement !== document.activeElement && document.activeElement !== document.body && targetElement.nodeName.toLowerCase() !== 'label') {
document.activeElement.blur();
}
// Send click
e.preventDefault();
sendClick(e);
return false;
}
function handleTouchCancel(e) {
trackClick = false;
targetElement = null;
// Remove Active State
clearTimeout(activeTimeout);
clearTimeout(tapHoldTimeout);
if (app.params.activeState) {
removeActive();
}
// Remove Ripple
if (app.params.material && app.params.materialRipple) {
rippleTouchEnd();
}
}
function handleClick(e) {
var allowClick = false;
if (trackClick) {
targetElement = null;
trackClick = false;
return true;
}
if (e.target.type === 'submit' && e.detail === 0) {
return true;
}
if (!targetElement) {
if (!isFormElement(e.target)) {
allowClick = true;
}
}
if (!needsFastClick) {
allowClick = true;
}
if (document.activeElement === targetElement) {
allowClick = true;
}
if (e.forwardedTouchEvent) {
allowClick = true;
}
if (!e.cancelable) {
allowClick = true;
}
if (app.params.tapHold && app.params.tapHoldPreventClicks && tapHoldFired) {
allowClick = false;
}
if (!allowClick) {
e.stopImmediatePropagation();
e.stopPropagation();
if (targetElement) {
if (targetNeedsPrevent(targetElement) || isMoved) {
e.preventDefault();
}
}
else {
e.preventDefault();
}
targetElement = null;
}
needsFastClickTimeOut = setTimeout(function () {
needsFastClick = false;
}, (app.device.ios || app.device.androidChrome ? 100 : 400));
if (app.params.tapHold) {
tapHoldTimeout = setTimeout(function () {
tapHoldFired = false;
}, (app.device.ios || app.device.androidChrome ? 100 : 400));
}
return allowClick;
}
if (app.support.touch) {
document.addEventListener('click', handleClick, true);
document.addEventListener('touchstart', handleTouchStart);
document.addEventListener('touchmove', handleTouchMove);
document.addEventListener('touchend', handleTouchEnd);
document.addEventListener('touchcancel', handleTouchCancel);
}
else {
if (app.params.activeState) {
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
}
if (app.params.material && app.params.materialRipple) {
document.addEventListener('contextmenu', function (e) {
if (activableElement) removeActive();
rippleTouchEnd();
});
}
};