UNPKG

@joshuafcole/fluorine

Version:

A highly reactive, optionally compiled, and strongly typed view layer for TypeScript.

414 lines (364 loc) 11.2 kB
import { inline_chunk, pad_chunk, now } from "efreet/utils"; import { FElement } from "./renderer"; export type RawValue = number | string | boolean; export type MaybeProperty<T> = T | Compilable<T>; export type MapToMaybeProperties<T> = { [K in keyof T]: MaybeProperty<T[K]> }; export function embed<Val extends Compilable<any> | any>(val: Val) { return val === undefined ? "undefined" : val instanceof Compilable ? val.compile() : JSON.stringify(val); } //---------------------------------------------------------------------- // Event Binder (statically generated event handlers) //---------------------------------------------------------------------- export class EventBinder { protected _when?: string; protected _thens: string[] = []; protected _preventer?: string; protected _prevent?: (...args: any[]) => boolean; protected _stopper?: string; protected _stop?: (...args: any[]) => boolean; // @NOTE: Working around a gnarly issue in the type system. constructor( protected _handle?: (...args: any[]) => void, protected _handler?: string, protected _prior_clause?: EventBinder ) {} compile_inner() { // pad_chunk(2) let inner = ` ${this._handler} ${this._preventer} ${this._stopper} ${this._thens.join("\n")} `; if (this._when) { // pad_chunk(2) inner = ` if(${this._when}) { ${inner} } `; } if (this._prior_clause) { // The inverted order here is important because we're unwinding the linked-list :/ inner += "\n\n // OR\n binder = binder._prior_clause;\n" + this._prior_clause.compile_inner(); } return inner; } compile() { let start = now(); let inner = this.compile_inner(); let compiled = new Function( "binder", // pad_chunk(2) ` return function catalyst_compiled_handler(event, elem) { var target = event.currentTarget || event.target; ${inner} }; ` )(this) as (event: Event, elem: FElement) => unknown; // console.log("COMPILED", this._handle && this._handle.name, now() - start); // console.log(compiled); return compiled; } /** Trigger the handler with the given properties when this event fires. */ static handle<U extends any[]>( handler: (...args: U) => unknown, ...args: MapToMaybeProperties<U> ) { return new EventBinder( handler, // inline_chunk ` // ${handler.name} binder._handle(${args.map(embed).join(", ")}); ` ); } /** Add another non-exclusive handling clause. Useful in conjunction with `when()`. */ or<U extends any[]>( handler: (...args: U) => unknown, ...args: MapToMaybeProperties<U> ) { return new EventBinder( handler, // inline_chunk ` // ${handler.name} binder._handle(${args.map(embed).join(", ")}); `, this ); } /** Add a condition which must be true for the handler to fire. */ when(prop: Compilable<boolean> | boolean) { this._when = embed(prop); return this; } /** Add a side effect to be triggered after the handler fires. */ then(effect: Compilable<any>) { this._thens.push(effect.compile()); return this; } /** Always stop propagation on this event. */ stop() { this._stopper = `event.stopPropagation();`; return this; } /** Always prevent default on this event. */ prevent() { this._preventer = `event.preventDefault();`; return this; } /** Stop propagation on this event if the predicate returns true. */ stop_if<U extends RawValue[]>( predicate: (...args: U) => boolean, ...args: MapToMaybeProperties<U> ) { this._stop = predicate; // inline_chunk this._stopper = ` // ${predicate.name} if(binder._stop(${args.map(embed).join(", ")})) { event.stopPropagation(); } `; return this; } /** Prevent default on this event if the predicate returns true. */ prevent_if<U extends RawValue[]>( predicate: (...args: U) => boolean, ...args: MapToMaybeProperties<U> ) { this._prevent = predicate; // inline_chunk this._preventer = ` // ${predicate.name} if(binder._prevent(${args.map(embed).join(", ")})) { event.preventDefault(); } `; return this; } } //---------------------------------------------------------------------- // Compilables (statically defined snippets for generating event handlers) //---------------------------------------------------------------------- export abstract class Compilable<Returns = any> { public returns: Returns; abstract compile(): string; equals<Rhs extends Compilable<any> | any>(rhs: Rhs) { return new Compilable.Infix("===", this, rhs); } is_not<Rhs extends Compilable<any> | any>(rhs: Rhs) { return new Compilable.Infix("!==", this, rhs); } dot<Prop extends Returns extends {} ? keyof Returns : never>(property: Prop) { return new Compilable.Dot<Returns, Prop>(this, property); } and(rhs: Compilable<any>) { return new Compilable.Infix("&&", this, rhs); } or(rhs: Compilable<any>) { return new Compilable.Infix("||", this, rhs); } to_string() { return new Compilable.Computed<string>(`""+$1`, this); } to_number() { return new Compilable.Computed<number>( `!isNaN(+$1) ? +$1 : undefined`, this ); } } export namespace Compilable { export class Snippet<Returns = any> extends Compilable<Returns> { constructor(public code: string) { super(); } compile() { return this.code; } } export class Computed<Returns = any> extends Compilable<Returns> { terms: any[]; constructor(public template: string, ...terms: any[]) { super(); this.terms = terms; } compile() { let code = this.template; for (let ix = 0; ix < this.terms.length; ix += 1) { code = code.replace( new RegExp(`\\$${ix + 1}`, "g"), embed(this.terms[ix]) ); } return code; } } export class Infix< Lhs extends Compilable<any> | any, Rhs extends Compilable<any> | any > extends Compilable<boolean> { constructor(public op: string, public lhs: Lhs, public rhs: Rhs) { super(); } compile() { return `(${embed(this.lhs)} ${this.op} ${embed(this.rhs)})`; } } export class Dot< Returns extends {}, Prop extends keyof Returns > extends Compilable< Returns extends null ? null : Returns extends undefined ? undefined : Returns[Prop] > { constructor(public parent: Compilable<Returns>, public property: Prop) { super(); } compile() { return `(${this.parent.compile()})["${this.property}"]`; } } export class Property< Obj extends {}, Prop extends keyof Obj > extends Compilable<Obj[Prop]> { static create<Obj extends {}, Prop extends keyof Obj = keyof Obj>( prefix: string, property: Prop ) { return new Property<Obj, Prop>(prefix, property); } static create_event<Obj extends {}, Prop extends keyof Obj>( eventCtor: new (...args: any[]) => Obj, property: Prop ) { return new Property<Obj, Prop>("event", property); } constructor(public prefix: string, public property: Prop) { super(); } compile() { return `${this.prefix}["${this.property}"]`; } } export class CategoryProperty< Obj extends {}, Prop extends keyof Obj > extends Compilable<Obj[Prop]> { static create< Obj extends {}, Prop extends keyof Obj, Categories extends { [name: string]: Obj[Prop] } >(prefix: string, property: Prop, categories: Categories) { let prop = new CategoryProperty(prefix, property, categories); return prop as typeof prop & { [K in keyof Categories]: Compilable<boolean> }; } static create_event< Obj extends {}, Prop extends keyof Obj, Categories extends { [name: string]: Obj[Prop] } >( obj: new (...args: any[]) => Obj, property: Prop, categories: Categories ) { let prop = new CategoryProperty("event", property, categories); return prop as typeof prop & { [K in keyof Categories]: Compilable<boolean> }; } constructor( public prefix: string, public property: Prop, public categories: { [name: string]: Obj[Prop] } ) { super(); for (let name in categories) { this[name] = this.equals(categories[name]); } } compile() { return `${this.prefix}["${this.property}"]`; } } } //---------------------------------------------------------------------- // Public API //---------------------------------------------------------------------- /** Trigger the handler with the given properties when this event fires. */ export let handle = EventBinder.handle; /** Helper objects for retrieving event and element properties in catalyst helpers. */ interface Target { value: string; textContent: string; selectedOptions: HTMLOptionElement[]; } export const get = { elem: (prop: string) => { return Compilable.Property.create<any>("elem", prop); }, target: { value: Compilable.Property.create<Target, "value">("target", "value"), content: Compilable.Property.create<Target, "textContent">( "target", "textContent" ), selected: new Compilable.Computed<string[]>( `[].slice.apply($1).map((opt) => opt.value)`, Compilable.Property.create<Target, "selectedOptions">( "target", "selectedOptions" ) ), bounds: new Compilable.Computed<ClientRect>( `target.getBoundingClientRect()` ) }, node: new Compilable.Computed<HTMLElement>(`event.target`), event: new Compilable.Computed<Event>(`event`), mouse: { button: Compilable.CategoryProperty.create_event(MouseEvent, "button", { left: 0, middle: 1, right: 2 }), x: Compilable.Property.create_event(MouseEvent, "clientX"), y: Compilable.Property.create_event(MouseEvent, "clientY"), wheel: { delta_mode: Compilable.Property.create_event(WheelEvent, "deltaMode"), delta_x: Compilable.Property.create_event(WheelEvent, "deltaX"), delta_y: Compilable.Property.create_event(WheelEvent, "deltaY"), delta_z: Compilable.Property.create_event(WheelEvent, "deltaZ") } }, keyboard: { key: Compilable.Property.create_event(KeyboardEvent, "key"), mod: (mod: string) => new Compilable.Computed<boolean>(`event.getModifierState("${mod}")`) }, do: { target: { clear: new Compilable.Snippet(`target.value = ""`), style: (property: MaybeProperty<string>, value: MaybeProperty<any>) => new Compilable.Snippet( `target.style.setProperty(${embed(property)}, ${embed(value)})` ) } } }; /** Registry for compiled event handlers (automatically maintained when compiled). */ export let handlers: { [id: string]: ReturnType<EventBinder["compile"]> } = {};