@xtia/jel 0.9.1 → 0.11.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, EmitterLike, CSSValue } 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, animationFrames, 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, animationFrames, 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,12 +1,4 @@
1
- import { EmitterLike } from "./types";
2
- type Handler<T> = (value: T) => void;
3
- type Period = {
4
- asMilliseconds: number;
5
- } | {
6
- asSeconds: number;
7
- };
8
- export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
9
- export type UnsubscribeFunc = () => void;
1
+ import { Dictionary, EmissionSource, EmitterLike, EventHandlerMap, EventSource, Handler, ListenFunc, Period, UnsubscribeFunc } from "./types";
10
2
  export declare class EventEmitter<T> {
11
3
  protected onListen: ListenFunc<T>;
12
4
  constructor(onListen: ListenFunc<T>);
@@ -30,6 +22,7 @@ export declare class EventEmitter<T> {
30
22
  */
31
23
  map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
32
24
  mapAsync<R>(mapFunc: (value: T) => Promise<R>): EventEmitter<R>;
25
+ as<R>(value: R): EventEmitter<R>;
33
26
  /**
34
27
  * Creates a chainable emitter that selectively forwards emissions along the chain
35
28
  * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
@@ -90,13 +83,15 @@ export declare class EventEmitter<T> {
90
83
  throttle(period: Period): EventEmitter<T>;
91
84
  batch(ms: number): EventEmitter<T[]>;
92
85
  /**
93
- * Creates a chainable emitter that
86
+ * Creates a chainable emitter that forwards the next emission from the parent
94
87
  * **Experimental**: May change in future revisions
95
88
  * Note: only listens to the parent while at least one downstream subscription is present
96
89
  * @param notifier
97
90
  * @returns
98
91
  */
99
92
  once(): EventEmitter<T>;
93
+ once(handler: Handler<T>): UnsubscribeFunc;
94
+ getNext(): Promise<T>;
100
95
  delay(ms: number): EventEmitter<T>;
101
96
  delay(period: Period): EventEmitter<T>;
102
97
  scan<S>(updater: (state: S, value: T) => S, initial: S): EventEmitter<S>;
@@ -140,11 +135,38 @@ export declare class EventEmitter<T> {
140
135
  */
141
136
  or(...emitters: EmitterLike<T>[]): EventEmitter<T>;
142
137
  or<U>(...emitters: EmitterLike<U>[]): EventEmitter<T | U>;
143
- then<R>(handler: (value: T) => R): Promise<R>;
144
138
  memo(): Memo<T | undefined>;
145
139
  memo(initial: T): Memo<T>;
146
140
  memo<U>(initial: U): Memo<T | U>;
141
+ record(): EventRecorder<T>;
142
+ }
143
+ export declare class EventRecorder<T> {
144
+ private startTime;
145
+ private entries;
146
+ private recording;
147
+ private unsubscribe;
148
+ constructor(emitter: EventEmitter<T>);
149
+ private add;
150
+ stop(): EventRecording<T>;
151
+ }
152
+ export declare class EventRecording<T> {
153
+ private _entries;
154
+ constructor(entries: [number, T][]);
155
+ export(): [number, T][];
156
+ play(speed?: number): EventEmitter<T>;
147
157
  }
158
+ type EmitEmitterPair<T> = {
159
+ emit: (value: T) => void;
160
+ emitter: EventEmitter<T>;
161
+ };
162
+ type CreateEventSourceOptions<T> = {
163
+ initialHandler?: Handler<T>;
164
+ /**
165
+ * Function to call when subscription count changes from 0
166
+ * Return a *deactivation* function, which will be called when subscription count changes back to 0
167
+ */
168
+ activate?(): UnsubscribeFunc;
169
+ };
148
170
  /**
149
171
  * Creates a linked EventEmitter and emit() pair
150
172
  * @example
@@ -176,12 +198,18 @@ export declare class EventEmitter<T> {
176
198
  * @param initialHandler Optional listener automatically applied to the resulting Emitter
177
199
  * @returns
178
200
  */
179
- export declare function createEventSource<T>(initialHandler?: Handler<T>): {
180
- emit: (value: T) => void;
181
- emitter: EventEmitter<T>;
201
+ export declare function createEventSource<T>(initialHandler?: Handler<T>): EmitEmitterPair<T>;
202
+ export declare function createEventSource<T>(options?: CreateEventSourceOptions<T>): EmitEmitterPair<T>;
203
+ export declare function createEventsSource<Map extends Dictionary<any>>(initialListeners?: EventHandlerMap<Map>): {
204
+ emitters: import("./types").EventEmitterMap<Map>;
205
+ trigger: <K extends keyof Map>(name: K, value: Map[K]) => void;
182
206
  };
183
207
  export declare function interval(ms: number): EventEmitter<number>;
184
208
  export declare function interval(period: Period): EventEmitter<number>;
209
+ /**
210
+ * Emits time deltas from a shared RAF loop
211
+ */
212
+ export declare const animationFrames: EventEmitter<number>;
185
213
  export declare function timeout(ms: number): EventEmitter<void>;
186
214
  export declare function timeout(period: Period): EventEmitter<void>;
187
215
  declare class Memo<T> {
@@ -198,26 +226,17 @@ export declare class SubjectEmitter<T> extends EventEmitter<T> {
198
226
  get value(): T;
199
227
  next(value: T): void;
200
228
  }
201
- type EventSource<T, E extends string> = {
202
- on: (eventName: E, handler: Handler<T>) => UnsubscribeFunc;
203
- } | {
204
- on: (eventName: E, handler: Handler<T>) => void | UnsubscribeFunc;
205
- off: (eventName: E, handler: Handler<T>) => void;
206
- } | {
207
- addEventListener: (eventName: E, handler: Handler<T>) => UnsubscribeFunc;
208
- } | {
209
- addEventListener: (eventName: E, handler: Handler<T>) => void | UnsubscribeFunc;
210
- removeEventListener: (eventName: E, handler: Handler<T>) => void;
211
- };
212
229
  /**
213
- * Create an EventEmitter from an event source. Event sources can be RxJS observables, existing EventEmitters, or objects that
214
- * provide a subscribe()/listen() => UnsubscribeFunc method.
230
+ * Create an EventEmitter from an event source. Event source can be RxJS observable, existing `EventEmitter`, an object that
231
+ * provides a `subscribe()`/`listen() => UnsubscribeFunc` method, or a subscribe function itself.
232
+ * @param source
233
+ */
234
+ export declare function toEventEmitter<E>(source: EmissionSource<E>): EventEmitter<E>;
235
+ /**
236
+ * 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.
215
237
  * @param source
216
238
  */
217
- export declare function toEventEmitter<T>(source: EmitterLike<T>): EventEmitter<T>;
218
- export declare function toEventEmitter<T, E extends string>(source: EventSource<T, E>, eventName: E): EventEmitter<T>;
219
- export declare function toEventEmitter<T>(subscribe: ListenFunc<T>): EventEmitter<T>;
220
- type Dictionary<T> = Record<string | symbol, T>;
239
+ export declare function toEventEmitter<E, N>(source: EventSource<E, N>, eventName: N): EventEmitter<E>;
221
240
  type ExtractEmitterValue<T> = T extends EmitterLike<infer U> ? U : never;
222
241
  type CombinedRecord<T extends Dictionary<EmitterLike<any>>> = {
223
242
  readonly [K in keyof T]: ExtractEmitterValue<T[K]>;
@@ -1,3 +1,4 @@
1
+ import { createEventsProxy } from "./proxy.js";
1
2
  import { isReactiveSource } from "./util";
2
3
  function periodAsMilliseconds(t) {
3
4
  if (typeof t == "number")
@@ -43,6 +44,10 @@ export class EventEmitter {
43
44
  const listen = this.transform((value, emit) => mapFunc(value).then(emit));
44
45
  return new EventEmitter(listen);
45
46
  }
47
+ as(value) {
48
+ const listen = this.transform((_, emit) => emit(value));
49
+ return new EventEmitter(listen);
50
+ }
46
51
  /**
47
52
  * Creates a chainable emitter that selectively forwards emissions along the chain
48
53
  * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
@@ -152,14 +157,7 @@ export class EventEmitter {
152
157
  });
153
158
  return new EventEmitter(listen);
154
159
  }
155
- /**
156
- * Creates a chainable emitter that
157
- * **Experimental**: May change in future revisions
158
- * Note: only listens to the parent while at least one downstream subscription is present
159
- * @param notifier
160
- * @returns
161
- */
162
- once() {
160
+ once(handler) {
163
161
  let parentUnsubscribe = null;
164
162
  let completed = false;
165
163
  const clear = () => {
@@ -178,11 +176,18 @@ export class EventEmitter {
178
176
  });
179
177
  return clear;
180
178
  });
181
- return new EventEmitter(listen);
179
+ const emitter = new EventEmitter(listen);
180
+ return handler
181
+ ? emitter.apply(handler)
182
+ : emitter;
183
+ }
184
+ getNext() {
185
+ return new Promise((resolve) => this.once(resolve));
182
186
  }
183
187
  delay(t) {
188
+ const ms = periodAsMilliseconds(t);
184
189
  return new EventEmitter(this.transform((value, emit) => {
185
- setTimeout(() => emit(value), periodAsMilliseconds(t));
190
+ return timeout(ms).apply(() => emit(value));
186
191
  }));
187
192
  }
188
193
  scan(updater, initial) {
@@ -322,48 +327,63 @@ export class EventEmitter {
322
327
  return () => unsubs.forEach(unsub => unsub());
323
328
  });
324
329
  }
325
- then(handler) {
326
- return new Promise(resolve => {
327
- this.once().apply(v => resolve(handler(v)));
328
- });
329
- }
330
330
  memo(initial) {
331
331
  return new Memo(this, initial);
332
332
  }
333
+ record() {
334
+ return new EventRecorder(this);
335
+ }
333
336
  }
334
- /**
335
- * Creates a linked EventEmitter and emit() pair
336
- * @example
337
- * ```ts
338
- * function createForm(options?: { onsubmit?: (data: FormData) => void }) {
339
- * const submitEvents = createEventSource(options?.onsubmit);
340
- * const form = $.form({
341
- * on: {
342
- * submit: (e) => {
343
- * e.preventDefault();
344
- * const data = new FormData(e.target);
345
- * submitEvents.emit(data); // emit when form is submitted
346
- * }
347
- * }
348
- * });
349
- *
350
- * return createEntity(form, {
351
- * events: {
352
- * submit: submitEvents.emitter
353
- * }
354
- * })
355
- * }
356
- *
357
- * const form = createForm({
358
- * onsubmit: (data) => handleSubmission(data)
359
- * });
360
- * ```
361
- *
362
- * @param initialHandler Optional listener automatically applied to the resulting Emitter
363
- * @returns
364
- */
365
- export function createEventSource(initialHandler) {
366
- const { emit, listen } = createListenable();
337
+ export class EventRecorder {
338
+ constructor(emitter) {
339
+ this.startTime = performance.now();
340
+ this.entries = [];
341
+ this.recording = true;
342
+ this.unsubscribe = emitter.listen(v => this.add(v));
343
+ }
344
+ add(value) {
345
+ const now = performance.now();
346
+ let time = now - this.startTime;
347
+ this.entries.push([time, value]);
348
+ }
349
+ stop() {
350
+ if (!this.recording) {
351
+ throw new Error("EventRecorder already stopped");
352
+ }
353
+ this.unsubscribe();
354
+ return new EventRecording(this.entries);
355
+ }
356
+ }
357
+ export class EventRecording {
358
+ constructor(entries) {
359
+ this._entries = entries;
360
+ }
361
+ export() {
362
+ return [...this._entries];
363
+ }
364
+ play(speed = 1) {
365
+ let idx = 0;
366
+ let elapsed = 0;
367
+ const { emit, listen } = createListenable();
368
+ const unsubscribe = animationFrames.listen((frameElapsed) => {
369
+ elapsed += frameElapsed * speed;
370
+ while (idx < this._entries.length && this._entries[idx][0] <= elapsed) {
371
+ emit(this._entries[idx][1]);
372
+ idx++;
373
+ }
374
+ if (idx >= this._entries.length) {
375
+ unsubscribe();
376
+ }
377
+ });
378
+ return new EventEmitter(listen);
379
+ }
380
+ }
381
+ export function createEventSource(arg) {
382
+ if (typeof arg === "function") {
383
+ arg = { initialHandler: arg };
384
+ }
385
+ const { initialHandler, activate } = arg !== null && arg !== void 0 ? arg : {};
386
+ const { emit, listen } = createListenable(activate);
367
387
  if (initialHandler)
368
388
  listen(initialHandler);
369
389
  return {
@@ -371,6 +391,30 @@ export function createEventSource(initialHandler) {
371
391
  emitter: new EventEmitter(listen)
372
392
  };
373
393
  }
394
+ export function createEventsSource(initialListeners) {
395
+ const handlers = {};
396
+ const emitters = createEventsProxy({
397
+ on: (name, handler) => {
398
+ if (!handlers[name])
399
+ handlers[name] = [];
400
+ const unique = { fn: handler };
401
+ handlers[name].push(unique);
402
+ return () => {
403
+ const idx = handlers[name].indexOf(unique);
404
+ handlers[name].splice(idx, 1);
405
+ if (handlers[name].length == 0)
406
+ delete handlers[name];
407
+ };
408
+ },
409
+ }, initialListeners);
410
+ return {
411
+ emitters,
412
+ trigger: (name, value) => {
413
+ var _a;
414
+ (_a = handlers[name]) === null || _a === void 0 ? void 0 : _a.forEach(entry => entry.fn(value));
415
+ }
416
+ };
417
+ }
374
418
  function createListenable(sourceListen) {
375
419
  const handlers = [];
376
420
  let onRemoveLast;
@@ -404,15 +448,35 @@ export function interval(t) {
404
448
  });
405
449
  return new EventEmitter(listen);
406
450
  }
451
+ /**
452
+ * Emits time deltas from a shared RAF loop
453
+ */
454
+ export const animationFrames = (() => {
455
+ const { emit, listen } = createListenable(() => {
456
+ let rafId = null;
457
+ let lastTime = null;
458
+ const frame = (time) => {
459
+ if (lastTime === null)
460
+ lastTime = time;
461
+ rafId = requestAnimationFrame(frame);
462
+ const elapsed = time - lastTime;
463
+ emit(elapsed);
464
+ };
465
+ rafId = requestAnimationFrame(frame);
466
+ return () => cancelAnimationFrame(rafId);
467
+ });
468
+ return new EventEmitter(listen);
469
+ })();
407
470
  export function timeout(t) {
408
471
  const ms = periodAsMilliseconds(t);
409
472
  const targetTime = Date.now() + ms;
410
- let timeoutId = null;
411
473
  const { emit, listen } = createListenable(() => {
412
474
  const reminaingMs = targetTime - Date.now();
413
475
  if (reminaingMs < 0)
414
476
  return;
415
- timeoutId = setTimeout(emit, reminaingMs);
477
+ const timeoutId = setTimeout(() => {
478
+ emit();
479
+ }, reminaingMs);
416
480
  return () => clearTimeout(timeoutId);
417
481
  });
418
482
  return new EventEmitter(listen);
@@ -437,7 +501,7 @@ export class SubjectEmitter extends EventEmitter {
437
501
  constructor(initial) {
438
502
  const { emit, listen } = createListenable();
439
503
  super(h => {
440
- h(this._value);
504
+ h(this._value); // immediate emit on listen
441
505
  return listen(h);
442
506
  });
443
507
  this.emit = emit;
@@ -461,8 +525,8 @@ export function toEventEmitter(source, eventName) {
461
525
  if ("addEventListener" in source) {
462
526
  if ("removeEventListener" in source && typeof source.removeEventListener == "function") {
463
527
  return new EventEmitter(h => {
464
- return source.addEventListener(eventName, h)
465
- || (() => source.removeEventListener(eventName, h));
528
+ source.addEventListener(eventName, h);
529
+ return () => source.removeEventListener(eventName, h);
466
530
  });
467
531
  }
468
532
  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,15 +1,18 @@
1
- import { type ClassAccessor } from "./element";
2
- import { EventEmitter, UnsubscribeFunc } from "./emitter";
1
+ import { ClassAccessor } from "./element";
2
+ import { EventEmitter } 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[];
6
6
  export type DomEntity<T extends HTMLElement> = JelEntity<ElementAPI<T>>;
7
7
  export type HTMLTag = keyof HTMLElementTagNameMap;
8
+ export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
9
+ export type UnsubscribeFunc = () => void;
8
10
  export type EmitterLike<T> = {
9
- subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
11
+ subscribe: ListenFunc<T>;
10
12
  } | {
11
- listen: (callback: (value: T) => void) => UnsubscribeFunc;
13
+ listen: ListenFunc<T>;
12
14
  };
15
+ export type EmissionSource<T> = EmitterLike<T> | ListenFunc<T>;
13
16
  export type CSSValue = string | number | null | HexCodeContainer;
14
17
  export type CSSProperty = keyof StylesDescriptor;
15
18
  type HexCodeContainer = {
@@ -64,7 +67,7 @@ type ElementAPI<T extends HTMLElement> = {
64
67
  readonly attribs: {
65
68
  [key: string]: string | null;
66
69
  };
67
- readonly events: EventsAccessor;
70
+ readonly events: EventEmitterMap<HTMLElementEventMap>;
68
71
  readonly style: StyleAccessor;
69
72
  setCSSVariable(variableName: string, value: CSSValue | EmitterLike<CSSValue>): void;
70
73
  setCSSVariable(table: Record<string, CSSValue | EmitterLike<CSSValue>>): void;
@@ -73,6 +76,13 @@ type ElementAPI<T extends HTMLElement> = {
73
76
  getRect(): DOMRect;
74
77
  focus(): void;
75
78
  blur(): void;
79
+ /**
80
+ * Add an event listener
81
+ * @param eventId
82
+ * @param handler
83
+ * @returns Function to remove the listener
84
+ * @deprecated Use ent.events
85
+ */
76
86
  on<E extends keyof HTMLElementEventMap>(eventId: E, handler: (this: ElementAPI<T>, data: HTMLElementEventMap[E]) => void): UnsubscribeFunc;
77
87
  } & (T extends ContentlessElement ? {} : {
78
88
  append(...content: DOMContent[]): void;
@@ -120,7 +130,7 @@ export type DomHelper = ((
120
130
  <T extends HTMLElement>(element: T) => DomEntity<T>) & {
121
131
  [T in HTMLTag]: (descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>;
122
132
  } & {
123
- [T in HTMLTag]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]>;
133
+ [T in HTMLTag]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (((content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & ((contentEmitter: EmitterLike<DOMContent>) => DomEntity<HTMLElementTagNameMap[T]>));
124
134
  });
125
135
  type JelEntityData = {
126
136
  dom: DOMContent;
@@ -128,10 +138,28 @@ type JelEntityData = {
128
138
  export type JelEntity<API extends object | void> = (API extends void ? {} : API) & {
129
139
  readonly [entityDataSymbol]: JelEntityData;
130
140
  };
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;
141
+ export type Handler<T> = (value: T) => void;
142
+ export type Period = {
143
+ asMilliseconds: number;
144
+ } | {
145
+ asSeconds: number;
146
+ };
147
+ export type EventSource<E, N> = {
148
+ on: (eventName: N, handler: Handler<E>) => UnsubscribeFunc;
149
+ } | {
150
+ on: (eventName: N, handler: Handler<E>) => void | UnsubscribeFunc;
151
+ off: (eventName: N, handler: Handler<E>) => void;
152
+ } | {
153
+ addEventListener: (eventName: N, handler: Handler<E>) => UnsubscribeFunc;
154
+ } | {
155
+ addEventListener: (eventName: N, handler: Handler<E>) => void;
156
+ removeEventListener: (eventName: N, handler: Handler<E>) => void;
157
+ };
158
+ export type Dictionary<T> = Record<string | symbol, T>;
159
+ export type EventEmitterMap<Map> = {
160
+ [K in keyof Map]: EventEmitter<Map[K]>;
161
+ };
162
+ export type EventHandlerMap<Map> = {
163
+ [K in keyof Map]?: (value: Map[K]) => void;
136
164
  };
137
165
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/jel",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/jel-ts",
6
6
  "type": "github"