summaryrefslogtreecommitdiff
path: root/public/src/jsx.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'public/src/jsx.tsx')
-rw-r--r--public/src/jsx.tsx338
1 files changed, 338 insertions, 0 deletions
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;