UNPKG

framework7

Version:

Full featured mobile HTML framework for building iOS & Android apps

510 lines (460 loc) 16.2 kB
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;