@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 +39 -2
- package/index.d.ts +3 -1
- package/index.js +3 -1
- package/internal/element.d.ts +17 -1
- package/internal/element.js +66 -4
- package/internal/emitter.d.ts +34 -52
- package/internal/emitter.js +136 -97
- package/internal/types.d.ts +15 -14
- package/package.json +6 -2
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
|
-
|
|
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
package/internal/element.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/internal/element.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/internal/emitter.d.ts
CHANGED
|
@@ -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
|
|
5
|
+
export declare class EventEmitter<T> {
|
|
5
6
|
protected onListen: ListenFunc<T>;
|
|
6
|
-
|
|
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):
|
|
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):
|
|
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):
|
|
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>):
|
|
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):
|
|
73
|
+
batch(ms: number): EventEmitter<T[]>;
|
|
76
74
|
/**
|
|
75
|
+
* Creates a chainable emitter that
|
|
77
76
|
* **Experimental**: May change in future revisions
|
|
78
|
-
* Note:
|
|
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:
|
|
89
|
-
*
|
|
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:
|
|
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:
|
|
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
|
|
118
|
-
*
|
|
119
|
-
* @returns
|
|
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
|
-
|
|
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
|
|
148
|
-
* const submitEvents = createEventSource(options
|
|
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 {};
|
package/internal/emitter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export class
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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:
|
|
192
|
-
*
|
|
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
|
-
|
|
208
|
+
let sourceUnsub = null;
|
|
198
209
|
let count = 0;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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:
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
318
|
-
* const submitEvents = createEventSource(options
|
|
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
|
+
}
|
package/internal/types.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
|
|
7
|
+
export type Listenable<T> = {
|
|
8
|
+
subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
|
|
8
9
|
} | {
|
|
9
|
-
|
|
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 |
|
|
22
|
+
] extends [string, string] ? K : never]+?: CSSValue | Listenable<CSSValue>;
|
|
22
23
|
};
|
|
23
|
-
export type SetStyleFunc = ((property: CSSProperty, value: CSSValue |
|
|
24
|
-
export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string |
|
|
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 |
|
|
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 |
|
|
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:
|
|
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 |
|
|
67
|
-
setCSSVariable(table: Record<string, CSSValue |
|
|
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 |
|
|
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.
|
|
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
|
-
"
|
|
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
|
],
|