@xtia/jel 0.6.5 → 0.7.1
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 +15 -3
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/internal/element.d.ts +17 -1
- package/internal/element.js +74 -12
- package/internal/emitter.d.ts +36 -12
- package/internal/emitter.js +99 -55
- package/internal/proxy.js +2 -9
- package/internal/types.d.ts +25 -22
- package/internal/util.d.ts +3 -2
- package/internal/util.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,6 @@ body.append([
|
|
|
42
42
|
))
|
|
43
43
|
)
|
|
44
44
|
])
|
|
45
|
-
|
|
46
45
|
```
|
|
47
46
|
|
|
48
47
|
## `DOMContent`
|
|
@@ -141,20 +140,33 @@ For RxJS users, events can be observed with `fromEvent(element.events, "mousemov
|
|
|
141
140
|
|
|
142
141
|
## Reactive styles
|
|
143
142
|
|
|
144
|
-
Style properties can be emitter subscriptions:
|
|
143
|
+
Style properties, content and class presence can be emitter subscriptions:
|
|
145
144
|
|
|
146
145
|
```ts
|
|
147
146
|
const mousePosition$ = $(document.body).events.mousemove
|
|
148
147
|
.map(ev => ({x: ev.clientX, y: ev.clientY}));
|
|
149
148
|
|
|
150
149
|
const virtualCursor = $.div({
|
|
151
|
-
classes:
|
|
150
|
+
classes: {
|
|
151
|
+
"virtual-cursor": true,
|
|
152
|
+
"near-top": mousePosition$.map(v => v.y < 100)
|
|
153
|
+
},
|
|
152
154
|
style: {
|
|
153
155
|
left: mousePosition$.map(v => v.x + "px"),
|
|
154
156
|
top: mousePosition$.map(v => v.y + "px")
|
|
155
157
|
}
|
|
156
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);
|
|
157
168
|
```
|
|
169
|
+
Removing an element from the page will unsubscribe from any attached stream, and resubscribe if subsequently appended.
|
|
158
170
|
|
|
159
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.
|
|
160
172
|
|
package/index.d.ts
CHANGED
|
@@ -1,6 +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
|
-
export { createEventSource, interval } from "./internal/emitter";
|
|
4
|
+
export { createEventSource, interval, timeout, SubjectEmitter, toEventEmitter } from "./internal/emitter";
|
|
5
5
|
export { $ };
|
|
6
6
|
export declare const $body: import(".").DomEntity<HTMLElement>;
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { $ } from "./internal/element";
|
|
2
2
|
export { createEntity } from "./internal/util";
|
|
3
|
-
export { createEventSource, interval } from "./internal/emitter";
|
|
3
|
+
export { createEventSource, interval, timeout, SubjectEmitter, toEventEmitter } from "./internal/emitter";
|
|
4
4
|
export { $ };
|
|
5
5
|
export const $body = $(document.body);
|
package/internal/element.d.ts
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
|
-
import { DomHelper } from "./types";
|
|
1
|
+
import { DomHelper, EmitterLike } 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: EmitterLike<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: EmitterLike<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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { attribsProxy, eventsProxy, styleProxy } from "./proxy";
|
|
2
|
-
import { entityDataSymbol, isContent, isJelEntity } from "./util";
|
|
2
|
+
import { entityDataSymbol, isContent, isJelEntity, isReactiveSource } from "./util";
|
|
3
3
|
const elementWrapCache = new WeakMap();
|
|
4
4
|
const recursiveAppend = (parent, c) => {
|
|
5
5
|
if (c === null || c === undefined)
|
|
@@ -18,9 +18,9 @@ const recursiveAppend = (parent, c) => {
|
|
|
18
18
|
};
|
|
19
19
|
function createElement(tag, descriptor = {}) {
|
|
20
20
|
if (isContent(descriptor))
|
|
21
|
-
|
|
21
|
+
descriptor = {
|
|
22
22
|
content: descriptor,
|
|
23
|
-
}
|
|
23
|
+
};
|
|
24
24
|
const domElement = document.createElement(tag);
|
|
25
25
|
const ent = getWrappedElement(domElement);
|
|
26
26
|
const applyClasses = (classes) => {
|
|
@@ -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 || []);
|
|
@@ -64,6 +68,8 @@ function createElement(tag, descriptor = {}) {
|
|
|
64
68
|
if (descriptor.on) {
|
|
65
69
|
Object.entries(descriptor.on).forEach(([eventName, handler]) => ent.events[eventName].apply(handler));
|
|
66
70
|
}
|
|
71
|
+
if (descriptor.init)
|
|
72
|
+
descriptor.init(ent);
|
|
67
73
|
return ent;
|
|
68
74
|
}
|
|
69
75
|
;
|
|
@@ -128,9 +134,6 @@ function observeMutations() {
|
|
|
128
134
|
subtree: true
|
|
129
135
|
});
|
|
130
136
|
}
|
|
131
|
-
function isReactiveSource(value) {
|
|
132
|
-
return typeof value == "object" && value && ("listen" in value || "subscribe" in value);
|
|
133
|
-
}
|
|
134
137
|
function getWrappedElement(element) {
|
|
135
138
|
if (!elementWrapCache.has(element)) {
|
|
136
139
|
const setCSSVariable = (k, v) => {
|
|
@@ -145,6 +148,7 @@ function getWrappedElement(element) {
|
|
|
145
148
|
style: {},
|
|
146
149
|
cssVariable: {},
|
|
147
150
|
content: {},
|
|
151
|
+
class: {},
|
|
148
152
|
};
|
|
149
153
|
function addListener(type, prop, source) {
|
|
150
154
|
const set = {
|
|
@@ -153,7 +157,8 @@ function getWrappedElement(element) {
|
|
|
153
157
|
content: (v) => {
|
|
154
158
|
element.innerHTML = "";
|
|
155
159
|
recursiveAppend(element, v);
|
|
156
|
-
}
|
|
160
|
+
},
|
|
161
|
+
class: (v) => element.classList.toggle(prop, v),
|
|
157
162
|
}[type];
|
|
158
163
|
const subscribe = "subscribe" in source
|
|
159
164
|
? () => source.subscribe(set)
|
|
@@ -166,7 +171,7 @@ function getWrappedElement(element) {
|
|
|
166
171
|
elementMutationMap.set(element, {
|
|
167
172
|
add: () => {
|
|
168
173
|
Object.values(listeners).forEach(group => {
|
|
169
|
-
Object.values(group).forEach(l =>
|
|
174
|
+
Object.values(group).forEach(l => l.unsubscribe = l.subscribe());
|
|
170
175
|
});
|
|
171
176
|
},
|
|
172
177
|
remove: () => {
|
|
@@ -214,9 +219,11 @@ function getWrappedElement(element) {
|
|
|
214
219
|
},
|
|
215
220
|
get element() { return element; },
|
|
216
221
|
on(eventId, handler) {
|
|
217
|
-
|
|
222
|
+
const fn = (eventData) => {
|
|
218
223
|
handler.call(domEntity, eventData);
|
|
219
|
-
}
|
|
224
|
+
};
|
|
225
|
+
element.addEventListener(eventId, fn);
|
|
226
|
+
return () => element.removeEventListener(eventId, fn);
|
|
220
227
|
},
|
|
221
228
|
append(...content) {
|
|
222
229
|
var _a;
|
|
@@ -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,11 +1,7 @@
|
|
|
1
|
+
import { EmitterLike } 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 type Listenable<T> = {
|
|
5
|
-
subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
|
|
6
|
-
} | {
|
|
7
|
-
listen: (callback: (value: T) => void) => UnsubscribeFunc;
|
|
8
|
-
};
|
|
9
5
|
export declare class EventEmitter<T> {
|
|
10
6
|
protected onListen: ListenFunc<T>;
|
|
11
7
|
constructor(onListen: ListenFunc<T>);
|
|
@@ -99,7 +95,7 @@ export declare class EventEmitter<T> {
|
|
|
99
95
|
* @param notifier
|
|
100
96
|
* @returns
|
|
101
97
|
*/
|
|
102
|
-
takeUntil(notifier:
|
|
98
|
+
takeUntil(notifier: EmitterLike<any>): EventEmitter<T>;
|
|
103
99
|
/**
|
|
104
100
|
* Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
|
|
105
101
|
* Disconnects from the parent and becomes inert when the predicate returns false
|
|
@@ -119,9 +115,15 @@ export declare class EventEmitter<T> {
|
|
|
119
115
|
* @returns
|
|
120
116
|
*/
|
|
121
117
|
cached(): EventEmitter<T>;
|
|
118
|
+
/**
|
|
119
|
+
* Creates a chainable emitter that forwards emissions from the parent and any of the provided emitters
|
|
120
|
+
* @param emitters
|
|
121
|
+
*/
|
|
122
|
+
or(...emitters: EmitterLike<T>[]): EmitterLike<T>;
|
|
123
|
+
or<U>(...emitters: EmitterLike<U>[]): EmitterLike<T | U>;
|
|
122
124
|
}
|
|
123
125
|
/**
|
|
124
|
-
* Creates a linked
|
|
126
|
+
* Creates a linked EventEmitter and emit() pair
|
|
125
127
|
* @example
|
|
126
128
|
* ```ts
|
|
127
129
|
* function createForm(options?: { onsubmit?: (data: FormData) => void }) {
|
|
@@ -155,17 +157,39 @@ export declare function createEventSource<T>(initialHandler?: Handler<T>): {
|
|
|
155
157
|
emit: (value: T) => void;
|
|
156
158
|
emitter: EventEmitter<T>;
|
|
157
159
|
};
|
|
158
|
-
export declare function createListenable<T>(
|
|
160
|
+
export declare function createListenable<T>(sourceListen?: () => UnsubscribeFunc | undefined): {
|
|
159
161
|
listen: (fn: (v: T) => void) => UnsubscribeFunc;
|
|
160
162
|
emit: (value: T) => void;
|
|
161
163
|
};
|
|
162
|
-
export declare function interval(
|
|
164
|
+
export declare function interval(ms: number | {
|
|
163
165
|
asMilliseconds: number;
|
|
164
166
|
}): EventEmitter<number>;
|
|
165
|
-
export declare function timeoutx(t: number | {
|
|
166
|
-
asMilliseconds: number;
|
|
167
|
-
}): EventEmitter<void>;
|
|
168
167
|
export declare function timeout(t: number | {
|
|
169
168
|
asMilliseconds: number;
|
|
170
169
|
}): EventEmitter<void>;
|
|
170
|
+
export declare class SubjectEmitter<T> extends EventEmitter<T> {
|
|
171
|
+
private emit;
|
|
172
|
+
private _value;
|
|
173
|
+
constructor(initial: T);
|
|
174
|
+
get value(): T;
|
|
175
|
+
next(value: T): void;
|
|
176
|
+
}
|
|
177
|
+
type EventSource<T, E extends string> = {
|
|
178
|
+
on: (eventName: E, handler: (value: T) => void) => UnsubscribeFunc;
|
|
179
|
+
} | {
|
|
180
|
+
on: (eventName: E, handler: (value: T) => void) => void | UnsubscribeFunc;
|
|
181
|
+
off: (eventName: E, handler: (value: T) => void) => void;
|
|
182
|
+
} | {
|
|
183
|
+
addEventListener: (eventName: E, handler: (value: T) => void) => UnsubscribeFunc;
|
|
184
|
+
} | {
|
|
185
|
+
addEventListener: (eventName: E, handler: (value: T) => void) => void | UnsubscribeFunc;
|
|
186
|
+
removeEventListener: (eventName: E, handler: (value: T) => void) => void;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Create an EventEmitter from an event source. Event sources can be RxJS observables, existing EventEmitters, or objects that
|
|
190
|
+
* provide a subscribe()/listen() => UnsubscribeFunc method.
|
|
191
|
+
* @param source
|
|
192
|
+
*/
|
|
193
|
+
export declare function toEventEmitter<T>(source: EmitterLike<T>): EventEmitter<T>;
|
|
194
|
+
export declare function toEventEmitter<T, E extends string>(source: EventSource<T, E>, eventName: E): EventEmitter<T>;
|
|
171
195
|
export {};
|
package/internal/emitter.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
+
import { isReactiveSource } from "./util";
|
|
1
2
|
export class EventEmitter {
|
|
2
3
|
constructor(onListen) {
|
|
3
4
|
this.onListen = onListen;
|
|
4
5
|
}
|
|
5
6
|
transform(handler) {
|
|
6
|
-
|
|
7
|
-
const parentListen = this.onListen;
|
|
8
|
-
const { emit, listen } = createListenable(() => parentUnsubscribe = parentListen(value => {
|
|
7
|
+
const { emit, listen } = createListenable(() => this.onListen(value => {
|
|
9
8
|
handler(value, emit);
|
|
10
|
-
})
|
|
11
|
-
parentUnsubscribe();
|
|
12
|
-
parentUnsubscribe = null;
|
|
13
|
-
});
|
|
9
|
+
}));
|
|
14
10
|
return listen;
|
|
15
11
|
}
|
|
16
12
|
/**
|
|
@@ -171,7 +167,8 @@ export class EventEmitter {
|
|
|
171
167
|
clear();
|
|
172
168
|
emit(v);
|
|
173
169
|
});
|
|
174
|
-
|
|
170
|
+
return clear;
|
|
171
|
+
});
|
|
175
172
|
return new EventEmitter(listen);
|
|
176
173
|
}
|
|
177
174
|
delay(ms) {
|
|
@@ -226,11 +223,7 @@ export class EventEmitter {
|
|
|
226
223
|
}
|
|
227
224
|
});
|
|
228
225
|
}
|
|
229
|
-
|
|
230
|
-
if (sourceUnsub) {
|
|
231
|
-
sourceUnsub();
|
|
232
|
-
sourceUnsub = null;
|
|
233
|
-
}
|
|
226
|
+
return sourceUnsub;
|
|
234
227
|
});
|
|
235
228
|
return new EventEmitter(listen);
|
|
236
229
|
}
|
|
@@ -245,14 +238,8 @@ export class EventEmitter {
|
|
|
245
238
|
let notifierUnsub = null;
|
|
246
239
|
let completed = false;
|
|
247
240
|
const clear = () => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
parentUnsubscribe = null;
|
|
251
|
-
}
|
|
252
|
-
if (notifierUnsub) {
|
|
253
|
-
notifierUnsub();
|
|
254
|
-
notifierUnsub = null;
|
|
255
|
-
}
|
|
241
|
+
parentUnsubscribe === null || parentUnsubscribe === void 0 ? void 0 : parentUnsubscribe();
|
|
242
|
+
notifierUnsub === null || notifierUnsub === void 0 ? void 0 : notifierUnsub();
|
|
256
243
|
};
|
|
257
244
|
const { emit, listen } = createListenable(() => {
|
|
258
245
|
if (completed)
|
|
@@ -262,10 +249,9 @@ export class EventEmitter {
|
|
|
262
249
|
completed = true;
|
|
263
250
|
clear();
|
|
264
251
|
};
|
|
265
|
-
notifierUnsub =
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}, clear);
|
|
252
|
+
notifierUnsub = toEventEmitter(notifier).listen(handler);
|
|
253
|
+
return clear;
|
|
254
|
+
});
|
|
269
255
|
return new EventEmitter(listen);
|
|
270
256
|
}
|
|
271
257
|
/**
|
|
@@ -274,14 +260,8 @@ export class EventEmitter {
|
|
|
274
260
|
* @param predicate Callback to determine whether to keep forwarding
|
|
275
261
|
*/
|
|
276
262
|
takeWhile(predicate) {
|
|
277
|
-
let parentUnsubscribe
|
|
263
|
+
let parentUnsubscribe;
|
|
278
264
|
let completed = false;
|
|
279
|
-
const clear = () => {
|
|
280
|
-
if (parentUnsubscribe) {
|
|
281
|
-
parentUnsubscribe();
|
|
282
|
-
parentUnsubscribe = null;
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
265
|
const { emit, listen } = createListenable(() => {
|
|
286
266
|
if (completed)
|
|
287
267
|
return;
|
|
@@ -291,10 +271,12 @@ export class EventEmitter {
|
|
|
291
271
|
}
|
|
292
272
|
else {
|
|
293
273
|
completed = true;
|
|
294
|
-
|
|
274
|
+
parentUnsubscribe();
|
|
275
|
+
parentUnsubscribe = undefined;
|
|
295
276
|
}
|
|
296
277
|
});
|
|
297
|
-
|
|
278
|
+
return () => parentUnsubscribe === null || parentUnsubscribe === void 0 ? void 0 : parentUnsubscribe();
|
|
279
|
+
});
|
|
298
280
|
return new EventEmitter(listen);
|
|
299
281
|
}
|
|
300
282
|
/**
|
|
@@ -316,24 +298,25 @@ export class EventEmitter {
|
|
|
316
298
|
*/
|
|
317
299
|
cached() {
|
|
318
300
|
let cache = null;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
emit(value);
|
|
324
|
-
}));
|
|
325
|
-
}, () => {
|
|
326
|
-
unsub();
|
|
327
|
-
});
|
|
301
|
+
const { listen, emit } = createListenable(() => this.onListen((value => {
|
|
302
|
+
cache = { value };
|
|
303
|
+
emit(value);
|
|
304
|
+
})));
|
|
328
305
|
return new EventEmitter(handler => {
|
|
329
306
|
if (cache)
|
|
330
307
|
handler(cache.value);
|
|
331
308
|
return listen(handler);
|
|
332
309
|
});
|
|
333
310
|
}
|
|
311
|
+
or(...emitters) {
|
|
312
|
+
return new EventEmitter(handler => {
|
|
313
|
+
const unsubs = [this, ...emitters].map(e => toEventEmitter(e).listen(handler));
|
|
314
|
+
return () => unsubs.forEach(unsub => unsub());
|
|
315
|
+
});
|
|
316
|
+
}
|
|
334
317
|
}
|
|
335
318
|
/**
|
|
336
|
-
* Creates a linked
|
|
319
|
+
* Creates a linked EventEmitter and emit() pair
|
|
337
320
|
* @example
|
|
338
321
|
* ```ts
|
|
339
322
|
* function createForm(options?: { onsubmit?: (data: FormData) => void }) {
|
|
@@ -372,13 +355,14 @@ export function createEventSource(initialHandler) {
|
|
|
372
355
|
emitter: new EventEmitter(listen)
|
|
373
356
|
};
|
|
374
357
|
}
|
|
375
|
-
export function createListenable(
|
|
358
|
+
export function createListenable(sourceListen) {
|
|
376
359
|
const handlers = [];
|
|
360
|
+
let onRemoveLast;
|
|
377
361
|
const addListener = (fn) => {
|
|
378
362
|
const unique = { fn };
|
|
379
363
|
handlers.push(unique);
|
|
380
|
-
if (
|
|
381
|
-
|
|
364
|
+
if (sourceListen && handlers.length == 1)
|
|
365
|
+
onRemoveLast = sourceListen();
|
|
382
366
|
return () => {
|
|
383
367
|
const idx = handlers.indexOf(unique);
|
|
384
368
|
if (idx === -1)
|
|
@@ -393,22 +377,82 @@ export function createListenable(onAddFirst, onRemoveLast) {
|
|
|
393
377
|
emit: (value) => handlers.forEach(h => h.fn(value)),
|
|
394
378
|
};
|
|
395
379
|
}
|
|
396
|
-
export function interval(
|
|
380
|
+
export function interval(ms) {
|
|
397
381
|
let intervalId = null;
|
|
398
382
|
let idx = 0;
|
|
399
383
|
const { emit, listen } = createListenable(() => {
|
|
400
384
|
intervalId = setInterval(() => {
|
|
401
385
|
emit(idx++);
|
|
402
|
-
}, typeof
|
|
403
|
-
|
|
386
|
+
}, typeof ms == "number" ? ms : ms.asMilliseconds);
|
|
387
|
+
return () => clearInterval(intervalId);
|
|
388
|
+
});
|
|
404
389
|
return new EventEmitter(listen);
|
|
405
390
|
}
|
|
406
|
-
export function timeoutx(t) {
|
|
407
|
-
return interval(t).once().map(() => { });
|
|
408
|
-
}
|
|
409
391
|
export function timeout(t) {
|
|
410
392
|
const ms = typeof t === "number" ? t : t.asMilliseconds;
|
|
411
|
-
const
|
|
412
|
-
|
|
393
|
+
const targetTime = Date.now() + ms;
|
|
394
|
+
let timeoutId = null;
|
|
395
|
+
const { emit, listen } = createListenable(() => {
|
|
396
|
+
const reminaingMs = targetTime - Date.now();
|
|
397
|
+
if (reminaingMs < 0)
|
|
398
|
+
return;
|
|
399
|
+
timeoutId = setTimeout(emit, reminaingMs);
|
|
400
|
+
return () => clearTimeout(timeoutId);
|
|
401
|
+
});
|
|
413
402
|
return new EventEmitter(listen);
|
|
414
403
|
}
|
|
404
|
+
export class SubjectEmitter extends EventEmitter {
|
|
405
|
+
constructor(initial) {
|
|
406
|
+
const { emit, listen } = createListenable();
|
|
407
|
+
super(h => {
|
|
408
|
+
h(this._value);
|
|
409
|
+
return listen(h);
|
|
410
|
+
});
|
|
411
|
+
this.emit = emit;
|
|
412
|
+
this._value = initial;
|
|
413
|
+
}
|
|
414
|
+
get value() {
|
|
415
|
+
return this._value;
|
|
416
|
+
}
|
|
417
|
+
next(value) {
|
|
418
|
+
this._value = value;
|
|
419
|
+
this.emit(value);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
export function toEventEmitter(source, eventName) {
|
|
423
|
+
if (source instanceof EventEmitter)
|
|
424
|
+
return source;
|
|
425
|
+
if (eventName !== undefined) {
|
|
426
|
+
// addEL()
|
|
427
|
+
if ("addEventListener" in source) {
|
|
428
|
+
if ("removeEventListener" in source && typeof source.removeEventListener == "function") {
|
|
429
|
+
return new EventEmitter(h => {
|
|
430
|
+
var _a;
|
|
431
|
+
return (_a = source.addEventListener(eventName, h)) !== null && _a !== void 0 ? _a : (() => source.removeEventListener(eventName, h));
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return new EventEmitter(h => {
|
|
435
|
+
return source.addEventListener(eventName, h);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
// on()
|
|
439
|
+
if ("on" in source) {
|
|
440
|
+
if ("off" in source && typeof source.off == "function") {
|
|
441
|
+
return new EventEmitter(h => {
|
|
442
|
+
var _a;
|
|
443
|
+
return (_a = source.on(eventName, h)) !== null && _a !== void 0 ? _a : (() => source.off(eventName, h));
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return new EventEmitter(h => {
|
|
447
|
+
return source.on(eventName, h);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (isReactiveSource(source)) {
|
|
452
|
+
const subscribe = "subscribe" in source
|
|
453
|
+
? (h) => source.subscribe(h)
|
|
454
|
+
: (h) => source.listen(h);
|
|
455
|
+
return new EventEmitter(subscribe);
|
|
456
|
+
}
|
|
457
|
+
throw new Error("Invalid event source");
|
|
458
|
+
}
|
package/internal/proxy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toEventEmitter } from "./emitter";
|
|
2
2
|
export const styleProxy = {
|
|
3
3
|
get(style, prop) {
|
|
4
4
|
return style(prop);
|
|
@@ -42,13 +42,6 @@ export const eventsProxy = {
|
|
|
42
42
|
if (key == "removeEventListener") {
|
|
43
43
|
return (name, handler) => element.removeEventListener(name, handler);
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
const wrappedHandler = (event) => handler(event);
|
|
47
|
-
element.addEventListener(key, wrappedHandler);
|
|
48
|
-
return () => {
|
|
49
|
-
element.removeEventListener(key, wrappedHandler);
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
return new EventEmitter(listen);
|
|
45
|
+
return toEventEmitter(element, key);
|
|
53
46
|
}
|
|
54
47
|
};
|
package/internal/types.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
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 | EmitterLike<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 HTMLTag = keyof HTMLElementTagNameMap;
|
|
8
|
+
export type EmitterLike<T> = {
|
|
9
|
+
subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
|
|
8
10
|
} | {
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
+
listen: (callback: (value: T) => void) => UnsubscribeFunc;
|
|
12
|
+
};
|
|
11
13
|
export type CSSValue = string | number | null | HexCodeContainer;
|
|
12
14
|
export type CSSProperty = keyof StylesDescriptor;
|
|
13
15
|
type HexCodeContainer = {
|
|
@@ -18,10 +20,10 @@ export type StylesDescriptor = {
|
|
|
18
20
|
[K in keyof CSSStyleDeclaration as [
|
|
19
21
|
K,
|
|
20
22
|
CSSStyleDeclaration[K]
|
|
21
|
-
] extends [string, string] ? K : never]+?: CSSValue |
|
|
23
|
+
] extends [string, string] ? K : never]+?: CSSValue | EmitterLike<CSSValue>;
|
|
22
24
|
};
|
|
23
|
-
export type SetStyleFunc = ((property: CSSProperty, value: CSSValue |
|
|
24
|
-
export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string |
|
|
25
|
+
export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | EmitterLike<CSSValue>) => void);
|
|
26
|
+
export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | EmitterLike<CSSValue>);
|
|
25
27
|
export type StyleAccessor = ((styles: StylesDescriptor) => void) & StylesDescriptor & SetStyleFunc;
|
|
26
28
|
type ContentlessTag = "area" | "br" | "hr" | "iframe" | "input" | "textarea" | "img" | "canvas" | "link" | "meta" | "source" | "embed" | "track" | "base";
|
|
27
29
|
type TagWithHref = "a" | "link" | "base";
|
|
@@ -31,18 +33,19 @@ type TagWithWidthHeight = "canvas" | "img" | "embed" | "iframe" | "video";
|
|
|
31
33
|
type TagWithType = "input" | "source" | "button";
|
|
32
34
|
type TagWithName = 'input' | 'textarea' | 'select' | 'form';
|
|
33
35
|
type ContentlessElement = HTMLElementTagNameMap[ContentlessTag];
|
|
34
|
-
export type ElementDescriptor<Tag extends
|
|
36
|
+
export type ElementDescriptor<Tag extends HTMLTag> = {
|
|
35
37
|
classes?: ElementClassDescriptor;
|
|
36
38
|
attribs?: Record<string, string | number | boolean>;
|
|
37
39
|
on?: {
|
|
38
40
|
[E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
|
|
39
41
|
};
|
|
40
42
|
style?: StylesDescriptor;
|
|
41
|
-
cssVariables?: Record<string, CSSValue |
|
|
43
|
+
cssVariables?: Record<string, CSSValue | EmitterLike<CSSValue>>;
|
|
44
|
+
init?: (entity: DomEntity<HTMLElementTagNameMap[Tag]>) => void;
|
|
42
45
|
} & (Tag extends TagWithValue ? {
|
|
43
46
|
value?: string | number;
|
|
44
47
|
} : {}) & (Tag extends ContentlessTag ? {} : {
|
|
45
|
-
content?: DOMContent |
|
|
48
|
+
content?: DOMContent | EmitterLike<DOMContent>;
|
|
46
49
|
}) & (Tag extends TagWithSrc ? {
|
|
47
50
|
src?: string;
|
|
48
51
|
} : {}) & (Tag extends TagWithHref ? {
|
|
@@ -57,24 +60,24 @@ export type ElementDescriptor<Tag extends string> = {
|
|
|
57
60
|
} : {});
|
|
58
61
|
type ElementAPI<T extends HTMLElement> = {
|
|
59
62
|
readonly element: T;
|
|
60
|
-
readonly classes:
|
|
63
|
+
readonly classes: ClassAccessor;
|
|
61
64
|
readonly attribs: {
|
|
62
65
|
[key: string]: string | null;
|
|
63
66
|
};
|
|
64
67
|
readonly events: EventsAccessor;
|
|
65
68
|
readonly style: StyleAccessor;
|
|
66
|
-
setCSSVariable(variableName: string, value: CSSValue |
|
|
67
|
-
setCSSVariable(table: Record<string, CSSValue |
|
|
69
|
+
setCSSVariable(variableName: string, value: CSSValue | EmitterLike<CSSValue>): void;
|
|
70
|
+
setCSSVariable(table: Record<string, CSSValue | EmitterLike<CSSValue>>): void;
|
|
68
71
|
qsa(selector: string): (Element | DomEntity<HTMLElement>)[];
|
|
69
72
|
remove(): void;
|
|
70
73
|
getRect(): DOMRect;
|
|
71
74
|
focus(): void;
|
|
72
75
|
blur(): void;
|
|
73
|
-
on<E extends keyof HTMLElementEventMap>(eventId: E, handler: (this: ElementAPI<T>, data: HTMLElementEventMap[E]) => void):
|
|
76
|
+
on<E extends keyof HTMLElementEventMap>(eventId: E, handler: (this: ElementAPI<T>, data: HTMLElementEventMap[E]) => void): UnsubscribeFunc;
|
|
74
77
|
} & (T extends ContentlessElement ? {} : {
|
|
75
78
|
append(...content: DOMContent[]): void;
|
|
76
79
|
innerHTML: string;
|
|
77
|
-
content: DOMContent |
|
|
80
|
+
content: DOMContent | EmitterLike<DOMContent>;
|
|
78
81
|
}) & (T extends HTMLElementTagNameMap[TagWithValue] ? {
|
|
79
82
|
value: string;
|
|
80
83
|
select(): void;
|
|
@@ -98,26 +101,26 @@ export type DomHelper = ((
|
|
|
98
101
|
/**
|
|
99
102
|
* Creates an element of the specified tag
|
|
100
103
|
*/
|
|
101
|
-
<T extends
|
|
104
|
+
<T extends HTMLTag>(tagName: T, descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>) & (
|
|
102
105
|
/**
|
|
103
106
|
* Creates an element of the specified tag
|
|
104
107
|
*/
|
|
105
|
-
<T extends
|
|
108
|
+
<T extends HTMLTag>(tagName: T) => DomEntity<HTMLElementTagNameMap[T]>) & (
|
|
106
109
|
/**
|
|
107
110
|
* Creates an element with ID and classes as specified by a selector-like string
|
|
108
111
|
*/
|
|
109
|
-
<T extends
|
|
112
|
+
<T extends HTMLTag>(selector: `${T}#${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
|
|
110
113
|
/**
|
|
111
114
|
* Creates an element with ID and classes as specified by a selector-like string
|
|
112
115
|
*/
|
|
113
|
-
<T extends
|
|
116
|
+
<T extends HTMLTag>(selector: `${T}.${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
|
|
114
117
|
/**
|
|
115
118
|
* Wraps an existing element as a DomEntity
|
|
116
119
|
*/
|
|
117
120
|
<T extends HTMLElement>(element: T) => DomEntity<T>) & {
|
|
118
|
-
[T in
|
|
121
|
+
[T in HTMLTag]: (descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>;
|
|
119
122
|
} & {
|
|
120
|
-
[T in
|
|
123
|
+
[T in HTMLTag]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]>;
|
|
121
124
|
});
|
|
122
125
|
type JelEntityData = {
|
|
123
126
|
dom: DOMContent;
|
package/internal/util.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DOMContent, ElementDescriptor, JelEntity } from "./types";
|
|
1
|
+
import { DOMContent, ElementDescriptor, EmitterLike, HTMLTag, JelEntity } from "./types";
|
|
2
2
|
export declare const entityDataSymbol: unique symbol;
|
|
3
|
-
export declare const isContent: (value: DOMContent | ElementDescriptor<
|
|
3
|
+
export declare const isContent: <T extends HTMLTag>(value: DOMContent | ElementDescriptor<T> | undefined) => value is DOMContent;
|
|
4
4
|
export declare function isJelEntity(content: DOMContent): content is JelEntity<object>;
|
|
5
5
|
/**
|
|
6
6
|
* Wraps an object such that it can be appended as DOM content while retaining its original API
|
|
@@ -9,3 +9,4 @@ export declare function isJelEntity(content: DOMContent): content is JelEntity<o
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function createEntity<API extends object>(content: DOMContent, api: API extends DOMContent ? never : API): JelEntity<API>;
|
|
11
11
|
export declare function createEntity(content: DOMContent): JelEntity<void>;
|
|
12
|
+
export declare function isReactiveSource(value: any): value is EmitterLike<any>;
|
package/internal/util.js
CHANGED
|
@@ -26,3 +26,7 @@ export function createEntity(content, api) {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
;
|
|
29
|
+
export function isReactiveSource(value) {
|
|
30
|
+
return typeof value == "object" && value && (("listen" in value && typeof value.listen == "function")
|
|
31
|
+
|| ("subscribe" in value && typeof value.subscribe == "function"));
|
|
32
|
+
}
|