From bedf43af45d97a10a6f62b4f1bb21cd66fda1d71 Mon Sep 17 00:00:00 2001 From: aura Date: Thu, 19 Feb 2026 19:57:10 +0100 Subject: empty moneyjsx site --- public/src/jsx.tsx | 338 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 public/src/jsx.tsx (limited to 'public/src/jsx.tsx') diff --git a/public/src/jsx.tsx b/public/src/jsx.tsx new file mode 100644 index 0000000..db46f15 --- /dev/null +++ b/public/src/jsx.tsx @@ -0,0 +1,338 @@ +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 = ""; +let onprenavigate: Function = () => {}; +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(); +} + +/** + * convenience function to pass to href elements + **/ +export function href( e: Event ) { + const el = $( e.target ); + if( el.is( 'a' ) ) { + e.preventDefault(); + navigate( el.attr( 'href' ) ); + } +} + +/** + * 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; +} + +document.addEventListener( "click", e => { + let a = ( e.target ) as HTMLAnchorElement; + if( !a ) + return; + if( a.origin !== location.origin ) + return; + + e.preventDefault(); + if( !a.onclick ) { + if( routeForPath( a.pathname ) ) + return navigate( a.pathname ); + + const hasExt = /\.[a-zA-Z0-9]{1,8}$/.test( a.href ); + if( hasExt ) + location.href = a.href; + else + navigate( err404page ); + } +} ); + +window.onpopstate = onPopState; -- cgit v1.2.3