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 ;
private _bindings = new Set < StateSubscription < T >>();
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 ) {
if ( this . _value == newValue ) return ;
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
// 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 >>();
2024-07-09 04:43:05 +02:00
private removalHandlers = new Map < T , 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 ) => {
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 );
2024-07-09 04:43:05 +02:00
if ( ! this . removalHandlers . has ( item )) return ;
this . removalHandlers . get ( item ) ! ( item );
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 {
2024-07-09 04:43:05 +02:00
this . removalHandlers . set ( item , handler );
2024-07-08 13:06:40 +02:00
}
// stringification
toString () : string {
const array = [... this . value ];
const json = JSON . stringify ( array );
return json ;
2024-07-05 16:04:12 +02:00
}
}
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 ) =>
state . subscribe (() => ( proxyState . value = fn ()))
);
return proxyState ;
}
2024-07-08 13:06:40 +02:00
function persistState ( localStorageKey : string , state : State < any >) {
state . subscribe (() => {
const stringifiedValue = state . toString ();
localStorage . setItem ( localStorageKey , stringifiedValue );
});
}
export function restoreState < T >(
localStorageKey : string ,
initialStateValue : T
) : State < T > {
const storedString =
localStorage . getItem ( localStorageKey ) ??
JSON . stringify ( initialStateValue );
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 >(
2024-07-08 13:06:40 +02:00
localStorageKey : string
) : ListState < T > {
const storedString = localStorage . getItem ( localStorageKey ) ?? "" ;
let initialItems : any [] = [];
try {
const array = JSON . parse ( storedString );
if ( ! Array . isArray ( array )) throw "" ;
initialItems = array ;
} catch {}
const state = new ListState < T >( initialItems );
persistState ( localStorageKey , state );
return state ;
}
2024-07-09 04:43:05 +02:00
export type ListItemConverter < T > = (
2024-07-08 13:06:40 +02:00
item : T
2024-07-05 16:04:12 +02:00
) => HTMLElement ;
/*
JSX
*/
export function createElement (
tagName : keyof HTMLElementTagNameMap ,
attributes : { [ key : string ] : any } | null = {},
... children : ( HTMLElement | string )[]
) {
const element = document . createElement ( tagName );
if ( attributes != null )
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 ) => {
if (( e as KeyboardEvent ). key != "Enter" ) return ;
value ();
});
break ;
}
default : {
element . addEventListener ( directiveValue , value );
}
}
break ;
}
case "subscribe" : {
if ( directiveValue == "children" ) {
2024-07-08 13:06:40 +02:00
element . style . scrollBehavior = "smooth" ;
try {
const [ listState , toElement ] = value as [
listState : ListState < any >,
2024-07-08 21:13:56 +02:00
ListItemConverter < any >
2024-07-08 13:06:40 +02:00
];
listState . handleAddition (( newItem ) => {
2024-07-08 21:13:56 +02:00
const child = toElement ( newItem );
2024-07-08 13:06:40 +02:00
listState . handleRemoval ( newItem , () =>
child . remove ()
);
element . append ( child );
2024-07-08 21:13:56 +02:00
element . scrollTop = element . scrollHeight ;
2024-07-08 13:06:40 +02:00
});
} catch {
throw `error: cannot process subscribe:children directive because ListItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation` ;
}
2024-07-05 16:04:12 +02:00
} else {
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" : {
2024-07-08 13:06:40 +02:00
if ( value . subscribe ) {
const state = value as State < any >;
state . subscribe (( newValue ) =>
element . toggleAttribute ( directiveValue , newValue )
);
} else {
element . toggleAttribute ( directiveValue , value );
}
break ;
}
case "set" : {
2024-07-05 16:04:12 +02:00
const state = value as State < any >;
state . subscribe (( newValue ) =>
2024-07-08 13:06:40 +02:00
element . setAttribute ( directiveValue , newValue )
2024-07-05 16:04:12 +02:00
);
break ;
}
default :
element . setAttribute ( attributename , value );
}
});
children . filter (( x ) => x ). forEach (( child ) => element . append ( child ));
return element ;
}