New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

@xtia/jel

Package Overview
Dependencies
Maintainers
1
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@xtia/jel - npm Package Compare versions

Comparing version
0.11.1
to
0.11.2
+19
lib/element.d.ts
import { DomHelper, EmitterLike } from "./types";
export declare const $: DomHelper;
export declare class ClassAccessor {
private classList;
private listen;
private unlisten;
constructor(classList: DOMTokenList, listen: (className: string, stream: EmitterLike<boolean>) => void, unlisten: (classNames: string[]) => void);
add(...className: string[]): void;
remove(...className: string[]): void;
toggle(className: string, value?: boolean): boolean;
toggle(className: string, value: EmitterLike<boolean>): void;
contains(className: string): boolean;
get length(): number;
get value(): string;
toString(): string;
replace(token: string, newToken: string): void;
forEach(cb: (token: string, idx: number) => void): void;
map<R>(cb: (token: string, idx: number) => R): R[];
}
import { toEventEmitter } from "./emitter.js";
import { attribsProxy, createEventsProxy, styleProxy } from "./proxy";
import { entityDataSymbol, isContent, isJelEntity, isReactiveSource } from "./util.js";
const elementWrapCache = new WeakMap();
const recursiveAppend = (parent, c) => {
if (c === null || c === undefined)
return;
if (Array.isArray(c)) {
c.forEach(item => recursiveAppend(parent, item));
return;
}
if (isJelEntity(c)) {
recursiveAppend(parent, c[entityDataSymbol].dom);
return;
}
if (typeof c == "number")
c = c.toString();
parent.append(c);
};
function createElement(tag, descriptor = {}) {
if (isContent(descriptor) || isReactiveSource(descriptor))
descriptor = {
content: descriptor,
};
const domElement = document.createElement(tag);
const ent = getWrappedElement(domElement);
const applyClasses = (classes) => {
if (Array.isArray(classes)) {
return classes.forEach(c => applyClasses(c));
}
if (typeof classes == "string") {
classes.trim().split(/\s+/).forEach(c => ent.classes.add(c));
return;
}
if (classes === undefined)
return;
Object.entries(classes).forEach(([className, state]) => {
if (isReactiveSource(state)) {
ent.classes.toggle(className, state);
}
else if (state) {
applyClasses(className);
}
});
};
applyClasses(descriptor.classes || []);
["value", "src", "href", "width", "height", "type", "name"].forEach(prop => {
if (descriptor[prop] !== undefined)
domElement.setAttribute(prop, descriptor[prop]);
});
// attribs.value / attribs.src / attribs.href override descriptor.*
if (descriptor.attribs) {
Object.entries(descriptor.attribs).forEach(([k, v]) => {
if (v === false) {
return;
}
domElement.setAttribute(k, v === true ? k : v);
});
}
if ("content" in descriptor) {
ent.content = descriptor.content;
}
if (descriptor.style) {
ent.style(descriptor.style);
}
if (descriptor.cssVariables) {
ent.setCSSVariable(descriptor.cssVariables);
}
if (descriptor.on) {
Object.entries(descriptor.on).forEach(([eventName, handler]) => ent.events[eventName].apply(handler));
}
if (descriptor.init)
descriptor.init(ent);
return ent;
}
;
export const $ = new Proxy(createElement, {
apply(create, _, [selectorOrTagName, contentOrDescriptor]) {
var _a;
if (selectorOrTagName instanceof HTMLElement)
return getWrappedElement(selectorOrTagName);
const tagName = ((_a = selectorOrTagName.match(/^[^.#]*/)) === null || _a === void 0 ? void 0 : _a[0]) || "";
if (!tagName)
throw new Error("Invalid tag");
const matches = selectorOrTagName.slice(tagName.length).match(/[.#][^.#]+/g);
const classes = {};
const descriptor = {
classes,
content: contentOrDescriptor,
};
matches === null || matches === void 0 ? void 0 : matches.forEach((m) => {
const value = m.slice(1);
if (m[0] == ".") {
classes[value] = true;
}
else {
descriptor.attribs = { id: value };
}
});
return create(tagName, descriptor);
},
get(create, tagName) {
return (descriptorOrContent) => {
return create(tagName, descriptorOrContent);
};
}
});
const elementMutationMap = new WeakMap();
let mutationObserver = null;
function observeMutations() {
if (mutationObserver !== null)
return;
mutationObserver = new MutationObserver((mutations) => {
const recursiveAdd = (node) => {
if (elementMutationMap.has(node)) {
elementMutationMap.get(node).add();
}
if (node.hasChildNodes())
node.childNodes.forEach(recursiveAdd);
};
const recursiveRemove = (node) => {
if (elementMutationMap.has(node)) {
elementMutationMap.get(node).remove();
}
if (node.hasChildNodes())
node.childNodes.forEach(recursiveRemove);
};
mutations.forEach(mut => {
mut.addedNodes.forEach(node => recursiveAdd(node));
mut.removedNodes.forEach(node => recursiveRemove(node));
});
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
function getWrappedElement(element) {
if (!elementWrapCache.has(element)) {
const setCSSVariable = (k, v) => {
if (v === null) {
element.style.removeProperty("--" + k);
}
else {
element.style.setProperty("--" + k, v);
}
};
const listeners = {
style: {},
cssVariable: {},
content: {},
class: {},
};
function addListener(type, prop, source) {
const set = {
style: (v) => element.style[prop] = v,
cssVariable: (v) => setCSSVariable(prop, v),
content: (v) => {
element.innerHTML = "";
recursiveAppend(element, v);
},
class: (v) => element.classList.toggle(prop, v),
}[type];
const subscribe = "subscribe" in source
? () => source.subscribe(set)
: () => source.listen(set);
listeners[type][prop] = {
subscribe,
unsubscribe: element.isConnected ? subscribe() : null,
};
if (!elementMutationMap.has(element)) {
elementMutationMap.set(element, {
add: () => {
Object.values(listeners).forEach(group => {
Object.values(group).forEach(l => l.unsubscribe = l.subscribe());
});
},
remove: () => {
Object.values(listeners).forEach(group => {
Object.values(group).forEach(l => {
var _a;
(_a = l.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(l);
l.unsubscribe = null;
});
});
}
});
}
observeMutations();
}
function removeListener(type, prop) {
if (listeners[type][prop].unsubscribe) {
listeners[type][prop].unsubscribe();
}
delete listeners[type][prop];
if (!Object.keys(listeners).some(group => Object.keys(group).length == 0)) {
elementMutationMap.delete(element);
}
}
function setStyle(prop, value) {
if (listeners.style[prop])
removeListener("style", prop);
if (typeof value == "object" && value) {
if (isReactiveSource(value)) {
addListener("style", prop, toEventEmitter(value));
return;
}
value = value.toString();
}
if (value === undefined) {
return prop in listeners
? listeners.style[prop].subscribe
: element.style[prop];
}
element.style[prop] = value;
}
const domEntity = {
[entityDataSymbol]: {
dom: element,
},
get element() { return element; },
on(eventId, handler) {
const fn = (eventData) => {
handler.call(domEntity, eventData);
};
element.addEventListener(eventId, fn);
return () => element.removeEventListener(eventId, fn);
},
append(...content) {
var _a;
if ((_a = listeners.content) === null || _a === void 0 ? void 0 : _a[""])
removeListener("content", "");
recursiveAppend(element, content);
},
remove: () => element.remove(),
setCSSVariable(variableNameOrTable, value) {
if (typeof variableNameOrTable == "object") {
Object.entries(variableNameOrTable).forEach(([k, v]) => {
if (isReactiveSource(v)) {
addListener("cssVariable", k, v);
return;
}
setCSSVariable(k, v);
});
return;
}
if (listeners.cssVariable[variableNameOrTable])
removeListener("cssVariable", variableNameOrTable);
if (isReactiveSource(value)) {
addListener("cssVariable", variableNameOrTable, value);
return;
}
setCSSVariable(variableNameOrTable, value);
},
qsa(selector) {
const results = [];
element.querySelectorAll(selector).forEach((el) => results.push(el instanceof HTMLElement ? getWrappedElement(el) : el));
return results;
},
getRect: () => element.getBoundingClientRect(),
focus: () => element.focus(),
blur: () => element.blur(),
select: () => element.select(),
play: () => element.play(),
pause: () => element.pause(),
getContext(mode, options) {
return element.getContext(mode, options);
},
get content() {
return [].slice.call(element.children).map((child) => {
if (child instanceof HTMLElement)
return getWrappedElement(child);
return child;
});
},
set content(v) {
var _a;
if ((_a = listeners.content) === null || _a === void 0 ? void 0 : _a[""])
removeListener("content", "");
if (isReactiveSource(v)) {
addListener("content", "", v);
return;
}
element.innerHTML = "";
recursiveAppend(element, v);
},
attribs: new Proxy(element, attribsProxy),
get innerHTML() {
return element.innerHTML;
},
set innerHTML(v) {
element.innerHTML = v;
},
get value() {
return element.value;
},
set value(v) {
element.value = v;
},
get href() {
return element.href;
},
set href(v) {
element.href = v;
},
get src() {
return element.src;
},
set src(v) {
element.src = v;
},
get width() {
return element.width;
},
set width(v) {
element.width = v;
},
get height() {
return element.height;
},
set height(v) {
element.height = v;
},
get currentTime() {
return element.currentTime;
},
set currentTime(v) {
element.currentTime = v;
},
get paused() {
return element.paused;
},
get name() {
return element.getAttribute("name");
},
set name(v) {
if (v === null) {
element.removeAttribute("name");
}
else {
element.setAttribute("name", v);
}
},
style: new Proxy(setStyle, styleProxy),
classes: new ClassAccessor(element.classList, (className, stream) => addListener("class", className, stream), (classNames) => {
classNames.forEach(c => {
if (listeners.class[c])
removeListener("class", c);
});
}),
events: createEventsProxy(element),
};
elementWrapCache.set(element, domEntity);
}
return elementWrapCache.get(element);
}
export class ClassAccessor {
constructor(classList, listen, unlisten) {
this.classList = classList;
this.listen = listen;
this.unlisten = unlisten;
}
add(...className) {
this.unlisten(className);
this.classList.add(...className);
}
remove(...className) {
this.unlisten(className);
this.classList.remove(...className);
}
toggle(className, value) {
this.unlisten([className]);
if (isReactiveSource(value)) {
this.listen(className, value);
return;
}
return this.classList.toggle(className, value);
}
contains(className) {
return this.classList.contains(className);
}
get length() {
return this.classList.length;
}
get value() {
return this.classList.value;
}
toString() {
return this.classList.toString();
}
replace(token, newToken) {
this.unlisten([token, newToken]);
this.classList.replace(token, newToken);
}
forEach(cb) {
this.classList.forEach(cb);
}
map(cb) {
const result = [];
this.classList.forEach((v, i) => {
result.push(cb(v, i));
});
return result;
}
}
import { Dictionary, EmissionSource, EmitterLike, EventHandlerMap, EventSource, Handler, ListenFunc, Period, UnsubscribeFunc } from "./types";
export declare class EventEmitter<T> {
protected onListen: ListenFunc<T>;
constructor(onListen: ListenFunc<T>);
protected transform<R = T>(handler: (value: T, emit: (value: R) => void) => void): (fn: (v: R) => void) => UnsubscribeFunc;
/**
* Compatibility alias for `apply()` - registers a function to receive emitted values
* @param handler
* @returns A function to deregister the handler
*/
listen(handler: Handler<T>): UnsubscribeFunc;
/**
* Registers a function to receive emitted values
* @param handler
* @returns A function to deregister the handler
*/
apply(handler: Handler<T>): UnsubscribeFunc;
/**
* Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
* @param mapFunc
* @returns Listenable: emits transformed values
*/
map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
mapAsync<R>(mapFunc: (value: T) => Promise<R>): EventEmitter<R>;
as<R>(value: R): EventEmitter<R>;
/**
* Creates a chainable emitter that selectively forwards emissions along the chain
* @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
* @returns Listenable: emits values that pass the filter
*/
filter(check: (value: T) => boolean): EventEmitter<T>;
/**
* Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
* @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
*
* If no `compare` function is provided, values will be compared via `===`
* @returns Listenable: emits non-repeating values
*/
dedupe(compare?: (a: T, b: T) => boolean): EventEmitter<T>;
/**
* Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
*
* The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
* All listeners attached to the returned emitter receive the same values as the parent emitter.
*
* *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
*
* @param cb A function to be called as a side effect for each value emitted by the parent emitter.
* @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
*/
tap(cb: Handler<T>): EventEmitter<T>;
/**
* Immediately passes this emitter to a callback and returns this emitter
*
* Allows branching without breaking a composition chain
*
* @example
* ```ts
* range
* .tween("0%", "100%")
* .fork(branch => branch
* .map(s => `Loading: ${s}`)
* .apply(s => document.title = s)
* )
* .apply(v => progressBar.style.width = v);
* ```
* @param cb
*/
fork(...cb: ((branch: this) => void)[]): this;
/**
* Creates a chainable emitter that forwards the parent's last emission after a period of time in which the parent doesn't emit
* @param ms Delay in milliseconds
* @returns Debounced emitter
*/
debounce(ms: number): EventEmitter<T>;
debounce(period: Period): EventEmitter<T>;
/**
* Creates a chainable emitter that forwards the parent's emissions, with a minimum delay between emissions during which parent emssions are ignored
* @param ms Delay in milliseconds
* @returns Throttled emitter
*/
throttle(ms: number): EventEmitter<T>;
throttle(period: Period): EventEmitter<T>;
batch(ms: number): EventEmitter<T[]>;
/**
* Creates a chainable emitter that forwards the next emission from the parent
* **Experimental**: May change in future revisions
* Note: only listens to the parent while at least one downstream subscription is present
* @param notifier
* @returns
*/
once(): EventEmitter<T>;
once(handler: Handler<T>): UnsubscribeFunc;
getNext(): Promise<T>;
delay(ms: number): EventEmitter<T>;
delay(period: Period): EventEmitter<T>;
scan<S>(updater: (state: S, value: T) => S, initial: S): EventEmitter<S>;
buffer(count: number): EventEmitter<T[]>;
/**
* **Experimental**: May change in future revisions
* Note: only listens to the notifier while at least one downstream subscription is present
* @param limit
* @returns
*/
take(limit: number): EventEmitter<T>;
/**
* **Experimental**: May change in future revisions
* Note: only listens to the notifier while at least one downstream subscription is present
* @param notifier
* @returns
*/
takeUntil(notifier: EmitterLike<any>): EventEmitter<T>;
/**
* Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
* Disconnects from the parent and becomes inert when the predicate returns false
* @param predicate Callback to determine whether to keep forwarding
*/
takeWhile(predicate: (value: T) => boolean): EventEmitter<T>;
/**
* Creates a chainable emitter that immediately emits a value to every new subscriber,
* then forwards parent emissions
* @param value
* @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
*/
immediate(value: T): EventEmitter<T>;
/**
* Creates a chainable emitter that forwards its parent's emissions, and
* immediately emits the latest value to new subscribers
* @returns
*/
cached(): EventEmitter<T>;
/**
* Creates a chainable emitter that forwards emissions from the parent and any of the provided emitters
* @param emitters
*/
or(...emitters: EmitterLike<T>[]): EventEmitter<T>;
or<U>(...emitters: EmitterLike<U>[]): EventEmitter<T | U>;
memo(): Memo<T | undefined>;
memo(initial: T): Memo<T>;
memo<U>(initial: U): Memo<T | U>;
record(): EventRecorder<T>;
}
export declare class EventRecorder<T> {
private startTime;
private entries;
private recording;
private unsubscribe;
constructor(emitter: EventEmitter<T>);
private add;
stop(): EventRecording<T>;
}
export declare class EventRecording<T> {
private _entries;
constructor(entries: [number, T][]);
export(): [number, T][];
play(speed?: number): EventEmitter<T>;
}
type EmitEmitterPair<T> = {
emit: (value: T) => void;
emitter: EventEmitter<T>;
};
type CreateEventSourceOptions<T> = {
initialHandler?: Handler<T>;
/**
* Function to call when subscription count changes from 0
* Return a *deactivation* function, which will be called when subscription count changes back to 0
*/
activate?(): UnsubscribeFunc;
};
/**
* Creates a linked EventEmitter and emit() pair
* @example
* ```ts
* function createForm(options?: { onsubmit?: (data: FormData) => void }) {
* const submitEvents = createEventSource(options?.onsubmit);
* const form = $.form({
* on: {
* submit: (e) => {
* e.preventDefault();
* const data = new FormData(e.target);
* submitEvents.emit(data); // emit when form is submitted
* }
* }
* });
*
* return createEntity(form, {
* events: {
* submit: submitEvents.emitter
* }
* })
* }
*
* const form = createForm({
* onsubmit: (data) => handleSubmission(data)
* });
* ```
*
* @param initialHandler Optional listener automatically applied to the resulting Emitter
* @returns
*/
export declare function createEventSource<T>(initialHandler?: Handler<T>): EmitEmitterPair<T>;
export declare function createEventSource<T>(options?: CreateEventSourceOptions<T>): EmitEmitterPair<T>;
export declare function createEventsSource<Map extends Dictionary<any>>(initialListeners?: EventHandlerMap<Map>): {
emitters: import("./types").EventEmitterMap<Map>;
trigger: <K extends keyof Map>(name: K, value: Map[K]) => void;
};
export declare function interval(ms: number): EventEmitter<number>;
export declare function interval(period: Period): EventEmitter<number>;
/**
* Emits time deltas from a shared RAF loop
*/
export declare const animationFrames: EventEmitter<number>;
export declare function timeout(ms: number): EventEmitter<void>;
export declare function timeout(period: Period): EventEmitter<void>;
declare class Memo<T> {
private _value;
private unsubscribeFunc;
get value(): T;
constructor(source: EmitterLike<T>, initial: T);
dispose(): void;
}
export declare class SubjectEmitter<T> extends EventEmitter<T> {
private emit;
private _value;
constructor(initial: T);
get value(): T;
next(value: T): void;
}
/**
* Create an EventEmitter from an event source. Event source can be RxJS observable, existing `EventEmitter`, an object that
* provides a `subscribe()`/`listen() => UnsubscribeFunc` method, or a subscribe function itself.
* @param source
*/
export declare function toEventEmitter<E>(source: EmissionSource<E>): EventEmitter<E>;
/**
* Create an EventEmitter from an event provider and event name. Event source may provide matching `addEventListener`/`on(name, handler)` and `removeEventListener`/`off(name, handler)` methods, or `addEventListener`/`on(name, handler): UnsubscribeFunc.
* @param source
*/
export declare function toEventEmitter<E, N>(source: EventSource<E, N>, eventName: N): EventEmitter<E>;
type ExtractEmitterValue<T> = T extends EmitterLike<infer U> ? U : never;
type CombinedRecord<T extends Dictionary<EmitterLike<any>>> = {
readonly [K in keyof T]: ExtractEmitterValue<T[K]>;
};
export declare function combineEmitters<U extends Dictionary<EmitterLike<any>>>(emitters: U): EventEmitter<CombinedRecord<U>>;
export declare function combineEmitters<U extends EmitterLike<any>[]>(emitters: [...U]): EventEmitter<{
[K in keyof U]: ExtractEmitterValue<U[K]>;
}>;
export {};
import { createEventsProxy } from "./proxy.js";
import { isReactiveSource } from "./util";
function periodAsMilliseconds(t) {
if (typeof t == "number")
return t;
return "asMilliseconds" in t ? t.asMilliseconds : (t.asSeconds * 1000);
}
export class EventEmitter {
constructor(onListen) {
this.onListen = onListen;
}
transform(handler) {
const { emit, listen } = createListenable(() => this.onListen(value => {
handler(value, emit);
}));
return listen;
}
/**
* Compatibility alias for `apply()` - registers a function to receive emitted values
* @param handler
* @returns A function to deregister the handler
*/
listen(handler) {
return this.onListen(handler);
}
/**
* Registers a function to receive emitted values
* @param handler
* @returns A function to deregister the handler
*/
apply(handler) {
return this.onListen(handler);
}
/**
* Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
* @param mapFunc
* @returns Listenable: emits transformed values
*/
map(mapFunc) {
const listen = this.transform((value, emit) => emit(mapFunc(value)));
return new EventEmitter(listen);
}
mapAsync(mapFunc) {
const listen = this.transform((value, emit) => mapFunc(value).then(emit));
return new EventEmitter(listen);
}
as(value) {
const listen = this.transform((_, emit) => emit(value));
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that selectively forwards emissions along the chain
* @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
* @returns Listenable: emits values that pass the filter
*/
filter(check) {
const listen = this.transform((value, emit) => check(value) && emit(value));
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
* @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
*
* If no `compare` function is provided, values will be compared via `===`
* @returns Listenable: emits non-repeating values
*/
dedupe(compare) {
let previous = null;
const listen = this.transform((value, emit) => {
if (!previous || (compare
? !compare(previous.value, value)
: (previous.value !== value))) {
emit(value);
previous = { value };
}
});
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
*
* The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
* All listeners attached to the returned emitter receive the same values as the parent emitter.
*
* *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
*
* @param cb A function to be called as a side effect for each value emitted by the parent emitter.
* @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
*/
tap(cb) {
const listen = this.transform((value, emit) => {
cb(value);
emit(value);
});
return new EventEmitter(listen);
}
/**
* Immediately passes this emitter to a callback and returns this emitter
*
* Allows branching without breaking a composition chain
*
* @example
* ```ts
* range
* .tween("0%", "100%")
* .fork(branch => branch
* .map(s => `Loading: ${s}`)
* .apply(s => document.title = s)
* )
* .apply(v => progressBar.style.width = v);
* ```
* @param cb
*/
fork(...cb) {
cb.forEach(cb => cb(this));
return this;
}
debounce(t) {
let reset = null;
const listen = this.transform((value, emit) => {
reset === null || reset === void 0 ? void 0 : reset();
const timeout = setTimeout(() => {
reset = null;
emit(value);
}, periodAsMilliseconds(t));
reset = () => {
reset = null;
clearTimeout(timeout);
};
});
return new EventEmitter(listen);
}
throttle(t) {
let lastTime = -Infinity;
const listen = this.transform((value, emit) => {
const now = performance.now();
if (now >= lastTime + periodAsMilliseconds(t)) {
lastTime = now;
emit(value);
}
});
return new EventEmitter(listen);
}
batch(ms) {
let items = [];
let active = false;
const listen = this.transform((value, emit) => {
items.push(value);
if (!active) {
active = true;
setTimeout(() => {
emit(items);
items = [];
active = false;
}, ms);
}
});
return new EventEmitter(listen);
}
once(handler) {
let parentUnsubscribe = null;
let completed = false;
const clear = () => {
if (parentUnsubscribe) {
parentUnsubscribe();
parentUnsubscribe = null;
}
};
const { emit, listen } = createListenable(() => {
if (completed)
return;
parentUnsubscribe = this.apply(v => {
completed = true;
clear();
emit(v);
});
return clear;
});
const emitter = new EventEmitter(listen);
return handler
? emitter.apply(handler)
: emitter;
}
getNext() {
return new Promise((resolve) => this.once(resolve));
}
delay(t) {
const ms = periodAsMilliseconds(t);
return new EventEmitter(this.transform((value, emit) => {
return timeout(ms).apply(() => emit(value));
}));
}
scan(updater, initial) {
let state = initial;
const listen = this.transform((value, emit) => {
state = updater(state, value);
emit(state);
});
return new EventEmitter(listen);
}
buffer(count) {
let buffer = [];
const listen = this.transform((value, emit) => {
buffer.push(value);
if (buffer.length >= count) {
emit(buffer);
buffer = [];
}
});
return new EventEmitter(listen);
}
/**
* **Experimental**: May change in future revisions
* Note: only listens to the notifier while at least one downstream subscription is present
* @param limit
* @returns
*/
take(limit) {
let sourceUnsub = null;
let count = 0;
let completed = false;
const { emit, listen } = createListenable(() => {
if (completed)
return;
if (!sourceUnsub) {
sourceUnsub = this.apply(v => {
if (count < limit) {
emit(v);
count++;
if (count >= limit) {
completed = true;
if (sourceUnsub) {
sourceUnsub();
sourceUnsub = null;
}
}
}
});
}
return sourceUnsub;
});
return new EventEmitter(listen);
}
/**
* **Experimental**: May change in future revisions
* Note: only listens to the notifier while at least one downstream subscription is present
* @param notifier
* @returns
*/
takeUntil(notifier) {
let parentUnsubscribe = null;
let notifierUnsub = null;
let completed = false;
const clear = () => {
parentUnsubscribe === null || parentUnsubscribe === void 0 ? void 0 : parentUnsubscribe();
notifierUnsub === null || notifierUnsub === void 0 ? void 0 : notifierUnsub();
};
const { emit, listen } = createListenable(() => {
if (completed)
return;
parentUnsubscribe = this.apply(emit);
notifierUnsub = toEventEmitter(notifier).listen(() => {
completed = true;
clear();
});
return clear;
});
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
* Disconnects from the parent and becomes inert when the predicate returns false
* @param predicate Callback to determine whether to keep forwarding
*/
takeWhile(predicate) {
let parentUnsubscribe;
let completed = false;
const { emit, listen } = createListenable(() => {
if (completed)
return;
parentUnsubscribe = this.apply(v => {
if (predicate(v)) {
emit(v);
}
else {
completed = true;
parentUnsubscribe();
parentUnsubscribe = undefined;
}
});
return () => parentUnsubscribe === null || parentUnsubscribe === void 0 ? void 0 : parentUnsubscribe();
});
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that immediately emits a value to every new subscriber,
* then forwards parent emissions
* @param value
* @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
*/
immediate(value) {
return new EventEmitter(handle => {
handle(value);
return this.onListen(handle);
});
}
/**
* Creates a chainable emitter that forwards its parent's emissions, and
* immediately emits the latest value to new subscribers
* @returns
*/
cached() {
let cache = null;
const { listen, emit } = createListenable(() => this.onListen((value => {
cache = { value };
emit(value);
})));
return new EventEmitter(handler => {
if (cache)
handler(cache.value);
return listen(handler);
});
}
or(...emitters) {
return new EventEmitter(handler => {
const unsubs = [this, ...emitters].map(e => toEventEmitter(e).listen(handler));
return () => unsubs.forEach(unsub => unsub());
});
}
memo(initial) {
return new Memo(this, initial);
}
record() {
return new EventRecorder(this);
}
}
export class EventRecorder {
constructor(emitter) {
this.startTime = performance.now();
this.entries = [];
this.recording = true;
this.unsubscribe = emitter.listen(v => this.add(v));
}
add(value) {
const now = performance.now();
let time = now - this.startTime;
this.entries.push([time, value]);
}
stop() {
if (!this.recording) {
throw new Error("EventRecorder already stopped");
}
this.unsubscribe();
return new EventRecording(this.entries);
}
}
export class EventRecording {
constructor(entries) {
this._entries = entries;
}
export() {
return [...this._entries];
}
play(speed = 1) {
let idx = 0;
let elapsed = 0;
const { emit, listen } = createListenable();
const unsubscribe = animationFrames.listen((frameElapsed) => {
elapsed += frameElapsed * speed;
while (idx < this._entries.length && this._entries[idx][0] <= elapsed) {
emit(this._entries[idx][1]);
idx++;
}
if (idx >= this._entries.length) {
unsubscribe();
}
});
return new EventEmitter(listen);
}
}
export function createEventSource(arg) {
if (typeof arg === "function") {
arg = { initialHandler: arg };
}
const { initialHandler, activate } = arg !== null && arg !== void 0 ? arg : {};
const { emit, listen } = createListenable(activate);
if (initialHandler)
listen(initialHandler);
return {
emit,
emitter: new EventEmitter(listen)
};
}
export function createEventsSource(initialListeners) {
const handlers = {};
const emitters = createEventsProxy({
on: (name, handler) => {
if (!handlers[name])
handlers[name] = [];
const unique = { fn: handler };
handlers[name].push(unique);
return () => {
const idx = handlers[name].indexOf(unique);
handlers[name].splice(idx, 1);
if (handlers[name].length == 0)
delete handlers[name];
};
},
}, initialListeners);
return {
emitters,
trigger: (name, value) => {
var _a;
(_a = handlers[name]) === null || _a === void 0 ? void 0 : _a.forEach(entry => entry.fn(value));
}
};
}
function createListenable(sourceListen) {
const handlers = [];
let onRemoveLast;
const addListener = (fn) => {
const unique = { fn };
handlers.push(unique);
if (sourceListen && handlers.length == 1)
onRemoveLast = sourceListen();
return () => {
const idx = handlers.indexOf(unique);
if (idx === -1)
throw new Error("Handler already unsubscribed");
handlers.splice(idx, 1);
if (onRemoveLast && handlers.length == 0)
onRemoveLast();
};
};
return {
listen: addListener,
emit: (value) => handlers.forEach(h => h.fn(value)),
};
}
export function interval(t) {
let intervalId = null;
let idx = 0;
const { emit, listen } = createListenable(() => {
intervalId = setInterval(() => {
emit(idx++);
}, periodAsMilliseconds(t));
return () => clearInterval(intervalId);
});
return new EventEmitter(listen);
}
/**
* Emits time deltas from a shared RAF loop
*/
export const animationFrames = (() => {
const { emit, listen } = createListenable(() => {
let rafId = null;
let lastTime = null;
const frame = (time) => {
rafId = requestAnimationFrame(frame);
const elapsed = time - (lastTime !== null && lastTime !== void 0 ? lastTime : time);
lastTime = time;
emit(elapsed);
};
rafId = requestAnimationFrame(frame);
return () => cancelAnimationFrame(rafId);
});
return new EventEmitter(listen);
})();
export function timeout(t) {
const ms = periodAsMilliseconds(t);
const targetTime = Date.now() + ms;
const { emit, listen } = createListenable(() => {
const reminaingMs = targetTime - Date.now();
if (reminaingMs < 0)
return;
const timeoutId = setTimeout(() => {
emit();
}, reminaingMs);
return () => clearTimeout(timeoutId);
});
return new EventEmitter(listen);
}
class Memo {
get value() {
return this._value;
}
constructor(source, initial) {
this._value = initial;
const emitter = toEventEmitter(source);
this.unsubscribeFunc = emitter.listen(v => this._value = v);
}
dispose() {
this.unsubscribeFunc();
this.unsubscribeFunc = () => {
throw new Error("Memo object already disposed");
};
}
}
export class SubjectEmitter extends EventEmitter {
constructor(initial) {
const { emit, listen } = createListenable();
super(h => {
h(this._value); // immediate emit on listen
return listen(h);
});
this.emit = emit;
this._value = initial;
}
get value() {
return this._value;
}
next(value) {
this._value = value;
this.emit(value);
}
}
export function toEventEmitter(source, eventName) {
if (source instanceof EventEmitter)
return source;
if (typeof source == "function")
return new EventEmitter(source);
if (eventName !== undefined) {
// addEL()
if ("addEventListener" in source) {
if ("removeEventListener" in source && typeof source.removeEventListener == "function") {
return new EventEmitter(h => {
source.addEventListener(eventName, h);
return () => source.removeEventListener(eventName, h);
});
}
return new EventEmitter(h => {
return source.addEventListener(eventName, h);
});
}
// on()
if ("on" in source) {
if ("off" in source && typeof source.off == "function") {
return new EventEmitter(h => {
return source.on(eventName, h)
|| (() => source.off(eventName, h));
});
}
return new EventEmitter(h => {
return source.on(eventName, h);
});
}
}
if (isReactiveSource(source)) {
const subscribe = "subscribe" in source
? (h) => source.subscribe(h)
: (h) => source.listen(h);
return new EventEmitter(subscribe);
}
throw new Error("Invalid event source");
}
function combineArray(emitters) {
let values = Array.from({ length: emitters.length });
const { emit, listen } = createListenable(() => {
const unsubFuncs = emitters.map((emitter, idx) => {
return emitter.listen(v => {
values[idx] = { value: v };
if (values.every(v => v !== undefined))
emit(values.map(vc => vc.value));
});
});
return () => unsubFuncs.forEach(f => f());
});
return new EventEmitter(listen);
}
function combineRecord(emitters) {
const keys = Object.keys(emitters);
let values = {};
const { emit, listen } = createListenable(() => {
const unsubFuncs = keys.map(key => {
return emitters[key].listen(v => {
values[key] = { value: v };
if (keys.every(k => values[k] !== undefined)) {
const record = Object.fromEntries(Object.entries(values).map(([k, vc]) => [k, vc.value]));
emit(record);
}
});
});
return () => unsubFuncs.forEach(f => f());
});
return new EventEmitter(listen);
}
export function combineEmitters(emitters) {
if (Array.isArray(emitters))
return combineArray(emitters.map(toEventEmitter));
return combineRecord(Object.fromEntries(Object.entries(emitters).map(([k, e]) => [k, toEventEmitter(e)])));
}
import { DomEntity } from "./types";
export declare const $body: DomEntity<HTMLElement>;
export declare const windowEvents: import("./types").EventEmitterMap<WindowEventMap>;
export declare const documentEvents: import("./types").EventEmitterMap<DocumentEventMap>;
import { $ } from "./element";
import { createEventsProxy } from "./proxy";
export const $body = "document" in globalThis ? $(document.body) : undefined;
export const windowEvents = createEventsProxy(window);
export const documentEvents = createEventsProxy(document);
export { $ } from "./element";
export { DomEntity, ElementClassDescriptor, ElementDescriptor, DOMContent, DomHelper, StyleAccessor, JelEntity, EventEmitterMap, EmitterLike, CSSValue } from "./types";
export { createEntity } from "./util";
export { createEventSource, createEventsSource, interval, timeout, animationFrames, SubjectEmitter, toEventEmitter, type EventEmitter, type EventRecording, type EventRecorder, combineEmitters } from "./emitter";
export { createEventsProxy } from "./proxy";
export { $body, windowEvents } from "./helpers";
export { $ } from "./element";
export { createEntity } from "./util";
export { createEventSource, createEventsSource, interval, timeout, animationFrames, SubjectEmitter, toEventEmitter, combineEmitters } from "./emitter";
export { createEventsProxy } from "./proxy";
export { $body, windowEvents } from "./helpers";
import { SetGetStyleFunc, EventSource, EventEmitterMap, EventHandlerMap } from "./types";
export declare const styleProxy: ProxyHandler<SetGetStyleFunc>;
export declare const attribsProxy: ProxyHandler<HTMLElement>;
export declare function createEventsProxy<Map>(source: EventSource<any, keyof Map>, initialListeners?: EventHandlerMap<Map>): EventEmitterMap<Map>;
import { toEventEmitter } from "./emitter";
export const styleProxy = {
get(style, prop) {
return style(prop);
},
set(style, prop, value) {
style(prop, value);
return true;
},
apply(style, _, [stylesOrProp, value]) {
if (typeof stylesOrProp == "object") {
Object.entries(stylesOrProp).forEach(([prop, val]) => style(prop, val));
return;
}
style(stylesOrProp, value);
},
deleteProperty(style, prop) {
style(prop, null);
return true;
}
};
export const attribsProxy = {
get: (element, key) => {
return element.getAttribute(key);
},
set: (element, key, value) => {
element.setAttribute(key, value);
return true;
},
has: (element, key) => {
return element.hasAttribute(key);
},
ownKeys: (element) => {
return element.getAttributeNames();
},
};
const eventsProxyDefinition = {
get: (object, key) => {
return toEventEmitter(object, key);
}
};
export function createEventsProxy(source, initialListeners) {
const proxy = new Proxy(source, eventsProxyDefinition);
if (initialListeners) {
Object.entries(initialListeners)
.forEach(([name, handler]) => toEventEmitter(source, name).apply(handler));
}
return proxy;
}
import { ClassAccessor } from "./element";
import { EventEmitter } from "./emitter";
import { entityDataSymbol } from "./util";
export type ElementClassDescriptor = string | Record<string, boolean | EmitterLike<boolean> | undefined> | undefined | ElementClassDescriptor[];
export type DOMContent = number | null | string | Element | JelEntity<object> | Text | DOMContent[];
export type DomEntity<T extends HTMLElement> = JelEntity<ElementAPI<T>>;
export type HTMLTag = keyof HTMLElementTagNameMap;
export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
export type UnsubscribeFunc = () => void;
export type EmitterLike<T> = {
subscribe: ListenFunc<T>;
} | {
listen: ListenFunc<T>;
};
export type EmissionSource<T> = EmitterLike<T> | ListenFunc<T>;
export type CSSValue = string | number | null | HexCodeContainer;
export type CSSProperty = keyof StylesDescriptor;
type HexCodeContainer = {
hexCode: string;
toString(): string;
};
export type StylesDescriptor = {
[K in keyof CSSStyleDeclaration as [
K,
CSSStyleDeclaration[K]
] extends [string, string] ? K : never]+?: CSSValue | EmitterLike<CSSValue>;
};
export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | EmitterLike<CSSValue>) => void);
export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | EmitterLike<CSSValue>);
export type StyleAccessor = ((styles: StylesDescriptor) => void) & StylesDescriptor & SetStyleFunc;
type ContentlessTag = "area" | "br" | "hr" | "iframe" | "input" | "textarea" | "img" | "canvas" | "link" | "meta" | "source" | "embed" | "track" | "base";
type TagWithHref = "a" | "link" | "base";
type TagWithSrc = "img" | "script" | "iframe" | "video" | "audio" | "embed" | "source" | "track";
type TagWithValue = "input" | "textarea";
type TagWithWidthHeight = "canvas" | "img" | "embed" | "iframe" | "video";
type TagWithType = "input" | "source" | "button";
type TagWithName = 'input' | 'textarea' | 'select' | 'form';
type ContentlessElement = HTMLElementTagNameMap[ContentlessTag];
export type ElementDescriptor<Tag extends HTMLTag> = {
classes?: ElementClassDescriptor;
attribs?: Record<string, string | number | boolean | undefined>;
on?: {
[E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
};
style?: StylesDescriptor;
cssVariables?: Record<string, CSSValue | EmitterLike<CSSValue>>;
init?: (entity: DomEntity<HTMLElementTagNameMap[Tag]>) => void;
} & (Tag extends TagWithValue ? {
value?: string | number;
} : {}) & (Tag extends ContentlessTag ? {} : {
content?: DOMContent | EmitterLike<DOMContent>;
}) & (Tag extends TagWithSrc ? {
src?: string;
} : {}) & (Tag extends TagWithHref ? {
href?: string;
} : {}) & (Tag extends TagWithWidthHeight ? {
width?: number;
height?: number;
} : {}) & (Tag extends TagWithType ? {
type?: string;
} : {}) & (Tag extends TagWithName ? {
name?: string;
} : {});
type ElementAPI<T extends HTMLElement> = {
readonly element: T;
readonly classes: ClassAccessor;
readonly attribs: {
[key: string]: string | null;
};
readonly events: EventEmitterMap<HTMLElementEventMap>;
readonly style: StyleAccessor;
setCSSVariable(variableName: string, value: CSSValue | EmitterLike<CSSValue>): void;
setCSSVariable(table: Record<string, CSSValue | EmitterLike<CSSValue>>): void;
qsa(selector: string): (Element | DomEntity<HTMLElement>)[];
remove(): void;
getRect(): DOMRect;
focus(): void;
blur(): void;
/**
* Add an event listener
* @param eventId
* @param handler
* @returns Function to remove the listener
* @deprecated Use ent.events
*/
on<E extends keyof HTMLElementEventMap>(eventId: E, handler: (this: ElementAPI<T>, data: HTMLElementEventMap[E]) => void): UnsubscribeFunc;
} & (T extends ContentlessElement ? {} : {
append(...content: DOMContent[]): void;
innerHTML: string;
content: DOMContent | EmitterLike<DOMContent>;
}) & (T extends HTMLElementTagNameMap[TagWithValue] ? {
value: string;
select(): void;
} : {}) & (T extends HTMLCanvasElement ? {
width: number;
height: number;
getContext: HTMLCanvasElement["getContext"];
} : {}) & (T extends HTMLElementTagNameMap[TagWithSrc] ? {
src: string;
} : {}) & (T extends HTMLElementTagNameMap[TagWithHref] ? {
href: string;
} : {}) & (T extends HTMLMediaElement ? {
play(): void;
pause(): void;
currentTime: number;
readonly paused: boolean;
} : {}) & (T extends HTMLElementTagNameMap[TagWithName] ? {
name: string | null;
} : {});
export type DomHelper = ((
/**
* Creates an element of the specified tag
*/
<T extends HTMLTag>(tagName: T, descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>) & (
/**
* Creates an element of the specified tag
*/
<T extends HTMLTag>(tagName: T) => DomEntity<HTMLElementTagNameMap[T]>) & (
/**
* Creates an element with ID and classes as specified by a selector-like string
*/
<T extends HTMLTag>(selector: `${T}#${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
/**
* Creates an element with ID and classes as specified by a selector-like string
*/
<T extends HTMLTag>(selector: `${T}.${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
/**
* Wraps an existing element as a DomEntity
*/
<T extends HTMLElement>(element: T) => DomEntity<T>) & {
[T in HTMLTag]: (descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>;
} & {
[T in HTMLTag]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (((content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & ((contentEmitter: EmitterLike<DOMContent>) => DomEntity<HTMLElementTagNameMap[T]>));
});
type JelEntityData = {
dom: DOMContent;
};
export type JelEntity<API extends object | void> = (API extends void ? {} : API) & {
readonly [entityDataSymbol]: JelEntityData;
};
export type Handler<T> = (value: T) => void;
export type Period = {
asMilliseconds: number;
} | {
asSeconds: number;
};
export type EventSource<E, N> = {
on: (eventName: N, handler: Handler<E>) => UnsubscribeFunc;
} | {
on: (eventName: N, handler: Handler<E>) => void | UnsubscribeFunc;
off: (eventName: N, handler: Handler<E>) => void;
} | {
addEventListener: (eventName: N, handler: Handler<E>) => UnsubscribeFunc;
} | {
addEventListener: (eventName: N, handler: Handler<E>) => void;
removeEventListener: (eventName: N, handler: Handler<E>) => void;
};
export type Dictionary<T> = Record<string | symbol, T>;
export type EventEmitterMap<Map> = {
[K in keyof Map]: EventEmitter<Map[K]>;
};
export type EventHandlerMap<Map> = {
[K in keyof Map]?: (value: Map[K]) => void;
};
export {};
import { entityDataSymbol } from "./util";
import { DOMContent, ElementDescriptor, EmitterLike, HTMLTag, JelEntity } from "./types";
export declare const entityDataSymbol: unique symbol;
export declare const isContent: <T extends HTMLTag>(value: DOMContent | ElementDescriptor<T> | undefined) => value is DOMContent;
export declare function isJelEntity(content: DOMContent): content is JelEntity<object>;
/**
* Wraps an object such that it can be appended as DOM content while retaining its original API
* @param content
* @param api
*/
export declare function createEntity<API extends object>(content: DOMContent, api: API extends DOMContent ? never : API): JelEntity<API>;
export declare function createEntity(content: DOMContent): JelEntity<void>;
export declare function isReactiveSource(value: any): value is EmitterLike<any>;
export const entityDataSymbol = Symbol("jelComponentData");
export const isContent = (value) => {
if (value === undefined)
return false;
return typeof value == "string"
|| typeof value == "number"
|| !value
|| value instanceof Element
|| value instanceof Text
|| entityDataSymbol in value
|| (Array.isArray(value) && value.every(isContent));
};
export function isJelEntity(content) {
return typeof content == "object" && !!content && entityDataSymbol in content;
}
export function createEntity(content, api) {
if (isContent(api)) {
throw new TypeError("API object is already valid content");
}
return Object.create(api !== null && api !== void 0 ? api : {}, {
[entityDataSymbol]: {
value: {
dom: content
}
},
});
}
;
export function isReactiveSource(value) {
return typeof value == "object" && value && (("listen" in value && typeof value.listen == "function")
|| ("subscribe" in value && typeof value.subscribe == "function"));
}
+32
-16
{
"name": "@xtia/jel",
"version": "0.11.1",
"repository": {
"url": "https://github.com/tiadrop/jel-ts",
"type": "github"
},
"version": "0.11.2",
"description": "Lightweight DOM manipulation, componentisation and reactivity",
"keywords": [
"dom",
"reactive",
"streams",
"reactive-programming",
"frontend",
"ui",
"framework"
],
"types": "./lib/index.d.ts",
"main": "./lib/index.js",
"sideEffects": false,
"types": "./index.d.ts",
"main": "./index.js",
"exports": {
".": {
"types": "./index.d.ts",
"default": "./index.js"
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"./internal/*": null
"./lib/*": null
},
"scripts": {
"prepublishOnly": "cp ../README.md .",
"postpublish": "rm README.md"
"build": "tsc",
"watch-demo": "webpack --watch",
"prepublishOnly": "tsc"
},
"description": "Lightweight DOM manipulation, componentisation and reactivity",
"keywords": [
"dom"
"repository": {
"url": "https://github.com/tiadrop/jel-ts",
"type": "github"
},
"files": [
"lib/"
],
"author": "Aleta Lovelace",
"license": "MIT"
"license": "MIT",
"devDependencies": {
"ts-loader": "^9.5.1",
"typescript": "^5.9.3",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4"
}
}
+117
-1

@@ -191,2 +191,118 @@ # Jel

button.style.opacity = animate(500).tween(0, 1);
```
```
## Custom streams
Several utilities are provided to create event streams from existing sources and custom emit logic.
### `toEventEmitter(source)`
Creates an `EventEmitter<T>` from an `EmitterLike<T>`, a listen function (`(Handler<T>) => UnsubscribeFunc`), or an `EventSource` + event name pair.
* `EmitterLike<T>` is any object with a compatible `subscribe|listen` method
* `EventSource` is any object with common `addEventListener/removeEventListener|on/off` methods.
```ts
import { toEventEmitter } from "@xtia/jel";
// EventSource + name:
const keypresses$ = toEventEmitter(window, "keydown");
keypresses$.map(ev => ev.key)
.listen(key => console.log(key, "pressed"));
// EmitterLike
function logEvents(emitter: EmitterLike<any>) {
// this function accepts Jel's EventEmitter, as well as RxJS
// streams and other compatible emitters
toEventEmitter(emitter).listen(value => console.log(value));
}
```
## createEventSource<T>()
Creates an EventEmitter<T> and a `emit(T)` function to control it.
```ts
import { createEventSource } from "@xtia/jel";
function createGame() {
const winEmitPair = createEventSource<string>();
// <insert game logic>
// when someone wins:
winEmitPair.emit("player1");
return {
winEvent: winEmitPair.emitter
};
}
const game = createGame();
game.winEvent
.filter(winner => winner === me)
.apply(showConfetti);
```
## createEventsSource<Map>()
Creates an 'events' object and a `trigger(name, Map[name])` function to trigger specific events.
```ts
import { createEventsSource } from "@xtia/jel";
type EventMap = {
end: { winner: string },
update: { state: GameState },
}
function createGame() {
const events = createEventsSource<EventMap>();
// when game ends
events.trigger("end", winnerName);
return {
events: events.emitters,
}
}
```
## createEventsProxy<Map>(source)
Creates an 'events' object from an `EventSource`.
```ts
import { createEventsProxy } from "@xtia/jel";
const windowEvents = createEventsProxy<WindowEventMap>(window);
// (this windowEvents is exported from @xtia/jel for convenience)
windowEvents.keydown
.filter(ev => ev.key == "Enter")
.apply(() => console.log("Enter pressed"));
```
## `interval(ms)`
Emits a number, incremented by 1 each time, as long as any subscriptions are active.
## `timeout(ms)`
Emits once after the specified time.
## `animationFrames`
Emits *delta times* from a `requestAnimationFrame()` loop, as long as any subscriptions are active.
```ts
import { animationFrames } from "@xtia.jel";
animationFrames.listen(delta => {
game.tick(delta);
});
## SubjectEmitter
Creates a manually-controlled emitter that maintains its last emitted value (`em.value`), emits it immediately to
and new subscription and can be updated with `em.next(value)`.
import { $ } from "./internal/element";
import { DomEntity } from "./internal/types";
export { DomEntity, ElementClassDescriptor, ElementDescriptor, DOMContent, DomHelper, StyleAccessor, JelEntity, EventEmitterMap, EmitterLike, CSSValue } from "./internal/types";
export { createEntity } from "./internal/util";
export { createEventSource, createEventsSource, interval, timeout, animationFrames, SubjectEmitter, toEventEmitter, type EventEmitter, type EventRecording, type EventRecorder, combineEmitters } from "./internal/emitter";
export { createEventsProxy } from "./internal/proxy";
export { $ };
export declare const $body: DomEntity<HTMLElement>;
export declare const windowEvents: import(".").EventEmitterMap<WindowEventMap>;
import { $ } from "./internal/element";
import { createEventsProxy } from "./internal/proxy";
export { createEntity } from "./internal/util";
export { createEventSource, createEventsSource, interval, timeout, animationFrames, SubjectEmitter, toEventEmitter, combineEmitters } from "./internal/emitter";
export { createEventsProxy } from "./internal/proxy";
export { $ };
export const $body = "document" in globalThis ? $(document.body) : undefined;
export const windowEvents = createEventsProxy(window);
import { DomHelper, EmitterLike } from "./types";
export declare const $: DomHelper;
export declare class ClassAccessor {
private classList;
private listen;
private unlisten;
constructor(classList: DOMTokenList, listen: (className: string, stream: EmitterLike<boolean>) => void, unlisten: (classNames: string[]) => void);
add(...className: string[]): void;
remove(...className: string[]): void;
toggle(className: string, value?: boolean): boolean;
toggle(className: string, value: EmitterLike<boolean>): void;
contains(className: string): boolean;
get length(): number;
get value(): string;
toString(): string;
replace(token: string, newToken: string): void;
forEach(cb: (token: string, idx: number) => void): void;
map<R>(cb: (token: string, idx: number) => R): R[];
}
import { toEventEmitter } from "./emitter.js";
import { attribsProxy, createEventsProxy, styleProxy } from "./proxy";
import { entityDataSymbol, isContent, isJelEntity, isReactiveSource } from "./util";
const elementWrapCache = new WeakMap();
const recursiveAppend = (parent, c) => {
if (c === null || c === undefined)
return;
if (Array.isArray(c)) {
c.forEach(item => recursiveAppend(parent, item));
return;
}
if (isJelEntity(c)) {
recursiveAppend(parent, c[entityDataSymbol].dom);
return;
}
if (typeof c == "number")
c = c.toString();
parent.append(c);
};
function createElement(tag, descriptor = {}) {
if (isContent(descriptor) || isReactiveSource(descriptor))
descriptor = {
content: descriptor,
};
const domElement = document.createElement(tag);
const ent = getWrappedElement(domElement);
const applyClasses = (classes) => {
if (Array.isArray(classes)) {
return classes.forEach(c => applyClasses(c));
}
if (typeof classes == "string") {
classes.trim().split(/\s+/).forEach(c => ent.classes.add(c));
return;
}
if (classes === undefined)
return;
Object.entries(classes).forEach(([className, state]) => {
if (isReactiveSource(state)) {
ent.classes.toggle(className, state);
}
else if (state) {
applyClasses(className);
}
});
};
applyClasses(descriptor.classes || []);
["value", "src", "href", "width", "height", "type", "name"].forEach(prop => {
if (descriptor[prop] !== undefined)
domElement.setAttribute(prop, descriptor[prop]);
});
// attribs.value / attribs.src / attribs.href override descriptor.*
if (descriptor.attribs) {
Object.entries(descriptor.attribs).forEach(([k, v]) => {
if (v === false) {
return;
}
domElement.setAttribute(k, v === true ? k : v);
});
}
if ("content" in descriptor) {
ent.content = descriptor.content;
}
if (descriptor.style) {
ent.style(descriptor.style);
}
if (descriptor.cssVariables) {
ent.setCSSVariable(descriptor.cssVariables);
}
if (descriptor.on) {
Object.entries(descriptor.on).forEach(([eventName, handler]) => ent.events[eventName].apply(handler));
}
if (descriptor.init)
descriptor.init(ent);
return ent;
}
;
export const $ = new Proxy(createElement, {
apply(create, _, [selectorOrTagName, contentOrDescriptor]) {
var _a;
if (selectorOrTagName instanceof HTMLElement)
return getWrappedElement(selectorOrTagName);
const tagName = ((_a = selectorOrTagName.match(/^[^.#]*/)) === null || _a === void 0 ? void 0 : _a[0]) || "";
if (!tagName)
throw new Error("Invalid tag");
const matches = selectorOrTagName.slice(tagName.length).match(/[.#][^.#]+/g);
const classes = {};
const descriptor = {
classes,
content: contentOrDescriptor,
};
matches === null || matches === void 0 ? void 0 : matches.forEach((m) => {
const value = m.slice(1);
if (m[0] == ".") {
classes[value] = true;
}
else {
descriptor.attribs = { id: value };
}
});
return create(tagName, descriptor);
},
get(create, tagName) {
return (descriptorOrContent) => {
return create(tagName, descriptorOrContent);
};
}
});
const elementMutationMap = new WeakMap();
let mutationObserver = null;
function observeMutations() {
if (mutationObserver !== null)
return;
mutationObserver = new MutationObserver((mutations) => {
const recursiveAdd = (node) => {
if (elementMutationMap.has(node)) {
elementMutationMap.get(node).add();
}
if (node.hasChildNodes())
node.childNodes.forEach(recursiveAdd);
};
const recursiveRemove = (node) => {
if (elementMutationMap.has(node)) {
elementMutationMap.get(node).remove();
}
if (node.hasChildNodes())
node.childNodes.forEach(recursiveRemove);
};
mutations.forEach(mut => {
mut.addedNodes.forEach(node => recursiveAdd(node));
mut.removedNodes.forEach(node => recursiveRemove(node));
});
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
function getWrappedElement(element) {
if (!elementWrapCache.has(element)) {
const setCSSVariable = (k, v) => {
if (v === null) {
element.style.removeProperty("--" + k);
}
else {
element.style.setProperty("--" + k, v);
}
};
const listeners = {
style: {},
cssVariable: {},
content: {},
class: {},
};
function addListener(type, prop, source) {
const set = {
style: (v) => element.style[prop] = v,
cssVariable: (v) => setCSSVariable(prop, v),
content: (v) => {
element.innerHTML = "";
recursiveAppend(element, v);
},
class: (v) => element.classList.toggle(prop, v),
}[type];
const subscribe = "subscribe" in source
? () => source.subscribe(set)
: () => source.listen(set);
listeners[type][prop] = {
subscribe,
unsubscribe: element.isConnected ? subscribe() : null,
};
if (!elementMutationMap.has(element)) {
elementMutationMap.set(element, {
add: () => {
Object.values(listeners).forEach(group => {
Object.values(group).forEach(l => l.unsubscribe = l.subscribe());
});
},
remove: () => {
Object.values(listeners).forEach(group => {
Object.values(group).forEach(l => {
var _a;
(_a = l.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(l);
l.unsubscribe = null;
});
});
}
});
}
observeMutations();
}
function removeListener(type, prop) {
if (listeners[type][prop].unsubscribe) {
listeners[type][prop].unsubscribe();
}
delete listeners[type][prop];
if (!Object.keys(listeners).some(group => Object.keys(group).length == 0)) {
elementMutationMap.delete(element);
}
}
function setStyle(prop, value) {
if (listeners.style[prop])
removeListener("style", prop);
if (typeof value == "object" && value) {
if (isReactiveSource(value)) {
addListener("style", prop, toEventEmitter(value));
return;
}
value = value.toString();
}
if (value === undefined) {
return prop in listeners
? listeners.style[prop].subscribe
: element.style[prop];
}
element.style[prop] = value;
}
const domEntity = {
[entityDataSymbol]: {
dom: element,
},
get element() { return element; },
on(eventId, handler) {
const fn = (eventData) => {
handler.call(domEntity, eventData);
};
element.addEventListener(eventId, fn);
return () => element.removeEventListener(eventId, fn);
},
append(...content) {
var _a;
if ((_a = listeners.content) === null || _a === void 0 ? void 0 : _a[""])
removeListener("content", "");
recursiveAppend(element, content);
},
remove: () => element.remove(),
setCSSVariable(variableNameOrTable, value) {
if (typeof variableNameOrTable == "object") {
Object.entries(variableNameOrTable).forEach(([k, v]) => {
if (isReactiveSource(v)) {
addListener("cssVariable", k, v);
return;
}
setCSSVariable(k, v);
});
return;
}
if (listeners.cssVariable[variableNameOrTable])
removeListener("cssVariable", variableNameOrTable);
if (isReactiveSource(value)) {
addListener("cssVariable", variableNameOrTable, value);
return;
}
setCSSVariable(variableNameOrTable, value);
},
qsa(selector) {
const results = [];
element.querySelectorAll(selector).forEach((el) => results.push(el instanceof HTMLElement ? getWrappedElement(el) : el));
return results;
},
getRect: () => element.getBoundingClientRect(),
focus: () => element.focus(),
blur: () => element.blur(),
select: () => element.select(),
play: () => element.play(),
pause: () => element.pause(),
getContext(mode, options) {
return element.getContext(mode, options);
},
get content() {
return [].slice.call(element.children).map((child) => {
if (child instanceof HTMLElement)
return getWrappedElement(child);
return child;
});
},
set content(v) {
var _a;
if ((_a = listeners.content) === null || _a === void 0 ? void 0 : _a[""])
removeListener("content", "");
if (isReactiveSource(v)) {
addListener("content", "", v);
return;
}
element.innerHTML = "";
recursiveAppend(element, v);
},
attribs: new Proxy(element, attribsProxy),
get innerHTML() {
return element.innerHTML;
},
set innerHTML(v) {
element.innerHTML = v;
},
get value() {
return element.value;
},
set value(v) {
element.value = v;
},
get href() {
return element.href;
},
set href(v) {
element.href = v;
},
get src() {
return element.src;
},
set src(v) {
element.src = v;
},
get width() {
return element.width;
},
set width(v) {
element.width = v;
},
get height() {
return element.height;
},
set height(v) {
element.height = v;
},
get currentTime() {
return element.currentTime;
},
set currentTime(v) {
element.currentTime = v;
},
get paused() {
return element.paused;
},
get name() {
return element.getAttribute("name");
},
set name(v) {
if (v === null) {
element.removeAttribute("name");
}
else {
element.setAttribute("name", v);
}
},
style: new Proxy(setStyle, styleProxy),
classes: new ClassAccessor(element.classList, (className, stream) => addListener("class", className, stream), (classNames) => {
classNames.forEach(c => {
if (listeners.class[c])
removeListener("class", c);
});
}),
events: createEventsProxy(element),
};
elementWrapCache.set(element, domEntity);
}
return elementWrapCache.get(element);
}
export class ClassAccessor {
constructor(classList, listen, unlisten) {
this.classList = classList;
this.listen = listen;
this.unlisten = unlisten;
}
add(...className) {
this.unlisten(className);
this.classList.add(...className);
}
remove(...className) {
this.unlisten(className);
this.classList.remove(...className);
}
toggle(className, value) {
this.unlisten([className]);
if (isReactiveSource(value)) {
this.listen(className, value);
return;
}
return this.classList.toggle(className, value);
}
contains(className) {
return this.classList.contains(className);
}
get length() {
return this.classList.length;
}
get value() {
return this.classList.value;
}
toString() {
return this.classList.toString();
}
replace(token, newToken) {
this.unlisten([token, newToken]);
this.classList.replace(token, newToken);
}
forEach(cb) {
this.classList.forEach(cb);
}
map(cb) {
const result = [];
this.classList.forEach((v, i) => {
result.push(cb(v, i));
});
return result;
}
}
import { Dictionary, EmissionSource, EmitterLike, EventHandlerMap, EventSource, Handler, ListenFunc, Period, UnsubscribeFunc } from "./types";
export declare class EventEmitter<T> {
protected onListen: ListenFunc<T>;
constructor(onListen: ListenFunc<T>);
protected transform<R = T>(handler: (value: T, emit: (value: R) => void) => void): (fn: (v: R) => void) => UnsubscribeFunc;
/**
* Compatibility alias for `apply()` - registers a function to receive emitted values
* @param handler
* @returns A function to deregister the handler
*/
listen(handler: Handler<T>): UnsubscribeFunc;
/**
* Registers a function to receive emitted values
* @param handler
* @returns A function to deregister the handler
*/
apply(handler: Handler<T>): UnsubscribeFunc;
/**
* Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
* @param mapFunc
* @returns Listenable: emits transformed values
*/
map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
mapAsync<R>(mapFunc: (value: T) => Promise<R>): EventEmitter<R>;
as<R>(value: R): EventEmitter<R>;
/**
* Creates a chainable emitter that selectively forwards emissions along the chain
* @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
* @returns Listenable: emits values that pass the filter
*/
filter(check: (value: T) => boolean): EventEmitter<T>;
/**
* Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
* @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
*
* If no `compare` function is provided, values will be compared via `===`
* @returns Listenable: emits non-repeating values
*/
dedupe(compare?: (a: T, b: T) => boolean): EventEmitter<T>;
/**
* Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
*
* The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
* All listeners attached to the returned emitter receive the same values as the parent emitter.
*
* *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
*
* @param cb A function to be called as a side effect for each value emitted by the parent emitter.
* @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
*/
tap(cb: Handler<T>): EventEmitter<T>;
/**
* Immediately passes this emitter to a callback and returns this emitter
*
* Allows branching without breaking a composition chain
*
* @example
* ```ts
* range
* .tween("0%", "100%")
* .fork(branch => branch
* .map(s => `Loading: ${s}`)
* .apply(s => document.title = s)
* )
* .apply(v => progressBar.style.width = v);
* ```
* @param cb
*/
fork(...cb: ((branch: this) => void)[]): this;
/**
* Creates a chainable emitter that forwards the parent's last emission after a period of time in which the parent doesn't emit
* @param ms Delay in milliseconds
* @returns Debounced emitter
*/
debounce(ms: number): EventEmitter<T>;
debounce(period: Period): EventEmitter<T>;
/**
* Creates a chainable emitter that forwards the parent's emissions, with a minimum delay between emissions during which parent emssions are ignored
* @param ms Delay in milliseconds
* @returns Throttled emitter
*/
throttle(ms: number): EventEmitter<T>;
throttle(period: Period): EventEmitter<T>;
batch(ms: number): EventEmitter<T[]>;
/**
* Creates a chainable emitter that forwards the next emission from the parent
* **Experimental**: May change in future revisions
* Note: only listens to the parent while at least one downstream subscription is present
* @param notifier
* @returns
*/
once(): EventEmitter<T>;
once(handler: Handler<T>): UnsubscribeFunc;
getNext(): Promise<T>;
delay(ms: number): EventEmitter<T>;
delay(period: Period): EventEmitter<T>;
scan<S>(updater: (state: S, value: T) => S, initial: S): EventEmitter<S>;
buffer(count: number): EventEmitter<T[]>;
/**
* **Experimental**: May change in future revisions
* Note: only listens to the notifier while at least one downstream subscription is present
* @param limit
* @returns
*/
take(limit: number): EventEmitter<T>;
/**
* **Experimental**: May change in future revisions
* Note: only listens to the notifier while at least one downstream subscription is present
* @param notifier
* @returns
*/
takeUntil(notifier: EmitterLike<any>): EventEmitter<T>;
/**
* Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
* Disconnects from the parent and becomes inert when the predicate returns false
* @param predicate Callback to determine whether to keep forwarding
*/
takeWhile(predicate: (value: T) => boolean): EventEmitter<T>;
/**
* Creates a chainable emitter that immediately emits a value to every new subscriber,
* then forwards parent emissions
* @param value
* @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
*/
immediate(value: T): EventEmitter<T>;
/**
* Creates a chainable emitter that forwards its parent's emissions, and
* immediately emits the latest value to new subscribers
* @returns
*/
cached(): EventEmitter<T>;
/**
* Creates a chainable emitter that forwards emissions from the parent and any of the provided emitters
* @param emitters
*/
or(...emitters: EmitterLike<T>[]): EventEmitter<T>;
or<U>(...emitters: EmitterLike<U>[]): EventEmitter<T | U>;
memo(): Memo<T | undefined>;
memo(initial: T): Memo<T>;
memo<U>(initial: U): Memo<T | U>;
record(): EventRecorder<T>;
}
export declare class EventRecorder<T> {
private startTime;
private entries;
private recording;
private unsubscribe;
constructor(emitter: EventEmitter<T>);
private add;
stop(): EventRecording<T>;
}
export declare class EventRecording<T> {
private _entries;
constructor(entries: [number, T][]);
export(): [number, T][];
play(speed?: number): EventEmitter<T>;
}
type EmitEmitterPair<T> = {
emit: (value: T) => void;
emitter: EventEmitter<T>;
};
type CreateEventSourceOptions<T> = {
initialHandler?: Handler<T>;
/**
* Function to call when subscription count changes from 0
* Return a *deactivation* function, which will be called when subscription count changes back to 0
*/
activate?(): UnsubscribeFunc;
};
/**
* Creates a linked EventEmitter and emit() pair
* @example
* ```ts
* function createForm(options?: { onsubmit?: (data: FormData) => void }) {
* const submitEvents = createEventSource(options?.onsubmit);
* const form = $.form({
* on: {
* submit: (e) => {
* e.preventDefault();
* const data = new FormData(e.target);
* submitEvents.emit(data); // emit when form is submitted
* }
* }
* });
*
* return createEntity(form, {
* events: {
* submit: submitEvents.emitter
* }
* })
* }
*
* const form = createForm({
* onsubmit: (data) => handleSubmission(data)
* });
* ```
*
* @param initialHandler Optional listener automatically applied to the resulting Emitter
* @returns
*/
export declare function createEventSource<T>(initialHandler?: Handler<T>): EmitEmitterPair<T>;
export declare function createEventSource<T>(options?: CreateEventSourceOptions<T>): EmitEmitterPair<T>;
export declare function createEventsSource<Map extends Dictionary<any>>(initialListeners?: EventHandlerMap<Map>): {
emitters: import("./types").EventEmitterMap<Map>;
trigger: <K extends keyof Map>(name: K, value: Map[K]) => void;
};
export declare function interval(ms: number): EventEmitter<number>;
export declare function interval(period: Period): EventEmitter<number>;
/**
* Emits time deltas from a shared RAF loop
*/
export declare const animationFrames: EventEmitter<number>;
export declare function timeout(ms: number): EventEmitter<void>;
export declare function timeout(period: Period): EventEmitter<void>;
declare class Memo<T> {
private _value;
private unsubscribeFunc;
get value(): T;
constructor(source: EmitterLike<T>, initial: T);
dispose(): void;
}
export declare class SubjectEmitter<T> extends EventEmitter<T> {
private emit;
private _value;
constructor(initial: T);
get value(): T;
next(value: T): void;
}
/**
* Create an EventEmitter from an event source. Event source can be RxJS observable, existing `EventEmitter`, an object that
* provides a `subscribe()`/`listen() => UnsubscribeFunc` method, or a subscribe function itself.
* @param source
*/
export declare function toEventEmitter<E>(source: EmissionSource<E>): EventEmitter<E>;
/**
* Create an EventEmitter from an event provider and event name. Event source may provide matching `addEventListener`/`on(name, handler)` and `removeEventListener`/`off(name, handler)` methods, or `addEventListener`/`on(name, handler): UnsubscribeFunc.
* @param source
*/
export declare function toEventEmitter<E, N>(source: EventSource<E, N>, eventName: N): EventEmitter<E>;
type ExtractEmitterValue<T> = T extends EmitterLike<infer U> ? U : never;
type CombinedRecord<T extends Dictionary<EmitterLike<any>>> = {
readonly [K in keyof T]: ExtractEmitterValue<T[K]>;
};
export declare function combineEmitters<U extends Dictionary<EmitterLike<any>>>(emitters: U): EventEmitter<CombinedRecord<U>>;
export declare function combineEmitters<U extends EmitterLike<any>[]>(emitters: [...U]): EventEmitter<{
[K in keyof U]: ExtractEmitterValue<U[K]>;
}>;
export {};
import { createEventsProxy } from "./proxy.js";
import { isReactiveSource } from "./util";
function periodAsMilliseconds(t) {
if (typeof t == "number")
return t;
return "asMilliseconds" in t ? t.asMilliseconds : (t.asSeconds * 1000);
}
export class EventEmitter {
constructor(onListen) {
this.onListen = onListen;
}
transform(handler) {
const { emit, listen } = createListenable(() => this.onListen(value => {
handler(value, emit);
}));
return listen;
}
/**
* Compatibility alias for `apply()` - registers a function to receive emitted values
* @param handler
* @returns A function to deregister the handler
*/
listen(handler) {
return this.onListen(handler);
}
/**
* Registers a function to receive emitted values
* @param handler
* @returns A function to deregister the handler
*/
apply(handler) {
return this.onListen(handler);
}
/**
* Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
* @param mapFunc
* @returns Listenable: emits transformed values
*/
map(mapFunc) {
const listen = this.transform((value, emit) => emit(mapFunc(value)));
return new EventEmitter(listen);
}
mapAsync(mapFunc) {
const listen = this.transform((value, emit) => mapFunc(value).then(emit));
return new EventEmitter(listen);
}
as(value) {
const listen = this.transform((_, emit) => emit(value));
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that selectively forwards emissions along the chain
* @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
* @returns Listenable: emits values that pass the filter
*/
filter(check) {
const listen = this.transform((value, emit) => check(value) && emit(value));
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
* @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
*
* If no `compare` function is provided, values will be compared via `===`
* @returns Listenable: emits non-repeating values
*/
dedupe(compare) {
let previous = null;
const listen = this.transform((value, emit) => {
if (!previous || (compare
? !compare(previous.value, value)
: (previous.value !== value))) {
emit(value);
previous = { value };
}
});
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
*
* The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
* All listeners attached to the returned emitter receive the same values as the parent emitter.
*
* *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
*
* @param cb A function to be called as a side effect for each value emitted by the parent emitter.
* @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
*/
tap(cb) {
const listen = this.transform((value, emit) => {
cb(value);
emit(value);
});
return new EventEmitter(listen);
}
/**
* Immediately passes this emitter to a callback and returns this emitter
*
* Allows branching without breaking a composition chain
*
* @example
* ```ts
* range
* .tween("0%", "100%")
* .fork(branch => branch
* .map(s => `Loading: ${s}`)
* .apply(s => document.title = s)
* )
* .apply(v => progressBar.style.width = v);
* ```
* @param cb
*/
fork(...cb) {
cb.forEach(cb => cb(this));
return this;
}
debounce(t) {
let reset = null;
const listen = this.transform((value, emit) => {
reset === null || reset === void 0 ? void 0 : reset();
const timeout = setTimeout(() => {
reset = null;
emit(value);
}, periodAsMilliseconds(t));
reset = () => {
reset = null;
clearTimeout(timeout);
};
});
return new EventEmitter(listen);
}
throttle(t) {
let lastTime = -Infinity;
const listen = this.transform((value, emit) => {
const now = performance.now();
if (now >= lastTime + periodAsMilliseconds(t)) {
lastTime = now;
emit(value);
}
});
return new EventEmitter(listen);
}
batch(ms) {
let items = [];
let active = false;
const listen = this.transform((value, emit) => {
items.push(value);
if (!active) {
active = true;
setTimeout(() => {
emit(items);
items = [];
active = false;
}, ms);
}
});
return new EventEmitter(listen);
}
once(handler) {
let parentUnsubscribe = null;
let completed = false;
const clear = () => {
if (parentUnsubscribe) {
parentUnsubscribe();
parentUnsubscribe = null;
}
};
const { emit, listen } = createListenable(() => {
if (completed)
return;
parentUnsubscribe = this.apply(v => {
completed = true;
clear();
emit(v);
});
return clear;
});
const emitter = new EventEmitter(listen);
return handler
? emitter.apply(handler)
: emitter;
}
getNext() {
return new Promise((resolve) => this.once(resolve));
}
delay(t) {
const ms = periodAsMilliseconds(t);
return new EventEmitter(this.transform((value, emit) => {
return timeout(ms).apply(() => emit(value));
}));
}
scan(updater, initial) {
let state = initial;
const listen = this.transform((value, emit) => {
state = updater(state, value);
emit(state);
});
return new EventEmitter(listen);
}
buffer(count) {
let buffer = [];
const listen = this.transform((value, emit) => {
buffer.push(value);
if (buffer.length >= count) {
emit(buffer);
buffer = [];
}
});
return new EventEmitter(listen);
}
/**
* **Experimental**: May change in future revisions
* Note: only listens to the notifier while at least one downstream subscription is present
* @param limit
* @returns
*/
take(limit) {
let sourceUnsub = null;
let count = 0;
let completed = false;
const { emit, listen } = createListenable(() => {
if (completed)
return;
if (!sourceUnsub) {
sourceUnsub = this.apply(v => {
if (count < limit) {
emit(v);
count++;
if (count >= limit) {
completed = true;
if (sourceUnsub) {
sourceUnsub();
sourceUnsub = null;
}
}
}
});
}
return sourceUnsub;
});
return new EventEmitter(listen);
}
/**
* **Experimental**: May change in future revisions
* Note: only listens to the notifier while at least one downstream subscription is present
* @param notifier
* @returns
*/
takeUntil(notifier) {
let parentUnsubscribe = null;
let notifierUnsub = null;
let completed = false;
const clear = () => {
parentUnsubscribe === null || parentUnsubscribe === void 0 ? void 0 : parentUnsubscribe();
notifierUnsub === null || notifierUnsub === void 0 ? void 0 : notifierUnsub();
};
const { emit, listen } = createListenable(() => {
if (completed)
return;
parentUnsubscribe = this.apply(emit);
notifierUnsub = toEventEmitter(notifier).listen(() => {
completed = true;
clear();
});
return clear;
});
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
* Disconnects from the parent and becomes inert when the predicate returns false
* @param predicate Callback to determine whether to keep forwarding
*/
takeWhile(predicate) {
let parentUnsubscribe;
let completed = false;
const { emit, listen } = createListenable(() => {
if (completed)
return;
parentUnsubscribe = this.apply(v => {
if (predicate(v)) {
emit(v);
}
else {
completed = true;
parentUnsubscribe();
parentUnsubscribe = undefined;
}
});
return () => parentUnsubscribe === null || parentUnsubscribe === void 0 ? void 0 : parentUnsubscribe();
});
return new EventEmitter(listen);
}
/**
* Creates a chainable emitter that immediately emits a value to every new subscriber,
* then forwards parent emissions
* @param value
* @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
*/
immediate(value) {
return new EventEmitter(handle => {
handle(value);
return this.onListen(handle);
});
}
/**
* Creates a chainable emitter that forwards its parent's emissions, and
* immediately emits the latest value to new subscribers
* @returns
*/
cached() {
let cache = null;
const { listen, emit } = createListenable(() => this.onListen((value => {
cache = { value };
emit(value);
})));
return new EventEmitter(handler => {
if (cache)
handler(cache.value);
return listen(handler);
});
}
or(...emitters) {
return new EventEmitter(handler => {
const unsubs = [this, ...emitters].map(e => toEventEmitter(e).listen(handler));
return () => unsubs.forEach(unsub => unsub());
});
}
memo(initial) {
return new Memo(this, initial);
}
record() {
return new EventRecorder(this);
}
}
export class EventRecorder {
constructor(emitter) {
this.startTime = performance.now();
this.entries = [];
this.recording = true;
this.unsubscribe = emitter.listen(v => this.add(v));
}
add(value) {
const now = performance.now();
let time = now - this.startTime;
this.entries.push([time, value]);
}
stop() {
if (!this.recording) {
throw new Error("EventRecorder already stopped");
}
this.unsubscribe();
return new EventRecording(this.entries);
}
}
export class EventRecording {
constructor(entries) {
this._entries = entries;
}
export() {
return [...this._entries];
}
play(speed = 1) {
let idx = 0;
let elapsed = 0;
const { emit, listen } = createListenable();
const unsubscribe = animationFrames.listen((frameElapsed) => {
elapsed += frameElapsed * speed;
while (idx < this._entries.length && this._entries[idx][0] <= elapsed) {
emit(this._entries[idx][1]);
idx++;
}
if (idx >= this._entries.length) {
unsubscribe();
}
});
return new EventEmitter(listen);
}
}
export function createEventSource(arg) {
if (typeof arg === "function") {
arg = { initialHandler: arg };
}
const { initialHandler, activate } = arg !== null && arg !== void 0 ? arg : {};
const { emit, listen } = createListenable(activate);
if (initialHandler)
listen(initialHandler);
return {
emit,
emitter: new EventEmitter(listen)
};
}
export function createEventsSource(initialListeners) {
const handlers = {};
const emitters = createEventsProxy({
on: (name, handler) => {
if (!handlers[name])
handlers[name] = [];
const unique = { fn: handler };
handlers[name].push(unique);
return () => {
const idx = handlers[name].indexOf(unique);
handlers[name].splice(idx, 1);
if (handlers[name].length == 0)
delete handlers[name];
};
},
}, initialListeners);
return {
emitters,
trigger: (name, value) => {
var _a;
(_a = handlers[name]) === null || _a === void 0 ? void 0 : _a.forEach(entry => entry.fn(value));
}
};
}
function createListenable(sourceListen) {
const handlers = [];
let onRemoveLast;
const addListener = (fn) => {
const unique = { fn };
handlers.push(unique);
if (sourceListen && handlers.length == 1)
onRemoveLast = sourceListen();
return () => {
const idx = handlers.indexOf(unique);
if (idx === -1)
throw new Error("Handler already unsubscribed");
handlers.splice(idx, 1);
if (onRemoveLast && handlers.length == 0)
onRemoveLast();
};
};
return {
listen: addListener,
emit: (value) => handlers.forEach(h => h.fn(value)),
};
}
export function interval(t) {
let intervalId = null;
let idx = 0;
const { emit, listen } = createListenable(() => {
intervalId = setInterval(() => {
emit(idx++);
}, periodAsMilliseconds(t));
return () => clearInterval(intervalId);
});
return new EventEmitter(listen);
}
/**
* Emits time deltas from a shared RAF loop
*/
export const animationFrames = (() => {
const { emit, listen } = createListenable(() => {
let rafId = null;
let lastTime = null;
const frame = (time) => {
rafId = requestAnimationFrame(frame);
const elapsed = time - (lastTime !== null && lastTime !== void 0 ? lastTime : time);
lastTime = time;
emit(elapsed);
};
rafId = requestAnimationFrame(frame);
return () => cancelAnimationFrame(rafId);
});
return new EventEmitter(listen);
})();
export function timeout(t) {
const ms = periodAsMilliseconds(t);
const targetTime = Date.now() + ms;
const { emit, listen } = createListenable(() => {
const reminaingMs = targetTime - Date.now();
if (reminaingMs < 0)
return;
const timeoutId = setTimeout(() => {
emit();
}, reminaingMs);
return () => clearTimeout(timeoutId);
});
return new EventEmitter(listen);
}
class Memo {
get value() {
return this._value;
}
constructor(source, initial) {
this._value = initial;
const emitter = toEventEmitter(source);
this.unsubscribeFunc = emitter.listen(v => this._value = v);
}
dispose() {
this.unsubscribeFunc();
this.unsubscribeFunc = () => {
throw new Error("Memo object already disposed");
};
}
}
export class SubjectEmitter extends EventEmitter {
constructor(initial) {
const { emit, listen } = createListenable();
super(h => {
h(this._value); // immediate emit on listen
return listen(h);
});
this.emit = emit;
this._value = initial;
}
get value() {
return this._value;
}
next(value) {
this._value = value;
this.emit(value);
}
}
export function toEventEmitter(source, eventName) {
if (source instanceof EventEmitter)
return source;
if (typeof source == "function")
return new EventEmitter(source);
if (eventName !== undefined) {
// addEL()
if ("addEventListener" in source) {
if ("removeEventListener" in source && typeof source.removeEventListener == "function") {
return new EventEmitter(h => {
source.addEventListener(eventName, h);
return () => source.removeEventListener(eventName, h);
});
}
return new EventEmitter(h => {
return source.addEventListener(eventName, h);
});
}
// on()
if ("on" in source) {
if ("off" in source && typeof source.off == "function") {
return new EventEmitter(h => {
return source.on(eventName, h)
|| (() => source.off(eventName, h));
});
}
return new EventEmitter(h => {
return source.on(eventName, h);
});
}
}
if (isReactiveSource(source)) {
const subscribe = "subscribe" in source
? (h) => source.subscribe(h)
: (h) => source.listen(h);
return new EventEmitter(subscribe);
}
throw new Error("Invalid event source");
}
function combineArray(emitters) {
let values = Array.from({ length: emitters.length });
const { emit, listen } = createListenable(() => {
const unsubFuncs = emitters.map((emitter, idx) => {
return emitter.listen(v => {
values[idx] = { value: v };
if (values.every(v => v !== undefined))
emit(values.map(vc => vc.value));
});
});
return () => unsubFuncs.forEach(f => f());
});
return new EventEmitter(listen);
}
function combineRecord(emitters) {
const keys = Object.keys(emitters);
let values = {};
const { emit, listen } = createListenable(() => {
const unsubFuncs = keys.map(key => {
return emitters[key].listen(v => {
values[key] = { value: v };
if (keys.every(k => values[k] !== undefined)) {
const record = Object.fromEntries(Object.entries(values).map(([k, vc]) => [k, vc.value]));
emit(record);
}
});
});
return () => unsubFuncs.forEach(f => f());
});
return new EventEmitter(listen);
}
export function combineEmitters(emitters) {
if (Array.isArray(emitters))
return combineArray(emitters.map(toEventEmitter));
return combineRecord(Object.fromEntries(Object.entries(emitters).map(([k, e]) => [k, toEventEmitter(e)])));
}
import { SetGetStyleFunc, EventSource, EventEmitterMap, EventHandlerMap } from "./types";
export declare const styleProxy: ProxyHandler<SetGetStyleFunc>;
export declare const attribsProxy: ProxyHandler<HTMLElement>;
export declare function createEventsProxy<Map>(source: EventSource<any, keyof Map>, initialListeners?: EventHandlerMap<Map>): EventEmitterMap<Map>;
import { toEventEmitter } from "./emitter";
export const styleProxy = {
get(style, prop) {
return style(prop);
},
set(style, prop, value) {
style(prop, value);
return true;
},
apply(style, _, [stylesOrProp, value]) {
if (typeof stylesOrProp == "object") {
Object.entries(stylesOrProp).forEach(([prop, val]) => style(prop, val));
return;
}
style(stylesOrProp, value);
},
deleteProperty(style, prop) {
style(prop, null);
return true;
}
};
export const attribsProxy = {
get: (element, key) => {
return element.getAttribute(key);
},
set: (element, key, value) => {
element.setAttribute(key, value);
return true;
},
has: (element, key) => {
return element.hasAttribute(key);
},
ownKeys: (element) => {
return element.getAttributeNames();
},
};
const eventsProxyDefinition = {
get: (object, key) => {
return toEventEmitter(object, key);
}
};
export function createEventsProxy(source, initialListeners) {
const proxy = new Proxy(source, eventsProxyDefinition);
if (initialListeners) {
Object.entries(initialListeners)
.forEach(([name, handler]) => toEventEmitter(source, name).apply(handler));
}
return proxy;
}
import { ClassAccessor } from "./element";
import { EventEmitter } from "./emitter";
import { entityDataSymbol } from "./util";
export type ElementClassDescriptor = string | Record<string, boolean | EmitterLike<boolean> | undefined> | undefined | ElementClassDescriptor[];
export type DOMContent = number | null | string | Element | JelEntity<object> | Text | DOMContent[];
export type DomEntity<T extends HTMLElement> = JelEntity<ElementAPI<T>>;
export type HTMLTag = keyof HTMLElementTagNameMap;
export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
export type UnsubscribeFunc = () => void;
export type EmitterLike<T> = {
subscribe: ListenFunc<T>;
} | {
listen: ListenFunc<T>;
};
export type EmissionSource<T> = EmitterLike<T> | ListenFunc<T>;
export type CSSValue = string | number | null | HexCodeContainer;
export type CSSProperty = keyof StylesDescriptor;
type HexCodeContainer = {
hexCode: string;
toString(): string;
};
export type StylesDescriptor = {
[K in keyof CSSStyleDeclaration as [
K,
CSSStyleDeclaration[K]
] extends [string, string] ? K : never]+?: CSSValue | EmitterLike<CSSValue>;
};
export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | EmitterLike<CSSValue>) => void);
export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | EmitterLike<CSSValue>);
export type StyleAccessor = ((styles: StylesDescriptor) => void) & StylesDescriptor & SetStyleFunc;
type ContentlessTag = "area" | "br" | "hr" | "iframe" | "input" | "textarea" | "img" | "canvas" | "link" | "meta" | "source" | "embed" | "track" | "base";
type TagWithHref = "a" | "link" | "base";
type TagWithSrc = "img" | "script" | "iframe" | "video" | "audio" | "embed" | "source" | "track";
type TagWithValue = "input" | "textarea";
type TagWithWidthHeight = "canvas" | "img" | "embed" | "iframe" | "video";
type TagWithType = "input" | "source" | "button";
type TagWithName = 'input' | 'textarea' | 'select' | 'form';
type ContentlessElement = HTMLElementTagNameMap[ContentlessTag];
export type ElementDescriptor<Tag extends HTMLTag> = {
classes?: ElementClassDescriptor;
attribs?: Record<string, string | number | boolean | undefined>;
on?: {
[E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
};
style?: StylesDescriptor;
cssVariables?: Record<string, CSSValue | EmitterLike<CSSValue>>;
init?: (entity: DomEntity<HTMLElementTagNameMap[Tag]>) => void;
} & (Tag extends TagWithValue ? {
value?: string | number;
} : {}) & (Tag extends ContentlessTag ? {} : {
content?: DOMContent | EmitterLike<DOMContent>;
}) & (Tag extends TagWithSrc ? {
src?: string;
} : {}) & (Tag extends TagWithHref ? {
href?: string;
} : {}) & (Tag extends TagWithWidthHeight ? {
width?: number;
height?: number;
} : {}) & (Tag extends TagWithType ? {
type?: string;
} : {}) & (Tag extends TagWithName ? {
name?: string;
} : {});
type ElementAPI<T extends HTMLElement> = {
readonly element: T;
readonly classes: ClassAccessor;
readonly attribs: {
[key: string]: string | null;
};
readonly events: EventEmitterMap<HTMLElementEventMap>;
readonly style: StyleAccessor;
setCSSVariable(variableName: string, value: CSSValue | EmitterLike<CSSValue>): void;
setCSSVariable(table: Record<string, CSSValue | EmitterLike<CSSValue>>): void;
qsa(selector: string): (Element | DomEntity<HTMLElement>)[];
remove(): void;
getRect(): DOMRect;
focus(): void;
blur(): void;
/**
* Add an event listener
* @param eventId
* @param handler
* @returns Function to remove the listener
* @deprecated Use ent.events
*/
on<E extends keyof HTMLElementEventMap>(eventId: E, handler: (this: ElementAPI<T>, data: HTMLElementEventMap[E]) => void): UnsubscribeFunc;
} & (T extends ContentlessElement ? {} : {
append(...content: DOMContent[]): void;
innerHTML: string;
content: DOMContent | EmitterLike<DOMContent>;
}) & (T extends HTMLElementTagNameMap[TagWithValue] ? {
value: string;
select(): void;
} : {}) & (T extends HTMLCanvasElement ? {
width: number;
height: number;
getContext: HTMLCanvasElement["getContext"];
} : {}) & (T extends HTMLElementTagNameMap[TagWithSrc] ? {
src: string;
} : {}) & (T extends HTMLElementTagNameMap[TagWithHref] ? {
href: string;
} : {}) & (T extends HTMLMediaElement ? {
play(): void;
pause(): void;
currentTime: number;
readonly paused: boolean;
} : {}) & (T extends HTMLElementTagNameMap[TagWithName] ? {
name: string | null;
} : {});
export type DomHelper = ((
/**
* Creates an element of the specified tag
*/
<T extends HTMLTag>(tagName: T, descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>) & (
/**
* Creates an element of the specified tag
*/
<T extends HTMLTag>(tagName: T) => DomEntity<HTMLElementTagNameMap[T]>) & (
/**
* Creates an element with ID and classes as specified by a selector-like string
*/
<T extends HTMLTag>(selector: `${T}#${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
/**
* Creates an element with ID and classes as specified by a selector-like string
*/
<T extends HTMLTag>(selector: `${T}.${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
/**
* Wraps an existing element as a DomEntity
*/
<T extends HTMLElement>(element: T) => DomEntity<T>) & {
[T in HTMLTag]: (descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>;
} & {
[T in HTMLTag]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (((content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & ((contentEmitter: EmitterLike<DOMContent>) => DomEntity<HTMLElementTagNameMap[T]>));
});
type JelEntityData = {
dom: DOMContent;
};
export type JelEntity<API extends object | void> = (API extends void ? {} : API) & {
readonly [entityDataSymbol]: JelEntityData;
};
export type Handler<T> = (value: T) => void;
export type Period = {
asMilliseconds: number;
} | {
asSeconds: number;
};
export type EventSource<E, N> = {
on: (eventName: N, handler: Handler<E>) => UnsubscribeFunc;
} | {
on: (eventName: N, handler: Handler<E>) => void | UnsubscribeFunc;
off: (eventName: N, handler: Handler<E>) => void;
} | {
addEventListener: (eventName: N, handler: Handler<E>) => UnsubscribeFunc;
} | {
addEventListener: (eventName: N, handler: Handler<E>) => void;
removeEventListener: (eventName: N, handler: Handler<E>) => void;
};
export type Dictionary<T> = Record<string | symbol, T>;
export type EventEmitterMap<Map> = {
[K in keyof Map]: EventEmitter<Map[K]>;
};
export type EventHandlerMap<Map> = {
[K in keyof Map]?: (value: Map[K]) => void;
};
export {};
import { entityDataSymbol } from "./util";
import { DOMContent, ElementDescriptor, EmitterLike, HTMLTag, JelEntity } from "./types";
export declare const entityDataSymbol: unique symbol;
export declare const isContent: <T extends HTMLTag>(value: DOMContent | ElementDescriptor<T> | undefined) => value is DOMContent;
export declare function isJelEntity(content: DOMContent): content is JelEntity<object>;
/**
* Wraps an object such that it can be appended as DOM content while retaining its original API
* @param content
* @param api
*/
export declare function createEntity<API extends object>(content: DOMContent, api: API extends DOMContent ? never : API): JelEntity<API>;
export declare function createEntity(content: DOMContent): JelEntity<void>;
export declare function isReactiveSource(value: any): value is EmitterLike<any>;
export const entityDataSymbol = Symbol("jelComponentData");
export const isContent = (value) => {
if (value === undefined)
return false;
return typeof value == "string"
|| typeof value == "number"
|| !value
|| value instanceof Element
|| value instanceof Text
|| entityDataSymbol in value
|| (Array.isArray(value) && value.every(isContent));
};
export function isJelEntity(content) {
return typeof content == "object" && !!content && entityDataSymbol in content;
}
export function createEntity(content, api) {
if (isContent(api)) {
throw new TypeError("API object is already valid content");
}
return Object.create(api !== null && api !== void 0 ? api : {}, {
[entityDataSymbol]: {
value: {
dom: content
}
},
});
}
;
export function isReactiveSource(value) {
return typeof value == "object" && value && (("listen" in value && typeof value.listen == "function")
|| ("subscribe" in value && typeof value.subscribe == "function"));
}