framework7
Version:
Full featured mobile HTML framework for building iOS & Android apps
512 lines (503 loc) • 18.4 kB
JavaScript
import $ from '../../shared/dom7.js';
import { deleteProps } from '../../shared/utils.js';
import Framework7Class from '../../shared/class.js';
import { getSupport } from '../../shared/get-support.js';
import { getDevice } from '../../shared/get-device.js';
class PullToRefresh extends Framework7Class {
constructor(app, el) {
super({}, [app]);
const ptr = this;
const device = getDevice();
const support = getSupport();
const $el = $(el);
const $preloaderEl = $el.find('.ptr-preloader');
ptr.$el = $el;
ptr.el = $el[0];
ptr.app = app;
ptr.bottom = ptr.$el.hasClass('ptr-bottom');
// Extend defaults with modules params
ptr.useModulesParams({});
const isMaterial = app.theme === 'md';
const isIos = app.theme === 'ios';
// Done
ptr.done = function done() {
const $transitionTarget = isMaterial ? $preloaderEl : $el;
const onTranstionEnd = e => {
if ($(e.target).closest($preloaderEl).length) return;
$el.removeClass('ptr-transitioning ptr-pull-up ptr-pull-down ptr-closing');
$el.trigger('ptr:done');
ptr.emit('local::done ptrDone', $el[0]);
$transitionTarget.off('transitionend', onTranstionEnd);
};
$transitionTarget.on('transitionend', onTranstionEnd);
$el.removeClass('ptr-refreshing').addClass('ptr-transitioning ptr-closing');
return ptr;
};
ptr.refresh = function refresh() {
if ($el.hasClass('ptr-refreshing')) return ptr;
$el.addClass('ptr-transitioning ptr-refreshing');
$el.trigger('ptr:refresh', ptr.done);
ptr.emit('local::refresh ptrRefresh', $el[0], ptr.done);
return ptr;
};
// Mousewheel
ptr.mousewheel = $el.attr('data-ptr-mousewheel') === 'true';
// Events handling
let touchId;
let isTouched;
let isMoved;
const touchesStart = {};
let isScrolling;
let touchesDiff;
let refresh = false;
let useTranslate = false;
let forceUseTranslate = false;
let startTranslate = 0;
let translate;
let scrollTop;
let wasScrolled;
let triggerDistance;
let dynamicTriggerDistance;
let pullStarted;
let hasNavbar = false;
let scrollHeight;
let offsetHeight;
let maxScrollTop;
const $pageEl = $el.parents('.page');
if ($pageEl.find('.navbar').length > 0 || $pageEl.parents('.view').children('.navbars').length > 0) hasNavbar = true;
if ($pageEl.hasClass('no-navbar')) hasNavbar = false;
if (!ptr.bottom) {
const pageNavbarEl = app.navbar.getElByPage($pageEl[0]);
if (pageNavbarEl) {
const $pageNavbarEl = $(pageNavbarEl);
const isLargeTransparent = $pageNavbarEl.hasClass('navbar-large-transparent') || $pageNavbarEl.hasClass('navbar-large') && $pageNavbarEl.hasClass('navbar-transparent');
const isTransparent = $pageNavbarEl.hasClass('navbar-transparent') && !$pageNavbarEl.hasClass('navbar-large');
if (isLargeTransparent) {
$el.addClass('ptr-with-navbar-large-transparent');
} else if (isTransparent) {
$el.addClass('ptr-with-navbar-transparent');
}
}
}
if (!hasNavbar && !ptr.bottom) $el.addClass('ptr-no-navbar');
// Define trigger distance
if ($el.attr('data-ptr-distance')) {
dynamicTriggerDistance = true;
} else if (isMaterial) {
triggerDistance = 66;
} else if (isIos) {
triggerDistance = 44;
}
function setPreloaderProgress(progress) {
if (progress === void 0) {
progress = 0;
}
const $bars = $preloaderEl.find('.preloader-inner-line');
const perBarProgress = 1 / $bars.length;
$bars.forEach((barEl, barIndex) => {
const barProgress = (progress - barIndex * perBarProgress) / perBarProgress;
barEl.style.opacity = Math.max(Math.min(barProgress, 1), 0) * 0.27;
});
}
function unsetPreloaderProgress() {
$preloaderEl.find('.preloader-inner-line').css('opacity', '');
}
function handleTouchStart(e) {
if (!e.isTrusted) return;
if (isTouched) {
if (device.os === 'android') {
if ('targetTouches' in e && e.targetTouches.length > 1) return;
} else return;
}
if ($el.hasClass('ptr-refreshing')) {
return;
}
if ($(e.target).closest('.sortable-handler, .ptr-ignore, .card-expandable.card-opened').length) return;
isMoved = false;
pullStarted = false;
isTouched = true;
isScrolling = undefined;
wasScrolled = undefined;
if (e.type === 'touchstart') touchId = e.targetTouches[0].identifier;
touchesStart.x = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
touchesStart.y = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;
}
function handleTouchMove(e) {
if (!isTouched || !e.isTrusted) return;
let pageX;
let pageY;
let touch;
if (e.type === 'touchmove') {
if (touchId && e.touches) {
for (let i = 0; i < e.touches.length; i += 1) {
if (e.touches[i].identifier === touchId) {
touch = e.touches[i];
}
}
}
if (!touch) touch = e.targetTouches[0];
pageX = touch.pageX;
pageY = touch.pageY;
} else {
pageX = e.pageX;
pageY = e.pageY;
}
if (!pageX || !pageY) return;
if (typeof isScrolling === 'undefined') {
isScrolling = !!(isScrolling || Math.abs(pageY - touchesStart.y) > Math.abs(pageX - touchesStart.x));
}
if (!isScrolling) {
isTouched = false;
return;
}
scrollTop = $el[0].scrollTop;
if (!isMoved) {
$el.removeClass('ptr-transitioning');
if (isIos) {
setPreloaderProgress(0);
}
let targetIsScrollable;
scrollHeight = $el[0].scrollHeight;
offsetHeight = $el[0].offsetHeight;
if (ptr.bottom) {
maxScrollTop = scrollHeight - offsetHeight;
}
if (scrollTop > scrollHeight) {
isTouched = false;
return;
}
const $ptrWatchScrollable = $(e.target).closest('.ptr-watch-scroll');
if ($ptrWatchScrollable.length) {
$ptrWatchScrollable.each(ptrScrollableEl => {
if (ptrScrollableEl === el) return;
if (ptrScrollableEl.scrollHeight > ptrScrollableEl.offsetHeight && $(ptrScrollableEl).css('overflow') === 'auto' && (!ptr.bottom && ptrScrollableEl.scrollTop > 0 || ptr.bottom && ptrScrollableEl.scrollTop < ptrScrollableEl.scrollHeight - ptrScrollableEl.offsetHeight)) {
targetIsScrollable = true;
}
});
}
if (targetIsScrollable) {
isTouched = false;
return;
}
if (dynamicTriggerDistance) {
triggerDistance = $el.attr('data-ptr-distance');
if (triggerDistance.indexOf('%') >= 0) triggerDistance = scrollHeight * parseInt(triggerDistance, 10) / 100;
}
startTranslate = $el.hasClass('ptr-refreshing') ? triggerDistance : 0;
if (scrollHeight === offsetHeight || device.os !== 'ios' || isMaterial) {
useTranslate = true;
} else {
useTranslate = false;
}
forceUseTranslate = false;
}
isMoved = true;
touchesDiff = pageY - touchesStart.y;
if (typeof wasScrolled === 'undefined' && (ptr.bottom ? scrollTop !== maxScrollTop : scrollTop !== 0)) wasScrolled = true;
const ptrStarted = ptr.bottom ? touchesDiff < 0 && scrollTop >= maxScrollTop || scrollTop > maxScrollTop : touchesDiff > 0 && scrollTop <= 0 || scrollTop < 0;
if (ptrStarted) {
// iOS 8 fix
if (device.os === 'ios' && parseInt(device.osVersion.split('.')[0], 10) > 7) {
if (!ptr.bottom && scrollTop === 0 && !wasScrolled) useTranslate = true;
if (ptr.bottom && scrollTop === maxScrollTop && !wasScrolled) useTranslate = true;
}
if (!useTranslate && ptr.bottom && !isMaterial) {
$el.css('-webkit-overflow-scrolling', 'auto');
$el.scrollTop(maxScrollTop);
forceUseTranslate = true;
}
if (useTranslate || forceUseTranslate) {
if (e.cancelable) {
e.preventDefault();
}
translate = (ptr.bottom ? -1 * Math.abs(touchesDiff) ** 0.85 : touchesDiff ** 0.85) + startTranslate;
if (isMaterial) {
$preloaderEl.transform(`translate3d(0,${translate}px,0)`).find('.ptr-arrow').transform(`rotate(${180 * (Math.abs(touchesDiff) / 66) + 100}deg)`);
} else {
// eslint-disable-next-line
if (ptr.bottom || isIos) {
$el.children().transform(`translate3d(0,${translate}px,0)`);
} else {
// eslint-disable-next-line
$el.transform(`translate3d(0,${translate}px,0)`);
}
if (isIos) {
$preloaderEl.transform(`translate3d(0,0px,0)`);
}
}
} else if (isIos && !ptr.bottom) {
$preloaderEl.transform(`translate3d(0,${scrollTop}px,0)`);
}
let progress;
if (isIos && !refresh) {
progress = useTranslate || forceUseTranslate ? Math.abs(touchesDiff) ** 0.85 / triggerDistance : Math.abs(touchesDiff) / (triggerDistance * 2);
setPreloaderProgress(progress);
}
if ((useTranslate || forceUseTranslate) && Math.abs(touchesDiff) ** 0.85 > triggerDistance || !useTranslate && Math.abs(touchesDiff) >= triggerDistance * 2) {
refresh = true;
$el.addClass('ptr-pull-up').removeClass('ptr-pull-down');
unsetPreloaderProgress();
} else {
refresh = false;
$el.removeClass('ptr-pull-up').addClass('ptr-pull-down');
}
if (!pullStarted) {
$el.trigger('ptr:pullstart');
ptr.emit('local::pullStart ptrPullStart', $el[0]);
pullStarted = true;
}
$el.trigger('ptr:pullmove', {
event: e,
scrollTop,
translate,
touchesDiff
});
ptr.emit('local::pullMove ptrPullMove', $el[0], {
event: e,
scrollTop,
translate,
touchesDiff
});
} else {
pullStarted = false;
$el.removeClass('ptr-pull-up ptr-pull-down');
refresh = false;
}
}
function handleTouchEnd(e) {
if (!e.isTrusted) return;
if (e.type === 'touchend' && e.changedTouches && e.changedTouches.length > 0 && touchId) {
if (e.changedTouches[0].identifier !== touchId) {
isTouched = false;
isScrolling = false;
isMoved = false;
touchId = null;
return;
}
}
if (!isTouched || !isMoved) {
isTouched = false;
isMoved = false;
return;
}
if (translate) {
$el.addClass('ptr-transitioning');
translate = 0;
}
if (isMaterial) {
$preloaderEl.transform('').find('.ptr-arrow').transform('');
} else {
$preloaderEl.transform('');
if (ptr.bottom || isIos) {
$el.children().transform('');
} else {
$el.transform('');
}
}
if (!useTranslate && ptr.bottom && !isMaterial) {
$el.css('-webkit-overflow-scrolling', '');
}
if (refresh) {
$el.addClass('ptr-refreshing');
$el.trigger('ptr:refresh', ptr.done);
ptr.emit('local::refresh ptrRefresh', $el[0], ptr.done);
} else {
$el.removeClass('ptr-pull-down');
}
isTouched = false;
isMoved = false;
if (pullStarted) {
$el.trigger('ptr:pullend');
ptr.emit('local::pullEnd ptrPullEnd', $el[0]);
}
}
let mousewheelTimeout;
let mousewheelMoved;
let mousewheelAllow = true;
let mousewheelTranslate = 0;
function handleMouseWheelRelease() {
mousewheelAllow = true;
mousewheelMoved = false;
mousewheelTranslate = 0;
if (translate) {
$el.addClass('ptr-transitioning');
translate = 0;
}
if (isMaterial) {
$preloaderEl.transform('').find('.ptr-arrow').transform('');
} else {
$preloaderEl.transform('');
if (ptr.bottom) {
$el.children().transform('');
} else {
$el.transform('');
}
}
if (refresh) {
$el.addClass('ptr-refreshing');
$el.trigger('ptr:refresh', ptr.done);
ptr.emit('local::refresh ptrRefresh', $el[0], ptr.done);
} else {
$el.removeClass('ptr-pull-down');
}
if (pullStarted) {
$el.trigger('ptr:pullend');
ptr.emit('local::pullEnd ptrPullEnd', $el[0]);
}
}
function handleMouseWheel(e) {
if (!mousewheelAllow) return;
const {
deltaX,
deltaY
} = e;
if (Math.abs(deltaX) > Math.abs(deltaY)) return;
if ($el.hasClass('ptr-refreshing')) {
return;
}
if ($(e.target).closest('.sortable-handler, .ptr-ignore, .card-expandable.card-opened').length) return;
clearTimeout(mousewheelTimeout);
scrollTop = $el[0].scrollTop;
if (!mousewheelMoved) {
$el.removeClass('ptr-transitioning');
if (isIos) {
setPreloaderProgress(0);
}
let targetIsScrollable;
scrollHeight = $el[0].scrollHeight;
offsetHeight = $el[0].offsetHeight;
if (ptr.bottom) {
maxScrollTop = scrollHeight - offsetHeight;
}
if (scrollTop > scrollHeight) {
mousewheelAllow = false;
return;
}
const $ptrWatchScrollable = $(e.target).closest('.ptr-watch-scroll');
if ($ptrWatchScrollable.length) {
$ptrWatchScrollable.each(ptrScrollableEl => {
if (ptrScrollableEl === el) return;
if (ptrScrollableEl.scrollHeight > ptrScrollableEl.offsetHeight && $(ptrScrollableEl).css('overflow') === 'auto' && (!ptr.bottom && ptrScrollableEl.scrollTop > 0 || ptr.bottom && ptrScrollableEl.scrollTop < ptrScrollableEl.scrollHeight - ptrScrollableEl.offsetHeight)) {
targetIsScrollable = true;
}
});
}
if (targetIsScrollable) {
mousewheelAllow = false;
return;
}
if (dynamicTriggerDistance) {
triggerDistance = $el.attr('data-ptr-distance');
if (triggerDistance.indexOf('%') >= 0) triggerDistance = scrollHeight * parseInt(triggerDistance, 10) / 100;
}
}
isMoved = true;
mousewheelTranslate -= deltaY;
touchesDiff = mousewheelTranslate; // pageY - touchesStart.y;
if (typeof wasScrolled === 'undefined' && (ptr.bottom ? scrollTop !== maxScrollTop : scrollTop !== 0)) wasScrolled = true;
const ptrStarted = ptr.bottom ? touchesDiff < 0 && scrollTop >= maxScrollTop || scrollTop > maxScrollTop : touchesDiff > 0 && scrollTop <= 0 || scrollTop < 0;
if (ptrStarted) {
if (e.cancelable) {
e.preventDefault();
}
translate = touchesDiff;
if (Math.abs(translate) > triggerDistance) {
translate = triggerDistance + (Math.abs(translate) - triggerDistance) ** 0.7;
if (ptr.bottom) translate = -translate;
}
if (isMaterial) {
$preloaderEl.transform(`translate3d(0,${translate}px,0)`).find('.ptr-arrow').transform(`rotate(${180 * (Math.abs(touchesDiff) / 66) + 100}deg)`);
} else {
// eslint-disable-next-line
if (ptr.bottom) {
$el.children().transform(`translate3d(0,${translate}px,0)`);
} else {
$el.transform(`translate3d(0,${translate}px,0)`);
if (isIos) {
$preloaderEl.transform(`translate3d(0,${-translate}px,0)`);
}
}
}
let progress;
if (isIos && !refresh) {
progress = Math.abs(translate) / triggerDistance;
setPreloaderProgress(progress);
}
if (Math.abs(translate) > triggerDistance) {
refresh = true;
$el.addClass('ptr-pull-up').removeClass('ptr-pull-down');
unsetPreloaderProgress();
} else {
refresh = false;
$el.removeClass('ptr-pull-up').addClass('ptr-pull-down');
}
if (!pullStarted) {
$el.trigger('ptr:pullstart');
ptr.emit('local::pullStart ptrPullStart', $el[0]);
pullStarted = true;
}
$el.trigger('ptr:pullmove', {
event: e,
scrollTop,
translate,
touchesDiff
});
ptr.emit('local::pullMove ptrPullMove', $el[0], {
event: e,
scrollTop,
translate,
touchesDiff
});
} else {
pullStarted = false;
$el.removeClass('ptr-pull-up ptr-pull-down');
refresh = false;
}
mousewheelTimeout = setTimeout(handleMouseWheelRelease, 300);
}
if (!$pageEl.length || !$el.length) return ptr;
$el[0].f7PullToRefresh = ptr;
// Events
ptr.attachEvents = function attachEvents() {
const passive = support.passiveListener ? {
passive: true
} : false;
$el.on(app.touchEvents.start, handleTouchStart, passive);
app.on('touchmove:active', handleTouchMove);
app.on('touchend:passive', handleTouchEnd);
if (ptr.mousewheel && !ptr.bottom) {
$el.on('wheel', handleMouseWheel);
}
};
ptr.detachEvents = function detachEvents() {
const passive = support.passiveListener ? {
passive: true
} : false;
$el.off(app.touchEvents.start, handleTouchStart, passive);
app.off('touchmove:active', handleTouchMove);
app.off('touchend:passive', handleTouchEnd);
if (ptr.mousewheel && !ptr.bottom) {
$el.off('wheel', handleMouseWheel);
}
};
// Install Modules
ptr.useModules();
// Init
ptr.init();
return ptr;
}
init() {
const ptr = this;
ptr.attachEvents();
}
destroy() {
let ptr = this;
ptr.emit('local::beforeDestroy ptrBeforeDestroy', ptr);
ptr.$el.trigger('ptr:beforedestroy');
delete ptr.el.f7PullToRefresh;
ptr.detachEvents();
deleteProps(ptr);
ptr = null;
}
}
export default PullToRefresh;