UNPKG

@stilljs/core

Version:

<div style="display:flex; justify-content: center"> <img src="@still/img/logo-no-bg.png" style="width: 5em;"/> </div>

603 lines (490 loc) 22.4 kB
import { StillAppSetup } from "../../config/app-setup.js"; import { AppTemplate } from "../../config/app-template.js"; import { stillRoutesMap as DefaultstillRoutesMap } from "../../config/route.map.js"; import { $still, ComponentRegistror } from "../component/manager/registror.js"; import { BaseComponent } from "../component/super/BaseComponent.js"; import { Components } from "../setup/components.js"; import { $stillconst, authErrorMessage, ST_UNAUTHOR_ID } from "../setup/constants.js"; import { UUIDUtil } from "../util/UUIDUtil.js"; import { getRoutesFile } from "../util/route.js"; const stillRoutesMap = await getRoutesFile(DefaultstillRoutesMap); const GotoParams = { data: {}, url: true, evt: { containerId: null } } export class Router { static routeMap; static baseUrl = window.location.href.replace('#', ''); #data = {}; static instance = null; static appPlaceholder = $stillconst.APP_PLACEHOLDER; static initRouting = false; static importedMap = {}; static navigatingView = null; static navigatingUrl = null; static urlParams = {}; /** clickEvetCntrId only takes place when it comes to lone component so that * it can identify the context which an event (e.g. Navigation) occurred */ static clickEvetCntrId = null; static preView = null; static navCounter = 0; static serviceId = null; /** @returns { Router } */ static getInstance() { if (!Router.instance) Router.instance = new Router(); return Router.instance; } static init() { StillAppSetup.get().loadComponent(); AppTemplate.get().storageSet('stAppInitStatus', true); Router.initRouting = true; } static data = (cmp) => Router.getInstance().#data[cmp.getName()]; /** @param {String} data */ static aliasGoto(cmp, data, url = false, containerId = null) { if (!url) Router.clearUrlPath(); if (data.startsWith($stillconst.RT_DT_PREFIX)) { data = Router.getInstance().#data[data]; delete Router.getInstance().#data[data]; } Router.goto(cmp, { data, url, evt: { containerId } }); } /** * * @param {String} data */ static aliasGoto1(cmp, url = false, containerId = null) { if (!url) Router.clearUrlPath(); Router.goto(cmp, { data: {}, url, evt: { containerId } }); } static initNavigation(cmp) { Router.clickEvetCntrId = null; Router.initRouting = false; Router.preView = $still.context.currentView; Components.setRemovingPartsVersionId($still.context.currentView?.versionId); Router.navCounter = Router.navCounter + 1; return Router.handleViewType(cmp); } /** * * @param {*} cmp * @param {{data, path}} param1 */ static goto(cmp, params = GotoParams) { const { data, evt, url } = params; cmp = Router.initNavigation(cmp); if (evt?.containerId) Router.clickEvetCntrId = evt.containerId; /** * The or (||) conditions serves to mount the application so the user can * be redirected straight to a specific page/page-component instead of being * forced to go to the main/home UI after the login, as the page is not rendered * in case the app was not loaded through StillAppSetup.get().loadComponent() */ if ( cmp === 'init' || (AppTemplate.get().isAuthN() && !StillAppSetup.get().isAppLoaded()) ) { StillAppSetup.get().loadComponent(); AppTemplate.get().storageSet('stAppInitStatus', true); Router.initRouting = true; } if (cmp === 'exit') return AppTemplate.get().unloadApp(); Router.getInstance().#data = null; if (data != '') { Router.getInstance().#data = {}; if (data instanceof Object) { if (Object.keys(data).length) Router.getInstance().#data[cmp] = data; } else Router.getInstance().#data[cmp] = data; } const routeInstance = { route: { ...stillRoutesMap.viewRoutes.lazyInitial, ...stillRoutesMap.viewRoutes.regular } } const route = routeInstance.route[cmp]?.path; const cmpRegistror = $still.context.componentRegistror.componentList; const cmpInstance = cmpRegistror[cmp]?.instance const isHomeCmp = StillAppSetup.get().entryComponentName == cmp; const isLoneCmp = Router.clickEvetCntrId != null && Router.clickEvetCntrId != 'null'; if (isHomeCmp && isLoneCmp) { if (cmp in cmpRegistror) { $still.context.currentView = cmpInstance; if ( (!AppTemplate.get().isAuthN() && !cmpInstance.isPublic) || !Components.obj().isInWhiteList(cmpInstance) ) document.write(authErrorMessage()); Router.getAndDisplayPage($still.context.currentView, true, isHomeCmp); } else { (async () => { const appTemplate = AppTemplate.get().template; const { newInstance } = await ( await Components.produceComponent({ cmp, loneCntrId: Router.clickEvetCntrId }) ); $still.context.currentView = newInstance; if ((!AppTemplate.get().isAuthN() && !$still.context.currentView.isPublic) || !Components.obj().isInWhiteList(newInstance) ) document.write(authErrorMessage()); if ($still.context.currentView.template == undefined) return Router.cmpTemplateNotDefinedCheck(cmp); let template = (new Components()).getHomeCmpTemplate($still.context.currentView); template = appTemplate.replace( $stillconst.STILL_COMPONENT, ` <div id="${Router.appPlaceholder}" class="${$stillconst.TOP_LEVEL_CMP}">${template}</div> ` ); const ctnrId = isLoneCmp ? Router.clickEvetCntrId : 'stillUiPlaceholder'; document.getElementById(ctnrId).innerHTML = template; setTimeout(() => Router.callCmpAfterInit(null, isHomeCmp, Router.appPlaceholder)); })(); } } else { (async () => { if (!Router.importedMap[cmp]) { if (cmp == 'init') return; if (cmp instanceof Object) if ('address' in cmp) cmp = cmp.address; const wasPrevLoaded = Components.prevLoadingTracking.has(cmp); /** the bellow line clears previous component from memory * @type { ViewComponent } */ const { newInstance } = await ( await Components.produceComponent({ cmp, loneCntrId: Router.clickEvetCntrId }) ); AppTemplate.get().storageSet('stAppInitStatus', true); if (newInstance.template == undefined) return Router.cmpTemplateNotDefinedCheck(cmp); if (newInstance.isPublic) { if (!AppTemplate.get().isAuthN()) { if (!Components.obj().isInWhiteList(newInstance)) return document.write(authErrorMessage()); if (url) Router.updateUrlPath(cmp); return (new Components()).renderPublicComponent(newInstance); } } ComponentRegistror.add(cmp, newInstance); const isWhiteListed = Components.obj().isInWhiteList(newInstance); if (!document.getElementById($stillconst.APP_PLACEHOLDER) && !newInstance.isPublic && isWhiteListed ) return document.write(authErrorMessage()); newInstance.isRoutable = true; if (!wasPrevLoaded && !newInstance.lone) if (!Router.importedMap[cmp]) Router.parseComponent(newInstance); newInstance.setRoutableCmp(true); if (isHomeCmp) newInstance.setUUID($stillconst.TOP_LEVEL_CMP); $still.context.currentView = newInstance; } else { const oldInstance = cmpRegistror[cmp]?.instance; $still.context.currentView = await ( await Components.produceComponent({ cmp }) ).newInstance; if (oldInstance?.cmpInternalId) $still.context.currentView.cmpInternalId = oldInstance.cmpInternalId; $still.context.currentView.isRoutable = true; } Router.getAndDisplayPage($still.context.currentView, Router.importedMap[cmp]); })() } if (url) Router.updateUrlPath(cmp); } static updateUrlPath(cmp) { let routeName = cmp; if (cmp instanceof Object) routeName = cmp.address; const newPath = Router.routeMap[routeName].url; window.history.pushState(null, null, '#/'); window.history.pushState(null, null, '#' + newPath); } static clearUrlPath() { window.history.pushState(null, null, '#/'); } static replaceUrlPath(path) { window.history.pushState(null, null, '#/'); window.history.pushState(null, null, '#' + path); Router.navigatingUrl = path; } /** * 1. Add new method for dynamic instantiation * 2. Add getter and setters for the components fields * @param { ViewComponent } cmp */ static parseComponent(cmp) { if (!cmp.setAndGetsParsed) { cmp.setAndGetsParsed = true; setTimeout(() => { (new Components).getNewParsedComponent(cmp); }); } } /** * the bellow line clears previous component from memory * @param { ViewComponent } cmp */ static getAndDisplayPage(cmp, isReRender = false, isHome = false) { const appCntrId = Router.appPlaceholder, isPrivate = !cmp.isPublic; let appPlaceholder = document.getElementById(appCntrId), soleRouting; const isLoneCmp = Router.clickEvetCntrId != null && Router.clickEvetCntrId != 'null'; if (isLoneCmp) { appPlaceholder = document.getElementById(Router.clickEvetCntrId); soleRouting = true; } const cmpId = cmp.getUUID(), cmpName = cmp.constructor.name; if (isReRender || isLoneCmp) { Components .unloadLoadedComponent(soleRouting && appPlaceholder) .then(async () => { Router.handleUnauthorizeIfPresent(); if (Router.noPermAccessProcess(isPrivate, appPlaceholder, cmp)) return; if (cmp.subImported) { const pageContent = ` <output id="${cmpId}-check" class="cmp-name-page-view-${cmpName}" style="display:contents;"> ${cmp.getTemplate()} </output>`; appPlaceholder.insertAdjacentHTML('afterbegin', pageContent); cmp.subImported = false; setTimeout(() => { cmp.parseOnChange(); }, 500); await cmp.onRender(); } else { await Components.reloadedComponent(cmp, isHome); } setTimeout(() => Router.callCmpAfterInit(`${cmpId}-check`, isHome)); }); } else { Components .unloadLoadedComponent(soleRouting && appPlaceholder) .then(async () => { Router.handleUnauthorizeIfPresent(); if (Router.noPermAccessProcess(isPrivate, appPlaceholder, cmp)) return; if (!appPlaceholder && cmp?.isPublic) { appPlaceholder = document.getElementById($stillconst.UI_PLACEHOLDER); } const pageContent = ` <output id="${cmpId}-check" class="cmp-name-page-view-${cmpName}" style="display:contents;"> ${cmp.getTemplate()} </output>`; appPlaceholder.insertAdjacentHTML('afterbegin', pageContent); setTimeout(() => cmp.parseOnChange(), 500); setTimeout(() => { if (!cmp.setAndGetsParsed) { cmp.setAndGetsParsed = true; (new Components) .parseGetsAndSets( ComponentRegistror.component(cmp.cmpInternalId) ) } }, 10); await cmp.onRender(); setTimeout(() => cmp.$stillLoadCounter = cmp.$stillLoadCounter + 1, 100); setTimeout(() => Router.callCmpAfterInit(`${cmpId}-check`)); Router.importedMap[cmpName] = true; ComponentRegistror.add(cmp.getUUID(), cmp); }); } } static callCmpAfterInit(cmpId, isHome, appPlaceholder = null) { /** * Timer for keep calling the function wrapped code until it finds that the main * component was loaded and proceeding compute (e.g. load subcomponent) can happen */ let cmpRef = appPlaceholder; if (cmpRef == null) cmpRef = isHome ? $stillconst.TOP_LEVEL_CMP : cmpId; const loadTImer = setTimeout(async () => { /** * Check if the main component was loaded/rendered */ if (document.getElementById(cmpRef)) { clearTimeout(loadTImer); /** @type { ViewComponent } */ const cmp = $still.context.currentView; /** * Runs stAfterInit special method in case it exists */ if (!Components.checkStInit(cmp.constructor.name)) setTimeout(() => Components.runAfterInit(cmp), 200); /** * Load component parts or sub-components inside the main loaded component * if(!Components.stAppInitStatus) is to prevent compoenent parts Parsing * When this is called in the App mounting phase, as this (handleInPlaceParts) * has been handled previously on the Components Funamentals (components.js) * by already calling Components.handleInPlaceParts($still.context.currentView)) */ if ( (!Components.stAppInitStatus || AppTemplate.get().storageGet('stAppInitStatus')) && !Router.initRouting ) { Components.handleInPlaceParts(cmp); } else if ( ( (Components.stAppInitStatus) && StillAppSetup.get().entryComponentName != cmp?.getName() ) || appPlaceholder != null ) { Components.handleInPlaceParts(cmp); } else { Components.stAppInitStatus = false; } } }, 200); Components.removeOldParts(); } static cmpTemplateNotDefinedCheck(cmpName) { document.write($stillconst.NO_TEMPLATE.replace('{{}}', cmpName)); } static handleViewType(cmp) { if ((cmp.prototype instanceof ViewComponent) || (cmp.prototype instanceof BaseComponent)) cmp = cmp.stName; Router.navigatingView = cmp; return cmp; } static routingDataParse(data) { if (data instanceof Array || data instanceof Object) { const hash = `${$stillconst.RT_DT_PREFIX}${UUIDUtil.newId()}`; Router.getInstance().#data[hash] = data; return hash; } return data; } static handleUnauthorizeIfPresent() { const unauthorizeContent = document.getElementById(ST_UNAUTHOR_ID); if (unauthorizeContent) { const parent = unauthorizeContent.parentElement; parent.removeChild(unauthorizeContent); } } static noPermAccessProcess(isPrivate, appPlaceholder, cmp) { const isUnauthorized = isPrivate && !AppTemplate.get().isAuthN(); Router.handleUnauthorizeIfPresent(); if (isUnauthorized && !Components.obj().isInWhiteList(cmp)) { appPlaceholder.insertAdjacentHTML('afterbegin', authErrorMessage()); return true; } return false; } static getUrlPath() { let path = window.location.hash.replace('#', ''), address; let urlAndParams = path.split('?'); const isThereParams = urlAndParams.length > 1; if (isThereParams) path = urlAndParams[0]; let route = Router.routeMap[path]; if (path.split('/').length > 2 || route?.isUrl) { route = route.path.split('/'); address = `${route.pop()}`; path = `#/${address}`; route = route.join('/'); } else { address = path.split('/')[1]?.trim(); route = Router.routeMap[address]?.path; } if (isThereParams) { Router.navigatingView = address; Router.urlParams[address] = urlAndParams[1]; } return route ? { address, route, path } : { state: false, path }; } static getUrlParams() { let routeName = Router.navigatingView; if (Router.navigatingView instanceof Object) routeName = Router.navigatingView.address; const params = Router.urlParams[routeName]; const result = params?.split('&')?.reduce((accum, param) => { const [key, value] = param.split('='); accum[key] = value; return accum; }, {}); setTimeout(() => { Object.entries(Router.urlParams).forEach(([key, _]) => { if (Router.navigatingView != key) delete Router.urlParams[key]; }); }, 1000); return result; } static listenUrlChange() { const { address, path } = Router.getUrlPath(); if (!address) window.location.assign('#'); window.addEventListener('popstate', () => { let url = location.href.toString(); if ( url.slice(0, -3) == Router.baseUrl || url.slice(0, -2) == Router.baseUrl) { const homeComponent = (new StillAppSetup()).entryComponentName; return Router.goto(homeComponent); } url = url.toString().split("#"); if (url == Router.navigatingUrl) return false; if (url[0]?.endsWith('?')) return Router.replaceUrlPath(url[1]); const route = Router.getUrlPath(); if (route.path != '' && route.path != '#/') { if (route.address) Router.goto(route); else { const pathValue = route.path.replace('#/', '/'); const err = $stillconst.MSG.UNKNOWN_ROUTE.replace('{{}}', pathValue) document.write(err); } } }); } static unknownRouteError() { const pathValue = route.path.replace('#/', '/'); const err = $stillconst.MSG.UNKNOWN_ROUTE.replace('{{}}', pathValue) document.write(err); } static async getComponentFromPath() { const route = Router.getUrlPath(); let cmpCls; if (route.address) { cmpCls = await ( await Components.produceComponent( { cmp: route.address, urlRequest: true, loneCntrId: Router.clickEvetCntrId } )); } return cmpCls?._class; } static getCleanUrl(url, clsName) { let baseUrl = Router.baseUrl; const currentUrl = baseUrl.split('//'); if (currentUrl.length > 2) baseUrl = currentUrl.slice(0, 2).join('//') + '/'; /** Edge case where the path is valid and component exists */ if (baseUrl.indexOf(`//${clsName}`) > 0) baseUrl = Router.baseUrl.replace(`//${clsName}`, '/'); if (url) baseUrl = baseUrl.replace(`/${clsName}`, ''); /** Case where # or #/ was entered as the path */ baseUrl = baseUrl.slice(-2) == '//' ? baseUrl.slice(0, -1) : baseUrl; return baseUrl; } static parsedRouteMap = {}; static async parseRouteMap() { return new Promise((resolve) => { if (Object.keys(Router.parsedRouteMap).length == 0) { Object.entries(stillRoutesMap.viewRoutes.regular).map(([name, address]) => { const path = address.url; Router.parsedRouteMap[path] = { path: address.path + '/' + name, isUrl: true }; }); const { regular } = stillRoutesMap.viewRoutes; stillRoutesMap.viewRoutes.regular = { ...regular, ...Router.parsedRouteMap }; Router.routeMap = { ...stillRoutesMap.viewRoutes.lazyInitial, ...stillRoutesMap.viewRoutes.regular }; }; resolve(''); }) } static setStillHomeUrl = () => Router.baseUrl = `${location.origin}/${STILL_HOME}`; static escape() { window.location.href = location.origin + '/#/' + Router.preView.getName(); window.location.reload(); } }