@xtia/jel 0.6.3 → 0.6.5
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 +26 -1
- package/index.d.ts +3 -1
- package/index.js +3 -1
- package/internal/element.js +66 -24
- package/internal/emitter.d.ts +38 -52
- package/internal/emitter.js +128 -97
- package/internal/types.d.ts +4 -4
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -137,4 +137,29 @@ element.events.mousemove
|
|
|
137
137
|
.apply(([x, y]) => console.log("mouse @ ", x, y));
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
For RxJS users, events can be observed with `fromEvent(element.events, "mousemove")`.
|
|
140
|
+
For RxJS users, events can be observed with `fromEvent(element.events, "mousemove")`.
|
|
141
|
+
|
|
142
|
+
## Reactive styles
|
|
143
|
+
|
|
144
|
+
Style properties can be emitter subscriptions:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const mousePosition$ = $(document.body).events.mousemove
|
|
148
|
+
.map(ev => ({x: ev.clientX, y: ev.clientY}));
|
|
149
|
+
|
|
150
|
+
const virtualCursor = $.div({
|
|
151
|
+
classes: "virtual-cursor",
|
|
152
|
+
style: {
|
|
153
|
+
left: mousePosition$.map(v => v.x + "px"),
|
|
154
|
+
top: mousePosition$.map(v => v.y + "px")
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
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.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { animate } from "@xtia/timeline";
|
|
163
|
+
|
|
164
|
+
button.style.opacity = animate(500).tween(0, 1);
|
|
165
|
+
```
|
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.js
CHANGED
|
@@ -2,7 +2,7 @@ import { attribsProxy, eventsProxy, styleProxy } from "./proxy";
|
|
|
2
2
|
import { entityDataSymbol, isContent, isJelEntity } from "./util";
|
|
3
3
|
const elementWrapCache = new WeakMap();
|
|
4
4
|
const recursiveAppend = (parent, c) => {
|
|
5
|
-
if (c === null)
|
|
5
|
+
if (c === null || c === undefined)
|
|
6
6
|
return;
|
|
7
7
|
if (Array.isArray(c)) {
|
|
8
8
|
c.forEach(item => recursiveAppend(parent, item));
|
|
@@ -52,8 +52,9 @@ function createElement(tag, descriptor = {}) {
|
|
|
52
52
|
domElement.setAttribute(k, v === true ? k : v);
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
if (
|
|
56
|
-
|
|
55
|
+
if ("content" in descriptor) {
|
|
56
|
+
ent.content = descriptor.content;
|
|
57
|
+
}
|
|
57
58
|
if (descriptor.style) {
|
|
58
59
|
ent.style(descriptor.style);
|
|
59
60
|
}
|
|
@@ -127,6 +128,9 @@ function observeMutations() {
|
|
|
127
128
|
subtree: true
|
|
128
129
|
});
|
|
129
130
|
}
|
|
131
|
+
function isReactiveSource(value) {
|
|
132
|
+
return typeof value == "object" && value && ("listen" in value || "subscribe" in value);
|
|
133
|
+
}
|
|
130
134
|
function getWrappedElement(element) {
|
|
131
135
|
if (!elementWrapCache.has(element)) {
|
|
132
136
|
const setCSSVariable = (k, v) => {
|
|
@@ -137,53 +141,69 @@ function getWrappedElement(element) {
|
|
|
137
141
|
element.style.setProperty("--" + k, v);
|
|
138
142
|
}
|
|
139
143
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
144
|
+
const listeners = {
|
|
145
|
+
style: {},
|
|
146
|
+
cssVariable: {},
|
|
147
|
+
content: {},
|
|
148
|
+
};
|
|
149
|
+
function addListener(type, prop, source) {
|
|
150
|
+
const set = {
|
|
151
|
+
style: (v) => element.style[prop] = v,
|
|
152
|
+
cssVariable: (v) => setCSSVariable(prop, v),
|
|
153
|
+
content: (v) => {
|
|
154
|
+
element.innerHTML = "";
|
|
155
|
+
recursiveAppend(element, v);
|
|
156
|
+
}
|
|
157
|
+
}[type];
|
|
142
158
|
const subscribe = "subscribe" in source
|
|
143
|
-
? () => source.subscribe(
|
|
144
|
-
: () => source.listen(
|
|
145
|
-
|
|
159
|
+
? () => source.subscribe(set)
|
|
160
|
+
: () => source.listen(set);
|
|
161
|
+
listeners[type][prop] = {
|
|
146
162
|
subscribe,
|
|
147
163
|
unsubscribe: element.isConnected ? subscribe() : null,
|
|
148
164
|
};
|
|
149
165
|
if (!elementMutationMap.has(element)) {
|
|
150
166
|
elementMutationMap.set(element, {
|
|
151
167
|
add: () => {
|
|
152
|
-
Object.values(
|
|
168
|
+
Object.values(listeners).forEach(group => {
|
|
169
|
+
Object.values(group).forEach(l => { var _a; return l.unsubscribe = (_a = l.subscribe) === null || _a === void 0 ? void 0 : _a.call(l); });
|
|
170
|
+
});
|
|
153
171
|
},
|
|
154
172
|
remove: () => {
|
|
155
|
-
Object.values(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
173
|
+
Object.values(listeners).forEach(group => {
|
|
174
|
+
Object.values(group).forEach(l => {
|
|
175
|
+
var _a;
|
|
176
|
+
(_a = l.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(l);
|
|
177
|
+
l.unsubscribe = null;
|
|
178
|
+
});
|
|
159
179
|
});
|
|
160
180
|
}
|
|
161
181
|
});
|
|
162
182
|
}
|
|
163
183
|
observeMutations();
|
|
164
184
|
}
|
|
165
|
-
function
|
|
166
|
-
if (
|
|
167
|
-
|
|
185
|
+
function removeListener(type, prop) {
|
|
186
|
+
if (listeners[type][prop].unsubscribe) {
|
|
187
|
+
listeners[type][prop].unsubscribe();
|
|
168
188
|
}
|
|
169
|
-
delete
|
|
170
|
-
if (Object.keys(
|
|
189
|
+
delete listeners[type][prop];
|
|
190
|
+
if (!Object.keys(listeners).some(group => Object.keys(group).length == 0)) {
|
|
171
191
|
elementMutationMap.delete(element);
|
|
172
192
|
}
|
|
173
193
|
}
|
|
174
194
|
function setStyle(prop, value) {
|
|
175
|
-
if (
|
|
176
|
-
|
|
195
|
+
if (listeners.style[prop])
|
|
196
|
+
removeListener("style", prop);
|
|
177
197
|
if (typeof value == "object" && value) {
|
|
178
198
|
if ("listen" in value || "subscribe" in value) {
|
|
179
|
-
|
|
199
|
+
addListener("style", prop, value);
|
|
180
200
|
return;
|
|
181
201
|
}
|
|
182
202
|
value = value.toString();
|
|
183
203
|
}
|
|
184
204
|
if (value === undefined) {
|
|
185
|
-
return prop in
|
|
186
|
-
?
|
|
205
|
+
return prop in listeners
|
|
206
|
+
? listeners.style[prop].subscribe
|
|
187
207
|
: element.style[prop];
|
|
188
208
|
}
|
|
189
209
|
element.style[prop] = value;
|
|
@@ -199,12 +219,27 @@ function getWrappedElement(element) {
|
|
|
199
219
|
});
|
|
200
220
|
},
|
|
201
221
|
append(...content) {
|
|
222
|
+
var _a;
|
|
223
|
+
if ((_a = listeners.content) === null || _a === void 0 ? void 0 : _a[""])
|
|
224
|
+
removeListener("content", "");
|
|
202
225
|
recursiveAppend(element, content);
|
|
203
226
|
},
|
|
204
227
|
remove: () => element.remove(),
|
|
205
228
|
setCSSVariable(variableNameOrTable, value) {
|
|
206
229
|
if (typeof variableNameOrTable == "object") {
|
|
207
|
-
Object.entries(variableNameOrTable).forEach(([k, v]) =>
|
|
230
|
+
Object.entries(variableNameOrTable).forEach(([k, v]) => {
|
|
231
|
+
if (isReactiveSource(v)) {
|
|
232
|
+
addListener("cssVariable", k, v);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
setCSSVariable(k, v);
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (listeners.cssVariable[variableNameOrTable])
|
|
240
|
+
removeListener("cssVariable", variableNameOrTable);
|
|
241
|
+
if (isReactiveSource(value)) {
|
|
242
|
+
addListener("cssVariable", variableNameOrTable, value);
|
|
208
243
|
return;
|
|
209
244
|
}
|
|
210
245
|
setCSSVariable(variableNameOrTable, value);
|
|
@@ -231,6 +266,13 @@ function getWrappedElement(element) {
|
|
|
231
266
|
});
|
|
232
267
|
},
|
|
233
268
|
set content(v) {
|
|
269
|
+
var _a;
|
|
270
|
+
if ((_a = listeners.content) === null || _a === void 0 ? void 0 : _a[""])
|
|
271
|
+
removeListener("content", "");
|
|
272
|
+
if (isReactiveSource(v)) {
|
|
273
|
+
addListener("content", "", v);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
234
276
|
element.innerHTML = "";
|
|
235
277
|
recursiveAppend(element, v);
|
|
236
278
|
},
|
package/internal/emitter.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
type Handler<T> = (value: T) => void;
|
|
2
2
|
export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
|
|
3
3
|
export type UnsubscribeFunc = () => void;
|
|
4
|
-
export
|
|
4
|
+
export type Listenable<T> = {
|
|
5
|
+
subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
|
|
6
|
+
} | {
|
|
7
|
+
listen: (callback: (value: T) => void) => UnsubscribeFunc;
|
|
8
|
+
};
|
|
9
|
+
export declare class EventEmitter<T> {
|
|
5
10
|
protected onListen: ListenFunc<T>;
|
|
6
|
-
|
|
11
|
+
constructor(onListen: ListenFunc<T>);
|
|
7
12
|
protected transform<R = T>(handler: (value: T, emit: (value: R) => void) => void): (fn: (v: R) => void) => UnsubscribeFunc;
|
|
8
13
|
/**
|
|
9
14
|
* Compatibility alias for `apply()` - registers a function to receive emitted values
|
|
@@ -22,13 +27,13 @@ export declare class Emitter<T> {
|
|
|
22
27
|
* @param mapFunc
|
|
23
28
|
* @returns Listenable: emits transformed values
|
|
24
29
|
*/
|
|
25
|
-
map<R>(mapFunc: (value: T) => R):
|
|
30
|
+
map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
|
|
26
31
|
/**
|
|
27
32
|
* Creates a chainable emitter that selectively forwards emissions along the chain
|
|
28
33
|
* @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
|
|
29
34
|
* @returns Listenable: emits values that pass the filter
|
|
30
35
|
*/
|
|
31
|
-
filter(check: (value: T) => boolean):
|
|
36
|
+
filter(check: (value: T) => boolean): EventEmitter<T>;
|
|
32
37
|
/**
|
|
33
38
|
* Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
|
|
34
39
|
* @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
|
|
@@ -36,7 +41,7 @@ export declare class Emitter<T> {
|
|
|
36
41
|
* If no `compare` function is provided, values will be compared via `===`
|
|
37
42
|
* @returns Listenable: emits non-repeating values
|
|
38
43
|
*/
|
|
39
|
-
dedupe(compare?: (a: T, b: T) => boolean):
|
|
44
|
+
dedupe(compare?: (a: T, b: T) => boolean): EventEmitter<T>;
|
|
40
45
|
/**
|
|
41
46
|
* Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
|
|
42
47
|
*
|
|
@@ -48,7 +53,7 @@ export declare class Emitter<T> {
|
|
|
48
53
|
* @param cb A function to be called as a side effect for each value emitted by the parent emitter.
|
|
49
54
|
* @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
|
|
50
55
|
*/
|
|
51
|
-
tap(cb: Handler<T>):
|
|
56
|
+
tap(cb: Handler<T>): EventEmitter<T>;
|
|
52
57
|
/**
|
|
53
58
|
* Immediately passes this emitter to a callback and returns this emitter
|
|
54
59
|
*
|
|
@@ -66,39 +71,41 @@ export declare class Emitter<T> {
|
|
|
66
71
|
* ```
|
|
67
72
|
* @param cb
|
|
68
73
|
*/
|
|
69
|
-
fork(cb: (branch: this) => void): this;
|
|
70
|
-
}
|
|
71
|
-
export declare class EventEmitter<T> extends Emitter<T> {
|
|
72
|
-
constructor(listen: ListenFunc<T>);
|
|
74
|
+
fork(...cb: ((branch: this) => void)[]): this;
|
|
73
75
|
debounce(ms: number): EventEmitter<T>;
|
|
74
76
|
throttle(ms: number): EventEmitter<T>;
|
|
75
|
-
batch(ms: number):
|
|
77
|
+
batch(ms: number): EventEmitter<T[]>;
|
|
76
78
|
/**
|
|
79
|
+
* Creates a chainable emitter that
|
|
77
80
|
* **Experimental**: May change in future revisions
|
|
78
|
-
* Note:
|
|
79
|
-
* until it emits, regardless of subscriptions to this link.
|
|
81
|
+
* Note: only listens to the parent while at least one downstream subscription is present
|
|
80
82
|
* @param notifier
|
|
81
83
|
* @returns
|
|
82
84
|
*/
|
|
83
85
|
once(): EventEmitter<T>;
|
|
86
|
+
delay(ms: number): EventEmitter<T>;
|
|
84
87
|
scan<S>(updater: (state: S, value: T) => S, initial: S): EventEmitter<S>;
|
|
85
88
|
buffer(count: number): EventEmitter<T[]>;
|
|
86
89
|
/**
|
|
87
90
|
* **Experimental**: May change in future revisions
|
|
88
|
-
* Note:
|
|
89
|
-
*
|
|
90
|
-
* @param notifier
|
|
91
|
+
* Note: only listens to the notifier while at least one downstream subscription is present
|
|
92
|
+
* @param limit
|
|
91
93
|
* @returns
|
|
92
94
|
*/
|
|
93
95
|
take(limit: number): EventEmitter<T>;
|
|
94
96
|
/**
|
|
95
97
|
* **Experimental**: May change in future revisions
|
|
96
|
-
* Note:
|
|
97
|
-
* until it emits, regardless of subscriptions to this link.
|
|
98
|
+
* Note: only listens to the notifier while at least one downstream subscription is present
|
|
98
99
|
* @param notifier
|
|
99
100
|
* @returns
|
|
100
101
|
*/
|
|
101
|
-
takeUntil(notifier:
|
|
102
|
+
takeUntil(notifier: Listenable<any>): EventEmitter<T>;
|
|
103
|
+
/**
|
|
104
|
+
* Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
|
|
105
|
+
* Disconnects from the parent and becomes inert when the predicate returns false
|
|
106
|
+
* @param predicate Callback to determine whether to keep forwarding
|
|
107
|
+
*/
|
|
108
|
+
takeWhile(predicate: (value: T) => boolean): EventEmitter<T>;
|
|
102
109
|
/**
|
|
103
110
|
* Creates a chainable emitter that immediately emits a value to every new subscriber,
|
|
104
111
|
* then forwards parent emissions
|
|
@@ -106,46 +113,19 @@ export declare class EventEmitter<T> extends Emitter<T> {
|
|
|
106
113
|
* @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
|
|
107
114
|
*/
|
|
108
115
|
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
|
-
/**
|
|
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
116
|
/**
|
|
123
|
-
* Creates a chainable emitter that
|
|
124
|
-
*
|
|
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.
|
|
117
|
+
* Creates a chainable emitter that forwards its parent's emissions, and
|
|
118
|
+
* immediately emits the latest value to new subscribers
|
|
119
|
+
* @returns
|
|
140
120
|
*/
|
|
141
|
-
|
|
121
|
+
cached(): EventEmitter<T>;
|
|
142
122
|
}
|
|
143
123
|
/**
|
|
144
124
|
* Creates a linked Emitter and emit() pair
|
|
145
125
|
* @example
|
|
146
126
|
* ```ts
|
|
147
|
-
* function createForm(options
|
|
148
|
-
* const submitEvents = createEventSource(options
|
|
127
|
+
* function createForm(options?: { onsubmit?: (data: FormData) => void }) {
|
|
128
|
+
* const submitEvents = createEventSource(options?.onsubmit);
|
|
149
129
|
* const form = $.form({
|
|
150
130
|
* on: {
|
|
151
131
|
* submit: (e) => {
|
|
@@ -182,4 +162,10 @@ export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLas
|
|
|
182
162
|
export declare function interval(t: number | {
|
|
183
163
|
asMilliseconds: number;
|
|
184
164
|
}): EventEmitter<number>;
|
|
165
|
+
export declare function timeoutx(t: number | {
|
|
166
|
+
asMilliseconds: number;
|
|
167
|
+
}): EventEmitter<void>;
|
|
168
|
+
export declare function timeout(t: number | {
|
|
169
|
+
asMilliseconds: number;
|
|
170
|
+
}): EventEmitter<void>;
|
|
185
171
|
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,12 @@ 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 { emit, listen } = createListenable();
|
|
412
|
+
setTimeout(emit, ms);
|
|
413
|
+
return new EventEmitter(listen);
|
|
414
|
+
}
|
package/internal/types.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type ElementDescriptor<Tag extends string> = {
|
|
|
38
38
|
[E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
|
|
39
39
|
};
|
|
40
40
|
style?: StylesDescriptor;
|
|
41
|
-
cssVariables?: Record<string, CSSValue
|
|
41
|
+
cssVariables?: Record<string, CSSValue | ReactiveSource<CSSValue>>;
|
|
42
42
|
} & (Tag extends TagWithValue ? {
|
|
43
43
|
value?: string | number;
|
|
44
44
|
} : {}) & (Tag extends ContentlessTag ? {} : {
|
|
@@ -63,8 +63,8 @@ type ElementAPI<T extends HTMLElement> = {
|
|
|
63
63
|
};
|
|
64
64
|
readonly events: EventsAccessor;
|
|
65
65
|
readonly style: StyleAccessor;
|
|
66
|
-
setCSSVariable(variableName: string, value: CSSValue): void;
|
|
67
|
-
setCSSVariable(table: Record<string, CSSValue
|
|
66
|
+
setCSSVariable(variableName: string, value: CSSValue | ReactiveSource<CSSValue>): void;
|
|
67
|
+
setCSSVariable(table: Record<string, CSSValue | ReactiveSource<CSSValue>>): void;
|
|
68
68
|
qsa(selector: string): (Element | DomEntity<HTMLElement>)[];
|
|
69
69
|
remove(): void;
|
|
70
70
|
getRect(): DOMRect;
|
|
@@ -74,7 +74,7 @@ type ElementAPI<T extends HTMLElement> = {
|
|
|
74
74
|
} & (T extends ContentlessElement ? {} : {
|
|
75
75
|
append(...content: DOMContent[]): void;
|
|
76
76
|
innerHTML: string;
|
|
77
|
-
content: DOMContent
|
|
77
|
+
content: DOMContent | ReactiveSource<DOMContent>;
|
|
78
78
|
}) & (T extends HTMLElementTagNameMap[TagWithValue] ? {
|
|
79
79
|
value: string;
|
|
80
80
|
select(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtia/jel",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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
|
],
|