@joshuafcole/fluorine
Version:
A highly reactive, optionally compiled, and strongly typed view layer for TypeScript.
414 lines (364 loc) • 11.2 kB
text/typescript
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"]> } = {};