@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 @@
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../__tests__/test.ts"],"names":[],"mappings":""}
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,eAAe,CAAC"}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * as Rx from './rx/index.js';
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import type { Interval } from "@ixfx/core";
|
2
|
+
/**
|
3
|
+
* Observe when element resizes. Specify `interval` to debounce, uses 100ms by default.
|
4
|
+
*
|
5
|
+
* ```
|
6
|
+
* const o = resizeObservable(myEl, 500);
|
7
|
+
* o.subscribe(() => {
|
8
|
+
* // called 500ms after last resize
|
9
|
+
* });
|
10
|
+
* ```
|
11
|
+
* @param elem
|
12
|
+
* @param interval Tiemout before event gets triggered
|
13
|
+
* @returns
|
14
|
+
*/
|
15
|
+
export declare const browserResizeObservable: (elem: Readonly<Element>, interval?: Interval) => import("@ixfx/rx").Reactive<ResizeObserverEntry[]>;
|
16
|
+
/**
|
17
|
+
* Returns an Reactive for window resize. Default 100ms debounce.
|
18
|
+
* @param elapsed
|
19
|
+
* @returns
|
20
|
+
*/
|
21
|
+
//# sourceMappingURL=browser-resize.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"browser-resize.d.ts","sourceRoot":"","sources":["../../../src/rx/browser-resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,GAClC,MAAM,QAAQ,CAAC,OAAO,CAAC,EACvB,WAAW,QAAQ,uDAqBpB,CAAA;AAED;;;;GAIG"}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { observable } from "@ixfx/rx/from";
|
2
|
+
import { debounce } from "@ixfx/rx/op/debounce";
|
3
|
+
/**
|
4
|
+
* Observe when element resizes. Specify `interval` to debounce, uses 100ms by default.
|
5
|
+
*
|
6
|
+
* ```
|
7
|
+
* const o = resizeObservable(myEl, 500);
|
8
|
+
* o.subscribe(() => {
|
9
|
+
* // called 500ms after last resize
|
10
|
+
* });
|
11
|
+
* ```
|
12
|
+
* @param elem
|
13
|
+
* @param interval Tiemout before event gets triggered
|
14
|
+
* @returns
|
15
|
+
*/
|
16
|
+
export const browserResizeObservable = (elem, interval) => {
|
17
|
+
if (elem === null) {
|
18
|
+
throw new Error(`Param 'elem' is null. Expected element to observe`);
|
19
|
+
}
|
20
|
+
if (elem === undefined) {
|
21
|
+
throw new Error(`Param 'elem' is undefined. Expected element to observe`);
|
22
|
+
}
|
23
|
+
const m = observable(stream => {
|
24
|
+
const ro = new ResizeObserver((entries) => {
|
25
|
+
stream.set(entries);
|
26
|
+
});
|
27
|
+
ro.observe(elem);
|
28
|
+
return () => {
|
29
|
+
ro.unobserve(elem);
|
30
|
+
};
|
31
|
+
});
|
32
|
+
//return debounce({ elapsed: interval ?? 100 })(m);
|
33
|
+
return debounce({ elapsed: interval ?? 100 })(m);
|
34
|
+
};
|
35
|
+
/**
|
36
|
+
* Returns an Reactive for window resize. Default 100ms debounce.
|
37
|
+
* @param elapsed
|
38
|
+
* @returns
|
39
|
+
*/
|
40
|
+
// export const windowResize = (elapsed?: Interval) => Rx.Ops.debounce<{ innerWidth: number, innerHeight: number }>({ elapsed: elapsed ?? 100 })(Rx.From.event(window, `resize`, { innerWidth: 0, innerHeight: 0 }));
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/**
|
2
|
+
* Observe when a class changes on a target element, by default the document.
|
3
|
+
* Useful for tracking theme changes.
|
4
|
+
*
|
5
|
+
* ```js
|
6
|
+
* const c = cssClassChange();
|
7
|
+
* c.on(msg => {
|
8
|
+
* // some class has changed on the document
|
9
|
+
* });
|
10
|
+
* ```
|
11
|
+
*/
|
12
|
+
export declare const cssClassChange: (target?: HTMLElement) => import("@ixfx/rx").Reactive<MutationRecord[]>;
|
13
|
+
//# sourceMappingURL=browser-theme-change.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"browser-theme-change.d.ts","sourceRoot":"","sources":["../../../src/rx/browser-theme-change.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,GAAI,oBAAiC,kDAgB/D,CAAA"}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { observable } from "@ixfx/rx/from/observable";
|
2
|
+
/**
|
3
|
+
* Observe when a class changes on a target element, by default the document.
|
4
|
+
* Useful for tracking theme changes.
|
5
|
+
*
|
6
|
+
* ```js
|
7
|
+
* const c = cssClassChange();
|
8
|
+
* c.on(msg => {
|
9
|
+
* // some class has changed on the document
|
10
|
+
* });
|
11
|
+
* ```
|
12
|
+
*/
|
13
|
+
export const cssClassChange = (target = document.documentElement) => {
|
14
|
+
const m = observable(stream => {
|
15
|
+
const ro = new MutationObserver((entries) => {
|
16
|
+
stream.set(entries);
|
17
|
+
});
|
18
|
+
const opts = {
|
19
|
+
attributeFilter: [`class`],
|
20
|
+
attributes: true,
|
21
|
+
};
|
22
|
+
ro.observe(target, opts);
|
23
|
+
return () => {
|
24
|
+
ro.disconnect();
|
25
|
+
};
|
26
|
+
});
|
27
|
+
return m;
|
28
|
+
};
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { type ReactiveInitial, type ReactiveNonInitial, type ReactiveWritable } from "@ixfx/rx";
|
2
|
+
import type { HslRelative } from "@ixfx/visual/colour";
|
3
|
+
export type ReactiveColour = ReactiveWritable<HslRelative> & {
|
4
|
+
setHsl: (hsl: HslRelative) => void;
|
5
|
+
};
|
6
|
+
export declare function colour(initialValue: HslRelative): ReactiveColour & ReactiveInitial<HslRelative>;
|
7
|
+
export declare function colour(): ReactiveColour & ReactiveNonInitial<HslRelative>;
|
8
|
+
//# sourceMappingURL=colour.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"colour.d.ts","sourceRoot":"","sources":["../../../src/rx/colour.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5G,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,MAAM,cAAc,GAAG,gBAAgB,CAAC,WAAW,CAAC,GAAG;IAC3D,MAAM,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,IAAI,CAAC;CACpC,CAAA;AAED,wBAAgB,MAAM,CAAC,YAAY,EAAE,WAAW,GAAG,cAAc,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;AACjG,wBAAgB,MAAM,IAAI,cAAc,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC"}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { initStream } from "@ixfx/rx";
|
2
|
+
export function colour(initialValue) {
|
3
|
+
let value = initialValue;
|
4
|
+
const events = initStream();
|
5
|
+
const set = (v) => {
|
6
|
+
value = v;
|
7
|
+
events.set(v);
|
8
|
+
};
|
9
|
+
return {
|
10
|
+
dispose: events.dispose,
|
11
|
+
isDisposed: events.isDisposed,
|
12
|
+
last: () => value,
|
13
|
+
on: events.on,
|
14
|
+
onValue: events.onValue,
|
15
|
+
set,
|
16
|
+
setHsl: (hsl) => {
|
17
|
+
set(hsl);
|
18
|
+
}
|
19
|
+
};
|
20
|
+
}
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import type { ReactiveInitial, ReactiveWritable, Reactive } from "@ixfx/rx";
|
2
|
+
import type { DomFormOptions, DomNumberInputValueOptions, DomValueOptions } from "./dom-types.js";
|
3
|
+
import { Colour } from "@ixfx/visual";
|
4
|
+
/**
|
5
|
+
* Reactive getting/setting of values to a HTML INPUT element.
|
6
|
+
*
|
7
|
+
* Options:
|
8
|
+
* - relative: if _true_, values are 0..1 (default: false)
|
9
|
+
* - inverted: if _true_, values are 1..0 (default: false)
|
10
|
+
*
|
11
|
+
* If element is missing a 'type' attribute, this will be set to 'range'.
|
12
|
+
* @param targetOrQuery
|
13
|
+
* @param options
|
14
|
+
* @returns
|
15
|
+
*/
|
16
|
+
export declare function domNumberInputValue(targetOrQuery: HTMLInputElement | string, options?: Partial<DomNumberInputValueOptions>): ReactiveInitial<number> & ReactiveWritable<number>;
|
17
|
+
export declare function domHslInputValue(targetOrQuery: HTMLInputElement | string, options?: Partial<DomValueOptions>): ReactiveInitial<Colour.HslRelative> & Reactive<Colour.HslRelative> & ReactiveWritable<Colour.HslRelative>;
|
18
|
+
/**
|
19
|
+
* A stream of values when the a HTMLInputElement changes. Eg a <input type="range">
|
20
|
+
* ```js
|
21
|
+
* const r = Rx.From.domInputValue(`#myEl`);
|
22
|
+
* r.onValue(value => {
|
23
|
+
* // value will be string
|
24
|
+
* });
|
25
|
+
* ```
|
26
|
+
*
|
27
|
+
* Options:
|
28
|
+
* * emitInitialValue: If _true_ emits the HTML value of element (default: false)
|
29
|
+
* * attributeName: If set, this is the HTML attribute value is set to when writing to stream (default: 'value')
|
30
|
+
* * fieldName: If set, this is the DOM object field set when writing to stream (default: 'value')
|
31
|
+
* * when: 'changed'|'changing' when values are emitted. (default: 'changed')
|
32
|
+
* * fallbackValue: Fallback value to use if field/attribute cannot be read (default: '')
|
33
|
+
* @param targetOrQuery
|
34
|
+
* @param options
|
35
|
+
* @returns
|
36
|
+
*/
|
37
|
+
export declare function domInputValue(targetOrQuery: HTMLInputElement | string, options?: Partial<DomValueOptions>): {
|
38
|
+
el: HTMLInputElement;
|
39
|
+
} & ReactiveInitial<string> & ReactiveWritable<string>;
|
40
|
+
/**
|
41
|
+
* Listens for data changes from elements within a HTML form element.
|
42
|
+
* Input elements must have a 'name' attribute.
|
43
|
+
*
|
44
|
+
* Simple usage:
|
45
|
+
* ```js
|
46
|
+
* const rx = Rx.From.domForm(`#my-form`);
|
47
|
+
* rx.onValue(value => {
|
48
|
+
* // Object containing values from form
|
49
|
+
* });
|
50
|
+
*
|
51
|
+
* rx.last(); // Read current values of form
|
52
|
+
* ```
|
53
|
+
*
|
54
|
+
* UI can be updated
|
55
|
+
* ```js
|
56
|
+
* // Set using an object of key-value pairs
|
57
|
+
* rx.set({
|
58
|
+
* size: 'large'
|
59
|
+
* });
|
60
|
+
*
|
61
|
+
* // Or set a single name-value pair
|
62
|
+
* rx.setNamedValue(`size`, `large`);
|
63
|
+
* ```
|
64
|
+
*
|
65
|
+
* If an 'upstream' reactive is provided, this is used to set initial values of the UI, overriding
|
66
|
+
* whatever may be in the HTML. Upstream changes modify UI elements, but UI changes do not modify the upstream
|
67
|
+
* source.
|
68
|
+
*
|
69
|
+
* ```js
|
70
|
+
* // Create a reactive object
|
71
|
+
* const obj = Rx.From.object({
|
72
|
+
* when: `2024-10-03`,
|
73
|
+
* size: 12,
|
74
|
+
* checked: true
|
75
|
+
* });
|
76
|
+
*
|
77
|
+
* // Use this as initial values for a HTML form
|
78
|
+
* // (assuming appropriate INPUT/SELECT elements exist)
|
79
|
+
* const rx = Rx.From.domForm(`form`, {
|
80
|
+
* upstreamSource: obj
|
81
|
+
* });
|
82
|
+
*
|
83
|
+
* // Listen for changes in the UI
|
84
|
+
* rx.onValue(value => {
|
85
|
+
*
|
86
|
+
* });
|
87
|
+
* ```
|
88
|
+
* @param formElOrQuery
|
89
|
+
* @param options
|
90
|
+
* @returns
|
91
|
+
*/
|
92
|
+
export declare function domForm<T extends Record<string, any>>(formElOrQuery: HTMLFormElement | string, options?: Partial<DomFormOptions<T>>): {
|
93
|
+
setNamedValue: (name: string, value: any) => void;
|
94
|
+
el: HTMLFormElement;
|
95
|
+
} & ReactiveInitial<T> & ReactiveWritable<T>;
|
96
|
+
//# sourceMappingURL=dom-source.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"dom-source.d.ts","sourceRoot":"","sources":["../../../src/rx/dom-source.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAC5E,OAAO,KAAK,EAAE,cAAc,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAIlG,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAItC;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,gBAAgB,GAAG,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,0BAA0B,CAAM,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAoCnL;AAED,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,gBAAgB,GAAG,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,eAAe,CAAM,GAAG,eAAe,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,gBAAgB,CAAC,MAAM,CAAC,WAAW,CAAC,CAoB5N;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,gBAAgB,GAAG,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,eAAe,CAAM,GAAG;IAAE,EAAE,EAAE,gBAAgB,CAAA;CAAE,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,gBAAgB,CAAC,MAAM,CAAC,CA+E7L;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,aAAa,EAAE,eAAe,GAAG,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAM,GAAG;IACzI,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAClD,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,eAAe,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAgK3C"}
|
@@ -0,0 +1,373 @@
|
|
1
|
+
import { resolveEl } from '@ixfx/dom';
|
2
|
+
import { transform } from '@ixfx/rx';
|
3
|
+
import { hasLast } from '@ixfx/rx';
|
4
|
+
import { Colour } from "@ixfx/visual";
|
5
|
+
import { eventTrigger } from "@ixfx/rx/from";
|
6
|
+
//import type { Colourish } from '@ixfx/visual/colour';
|
7
|
+
/**
|
8
|
+
* Reactive getting/setting of values to a HTML INPUT element.
|
9
|
+
*
|
10
|
+
* Options:
|
11
|
+
* - relative: if _true_, values are 0..1 (default: false)
|
12
|
+
* - inverted: if _true_, values are 1..0 (default: false)
|
13
|
+
*
|
14
|
+
* If element is missing a 'type' attribute, this will be set to 'range'.
|
15
|
+
* @param targetOrQuery
|
16
|
+
* @param options
|
17
|
+
* @returns
|
18
|
+
*/
|
19
|
+
export function domNumberInputValue(targetOrQuery, options = {}) {
|
20
|
+
const input = domInputValue(targetOrQuery, options);
|
21
|
+
const el = input.el;
|
22
|
+
const relative = options.relative ?? false;
|
23
|
+
const inverted = options.inverted ?? false;
|
24
|
+
const rx = transform(input, v => {
|
25
|
+
return Number.parseFloat(v);
|
26
|
+
});
|
27
|
+
if (relative) {
|
28
|
+
//el.setAttribute(`max`, inverted ? "0" : "1");
|
29
|
+
el.max = inverted ? "0" : "1";
|
30
|
+
//el.setAttribute(`min`, inverted ? "1" : "0");
|
31
|
+
el.min = inverted ? "1" : "0";
|
32
|
+
if (!el.hasAttribute(`step`)) {
|
33
|
+
//el.setAttribute(`step`, "0.1");
|
34
|
+
el.step = "0.1";
|
35
|
+
}
|
36
|
+
}
|
37
|
+
if (el.getAttribute(`type`) === null) {
|
38
|
+
el.type = `range`;
|
39
|
+
}
|
40
|
+
const set = (value) => {
|
41
|
+
input.set(value.toString());
|
42
|
+
};
|
43
|
+
return {
|
44
|
+
...rx,
|
45
|
+
last() {
|
46
|
+
//console.log(`domNumberInputValue last: ${ input.last() }`);
|
47
|
+
return Number.parseFloat(input.last());
|
48
|
+
},
|
49
|
+
set
|
50
|
+
};
|
51
|
+
}
|
52
|
+
export function domHslInputValue(targetOrQuery, options = {}) {
|
53
|
+
const input = domInputValue(targetOrQuery, {
|
54
|
+
...options,
|
55
|
+
upstreamFilter: (value) => {
|
56
|
+
return (typeof value === `object`) ? Colour.toHex(value) : value;
|
57
|
+
},
|
58
|
+
});
|
59
|
+
const rx = transform(input, v => {
|
60
|
+
return Colour.toHsl(v, true);
|
61
|
+
});
|
62
|
+
return {
|
63
|
+
...rx,
|
64
|
+
last() {
|
65
|
+
return Colour.toHsl(input.last(), true);
|
66
|
+
},
|
67
|
+
set(value) {
|
68
|
+
input.set(Colour.toHex(value));
|
69
|
+
},
|
70
|
+
};
|
71
|
+
}
|
72
|
+
/**
|
73
|
+
* A stream of values when the a HTMLInputElement changes. Eg a <input type="range">
|
74
|
+
* ```js
|
75
|
+
* const r = Rx.From.domInputValue(`#myEl`);
|
76
|
+
* r.onValue(value => {
|
77
|
+
* // value will be string
|
78
|
+
* });
|
79
|
+
* ```
|
80
|
+
*
|
81
|
+
* Options:
|
82
|
+
* * emitInitialValue: If _true_ emits the HTML value of element (default: false)
|
83
|
+
* * attributeName: If set, this is the HTML attribute value is set to when writing to stream (default: 'value')
|
84
|
+
* * fieldName: If set, this is the DOM object field set when writing to stream (default: 'value')
|
85
|
+
* * when: 'changed'|'changing' when values are emitted. (default: 'changed')
|
86
|
+
* * fallbackValue: Fallback value to use if field/attribute cannot be read (default: '')
|
87
|
+
* @param targetOrQuery
|
88
|
+
* @param options
|
89
|
+
* @returns
|
90
|
+
*/
|
91
|
+
export function domInputValue(targetOrQuery, options = {}) {
|
92
|
+
const target = (typeof targetOrQuery === `string` ? document.querySelector(targetOrQuery) : targetOrQuery);
|
93
|
+
if (target === null && typeof targetOrQuery === `string`)
|
94
|
+
throw new Error(`Element query could not be resolved '${targetOrQuery}'`);
|
95
|
+
if (target === null)
|
96
|
+
throw new Error(`targetOrQuery is null`);
|
97
|
+
const el = resolveEl(targetOrQuery);
|
98
|
+
const when = options.when ?? `changed`;
|
99
|
+
const eventName = when === `changed` ? `change` : `input`;
|
100
|
+
const emitInitialValue = options.emitInitialValue ?? false;
|
101
|
+
const fallbackValue = options.fallbackValue ?? ``;
|
102
|
+
const upstreamSource = options.upstreamSource;
|
103
|
+
let upstreamSourceUnsub = () => { };
|
104
|
+
let attribName = options.attributeName;
|
105
|
+
let fieldName = options.fieldName;
|
106
|
+
if (fieldName === undefined && attribName === undefined) {
|
107
|
+
attribName = fieldName = `value`;
|
108
|
+
}
|
109
|
+
const readValue = () => {
|
110
|
+
let value;
|
111
|
+
if (attribName) {
|
112
|
+
value = el.getAttribute(attribName);
|
113
|
+
//console.log(` attrib: ${ attribName } value: ${ value }`);
|
114
|
+
}
|
115
|
+
if (fieldName) {
|
116
|
+
value = el[fieldName];
|
117
|
+
}
|
118
|
+
if (value === undefined || value === null)
|
119
|
+
value = fallbackValue;
|
120
|
+
//console.log(`domInputValue readValue: ${ value }. attrib: ${ attribName } field: ${ fieldName }`);
|
121
|
+
return value;
|
122
|
+
};
|
123
|
+
const setValue = (value) => {
|
124
|
+
if (attribName) {
|
125
|
+
el.setAttribute(attribName, value);
|
126
|
+
}
|
127
|
+
if (fieldName) {
|
128
|
+
el[fieldName] = value;
|
129
|
+
}
|
130
|
+
};
|
131
|
+
const setUpstream = (v) => {
|
132
|
+
v = options.upstreamFilter ? options.upstreamFilter(v) : v;
|
133
|
+
setValue(v);
|
134
|
+
};
|
135
|
+
if (upstreamSource) {
|
136
|
+
upstreamSourceUnsub = upstreamSource.onValue(setUpstream);
|
137
|
+
if (hasLast(upstreamSource)) {
|
138
|
+
setUpstream(upstreamSource.last());
|
139
|
+
}
|
140
|
+
}
|
141
|
+
// Input element change event stream
|
142
|
+
const rxEvents = eventTrigger(el, eventName, {
|
143
|
+
fireInitial: emitInitialValue,
|
144
|
+
debugFiring: options.debugFiring ?? false,
|
145
|
+
debugLifecycle: options.debugLifecycle ?? false,
|
146
|
+
});
|
147
|
+
// Transform to get values
|
148
|
+
const rxValues = transform(rxEvents, _trigger => readValue());
|
149
|
+
return {
|
150
|
+
...rxValues,
|
151
|
+
el,
|
152
|
+
last() {
|
153
|
+
return readValue();
|
154
|
+
},
|
155
|
+
set(value) {
|
156
|
+
setValue(value);
|
157
|
+
},
|
158
|
+
dispose(reason) {
|
159
|
+
upstreamSourceUnsub();
|
160
|
+
rxValues.dispose(reason);
|
161
|
+
rxEvents.dispose(reason);
|
162
|
+
},
|
163
|
+
};
|
164
|
+
}
|
165
|
+
/**
|
166
|
+
* Listens for data changes from elements within a HTML form element.
|
167
|
+
* Input elements must have a 'name' attribute.
|
168
|
+
*
|
169
|
+
* Simple usage:
|
170
|
+
* ```js
|
171
|
+
* const rx = Rx.From.domForm(`#my-form`);
|
172
|
+
* rx.onValue(value => {
|
173
|
+
* // Object containing values from form
|
174
|
+
* });
|
175
|
+
*
|
176
|
+
* rx.last(); // Read current values of form
|
177
|
+
* ```
|
178
|
+
*
|
179
|
+
* UI can be updated
|
180
|
+
* ```js
|
181
|
+
* // Set using an object of key-value pairs
|
182
|
+
* rx.set({
|
183
|
+
* size: 'large'
|
184
|
+
* });
|
185
|
+
*
|
186
|
+
* // Or set a single name-value pair
|
187
|
+
* rx.setNamedValue(`size`, `large`);
|
188
|
+
* ```
|
189
|
+
*
|
190
|
+
* If an 'upstream' reactive is provided, this is used to set initial values of the UI, overriding
|
191
|
+
* whatever may be in the HTML. Upstream changes modify UI elements, but UI changes do not modify the upstream
|
192
|
+
* source.
|
193
|
+
*
|
194
|
+
* ```js
|
195
|
+
* // Create a reactive object
|
196
|
+
* const obj = Rx.From.object({
|
197
|
+
* when: `2024-10-03`,
|
198
|
+
* size: 12,
|
199
|
+
* checked: true
|
200
|
+
* });
|
201
|
+
*
|
202
|
+
* // Use this as initial values for a HTML form
|
203
|
+
* // (assuming appropriate INPUT/SELECT elements exist)
|
204
|
+
* const rx = Rx.From.domForm(`form`, {
|
205
|
+
* upstreamSource: obj
|
206
|
+
* });
|
207
|
+
*
|
208
|
+
* // Listen for changes in the UI
|
209
|
+
* rx.onValue(value => {
|
210
|
+
*
|
211
|
+
* });
|
212
|
+
* ```
|
213
|
+
* @param formElOrQuery
|
214
|
+
* @param options
|
215
|
+
* @returns
|
216
|
+
*/
|
217
|
+
export function domForm(formElOrQuery, options = {}) {
|
218
|
+
const formEl = resolveEl(formElOrQuery);
|
219
|
+
const when = options.when ?? `changed`;
|
220
|
+
const eventName = when === `changed` ? `change` : `input`;
|
221
|
+
const emitInitialValue = options.emitInitialValue ?? false;
|
222
|
+
const upstreamSource = options.upstreamSource;
|
223
|
+
const typeHints = new Map();
|
224
|
+
let upstreamSourceUnsub = () => { };
|
225
|
+
const readValue = () => {
|
226
|
+
const fd = new FormData(formEl);
|
227
|
+
const entries = [];
|
228
|
+
for (const [k, v] of fd.entries()) {
|
229
|
+
const vString = v.toString();
|
230
|
+
// Get type hint for key
|
231
|
+
let typeHint = typeHints.get(k);
|
232
|
+
if (!typeHint) {
|
233
|
+
// If not found, use the kind of input element as a hint
|
234
|
+
const el = getFormElement(k, vString);
|
235
|
+
if (el) {
|
236
|
+
if (el.type === `range` || el.type === `number`) {
|
237
|
+
typeHint = `number`;
|
238
|
+
}
|
239
|
+
else if (el.type === `color`) {
|
240
|
+
typeHint = `colour`;
|
241
|
+
}
|
242
|
+
else if (el.type === `checkbox` && (v === `true` || v === `on`)) {
|
243
|
+
typeHint = `boolean`;
|
244
|
+
}
|
245
|
+
else {
|
246
|
+
typeHint = `string`;
|
247
|
+
}
|
248
|
+
typeHints.set(k, typeHint);
|
249
|
+
}
|
250
|
+
}
|
251
|
+
if (typeHint === `number`) {
|
252
|
+
entries.push([k, Number.parseFloat(vString)]);
|
253
|
+
}
|
254
|
+
else if (typeHint === `boolean`) {
|
255
|
+
const vBool = (vString === `true`) ? true : false;
|
256
|
+
entries.push([k, vBool]);
|
257
|
+
}
|
258
|
+
else if (typeHint === `colour`) {
|
259
|
+
const vRgb = Colour.toString(vString);
|
260
|
+
entries.push([k, Colour.toRgb(vRgb)]);
|
261
|
+
}
|
262
|
+
else {
|
263
|
+
entries.push([k, v.toString()]);
|
264
|
+
}
|
265
|
+
}
|
266
|
+
// Checkboxes that aren't checked don't give a value, so find those
|
267
|
+
for (const el of formEl.querySelectorAll(`input[type="checkbox"]`)) {
|
268
|
+
if (!el.checked && el.value === `true`) {
|
269
|
+
entries.push([el.name, false]);
|
270
|
+
}
|
271
|
+
}
|
272
|
+
const asObject = Object.fromEntries(entries);
|
273
|
+
//console.log(`readValue`, asObj);
|
274
|
+
return asObject;
|
275
|
+
};
|
276
|
+
const getFormElement = (name, value) => {
|
277
|
+
const el = formEl.querySelector(`[name="${name}"]`);
|
278
|
+
if (!el) {
|
279
|
+
console.warn(`Form does not contain an element with name="${name}"`);
|
280
|
+
return;
|
281
|
+
}
|
282
|
+
if (el.type === `radio`) {
|
283
|
+
// Get right radio option
|
284
|
+
const radioEl = formEl.querySelector(`[name="${name}"][value="${value}"]`);
|
285
|
+
if (!radioEl) {
|
286
|
+
console.warn(`Form does not contain radio option for name=${name} value=${value}`);
|
287
|
+
return;
|
288
|
+
}
|
289
|
+
return radioEl;
|
290
|
+
}
|
291
|
+
return el;
|
292
|
+
};
|
293
|
+
const setNamedValue = (name, value) => {
|
294
|
+
const el = getFormElement(name, value);
|
295
|
+
if (!el)
|
296
|
+
return;
|
297
|
+
//let typeHint = typeHints.get(name);
|
298
|
+
// if (typeHint) {
|
299
|
+
// console.log(`${ name } hint: ${ typeHint } input type: ${ el.type }`);
|
300
|
+
// } else {
|
301
|
+
// console.warn(`Rx.Sources.Dom.domForm no type hint for: ${ name }`);
|
302
|
+
// }
|
303
|
+
if (el.nodeName === `INPUT` || el.nodeName === `SELECT`) {
|
304
|
+
if (el.type === `color`) {
|
305
|
+
if (typeof value === `object`) {
|
306
|
+
// Try to parse colour if value is an object
|
307
|
+
//const c = Colour.resolve(value, true);
|
308
|
+
value = Colour.toHex(value);
|
309
|
+
}
|
310
|
+
}
|
311
|
+
else if (el.type === `checkbox`) {
|
312
|
+
if (typeof value === `boolean`) {
|
313
|
+
el.checked = value;
|
314
|
+
return;
|
315
|
+
}
|
316
|
+
else {
|
317
|
+
console.warn(`Rx.Sources.domForm: Trying to set non boolean type to a checkbox. Name: ${name} Value: ${value} (${typeof value})`);
|
318
|
+
}
|
319
|
+
}
|
320
|
+
else if (el.type === `radio`) {
|
321
|
+
el.checked = true;
|
322
|
+
return;
|
323
|
+
}
|
324
|
+
el.value = value;
|
325
|
+
}
|
326
|
+
};
|
327
|
+
const setFromUpstream = (value) => {
|
328
|
+
//console.log(`setUpstream`, value);
|
329
|
+
for (const [name, v] of Object.entries(value)) {
|
330
|
+
let hint = typeHints.get(name);
|
331
|
+
if (!hint) {
|
332
|
+
hint = typeof v;
|
333
|
+
if (hint === `object`) {
|
334
|
+
const rgb = Colour.parseRgbObject(v);
|
335
|
+
if (rgb.success) {
|
336
|
+
hint = `colour`;
|
337
|
+
}
|
338
|
+
}
|
339
|
+
typeHints.set(name, hint);
|
340
|
+
}
|
341
|
+
const valueFiltered = options.upstreamFilter ? options.upstreamFilter(name, v) : v;
|
342
|
+
setNamedValue(name, valueFiltered);
|
343
|
+
}
|
344
|
+
};
|
345
|
+
if (upstreamSource) {
|
346
|
+
upstreamSourceUnsub = upstreamSource.onValue(setFromUpstream);
|
347
|
+
if (hasLast(upstreamSource)) {
|
348
|
+
setFromUpstream(upstreamSource.last());
|
349
|
+
}
|
350
|
+
}
|
351
|
+
// Input element change event stream
|
352
|
+
const rxEvents = eventTrigger(formEl, eventName, {
|
353
|
+
fireInitial: emitInitialValue,
|
354
|
+
debugFiring: options.debugFiring ?? false,
|
355
|
+
debugLifecycle: options.debugLifecycle ?? false,
|
356
|
+
});
|
357
|
+
// Transform to get values
|
358
|
+
const rxValues = transform(rxEvents, _trigger => readValue());
|
359
|
+
return {
|
360
|
+
...rxValues,
|
361
|
+
el: formEl,
|
362
|
+
last() {
|
363
|
+
return readValue();
|
364
|
+
},
|
365
|
+
set: setFromUpstream,
|
366
|
+
setNamedValue,
|
367
|
+
dispose(reason) {
|
368
|
+
upstreamSourceUnsub();
|
369
|
+
rxValues.dispose(reason);
|
370
|
+
rxEvents.dispose(reason);
|
371
|
+
},
|
372
|
+
};
|
373
|
+
}
|