@ixfx/ui 0.36.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/dist/__tests__/test.d.ts +2 -0
- package/dist/__tests__/test.d.ts.map +1 -0
- package/dist/__tests__/test.js +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/rx/browser-resize.d.ts +21 -0
- package/dist/src/rx/browser-resize.d.ts.map +1 -0
- package/dist/src/rx/browser-resize.js +40 -0
- package/dist/src/rx/browser-theme-change.d.ts +13 -0
- package/dist/src/rx/browser-theme-change.d.ts.map +1 -0
- package/dist/src/rx/browser-theme-change.js +28 -0
- package/dist/src/rx/colour.d.ts +8 -0
- package/dist/src/rx/colour.d.ts.map +1 -0
- package/dist/src/rx/colour.js +20 -0
- package/dist/src/rx/dom-source.d.ts +96 -0
- package/dist/src/rx/dom-source.d.ts.map +1 -0
- package/dist/src/rx/dom-source.js +373 -0
- package/dist/src/rx/dom-types.d.ts +128 -0
- package/dist/src/rx/dom-types.d.ts.map +1 -0
- package/dist/src/rx/dom-types.js +1 -0
- package/dist/src/rx/dom.d.ts +284 -0
- package/dist/src/rx/dom.d.ts.map +1 -0
- package/dist/src/rx/dom.js +727 -0
- package/dist/src/rx/index.d.ts +7 -0
- package/dist/src/rx/index.d.ts.map +1 -0
- package/dist/src/rx/index.js +5 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +32 -0
@@ -0,0 +1,727 @@
|
|
1
|
+
//import * as Immutable from "@ixfx/core/records";
|
2
|
+
import { resolveEl } from "@ixfx/dom";
|
3
|
+
import { getPathsAndData } from "@ixfx/core/records";
|
4
|
+
import * as Rx from "@ixfx/rx";
|
5
|
+
import * as RxFrom from "@ixfx/rx/from";
|
6
|
+
import { getFromKeys } from "@ixfx/core/maps";
|
7
|
+
import { afterMatch, beforeMatch, stringSegmentsWholeToEnd, stringSegmentsWholeToFirst } from "@ixfx/core/text";
|
8
|
+
import { QueueMutable } from "@ixfx/collections";
|
9
|
+
/**
|
10
|
+
* Reactive stream of array of elements that match `query`.
|
11
|
+
* @param query
|
12
|
+
* @returns
|
13
|
+
*/
|
14
|
+
export function fromDomQuery(query) {
|
15
|
+
const elements = [...document.querySelectorAll(query)];
|
16
|
+
return Rx.From.object(elements);
|
17
|
+
/// TODO: MutationObserver to update element list
|
18
|
+
}
|
19
|
+
/**
|
20
|
+
* Updates an element's `textContent` when the source value changes.
|
21
|
+
* ```js
|
22
|
+
* bindText(source, `#blah`);
|
23
|
+
* ```
|
24
|
+
* @param elOrQuery
|
25
|
+
* @param source
|
26
|
+
* @param bindOpts
|
27
|
+
*/
|
28
|
+
export const bindText = (source, elOrQuery, bindOpts = {}) => {
|
29
|
+
return bindElement(source, elOrQuery, { ...bindOpts, elField: `textContent` });
|
30
|
+
};
|
31
|
+
/**
|
32
|
+
* Updates an element's `value` (as well as the 'value' attribute) when the source value changes.s
|
33
|
+
* @param source
|
34
|
+
* @param elOrQuery
|
35
|
+
* @param bindOpts
|
36
|
+
* @returns
|
37
|
+
*/
|
38
|
+
export const bindValueText = (source, elOrQuery, bindOpts = {}) => {
|
39
|
+
return bindElement(source, elOrQuery, { ...bindOpts, elField: `value`, attribName: `value` });
|
40
|
+
};
|
41
|
+
/**
|
42
|
+
* Updates an element's `valueAsNumber` (as well as the 'value' attribute) when the source value changes.
|
43
|
+
* ```js
|
44
|
+
* // Create a reactive number, with a default value of 10
|
45
|
+
* const r1 = Rx.From.number(10);
|
46
|
+
* // Bind reactive to HTML input element with id 'inputRange'
|
47
|
+
* const b1 = Rx.Dom.bindValueRange(r1,`#inputRange`);
|
48
|
+
*
|
49
|
+
* // Demo: Change the reactive value every second
|
50
|
+
* // ...changing the reactive in turn updates the HTML
|
51
|
+
* setInterval(() => {
|
52
|
+
* r1.set(Math.floor(Math.random()*100));
|
53
|
+
* }, 1000);
|
54
|
+
* ```
|
55
|
+
* @param source
|
56
|
+
* @param elOrQuery
|
57
|
+
* @param bindOpts
|
58
|
+
* @returns
|
59
|
+
*/
|
60
|
+
// export const bindValueRange = (source: Rx.Reactive<number>, elOrQuery: string | HTMLInputElement | null, bindOpts: Partial<Rx.DomBindInputOptions<number, number>> = {}) => {
|
61
|
+
// const el = validateElement(elOrQuery, `range`);
|
62
|
+
// const b = bindElement<number, number>(source, el, { ...bindOpts, elField: `valueAsNumber`, attribName: `value` });
|
63
|
+
// const twoway = bindOpts.twoway ?? false;
|
64
|
+
// const transformFromInput = bindOpts.transformFromInput ?? ((value) => {
|
65
|
+
// if (typeof value === `number`) return value;
|
66
|
+
// return Number.parseFloat(value);
|
67
|
+
// });
|
68
|
+
// const input = Rx.From.domValueAsNumber(el);
|
69
|
+
// return setupInput(b, input, source, twoway, transformFromInput);
|
70
|
+
// }
|
71
|
+
// export const bindValueColour = (source: Rx.Reactive<Colour.Colourish>, elOrQuery: string | HTMLInputElement | null, bindOpts: Partial<Rx.DomBindInputOptions<Colour.Colourish, string>> = {}) => {
|
72
|
+
// const el = validateElement(elOrQuery, `color`);
|
73
|
+
// const b = bindElement<Colour.Colourish, string>(source, el, {
|
74
|
+
// ...bindOpts,
|
75
|
+
// elField: `value`,
|
76
|
+
// attribName: `value`,
|
77
|
+
// transform(input) {
|
78
|
+
// console.log(`transform from: ${ JSON.stringify(input) } to hex`);
|
79
|
+
// const c = Colour.resolve(input);
|
80
|
+
// return c.to(`srgb`).toString({ format: `hex`, collapse: false });
|
81
|
+
// },
|
82
|
+
// });
|
83
|
+
// const twoway = bindOpts.twoway ?? false;
|
84
|
+
// const transformFromInput = bindOpts.transformFromInput ?? ((value) => {
|
85
|
+
// const x = Colour.toHsl(value);
|
86
|
+
// console.log(`transformFromInput: ${ value } x: ${ JSON.stringify(x) }`);
|
87
|
+
// return x;
|
88
|
+
// });
|
89
|
+
// const input = Rx.From.domValue<Colour.Hsl>(el, {
|
90
|
+
// domToValue: transformFromInput
|
91
|
+
// });
|
92
|
+
// return setupInput(b, input, source, twoway, transformFromInput);
|
93
|
+
// }
|
94
|
+
const setupInput = (b, input, source, twoway, transformFromInput) => {
|
95
|
+
input.onValue(value => {
|
96
|
+
const v = transformFromInput(value);
|
97
|
+
if (twoway && Rx.isWritable(source)) {
|
98
|
+
source.set(v);
|
99
|
+
}
|
100
|
+
});
|
101
|
+
const dispose = () => {
|
102
|
+
input.dispose(`bindInput twoway dispose`);
|
103
|
+
b.remove(false);
|
104
|
+
};
|
105
|
+
return { ...b, dispose, input };
|
106
|
+
};
|
107
|
+
const validateElement = (elOrQuery, type) => {
|
108
|
+
const el = resolveEl(elOrQuery);
|
109
|
+
if (el.nodeName !== `INPUT`)
|
110
|
+
throw new Error(`HTML INPUT element expected. Got: ${el.nodeName}`);
|
111
|
+
if (type !== undefined && el.type !== type)
|
112
|
+
throw new Error(`HTML INPUT element expected with type 'range'. Got: ${el.type}`);
|
113
|
+
return el;
|
114
|
+
};
|
115
|
+
/**
|
116
|
+
* Updates an element's `innerHTML` when the source value changes
|
117
|
+
* ```js
|
118
|
+
* bindHtml(source, `#blah`);
|
119
|
+
* ```
|
120
|
+
*
|
121
|
+
* Uses {@link bindElement}, with `{elField:'innerHTML'}` as the options.
|
122
|
+
* @param elOrQuery
|
123
|
+
* @param source
|
124
|
+
* @param bindOpts
|
125
|
+
* @returns
|
126
|
+
*/
|
127
|
+
export const bindHtml = (source, elOrQuery, bindOpts = {}) => {
|
128
|
+
return bindElement(source, elOrQuery, { ...bindOpts, elField: `innerHTML` });
|
129
|
+
};
|
130
|
+
/**
|
131
|
+
* Shortcut to bind to an elements attribute
|
132
|
+
* @param elOrQuery
|
133
|
+
* @param source
|
134
|
+
* @param attribute
|
135
|
+
* @param bindOpts
|
136
|
+
* @returns
|
137
|
+
*/
|
138
|
+
// export const bindAttribute = <V>(elOrQuery: string | HTMLElement, source: Rx.Reactive<V>, attribute: string, bindOpts: Partial<DomBindOptions<V>> = {}) => {
|
139
|
+
// return bind(elOrQuery, source, { ...bindOpts, attribName: attribute });
|
140
|
+
// }
|
141
|
+
/**
|
142
|
+
* Shortcut to bind to a CSS variable
|
143
|
+
* @param elOrQuery
|
144
|
+
* @param source
|
145
|
+
* @param cssVariable
|
146
|
+
* @param bindOpts
|
147
|
+
* @returns
|
148
|
+
*/
|
149
|
+
// export const bindCssVariable = <V>(elOrQuery: string | HTMLElement, source: Rx.Reactive<V>, cssVariable: string, bindOpts: Partial<DomBindOptions<V>> = {}) => {
|
150
|
+
// return bind(elOrQuery, source, { ...bindOpts, cssVariable: cssVariable });
|
151
|
+
// }
|
152
|
+
/**
|
153
|
+
* Creates a new HTML element, calling {@link bind} on it to update when `source` emits new values.
|
154
|
+
*
|
155
|
+
*
|
156
|
+
* ```js
|
157
|
+
* // Set textContent of a SPAN with values from `source`
|
158
|
+
* create(source, { tagName: `span`, parentEl: document.body })
|
159
|
+
* ```
|
160
|
+
*
|
161
|
+
* If `parentEl` is not given in the options, the created element needs to be manually added
|
162
|
+
* ```js
|
163
|
+
* const b = create(source);
|
164
|
+
* someEl.append(b.el); // Append manually
|
165
|
+
* ```
|
166
|
+
*
|
167
|
+
* ```
|
168
|
+
* // Set 'title' attribute based on values from `source`
|
169
|
+
* create(source, { parentEl: document.body, attribName: `title` })
|
170
|
+
* ```
|
171
|
+
* @param source
|
172
|
+
* @param options
|
173
|
+
* @returns
|
174
|
+
*/
|
175
|
+
// export const create = <V>(source: Rx.Reactive<V>, options: Partial<DomCreateOptions> & Partial<DomBindOptions<V>> = {}): PipeDomBinding => {
|
176
|
+
// const nodeType = options.tagName ?? `DIV`;
|
177
|
+
// const el = document.createElement(nodeType);
|
178
|
+
// const b = bind(el, source, options);
|
179
|
+
// if (options.parentEl) {
|
180
|
+
// const parentElementOrQuery = resolveEl(options.parentEl);
|
181
|
+
// if (parentElementOrQuery === undefined) throw new Error(`Parent element could not be resolved`);
|
182
|
+
// parentElementOrQuery.append(el);
|
183
|
+
// }
|
184
|
+
// return b;
|
185
|
+
// }
|
186
|
+
/**
|
187
|
+
* Update a DOM element's field, attribute or CSS variable when `source` produces a value.
|
188
|
+
*
|
189
|
+
* ```js
|
190
|
+
* // Access via DOM query. Binds to 'textContent' by default
|
191
|
+
* bind(readableSource, `#someEl`);
|
192
|
+
*
|
193
|
+
* // Set innerHTML instead
|
194
|
+
* bind(readableSource, someEl, { elField: `innerHTML` });
|
195
|
+
*
|
196
|
+
* // An attribute
|
197
|
+
* bind(readableSource, someEl, { attribName: `width` });
|
198
|
+
*
|
199
|
+
* // A css variable ('--' optiona)
|
200
|
+
* bind(readableSource, someEl, { cssVariable: `hue` });
|
201
|
+
*
|
202
|
+
* // Pluck a particular field from source data.
|
203
|
+
* // Ie someEl.textContent = value.colour
|
204
|
+
* bind(readableSource, someEl, { sourceField: `colour` });
|
205
|
+
*
|
206
|
+
* // Transform value before setting it to field
|
207
|
+
* bind(readableSource, someEl, {
|
208
|
+
* field: `innerHTML`,
|
209
|
+
* transform: (v) => `Colour: ${v.colour}`
|
210
|
+
* })
|
211
|
+
* ```
|
212
|
+
*
|
213
|
+
* If `source` has an initial value, this is used when first bound.
|
214
|
+
*
|
215
|
+
* Returns {@link PipeDomBinding} to control binding:
|
216
|
+
* ```js
|
217
|
+
* const bind = bind(source, `#someEl`);
|
218
|
+
* bind.remove(); // Unbind
|
219
|
+
* bind.remove(true); // Unbind and remove HTML element
|
220
|
+
* ```
|
221
|
+
*
|
222
|
+
* If several fields need to be updated based on a new value, consider using {@link bindUpdate} instead.
|
223
|
+
* @param elOrQuery Element to update to, or query string such as '#someid'
|
224
|
+
* @param source Source of data
|
225
|
+
* @param binds Bindings
|
226
|
+
*/
|
227
|
+
export const bindElement = (source, elOrQuery, ...binds) => {
|
228
|
+
if (elOrQuery === null)
|
229
|
+
throw new Error(`Param 'elOrQuery' is null`);
|
230
|
+
if (elOrQuery === undefined)
|
231
|
+
throw new Error(`Param 'elOrQuery' is undefined`);
|
232
|
+
const el = resolveEl(elOrQuery);
|
233
|
+
let b = [];
|
234
|
+
if (binds.length === 0) {
|
235
|
+
b.push({ elField: `textContent` });
|
236
|
+
}
|
237
|
+
else {
|
238
|
+
b = [...binds];
|
239
|
+
}
|
240
|
+
const bb = b.map(bind => {
|
241
|
+
if (`element` in bind)
|
242
|
+
return bind;
|
243
|
+
return { ...bind, element: el };
|
244
|
+
});
|
245
|
+
return bind(source, ...bb);
|
246
|
+
};
|
247
|
+
const resolveBindUpdater = (bind, element) => {
|
248
|
+
const b = resolveBindUpdaterBase(bind);
|
249
|
+
return (value) => {
|
250
|
+
b(value, element);
|
251
|
+
};
|
252
|
+
};
|
253
|
+
const resolveBindUpdaterBase = (bind) => {
|
254
|
+
if (bind.elField !== undefined || (bind.cssVariable === undefined && bind.attribName === undefined && bind.cssProperty === undefined && bind.textContent === undefined && bind.htmlContent === undefined)) {
|
255
|
+
const field = bind.elField ?? `textContent`;
|
256
|
+
return (v, element) => {
|
257
|
+
element[field] = v;
|
258
|
+
};
|
259
|
+
}
|
260
|
+
if (bind.attribName !== undefined) {
|
261
|
+
const attrib = bind.attribName;
|
262
|
+
return (v, element) => {
|
263
|
+
element.setAttribute(attrib, v);
|
264
|
+
};
|
265
|
+
}
|
266
|
+
if (bind.textContent) {
|
267
|
+
return (v, element) => {
|
268
|
+
element.textContent = v;
|
269
|
+
};
|
270
|
+
}
|
271
|
+
if (bind.htmlContent) {
|
272
|
+
return (v, element) => {
|
273
|
+
element.innerHTML = v;
|
274
|
+
};
|
275
|
+
}
|
276
|
+
if (bind.cssVariable !== undefined) {
|
277
|
+
let css = bind.cssVariable;
|
278
|
+
if (!css.startsWith(`--`))
|
279
|
+
css = `--` + css;
|
280
|
+
return (v, element) => {
|
281
|
+
element.style.setProperty(css, v);
|
282
|
+
};
|
283
|
+
}
|
284
|
+
if (bind.cssProperty !== undefined) {
|
285
|
+
return (v, element) => {
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
287
|
+
element.style[bind.cssProperty] = v;
|
288
|
+
};
|
289
|
+
}
|
290
|
+
return (_, _element) => {
|
291
|
+
/** no-op */
|
292
|
+
};
|
293
|
+
};
|
294
|
+
const resolveTransform = (bind) => {
|
295
|
+
if (!bind.transform && !bind.transformValue)
|
296
|
+
return;
|
297
|
+
if (bind.transformValue) {
|
298
|
+
if (bind.sourceField === undefined)
|
299
|
+
throw new Error(`Expects 'sourceField' to be set when 'transformValue' is set`);
|
300
|
+
return (value) => {
|
301
|
+
const fieldValue = value[bind.sourceField];
|
302
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
303
|
+
return bind.transformValue(fieldValue);
|
304
|
+
};
|
305
|
+
}
|
306
|
+
else if (bind.transform) {
|
307
|
+
if (bind.sourceField !== undefined)
|
308
|
+
throw new Error(`If 'transform' is set, 'sourceField' is ignored`);
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
310
|
+
return (value) => bind.transform(value);
|
311
|
+
}
|
312
|
+
};
|
313
|
+
/**
|
314
|
+
* Binds `source` to one or more element(s). One or more bindings for the same source
|
315
|
+
* can be provided.
|
316
|
+
*
|
317
|
+
* ```js
|
318
|
+
* bind(source,
|
319
|
+
* // Binds .name field of source values to textContent of #some-element
|
320
|
+
* { query: `#some-element`, sourceField: `name` },
|
321
|
+
* { query: `section`, }
|
322
|
+
* );
|
323
|
+
* ```
|
324
|
+
*
|
325
|
+
* Can update
|
326
|
+
* * CSS variables
|
327
|
+
* * CSS styles
|
328
|
+
* * textContent / innerHTML
|
329
|
+
* * HTML DOM attributes and object fields
|
330
|
+
*
|
331
|
+
* Can use a particular field on source values, or use the whole value. These can
|
332
|
+
* pass through `transformValue` or `transform` respectively.
|
333
|
+
*
|
334
|
+
* Returns a function to unbind from source and optionally remove HTML element
|
335
|
+
* ```js
|
336
|
+
* const unbind = bind( . . . );
|
337
|
+
* unbind(); // Unbind
|
338
|
+
* unbind(true); // Unbind and remove HTML element(s)
|
339
|
+
* ```
|
340
|
+
* @param source
|
341
|
+
* @param bindsUnresolvedElements
|
342
|
+
* @returns
|
343
|
+
*/
|
344
|
+
export const bind = (source, ...bindsUnresolvedElements) => {
|
345
|
+
const binds = bindsUnresolvedElements.map(bind => {
|
346
|
+
if (bind.element && bind.element !== undefined)
|
347
|
+
return bind;
|
348
|
+
if (bind.query)
|
349
|
+
return {
|
350
|
+
...bind,
|
351
|
+
element: resolveEl(bind.query)
|
352
|
+
};
|
353
|
+
throw new Error(`Unable to resolve element. Missing 'element' or 'query' values on bind. ${JSON.stringify(bind)}`);
|
354
|
+
});
|
355
|
+
const bindsResolved = binds.map(bind => ({
|
356
|
+
update: resolveBindUpdater(bind, bind.element),
|
357
|
+
transformer: resolveTransform(bind),
|
358
|
+
sourceField: bind.sourceField
|
359
|
+
}));
|
360
|
+
const update = (value) => {
|
361
|
+
for (const bind of bindsResolved) {
|
362
|
+
if (bind.transformer) {
|
363
|
+
bind.update(bind.transformer(value));
|
364
|
+
}
|
365
|
+
else {
|
366
|
+
const v = (bind.sourceField) ? value[bind.sourceField] : value;
|
367
|
+
if (typeof v === `object`) {
|
368
|
+
if (bind.sourceField) {
|
369
|
+
bind.update(JSON.stringify(v));
|
370
|
+
}
|
371
|
+
else {
|
372
|
+
bind.update(JSON.stringify(v));
|
373
|
+
}
|
374
|
+
}
|
375
|
+
else
|
376
|
+
bind.update(v);
|
377
|
+
}
|
378
|
+
}
|
379
|
+
};
|
380
|
+
const unsub = source.on(message => {
|
381
|
+
if (Rx.messageHasValue(message)) {
|
382
|
+
update(message.value);
|
383
|
+
}
|
384
|
+
else if (Rx.messageIsSignal(message)) {
|
385
|
+
console.warn(message);
|
386
|
+
}
|
387
|
+
});
|
388
|
+
if (Rx.hasLast(source)) {
|
389
|
+
update(source.last());
|
390
|
+
}
|
391
|
+
return {
|
392
|
+
remove: (removeElements) => {
|
393
|
+
unsub();
|
394
|
+
if (removeElements) {
|
395
|
+
for (const bind of binds) {
|
396
|
+
bind.element.remove();
|
397
|
+
}
|
398
|
+
}
|
399
|
+
}
|
400
|
+
};
|
401
|
+
};
|
402
|
+
/**
|
403
|
+
* Calls `updater` whenever `source` produces a value. Useful when several fields from a value
|
404
|
+
* are needed to update an element.
|
405
|
+
* ```js
|
406
|
+
* bindUpdate(source, `#someEl`, (v, el) => {
|
407
|
+
* el.setAttribute(`width`, v.width);
|
408
|
+
* el.setAttribute(`height`, v.height);
|
409
|
+
* });
|
410
|
+
* ```
|
411
|
+
*
|
412
|
+
* Returns a {@link PipeDomBinding} to manage binding
|
413
|
+
* ```js
|
414
|
+
* const b = bindUpdate(...);
|
415
|
+
* b.remove(); // Disconnect binding
|
416
|
+
* b.remove(true); // Disconnect binding and remove element
|
417
|
+
* b.el; // HTML element
|
418
|
+
* ```
|
419
|
+
* @param elOrQuery
|
420
|
+
* @param source
|
421
|
+
* @param updater
|
422
|
+
* @returns
|
423
|
+
*/
|
424
|
+
export const bindUpdate = (source, elOrQuery, updater) => {
|
425
|
+
const el = resolveEl(elOrQuery);
|
426
|
+
const update = (value) => {
|
427
|
+
updater(value, el);
|
428
|
+
};
|
429
|
+
const unsub = source.on(message => {
|
430
|
+
if (Rx.messageHasValue(message)) {
|
431
|
+
console.log(message);
|
432
|
+
update(message.value);
|
433
|
+
}
|
434
|
+
else {
|
435
|
+
console.warn(message);
|
436
|
+
}
|
437
|
+
});
|
438
|
+
if (Rx.hasLast(source)) {
|
439
|
+
update(source.last());
|
440
|
+
}
|
441
|
+
return {
|
442
|
+
remove: (removeElement) => {
|
443
|
+
unsub();
|
444
|
+
if (removeElement) {
|
445
|
+
el.remove();
|
446
|
+
}
|
447
|
+
}
|
448
|
+
};
|
449
|
+
};
|
450
|
+
/**
|
451
|
+
* Updates a HTML element based on diffs on an object.
|
452
|
+
* ```js
|
453
|
+
* // Wrap an object
|
454
|
+
* const o = Rx.object({ name: `Jane`, ticks: 0 });
|
455
|
+
* const b = bindDiffUpdate(`#test`, o, (diffs, el) => {
|
456
|
+
* // el = reference to #test
|
457
|
+
* // diff = Array of Changes,
|
458
|
+
* // eg [ { path: `ticks`, value: 797, previous: 0 } ]
|
459
|
+
* for (const diff of diffs) {
|
460
|
+
* if (diff.path === `ticks`) el.textContent = `${diff.previous} -> ${diff.value}`
|
461
|
+
* }
|
462
|
+
* })
|
463
|
+
*
|
464
|
+
* // Eg. update field
|
465
|
+
* o.updateField(`ticks`, Math.floor(Math.random()*1000));
|
466
|
+
* ```
|
467
|
+
*
|
468
|
+
* If `initial` is provided as an option, this will be called if `source` has an initial value. Without this, the DOM won't be updated until the first data
|
469
|
+
* update happens.
|
470
|
+
* ```js
|
471
|
+
* bindDiffUpdate(el, source, updater, {
|
472
|
+
* initial: (v, el) => {
|
473
|
+
* el.innerHTML = v.name;
|
474
|
+
* }
|
475
|
+
* })
|
476
|
+
* ```
|
477
|
+
* @param elOrQuery
|
478
|
+
* @param source
|
479
|
+
* @param updater
|
480
|
+
* @param opts
|
481
|
+
* @returns
|
482
|
+
*/
|
483
|
+
export const bindDiffUpdate = (source, elOrQuery, updater, opts = {}) => {
|
484
|
+
if (elOrQuery === null)
|
485
|
+
throw new Error(`Param 'elOrQuery' is null`);
|
486
|
+
if (elOrQuery === undefined)
|
487
|
+
throw new Error(`Param 'elOrQuery' is undefined`);
|
488
|
+
const el = resolveEl(elOrQuery);
|
489
|
+
//const binds = opts.binds;
|
490
|
+
const update = (value) => {
|
491
|
+
updater(value, el);
|
492
|
+
};
|
493
|
+
const unsub = source.onDiff(value => {
|
494
|
+
update(value);
|
495
|
+
});
|
496
|
+
const init = () => {
|
497
|
+
if (Rx.hasLast(source) && opts.initial)
|
498
|
+
opts.initial(source.last(), el);
|
499
|
+
};
|
500
|
+
init();
|
501
|
+
return {
|
502
|
+
refresh: () => {
|
503
|
+
init();
|
504
|
+
},
|
505
|
+
remove: (removeElement) => {
|
506
|
+
unsub();
|
507
|
+
if (removeElement) {
|
508
|
+
el.remove();
|
509
|
+
}
|
510
|
+
}
|
511
|
+
};
|
512
|
+
};
|
513
|
+
/**
|
514
|
+
* Creates a new HTML element and calls `bindUpdate` so values from `source` can be used
|
515
|
+
* to update it.
|
516
|
+
*
|
517
|
+
*
|
518
|
+
* ```js
|
519
|
+
* // Creates a span, adding it to <body>
|
520
|
+
* const b = createUpdate(dataSource, (value, el) => {
|
521
|
+
* el.width = value.width;
|
522
|
+
* el.height = value.height;
|
523
|
+
* }, {
|
524
|
+
* tagName: `SPAN`,
|
525
|
+
* parentEl: document.body
|
526
|
+
* })
|
527
|
+
* ```
|
528
|
+
* @param source
|
529
|
+
* @param updater
|
530
|
+
* @param options
|
531
|
+
* @returns
|
532
|
+
*/
|
533
|
+
// export const createUpdate = <V>(source: Rx.Reactive<V>, updater: (v: V, el: HTMLElement) => void, options: Partial<DomCreateOptions> = {}): PipeDomBinding => {
|
534
|
+
// const tag = options.tagName ?? `DIV`;
|
535
|
+
// const el = document.createElement(tag);
|
536
|
+
// if (options.parentEl) {
|
537
|
+
// const parent = resolveEl(options.parentEl);
|
538
|
+
// parent.append(el);
|
539
|
+
// }
|
540
|
+
// const b = bindUpdate(source, el, updater);
|
541
|
+
// return b;
|
542
|
+
// }
|
543
|
+
/**
|
544
|
+
* Creates, updates & deletes elements based on pathed values from a reactive.
|
545
|
+
*
|
546
|
+
* This means that elements are only manipulated if its associated data changes,
|
547
|
+
* and elements are not modified if there's no need to.
|
548
|
+
* @param source
|
549
|
+
* @param options
|
550
|
+
*/
|
551
|
+
export const elements = (source, options) => {
|
552
|
+
const containerEl = options.container ? resolveEl(options.container) : document.body;
|
553
|
+
const defaultTag = options.defaultTag ?? `div`;
|
554
|
+
const elByField = new Map();
|
555
|
+
const binds = new Map();
|
556
|
+
for (const [key, value] of Object.entries(options.binds ?? {})) {
|
557
|
+
const tagName = value.tagName ?? defaultTag;
|
558
|
+
//console.log(`key: ${ key }`);
|
559
|
+
binds.set(key, {
|
560
|
+
...value,
|
561
|
+
update: resolveBindUpdaterBase(value),
|
562
|
+
transform: resolveTransform(value),
|
563
|
+
tagName,
|
564
|
+
path: key
|
565
|
+
});
|
566
|
+
}
|
567
|
+
const findBind = (path) => {
|
568
|
+
const bind = getFromKeys(binds, stringSegmentsWholeToEnd(path));
|
569
|
+
if (bind !== undefined)
|
570
|
+
return bind;
|
571
|
+
if (!path.includes(`.`))
|
572
|
+
return binds.get(`_root`);
|
573
|
+
};
|
574
|
+
function* ancestorBinds(path) {
|
575
|
+
for (const p of stringSegmentsWholeToFirst(path)) {
|
576
|
+
//console.log(` ancestorBinds path: ${ path } segment: ${ p }`)
|
577
|
+
if (binds.has(p)) {
|
578
|
+
//console.log(` bind: ${ p } found: ${ JSON.stringify(binds.get(p)) }`);
|
579
|
+
yield binds.get(p);
|
580
|
+
}
|
581
|
+
else {
|
582
|
+
//console.log(` bind: ${ p } not found`);
|
583
|
+
}
|
584
|
+
}
|
585
|
+
if (binds.has(`_root`) && path.includes(`.`))
|
586
|
+
yield binds.get(`_root`);
|
587
|
+
}
|
588
|
+
const create = (path, value) => {
|
589
|
+
const rootedPath = getRootedPath(path);
|
590
|
+
console.log(`Rx.Dom.elements.create: ${path} rooted: ${rootedPath} value: ${JSON.stringify(value)}`);
|
591
|
+
// Create
|
592
|
+
const bind = findBind(getRootedPath(path));
|
593
|
+
let tagName = defaultTag;
|
594
|
+
if (bind?.tagName)
|
595
|
+
tagName = bind.tagName;
|
596
|
+
const el = document.createElement(tagName);
|
597
|
+
el.setAttribute(`data-path`, path);
|
598
|
+
update(path, el, value);
|
599
|
+
let parentForEl;
|
600
|
+
for (const b of ancestorBinds(rootedPath)) {
|
601
|
+
//console.log(` path: ${ rootedPath } b: ${ JSON.stringify(b) }`);
|
602
|
+
if (b?.nestChildren) {
|
603
|
+
// Get root of path
|
604
|
+
const absoluteRoot = beforeMatch(path, `.`);
|
605
|
+
const findBy = b.path.replace(`_root`, absoluteRoot);
|
606
|
+
parentForEl = elByField.get(findBy);
|
607
|
+
if (parentForEl === undefined) {
|
608
|
+
//console.log(` could not find parent. path: ${ path } b.path: ${ b.path } findBy: ${ findBy }`);
|
609
|
+
}
|
610
|
+
else {
|
611
|
+
//console.log(` found parent`);
|
612
|
+
break;
|
613
|
+
}
|
614
|
+
}
|
615
|
+
}
|
616
|
+
(parentForEl ?? containerEl).append(el);
|
617
|
+
elByField.set(path, el);
|
618
|
+
console.log(`Added el: ${path}`);
|
619
|
+
};
|
620
|
+
const update = (path, el, value) => {
|
621
|
+
console.log(`Rx.dom.update path: ${path} value:`, value);
|
622
|
+
const bind = findBind(getRootedPath(path));
|
623
|
+
if (bind === undefined) {
|
624
|
+
//console.log(`Rx.dom.update no bind for ${ path }`)
|
625
|
+
if (typeof value === `object`)
|
626
|
+
value = JSON.stringify(value);
|
627
|
+
el.textContent = value;
|
628
|
+
}
|
629
|
+
else {
|
630
|
+
//console.log(`Rx.dom.update got bind! ${ path } `);
|
631
|
+
if (bind.transform)
|
632
|
+
value = bind.transform(value);
|
633
|
+
bind.update(value, el);
|
634
|
+
}
|
635
|
+
};
|
636
|
+
const changes = (changes) => {
|
637
|
+
const queue = new QueueMutable({}, changes);
|
638
|
+
let d = queue.dequeue();
|
639
|
+
const seenPaths = new Set();
|
640
|
+
while (d !== undefined) {
|
641
|
+
//for (const d of changes) {
|
642
|
+
const path = d.path;
|
643
|
+
if (!(`previous` in d) || d.previous === undefined) {
|
644
|
+
// Create
|
645
|
+
console.log(`Rx.Dom.elements.changes no previous. path: ${path}`);
|
646
|
+
create(path, d.value);
|
647
|
+
const subdata = [...getPathsAndData(d.value, false, Number.MAX_SAFE_INTEGER, path)];
|
648
|
+
console.log(subdata);
|
649
|
+
for (const dd of subdata) {
|
650
|
+
if (!seenPaths.has(dd.path)) {
|
651
|
+
queue.enqueue(dd);
|
652
|
+
seenPaths.add(dd.path);
|
653
|
+
}
|
654
|
+
}
|
655
|
+
}
|
656
|
+
else if (d.value === undefined) {
|
657
|
+
// Delete
|
658
|
+
const el = elByField.get(path);
|
659
|
+
if (el === undefined) {
|
660
|
+
console.warn(`No element to delete? ${path} `);
|
661
|
+
}
|
662
|
+
else {
|
663
|
+
console.log(`Rx.Dom.elements.changes delete ${path}`);
|
664
|
+
el.remove();
|
665
|
+
}
|
666
|
+
}
|
667
|
+
else {
|
668
|
+
// Update
|
669
|
+
const el = elByField.get(path);
|
670
|
+
if (el === undefined) {
|
671
|
+
console.warn(`Rx.Dom.elements.changes No element to update ? ${path} `);
|
672
|
+
create(path, d.value);
|
673
|
+
}
|
674
|
+
else {
|
675
|
+
//console.log(`Rx.Dom.elements.changes Updating ${ path } `, el);
|
676
|
+
update(path, el, d.value);
|
677
|
+
}
|
678
|
+
}
|
679
|
+
d = queue.dequeue();
|
680
|
+
}
|
681
|
+
};
|
682
|
+
/**
|
683
|
+
* Source has changed
|
684
|
+
*/
|
685
|
+
source.onDiff(value => {
|
686
|
+
//console.log(`Rx.Dom.elements diff ${ JSON.stringify(value) } `);
|
687
|
+
changes(value);
|
688
|
+
});
|
689
|
+
// Source has an initial value, use that
|
690
|
+
if (Rx.hasLast(source)) {
|
691
|
+
const last = source.last();
|
692
|
+
// Get data of value as a set of paths and data
|
693
|
+
// but only at first level of depth, because changes() will probe
|
694
|
+
// deeper itself
|
695
|
+
changes([...getPathsAndData(last, false, 1)]);
|
696
|
+
}
|
697
|
+
};
|
698
|
+
/**
|
699
|
+
* Replaces the root portion of `path` with the magic keyword `_root`
|
700
|
+
* @param path
|
701
|
+
* @returns
|
702
|
+
*/
|
703
|
+
const getRootedPath = (path) => {
|
704
|
+
const after = afterMatch(path, `.`);
|
705
|
+
return after === path ? `_root` : `_root.` + after;
|
706
|
+
};
|
707
|
+
export function win() {
|
708
|
+
const generateRect = () => ({ width: window.innerWidth, height: window.innerHeight });
|
709
|
+
const size = RxFrom.event(window, `resize`, {
|
710
|
+
lazy: `very`,
|
711
|
+
transform: () => generateRect(),
|
712
|
+
});
|
713
|
+
const pointer = RxFrom.event(window, `pointermove`, {
|
714
|
+
lazy: `very`,
|
715
|
+
transform: (args) => {
|
716
|
+
if (args === undefined)
|
717
|
+
return { x: 0, y: 0 };
|
718
|
+
const pe = args;
|
719
|
+
return { x: pe.x, y: pe.y };
|
720
|
+
}
|
721
|
+
});
|
722
|
+
const dispose = (reason = `Reactive.win.dispose`) => {
|
723
|
+
size.dispose(reason);
|
724
|
+
pointer.dispose(reason);
|
725
|
+
};
|
726
|
+
return { dispose, size, pointer };
|
727
|
+
}
|