framework7
Version:
Full featured mobile HTML framework for building iOS & Android apps
510 lines (460 loc) • 16.2 kB
JavaScript
import $ from 'dom7';
import Utils from '../../utils/utils';
import Framework7Class from '../../utils/class';
import Support from '../../utils/support';
import Device from '../../utils/device';
class PullToRefresh extends Framework7Class {
constructor(app, el) {
super({}, [app]);
const ptr = this;
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';
const isAurora = app.theme === 'aurora';
// Done
ptr.done = function done() {
const $transitionTarget = isMaterial ? $preloaderEl : $el;
$transitionTarget.transitionEnd(() => {
$el.removeClass('ptr-transitioning ptr-pull-up ptr-pull-down');
$el.trigger('ptr:done');
ptr.emit('local::done ptrDone', $el[0]);
});
$el.removeClass('ptr-refreshing').addClass('ptr-transitioning');
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('.navbar').length > 0) hasNavbar = true;
if ($pageEl.hasClass('no-navbar')) hasNavbar = false;
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;
} else if (isAurora) {
triggerDistance = 38;
}
function handleTouchStart(e) {
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) 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');
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((ptrScrollableIndex, 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) {
$el.children().transform(`translate3d(0,${translate}px,0)`);
} else {
$el.transform(`translate3d(0,${translate}px,0)`);
}
}
}
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');
} 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.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 {
// eslint-disable-next-line
if (ptr.bottom) {
$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 {
// eslint-disable-next-line
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');
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((ptrScrollableIndex, 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 (Math.abs(translate) > triggerDistance) {
refresh = true;
$el.addClass('ptr-pull-up').removeClass('ptr-pull-down');
} 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', ptr);
delete ptr.el.f7PullToRefresh;
ptr.detachEvents();
Utils.deleteProps(ptr);
ptr = null;
}
}
export default PullToRefresh;