2026-09-22 21:41:28 +02:00
import { v4 } from "uuid" ;
/*
DATA MODEL
*/
/* Types */
export interface Identifiable {
id : string ;
}
export interface Stringifiable {
toString : () => string ;
}
/* Utility */
export function UUID () : string {
return v4 ();
}
/* 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 ;
_bindings = new Set < StateSubscription < T >>();
// init
constructor ( initialValue : T ) {
this . _value = initialValue ;
}
// value
get value () : T {
return this . _value ;
}
set value ( newValue : T ) {
this . _value = newValue ;
this . callSubscriptions ();
}
// subscriptions
callSubscriptions () : void {
this . _bindings . forEach (( fn ) => fn ( this . _value ));
}
subscribe ( fn : ( newValue : T ) => void ) : void {
this . _bindings . add ( fn );
fn ( this . _value );
}
subscribeSilent ( fn : ( newValue : T ) => void ) : void {
this . _bindings . add ( fn );
}
// stringify
toString () : string {
return JSON . stringify ( this . _value );
}
}
export class ListState < T > extends State < Set < T >> {
private additionHandlers = new Set < AdditionSubscription < T >>();
private removalHandlers = new Map < T , Set < RemovalSubscription < T >>>();
private genericRemovalHandlers = new Set < RemovalSubscription < T >>();
// init
constructor ( initialItems? : T []) {
super ( new Set < T >( initialItems ));
}
// list
add (... items : T []) : void {
items . forEach (( item ) => {
if ( this . value . has ( item )) return ;
this . value . add ( item );
this . additionHandlers . forEach (( handler ) => handler ( item ));
});
this . callSubscriptions ();
}
remove (... items : T []) : void {
items . forEach (( item ) => {
this . value . delete ( item );
2026-09-23 00:09:09 +02:00
this . genericRemovalHandlers . forEach (( handler ) => handler ( item ));
2026-09-22 21:41:28 +02:00
if ( ! this . removalHandlers . has ( item )) return ;
this . removalHandlers . get ( item ) ! . forEach (( handler ) => handler ( item ));
this . removalHandlers . delete ( item );
});
this . callSubscriptions ();
}
clear() {
this . remove (... this . value . values ());
}
// 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 );
}
2026-09-22 21:57:22 +02:00
2026-09-22 21:41:28 +02:00
handleRemovals ( handler : RemovalSubscription < T >) : void {
this . genericRemovalHandlers . add ( handler );
}
// stringification
toString () : string {
const array = [... this . value . values ()];
const json = JSON . stringify ( array );
return json ;
}
}
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 ();
2026-09-23 00:09:09 +02:00
this . genericRemovalHandlers . forEach (( handler ) => handler ( item ));
if ( ! this . removalHandlers . has ( item )) return ;
this . removalHandlers . get ( item ) ! . forEach (( handler ) => handler ( item ));
this . removalHandlers . delete ( item );
2026-09-22 21:41:28 +02:00
}
clear() {
2026-09-23 00:09:09 +02:00
[... this . value . keys ()]. forEach (( key ) => this . remove ( key ));
2026-09-22 21:41:28 +02:00
}
// handlers
handleAddition ( handler : AdditionSubscription < T >) : void {
2026-09-23 00:09:09 +02:00
this . additionHandlers . add ( handler );
[... this . value . values ()]. forEach ( handler );
2026-09-22 21:41:28 +02:00
}
handleRemoval ( item : T , handler : RemovalSubscription < T >) : void {
2026-09-23 00:09:09 +02:00
if ( ! this . removalHandlers . has ( item ))
this . removalHandlers . set ( item , new Set ());
this . removalHandlers . get ( item ) ! . add ( handler );
2026-09-22 21:41:28 +02:00
}
handleRemovals ( handler : RemovalSubscription < T >) : void {
2026-09-23 00:09:09 +02:00
this . genericRemovalHandlers . add ( handler );
2026-09-22 21:41:28 +02:00
}
// stringification
toString () : string {
2026-09-23 00:09:09 +02:00
const array = [... this . value . entries ()];
const json = JSON . stringify ( array );
return json ;
2026-09-22 21:41:28 +02:00
}
}
// UTILITY
export function createProxyState < T >(
statesToSubscibe : State < any >[],
2026-09-23 00:09:09 +02:00
fn : () => T ,
2026-09-22 21:41:28 +02:00
) : State < T > {
const proxyState = new State < T >( fn ());
statesToSubscibe . forEach (( state ) =>
2026-09-23 00:09:09 +02:00
state . subscribeSilent (() => ( proxyState . value = fn ())),
2026-09-22 21:41:28 +02:00
);
return proxyState ;
}
export function bulkSubscribe (
statesToSubscibe : State < any >[],
2026-09-23 00:09:09 +02:00
fn : () => void ,
2026-09-22 21:41:28 +02:00
) : void {
statesToSubscibe . forEach (( state ) => state . subscribeSilent ( fn ));
}
function persistState ( localStorageKey : string , state : State < any >) {
state . subscribe (() => {
2026-09-23 00:09:09 +02:00
const stringifiedValue = state . toString ();
localStorage . setItem ( localStorageKey , stringifiedValue );
2026-09-22 21:41:28 +02:00
});
}
export function restoreState < T >(
localStorageKey : string ,
2026-09-23 00:09:09 +02:00
initialStateValue : T ,
2026-09-22 21:41:28 +02:00
) : State < T > {
const storedString =
2026-09-23 00:09:09 +02:00
localStorage . getItem ( localStorageKey ) ??
JSON . stringify ( initialStateValue );
2026-09-22 21:41:28 +02:00
const convertedValue = JSON . parse ( storedString );
const state = new State ( convertedValue );
persistState ( localStorageKey , state );
return state ;
}
export function restoreListState < T >(
localStorageKey : string ,
2026-09-23 00:09:09 +02:00
initialItems : any [] = [],
2026-09-22 21:41:28 +02:00
) : ListState < T > {
const storedString = localStorage . getItem ( localStorageKey ) ?? "" ;
try {
2026-09-23 00:09:09 +02:00
const array = JSON . parse ( storedString );
if ( ! Array . isArray ( array )) throw "" ;
initialItems = array ;
2026-09-22 21:41:28 +02:00
} catch {}
const state = new ListState < T >( initialItems );
persistState ( localStorageKey , state );
return state ;
}
export function restoreMapState < T >(
localStorageKey : string ,
2026-09-23 00:09:09 +02:00
initialItems : [ string , any ][] = [],
2026-09-22 21:41:28 +02:00
) : MapState < T > {
const storedString = localStorage . getItem ( localStorageKey ) ?? "" ;
try {
2026-09-23 00:09:09 +02:00
const array = JSON . parse ( storedString );
if ( ! Array . isArray ( array )) throw "" ;
initialItems = array ;
2026-09-22 21:41:28 +02:00
} catch {}
const state = new MapState < T >( initialItems );
persistState ( localStorageKey , state );
return state ;
}
export type StateItemConverter < T > = ( item : T ) => HTMLElement ;
/*
JSX
*/
export function createElement (
tagName : keyof HTMLElementTagNameMap ,
attributes : { [ key : string ] : any } | null = {},
... children : ( HTMLElement | string )[]
) {
const element = document . createElement ( tagName );
if ( attributes != null )
2026-09-23 00:09:09 +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 : Event ) => {
if ( ! ( e instanceof KeyboardEvent )) return ;
if ( e . key != "Enter" ) return ;
value ( e );
});
break ;
}
default : {
element . addEventListener ( directiveValue , value );
}
}
break ;
}
case "keystroke" : {
element . addEventListener ( "keydown" , ( e : Event ) => {
if ( ! ( e instanceof KeyboardEvent )) return ;
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 );
}
});
2026-09-22 21:41:28 +02:00
children . filter (( x ) => x ). forEach (( child ) => element . append ( child ));
return element ;
}