UNPKG

framework7

Version:

Full featured mobile HTML framework for building iOS & Android apps

512 lines (503 loc) 18.4 kB
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;