UNPKG

framework7

Version:

Full featured mobile HTML framework for building iOS & Android apps

1,268 lines (1,248 loc) 44 kB
import { getWindow, getDocument } from 'ssr-window'; import { pathToRegexp, compile } from 'path-to-regexp'; import $ from '../../shared/dom7.js'; import Framework7Class from '../../shared/class.js'; import { extend, nextFrame, parseUrlQuery, serializeObject, now, eventNameToColonCase } from '../../shared/utils.js'; import History from '../../shared/history.js'; import SwipeBack from './swipe-back.js'; import { refreshPage, navigate } from './navigate.js'; import { tabLoad, tabRemove } from './tab.js'; import { modalLoad, modalRemove } from './modal.js'; import { back } from './back.js'; import { clearPreviousHistory } from './clear-previous-history.js'; import appRouterCheck from './app-router-check.js'; 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 extend(false, router, { app, params: app.params.view, routes: app.routes || [], cache: app.cache }); } else { // View Router extend(false, router, { app, view, viewId: view.id, id: view.params.routerId, params: view.params, routes: view.routes, history: view.history, propsHistory: [], scrollHistory: view.scrollHistory, cache: app.cache, dynamicNavbar: app.theme === 'ios' && view.params.iosDynamicNavbar, initialPages: [], initialNavbars: [] }); } // Install Modules router.useModules(); // AllowPageChage router.allowPageChange = true; // Current Route let currentRoute = {}; let previousRoute = {}; Object.defineProperty(router, 'currentRoute', { enumerable: true, configurable: true, set(newRoute) { if (newRoute === void 0) { newRoute = {}; } previousRoute = 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; } mount() { const router = this; const view = router.view; const document = getDocument(); extend(false, router, { tempDom: document.createElement('div'), $el: view.$el, el: view.el, $navbarsEl: view.$navbarsEl, navbarsEl: view.navbarsEl }); router.emit('local::mount routerMount', router); } animatableNavElements($newNavbarEl, $oldNavbarEl, toLarge, fromLarge, direction) { const router = this; const dynamicNavbar = router.dynamicNavbar; const animateIcon = router.params.iosAnimateNavbarBackIcon; let newNavEls; let oldNavEls; function animatableNavEl($el, $navbarInner) { const isSliding = $el.hasClass('sliding') || $navbarInner.hasClass('sliding'); const isSubnavbar = $el.hasClass('subnavbar'); const needsOpacityTransition = isSliding ? !isSubnavbar : true; const $iconEl = $el.find('.back .icon'); let isIconLabel; if (isSliding && animateIcon && $el.hasClass('left') && $iconEl.length > 0 && $iconEl.next('span').length) { $el = $iconEl.next('span'); // eslint-disable-line isIconLabel = true; } return { $el, isIconLabel, leftOffset: $el[0].f7NavbarLeftOffset, rightOffset: $el[0].f7NavbarRightOffset, isSliding, isSubnavbar, needsOpacityTransition }; } if (dynamicNavbar) { newNavEls = []; oldNavEls = []; $newNavbarEl.children('.navbar-inner').children('.left, .right, .title, .subnavbar').each(navEl => { const $navEl = $(navEl); if ($navEl.hasClass('left') && fromLarge && direction === 'forward') return; if ($navEl.hasClass('title') && toLarge) return; newNavEls.push(animatableNavEl($navEl, $newNavbarEl.children('.navbar-inner'))); }); if (!($oldNavbarEl.hasClass('navbar-master') && router.params.masterDetailBreakpoint > 0 && router.app.width >= router.params.masterDetailBreakpoint)) { $oldNavbarEl.children('.navbar-inner').children('.left, .right, .title, .subnavbar').each(navEl => { const $navEl = $(navEl); if ($navEl.hasClass('left') && toLarge && !fromLarge && direction === 'forward') return; if ($navEl.hasClass('left') && toLarge && direction === 'backward') return; if ($navEl.hasClass('title') && fromLarge) { return; } oldNavEls.push(animatableNavEl($navEl, $oldNavbarEl.children('.navbar-inner'))); }); } [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.isIconLabel) { const iconTextEl = otherNavEl.$el[0]; n.leftOffset += iconTextEl ? iconTextEl.offsetLeft || 0 : 0; } }); }); }); } return { newNavEls, oldNavEls }; } animate($oldPageEl, $newPageEl, $oldNavbarEl, $newNavbarEl, direction, transition, callback) { const router = this; if (router.params.animateCustom) { router.params.animateCustom.apply(router, [$oldPageEl, $newPageEl, $oldNavbarEl, $newNavbarEl, direction, callback]); return; } const dynamicNavbar = router.dynamicNavbar; const ios = router.app.theme === 'ios'; if (transition) { const routerCustomTransitionClass = `router-transition-custom router-transition-${transition}-${direction}`; // Animate const onCustomTransitionDone = () => { router.$el.removeClass(routerCustomTransitionClass); if (dynamicNavbar && router.$navbarsEl.length) { if ($newNavbarEl) { router.$navbarsEl.prepend($newNavbarEl); } if ($oldNavbarEl) { router.$navbarsEl.prepend($oldNavbarEl); } } if (callback) callback(); }; (direction === 'forward' ? $newPageEl : $oldPageEl).animationEnd(onCustomTransitionDone); if (dynamicNavbar) { if ($newNavbarEl && $newPageEl) { router.setNavbarPosition($newNavbarEl, ''); $newNavbarEl.removeClass('navbar-next navbar-previous navbar-current'); $newPageEl.prepend($newNavbarEl); } if ($oldNavbarEl && $oldPageEl) { router.setNavbarPosition($oldNavbarEl, ''); $oldNavbarEl.removeClass('navbar-next navbar-previous navbar-current'); $oldPageEl.prepend($oldNavbarEl); } } router.$el.addClass(routerCustomTransitionClass); return; } // Router Animation class const routerTransitionClass = `router-transition-${direction} router-transition`; let newNavEls; let oldNavEls; let fromLarge; let toLarge; let toDifferent; let oldIsLarge; let newIsLarge; if (ios && dynamicNavbar) { const betweenMasterAndDetail = router.params.masterDetailBreakpoint > 0 && router.app.width >= router.params.masterDetailBreakpoint && ($oldNavbarEl.hasClass('navbar-master') && $newNavbarEl.hasClass('navbar-master-detail') || $oldNavbarEl.hasClass('navbar-master-detail') && $newNavbarEl.hasClass('navbar-master')); if (!betweenMasterAndDetail) { oldIsLarge = $oldNavbarEl && $oldNavbarEl.hasClass('navbar-large'); newIsLarge = $newNavbarEl && $newNavbarEl.hasClass('navbar-large'); fromLarge = oldIsLarge && !$oldNavbarEl.hasClass('navbar-large-collapsed'); toLarge = newIsLarge && !$newNavbarEl.hasClass('navbar-large-collapsed'); toDifferent = fromLarge && !toLarge || toLarge && !fromLarge; } const navEls = router.animatableNavElements($newNavbarEl, $oldNavbarEl, toLarge, fromLarge, direction); newNavEls = navEls.newNavEls; oldNavEls = navEls.oldNavEls; } function animateNavbars(progress) { if (!(ios && dynamicNavbar)) return; if (progress === 1) { if (toLarge) { $newNavbarEl.addClass('router-navbar-transition-to-large'); $oldNavbarEl.addClass('router-navbar-transition-to-large'); } if (fromLarge) { $newNavbarEl.addClass('router-navbar-transition-from-large'); $oldNavbarEl.addClass('router-navbar-transition-from-large'); } } newNavEls.forEach(navEl => { const $el = navEl.$el; const offset = direction === 'forward' ? navEl.rightOffset : navEl.leftOffset; if (navEl.isSliding) { if (navEl.isSubnavbar && newIsLarge) { // prettier-ignore $el[0].style.setProperty('transform', `translate3d(${offset * (1 - progress)}px, calc(-1 * var(--f7-navbar-large-collapse-progress) * var(--f7-navbar-large-title-height)), 0)`, 'important'); } else { $el.transform(`translate3d(${offset * (1 - progress)}px,0,0)`); } } }); oldNavEls.forEach(navEl => { const $el = navEl.$el; const offset = direction === 'forward' ? navEl.leftOffset : navEl.rightOffset; if (navEl.isSliding) { if (navEl.isSubnavbar && oldIsLarge) { $el.transform(`translate3d(${offset * progress}px, calc(-1 * var(--f7-navbar-large-collapse-progress) * var(--f7-navbar-large-title-height)), 0)`); } else { $el.transform(`translate3d(${offset * progress}px,0,0)`); } } }); } // AnimationEnd Callback function onDone() { if (router.dynamicNavbar) { if ($newNavbarEl) { $newNavbarEl.removeClass('router-navbar-transition-to-large router-navbar-transition-from-large'); $newNavbarEl.addClass('navbar-no-title-large-transition'); nextFrame(() => { $newNavbarEl.removeClass('navbar-no-title-large-transition'); }); } if ($oldNavbarEl) { $oldNavbarEl.removeClass('router-navbar-transition-to-large router-navbar-transition-from-large'); } if ($newNavbarEl.hasClass('sliding') || $newNavbarEl.children('.navbar-inner.sliding').length) { $newNavbarEl.find('.title, .left, .right, .left .icon, .subnavbar').transform(''); } else { $newNavbarEl.find('.sliding').transform(''); } if ($oldNavbarEl.hasClass('sliding') || $oldNavbarEl.children('.navbar-inner.sliding').length) { $oldNavbarEl.find('.title, .left, .right, .left .icon, .subnavbar').transform(''); } else { $oldNavbarEl.find('.sliding').transform(''); } } router.$el.removeClass(routerTransitionClass); if (callback) callback(); } // eslint-disable-next-line (direction === 'forward' ? $newPageEl : ios ? $oldPageEl : $newPageEl).animationEnd(() => { onDone(); }); // Animate if (dynamicNavbar) { // Prepare Navbars animateNavbars(0); nextFrame(() => { // Add class, start animation router.$el.addClass(routerTransitionClass); if (toDifferent) { // eslint-disable-next-line router.el._clientLeft = router.el.clientLeft; } animateNavbars(1); }); } else { // Add class, start animation router.$el.addClass(routerTransitionClass); } } 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(tabEl => { $(tabEl).children().each(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) { 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); const selector = stringSelector; let found = $container.find(selector).filter(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; found = router.findElement(selector, $container); if (found && found.length === 1) return found; if (found && found.length > 1) return $(found[0]); return undefined; } flattenRoutes(routes) { if (routes === void 0) { routes = this.routes; } const router = this; let flattenedRoutes = []; routes.forEach(route => { let hasTabRoutes = false; if ('tabs' in route && route.tabs) { const mergedPathsRoutes = route.tabs.map(tabRoute => { const tRoute = 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(router.flattenRoutes(mergedPathsRoutes)); } if ('detailRoutes' in route) { const mergedPathsRoutes = route.detailRoutes.map(detailRoute => { const dRoute = extend({}, detailRoute); dRoute.masterRoute = route; dRoute.masterRoutePath = route.path; return dRoute; }); flattenedRoutes = flattenedRoutes.concat(route, router.flattenRoutes(mergedPathsRoutes)); } if ('routes' in route) { const mergedPathsRoutes = route.routes.map(childRoute => { const cRoute = extend({}, childRoute); cRoute.path = `${route.path}/${cRoute.path}`.replace('///', '/').replace('//', '/'); return cRoute; }); if (hasTabRoutes) { flattenedRoutes = flattenedRoutes.concat(router.flattenRoutes(mergedPathsRoutes)); } else { flattenedRoutes = flattenedRoutes.concat(route, router.flattenRoutes(mergedPathsRoutes)); } } if (!('routes' in route) && !('tabs' in route && route.tabs) && !('detailRoutes' in route)) { flattenedRoutes.push(route); } }); return flattenedRoutes; } // eslint-disable-next-line parseRouteUrl(url) { if (!url) return {}; const query = parseUrlQuery(url); const hash = url.split('#')[1]; const params = {}; const path = url.split('#')[0].split('?')[0]; return { query, hash, params, url, path }; } generateUrl(parameters) { if (parameters === void 0) { parameters = {}; } if (typeof parameters === 'string') { return parameters; } const { name, path, params, query } = parameters; if (!name && !path) { throw new Error('Framework7: "name" or "path" parameter is required'); } const router = this; const route = name ? router.findRouteByKey('name', name) : router.findRouteByKey('path', path); if (!route) { if (name) { throw new Error(`Framework7: route with name "${name}" not found`); } else { throw new Error(`Framework7: route with path "${path}" not found`); } } const url = router.constructRouteUrl(route, { params, query }); if (url === '') { return '/'; } if (!url) { throw new Error(`Framework7: can't construct URL for route with name "${name}"`); } return url; } // eslint-disable-next-line constructRouteUrl(route, _temp) { let { params, query } = _temp === void 0 ? {} : _temp; const { path } = route; const toUrl = 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 if (Object.keys(query).length) url += `?${serializeObject(query)}`; } return url; } findTabRouteUrl(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 foundTabRouteUrl; flattenedRoutes.forEach(route => { if (route.parentPath === parentPath && route.tab && route.tab.id === tabId) { if (router.currentRoute.params && Object.keys(router.currentRoute.params).length > 0) { foundTabRouteUrl = router.constructRouteUrl(route, { params: router.currentRoute.params, query: router.currentRoute.query }); } else { foundTabRouteUrl = route.path; } } }); return foundTabRouteUrl; } 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]; if (typeof paramValue === 'undefined' || paramValue === null) { params[keyObj.name] = paramValue; } else { params[keyObj.name] = decodeURIComponent(paramValue); } }); let parentPath; if (route.parentPath) { parentPath = (path || '/').split('/').slice(0, route.parentPath.split('/').length - 1).join('/'); } matchingRoute = { query, hash, params, url, path: path || '/', parentPath, route, name: route.name }; } }); return matchingRoute; } // eslint-disable-next-line replaceRequestUrlParams(url, options) { if (url === void 0) { url = ''; } if (options === void 0) { 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 ? '&' : '?'}${serializeObject(options.route.query)}`; hasQuery = true; } if (params.passRouteParamsToRequest && options && options.route && options.route.params && Object.keys(options.route.params).length) { url += `${hasQuery ? '&' : '?'}${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 new 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 (now() - cachedUrl.time < params.xhrCacheDuration) { // Load from cache resolve(cachedUrl.content); return; } } } } router.xhrAbortController = new AbortController(); let fetchRes; fetch(url, { signal: router.xhrAbortController.signal, method: 'GET' }).then(res => { fetchRes = res; return res.text(); }).then(responseText => { const { status } = fetchRes; router.emit('routerAjaxComplete', fetchRes); if (status !== 'error' && status !== 'timeout' && status >= 200 && status < 300 || status === 0) { if (params.xhrCache && responseText !== '') { router.removeFromXhrCache(url); router.cache.xhr.push({ url, time: now(), content: responseText }); } router.emit('routerAjaxSuccess', fetchRes, options); resolve(responseText); } else { router.emit('routerAjaxError', fetchRes, options); reject(fetchRes); } }).catch(err => { reject(err); }); }); } setNavbarPosition($el, position, ariaHidden) { const router = this; $el.removeClass('navbar-previous navbar-current navbar-next'); if (position) { $el.addClass(`navbar-${position}`); } if (ariaHidden === false) { $el.removeAttr('aria-hidden'); } else if (ariaHidden === true) { $el.attr('aria-hidden', 'true'); } $el.trigger('navbar:position', { position }); router.emit('navbarPosition', $el[0], position); } setPagePosition($el, position, ariaHidden) { const router = this; $el.removeClass('page-previous page-current page-next'); $el.addClass(`page-${position}`); if (ariaHidden === false) { $el.removeAttr('aria-hidden'); } else if (ariaHidden === true) { $el.attr('aria-hidden', 'true'); } $el.trigger('page:position', { position }); router.emit('pagePosition', $el[0], position); } // Remove theme elements removeThemeElements(el) { const router = this; const theme = router.app.theme; let toRemove; if (theme === 'ios') { toRemove = '.md-only, .if-md, .if-not-ios, .not-ios'; } else if (theme === 'md') { toRemove = '.ios-only, .if-ios, .if-not-md, .not-md'; } $(el).find(toRemove).remove(); } getPageData(pageEl, navbarEl, from, to, route, pageFromEl) { if (route === void 0) { route = {}; } const router = this; const $pageEl = $(pageEl).eq(0); const $navbarEl = $(navbarEl).eq(0); 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 (options === void 0) { options = {}; } if (!pageEl) return; const router = this; const $pageEl = $(pageEl); if (!$pageEl.length) return; const $navbarEl = $(navbarEl); const { route } = options; const restoreScrollTopOnBack = router.params.restoreScrollTopOnBack && !(router.params.masterDetailBreakpoint > 0 && $pageEl.hasClass('page-master') && router.app.width >= router.params.masterDetailBreakpoint); 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 = extend($pageEl[0].f7Page, { from, to, position: from }); } else { page = router.getPageData($pageEl[0], $navbarEl[0], from, to, route, pageFromEl); } page.swipeBack = !!options.swipeBack; const { on = {}, once = {} } = options.route ? options.route.route : {}; if (options.on) { extend(on, options.on); } if (options.once) { 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(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(eventNameToColonCase(eventName), once[eventName]); }); } } function detachEvents() { if (!$pageEl[0].f7RouteEventsAttached) return; if ($pageEl[0].f7RouteEventsOn) { Object.keys($pageEl[0].f7RouteEventsOn).forEach(eventName => { $pageEl.off(eventNameToColonCase(eventName), $pageEl[0].f7RouteEventsOn[eventName]); }); } if ($pageEl[0].f7RouteEventsOnce) { Object.keys($pageEl[0].f7RouteEventsOnce).forEach(eventName => { $pageEl.off(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(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(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; const window = getWindow(); router.view.history = router.history; if (router.params.browserHistory && router.params.browserHistoryStoreHistory && window.localStorage) { window.localStorage[`f7router-${router.view.id}-history`] = JSON.stringify(router.history); } } restoreHistory() { const router = this; const window = getWindow(); if (router.params.browserHistory && router.params.browserHistoryStoreHistory && window.localStorage && 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; appRouterCheck(router, 'updateCurrentUrl'); // 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) { extend(router.currentRoute, { query, hash, params, url, path }); } if (router.params.browserHistory) { const browserHistoryRoot = router.params.browserHistoryRoot || ''; History.replace(router.view.id, { url: newUrl }, browserHistoryRoot + router.params.browserHistorySeparator + newUrl); } // Save History router.saveHistory(); router.emit('routeUrlUpdate', router.currentRoute, router); } getInitialUrl() { const router = this; if (router.initialUrl) { return { initialUrl: router.initialUrl, historyRestored: router.historyRestored }; } const { app, view } = router; const document = getDocument(); const window = getWindow(); const location = app.params.url && typeof app.params.url === 'string' && typeof URL !== 'undefined' ? new URL(app.params.url) : document.location; let initialUrl = router.params.url; let documentUrl = location.href.split(location.origin)[1]; let historyRestored; const { browserHistory, browserHistoryOnLoad, browserHistorySeparator } = router.params; let { browserHistoryRoot } = router.params; if ((window.cordova || window.Capacitor && window.Capacitor.isNative) && browserHistory && !browserHistorySeparator && !browserHistoryRoot && location.pathname.indexOf('index.html')) { // eslint-disable-next-line console.warn('Framework7: wrong or not complete browserHistory configuration, trying to guess browserHistoryRoot'); browserHistoryRoot = location.pathname.split('index.html')[0]; } if (!browserHistory || !browserHistoryOnLoad) { if (!initialUrl) { initialUrl = documentUrl; } if (location.search && initialUrl.indexOf('?') < 0) { initialUrl += location.search; } if (location.hash && initialUrl.indexOf('#') < 0) { initialUrl += location.hash; } } else { if (browserHistoryRoot && documentUrl.indexOf(browserHistoryRoot) >= 0) { documentUrl = documentUrl.substring(documentUrl.indexOf(browserHistoryRoot) + browserHistoryRoot.length); if (documentUrl === '') documentUrl = '/'; } if (browserHistorySeparator.length > 0 && documentUrl.indexOf(browserHistorySeparator) >= 0) { initialUrl = documentUrl.substring(documentUrl.indexOf(browserHistorySeparator) + browserHistorySeparator.length); } else { initialUrl = documentUrl; } router.restoreHistory(); if (router.history.indexOf(initialUrl) >= 0) { router.history = router.history.slice(0, router.history.indexOf(initialUrl) + 1); } else if (router.params.url === initialUrl) { router.history = [initialUrl]; } else if (History.state && History.state[view.id] && History.state[view.id].url === router.history[router.history.length - 1]) { initialUrl = router.history[router.history.length - 1]; } else { router.history = [documentUrl.split(browserHistorySeparator)[0] || '/', initialUrl]; } if (router.history.length > 1) { historyRestored = true; } else { router.history = []; } router.saveHistory(); } router.initialUrl = initialUrl; router.historyRestored = historyRestored; return { initialUrl, historyRestored }; } init() { const router = this; const { app, view } = router; const document = getDocument(); router.mount(); const { initialUrl, historyRestored } = router.getInitialUrl(); // Init Swipeback if (view && router.params.iosSwipeBack && app.theme === 'ios' || view && router.params.mdSwipeBack && app.theme === 'md') { SwipeBack(router); } const { browserHistory, browserHistoryOnLoad, browserHistoryAnimateOnLoad, browserHistoryInitialMatch } = router.params; let currentRoute; if (router.history.length > 1) { // Will load page const initUrl = browserHistoryInitialMatch ? initialUrl : router.history[0]; currentRoute = router.findMatchingRoute(initUrl); if (!currentRoute) { currentRoute = extend(router.parseRouteUrl(initUrl), { route: { url: initUrl, path: initUrl.split('?')[0] } }); } } else { // Don't load page currentRoute = router.findMatchingRoute(initialUrl); if (!currentRoute) { currentRoute = extend(router.parseRouteUrl(initialUrl), { route: { url: initialUrl, path: initialUrl.split('?')[0] } }); } } if (router.$el.children('.page').length === 0 && initialUrl && router.params.loadInitialPage) { // No pages presented in DOM, reload new page router.navigate(initialUrl, { initial: true, reloadCurrent: true, browserHistory: false, animate: false, once: { modalOpen() { if (!historyRestored) return; const preloadPreviousPage = router.params.preloadPreviousPage || router.params[`${app.theme}SwipeBack`]; if (preloadPreviousPage && router.history.length > 1) { router.back({ preload: true }); } }, pageAfterIn() { if (!historyRestored) return; const preloadPreviousPage = router.params.preloadPreviousPage || router.params[`${app.theme}SwipeBack`]; if (preloadPreviousPage && router.history.length > 1) { router.back({ preload: true }); } } } }); } else if (router.$el.children('.page').length) { // Init current DOM page let hasTabRoute; router.currentRoute = currentRoute; router.$el.children('.page').each(pageEl => { const $pageEl = $(pageEl); let $navbarEl; router.setPagePosition($pageEl, 'current'); if (router.dynamicNavbar) { $navbarEl = $pageEl.children('.navbar'); if ($navbarEl.length > 0) { if (!router.$navbarsEl.parents(document).length) { router.$el.prepend(router.$navbarsEl); } router.setNavbarPosition($navbarEl, 'current'); router.$navbarsEl.append($navbarEl); if ($navbarEl.children('.title-large').length) { $navbarEl.addClass('navbar-large'); } $pageEl.children('.navbar').remove(); } else { router.$navbarsEl.addClass('navbar-hidden'); if ($navbarEl.children('.title-large').length) { router.$navbarsEl.addClass('navbar-hidden navbar-large-hidden'); } } } if (router.currentRoute && router.currentRoute.route && (router.currentRoute.route.master === true || typeof router.currentRoute.route.master === 'function' && router.currentRoute.route.master(app, router)) && router.params.masterDetailBreakpoint > 0) { $pageEl.addClass('page-master'); $pageEl.trigger('page:role', { role: 'master' }); if ($navbarEl && $navbarEl.length) { $navbarEl.addClass('navbar-master'); } view.checkMasterDetailBreakpoint(); } const initOptions = { route: router.currentRoute }; if (router.currentRoute && router.currentRoute.route && router.currentRoute.route.options) { extend(initOptions, router.currentRoute.route.options); } router.currentPageEl = $pageEl[0]; if (router.dynamicNavbar && $navbarEl.length) { router.currentNavbarEl = $navbarEl[0]; } router.removeThemeElements($pageEl); if (router.dynamicNavbar && $navbarEl.length) { router.removeThemeElements($navbarEl); } if (initOptions.route.route.tab) { hasTabRoute = true; router.tabLoad(initOptions.route.route.tab, extend({}, initOptions)); } router.pageCallback('init', $pageEl, $navbarEl, 'current', undefined, initOptions); router.pageCallback('beforeIn', $pageEl, $navbarEl, 'current', undefined, initOptions); router.pageCallback('afterIn', $pageEl, $navbarEl, 'current', undefined, initOptions); }); if (historyRestored) { if (browserHistoryInitialMatch) { const preloadPreviousPage = router.params.preloadPreviousPage || router.params[`${app.theme}SwipeBack`]; if (preloadPreviousPage && router.history.length > 1) { router.back({ preload: true }); } } else { router.navigate(initialUrl, { initial: true, browserHistory: false, history: false, animate: browserHistoryAnimateOnLoad, once: { pageAfterIn() { const preloadPreviousPage = router.params.preloadPreviousPage || router.params[`${app.theme}SwipeBack`]; if (preloadPreviousPage && router.history.length > 2) { router.back({ preload: true }); } } } }); } } if (!historyRestored && !hasTabRoute) { router.history.push(initialUrl); router.saveHistory(); } } if (initialUrl && browserHistory && browserHistoryOnLoad && (!History.state || !History.state[view.id])) { History.initViewState(view.id, { url: initialUrl }); } 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.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.back = back; // Clear history Router.prototype.clearPreviousHistory = clearPreviousHistory; export default Router;