@xtia/jel 0.6.5 → 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 +15 -3
- package/internal/element.d.ts +17 -1
- package/internal/element.js +66 -4
- package/internal/emitter.d.ts +1 -5
- package/internal/emitter.js +10 -2
- package/internal/types.d.ts +15 -14
- 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/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,11 +1,7 @@
|
|
|
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 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>);
|
package/internal/emitter.js
CHANGED
|
@@ -408,7 +408,15 @@ export function timeoutx(t) {
|
|
|
408
408
|
}
|
|
409
409
|
export function timeout(t) {
|
|
410
410
|
const ms = typeof t === "number" ? t : t.asMilliseconds;
|
|
411
|
-
const
|
|
412
|
-
|
|
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
|
+
});
|
|
413
421
|
return new EventEmitter(listen);
|
|
414
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;
|