import $ from 'jquery'; const assetAttributeNames = new Set( ['data', 'srcset', 'src', 'href'] ); let curRoute = null; export interface Route { path: string, component: Function, wildcard: boolean }; const routes: Route[] = []; let err404page = "/"; let rootId = "moneyjsx-root"; let defaultTitle = ""; export let onprenavigate: Function = () => {}; export let onpostnavigate: Function = () => {}; function routeForPath( route: string ) : Function | null { if( !routes[route] ) { for( let key of Object.keys( routes ) ) { const r = routes[key]; if( r.wildcard ) { if( route.slice( 0, r.path.length ) == r.path ) { return r.component; } } } return null; } else return routes[route].component; } /** * sets the id of the element to be replaced by the navigator **/ export function setRootId( rootId: string ) { rootId = rootId; } export function setDefaultTitle( title: string ) { defaultTitle = title; } /** * adds a route component to the routes list * the component function must return either a jquery or a DOM element **/ export function addRoute( name: string, component: Function ) { let path = name; let wildcard = false; if( path[path.length - 1] === "*" ) { path = path.substring( 0, path.length - 1 ); wildcard = true; } routes[path] = { path, component, wildcard }; } /** * sets the route for a 404 page **/ export function set404Route( name: string ) { err404page = name; } /** * sets the callback that will get called when a route changes **/ export function onPreNavigate( callback: Function ) { onprenavigate = callback; } /** * sets the callback that will get called when a route changes **/ export function onPostNavigate( callback: Function ) { onpostnavigate = callback; } /** * replaces the root element with the route component **/ export function navigate( route: string ) { let url = new URL( window.location.href ); let cb = routeForPath( route ); url.pathname = route; if( !cb ) return navigate( err404page ); if( curRoute != cb ) window.history.pushState( {}, null, url.href ); onprenavigate(); setTitle(); const el = $( cb() ); curRoute = cb; $( `#${rootId}` ).children().remove(); $( `#${rootId}` ).append( el ); onpostnavigate(); } /** * navigate with params. see: navigate **/ export function navigateParams( route: string, params: any ) { let url = new URL( window.location.href ); let uparams = new URLSearchParams( params ); url.pathname = route; url.search = uparams.toString(); let cb = routeForPath( route ); if( !cb ) return navigate( err404page ); if( curRoute != cb ) window.history.pushState( {}, null, url.href ); onprenavigate(); setTitle(); const el = $( cb() ); curRoute = cb; $( `#${rootId}` ).children().remove(); $( `#${rootId}` ).append( el ); onpostnavigate(); } /** * wrapper for history.pushState **/ export function pushParams( params: any ) { const url = new URL( window.location.href ); url.search = new URLSearchParams( params ).toString(); window.history.pushState( {}, null, url.href ); } /** * navigates without adding a history entry * useful for e.g. re-rendering a page after waiting for a data callback **/ export function navigateParamsSilent( route: string, params: any ) { let url = new URL( window.location.href ); let uparams = new URLSearchParams( params ); url.pathname = route; url.search = uparams.toString(); let cb = routeForPath( route ); if( !cb ) return navigateSilent( err404page ); onprenavigate(); setTitle(); const el = $( cb() ); curRoute = cb; $( `#${rootId}` ).children().remove(); $( `#${rootId}` ).append( el ); onpostnavigate(); } /** * see: navigateParamsSilent **/ export function navigateSilent( route: string ) { let url = new URL( window.location.href ); url.pathname = route; let cb = routeForPath( route ); if( !cb ) return navigateSilent( err404page ); onprenavigate(); setTitle(); const el = $( cb() ); curRoute = cb; $( `#${rootId}` ).children().remove(); $( `#${rootId}` ).append( el ); onpostnavigate(); } /** * action when the back button is pressed **/ export function onPopState() { let url = new URL( window.location.href ); let uparams = new URLSearchParams( url.searchParams ); url.search = uparams.toString(); let cb = routeForPath( url.pathname ); if( !cb ) return navigateSilent( err404page ); if( cb == curRoute ) return; onprenavigate(); setTitle(); const el = $( cb() ); curRoute = cb; $( `#${rootId}` ).children().remove(); $( `#${rootId}` ).append( el ); onpostnavigate(); } /** * navigates to the parent directory from the current page **/ export function goUpDirectory() { const url = new URL( window.location.href ); if( url.pathname.endsWith( "/" ) ) url.pathname = url.pathname.slice( 0, -1 ); let idx = url.pathname.lastIndexOf( "/" ); if( idx === -1 ) return; url.pathname = url.pathname.slice( 0, url.pathname.lastIndexOf( "/" ) ); navigate( url.pathname ); } export function getRoutes() : Route[] { return routes; } // internal stuff below const originalAppendChild = Element.prototype.appendChild; Element.prototype.appendChild = function( child: any ) { if( Array.isArray( child ) ) { for( const childArrayMember of child ) this.appendChild( childArrayMember ); return child; } else if( typeof child === 'string' ) { return originalAppendChild.call( this, document.createTextNode( child ) ); } else if( child ) { return originalAppendChild.call( this, child ); } }; export function createElement( tag: any, props: any, ...children: any ) { props = props || {}; if( typeof tag === "function" ) { props.children = children; return tag( props ); } if( tag === 'raw-content' ) { const dummy = document.createElement( 'div' ); dummy.innerHTML = props.content; return [...dummy.children]; } const element = document.createElement( tag ); for( const [name, value] of Object.entries( props ) ) { if( name.startsWith( 'on' ) ) { const lowercaseName = name.toLowerCase(); if( lowercaseName in window ) { element.addEventListener( lowercaseName.substring( 2 ), value ); continue; } } if( name == 'ref' ) { ( value as any ).current = element; continue; } if( value === false ) continue; if( value === true ) { element.setAttribute( name, '' ); continue; } if( assetAttributeNames.has( name ) ) { if( typeof value === 'string' ) { element.setAttribute( name, value ); } continue; } element.setAttribute( name, value ); }; for( const child of children ) element.appendChild( child ); return element; } export function createFragment( props: any ) { return props.children; } function setTitle() { document.title = defaultTitle; }