@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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test.d.ts.map
@@ -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,2 @@
1
+ export * as Rx from './rx/index.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }