| 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[]; | ||
| } |
+405
| 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; | ||
| } | ||
| } |
+248
| 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 {}; |
+591
| 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>; |
+49
| 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; | ||
| } |
+165
| 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>; |
+32
| 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>; |
-8
| 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")); | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
65516
5.31%16
14.29%1546
0.19%307
60.73%4
Infinity%1
Infinity%