@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 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
- element.events.mousemove
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.events, "mousemove")`.
139
+ For RxJS users, events can be observed with `fromEvent(ent.element, "mousemove")`.
140
140
 
141
- ## Reactive styles
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);
@@ -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;
@@ -1,4 +1,5 @@
1
- import { attribsProxy, eventsProxy, styleProxy } from "./proxy";
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 ("listen" in value || "subscribe" in value) {
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: new Proxy(element, eventsProxy)
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
- const entries = this.classList.entries();
397
- let entry = entries.next();
398
- while (!entry.done) {
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
  }
@@ -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
- emit: (value: T) => void;
161
- emitter: EventEmitter<T>;
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
- asMilliseconds: number;
165
- }): EventEmitter<number>;
166
- export declare function timeout(t: number | {
167
- asMilliseconds: number;
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 sources can be RxJS observables, existing EventEmitters, or objects that
196
- * provide a subscribe()/listen() => UnsubscribeFunc method.
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<T>(source: EmitterLike<T>): EventEmitter<T>;
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]>;
@@ -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(ms) {
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
- }, ms);
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(ms) {
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 + ms) {
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
- return new EventEmitter(listen);
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(ms) {
188
+ delay(t) {
189
+ const ms = periodAsMilliseconds(t);
175
190
  return new EventEmitter(this.transform((value, emit) => {
176
- setTimeout(() => emit(value), ms);
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
- const handler = () => {
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
- * Creates a linked EventEmitter and emit() pair
323
- * @example
324
- * ```ts
325
- * function createForm(options?: { onsubmit?: (data: FormData) => void }) {
326
- * const submitEvents = createEventSource(options?.onsubmit);
327
- * const form = $.form({
328
- * on: {
329
- * submit: (e) => {
330
- * e.preventDefault();
331
- * const data = new FormData(e.target);
332
- * submitEvents.emit(data); // emit when form is submitted
333
- * }
334
- * }
335
- * });
336
- *
337
- * return createEntity(form, {
338
- * events: {
339
- * submit: submitEvents.emitter
340
- * }
341
- * })
342
- * }
343
- *
344
- * const form = createForm({
345
- * onsubmit: (data) => handleSubmission(data)
346
- * });
347
- * ```
348
- *
349
- * @param initialHandler Optional listener automatically applied to the resulting Emitter
350
- * @returns
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(ms) {
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
- }, typeof ms == "number" ? ms : ms.asMilliseconds);
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 = typeof t === "number" ? t : t.asMilliseconds;
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(emit, reminaingMs);
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
- return source.addEventListener(eventName, h)
450
- || (() => source.removeEventListener(eventName, h));
525
+ source.addEventListener(eventName, h);
526
+ return () => source.removeEventListener(eventName, h);
451
527
  });
452
528
  }
453
529
  return new EventEmitter(h => {
@@ -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 const eventsProxy: ProxyHandler<HTMLElement>;
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
- export const eventsProxy = {
38
- get: (element, key) => {
39
- if (key == "addEventListener") {
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
+ }
@@ -1,5 +1,5 @@
1
- import { type ClassAccessor } from "./element";
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: EventsAccessor;
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 EventsAccessor = {
132
- [K in keyof HTMLElementEventMap]: EventEmitter<HTMLElementEventMap[K]>;
133
- } & {
134
- addEventListener<K extends keyof HTMLElementEventMap>(eventName: K, listener: (event: HTMLElementEventMap[K]) => void): void;
135
- removeEventListener<K extends keyof HTMLElementEventMap>(eventName: K, listener: (event: HTMLElementEventMap[K]) => void): void;
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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/jel",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/jel-ts",
6
6
  "type": "github"