@xtia/jel 0.6.4 → 0.7.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
@@ -42,7 +42,6 @@ body.append([
42
42
  ))
43
43
  )
44
44
  ])
45
-
46
45
  ```
47
46
 
48
47
  ## `DOMContent`
@@ -137,4 +136,42 @@ element.events.mousemove
137
136
  .apply(([x, y]) => console.log("mouse @ ", x, y));
138
137
  ```
139
138
 
140
- For RxJS users, events can be observed with `fromEvent(element.events, "mousemove")`.
139
+ For RxJS users, events can be observed with `fromEvent(element.events, "mousemove")`.
140
+
141
+ ## Reactive styles
142
+
143
+ Style properties, content and class presence can be emitter subscriptions:
144
+
145
+ ```ts
146
+ const mousePosition$ = $(document.body).events.mousemove
147
+ .map(ev => ({x: ev.clientX, y: ev.clientY}));
148
+
149
+ const virtualCursor = $.div({
150
+ classes: {
151
+ "virtual-cursor": true,
152
+ "near-top": mousePosition$.map(v => v.y < 100)
153
+ },
154
+ style: {
155
+ left: mousePosition$.map(v => v.x + "px"),
156
+ top: mousePosition$.map(v => v.y + "px")
157
+ }
158
+ });
159
+
160
+ virtualCursor.classes.toggle(
161
+ "near-left",
162
+ mousePosition$.map(v => v.x < 100>)
163
+ );
164
+
165
+ h1.content = websocket$
166
+ .filter(msg => msg.type == "title")
167
+ .map(msg => msg.text);
168
+ ```
169
+ Removing an element from the page will unsubscribe from any attached stream, and resubscribe if subsequently appended.
170
+
171
+ Emitters for this purpose can be Jel events, [@xtia/timeline](https://github.com/tiadrop/timeline) progressions, RxJS Observables or any object with either `subscribe()` or `listen()` that returns teardown logic.
172
+
173
+ ```ts
174
+ import { animate } from "@xtia/timeline";
175
+
176
+ button.style.opacity = animate(500).tween(0, 1);
177
+ ```
package/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { DomEntity, ElementClassDescriptor, ElementDescriptor, DOMContent, DomHelper, StyleAccessor, JelEntity } from "./internal/types";
2
- export { $ } from "./internal/element";
2
+ import { $ } from "./internal/element";
3
3
  export { createEntity } from "./internal/util";
4
4
  export { createEventSource, interval } from "./internal/emitter";
5
+ export { $ };
6
+ export declare const $body: import(".").DomEntity<HTMLElement>;
package/index.js CHANGED
@@ -1,3 +1,5 @@
1
- export { $ } from "./internal/element";
1
+ import { $ } from "./internal/element";
2
2
  export { createEntity } from "./internal/util";
3
3
  export { createEventSource, interval } from "./internal/emitter";
4
+ export { $ };
5
+ export const $body = $(document.body);
@@ -1,2 +1,18 @@
1
- import { DomHelper } from "./types";
1
+ import { DomHelper, Listenable } from "./types";
2
2
  export declare const $: DomHelper;
3
+ export declare class ClassAccessor {
4
+ private classList;
5
+ private listen;
6
+ private unlisten;
7
+ constructor(classList: DOMTokenList, listen: (className: string, stream: Listenable<boolean>) => void, unlisten: (classNames: string[]) => void);
8
+ add(...className: string[]): void;
9
+ remove(...className: string[]): void;
10
+ toggle(className: string, value?: boolean): boolean;
11
+ toggle(className: string, value: Listenable<boolean>): void;
12
+ contains(className: string): boolean;
13
+ get length(): number;
14
+ toString(): string;
15
+ replace(token: string, newToken: string): void;
16
+ forEach(cb: (token: string, idx: number) => void): void;
17
+ map<R>(cb: (token: string, idx: number) => R): R[];
18
+ }
@@ -34,8 +34,12 @@ function createElement(tag, descriptor = {}) {
34
34
  if (classes === undefined)
35
35
  return;
36
36
  Object.entries(classes).forEach(([className, state]) => {
37
- if (state)
37
+ if (isReactiveSource(state)) {
38
+ ent.classes.toggle(className, state);
39
+ }
40
+ else if (state) {
38
41
  applyClasses(className);
42
+ }
39
43
  });
40
44
  };
41
45
  applyClasses(descriptor.classes || []);
@@ -129,7 +133,8 @@ function observeMutations() {
129
133
  });
130
134
  }
131
135
  function isReactiveSource(value) {
132
- return typeof value == "object" && value && ("listen" in value || "subscribe" in value);
136
+ return typeof value == "object" && value && (("listen" in value && typeof value.listen == "function")
137
+ || ("subscribe" in value && typeof value.subscribe == "function"));
133
138
  }
134
139
  function getWrappedElement(element) {
135
140
  if (!elementWrapCache.has(element)) {
@@ -145,6 +150,7 @@ function getWrappedElement(element) {
145
150
  style: {},
146
151
  cssVariable: {},
147
152
  content: {},
153
+ class: {},
148
154
  };
149
155
  function addListener(type, prop, source) {
150
156
  const set = {
@@ -153,7 +159,8 @@ function getWrappedElement(element) {
153
159
  content: (v) => {
154
160
  element.innerHTML = "";
155
161
  recursiveAppend(element, v);
156
- }
162
+ },
163
+ class: (v) => element.classList.toggle(prop, v),
157
164
  }[type];
158
165
  const subscribe = "subscribe" in source
159
166
  ? () => source.subscribe(set)
@@ -334,10 +341,65 @@ function getWrappedElement(element) {
334
341
  }
335
342
  },
336
343
  style: new Proxy(setStyle, styleProxy),
337
- classes: element.classList,
344
+ classes: new ClassAccessor(element.classList, (className, stream) => addListener("class", className, stream), (classNames) => {
345
+ classNames.forEach(c => {
346
+ if (listeners.class[c])
347
+ removeListener("class", c);
348
+ });
349
+ }),
338
350
  events: new Proxy(element, eventsProxy)
339
351
  };
340
352
  elementWrapCache.set(element, domEntity);
341
353
  }
342
354
  return elementWrapCache.get(element);
343
355
  }
356
+ export class ClassAccessor {
357
+ constructor(classList, listen, unlisten) {
358
+ this.classList = classList;
359
+ this.listen = listen;
360
+ this.unlisten = unlisten;
361
+ }
362
+ add(...className) {
363
+ this.unlisten(className);
364
+ this.classList.add(...className);
365
+ }
366
+ remove(...className) {
367
+ this.unlisten(className);
368
+ this.classList.remove(...className);
369
+ }
370
+ toggle(className, value) {
371
+ this.unlisten([className]);
372
+ if (isReactiveSource(value)) {
373
+ this.listen(className, value);
374
+ return;
375
+ }
376
+ return this.classList.toggle(className, value);
377
+ }
378
+ contains(className) {
379
+ return this.classList.contains(className);
380
+ }
381
+ get length() {
382
+ return this.classList.length;
383
+ }
384
+ toString() {
385
+ return this.classList.toString();
386
+ }
387
+ replace(token, newToken) {
388
+ this.unlisten([token, newToken]);
389
+ this.classList.replace(token, newToken);
390
+ }
391
+ forEach(cb) {
392
+ this.classList.forEach(cb);
393
+ }
394
+ map(cb) {
395
+ 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
+ }
403
+ return result;
404
+ }
405
+ }
@@ -1,9 +1,10 @@
1
+ import { Listenable } from "./types";
1
2
  type Handler<T> = (value: T) => void;
2
3
  export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
3
4
  export type UnsubscribeFunc = () => void;
4
- export declare class Emitter<T> {
5
+ export declare class EventEmitter<T> {
5
6
  protected onListen: ListenFunc<T>;
6
- protected constructor(onListen: ListenFunc<T>);
7
+ constructor(onListen: ListenFunc<T>);
7
8
  protected transform<R = T>(handler: (value: T, emit: (value: R) => void) => void): (fn: (v: R) => void) => UnsubscribeFunc;
8
9
  /**
9
10
  * Compatibility alias for `apply()` - registers a function to receive emitted values
@@ -22,13 +23,13 @@ export declare class Emitter<T> {
22
23
  * @param mapFunc
23
24
  * @returns Listenable: emits transformed values
24
25
  */
25
- map<R>(mapFunc: (value: T) => R): Emitter<R>;
26
+ map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
26
27
  /**
27
28
  * Creates a chainable emitter that selectively forwards emissions along the chain
28
29
  * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
29
30
  * @returns Listenable: emits values that pass the filter
30
31
  */
31
- filter(check: (value: T) => boolean): Emitter<T>;
32
+ filter(check: (value: T) => boolean): EventEmitter<T>;
32
33
  /**
33
34
  * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
34
35
  * @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
@@ -36,7 +37,7 @@ export declare class Emitter<T> {
36
37
  * If no `compare` function is provided, values will be compared via `===`
37
38
  * @returns Listenable: emits non-repeating values
38
39
  */
39
- dedupe(compare?: (a: T, b: T) => boolean): Emitter<T>;
40
+ dedupe(compare?: (a: T, b: T) => boolean): EventEmitter<T>;
40
41
  /**
41
42
  * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
42
43
  *
@@ -48,7 +49,7 @@ export declare class Emitter<T> {
48
49
  * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
49
50
  * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
50
51
  */
51
- tap(cb: Handler<T>): Emitter<T>;
52
+ tap(cb: Handler<T>): EventEmitter<T>;
52
53
  /**
53
54
  * Immediately passes this emitter to a callback and returns this emitter
54
55
  *
@@ -66,39 +67,41 @@ export declare class Emitter<T> {
66
67
  * ```
67
68
  * @param cb
68
69
  */
69
- fork(cb: (branch: this) => void): this;
70
- }
71
- export declare class EventEmitter<T> extends Emitter<T> {
72
- constructor(listen: ListenFunc<T>);
70
+ fork(...cb: ((branch: this) => void)[]): this;
73
71
  debounce(ms: number): EventEmitter<T>;
74
72
  throttle(ms: number): EventEmitter<T>;
75
- batch(ms: number): Emitter<T[]>;
73
+ batch(ms: number): EventEmitter<T[]>;
76
74
  /**
75
+ * Creates a chainable emitter that
77
76
  * **Experimental**: May change in future revisions
78
- * Note: potential leak - This link will remain subscribed to the parent
79
- * until it emits, regardless of subscriptions to this link.
77
+ * Note: only listens to the parent while at least one downstream subscription is present
80
78
  * @param notifier
81
79
  * @returns
82
80
  */
83
81
  once(): EventEmitter<T>;
82
+ delay(ms: number): EventEmitter<T>;
84
83
  scan<S>(updater: (state: S, value: T) => S, initial: S): EventEmitter<S>;
85
84
  buffer(count: number): EventEmitter<T[]>;
86
85
  /**
87
86
  * **Experimental**: May change in future revisions
88
- * Note: potential leak - This link will remain subscribed to the parent
89
- * until emission limit is reached, regardless of subscriptions to this link.
90
- * @param notifier
87
+ * Note: only listens to the notifier while at least one downstream subscription is present
88
+ * @param limit
91
89
  * @returns
92
90
  */
93
91
  take(limit: number): EventEmitter<T>;
94
92
  /**
95
93
  * **Experimental**: May change in future revisions
96
- * Note: potential leak - This link will remain subscribed to the notifier
97
- * until it emits, regardless of subscriptions to this link.
94
+ * Note: only listens to the notifier while at least one downstream subscription is present
98
95
  * @param notifier
99
96
  * @returns
100
97
  */
101
- takeUntil(notifier: Emitter<any>): Emitter<T>;
98
+ takeUntil(notifier: Listenable<any>): EventEmitter<T>;
99
+ /**
100
+ * Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
101
+ * Disconnects from the parent and becomes inert when the predicate returns false
102
+ * @param predicate Callback to determine whether to keep forwarding
103
+ */
104
+ takeWhile(predicate: (value: T) => boolean): EventEmitter<T>;
102
105
  /**
103
106
  * Creates a chainable emitter that immediately emits a value to every new subscriber,
104
107
  * then forwards parent emissions
@@ -106,46 +109,19 @@ export declare class EventEmitter<T> extends Emitter<T> {
106
109
  * @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
107
110
  */
108
111
  immediate(value: T): EventEmitter<T>;
109
- cached(): EventEmitter<T>;
110
- /**
111
- * Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
112
- * @param mapFunc
113
- * @returns Listenable: emits transformed values
114
- */
115
- map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
116
112
  /**
117
- * Creates a chainable emitter that selectively forwards emissions along the chain
118
- * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
119
- * @returns Listenable: emits values that pass the filter
120
- */
121
- filter(check: (value: T) => boolean): EventEmitter<T>;
122
- /**
123
- * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
124
- * @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
125
- *
126
- * If no `compare` function is provided, values will be compared via `===`
127
- * @returns Listenable: emits non-repeating values
128
- */
129
- dedupe(compare?: (a: T, b: T) => boolean): EventEmitter<T>;
130
- /**
131
- * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
132
- *
133
- * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
134
- * All listeners attached to the returned emitter receive the same values as the parent emitter.
135
- *
136
- * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
137
- *
138
- * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
139
- * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
113
+ * Creates a chainable emitter that forwards its parent's emissions, and
114
+ * immediately emits the latest value to new subscribers
115
+ * @returns
140
116
  */
141
- tap(cb: Handler<T>): EventEmitter<T>;
117
+ cached(): EventEmitter<T>;
142
118
  }
143
119
  /**
144
120
  * Creates a linked Emitter and emit() pair
145
121
  * @example
146
122
  * ```ts
147
- * function createForm(options: { onsubmit?: (data: FormData) => void }) {
148
- * const submitEvents = createEventSource(options.onsubmit);
123
+ * function createForm(options?: { onsubmit?: (data: FormData) => void }) {
124
+ * const submitEvents = createEventSource(options?.onsubmit);
149
125
  * const form = $.form({
150
126
  * on: {
151
127
  * submit: (e) => {
@@ -182,4 +158,10 @@ export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLas
182
158
  export declare function interval(t: number | {
183
159
  asMilliseconds: number;
184
160
  }): EventEmitter<number>;
161
+ export declare function timeoutx(t: number | {
162
+ asMilliseconds: number;
163
+ }): EventEmitter<void>;
164
+ export declare function timeout(t: number | {
165
+ asMilliseconds: number;
166
+ }): EventEmitter<void>;
185
167
  export {};
@@ -1,4 +1,4 @@
1
- export class Emitter {
1
+ export class EventEmitter {
2
2
  constructor(onListen) {
3
3
  this.onListen = onListen;
4
4
  }
@@ -36,7 +36,7 @@ export class Emitter {
36
36
  */
37
37
  map(mapFunc) {
38
38
  const listen = this.transform((value, emit) => emit(mapFunc(value)));
39
- return new Emitter(listen);
39
+ return new EventEmitter(listen);
40
40
  }
41
41
  /**
42
42
  * Creates a chainable emitter that selectively forwards emissions along the chain
@@ -45,7 +45,7 @@ export class Emitter {
45
45
  */
46
46
  filter(check) {
47
47
  const listen = this.transform((value, emit) => check(value) && emit(value));
48
- return new Emitter(listen);
48
+ return new EventEmitter(listen);
49
49
  }
50
50
  /**
51
51
  * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
@@ -64,7 +64,7 @@ export class Emitter {
64
64
  previous = { value };
65
65
  }
66
66
  });
67
- return new Emitter(listen);
67
+ return new EventEmitter(listen);
68
68
  }
69
69
  /**
70
70
  * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
@@ -82,7 +82,7 @@ export class Emitter {
82
82
  cb(value);
83
83
  emit(value);
84
84
  });
85
- return new Emitter(listen);
85
+ return new EventEmitter(listen);
86
86
  }
87
87
  /**
88
88
  * Immediately passes this emitter to a callback and returns this emitter
@@ -101,15 +101,10 @@ export class Emitter {
101
101
  * ```
102
102
  * @param cb
103
103
  */
104
- fork(cb) {
105
- cb(this);
104
+ fork(...cb) {
105
+ cb.forEach(cb => cb(this));
106
106
  return this;
107
107
  }
108
- }
109
- export class EventEmitter extends Emitter {
110
- constructor(listen) {
111
- super(listen);
112
- }
113
108
  debounce(ms) {
114
109
  let reset = null;
115
110
  const listen = this.transform((value, emit) => {
@@ -153,20 +148,37 @@ export class EventEmitter extends Emitter {
153
148
  return new EventEmitter(listen);
154
149
  }
155
150
  /**
151
+ * Creates a chainable emitter that
156
152
  * **Experimental**: May change in future revisions
157
- * Note: potential leak - This link will remain subscribed to the parent
158
- * until it emits, regardless of subscriptions to this link.
153
+ * Note: only listens to the parent while at least one downstream subscription is present
159
154
  * @param notifier
160
155
  * @returns
161
156
  */
162
157
  once() {
163
- const { emit, listen } = createListenable();
164
- const unsub = this.apply(v => {
165
- unsub();
166
- emit(v);
167
- });
158
+ let parentUnsubscribe = null;
159
+ let completed = false;
160
+ const clear = () => {
161
+ if (parentUnsubscribe) {
162
+ parentUnsubscribe();
163
+ parentUnsubscribe = null;
164
+ }
165
+ };
166
+ const { emit, listen } = createListenable(() => {
167
+ if (completed)
168
+ return;
169
+ parentUnsubscribe = this.apply(v => {
170
+ completed = true;
171
+ clear();
172
+ emit(v);
173
+ });
174
+ }, clear);
168
175
  return new EventEmitter(listen);
169
176
  }
177
+ delay(ms) {
178
+ return new EventEmitter(this.transform((value, emit) => {
179
+ setTimeout(() => emit(value), ms);
180
+ }));
181
+ }
170
182
  scan(updater, initial) {
171
183
  let state = initial;
172
184
  const listen = this.transform((value, emit) => {
@@ -188,41 +200,101 @@ export class EventEmitter extends Emitter {
188
200
  }
189
201
  /**
190
202
  * **Experimental**: May change in future revisions
191
- * Note: potential leak - This link will remain subscribed to the parent
192
- * until emission limit is reached, regardless of subscriptions to this link.
193
- * @param notifier
203
+ * Note: only listens to the notifier while at least one downstream subscription is present
204
+ * @param limit
194
205
  * @returns
195
206
  */
196
207
  take(limit) {
197
- const { emit, listen } = createListenable();
208
+ let sourceUnsub = null;
198
209
  let count = 0;
199
- const unsub = this.apply(v => {
200
- if (count < limit) {
201
- emit(v);
202
- count++;
203
- if (count >= limit) {
204
- unsub();
205
- }
210
+ let completed = false;
211
+ const { emit, listen } = createListenable(() => {
212
+ if (completed)
213
+ return;
214
+ if (!sourceUnsub) {
215
+ sourceUnsub = this.apply(v => {
216
+ if (count < limit) {
217
+ emit(v);
218
+ count++;
219
+ if (count >= limit) {
220
+ completed = true;
221
+ if (sourceUnsub) {
222
+ sourceUnsub();
223
+ sourceUnsub = null;
224
+ }
225
+ }
226
+ }
227
+ });
228
+ }
229
+ }, () => {
230
+ if (sourceUnsub) {
231
+ sourceUnsub();
232
+ sourceUnsub = null;
206
233
  }
207
234
  });
208
235
  return new EventEmitter(listen);
209
236
  }
210
237
  /**
211
238
  * **Experimental**: May change in future revisions
212
- * Note: potential leak - This link will remain subscribed to the notifier
213
- * until it emits, regardless of subscriptions to this link.
239
+ * Note: only listens to the notifier while at least one downstream subscription is present
214
240
  * @param notifier
215
241
  * @returns
216
242
  */
217
243
  takeUntil(notifier) {
218
- const { emit, listen } = createListenable();
219
- const unsub = this.apply(v => {
220
- emit(v);
221
- });
222
- const unsubNotifier = notifier.apply(() => {
223
- unsub();
224
- unsubNotifier();
225
- });
244
+ let parentUnsubscribe = null;
245
+ let notifierUnsub = null;
246
+ let completed = false;
247
+ const clear = () => {
248
+ if (parentUnsubscribe) {
249
+ parentUnsubscribe();
250
+ parentUnsubscribe = null;
251
+ }
252
+ if (notifierUnsub) {
253
+ notifierUnsub();
254
+ notifierUnsub = null;
255
+ }
256
+ };
257
+ const { emit, listen } = createListenable(() => {
258
+ if (completed)
259
+ return;
260
+ parentUnsubscribe = this.apply(emit);
261
+ const handler = () => {
262
+ completed = true;
263
+ clear();
264
+ };
265
+ notifierUnsub = "subscribe" in notifier
266
+ ? notifier.subscribe(handler)
267
+ : notifier.listen(handler);
268
+ }, clear);
269
+ return new EventEmitter(listen);
270
+ }
271
+ /**
272
+ * Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
273
+ * Disconnects from the parent and becomes inert when the predicate returns false
274
+ * @param predicate Callback to determine whether to keep forwarding
275
+ */
276
+ takeWhile(predicate) {
277
+ let parentUnsubscribe = null;
278
+ let completed = false;
279
+ const clear = () => {
280
+ if (parentUnsubscribe) {
281
+ parentUnsubscribe();
282
+ parentUnsubscribe = null;
283
+ }
284
+ };
285
+ const { emit, listen } = createListenable(() => {
286
+ if (completed)
287
+ return;
288
+ parentUnsubscribe = this.apply(v => {
289
+ if (predicate(v)) {
290
+ emit(v);
291
+ }
292
+ else {
293
+ completed = true;
294
+ clear();
295
+ }
296
+ });
297
+ }, clear);
226
298
  return new EventEmitter(listen);
227
299
  }
228
300
  /**
@@ -237,6 +309,11 @@ export class EventEmitter extends Emitter {
237
309
  return this.onListen(handle);
238
310
  });
239
311
  }
312
+ /**
313
+ * Creates a chainable emitter that forwards its parent's emissions, and
314
+ * immediately emits the latest value to new subscribers
315
+ * @returns
316
+ */
240
317
  cached() {
241
318
  let cache = null;
242
319
  let unsub = null;
@@ -254,68 +331,13 @@ export class EventEmitter extends Emitter {
254
331
  return listen(handler);
255
332
  });
256
333
  }
257
- /**
258
- * Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
259
- * @param mapFunc
260
- * @returns Listenable: emits transformed values
261
- */
262
- map(mapFunc) {
263
- const listen = this.transform((value, emit) => emit(mapFunc(value)));
264
- return new EventEmitter(listen);
265
- }
266
- /**
267
- * Creates a chainable emitter that selectively forwards emissions along the chain
268
- * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
269
- * @returns Listenable: emits values that pass the filter
270
- */
271
- filter(check) {
272
- const listen = this.transform((value, emit) => check(value) && emit(value));
273
- return new EventEmitter(listen);
274
- }
275
- /**
276
- * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
277
- * @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
278
- *
279
- * If no `compare` function is provided, values will be compared via `===`
280
- * @returns Listenable: emits non-repeating values
281
- */
282
- dedupe(compare) {
283
- let previous = null;
284
- const listen = this.transform((value, emit) => {
285
- if (!previous || (compare
286
- ? !compare(previous.value, value)
287
- : (previous.value !== value))) {
288
- emit(value);
289
- previous = { value };
290
- }
291
- });
292
- return new EventEmitter(listen);
293
- }
294
- /**
295
- * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
296
- *
297
- * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
298
- * All listeners attached to the returned emitter receive the same values as the parent emitter.
299
- *
300
- * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
301
- *
302
- * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
303
- * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
304
- */
305
- tap(cb) {
306
- const listen = this.transform((value, emit) => {
307
- cb(value);
308
- emit(value);
309
- });
310
- return new EventEmitter(listen);
311
- }
312
334
  }
313
335
  /**
314
336
  * Creates a linked Emitter and emit() pair
315
337
  * @example
316
338
  * ```ts
317
- * function createForm(options: { onsubmit?: (data: FormData) => void }) {
318
- * const submitEvents = createEventSource(options.onsubmit);
339
+ * function createForm(options?: { onsubmit?: (data: FormData) => void }) {
340
+ * const submitEvents = createEventSource(options?.onsubmit);
319
341
  * const form = $.form({
320
342
  * on: {
321
343
  * submit: (e) => {
@@ -381,3 +403,20 @@ export function interval(t) {
381
403
  }, () => clearInterval(intervalId));
382
404
  return new EventEmitter(listen);
383
405
  }
406
+ export function timeoutx(t) {
407
+ return interval(t).once().map(() => { });
408
+ }
409
+ export function timeout(t) {
410
+ const ms = typeof t === "number" ? t : t.asMilliseconds;
411
+ const targetTime = Date.now() + ms;
412
+ let timeoutId = null;
413
+ const { emit, listen } = createListenable(() => {
414
+ const reminaingMs = targetTime - Date.now();
415
+ if (reminaingMs < 0)
416
+ return;
417
+ timeoutId = setTimeout(emit, reminaingMs);
418
+ }, () => {
419
+ clearTimeout(timeoutId);
420
+ });
421
+ return new EventEmitter(listen);
422
+ }
@@ -1,13 +1,14 @@
1
+ import { type ClassAccessor } from "./element";
1
2
  import { EventEmitter, UnsubscribeFunc } from "./emitter";
2
3
  import { entityDataSymbol } from "./util";
3
- export type ElementClassDescriptor = string | Record<string, boolean | undefined> | undefined | ElementClassDescriptor[];
4
+ export type ElementClassDescriptor = string | Record<string, boolean | Listenable<boolean> | undefined> | undefined | ElementClassDescriptor[];
4
5
  export type DOMContent = number | null | string | Element | JelEntity<object> | Text | DOMContent[];
5
6
  export type DomEntity<T extends HTMLElement> = JelEntity<ElementAPI<T>>;
6
- export type ReactiveSource<T> = ({
7
- listen: (handler: (value: T) => void) => UnsubscribeFunc;
7
+ export type Listenable<T> = {
8
+ subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
8
9
  } | {
9
- subscribe: (handler: (value: T) => void) => UnsubscribeFunc;
10
- });
10
+ listen: (callback: (value: T) => void) => UnsubscribeFunc;
11
+ };
11
12
  export type CSSValue = string | number | null | HexCodeContainer;
12
13
  export type CSSProperty = keyof StylesDescriptor;
13
14
  type HexCodeContainer = {
@@ -18,10 +19,10 @@ export type StylesDescriptor = {
18
19
  [K in keyof CSSStyleDeclaration as [
19
20
  K,
20
21
  CSSStyleDeclaration[K]
21
- ] extends [string, string] ? K : never]+?: CSSValue | ReactiveSource<CSSValue>;
22
+ ] extends [string, string] ? K : never]+?: CSSValue | Listenable<CSSValue>;
22
23
  };
23
- export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | ReactiveSource<CSSValue>) => void);
24
- export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | ReactiveSource<CSSValue>);
24
+ export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | Listenable<CSSValue>) => void);
25
+ export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | Listenable<CSSValue>);
25
26
  export type StyleAccessor = ((styles: StylesDescriptor) => void) & StylesDescriptor & SetStyleFunc;
26
27
  type ContentlessTag = "area" | "br" | "hr" | "iframe" | "input" | "textarea" | "img" | "canvas" | "link" | "meta" | "source" | "embed" | "track" | "base";
27
28
  type TagWithHref = "a" | "link" | "base";
@@ -38,11 +39,11 @@ export type ElementDescriptor<Tag extends string> = {
38
39
  [E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
39
40
  };
40
41
  style?: StylesDescriptor;
41
- cssVariables?: Record<string, CSSValue | ReactiveSource<CSSValue>>;
42
+ cssVariables?: Record<string, CSSValue | Listenable<CSSValue>>;
42
43
  } & (Tag extends TagWithValue ? {
43
44
  value?: string | number;
44
45
  } : {}) & (Tag extends ContentlessTag ? {} : {
45
- content?: DOMContent | ReactiveSource<DOMContent>;
46
+ content?: DOMContent | Listenable<DOMContent>;
46
47
  }) & (Tag extends TagWithSrc ? {
47
48
  src?: string;
48
49
  } : {}) & (Tag extends TagWithHref ? {
@@ -57,14 +58,14 @@ export type ElementDescriptor<Tag extends string> = {
57
58
  } : {});
58
59
  type ElementAPI<T extends HTMLElement> = {
59
60
  readonly element: T;
60
- readonly classes: DOMTokenList;
61
+ readonly classes: ClassAccessor;
61
62
  readonly attribs: {
62
63
  [key: string]: string | null;
63
64
  };
64
65
  readonly events: EventsAccessor;
65
66
  readonly style: StyleAccessor;
66
- setCSSVariable(variableName: string, value: CSSValue | ReactiveSource<CSSValue>): void;
67
- setCSSVariable(table: Record<string, CSSValue | ReactiveSource<CSSValue>>): void;
67
+ setCSSVariable(variableName: string, value: CSSValue | Listenable<CSSValue>): void;
68
+ setCSSVariable(table: Record<string, CSSValue | Listenable<CSSValue>>): void;
68
69
  qsa(selector: string): (Element | DomEntity<HTMLElement>)[];
69
70
  remove(): void;
70
71
  getRect(): DOMRect;
@@ -74,7 +75,7 @@ type ElementAPI<T extends HTMLElement> = {
74
75
  } & (T extends ContentlessElement ? {} : {
75
76
  append(...content: DOMContent[]): void;
76
77
  innerHTML: string;
77
- content: DOMContent | ReactiveSource<DOMContent>;
78
+ content: DOMContent | Listenable<DOMContent>;
78
79
  }) & (T extends HTMLElementTagNameMap[TagWithValue] ? {
79
80
  value: string;
80
81
  select(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/jel",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/jel-ts",
6
6
  "type": "github"
@@ -15,7 +15,11 @@
15
15
  },
16
16
  "./internal/*": null
17
17
  },
18
- "description": "Lightweight DOM manipulation and componentisation",
18
+ "scripts": {
19
+ "prepublishOnly": "cp ../README.md .",
20
+ "postpublish": "rm README.md"
21
+ },
22
+ "description": "Lightweight DOM manipulation, componentisation and reactivity",
19
23
  "keywords": [
20
24
  "dom"
21
25
  ],