@xtia/jel 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/index.d.ts +4 -2
- package/index.js +4 -1
- package/internal/element.d.ts +1 -0
- package/internal/element.js +12 -12
- package/internal/emitter.d.ts +65 -28
- package/internal/emitter.js +135 -59
- package/internal/proxy.d.ts +2 -2
- package/internal/proxy.js +11 -9
- package/internal/types.d.ts +36 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,14 +65,14 @@ function showDialogue(content: DOMContent) => {
|
|
|
65
65
|
|
|
66
66
|
interface Job {
|
|
67
67
|
name: string;
|
|
68
|
-
completionMessage: DOMContent;
|
|
68
|
+
completionMessage: () => DOMContent;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
showDialogue("Hello, world");
|
|
72
72
|
showDialogue(["Hello, ", $.i("world")]);
|
|
73
73
|
showDialogue([
|
|
74
74
|
$.h2(`${job.name} Complete`),
|
|
75
|
-
$.p(job.completionMessage),
|
|
75
|
+
$.p(job.completionMessage()),
|
|
76
76
|
]);
|
|
77
77
|
```
|
|
78
78
|
|
|
@@ -130,15 +130,15 @@ showDialogue(["Hello ", $("span.green", "world")]);
|
|
|
130
130
|
Event emitters can be chained:
|
|
131
131
|
|
|
132
132
|
```ts
|
|
133
|
-
|
|
133
|
+
div.events.mousemove
|
|
134
134
|
.takeUntil(body.events.mousedown.filter(e => e.button === 1))
|
|
135
135
|
.map(ev => [ev.offsetX, ev.offsetY])
|
|
136
136
|
.apply(([x, y]) => console.log("mouse @ ", x, y));
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
For RxJS users, events can be observed with `fromEvent(element
|
|
139
|
+
For RxJS users, events can be observed with `fromEvent(ent.element, "mousemove")`.
|
|
140
140
|
|
|
141
|
-
## Reactive
|
|
141
|
+
## Reactive properties
|
|
142
142
|
|
|
143
143
|
Style properties, content and class presence can be emitter subscriptions:
|
|
144
144
|
|
|
@@ -165,6 +165,20 @@ virtualCursor.classes.toggle(
|
|
|
165
165
|
h1.content = websocket$
|
|
166
166
|
.filter(msg => msg.type == "title")
|
|
167
167
|
.map(msg => msg.text);
|
|
168
|
+
|
|
169
|
+
const searchInput = $("input.search");
|
|
170
|
+
const searchResults$ = searchInput.events.input
|
|
171
|
+
.debounce(300)
|
|
172
|
+
.map(() => searchInput.value)
|
|
173
|
+
.filter(term => term.length >= 2)
|
|
174
|
+
.mapAsync(term => performSearch(term)); // Returns emitter of search results
|
|
175
|
+
|
|
176
|
+
// Then use it reactively
|
|
177
|
+
$.ul({
|
|
178
|
+
content: searchResults$.map(results =>
|
|
179
|
+
results.map(result => $.li(result.title))
|
|
180
|
+
)
|
|
181
|
+
});
|
|
168
182
|
```
|
|
169
183
|
Removing an element from the page will unsubscribe from any attached stream, and resubscribe if subsequently appended.
|
|
170
184
|
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export { DomEntity, ElementClassDescriptor, ElementDescriptor, DOMContent, DomHelper, StyleAccessor, JelEntity } from "./internal/types";
|
|
2
1
|
import { $ } from "./internal/element";
|
|
3
2
|
import { DomEntity } from "./internal/types";
|
|
3
|
+
export { DomEntity, ElementClassDescriptor, ElementDescriptor, DOMContent, DomHelper, StyleAccessor, JelEntity, EventEmitterMap } from "./internal/types";
|
|
4
4
|
export { createEntity } from "./internal/util";
|
|
5
|
-
export { createEventSource, interval, timeout, SubjectEmitter, toEventEmitter, type EventEmitter, combineEmitters } from "./internal/emitter";
|
|
5
|
+
export { createEventSource, createEventsSource, interval, timeout, SubjectEmitter, toEventEmitter, type EventEmitter, type EventRecording, type EventRecorder, combineEmitters } from "./internal/emitter";
|
|
6
|
+
export { createEventsProxy } from "./internal/proxy";
|
|
6
7
|
export { $ };
|
|
7
8
|
export declare const $body: DomEntity<HTMLElement>;
|
|
9
|
+
export declare const windowEvents: import(".").EventEmitterMap<WindowEventMap>;
|
package/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { $ } from "./internal/element";
|
|
2
|
+
import { createEventsProxy } from "./internal/proxy";
|
|
2
3
|
export { createEntity } from "./internal/util";
|
|
3
|
-
export { createEventSource, interval, timeout, SubjectEmitter, toEventEmitter, combineEmitters } from "./internal/emitter";
|
|
4
|
+
export { createEventSource, createEventsSource, interval, timeout, SubjectEmitter, toEventEmitter, combineEmitters } from "./internal/emitter";
|
|
5
|
+
export { createEventsProxy } from "./internal/proxy";
|
|
4
6
|
export { $ };
|
|
5
7
|
export const $body = "document" in globalThis ? $(document.body) : undefined;
|
|
8
|
+
export const windowEvents = createEventsProxy(window);
|
package/internal/element.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class ClassAccessor {
|
|
|
11
11
|
toggle(className: string, value: EmitterLike<boolean>): void;
|
|
12
12
|
contains(className: string): boolean;
|
|
13
13
|
get length(): number;
|
|
14
|
+
get value(): string;
|
|
14
15
|
toString(): string;
|
|
15
16
|
replace(token: string, newToken: string): void;
|
|
16
17
|
forEach(cb: (token: string, idx: number) => void): void;
|
package/internal/element.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toEventEmitter } from "./emitter.js";
|
|
2
|
+
import { attribsProxy, createEventsProxy, styleProxy } from "./proxy";
|
|
2
3
|
import { entityDataSymbol, isContent, isJelEntity, isReactiveSource } from "./util";
|
|
3
4
|
const elementWrapCache = new WeakMap();
|
|
4
5
|
const recursiveAppend = (parent, c) => {
|
|
@@ -17,7 +18,7 @@ const recursiveAppend = (parent, c) => {
|
|
|
17
18
|
parent.append(c);
|
|
18
19
|
};
|
|
19
20
|
function createElement(tag, descriptor = {}) {
|
|
20
|
-
if (isContent(descriptor))
|
|
21
|
+
if (isContent(descriptor) || isReactiveSource(descriptor))
|
|
21
22
|
descriptor = {
|
|
22
23
|
content: descriptor,
|
|
23
24
|
};
|
|
@@ -200,8 +201,8 @@ function getWrappedElement(element) {
|
|
|
200
201
|
if (listeners.style[prop])
|
|
201
202
|
removeListener("style", prop);
|
|
202
203
|
if (typeof value == "object" && value) {
|
|
203
|
-
if (
|
|
204
|
-
addListener("style", prop, value);
|
|
204
|
+
if (isReactiveSource(value)) {
|
|
205
|
+
addListener("style", prop, toEventEmitter(value));
|
|
205
206
|
return;
|
|
206
207
|
}
|
|
207
208
|
value = value.toString();
|
|
@@ -347,7 +348,7 @@ function getWrappedElement(element) {
|
|
|
347
348
|
removeListener("class", c);
|
|
348
349
|
});
|
|
349
350
|
}),
|
|
350
|
-
events:
|
|
351
|
+
events: createEventsProxy(element),
|
|
351
352
|
};
|
|
352
353
|
elementWrapCache.set(element, domEntity);
|
|
353
354
|
}
|
|
@@ -381,6 +382,9 @@ export class ClassAccessor {
|
|
|
381
382
|
get length() {
|
|
382
383
|
return this.classList.length;
|
|
383
384
|
}
|
|
385
|
+
get value() {
|
|
386
|
+
return this.classList.value;
|
|
387
|
+
}
|
|
384
388
|
toString() {
|
|
385
389
|
return this.classList.toString();
|
|
386
390
|
}
|
|
@@ -393,13 +397,9 @@ export class ClassAccessor {
|
|
|
393
397
|
}
|
|
394
398
|
map(cb) {
|
|
395
399
|
const result = [];
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const [idx, value] = entry.value;
|
|
400
|
-
result.push(cb(value, idx));
|
|
401
|
-
entry = entries.next();
|
|
402
|
-
}
|
|
400
|
+
this.classList.forEach((v, i) => {
|
|
401
|
+
result.push(cb(v, i));
|
|
402
|
+
});
|
|
403
403
|
return result;
|
|
404
404
|
}
|
|
405
405
|
}
|
package/internal/emitter.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { EmitterLike } from "./types";
|
|
2
|
-
type Handler<T> = (value: T) => void;
|
|
1
|
+
import { Dictionary, EmissionSource, EmitterLike, EventHandlerMap, EventSource, Handler, Period } from "./types";
|
|
3
2
|
export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
|
|
4
3
|
export type UnsubscribeFunc = () => void;
|
|
5
4
|
export declare class EventEmitter<T> {
|
|
@@ -24,6 +23,8 @@ export declare class EventEmitter<T> {
|
|
|
24
23
|
* @returns Listenable: emits transformed values
|
|
25
24
|
*/
|
|
26
25
|
map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
|
|
26
|
+
mapAsync<R>(mapFunc: (value: T) => Promise<R>): EventEmitter<R>;
|
|
27
|
+
as<R>(value: R): EventEmitter<R>;
|
|
27
28
|
/**
|
|
28
29
|
* Creates a chainable emitter that selectively forwards emissions along the chain
|
|
29
30
|
* @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
|
|
@@ -68,18 +69,33 @@ export declare class EventEmitter<T> {
|
|
|
68
69
|
* @param cb
|
|
69
70
|
*/
|
|
70
71
|
fork(...cb: ((branch: this) => void)[]): this;
|
|
72
|
+
/**
|
|
73
|
+
* Creates a chainable emitter that forwards the parent's last emission after a period of time in which the parent doesn't emit
|
|
74
|
+
* @param ms Delay in milliseconds
|
|
75
|
+
* @returns Debounced emitter
|
|
76
|
+
*/
|
|
71
77
|
debounce(ms: number): EventEmitter<T>;
|
|
78
|
+
debounce(period: Period): EventEmitter<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Creates a chainable emitter that forwards the parent's emissions, with a minimum delay between emissions during which parent emssions are ignored
|
|
81
|
+
* @param ms Delay in milliseconds
|
|
82
|
+
* @returns Throttled emitter
|
|
83
|
+
*/
|
|
72
84
|
throttle(ms: number): EventEmitter<T>;
|
|
85
|
+
throttle(period: Period): EventEmitter<T>;
|
|
73
86
|
batch(ms: number): EventEmitter<T[]>;
|
|
74
87
|
/**
|
|
75
|
-
* Creates a chainable emitter that
|
|
88
|
+
* Creates a chainable emitter that forwards the next emission from the parent
|
|
76
89
|
* **Experimental**: May change in future revisions
|
|
77
90
|
* Note: only listens to the parent while at least one downstream subscription is present
|
|
78
91
|
* @param notifier
|
|
79
92
|
* @returns
|
|
80
93
|
*/
|
|
81
94
|
once(): EventEmitter<T>;
|
|
95
|
+
once(handler: Handler<T>): UnsubscribeFunc;
|
|
96
|
+
getNext(): Promise<T>;
|
|
82
97
|
delay(ms: number): EventEmitter<T>;
|
|
98
|
+
delay(period: Period): EventEmitter<T>;
|
|
83
99
|
scan<S>(updater: (state: S, value: T) => S, initial: S): EventEmitter<S>;
|
|
84
100
|
buffer(count: number): EventEmitter<T[]>;
|
|
85
101
|
/**
|
|
@@ -124,7 +140,35 @@ export declare class EventEmitter<T> {
|
|
|
124
140
|
memo(): Memo<T | undefined>;
|
|
125
141
|
memo(initial: T): Memo<T>;
|
|
126
142
|
memo<U>(initial: U): Memo<T | U>;
|
|
143
|
+
record(): EventRecorder<T>;
|
|
144
|
+
}
|
|
145
|
+
export declare class EventRecorder<T> {
|
|
146
|
+
private startTime;
|
|
147
|
+
private entries;
|
|
148
|
+
private recording;
|
|
149
|
+
private unsubscribe;
|
|
150
|
+
constructor(emitter: EventEmitter<T>);
|
|
151
|
+
private add;
|
|
152
|
+
stop(): EventRecording<T>;
|
|
127
153
|
}
|
|
154
|
+
export declare class EventRecording<T> {
|
|
155
|
+
private _entries;
|
|
156
|
+
constructor(entries: [number, T][]);
|
|
157
|
+
export(): [number, T][];
|
|
158
|
+
play(speed?: number): EventEmitter<T>;
|
|
159
|
+
}
|
|
160
|
+
type EmitEmitterPair<T> = {
|
|
161
|
+
emit: (value: T) => void;
|
|
162
|
+
emitter: EventEmitter<T>;
|
|
163
|
+
};
|
|
164
|
+
type CreateEventSourceOptions<T> = {
|
|
165
|
+
initialHandler?: Handler<T>;
|
|
166
|
+
/**
|
|
167
|
+
* Function to call when subscription count changes from 0
|
|
168
|
+
* Return a *deactivation* function, which will be called when subscription count changes back to 0
|
|
169
|
+
*/
|
|
170
|
+
activate?(): UnsubscribeFunc;
|
|
171
|
+
};
|
|
128
172
|
/**
|
|
129
173
|
* Creates a linked EventEmitter and emit() pair
|
|
130
174
|
* @example
|
|
@@ -156,16 +200,17 @@ export declare class EventEmitter<T> {
|
|
|
156
200
|
* @param initialHandler Optional listener automatically applied to the resulting Emitter
|
|
157
201
|
* @returns
|
|
158
202
|
*/
|
|
159
|
-
export declare function createEventSource<T>(initialHandler?: Handler<T>):
|
|
160
|
-
|
|
161
|
-
|
|
203
|
+
export declare function createEventSource<T>(initialHandler?: Handler<T>): EmitEmitterPair<T>;
|
|
204
|
+
export declare function createEventSource<T>(options?: CreateEventSourceOptions<T>): EmitEmitterPair<T>;
|
|
205
|
+
export declare function createEventsSource<Map extends Dictionary<any>>(initialListeners?: EventHandlerMap<Map>): {
|
|
206
|
+
emitters: import("./types").EventEmitterMap<Map>;
|
|
207
|
+
trigger: <K extends keyof Map>(name: K, value: Map[K]) => void;
|
|
162
208
|
};
|
|
163
|
-
export declare function interval(ms: number
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
export declare function timeout(
|
|
167
|
-
|
|
168
|
-
}): EventEmitter<void>;
|
|
209
|
+
export declare function interval(ms: number): EventEmitter<number>;
|
|
210
|
+
export declare function interval(period: Period): EventEmitter<number>;
|
|
211
|
+
export declare const animationFrames: EventEmitter<number>;
|
|
212
|
+
export declare function timeout(ms: number): EventEmitter<void>;
|
|
213
|
+
export declare function timeout(period: Period): EventEmitter<void>;
|
|
169
214
|
declare class Memo<T> {
|
|
170
215
|
private _value;
|
|
171
216
|
private unsubscribeFunc;
|
|
@@ -180,25 +225,17 @@ export declare class SubjectEmitter<T> extends EventEmitter<T> {
|
|
|
180
225
|
get value(): T;
|
|
181
226
|
next(value: T): void;
|
|
182
227
|
}
|
|
183
|
-
type EventSource<T, E extends string> = {
|
|
184
|
-
on: (eventName: E, handler: (value: T) => void) => UnsubscribeFunc;
|
|
185
|
-
} | {
|
|
186
|
-
on: (eventName: E, handler: (value: T) => void) => void | UnsubscribeFunc;
|
|
187
|
-
off: (eventName: E, handler: (value: T) => void) => void;
|
|
188
|
-
} | {
|
|
189
|
-
addEventListener: (eventName: E, handler: (value: T) => void) => UnsubscribeFunc;
|
|
190
|
-
} | {
|
|
191
|
-
addEventListener: (eventName: E, handler: (value: T) => void) => void | UnsubscribeFunc;
|
|
192
|
-
removeEventListener: (eventName: E, handler: (value: T) => void) => void;
|
|
193
|
-
};
|
|
194
228
|
/**
|
|
195
|
-
* Create an EventEmitter from an event source. Event
|
|
196
|
-
*
|
|
229
|
+
* Create an EventEmitter from an event source. Event source can be RxJS observable, existing `EventEmitter`, an object that
|
|
230
|
+
* provides a `subscribe()`/`listen() => UnsubscribeFunc` method, or a subscribe function itself.
|
|
231
|
+
* @param source
|
|
232
|
+
*/
|
|
233
|
+
export declare function toEventEmitter<E>(source: EmissionSource<E>): EventEmitter<E>;
|
|
234
|
+
/**
|
|
235
|
+
* 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.
|
|
197
236
|
* @param source
|
|
198
237
|
*/
|
|
199
|
-
export declare function toEventEmitter<
|
|
200
|
-
export declare function toEventEmitter<T, E extends string>(source: EventSource<T, E>, eventName: E): EventEmitter<T>;
|
|
201
|
-
type Dictionary<T> = Record<string | symbol, T>;
|
|
238
|
+
export declare function toEventEmitter<E, N>(source: EventSource<E, N>, eventName: N): EventEmitter<E>;
|
|
202
239
|
type ExtractEmitterValue<T> = T extends EmitterLike<infer U> ? U : never;
|
|
203
240
|
type CombinedRecord<T extends Dictionary<EmitterLike<any>>> = {
|
|
204
241
|
readonly [K in keyof T]: ExtractEmitterValue<T[K]>;
|
package/internal/emitter.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { createEventsProxy } from "./proxy.js";
|
|
1
2
|
import { isReactiveSource } from "./util";
|
|
3
|
+
const NOOP = () => { };
|
|
4
|
+
function periodAsMilliseconds(t) {
|
|
5
|
+
if (typeof t == "number")
|
|
6
|
+
return t;
|
|
7
|
+
return "asMilliseconds" in t ? t.asMilliseconds : (t.asSeconds * 1000);
|
|
8
|
+
}
|
|
2
9
|
export class EventEmitter {
|
|
3
10
|
constructor(onListen) {
|
|
4
11
|
this.onListen = onListen;
|
|
@@ -34,6 +41,14 @@ export class EventEmitter {
|
|
|
34
41
|
const listen = this.transform((value, emit) => emit(mapFunc(value)));
|
|
35
42
|
return new EventEmitter(listen);
|
|
36
43
|
}
|
|
44
|
+
mapAsync(mapFunc) {
|
|
45
|
+
const listen = this.transform((value, emit) => mapFunc(value).then(emit));
|
|
46
|
+
return new EventEmitter(listen);
|
|
47
|
+
}
|
|
48
|
+
as(value) {
|
|
49
|
+
const listen = this.transform((_, emit) => emit(value));
|
|
50
|
+
return new EventEmitter(listen);
|
|
51
|
+
}
|
|
37
52
|
/**
|
|
38
53
|
* Creates a chainable emitter that selectively forwards emissions along the chain
|
|
39
54
|
* @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
|
|
@@ -101,14 +116,14 @@ export class EventEmitter {
|
|
|
101
116
|
cb.forEach(cb => cb(this));
|
|
102
117
|
return this;
|
|
103
118
|
}
|
|
104
|
-
debounce(
|
|
119
|
+
debounce(t) {
|
|
105
120
|
let reset = null;
|
|
106
121
|
const listen = this.transform((value, emit) => {
|
|
107
122
|
reset === null || reset === void 0 ? void 0 : reset();
|
|
108
123
|
const timeout = setTimeout(() => {
|
|
109
124
|
reset = null;
|
|
110
125
|
emit(value);
|
|
111
|
-
},
|
|
126
|
+
}, periodAsMilliseconds(t));
|
|
112
127
|
reset = () => {
|
|
113
128
|
reset = null;
|
|
114
129
|
clearTimeout(timeout);
|
|
@@ -116,11 +131,11 @@ export class EventEmitter {
|
|
|
116
131
|
});
|
|
117
132
|
return new EventEmitter(listen);
|
|
118
133
|
}
|
|
119
|
-
throttle(
|
|
134
|
+
throttle(t) {
|
|
120
135
|
let lastTime = -Infinity;
|
|
121
136
|
const listen = this.transform((value, emit) => {
|
|
122
137
|
const now = performance.now();
|
|
123
|
-
if (now >= lastTime +
|
|
138
|
+
if (now >= lastTime + periodAsMilliseconds(t)) {
|
|
124
139
|
lastTime = now;
|
|
125
140
|
emit(value);
|
|
126
141
|
}
|
|
@@ -143,14 +158,7 @@ export class EventEmitter {
|
|
|
143
158
|
});
|
|
144
159
|
return new EventEmitter(listen);
|
|
145
160
|
}
|
|
146
|
-
|
|
147
|
-
* Creates a chainable emitter that
|
|
148
|
-
* **Experimental**: May change in future revisions
|
|
149
|
-
* Note: only listens to the parent while at least one downstream subscription is present
|
|
150
|
-
* @param notifier
|
|
151
|
-
* @returns
|
|
152
|
-
*/
|
|
153
|
-
once() {
|
|
161
|
+
once(handler) {
|
|
154
162
|
let parentUnsubscribe = null;
|
|
155
163
|
let completed = false;
|
|
156
164
|
const clear = () => {
|
|
@@ -169,11 +177,18 @@ export class EventEmitter {
|
|
|
169
177
|
});
|
|
170
178
|
return clear;
|
|
171
179
|
});
|
|
172
|
-
|
|
180
|
+
const emitter = new EventEmitter(listen);
|
|
181
|
+
return handler
|
|
182
|
+
? emitter.apply(handler)
|
|
183
|
+
: emitter;
|
|
184
|
+
}
|
|
185
|
+
getNext() {
|
|
186
|
+
return new Promise((resolve) => this.once(resolve));
|
|
173
187
|
}
|
|
174
|
-
delay(
|
|
188
|
+
delay(t) {
|
|
189
|
+
const ms = periodAsMilliseconds(t);
|
|
175
190
|
return new EventEmitter(this.transform((value, emit) => {
|
|
176
|
-
|
|
191
|
+
return timeout(ms).apply(() => emit(value));
|
|
177
192
|
}));
|
|
178
193
|
}
|
|
179
194
|
scan(updater, initial) {
|
|
@@ -245,11 +260,10 @@ export class EventEmitter {
|
|
|
245
260
|
if (completed)
|
|
246
261
|
return;
|
|
247
262
|
parentUnsubscribe = this.apply(emit);
|
|
248
|
-
|
|
263
|
+
notifierUnsub = toEventEmitter(notifier).listen(() => {
|
|
249
264
|
completed = true;
|
|
250
265
|
clear();
|
|
251
|
-
};
|
|
252
|
-
notifierUnsub = toEventEmitter(notifier).listen(handler);
|
|
266
|
+
});
|
|
253
267
|
return clear;
|
|
254
268
|
});
|
|
255
269
|
return new EventEmitter(listen);
|
|
@@ -317,40 +331,60 @@ export class EventEmitter {
|
|
|
317
331
|
memo(initial) {
|
|
318
332
|
return new Memo(this, initial);
|
|
319
333
|
}
|
|
334
|
+
record() {
|
|
335
|
+
return new EventRecorder(this);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export class EventRecorder {
|
|
339
|
+
constructor(emitter) {
|
|
340
|
+
this.startTime = performance.now();
|
|
341
|
+
this.entries = [];
|
|
342
|
+
this.recording = true;
|
|
343
|
+
this.unsubscribe = emitter.listen(v => this.add(v));
|
|
344
|
+
}
|
|
345
|
+
add(value) {
|
|
346
|
+
const now = performance.now();
|
|
347
|
+
let time = now - this.startTime;
|
|
348
|
+
this.entries.push([time, value]);
|
|
349
|
+
}
|
|
350
|
+
stop() {
|
|
351
|
+
if (!this.recording) {
|
|
352
|
+
throw new Error("EventRecorder already stopped");
|
|
353
|
+
}
|
|
354
|
+
this.unsubscribe();
|
|
355
|
+
return new EventRecording(this.entries);
|
|
356
|
+
}
|
|
320
357
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
*
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
*/
|
|
352
|
-
export function createEventSource(initialHandler) {
|
|
353
|
-
const { emit, listen } = createListenable();
|
|
358
|
+
export class EventRecording {
|
|
359
|
+
constructor(entries) {
|
|
360
|
+
this._entries = entries;
|
|
361
|
+
}
|
|
362
|
+
export() {
|
|
363
|
+
return [...this._entries];
|
|
364
|
+
}
|
|
365
|
+
play(speed = 1) {
|
|
366
|
+
let idx = 0;
|
|
367
|
+
let elapsed = 0;
|
|
368
|
+
const { emit, listen } = createListenable();
|
|
369
|
+
const unsubscribe = animationFrames.listen((frameElapsed) => {
|
|
370
|
+
elapsed += frameElapsed * speed;
|
|
371
|
+
while (idx < this._entries.length && this._entries[idx][0] <= elapsed) {
|
|
372
|
+
emit(this._entries[idx][1]);
|
|
373
|
+
idx++;
|
|
374
|
+
}
|
|
375
|
+
if (idx >= this._entries.length) {
|
|
376
|
+
unsubscribe();
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
return new EventEmitter(listen);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
export function createEventSource(arg) {
|
|
383
|
+
if (typeof arg === "function") {
|
|
384
|
+
arg = { initialHandler: arg };
|
|
385
|
+
}
|
|
386
|
+
const { initialHandler, activate } = arg !== null && arg !== void 0 ? arg : {};
|
|
387
|
+
const { emit, listen } = createListenable(activate);
|
|
354
388
|
if (initialHandler)
|
|
355
389
|
listen(initialHandler);
|
|
356
390
|
return {
|
|
@@ -358,6 +392,30 @@ export function createEventSource(initialHandler) {
|
|
|
358
392
|
emitter: new EventEmitter(listen)
|
|
359
393
|
};
|
|
360
394
|
}
|
|
395
|
+
export function createEventsSource(initialListeners) {
|
|
396
|
+
const handlers = {};
|
|
397
|
+
const emitters = createEventsProxy({
|
|
398
|
+
on: (name, handler) => {
|
|
399
|
+
if (!handlers[name])
|
|
400
|
+
handlers[name] = [];
|
|
401
|
+
const unique = { fn: handler };
|
|
402
|
+
handlers[name].push(unique);
|
|
403
|
+
return () => {
|
|
404
|
+
const idx = handlers[name].indexOf(unique);
|
|
405
|
+
handlers[name].splice(idx, 1);
|
|
406
|
+
if (handlers[name].length == 0)
|
|
407
|
+
delete handlers[name];
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
}, initialListeners);
|
|
411
|
+
return {
|
|
412
|
+
emitters,
|
|
413
|
+
trigger: (name, value) => {
|
|
414
|
+
var _a;
|
|
415
|
+
(_a = handlers[name]) === null || _a === void 0 ? void 0 : _a.forEach(entry => entry.fn(value));
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
361
419
|
function createListenable(sourceListen) {
|
|
362
420
|
const handlers = [];
|
|
363
421
|
let onRemoveLast;
|
|
@@ -380,26 +438,42 @@ function createListenable(sourceListen) {
|
|
|
380
438
|
emit: (value) => handlers.forEach(h => h.fn(value)),
|
|
381
439
|
};
|
|
382
440
|
}
|
|
383
|
-
export function interval(
|
|
441
|
+
export function interval(t) {
|
|
384
442
|
let intervalId = null;
|
|
385
443
|
let idx = 0;
|
|
386
444
|
const { emit, listen } = createListenable(() => {
|
|
387
445
|
intervalId = setInterval(() => {
|
|
388
446
|
emit(idx++);
|
|
389
|
-
},
|
|
447
|
+
}, periodAsMilliseconds(t));
|
|
390
448
|
return () => clearInterval(intervalId);
|
|
391
449
|
});
|
|
392
450
|
return new EventEmitter(listen);
|
|
393
451
|
}
|
|
452
|
+
export const animationFrames = (() => {
|
|
453
|
+
const { emit, listen } = createListenable(() => {
|
|
454
|
+
let rafId = null;
|
|
455
|
+
let lastTime = null;
|
|
456
|
+
const frame = (time) => {
|
|
457
|
+
rafId = requestAnimationFrame(frame);
|
|
458
|
+
const elapsed = time - lastTime;
|
|
459
|
+
emit(elapsed);
|
|
460
|
+
};
|
|
461
|
+
lastTime = performance.now();
|
|
462
|
+
rafId = requestAnimationFrame(frame);
|
|
463
|
+
return () => cancelAnimationFrame(rafId);
|
|
464
|
+
});
|
|
465
|
+
return new EventEmitter(listen);
|
|
466
|
+
})();
|
|
394
467
|
export function timeout(t) {
|
|
395
|
-
const ms =
|
|
468
|
+
const ms = periodAsMilliseconds(t);
|
|
396
469
|
const targetTime = Date.now() + ms;
|
|
397
|
-
let timeoutId = null;
|
|
398
470
|
const { emit, listen } = createListenable(() => {
|
|
399
471
|
const reminaingMs = targetTime - Date.now();
|
|
400
472
|
if (reminaingMs < 0)
|
|
401
473
|
return;
|
|
402
|
-
timeoutId = setTimeout(
|
|
474
|
+
const timeoutId = setTimeout(() => {
|
|
475
|
+
emit();
|
|
476
|
+
}, reminaingMs);
|
|
403
477
|
return () => clearTimeout(timeoutId);
|
|
404
478
|
});
|
|
405
479
|
return new EventEmitter(listen);
|
|
@@ -424,7 +498,7 @@ export class SubjectEmitter extends EventEmitter {
|
|
|
424
498
|
constructor(initial) {
|
|
425
499
|
const { emit, listen } = createListenable();
|
|
426
500
|
super(h => {
|
|
427
|
-
h(this._value);
|
|
501
|
+
h(this._value); // immediate emit on listen
|
|
428
502
|
return listen(h);
|
|
429
503
|
});
|
|
430
504
|
this.emit = emit;
|
|
@@ -441,13 +515,15 @@ export class SubjectEmitter extends EventEmitter {
|
|
|
441
515
|
export function toEventEmitter(source, eventName) {
|
|
442
516
|
if (source instanceof EventEmitter)
|
|
443
517
|
return source;
|
|
518
|
+
if (typeof source == "function")
|
|
519
|
+
return new EventEmitter(source);
|
|
444
520
|
if (eventName !== undefined) {
|
|
445
521
|
// addEL()
|
|
446
522
|
if ("addEventListener" in source) {
|
|
447
523
|
if ("removeEventListener" in source && typeof source.removeEventListener == "function") {
|
|
448
524
|
return new EventEmitter(h => {
|
|
449
|
-
|
|
450
|
-
|
|
525
|
+
source.addEventListener(eventName, h);
|
|
526
|
+
return () => source.removeEventListener(eventName, h);
|
|
451
527
|
});
|
|
452
528
|
}
|
|
453
529
|
return new EventEmitter(h => {
|
package/internal/proxy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SetGetStyleFunc } from "./types";
|
|
1
|
+
import { SetGetStyleFunc, EventSource, EventEmitterMap, EventHandlerMap } from "./types";
|
|
2
2
|
export declare const styleProxy: ProxyHandler<SetGetStyleFunc>;
|
|
3
3
|
export declare const attribsProxy: ProxyHandler<HTMLElement>;
|
|
4
|
-
export declare
|
|
4
|
+
export declare function createEventsProxy<Map>(source: EventSource<any, keyof Map>, initialListeners?: EventHandlerMap<Map>): EventEmitterMap<Map>;
|
package/internal/proxy.js
CHANGED
|
@@ -34,14 +34,16 @@ export const attribsProxy = {
|
|
|
34
34
|
return element.getAttributeNames();
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
|
-
|
|
38
|
-
get: (
|
|
39
|
-
|
|
40
|
-
return (name, handler) => element.addEventListener(name, handler);
|
|
41
|
-
}
|
|
42
|
-
if (key == "removeEventListener") {
|
|
43
|
-
return (name, handler) => element.removeEventListener(name, handler);
|
|
44
|
-
}
|
|
45
|
-
return toEventEmitter(element, key);
|
|
37
|
+
const eventsProxyDefinition = {
|
|
38
|
+
get: (object, key) => {
|
|
39
|
+
return toEventEmitter(object, key);
|
|
46
40
|
}
|
|
47
41
|
};
|
|
42
|
+
export function createEventsProxy(source, initialListeners) {
|
|
43
|
+
const proxy = new Proxy(source, eventsProxyDefinition);
|
|
44
|
+
if (initialListeners) {
|
|
45
|
+
Object.entries(initialListeners)
|
|
46
|
+
.forEach(([name, handler]) => toEventEmitter(source, name).apply(handler));
|
|
47
|
+
}
|
|
48
|
+
return proxy;
|
|
49
|
+
}
|
package/internal/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { EventEmitter, UnsubscribeFunc } from "./emitter";
|
|
1
|
+
import { ClassAccessor } from "./element";
|
|
2
|
+
import { EventEmitter, ListenFunc, UnsubscribeFunc } from "./emitter";
|
|
3
3
|
import { entityDataSymbol } from "./util";
|
|
4
4
|
export type ElementClassDescriptor = string | Record<string, boolean | EmitterLike<boolean> | undefined> | undefined | ElementClassDescriptor[];
|
|
5
5
|
export type DOMContent = number | null | string | Element | JelEntity<object> | Text | DOMContent[];
|
|
@@ -10,6 +10,7 @@ export type EmitterLike<T> = {
|
|
|
10
10
|
} | {
|
|
11
11
|
listen: (callback: (value: T) => void) => UnsubscribeFunc;
|
|
12
12
|
};
|
|
13
|
+
export type EmissionSource<T> = EmitterLike<T> | ListenFunc<T>;
|
|
13
14
|
export type CSSValue = string | number | null | HexCodeContainer;
|
|
14
15
|
export type CSSProperty = keyof StylesDescriptor;
|
|
15
16
|
type HexCodeContainer = {
|
|
@@ -35,7 +36,7 @@ type TagWithName = 'input' | 'textarea' | 'select' | 'form';
|
|
|
35
36
|
type ContentlessElement = HTMLElementTagNameMap[ContentlessTag];
|
|
36
37
|
export type ElementDescriptor<Tag extends HTMLTag> = {
|
|
37
38
|
classes?: ElementClassDescriptor;
|
|
38
|
-
attribs?: Record<string, string | number | boolean>;
|
|
39
|
+
attribs?: Record<string, string | number | boolean | undefined>;
|
|
39
40
|
on?: {
|
|
40
41
|
[E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
|
|
41
42
|
};
|
|
@@ -64,7 +65,7 @@ type ElementAPI<T extends HTMLElement> = {
|
|
|
64
65
|
readonly attribs: {
|
|
65
66
|
[key: string]: string | null;
|
|
66
67
|
};
|
|
67
|
-
readonly events:
|
|
68
|
+
readonly events: EventEmitterMap<HTMLElementEventMap>;
|
|
68
69
|
readonly style: StyleAccessor;
|
|
69
70
|
setCSSVariable(variableName: string, value: CSSValue | EmitterLike<CSSValue>): void;
|
|
70
71
|
setCSSVariable(table: Record<string, CSSValue | EmitterLike<CSSValue>>): void;
|
|
@@ -73,6 +74,13 @@ type ElementAPI<T extends HTMLElement> = {
|
|
|
73
74
|
getRect(): DOMRect;
|
|
74
75
|
focus(): void;
|
|
75
76
|
blur(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Add an event listener
|
|
79
|
+
* @param eventId
|
|
80
|
+
* @param handler
|
|
81
|
+
* @returns Function to remove the listener
|
|
82
|
+
* @deprecated Use ent.events
|
|
83
|
+
*/
|
|
76
84
|
on<E extends keyof HTMLElementEventMap>(eventId: E, handler: (this: ElementAPI<T>, data: HTMLElementEventMap[E]) => void): UnsubscribeFunc;
|
|
77
85
|
} & (T extends ContentlessElement ? {} : {
|
|
78
86
|
append(...content: DOMContent[]): void;
|
|
@@ -120,7 +128,7 @@ export type DomHelper = ((
|
|
|
120
128
|
<T extends HTMLElement>(element: T) => DomEntity<T>) & {
|
|
121
129
|
[T in HTMLTag]: (descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>;
|
|
122
130
|
} & {
|
|
123
|
-
[T in HTMLTag]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]
|
|
131
|
+
[T in HTMLTag]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (((content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & ((contentEmitter: EmitterLike<DOMContent>) => DomEntity<HTMLElementTagNameMap[T]>));
|
|
124
132
|
});
|
|
125
133
|
type JelEntityData = {
|
|
126
134
|
dom: DOMContent;
|
|
@@ -128,10 +136,28 @@ type JelEntityData = {
|
|
|
128
136
|
export type JelEntity<API extends object | void> = (API extends void ? {} : API) & {
|
|
129
137
|
readonly [entityDataSymbol]: JelEntityData;
|
|
130
138
|
};
|
|
131
|
-
export type
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
export type Handler<T> = (value: T) => void;
|
|
140
|
+
export type Period = {
|
|
141
|
+
asMilliseconds: number;
|
|
142
|
+
} | {
|
|
143
|
+
asSeconds: number;
|
|
144
|
+
};
|
|
145
|
+
export type EventSource<E, N> = {
|
|
146
|
+
on: (eventName: N, handler: Handler<E>) => UnsubscribeFunc;
|
|
147
|
+
} | {
|
|
148
|
+
on: (eventName: N, handler: Handler<E>) => void | UnsubscribeFunc;
|
|
149
|
+
off: (eventName: N, handler: Handler<E>) => void;
|
|
150
|
+
} | {
|
|
151
|
+
addEventListener: (eventName: N, handler: Handler<E>) => UnsubscribeFunc;
|
|
152
|
+
} | {
|
|
153
|
+
addEventListener: (eventName: N, handler: Handler<E>) => void;
|
|
154
|
+
removeEventListener: (eventName: N, handler: Handler<E>) => void;
|
|
155
|
+
};
|
|
156
|
+
export type Dictionary<T> = Record<string | symbol, T>;
|
|
157
|
+
export type EventEmitterMap<Map> = {
|
|
158
|
+
[K in keyof Map]: EventEmitter<Map[K]>;
|
|
159
|
+
};
|
|
160
|
+
export type EventHandlerMap<Map> = {
|
|
161
|
+
[K in keyof Map]?: (value: Map[K]) => void;
|
|
136
162
|
};
|
|
137
163
|
export {};
|