2024-07-05 16:04:12 +02:00
import { v4 } from "uuid" ;
/*
DATA MODEL
*/
/* Types */
export interface Identifiable {
2024-07-08 13:06:40 +02:00
id : string ;
2024-07-05 16:04:12 +02:00
}
2024-07-08 13:06:40 +02:00
export interface Stringifiable {
toString : () => string ;
}
2024-07-05 16:04:12 +02:00
2024-07-08 13:06:40 +02:00
/* Utility */
export function UUID () : string {
return v4 ();
2024-07-05 16:04:12 +02:00
}
/* State */
export type StateSubscription < T > = ( newValue : T ) => void ;
export type AdditionSubscription < T > = ( newItem : T ) => void ;
export type RemovalSubscription < T > = ( removedItem : T ) => void ;
export class State < T > {
private _value : T ;
2026-10-12 20:38:04 +02:00
_bindings = new Set < StateSubscription < T >>();
2024-07-05 16:04:12 +02:00
2024-07-08 13:06:40 +02:00
// init
2024-07-05 16:04:12 +02:00
constructor ( initialValue : T ) {
this . _value = initialValue ;
}
2024-07-08 13:06:40 +02:00
// value
2024-07-05 16:04:12 +02:00
get value () : T {
return this . _value ;
}
set value ( newValue : T ) {
this . _value = newValue ;
this . callSubscriptions ();
}
2024-07-08 13:06:40 +02:00
// subscriptions
2024-07-05 16:04:12 +02:00
callSubscriptions () : void {
this . _bindings . forEach (( fn ) => fn ( this . _value ));
}
subscribe ( fn : ( newValue : T ) => void ) : void {
this . _bindings . add ( fn );
fn ( this . _value );
}
2024-07-08 13:06:40 +02:00
2026-10-12 20:38:04 +02:00
subscribeSilent ( fn : ( newValue : T ) => void ) : void {
this . _bindings . add ( fn );
}
2024-07-08 13:06:40 +02:00
// stringify
toString () : string {
return JSON . stringify ( this . _value );
}
2024-07-05 16:04:12 +02:00
}
2024-07-09 04:43:05 +02:00
export class ListState < T > extends State < Set < T >> {
2024-07-05 16:04:12 +02:00
private additionHandlers = new Set < AdditionSubscription < T >>();
2026-10-12 20:38:04 +02:00
private removalHandlers = new Map < T , Set < RemovalSubscription < T >>>();
private genericRemovalHandlers = new Set < RemovalSubscription < T >>();
2024-07-05 16:04:12 +02:00
2024-07-08 13:06:40 +02:00
// init
constructor ( initialItems? : T []) {
super ( new Set < T >( initialItems ));
2024-07-05 16:04:12 +02:00
}
2024-07-08 13:06:40 +02:00
// list
2024-07-05 16:04:12 +02:00
add (... items : T []) : void {
items . forEach (( item ) => {
2026-10-12 20:38:04 +02:00
if ( this . value . has ( item )) return ;
2024-07-05 16:04:12 +02:00
this . value . add ( item );
this . additionHandlers . forEach (( handler ) => handler ( item ));
});
2024-07-08 13:06:40 +02:00
this . callSubscriptions ();
2024-07-05 16:04:12 +02:00
}
remove (... items : T []) : void {
items . forEach (( item ) => {
this . value . delete ( item );
2026-10-12 20:38:04 +02:00
this . genericRemovalHandlers . forEach ( handler => handler ( item ));
2024-07-09 04:43:05 +02:00
if ( ! this . removalHandlers . has ( item )) return ;
2026-10-12 20:38:04 +02:00
this . removalHandlers . get ( item ) ! . forEach (( handler ) => handler ( item ));
2024-07-09 04:43:05 +02:00
this . removalHandlers . delete ( item );
2024-07-05 16:04:12 +02:00
});
2024-07-08 13:06:40 +02:00
this . callSubscriptions ();
2024-07-05 16:04:12 +02:00
}
2024-07-08 13:06:40 +02:00
clear() {
this . remove (... this . value . values ());
}
// handlers
2024-07-05 16:04:12 +02:00
handleAddition ( handler : AdditionSubscription < T >) : void {
this . additionHandlers . add ( handler );
2024-07-08 13:06:40 +02:00
[... this . value . values ()]. forEach ( handler );
2024-07-05 16:04:12 +02:00
}
handleRemoval ( item : T , handler : RemovalSubscription < T >) : void {
2026-10-12 20:38:04 +02:00
if ( ! this . removalHandlers . has ( item ))
this . removalHandlers . set ( item , new Set ());
this . removalHandlers . get ( item ) ! . add ( handler );
}
handleRemovals ( handler : RemovalSubscription < T >) : void {
this . genericRemovalHandlers . add ( handler );
2024-07-08 13:06:40 +02:00
}
// stringification
toString () : string {
2026-10-12 20:38:04 +02:00
const array = [... this . value . values ()];
2024-07-08 13:06:40 +02:00
const json = JSON . stringify ( array );
return json ;
2024-07-05 16:04:12 +02:00
}
}
2026-10-12 20:38:04 +02:00
export class MapState < T > extends State < Map < string , T >> {
private additionHandlers = new Set < AdditionSubscription < T >>();
private removalHandlers = new Map < T , Set < RemovalSubscription < T >>>();
private genericRemovalHandlers = new Set < RemovalSubscription < T >>();
// init
constructor ( initialItems ?: [ string , T ][]) {
super ( new Map < string , T >( initialItems ));
}
// list
set ( key : string , item : T ) : void {
this . remove ( key );
this . value . set ( key , item );
this . additionHandlers . forEach (( handler ) => handler ( item ));
this . callSubscriptions ();
}
remove ( key : string ) : void {
const item = this . value . get ( key );
if ( ! item ) return ;
this . value . delete ( key );
this . callSubscriptions ();
this . genericRemovalHandlers . forEach ( handler => handler ( item ));
if ( ! this . removalHandlers . has ( item )) return ;
this . removalHandlers . get ( item ) ! . forEach (( handler ) => handler ( item ));
this . removalHandlers . delete ( item );
}
clear() {
[... this . value . keys ()]. forEach (( key ) => this . remove ( key ));
}
// handlers
handleAddition ( handler : AdditionSubscription < T >) : void {
this . additionHandlers . add ( handler );
[... this . value . values ()]. forEach ( handler );
}
handleRemoval ( item : T , handler : RemovalSubscription < T >) : void {
if ( ! this . removalHandlers . has ( item ))
this . removalHandlers . set ( item , new Set ());
this . removalHandlers . get ( item ) ! . add ( handler );
}
handleRemovals ( handler : RemovalSubscription < T >) : void {
this . genericRemovalHandlers . add ( handler );
}
// stringification
toString () : string {
const array = [... this . value . entries ()];
const json = JSON . stringify ( array );
return json ;
}
}
2024-07-08 13:06:40 +02:00
// UTILITY
2024-07-05 16:04:12 +02:00
export function createProxyState < T >(
statesToSubscibe : State < any >[],
fn : () => T
) : State < T > {
const proxyState = new State < T >( fn ());
statesToSubscibe . forEach (( state ) =>
2026-10-12 20:38:04 +02:00
state . subscribeSilent (() => ( proxyState . value = fn ()))
2024-07-05 16:04:12 +02:00
);
return proxyState ;
}
2026-10-12 20:38:04 +02:00
export function bulkSubscribe (
statesToSubscibe : State < any >[],
fn : () => void
) : void {
statesToSubscibe . forEach (( state ) => state . subscribeSilent ( fn ));
}
2024-07-08 13:06:40 +02:00
function persistState ( localStorageKey : string , state : State < any >) {
state . subscribe (() => {
2026-10-12 20:38:04 +02:00
const stringifiedValue = state . toString ();
localStorage . setItem ( localStorageKey , stringifiedValue );
2024-07-08 13:06:40 +02:00
});
}
export function restoreState < T >(
localStorageKey : string ,
initialStateValue : T
) : State < T > {
const storedString =
2026-10-12 20:38:04 +02:00
localStorage . getItem ( localStorageKey ) ??
JSON . stringify ( initialStateValue );
2024-07-08 13:06:40 +02:00
const convertedValue = JSON . parse ( storedString );
const state = new State ( convertedValue );
persistState ( localStorageKey , state );
return state ;
}
2024-07-09 04:43:05 +02:00
export function restoreListState < T >(
2026-10-12 20:38:04 +02:00
localStorageKey : string ,
initialItems : any [] = []
2024-07-08 13:06:40 +02:00
) : ListState < T > {
const storedString = localStorage . getItem ( localStorageKey ) ?? "" ;
try {
2026-10-12 20:38:04 +02:00
const array = JSON . parse ( storedString );
if ( ! Array . isArray ( array )) throw "" ;
initialItems = array ;
2024-07-08 13:06:40 +02:00
} catch {}
const state = new ListState < T >( initialItems );
persistState ( localStorageKey , state );
return state ;
}
2026-10-12 20:38:04 +02:00
export function restoreMapState < T >(
localStorageKey : string ,
initialItems : [ string , any ][] = []
) : MapState < T > {
const storedString = localStorage . getItem ( localStorageKey ) ?? "" ;
try {
const array = JSON . parse ( storedString );
if ( ! Array . isArray ( array )) throw "" ;
initialItems = array ;
} catch {}
const state = new MapState < T >( initialItems );
persistState ( localStorageKey , state );
return state ;
}
export type StateItemConverter < T > = ( item : T ) => HTMLElement ;
2024-07-05 16:04:12 +02:00
/*
JSX
2026-10-12 20:38:04 +02:00
*/
2024-07-05 16:04:12 +02:00
export function createElement (
tagName : keyof HTMLElementTagNameMap ,
attributes : { [ key : string ] : any } | null = {},
... children : ( HTMLElement | string )[]
) {
const element = document . createElement ( tagName );
if ( attributes != null )
2026-10-12 20:38:04 +02:00
Object . entries ( attributes ). forEach (( entry ) => {
const [ attributename , value ] = entry ;
const [ directiveKey , directiveValue ] = attributename . split ( ":" );
switch ( directiveKey ) {
case "on" : {
switch ( directiveValue ) {
case "enter" : {
element . addEventListener ( "keydown" , ( e : KeyboardEvent ) => {
if ( e . key != "Enter" ) return ;
value ( e );
});
break ;
}
default : {
element . addEventListener ( directiveValue , value );
}
}
break ;
}
case "keystroke" : {
element . addEventListener ( "keydown" , ( e : KeyboardEvent ) => {
if ( e . metaKey == false && e . ctrlKey == false ) return ;
if ( e . key != directiveValue ) return ;
value ( e );
})
break ;
}
case "subscribe" : {
const state = value as State < any >;
state . subscribe (
( newValue ) => ( element [ directiveValue ] = newValue )
);
break ;
}
case "bind" : {
const state = value as State < any >;
state . subscribe (
( newValue ) => ( element [ directiveValue ] = newValue )
);
element . addEventListener (
"input" ,
() => ( state . value = ( element as any )[ directiveValue ])
);
break ;
}
case "toggle" : {
if ( value . subscribe ) {
const state = value as State < any >;
state . subscribe (( newValue ) =>
element . toggleAttribute ( directiveValue , newValue )
);
} else {
element . toggleAttribute ( directiveValue , value );
}
break ;
}
case "set" : {
const state = value as State < any >;
state . subscribe (( newValue ) =>
element . setAttribute ( directiveValue , newValue )
);
break ;
}
case "children" : {
switch ( directiveValue ) {
case "set" : {
const state = value as State < Node | Node [] >;
state . subscribe (( newValue ) => {
element . innerHTML = "" ;
element . append (...[ newValue ]. flat ());
});
break ;
}
case "append" :
case "prepend" : {
try {
const [ listState , toElement ] = value as [
listState : ListState < any >,
StateItemConverter < any >
];
listState . handleAddition (( newItem ) => {
const child = toElement ( newItem );
listState . handleRemoval ( newItem , () =>
child . remove ()
);
if ( directiveValue == "append" ) {
element . append ( child );
} else if ( directiveValue == "prepend" ) {
element . prepend ( child );
}
});
} catch ( error ) {
console . error ( error );
throw `error: cannot process subscribe:children directive. \ n Usage: "children:append={[list, converter]}"; you can find a more detailed example in the documentation.` ;
}
}
}
break ;
}
default :
element . setAttribute ( attributename , value );
}
});
2024-07-05 16:04:12 +02:00
children . filter (( x ) => x ). forEach (( child ) => element . append ( child ));
return element ;
}