UNPKG

framework7

Version:

Full featured mobile HTML framework for building iOS & Android apps

1,382 lines (1,271 loc) 43.9 kB
import { window, document } from 'ssr-window'; import $ from 'dom7'; import Template7 from 'template7'; import PathToRegexp from 'path-to-regexp'; // eslint-disable-line import Framework7Class from '../../utils/class'; import Utils from '../../utils/utils'; import History from '../../utils/history'; import SwipeBack from './swipe-back'; import { refreshPage, forward, load, navigate } from './navigate'; import { tabLoad, tabRemove } from './tab'; import { modalLoad, modalRemove } from './modal'; import { backward, loadBack, back } from './back'; import { clearPreviousHistory, clearPreviousPages } from './clear-previous-history'; class Router extends Framework7Class { constructor(app, view) { super({}, [typeof view === 'undefined' ? app : view]); const router = this; // Is App Router router.isAppRouter = typeof view === 'undefined'; if (router.isAppRouter) { // App Router Utils.extend(false, router, { app, params: app.params.view, routes: app.routes || [], cache: app.cache, }); } else { // View Router Utils.extend(false, router, { app, view, viewId: view.id, params: view.params, routes: view.routes, $el: view.$el, el: view.el, $navbarEl: view.$navbarEl, navbarEl: view.navbarEl, history: view.history, scrollHistory: view.scrollHistory, cache: app.cache, dynamicNavbar: app.theme === 'ios' && view.params.iosDynamicNavbar, separateNavbar: app.theme === 'ios' && view.params.iosDynamicNavbar && view.params.iosSeparateDynamicNavbar, initialPages: [], initialNavbars: [], }); } // Install Modules router.useModules(); // Temporary Dom router.tempDom = document.createElement('div'); // AllowPageChage router.allowPageChange = true; // Current Route let currentRoute = {}; let previousRoute = {}; Object.defineProperty(router, 'currentRoute', { enumerable: true, configurable: true, set(newRoute = {}) { previousRoute = Utils.extend({}, currentRoute); currentRoute = newRoute; if (!currentRoute) return; router.url = currentRoute.url; router.emit('routeChange', newRoute, previousRoute, router); }, get() { return currentRoute; }, }); Object.defineProperty(router, 'previousRoute', { enumerable: true, configurable: true, get() { return previousRoute; }, set(newRoute) { previousRoute = newRoute; }, }); return router; } animatableNavElements(newNavbarInner, oldNavbarInner) { const router = this; const dynamicNavbar = router.dynamicNavbar; const animateIcon = router.params.iosAnimateNavbarBackIcon; let newNavEls; let oldNavEls; function animatableNavEl(el, navbarInner) { const $el = $(el); const isSliding = $el.hasClass('sliding') || navbarInner.hasClass('sliding'); const isSubnavbar = $el.hasClass('subnavbar'); const needsOpacityTransition = isSliding ? !isSubnavbar : true; const hasIcon = isSliding && animateIcon && $el.hasClass('left') && $el.find('.back .icon').length > 0; let $iconEl; if (hasIcon) $iconEl = $el.find('.back .icon'); return { $el, $iconEl, hasIcon, leftOffset: $el[0].f7NavbarLeftOffset, rightOffset: $el[0].f7NavbarRightOffset, isSliding, isSubnavbar, needsOpacityTransition, }; } if (dynamicNavbar) { newNavEls = []; oldNavEls = []; newNavbarInner.children('.left, .right, .title, .subnavbar').each((index, navEl) => { newNavEls.push(animatableNavEl(navEl, newNavbarInner)); }); oldNavbarInner.children('.left, .right, .title, .subnavbar').each((index, navEl) => { oldNavEls.push(animatableNavEl(navEl, oldNavbarInner)); }); [oldNavEls, newNavEls].forEach((navEls) => { navEls.forEach((navEl) => { const n = navEl; const { isSliding, $el } = navEl; const otherEls = navEls === oldNavEls ? newNavEls : oldNavEls; if (!(isSliding && $el.hasClass('title') && otherEls)) return; otherEls.forEach((otherNavEl) => { if (otherNavEl.$el.hasClass('left') && otherNavEl.hasIcon) { const iconTextEl = otherNavEl.$el.find('.back span')[0]; n.leftOffset += iconTextEl ? iconTextEl.offsetLeft : 0; } }); }); }); } return { newNavEls, oldNavEls }; } animateWithCSS(oldPage, newPage, oldNavbarInner, newNavbarInner, direction, callback) { const router = this; const dynamicNavbar = router.dynamicNavbar; const separateNavbar = router.separateNavbar; const ios = router.app.theme === 'ios'; // Router Animation class const routerTransitionClass = `router-transition-${direction} router-transition-css-${direction}`; let newNavEls; let oldNavEls; let navbarWidth = 0; if (ios && dynamicNavbar) { if (!separateNavbar) { navbarWidth = newNavbarInner[0].offsetWidth; } const navEls = router.animatableNavElements(newNavbarInner, oldNavbarInner); newNavEls = navEls.newNavEls; oldNavEls = navEls.oldNavEls; } function animateNavbars(progress) { if (ios && dynamicNavbar) { newNavEls.forEach((navEl) => { const $el = navEl.$el; const offset = direction === 'forward' ? navEl.rightOffset : navEl.leftOffset; if (navEl.isSliding) { $el.transform(`translate3d(${offset * (1 - progress)}px,0,0)`); } if (navEl.hasIcon) { if (direction === 'forward') { navEl.$iconEl.transform(`translate3d(${(-offset - navbarWidth) * (1 - progress)}px,0,0)`); } else { navEl.$iconEl.transform(`translate3d(${(-offset + (navbarWidth / 5)) * (1 - progress)}px,0,0)`); } } }); oldNavEls.forEach((navEl) => { const $el = navEl.$el; const offset = direction === 'forward' ? navEl.leftOffset : navEl.rightOffset; if (navEl.isSliding) { $el.transform(`translate3d(${offset * (progress)}px,0,0)`); } if (navEl.hasIcon) { if (direction === 'forward') { navEl.$iconEl.transform(`translate3d(${(-offset + (navbarWidth / 5)) * (progress)}px,0,0)`); } else { navEl.$iconEl.transform(`translate3d(${(-offset - navbarWidth) * (progress)}px,0,0)`); } } }); } } // AnimationEnd Callback function onDone() { if (router.dynamicNavbar) { if (newNavbarInner.hasClass('sliding')) { newNavbarInner.find('.title, .left, .right, .left .icon, .subnavbar').transform(''); } else { newNavbarInner.find('.sliding').transform(''); } if (oldNavbarInner.hasClass('sliding')) { oldNavbarInner.find('.title, .left, .right, .left .icon, .subnavbar').transform(''); } else { oldNavbarInner.find('.sliding').transform(''); } } router.$el.removeClass(routerTransitionClass); if (callback) callback(); } (direction === 'forward' ? newPage : oldPage).animationEnd(() => { onDone(); }); // Animate if (dynamicNavbar) { // Prepare Navbars animateNavbars(0); Utils.nextFrame(() => { // Add class, start animation animateNavbars(1); router.$el.addClass(routerTransitionClass); }); } else { // Add class, start animation router.$el.addClass(routerTransitionClass); } } animateWithJS(oldPage, newPage, oldNavbarInner, newNavbarInner, direction, callback) { const router = this; const dynamicNavbar = router.dynamicNavbar; const separateNavbar = router.separateNavbar; const ios = router.app.theme === 'ios'; const duration = ios ? 400 : 250; const routerTransitionClass = `router-transition-${direction} router-transition-js-${direction}`; let startTime = null; let done = false; let newNavEls; let oldNavEls; let navbarWidth = 0; if (ios && dynamicNavbar) { if (!separateNavbar) { navbarWidth = newNavbarInner[0].offsetWidth; } const navEls = router.animatableNavElements(newNavbarInner, oldNavbarInner); newNavEls = navEls.newNavEls; oldNavEls = navEls.oldNavEls; } let $shadowEl; let $opacityEl; if (ios) { $shadowEl = $('<div class="page-shadow-effect"></div>'); $opacityEl = $('<div class="page-opacity-effect"></div>'); if (direction === 'forward') { newPage.append($shadowEl); oldPage.append($opacityEl); } else { newPage.append($opacityEl); oldPage.append($shadowEl); } } const easing = Utils.bezier(0.25, 0.1, 0.25, 1); function onDone() { newPage.transform('').css('opacity', ''); oldPage.transform('').css('opacity', ''); if (ios) { $shadowEl.remove(); $opacityEl.remove(); if (dynamicNavbar) { newNavEls.forEach((navEl) => { navEl.$el.transform(''); navEl.$el.css('opacity', ''); }); oldNavEls.forEach((navEl) => { navEl.$el.transform(''); navEl.$el.css('opacity', ''); }); newNavEls = []; oldNavEls = []; } } router.$el.removeClass(routerTransitionClass); if (callback) callback(); } function render() { const time = Utils.now(); if (!startTime) startTime = time; const progress = Math.max(Math.min((time - startTime) / duration, 1), 0); const easeProgress = easing(progress); if (progress >= 1) { done = true; } const inverter = router.app.rtl ? -1 : 1; if (ios) { if (direction === 'forward') { newPage.transform(`translate3d(${(1 - easeProgress) * 100 * inverter}%,0,0)`); oldPage.transform(`translate3d(${-easeProgress * 20 * inverter}%,0,0)`); $shadowEl[0].style.opacity = easeProgress; $opacityEl[0].style.opacity = easeProgress; } else { newPage.transform(`translate3d(${-(1 - easeProgress) * 20 * inverter}%,0,0)`); oldPage.transform(`translate3d(${easeProgress * 100 * inverter}%,0,0)`); $shadowEl[0].style.opacity = 1 - easeProgress; $opacityEl[0].style.opacity = 1 - easeProgress; } if (dynamicNavbar) { newNavEls.forEach((navEl) => { const $el = navEl.$el; const offset = direction === 'forward' ? navEl.rightOffset : navEl.leftOffset; if (navEl.needsOpacityTransition) { $el[0].style.opacity = easeProgress; } if (navEl.isSliding) { $el.transform(`translate3d(${offset * (1 - easeProgress)}px,0,0)`); } if (navEl.hasIcon) { if (direction === 'forward') { navEl.$iconEl.transform(`translate3d(${(-offset - navbarWidth) * (1 - easeProgress)}px,0,0)`); } else { navEl.$iconEl.transform(`translate3d(${(-offset + (navbarWidth / 5)) * (1 - easeProgress)}px,0,0)`); } } }); oldNavEls.forEach((navEl) => { const $el = navEl.$el; const offset = direction === 'forward' ? navEl.leftOffset : navEl.rightOffset; if (navEl.needsOpacityTransition) { $el[0].style.opacity = (1 - easeProgress); } if (navEl.isSliding) { $el.transform(`translate3d(${offset * (easeProgress)}px,0,0)`); } if (navEl.hasIcon) { if (direction === 'forward') { navEl.$iconEl.transform(`translate3d(${(-offset + (navbarWidth / 5)) * (easeProgress)}px,0,0)`); } else { navEl.$iconEl.transform(`translate3d(${(-offset - navbarWidth) * (easeProgress)}px,0,0)`); } } }); } } else if (direction === 'forward') { newPage.transform(`translate3d(0, ${(1 - easeProgress) * 56}px,0)`); newPage.css('opacity', easeProgress); } else { oldPage.transform(`translate3d(0, ${easeProgress * 56}px,0)`); oldPage.css('opacity', 1 - easeProgress); } if (done) { onDone(); return; } Utils.requestAnimationFrame(render); } router.$el.addClass(routerTransitionClass); Utils.requestAnimationFrame(render); } animate(...args) { // Args: oldPage, newPage, oldNavbarInner, newNavbarInner, direction, callback const router = this; if (router.params.animateCustom) { router.params.animateCustom.apply(router, args); } else if (router.params.animateWithJS) { router.animateWithJS(...args); } else { router.animateWithCSS(...args); } } removeModal(modalEl) { const router = this; router.removeEl(modalEl); } // eslint-disable-next-line removeTabContent(tabEl) { const $tabEl = $(tabEl); $tabEl.html(''); } removeNavbar(el) { const router = this; router.removeEl(el); } removePage(el) { const $el = $(el); const f7Page = $el && $el[0] && $el[0].f7Page; const router = this; if (f7Page && f7Page.route && f7Page.route.route && f7Page.route.route.keepAlive) { $el.remove(); return; } router.removeEl(el); } removeEl(el) { if (!el) return; const router = this; const $el = $(el); if ($el.length === 0) return; $el.find('.tab').each((tabIndex, tabEl) => { $(tabEl).children().each((index, tabChild) => { if (tabChild.f7Component) { $(tabChild).trigger('tab:beforeremove'); tabChild.f7Component.$destroy(); } }); }); if ($el[0].f7Component && $el[0].f7Component.$destroy) { $el[0].f7Component.$destroy(); } if (!router.params.removeElements) { return; } if (router.params.removeElementsWithTimeout) { setTimeout(() => { $el.remove(); }, router.params.removeElementsTimeout); } else { $el.remove(); } } getPageEl(content) { const router = this; if (typeof content === 'string') { router.tempDom.innerHTML = content; } else { if ($(content).hasClass('page')) { return content; } router.tempDom.innerHTML = ''; $(router.tempDom).append(content); } return router.findElement('.page', router.tempDom); } findElement(stringSelector, container, notStacked) { const router = this; const view = router.view; const app = router.app; // Modals Selector const modalsSelector = '.popup, .dialog, .popover, .actions-modal, .sheet-modal, .login-screen, .page'; const $container = $(container); let selector = stringSelector; if (notStacked) selector += ':not(.stacked)'; let found = $container .find(selector) .filter((index, el) => $(el).parents(modalsSelector).length === 0); if (found.length > 1) { if (typeof view.selector === 'string') { // Search in related view found = $container.find(`${view.selector} ${selector}`); } if (found.length > 1) { // Search in main view found = $container.find(`.${app.params.viewMainClass} ${selector}`); } } if (found.length === 1) return found; // Try to find not stacked if (!notStacked) found = router.findElement(selector, $container, true); if (found && found.length === 1) return found; if (found && found.length > 1) return $(found[0]); return undefined; } flattenRoutes(routes = this.routes) { let flattenedRoutes = []; routes.forEach((route) => { let hasTabRoutes = false; if ('tabs' in route && route.tabs) { const mergedPathsRoutes = route.tabs.map((tabRoute) => { const tRoute = Utils.extend({}, route, { path: (`${route.path}/${tabRoute.path}`).replace('///', '/').replace('//', '/'), parentPath: route.path, tab: tabRoute, }); delete tRoute.tabs; delete tRoute.routes; return tRoute; }); hasTabRoutes = true; flattenedRoutes = flattenedRoutes.concat(this.flattenRoutes(mergedPathsRoutes)); } if ('routes' in route) { const mergedPathsRoutes = route.routes.map((childRoute) => { const cRoute = Utils.extend({}, childRoute); cRoute.path = (`${route.path}/${cRoute.path}`).replace('///', '/').replace('//', '/'); return cRoute; }); if (hasTabRoutes) { flattenedRoutes = flattenedRoutes.concat(this.flattenRoutes(mergedPathsRoutes)); } else { flattenedRoutes = flattenedRoutes.concat(route, this.flattenRoutes(mergedPathsRoutes)); } } if (!('routes' in route) && !('tabs' in route && route.tabs)) { flattenedRoutes.push(route); } }); return flattenedRoutes; } // eslint-disable-next-line parseRouteUrl(url) { if (!url) return {}; const query = Utils.parseUrlQuery(url); const hash = url.split('#')[1]; const params = {}; const path = url.split('#')[0].split('?')[0]; return { query, hash, params, url, path, }; } // eslint-disable-next-line constructRouteUrl(route, { params, query } = {}) { const { path } = route; const toUrl = PathToRegexp.compile(path); let url; try { url = toUrl(params || {}); } catch (error) { throw new Error(`Framework7: error constructing route URL from passed params:\nRoute: ${path}\n${error.toString()}`); } if (query) { if (typeof query === 'string') url += `?${query}`; else url += `?${Utils.serializeObject(query)}`; } return url; } findTabRoute(tabEl) { const router = this; const $tabEl = $(tabEl); const parentPath = router.currentRoute.route.parentPath; const tabId = $tabEl.attr('id'); const flattenedRoutes = router.flattenRoutes(router.routes); let foundTabRoute; flattenedRoutes.forEach((route) => { if ( route.parentPath === parentPath && route.tab && route.tab.id === tabId ) { foundTabRoute = route; } }); return foundTabRoute; } findRouteByKey(key, value) { const router = this; const routes = router.routes; const flattenedRoutes = router.flattenRoutes(routes); let matchingRoute; flattenedRoutes.forEach((route) => { if (matchingRoute) return; if (route[key] === value) { matchingRoute = route; } }); return matchingRoute; } findMatchingRoute(url) { if (!url) return undefined; const router = this; const routes = router.routes; const flattenedRoutes = router.flattenRoutes(routes); const { path, query, hash, params } = router.parseRouteUrl(url); let matchingRoute; flattenedRoutes.forEach((route) => { if (matchingRoute) return; const keys = []; const pathsToMatch = [route.path]; if (route.alias) { if (typeof route.alias === 'string') pathsToMatch.push(route.alias); else if (Array.isArray(route.alias)) { route.alias.forEach((aliasPath) => { pathsToMatch.push(aliasPath); }); } } let matched; pathsToMatch.forEach((pathToMatch) => { if (matched) return; matched = PathToRegexp(pathToMatch, keys).exec(path); }); if (matched) { keys.forEach((keyObj, index) => { if (typeof keyObj.name === 'number') return; const paramValue = matched[index + 1]; params[keyObj.name] = paramValue; }); let parentPath; if (route.parentPath) { parentPath = path.split('/').slice(0, route.parentPath.split('/').length - 1).join('/'); } matchingRoute = { query, hash, params, url, path, parentPath, route, name: route.name, }; } }); return matchingRoute; } // eslint-disable-next-line replaceRequestUrlParams(url = '', options = {}) { let compiledUrl = url; if (typeof compiledUrl === 'string' && compiledUrl.indexOf('{{') >= 0 && options && options.route && options.route.params && Object.keys(options.route.params).length ) { Object.keys(options.route.params).forEach((paramName) => { const regExp = new RegExp(`{{${paramName}}}`, 'g'); compiledUrl = compiledUrl.replace(regExp, options.route.params[paramName] || ''); }); } return compiledUrl; } removeFromXhrCache(url) { const router = this; const xhrCache = router.cache.xhr; let index = false; for (let i = 0; i < xhrCache.length; i += 1) { if (xhrCache[i].url === url) index = i; } if (index !== false) xhrCache.splice(index, 1); } xhrRequest(requestUrl, options) { const router = this; const params = router.params; const { ignoreCache } = options; let url = requestUrl; let hasQuery = url.indexOf('?') >= 0; if (params.passRouteQueryToRequest && options && options.route && options.route.query && Object.keys(options.route.query).length ) { url += `${hasQuery ? '&' : '?'}${Utils.serializeObject(options.route.query)}`; hasQuery = true; } if (params.passRouteParamsToRequest && options && options.route && options.route.params && Object.keys(options.route.params).length ) { url += `${hasQuery ? '&' : '?'}${Utils.serializeObject(options.route.params)}`; hasQuery = true; } if (url.indexOf('{{') >= 0) { url = router.replaceRequestUrlParams(url, options); } // should we ignore get params or not if (params.xhrCacheIgnoreGetParameters && url.indexOf('?') >= 0) { url = url.split('?')[0]; } return Utils.promise((resolve, reject) => { if (params.xhrCache && !ignoreCache && url.indexOf('nocache') < 0 && params.xhrCacheIgnore.indexOf(url) < 0) { for (let i = 0; i < router.cache.xhr.length; i += 1) { const cachedUrl = router.cache.xhr[i]; if (cachedUrl.url === url) { // Check expiration if (Utils.now() - cachedUrl.time < params.xhrCacheDuration) { // Load from cache resolve(cachedUrl.content); return; } } } } router.xhr = router.app.request({ url, method: 'GET', beforeSend(xhr) { router.emit('routerAjaxStart', xhr, options); }, complete(xhr, status) { router.emit('routerAjaxComplete', xhr); if ((status !== 'error' && status !== 'timeout' && (xhr.status >= 200 && xhr.status < 300)) || xhr.status === 0) { if (params.xhrCache && xhr.responseText !== '') { router.removeFromXhrCache(url); router.cache.xhr.push({ url, time: Utils.now(), content: xhr.responseText, }); } router.emit('routerAjaxSuccess', xhr, options); resolve(xhr.responseText); } else { router.emit('routerAjaxError', xhr, options); reject(xhr); } }, error(xhr) { router.emit('routerAjaxError', xhr, options); reject(xhr); }, }); }); } // Remove theme elements removeThemeElements(el) { const router = this; const theme = router.app.theme; $(el).find(`.${theme === 'md' ? 'ios' : 'md'}-only, .if-${theme === 'md' ? 'ios' : 'md'}`).remove(); } templateLoader(template, templateUrl, options, resolve, reject) { const router = this; function compile(t) { let compiledHtml; let context; try { context = options.context || {}; if (typeof context === 'function') context = context.call(router); else if (typeof context === 'string') { try { context = JSON.parse(context); } catch (err) { reject(); throw (err); } } if (typeof t === 'function') { compiledHtml = t(context); } else { compiledHtml = Template7.compile(t)(Utils.extend({}, context || {}, { $app: router.app, $root: Utils.extend({}, router.app.data, router.app.methods), $route: options.route, $router: router, $theme: { ios: router.app.theme === 'ios', md: router.app.theme === 'md', }, })); } } catch (err) { reject(); throw (err); } resolve(compiledHtml, { context }); } if (templateUrl) { // Load via XHR if (router.xhr) { router.xhr.abort(); router.xhr = false; } router .xhrRequest(templateUrl, options) .then((templateContent) => { compile(templateContent); }) .catch(() => { reject(); }); } else { compile(template); } } modalTemplateLoader(template, templateUrl, options, resolve, reject) { const router = this; return router.templateLoader(template, templateUrl, options, (html) => { resolve(html); }, reject); } tabTemplateLoader(template, templateUrl, options, resolve, reject) { const router = this; return router.templateLoader(template, templateUrl, options, (html) => { resolve(html); }, reject); } pageTemplateLoader(template, templateUrl, options, resolve, reject) { const router = this; return router.templateLoader(template, templateUrl, options, (html, newOptions = {}) => { resolve(router.getPageEl(html), newOptions); }, reject); } componentLoader(component, componentUrl, options = {}, resolve, reject) { const router = this; const { app } = router; const url = typeof component === 'string' ? component : componentUrl; const compiledUrl = router.replaceRequestUrlParams(url, options); function compile(componentOptions) { let context = options.context || {}; if (typeof context === 'function') context = context.call(router); else if (typeof context === 'string') { try { context = JSON.parse(context); } catch (err) { reject(); throw (err); } } const extendContext = Utils.merge( {}, context, { $route: options.route, $router: router, $theme: { ios: app.theme === 'ios', md: app.theme === 'md', }, } ); const createdComponent = app.component.create(componentOptions, extendContext); resolve(createdComponent.el); } let cachedComponent; if (compiledUrl) { router.cache.components.forEach((cached) => { if (cached.url === compiledUrl) cachedComponent = cached.component; }); } if (compiledUrl && cachedComponent) { compile(cachedComponent); } else if (compiledUrl && !cachedComponent) { // Load via XHR if (router.xhr) { router.xhr.abort(); router.xhr = false; } router .xhrRequest(url, options) .then((loadedComponent) => { const parsedComponent = app.component.parse(loadedComponent); router.cache.components.push({ url: compiledUrl, component: parsedComponent, }); compile(parsedComponent); }) .catch((err) => { reject(); throw (err); }); } else { compile(component); } } modalComponentLoader(rootEl, component, componentUrl, options, resolve, reject) { const router = this; router.componentLoader(component, componentUrl, options, (el) => { resolve(el); }, reject); } tabComponentLoader(tabEl, component, componentUrl, options, resolve, reject) { const router = this; router.componentLoader(component, componentUrl, options, (el) => { resolve(el); }, reject); } pageComponentLoader(routerEl, component, componentUrl, options, resolve, reject) { const router = this; router.componentLoader(component, componentUrl, options, (el, newOptions = {}) => { resolve(el, newOptions); }, reject); } getPageData(pageEl, navbarEl, from, to, route = {}, pageFromEl) { const router = this; const $pageEl = $(pageEl); const $navbarEl = $(navbarEl); const currentPage = $pageEl[0].f7Page || {}; let direction; let pageFrom; if ((from === 'next' && to === 'current') || (from === 'current' && to === 'previous')) direction = 'forward'; if ((from === 'current' && to === 'next') || (from === 'previous' && to === 'current')) direction = 'backward'; if (currentPage && !currentPage.fromPage) { const $pageFromEl = $(pageFromEl); if ($pageFromEl.length) { pageFrom = $pageFromEl[0].f7Page; } } pageFrom = currentPage.pageFrom || pageFrom; if (pageFrom && pageFrom.pageFrom) { pageFrom.pageFrom = null; } const page = { app: router.app, view: router.view, router, $el: $pageEl, el: $pageEl[0], $pageEl, pageEl: $pageEl[0], $navbarEl, navbarEl: $navbarEl[0], name: $pageEl.attr('data-name'), position: from, from, to, direction, route: currentPage.route ? currentPage.route : route, pageFrom, }; $pageEl[0].f7Page = page; return page; } // Callbacks pageCallback(callback, pageEl, navbarEl, from, to, options = {}, pageFromEl) { if (!pageEl) return; const router = this; const $pageEl = $(pageEl); if (!$pageEl.length) return; const { route } = options; const restoreScrollTopOnBack = router.params.restoreScrollTopOnBack; const keepAlive = $pageEl[0].f7Page && $pageEl[0].f7Page.route && $pageEl[0].f7Page.route.route && $pageEl[0].f7Page.route.route.keepAlive; if (callback === 'beforeRemove' && keepAlive) { callback = 'beforeUnmount'; // eslint-disable-line } const camelName = `page${callback[0].toUpperCase() + callback.slice(1, callback.length)}`; const colonName = `page:${callback.toLowerCase()}`; let page = {}; if (callback === 'beforeRemove' && $pageEl[0].f7Page) { page = Utils.extend($pageEl[0].f7Page, { from, to, position: from }); } else { page = router.getPageData(pageEl, navbarEl, from, to, route, pageFromEl); } page.swipeBack = !!options.swipeBack; const { on = {}, once = {} } = options.route ? options.route.route : {}; if (options.on) { Utils.extend(on, options.on); } if (options.once) { Utils.extend(once, options.once); } function attachEvents() { if ($pageEl[0].f7RouteEventsAttached) return; $pageEl[0].f7RouteEventsAttached = true; if (on && Object.keys(on).length > 0) { $pageEl[0].f7RouteEventsOn = on; Object.keys(on).forEach((eventName) => { on[eventName] = on[eventName].bind(router); $pageEl.on(Utils.eventNameToColonCase(eventName), on[eventName]); }); } if (once && Object.keys(once).length > 0) { $pageEl[0].f7RouteEventsOnce = once; Object.keys(once).forEach((eventName) => { once[eventName] = once[eventName].bind(router); $pageEl.once(Utils.eventNameToColonCase(eventName), once[eventName]); }); } } function detachEvents() { if (!$pageEl[0].f7RouteEventsAttached) return; if ($pageEl[0].f7RouteEventsOn) { Object.keys($pageEl[0].f7RouteEventsOn).forEach((eventName) => { $pageEl.off(Utils.eventNameToColonCase(eventName), $pageEl[0].f7RouteEventsOn[eventName]); }); } if ($pageEl[0].f7RouteEventsOnce) { Object.keys($pageEl[0].f7RouteEventsOnce).forEach((eventName) => { $pageEl.off(Utils.eventNameToColonCase(eventName), $pageEl[0].f7RouteEventsOnce[eventName]); }); } $pageEl[0].f7RouteEventsAttached = null; $pageEl[0].f7RouteEventsOn = null; $pageEl[0].f7RouteEventsOnce = null; delete $pageEl[0].f7RouteEventsAttached; delete $pageEl[0].f7RouteEventsOn; delete $pageEl[0].f7RouteEventsOnce; } if (callback === 'mounted') { attachEvents(); } if (callback === 'init') { if (restoreScrollTopOnBack && (from === 'previous' || !from) && to === 'current' && router.scrollHistory[page.route.url] && !$pageEl.hasClass('no-restore-scroll')) { let $pageContent = $pageEl.find('.page-content'); if ($pageContent.length > 0) { // eslint-disable-next-line $pageContent = $pageContent.filter((pageContentIndex, pageContentEl) => { return ( $(pageContentEl).parents('.tab:not(.tab-active)').length === 0 && !$(pageContentEl).is('.tab:not(.tab-active)') ); }); } $pageContent.scrollTop(router.scrollHistory[page.route.url]); } attachEvents(); if ($pageEl[0].f7PageInitialized) { $pageEl.trigger('page:reinit', page); router.emit('pageReinit', page); return; } $pageEl[0].f7PageInitialized = true; } if (restoreScrollTopOnBack && callback === 'beforeOut' && from === 'current' && to === 'previous') { // Save scroll position let $pageContent = $pageEl.find('.page-content'); if ($pageContent.length > 0) { // eslint-disable-next-line $pageContent = $pageContent.filter((pageContentIndex, pageContentEl) => { return ( $(pageContentEl).parents('.tab:not(.tab-active)').length === 0 && !$(pageContentEl).is('.tab:not(.tab-active)') ); }); } router.scrollHistory[page.route.url] = $pageContent.scrollTop(); } if (restoreScrollTopOnBack && callback === 'beforeOut' && from === 'current' && to === 'next') { // Delete scroll position delete router.scrollHistory[page.route.url]; } $pageEl.trigger(colonName, page); router.emit(camelName, page); if (callback === 'beforeRemove' || callback === 'beforeUnmount') { detachEvents(); if (!keepAlive) { if ($pageEl[0].f7Page && $pageEl[0].f7Page.navbarEl) { delete $pageEl[0].f7Page.navbarEl.f7Page; } $pageEl[0].f7Page = null; } } } saveHistory() { const router = this; router.view.history = router.history; if (router.params.pushState) { window.localStorage[`f7router-${router.view.id}-history`] = JSON.stringify(router.history); } } restoreHistory() { const router = this; if (router.params.pushState && window.localStorage[`f7router-${router.view.id}-history`]) { router.history = JSON.parse(window.localStorage[`f7router-${router.view.id}-history`]); router.view.history = router.history; } } clearHistory() { const router = this; router.history = []; if (router.view) router.view.history = []; router.saveHistory(); } updateCurrentUrl(newUrl) { const router = this; // Update history if (router.history.length) { router.history[router.history.length - 1] = newUrl; } else { router.history.push(newUrl); } // Update current route params const { query, hash, params, url, path } = router.parseRouteUrl(newUrl); if (router.currentRoute) { Utils.extend(router.currentRoute, { query, hash, params, url, path, }); } if (router.params.pushState) { const pushStateRoot = router.params.pushStateRoot || ''; History.replace( router.view.id, { url: newUrl, }, pushStateRoot + router.params.pushStateSeparator + newUrl ); } // Save History router.saveHistory(); router.emit('routeUrlUpdate', router.currentRoute, router); } init() { const router = this; const { app, view } = router; // Init Swipeback if ("universal" !== 'desktop') { if ( (view && router.params.iosSwipeBack && app.theme === 'ios') || (view && router.params.mdSwipeBack && app.theme === 'md') ) { SwipeBack(router); } } // Dynamic not separated navbbar if (router.dynamicNavbar && !router.separateNavbar) { router.$el.addClass('router-dynamic-navbar-inside'); } let initUrl = router.params.url; let documentUrl = document.location.href.split(document.location.origin)[1]; let historyRestored; const { pushState, pushStateOnLoad, pushStateSeparator, pushStateAnimateOnLoad } = router.params; let { pushStateRoot } = router.params; if (window.cordova && pushState && !pushStateSeparator && !pushStateRoot && document.location.pathname.indexOf('index.html')) { // eslint-disable-next-line console.warn('Framework7: wrong or not complete pushState configuration, trying to guess pushStateRoot'); pushStateRoot = document.location.pathname.split('index.html')[0]; } if (!pushState || !pushStateOnLoad) { if (!initUrl) { initUrl = documentUrl; } if (document.location.search && initUrl.indexOf('?') < 0) { initUrl += document.location.search; } if (document.location.hash && initUrl.indexOf('#') < 0) { initUrl += document.location.hash; } } else { if (pushStateRoot && documentUrl.indexOf(pushStateRoot) >= 0) { documentUrl = documentUrl.split(pushStateRoot)[1]; if (documentUrl === '') documentUrl = '/'; } if (pushStateSeparator.length > 0 && documentUrl.indexOf(pushStateSeparator) >= 0) { initUrl = documentUrl.split(pushStateSeparator)[1]; } else { initUrl = documentUrl; } router.restoreHistory(); if (router.history.indexOf(initUrl) >= 0) { router.history = router.history.slice(0, router.history.indexOf(initUrl) + 1); } else if (router.params.url === initUrl) { router.history = [initUrl]; } else if (History.state && History.state[view.id] && History.state[view.id].url === router.history[router.history.length - 1]) { initUrl = router.history[router.history.length - 1]; } else { router.history = [documentUrl.split(pushStateSeparator)[0] || '/', initUrl]; } if (router.history.length > 1) { historyRestored = true; } else { router.history = []; } router.saveHistory(); } let currentRoute; if (router.history.length > 1) { // Will load page currentRoute = router.findMatchingRoute(router.history[0]); if (!currentRoute) { currentRoute = Utils.extend(router.parseRouteUrl(router.history[0]), { route: { url: router.history[0], path: router.history[0].split('?')[0], }, }); } } else { // Don't load page currentRoute = router.findMatchingRoute(initUrl); if (!currentRoute) { currentRoute = Utils.extend(router.parseRouteUrl(initUrl), { route: { url: initUrl, path: initUrl.split('?')[0], }, }); } } if (router.params.stackPages) { router.$el.children('.page').each((index, pageEl) => { const $pageEl = $(pageEl); router.initialPages.push($pageEl[0]); if (router.separateNavbar && $pageEl.children('.navbar').length > 0) { router.initialNavbars.push($pageEl.children('.navbar').find('.navbar-inner')[0]); } }); } if (router.$el.children('.page:not(.stacked)').length === 0 && initUrl) { // No pages presented in DOM, reload new page router.navigate(initUrl, { initial: true, reloadCurrent: true, pushState: false, }); } else { // Init current DOM page let hasTabRoute; router.currentRoute = currentRoute; router.$el.children('.page:not(.stacked)').each((index, pageEl) => { const $pageEl = $(pageEl); let $navbarInnerEl; $pageEl.addClass('page-current'); if (router.separateNavbar) { $navbarInnerEl = $pageEl.children('.navbar').children('.navbar-inner'); if ($navbarInnerEl.length > 0) { if (!router.$navbarEl.parents(document).length) { router.$el.prepend(router.$navbarEl); } router.$navbarEl.append($navbarInnerEl); $pageEl.children('.navbar').remove(); } else { router.$navbarEl.addClass('navbar-hidden'); } } const initOptions = { route: router.currentRoute, }; if (router.currentRoute && router.currentRoute.route && router.currentRoute.route.options) { Utils.extend(initOptions, router.currentRoute.route.options); } router.currentPageEl = $pageEl[0]; if (router.dynamicNavbar && $navbarInnerEl.length) { router.currentNavbarEl = $navbarInnerEl[0]; } router.removeThemeElements($pageEl); if (router.dynamicNavbar && $navbarInnerEl.length) { router.removeThemeElements($navbarInnerEl); } if (initOptions.route.route.tab) { hasTabRoute = true; router.tabLoad(initOptions.route.route.tab, Utils.extend({}, initOptions)); } router.pageCallback('init', $pageEl, $navbarInnerEl, 'current', undefined, initOptions); }); if (historyRestored) { router.navigate(initUrl, { initial: true, pushState: false, history: false, animate: pushStateAnimateOnLoad, once: { pageAfterIn() { if (router.history.length > 2) { router.back({ preload: true }); } }, }, }); } if (!historyRestored && !hasTabRoute) { router.history.push(initUrl); router.saveHistory(); } } if (initUrl && pushState && pushStateOnLoad && (!History.state || !History.state[view.id])) { History.initViewState(view.id, { url: initUrl, }); } router.emit('local::init routerInit', router); } destroy() { let router = this; router.emit('local::destroy routerDestroy', router); // Delete props & methods Object.keys(router).forEach((routerProp) => { router[routerProp] = null; delete router[routerProp]; }); router = null; } } // Load Router.prototype.forward = forward; Router.prototype.load = load; Router.prototype.navigate = navigate; Router.prototype.refreshPage = refreshPage; // Tab Router.prototype.tabLoad = tabLoad; Router.prototype.tabRemove = tabRemove; // Modal Router.prototype.modalLoad = modalLoad; Router.prototype.modalRemove = modalRemove; // Back Router.prototype.backward = backward; Router.prototype.loadBack = loadBack; Router.prototype.back = back; // Clear previoius pages from the DOM Router.prototype.clearPreviousPages = clearPreviousPages; // Clear history Router.prototype.clearPreviousHistory = clearPreviousHistory; export default Router;