@xtia/jel 0.5.1 → 0.6.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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { DomEntity, ElementClassDescriptor, ElementDescriptor, DOMContent, DomHelper, StyleAccessor, JelEntity } from "./internal/types";
2
2
  export { $ } from "./internal/element";
3
3
  export { createEntity } from "./internal/util";
4
- export { createEventSource } from "./internal/emitter";
4
+ export { createEventSource, interval } from "./internal/emitter";
package/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { $ } from "./internal/element";
2
2
  export { createEntity } from "./internal/util";
3
- export { createEventSource } from "./internal/emitter";
3
+ export { createEventSource, interval } from "./internal/emitter";
@@ -97,6 +97,30 @@ export const $ = new Proxy(createElement, {
97
97
  };
98
98
  }
99
99
  });
100
+ const elementMutationMap = new WeakMap();
101
+ let mutationObserver = null;
102
+ function observeMutations() {
103
+ if (mutationObserver !== null)
104
+ return;
105
+ mutationObserver = new MutationObserver((mutations) => {
106
+ mutations.forEach(mut => {
107
+ mut.addedNodes.forEach(node => {
108
+ if (elementMutationMap.has(node)) {
109
+ elementMutationMap.get(node).add();
110
+ }
111
+ });
112
+ mut.removedNodes.forEach(node => {
113
+ if (elementMutationMap.has(node)) {
114
+ elementMutationMap.get(node).remove();
115
+ }
116
+ });
117
+ });
118
+ });
119
+ mutationObserver.observe(document.body, {
120
+ childList: true,
121
+ subtree: true
122
+ });
123
+ }
100
124
  function getWrappedElement(element) {
101
125
  if (!elementWrapCache.has(element)) {
102
126
  const setCSSVariable = (k, v) => {
@@ -107,6 +131,57 @@ function getWrappedElement(element) {
107
131
  element.style.setProperty("--" + k, v);
108
132
  }
109
133
  };
134
+ const styleListeners = {};
135
+ function addStyleListener(prop, source) {
136
+ const subscribe = "subscribe" in source
137
+ ? () => source.subscribe(v => element.style[prop] = v)
138
+ : () => source.listen(v => element.style[prop] = v);
139
+ styleListeners[prop] = {
140
+ subscribe,
141
+ unsubscribe: element.isConnected ? subscribe() : null,
142
+ };
143
+ if (!elementMutationMap.has(element)) {
144
+ elementMutationMap.set(element, {
145
+ add: () => {
146
+ Object.values(styleListeners).forEach(l => { var _a; return l.unsubscribe = (_a = l.subscribe) === null || _a === void 0 ? void 0 : _a.call(l); });
147
+ },
148
+ remove: () => {
149
+ Object.values(styleListeners).forEach(l => {
150
+ var _a;
151
+ (_a = l.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(l);
152
+ l.unsubscribe = null;
153
+ });
154
+ }
155
+ });
156
+ }
157
+ observeMutations();
158
+ }
159
+ function removeStyleListener(prop) {
160
+ if (styleListeners[prop].unsubscribe) {
161
+ styleListeners[prop].unsubscribe();
162
+ }
163
+ delete styleListeners[prop];
164
+ if (Object.keys(styleListeners).length == 0) {
165
+ elementMutationMap.delete(element);
166
+ }
167
+ }
168
+ function setStyle(prop, value) {
169
+ if (styleListeners[prop])
170
+ removeStyleListener(prop);
171
+ if (typeof value == "object" && value) {
172
+ if ("listen" in value || "subscribe" in value) {
173
+ addStyleListener(prop, value);
174
+ return;
175
+ }
176
+ value = value.toString();
177
+ }
178
+ if (value === undefined) {
179
+ return prop in styleListeners
180
+ ? styleListeners[prop].subscribe
181
+ : element.style[prop];
182
+ }
183
+ element.style[prop] = value;
184
+ }
110
185
  const domEntity = {
111
186
  [entityDataSymbol]: {
112
187
  dom: element,
@@ -210,7 +285,7 @@ function getWrappedElement(element) {
210
285
  element.setAttribute("name", v);
211
286
  }
212
287
  },
213
- style: new Proxy(() => element.style, styleProxy),
288
+ style: new Proxy(setStyle, styleProxy),
214
289
  classes: element.classList,
215
290
  events: new Proxy(element, eventsProxy)
216
291
  };
@@ -73,13 +73,72 @@ export declare class EventEmitter<T> extends Emitter<T> {
73
73
  debounce(ms: number): EventEmitter<T>;
74
74
  throttle(ms: number): EventEmitter<T>;
75
75
  batch(ms: number): Emitter<T[]>;
76
- filter(check: (value: T) => boolean): EventEmitter<T>;
76
+ /**
77
+ * **Experimental**: May change in future revisions
78
+ * Note: potential leak - This link will remain subscribed to the parent
79
+ * until it emits, regardless of subscriptions to this link.
80
+ * @param notifier
81
+ * @returns
82
+ */
77
83
  once(): EventEmitter<T>;
78
84
  scan<S>(updater: (state: S, value: T) => S, initial: S): EventEmitter<S>;
79
85
  buffer(count: number): EventEmitter<T[]>;
86
+ /**
87
+ * **Experimental**: May change in future revisions
88
+ * Note: potential leak - This link will remain subscribed to the parent
89
+ * until emission limit is reached, regardless of subscriptions to this link.
90
+ * @param notifier
91
+ * @returns
92
+ */
80
93
  take(limit: number): EventEmitter<T>;
81
- tap(cb: Handler<T>): EventEmitter<T>;
94
+ /**
95
+ * **Experimental**: May change in future revisions
96
+ * Note: potential leak - This link will remain subscribed to the notifier
97
+ * until it emits, regardless of subscriptions to this link.
98
+ * @param notifier
99
+ * @returns
100
+ */
82
101
  takeUntil(notifier: Emitter<any>): Emitter<T>;
102
+ /**
103
+ * Creates a chainable emitter that immediately emits a value to every new subscriber,
104
+ * then forwards parent emissions
105
+ * @param value
106
+ * @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
107
+ */
108
+ 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
+ /**
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.
140
+ */
141
+ tap(cb: Handler<T>): EventEmitter<T>;
83
142
  }
84
143
  /**
85
144
  * Creates a linked Emitter and emit() pair
@@ -120,4 +179,7 @@ export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLas
120
179
  listen: (fn: (v: T) => void) => UnsubscribeFunc;
121
180
  emit: (value: T) => void;
122
181
  };
182
+ export declare function interval(t: number | {
183
+ asMilliseconds: number;
184
+ }): EventEmitter<number>;
123
185
  export {};
@@ -152,10 +152,13 @@ export class EventEmitter extends Emitter {
152
152
  });
153
153
  return new EventEmitter(listen);
154
154
  }
155
- filter(check) {
156
- const listen = this.transform((value, emit) => check(value) && emit(value));
157
- return new EventEmitter(listen);
158
- }
155
+ /**
156
+ * **Experimental**: May change in future revisions
157
+ * Note: potential leak - This link will remain subscribed to the parent
158
+ * until it emits, regardless of subscriptions to this link.
159
+ * @param notifier
160
+ * @returns
161
+ */
159
162
  once() {
160
163
  const { emit, listen } = createListenable();
161
164
  const unsub = this.apply(v => {
@@ -183,6 +186,13 @@ export class EventEmitter extends Emitter {
183
186
  });
184
187
  return new EventEmitter(listen);
185
188
  }
189
+ /**
190
+ * **Experimental**: May change in future revisions
191
+ * Note: potential leak - This link will remain subscribed to the parent
192
+ * until emission limit is reached, regardless of subscriptions to this link.
193
+ * @param notifier
194
+ * @returns
195
+ */
186
196
  take(limit) {
187
197
  const { emit, listen } = createListenable();
188
198
  let count = 0;
@@ -197,13 +207,13 @@ export class EventEmitter extends Emitter {
197
207
  });
198
208
  return new EventEmitter(listen);
199
209
  }
200
- tap(cb) {
201
- const listen = this.transform((value, emit) => {
202
- cb(value);
203
- emit(value);
204
- });
205
- return new EventEmitter(listen);
206
- }
210
+ /**
211
+ * **Experimental**: May change in future revisions
212
+ * Note: potential leak - This link will remain subscribed to the notifier
213
+ * until it emits, regardless of subscriptions to this link.
214
+ * @param notifier
215
+ * @returns
216
+ */
207
217
  takeUntil(notifier) {
208
218
  const { emit, listen } = createListenable();
209
219
  const unsub = this.apply(v => {
@@ -213,7 +223,91 @@ export class EventEmitter extends Emitter {
213
223
  unsub();
214
224
  unsubNotifier();
215
225
  });
216
- return new Emitter(listen);
226
+ return new EventEmitter(listen);
227
+ }
228
+ /**
229
+ * Creates a chainable emitter that immediately emits a value to every new subscriber,
230
+ * then forwards parent emissions
231
+ * @param value
232
+ * @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
233
+ */
234
+ immediate(value) {
235
+ return new EventEmitter(handle => {
236
+ handle(value);
237
+ return this.onListen(handle);
238
+ });
239
+ }
240
+ cached() {
241
+ let cache = null;
242
+ let unsub = null;
243
+ const { listen, emit } = createListenable(() => {
244
+ unsub = this.onListen((value => {
245
+ cache = { value };
246
+ emit(value);
247
+ }));
248
+ }, () => {
249
+ unsub();
250
+ });
251
+ return new EventEmitter(handler => {
252
+ if (cache)
253
+ handler(cache.value);
254
+ return listen(handler);
255
+ });
256
+ }
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);
217
311
  }
218
312
  }
219
313
  /**
@@ -277,3 +371,13 @@ export function createListenable(onAddFirst, onRemoveLast) {
277
371
  emit: (value) => handlers.forEach(h => h.fn(value)),
278
372
  };
279
373
  }
374
+ export function interval(t) {
375
+ let intervalId = null;
376
+ let idx = 0;
377
+ const { emit, listen } = createListenable(() => {
378
+ intervalId = setInterval(() => {
379
+ emit(idx++);
380
+ }, typeof t == "number" ? t : t.asMilliseconds);
381
+ }, () => clearInterval(intervalId));
382
+ return new EventEmitter(listen);
383
+ }
@@ -1,3 +1,4 @@
1
- export declare const styleProxy: ProxyHandler<() => CSSStyleDeclaration>;
1
+ import { SetGetStyleFunc } from "./types";
2
+ export declare const styleProxy: ProxyHandler<SetGetStyleFunc>;
2
3
  export declare const attribsProxy: ProxyHandler<HTMLElement>;
3
4
  export declare const eventsProxy: ProxyHandler<HTMLElement>;
package/internal/proxy.js CHANGED
@@ -1,22 +1,21 @@
1
1
  import { EventEmitter } from "./emitter";
2
2
  export const styleProxy = {
3
- get(getStyle, prop) {
4
- return getStyle()[prop];
3
+ get(style, prop) {
4
+ return style(prop);
5
5
  },
6
- set(getStyle, prop, value) {
7
- getStyle()[prop] = value;
6
+ set(style, prop, value) {
7
+ style(prop, value);
8
8
  return true;
9
9
  },
10
- apply(getStyle, _, [stylesOrProp, value]) {
11
- const style = getStyle();
10
+ apply(style, _, [stylesOrProp, value]) {
12
11
  if (typeof stylesOrProp == "object") {
13
- Object.entries(stylesOrProp).forEach(([prop, val]) => style[prop] = val);
12
+ Object.entries(stylesOrProp).forEach(([prop, val]) => style(prop, val));
14
13
  return;
15
14
  }
16
- style[stylesOrProp] = value;
15
+ style(stylesOrProp, value);
17
16
  },
18
- deleteProperty(getStyle, prop) {
19
- getStyle()[prop] = null;
17
+ deleteProperty(style, prop) {
18
+ style(prop, null);
20
19
  return true;
21
20
  }
22
21
  };
@@ -1,9 +1,15 @@
1
- import { EventEmitter } from "./emitter";
1
+ import { EventEmitter, UnsubscribeFunc } from "./emitter";
2
2
  import { entityDataSymbol } from "./util";
3
3
  export type ElementClassDescriptor = string | Record<string, boolean | undefined> | undefined | ElementClassDescriptor[];
4
4
  export type DOMContent = number | null | string | Element | JelEntity<object> | Text | DOMContent[];
5
5
  export type DomEntity<T extends HTMLElement> = JelEntity<ElementAPI<T>>;
6
- type CSSValue = string | number | null | HexCodeContainer;
6
+ export type ReactiveSource<T> = ({
7
+ listen: (handler: (value: T) => void) => UnsubscribeFunc;
8
+ } | {
9
+ subscribe: (handler: (value: T) => void) => UnsubscribeFunc;
10
+ });
11
+ export type CSSValue = string | number | null | HexCodeContainer;
12
+ export type CSSProperty = keyof StylesDescriptor;
7
13
  type HexCodeContainer = {
8
14
  hexCode: string;
9
15
  toString(): string;
@@ -12,9 +18,11 @@ export type StylesDescriptor = {
12
18
  [K in keyof CSSStyleDeclaration as [
13
19
  K,
14
20
  CSSStyleDeclaration[K]
15
- ] extends [string, string] ? K : never]+?: CSSValue;
21
+ ] extends [string, string] ? K : never]+?: CSSValue | ReactiveSource<CSSValue>;
16
22
  };
17
- export type StyleAccessor = StylesDescriptor & ((styles: StylesDescriptor) => void) & ((property: keyof StylesDescriptor, value: CSSValue) => void);
23
+ export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | ReactiveSource<CSSValue>) => void);
24
+ export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | ReactiveSource<CSSValue>);
25
+ export type StyleAccessor = ((styles: StylesDescriptor) => void) & StylesDescriptor & SetStyleFunc;
18
26
  type ContentlessTag = "area" | "br" | "hr" | "iframe" | "input" | "textarea" | "img" | "canvas" | "link" | "meta" | "source" | "embed" | "track" | "base";
19
27
  type TagWithHref = "a" | "link" | "base";
20
28
  type TagWithSrc = "img" | "script" | "iframe" | "video" | "audio" | "embed" | "source" | "track";
@@ -34,7 +42,7 @@ export type ElementDescriptor<Tag extends string> = {
34
42
  } & (Tag extends TagWithValue ? {
35
43
  value?: string | number;
36
44
  } : {}) & (Tag extends ContentlessTag ? {} : {
37
- content?: DOMContent;
45
+ content?: DOMContent | ReactiveSource<DOMContent>;
38
46
  }) & (Tag extends TagWithSrc ? {
39
47
  src?: string;
40
48
  } : {}) & (Tag extends TagWithHref ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/jel",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/jel-ts",
6
6
  "type": "github"