@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 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: "virtual-cursor",
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
 
@@ -1,2 +1,18 @@
1
- import { DomHelper } from "./types";
1
+ import { DomHelper, Listenable } from "./types";
2
2
  export declare const $: DomHelper;
3
+ export declare class ClassAccessor {
4
+ private classList;
5
+ private listen;
6
+ private unlisten;
7
+ constructor(classList: DOMTokenList, listen: (className: string, stream: Listenable<boolean>) => void, unlisten: (classNames: string[]) => void);
8
+ add(...className: string[]): void;
9
+ remove(...className: string[]): void;
10
+ toggle(className: string, value?: boolean): boolean;
11
+ toggle(className: string, value: Listenable<boolean>): void;
12
+ contains(className: string): boolean;
13
+ get length(): number;
14
+ toString(): string;
15
+ replace(token: string, newToken: string): void;
16
+ forEach(cb: (token: string, idx: number) => void): void;
17
+ map<R>(cb: (token: string, idx: number) => R): R[];
18
+ }
@@ -34,8 +34,12 @@ function createElement(tag, descriptor = {}) {
34
34
  if (classes === undefined)
35
35
  return;
36
36
  Object.entries(classes).forEach(([className, state]) => {
37
- if (state)
37
+ if (isReactiveSource(state)) {
38
+ ent.classes.toggle(className, state);
39
+ }
40
+ else if (state) {
38
41
  applyClasses(className);
42
+ }
39
43
  });
40
44
  };
41
45
  applyClasses(descriptor.classes || []);
@@ -129,7 +133,8 @@ function observeMutations() {
129
133
  });
130
134
  }
131
135
  function isReactiveSource(value) {
132
- return typeof value == "object" && value && ("listen" in value || "subscribe" in value);
136
+ return typeof value == "object" && value && (("listen" in value && typeof value.listen == "function")
137
+ || ("subscribe" in value && typeof value.subscribe == "function"));
133
138
  }
134
139
  function getWrappedElement(element) {
135
140
  if (!elementWrapCache.has(element)) {
@@ -145,6 +150,7 @@ function getWrappedElement(element) {
145
150
  style: {},
146
151
  cssVariable: {},
147
152
  content: {},
153
+ class: {},
148
154
  };
149
155
  function addListener(type, prop, source) {
150
156
  const set = {
@@ -153,7 +159,8 @@ function getWrappedElement(element) {
153
159
  content: (v) => {
154
160
  element.innerHTML = "";
155
161
  recursiveAppend(element, v);
156
- }
162
+ },
163
+ class: (v) => element.classList.toggle(prop, v),
157
164
  }[type];
158
165
  const subscribe = "subscribe" in source
159
166
  ? () => source.subscribe(set)
@@ -334,10 +341,65 @@ function getWrappedElement(element) {
334
341
  }
335
342
  },
336
343
  style: new Proxy(setStyle, styleProxy),
337
- classes: element.classList,
344
+ classes: new ClassAccessor(element.classList, (className, stream) => addListener("class", className, stream), (classNames) => {
345
+ classNames.forEach(c => {
346
+ if (listeners.class[c])
347
+ removeListener("class", c);
348
+ });
349
+ }),
338
350
  events: new Proxy(element, eventsProxy)
339
351
  };
340
352
  elementWrapCache.set(element, domEntity);
341
353
  }
342
354
  return elementWrapCache.get(element);
343
355
  }
356
+ export class ClassAccessor {
357
+ constructor(classList, listen, unlisten) {
358
+ this.classList = classList;
359
+ this.listen = listen;
360
+ this.unlisten = unlisten;
361
+ }
362
+ add(...className) {
363
+ this.unlisten(className);
364
+ this.classList.add(...className);
365
+ }
366
+ remove(...className) {
367
+ this.unlisten(className);
368
+ this.classList.remove(...className);
369
+ }
370
+ toggle(className, value) {
371
+ this.unlisten([className]);
372
+ if (isReactiveSource(value)) {
373
+ this.listen(className, value);
374
+ return;
375
+ }
376
+ return this.classList.toggle(className, value);
377
+ }
378
+ contains(className) {
379
+ return this.classList.contains(className);
380
+ }
381
+ get length() {
382
+ return this.classList.length;
383
+ }
384
+ toString() {
385
+ return this.classList.toString();
386
+ }
387
+ replace(token, newToken) {
388
+ this.unlisten([token, newToken]);
389
+ this.classList.replace(token, newToken);
390
+ }
391
+ forEach(cb) {
392
+ this.classList.forEach(cb);
393
+ }
394
+ map(cb) {
395
+ const result = [];
396
+ const entries = this.classList.entries();
397
+ let entry = entries.next();
398
+ while (!entry.done) {
399
+ const [idx, value] = entry.value;
400
+ result.push(cb(value, idx));
401
+ entry = entries.next();
402
+ }
403
+ return result;
404
+ }
405
+ }
@@ -1,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>);
@@ -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 { emit, listen } = createListenable();
412
- setTimeout(emit, ms);
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
  }
@@ -1,13 +1,14 @@
1
+ import { type ClassAccessor } from "./element";
1
2
  import { EventEmitter, UnsubscribeFunc } from "./emitter";
2
3
  import { entityDataSymbol } from "./util";
3
- export type ElementClassDescriptor = string | Record<string, boolean | undefined> | undefined | ElementClassDescriptor[];
4
+ export type ElementClassDescriptor = string | Record<string, boolean | Listenable<boolean> | undefined> | undefined | ElementClassDescriptor[];
4
5
  export type DOMContent = number | null | string | Element | JelEntity<object> | Text | DOMContent[];
5
6
  export type DomEntity<T extends HTMLElement> = JelEntity<ElementAPI<T>>;
6
- export type ReactiveSource<T> = ({
7
- listen: (handler: (value: T) => void) => UnsubscribeFunc;
7
+ export type Listenable<T> = {
8
+ subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
8
9
  } | {
9
- subscribe: (handler: (value: T) => void) => UnsubscribeFunc;
10
- });
10
+ listen: (callback: (value: T) => void) => UnsubscribeFunc;
11
+ };
11
12
  export type CSSValue = string | number | null | HexCodeContainer;
12
13
  export type CSSProperty = keyof StylesDescriptor;
13
14
  type HexCodeContainer = {
@@ -18,10 +19,10 @@ export type StylesDescriptor = {
18
19
  [K in keyof CSSStyleDeclaration as [
19
20
  K,
20
21
  CSSStyleDeclaration[K]
21
- ] extends [string, string] ? K : never]+?: CSSValue | ReactiveSource<CSSValue>;
22
+ ] extends [string, string] ? K : never]+?: CSSValue | Listenable<CSSValue>;
22
23
  };
23
- export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | ReactiveSource<CSSValue>) => void);
24
- export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | ReactiveSource<CSSValue>);
24
+ export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | Listenable<CSSValue>) => void);
25
+ export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | Listenable<CSSValue>);
25
26
  export type StyleAccessor = ((styles: StylesDescriptor) => void) & StylesDescriptor & SetStyleFunc;
26
27
  type ContentlessTag = "area" | "br" | "hr" | "iframe" | "input" | "textarea" | "img" | "canvas" | "link" | "meta" | "source" | "embed" | "track" | "base";
27
28
  type TagWithHref = "a" | "link" | "base";
@@ -38,11 +39,11 @@ export type ElementDescriptor<Tag extends string> = {
38
39
  [E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
39
40
  };
40
41
  style?: StylesDescriptor;
41
- cssVariables?: Record<string, CSSValue | ReactiveSource<CSSValue>>;
42
+ cssVariables?: Record<string, CSSValue | Listenable<CSSValue>>;
42
43
  } & (Tag extends TagWithValue ? {
43
44
  value?: string | number;
44
45
  } : {}) & (Tag extends ContentlessTag ? {} : {
45
- content?: DOMContent | ReactiveSource<DOMContent>;
46
+ content?: DOMContent | Listenable<DOMContent>;
46
47
  }) & (Tag extends TagWithSrc ? {
47
48
  src?: string;
48
49
  } : {}) & (Tag extends TagWithHref ? {
@@ -57,14 +58,14 @@ export type ElementDescriptor<Tag extends string> = {
57
58
  } : {});
58
59
  type ElementAPI<T extends HTMLElement> = {
59
60
  readonly element: T;
60
- readonly classes: DOMTokenList;
61
+ readonly classes: ClassAccessor;
61
62
  readonly attribs: {
62
63
  [key: string]: string | null;
63
64
  };
64
65
  readonly events: EventsAccessor;
65
66
  readonly style: StyleAccessor;
66
- setCSSVariable(variableName: string, value: CSSValue | ReactiveSource<CSSValue>): void;
67
- setCSSVariable(table: Record<string, CSSValue | ReactiveSource<CSSValue>>): void;
67
+ setCSSVariable(variableName: string, value: CSSValue | Listenable<CSSValue>): void;
68
+ setCSSVariable(table: Record<string, CSSValue | Listenable<CSSValue>>): void;
68
69
  qsa(selector: string): (Element | DomEntity<HTMLElement>)[];
69
70
  remove(): void;
70
71
  getRect(): DOMRect;
@@ -74,7 +75,7 @@ type ElementAPI<T extends HTMLElement> = {
74
75
  } & (T extends ContentlessElement ? {} : {
75
76
  append(...content: DOMContent[]): void;
76
77
  innerHTML: string;
77
- content: DOMContent | ReactiveSource<DOMContent>;
78
+ content: DOMContent | Listenable<DOMContent>;
78
79
  }) & (T extends HTMLElementTagNameMap[TagWithValue] ? {
79
80
  value: string;
80
81
  select(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/jel",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/jel-ts",
6
6
  "type": "github"