@rettangoli/ui 1.0.0-rc8 → 1.0.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.
Files changed (45) hide show
  1. package/dist/rettangoli-iife-layout.min.js +74 -42
  2. package/dist/rettangoli-iife-ui.min.js +161 -60
  3. package/dist/themes/base.css +4 -4
  4. package/dist/themes/theme-catppuccin.css +1 -1
  5. package/dist/themes/theme-rtgl-mono.css +1 -1
  6. package/dist/themes/theme-rtgl-slate.css +1 -1
  7. package/package.json +11 -7
  8. package/src/common/dimensions.js +85 -0
  9. package/src/common/responsive.js +72 -0
  10. package/src/common.js +6 -4
  11. package/src/components/dropdownMenu/dropdownMenu.schema.yaml +1 -1
  12. package/src/components/form/form.handlers.js +328 -152
  13. package/src/components/form/form.methods.js +205 -0
  14. package/src/components/form/form.schema.yaml +16 -271
  15. package/src/components/form/form.store.js +542 -97
  16. package/src/components/form/form.view.yaml +73 -52
  17. package/src/components/globalUi/globalUi.handlers.js +4 -4
  18. package/src/components/popoverInput/popoverInput.handlers.js +64 -50
  19. package/src/components/popoverInput/popoverInput.schema.yaml +3 -1
  20. package/src/components/popoverInput/popoverInput.store.js +9 -3
  21. package/src/components/popoverInput/popoverInput.view.yaml +4 -4
  22. package/src/components/select/select.handlers.js +15 -19
  23. package/src/components/select/select.schema.yaml +2 -0
  24. package/src/components/select/select.store.js +8 -6
  25. package/src/components/select/select.view.yaml +4 -4
  26. package/src/components/sliderInput/sliderInput.handlers.js +15 -1
  27. package/src/components/sliderInput/sliderInput.schema.yaml +3 -0
  28. package/src/components/sliderInput/sliderInput.store.js +2 -1
  29. package/src/components/sliderInput/sliderInput.view.yaml +2 -2
  30. package/src/components/tooltip/tooltip.schema.yaml +1 -1
  31. package/src/deps/createGlobalUI.js +4 -4
  32. package/src/entry-iife-layout.js +6 -0
  33. package/src/entry-iife-ui.js +8 -0
  34. package/src/index.js +8 -0
  35. package/src/primitives/checkbox.js +295 -0
  36. package/src/primitives/input-date.js +31 -0
  37. package/src/primitives/input-datetime.js +31 -0
  38. package/src/primitives/input-time.js +31 -0
  39. package/src/primitives/input.js +43 -1
  40. package/src/primitives/textarea.js +3 -0
  41. package/src/primitives/view.js +8 -2
  42. package/src/themes/base.css +4 -4
  43. package/src/themes/theme-catppuccin.css +1 -1
  44. package/src/themes/theme-rtgl-mono.css +1 -1
  45. package/src/themes/theme-rtgl-slate.css +1 -1
@@ -1,194 +1,405 @@
1
- const updateAttributes = ({ form, defaultValues = {}, refs }) => {
2
- const { fields = [] } = form;
3
- fields.forEach((field, index) => {
4
- const ref = refs[`field${index}`];
1
+ import {
2
+ get,
3
+ set,
4
+ selectForm,
5
+ selectFormValues,
6
+ collectAllDataFields,
7
+ getDefaultValue,
8
+ pruneHiddenValues,
9
+ validateField,
10
+ validateForm,
11
+ } from "./form.store.js";
5
12
 
6
- if (!ref) {
7
- return;
8
- }
13
+ const syncInteractiveFieldAttribute = ({ field, target, value }) => {
14
+ if (!field || !target) return;
15
+ if (!["slider-with-input", "popover-input"].includes(field.type)) return;
9
16
 
10
- if (['input-textarea', 'input-text', 'input-number', 'color-picker', 'slider', 'slider-input', 'popover-input'].includes(field.inputType)) {
11
- const defaultValue = defaultValues[field.name];
12
- if (defaultValue === undefined || defaultValue === null) {
13
- ref.removeAttribute('value')
14
- } else {
15
- ref.setAttribute('value', defaultValue)
17
+ if (value === undefined || value === null) {
18
+ target.removeAttribute("value");
19
+ } else {
20
+ target.setAttribute("value", String(value));
21
+ }
22
+ };
23
+
24
+ const updateFieldAttributes = ({
25
+ form,
26
+ formValues = {},
27
+ refs,
28
+ formDisabled = false,
29
+ }) => {
30
+ const fields = form.fields || [];
31
+ let idx = 0;
32
+
33
+ const walk = (fieldList) => {
34
+ for (const field of fieldList) {
35
+ if (field.type === "section") {
36
+ idx++;
37
+ if (Array.isArray(field.fields)) {
38
+ walk(field.fields);
39
+ }
40
+ continue;
16
41
  }
17
- }
18
- if (['input-text', 'input-textarea'].includes(field.inputType) && field.placeholder) {
19
- const currentPlaceholder = ref.getAttribute('placeholder')
20
- if (currentPlaceholder !== field.placeholder) {
21
- if (field.placeholder === undefined || field.placeholder === null) {
22
- ref.removeAttribute('placeholder');
42
+
43
+ const ref = refs[`field${idx}`];
44
+ idx++;
45
+
46
+ if (!ref) continue;
47
+
48
+ const disabled = formDisabled || !!field.disabled;
49
+
50
+ if (["input-text", "input-date", "input-time", "input-datetime", "input-number", "input-textarea", "color-picker", "slider", "slider-with-input", "popover-input"].includes(field.type)) {
51
+ const value = get(formValues, field.name);
52
+ if (value === undefined || value === null) {
53
+ ref.removeAttribute("value");
23
54
  } else {
24
- ref.setAttribute('placeholder', field.placeholder);
55
+ ref.setAttribute("value", String(value));
56
+ }
57
+
58
+ if (field.type === "slider-with-input" && ref.store?.setValue) {
59
+ const normalized = Number(value ?? 0);
60
+ ref.store.setValue({ value: Number.isFinite(normalized) ? normalized : 0 });
61
+ if (typeof ref.render === "function") {
62
+ ref.render();
63
+ }
64
+ }
65
+
66
+ if (field.type === "popover-input" && ref.store?.setValue) {
67
+ ref.store.setValue({ value: value === undefined || value === null ? "" : String(value) });
68
+ if (typeof ref.render === "function") {
69
+ ref.render();
70
+ }
25
71
  }
26
72
  }
27
- }
28
- })
29
- }
30
-
31
- const autoFocusFirstInput = (refs) => {
32
- // Find first focusable field
33
- for (const fieldKey in refs) {
34
- if (fieldKey.startsWith('field')) {
35
- const fieldRef = refs[fieldKey];
36
- if (fieldRef && fieldRef.focus) {
37
- // Currently only available for input-text and input-textarea
38
- fieldRef.focus();
39
- return;
73
+
74
+ if (field.type === "checkbox") {
75
+ const value = get(formValues, field.name);
76
+ if (value) {
77
+ ref.setAttribute("checked", "");
78
+ } else {
79
+ ref.removeAttribute("checked");
80
+ }
81
+ }
82
+
83
+ if (["input-text", "input-date", "input-time", "input-datetime", "input-number", "input-textarea", "popover-input"].includes(field.type) && field.placeholder) {
84
+ const current = ref.getAttribute("placeholder");
85
+ if (current !== field.placeholder) {
86
+ if (field.placeholder === undefined || field.placeholder === null) {
87
+ ref.removeAttribute("placeholder");
88
+ } else {
89
+ ref.setAttribute("placeholder", field.placeholder);
90
+ }
91
+ }
92
+ }
93
+
94
+ if (disabled) {
95
+ ref.setAttribute("disabled", "");
96
+ } else {
97
+ ref.removeAttribute("disabled");
40
98
  }
41
99
  }
42
- }
100
+ };
101
+
102
+ walk(fields);
43
103
  };
44
104
 
105
+ const initFormValues = (store, props) => {
106
+ const defaultValues = props?.defaultValues || {};
107
+ const seededValues = {};
108
+ Object.keys(defaultValues).forEach((path) => {
109
+ set(seededValues, path, defaultValues[path]);
110
+ });
111
+ const form = selectForm({ state: { formValues: seededValues }, props });
112
+ const dataFields = collectAllDataFields(form.fields || []);
113
+ const initial = {};
114
+
115
+ for (const field of dataFields) {
116
+ const defaultVal = get(defaultValues, field.name);
117
+ if (defaultVal !== undefined) {
118
+ set(initial, field.name, defaultVal);
119
+ } else {
120
+ set(initial, field.name, getDefaultValue(field));
121
+ }
122
+ }
123
+
124
+ store.resetFormValues({ defaultValues: initial });
125
+ };
45
126
 
46
127
  export const handleBeforeMount = (deps) => {
47
128
  const { store, props } = deps;
48
- store.setFormValues({ formValues: props.defaultValues });
129
+ initFormValues(store, props);
49
130
  };
50
131
 
51
132
  export const handleAfterMount = (deps) => {
52
133
  const { props, refs, render } = deps;
53
- const { form = {}, defaultValues } = props;
54
- updateAttributes({ form, defaultValues, refs });
134
+ const state = deps.store.getState();
135
+ const form = selectForm({ state, props });
136
+ updateFieldAttributes({
137
+ form,
138
+ formValues: state.formValues,
139
+ refs,
140
+ formDisabled: !!props?.disabled,
141
+ });
55
142
  render();
56
-
57
- // Auto-focus first input field if autofocus attribute is set
58
- if (props?.autofocus) {
59
- setTimeout(() => {
60
- autoFocusFirstInput(refs);
61
- }, 50);
62
- }
63
143
  };
64
144
 
65
145
  export const handleOnUpdate = (deps, payload) => {
66
- const { oldProps, newProps } = payload;
146
+ const { newProps } = payload;
67
147
  const { store, render, refs } = deps;
68
- const { form = {}, defaultValues } = newProps;
69
- if (oldProps?.key !== newProps?.key) {
70
- updateAttributes({ form, defaultValues, refs });
71
- store.setFormValues({ formValues: defaultValues });
72
- render();
148
+ const formDisabled = !!newProps?.disabled;
149
+
150
+ const state = store.getState();
151
+ pruneHiddenValues({ state, props: newProps });
152
+ const form = selectForm({ state, props: newProps });
153
+ updateFieldAttributes({
154
+ form,
155
+ formValues: state.formValues,
156
+ refs,
157
+ formDisabled,
158
+ });
159
+ render();
160
+ };
161
+
162
+ export const handleValueInput = (deps, payload) => {
163
+ const { store, dispatchEvent, render, props } = deps;
164
+ const event = payload._event;
165
+ const name = event.currentTarget.dataset.fieldName;
166
+ if (!name || !event.detail || !Object.prototype.hasOwnProperty.call(event.detail, "value")) {
73
167
  return;
74
168
  }
169
+
170
+ const value = event.detail.value;
171
+ store.setFormFieldValue({ name, value });
172
+
173
+ const state = store.getState();
174
+ pruneHiddenValues({ state, props });
175
+ const form = selectForm({ state, props });
176
+ const dataFields = collectAllDataFields(form.fields || []);
177
+ const field = dataFields.find((f) => f.name === name);
178
+
179
+ syncInteractiveFieldAttribute({
180
+ field,
181
+ target: event.currentTarget,
182
+ value,
183
+ });
184
+
185
+ // Reactive validation
186
+ if (state.reactiveMode) {
187
+ if (field) {
188
+ const error = validateField(field, value);
189
+ if (error) {
190
+ store.setErrors({ errors: { ...state.errors, [name]: error } });
191
+ } else {
192
+ store.clearFieldError({ name });
193
+ }
194
+ }
195
+ }
196
+
197
+ // Keep conditional fields and jempl-rendered content in sync while typing.
75
198
  render();
76
- };
77
199
 
78
- const dispatchFormChange = (name, fieldValue, formValues, dispatchEvent) => {
79
200
  dispatchEvent(
80
- new CustomEvent("form-change", {
201
+ new CustomEvent("form-input", {
202
+ bubbles: true,
81
203
  detail: {
82
204
  name,
83
- fieldValue,
84
- formValues,
205
+ value,
206
+ values: selectFormValues({ state: store.getState(), props }),
85
207
  },
86
208
  }),
87
209
  );
88
210
  };
89
211
 
90
- export const handleActionClick = (deps, payload) => {
91
- const { store, dispatchEvent } = deps;
212
+ export const handleValueChange = (deps, payload) => {
213
+ const { store, dispatchEvent, render, props } = deps;
92
214
  const event = payload._event;
93
- const id = event.currentTarget.dataset.actionId || event.currentTarget.id.slice("action".length);
215
+ const name = event.currentTarget.dataset.fieldName;
216
+ if (!name || !event.detail || !Object.prototype.hasOwnProperty.call(event.detail, "value")) {
217
+ return;
218
+ }
219
+
220
+ const value = event.detail.value;
221
+ store.setFormFieldValue({ name, value });
222
+
223
+ const state = store.getState();
224
+ pruneHiddenValues({ state, props });
225
+ const form = selectForm({ state, props });
226
+ const dataFields = collectAllDataFields(form.fields || []);
227
+ const field = dataFields.find((f) => f.name === name);
228
+
229
+ syncInteractiveFieldAttribute({
230
+ field,
231
+ target: event.currentTarget,
232
+ value,
233
+ });
234
+
235
+ // Reactive validation
236
+ if (state.reactiveMode) {
237
+ if (field) {
238
+ const error = validateField(field, value);
239
+ if (error) {
240
+ store.setErrors({ errors: { ...state.errors, [name]: error } });
241
+ } else {
242
+ store.clearFieldError({ name });
243
+ }
244
+ }
245
+ }
246
+
247
+ // Re-render on committed changes so controlled child components stay synchronized.
248
+ render();
249
+
94
250
  dispatchEvent(
95
- new CustomEvent("action-click", {
251
+ new CustomEvent("form-change", {
252
+ bubbles: true,
96
253
  detail: {
97
- actionId: id,
98
- formValues: store.selectFormValues(),
254
+ name,
255
+ value,
256
+ values: selectFormValues({ state: store.getState(), props }),
99
257
  },
100
258
  }),
101
259
  );
102
260
  };
103
261
 
104
- export const handleInputChange = (deps, payload) => {
105
- const { store, dispatchEvent } = deps;
262
+ export const handleActionClick = (deps, payload) => {
263
+ const { store, dispatchEvent, render, props } = deps;
106
264
  const event = payload._event;
107
- let name = event.currentTarget.dataset.fieldName || event.currentTarget.id.slice("field".length);
108
- if (name && event.detail && Object.prototype.hasOwnProperty.call(event.detail, "value")) {
109
- const value = event.detail.value
110
- store.setFormFieldValue({
111
- name: name,
112
- value,
113
- });
114
- dispatchFormChange(
115
- name,
116
- value,
117
- store.selectFormValues(),
118
- dispatchEvent,
265
+ const actionId = event.currentTarget.dataset.actionId;
266
+ if (!actionId) return;
267
+
268
+ const state = store.getState();
269
+ const form = selectForm({ state, props });
270
+ const actions = form.actions || {};
271
+ const buttons = actions.buttons || [];
272
+ const button = buttons.find((b) => b.id === actionId);
273
+
274
+ const values = selectFormValues({ state, props });
275
+
276
+ if (button && button.validate) {
277
+ const dataFields = collectAllDataFields(form.fields || []);
278
+ const { valid, errors } = validateForm(dataFields, state.formValues);
279
+ store.setErrors({ errors });
280
+ if (!valid) {
281
+ store.setReactiveMode();
282
+ }
283
+ render();
284
+
285
+ dispatchEvent(
286
+ new CustomEvent("form-action", {
287
+ bubbles: true,
288
+ detail: {
289
+ actionId,
290
+ values,
291
+ valid,
292
+ errors,
293
+ },
294
+ }),
295
+ );
296
+ } else {
297
+ dispatchEvent(
298
+ new CustomEvent("form-action", {
299
+ bubbles: true,
300
+ detail: {
301
+ actionId,
302
+ values,
303
+ },
304
+ }),
119
305
  );
120
306
  }
121
307
  };
122
308
 
123
-
124
309
  export const handleImageClick = (deps, payload) => {
125
310
  const event = payload._event;
126
311
  if (event.type === "contextmenu") {
127
312
  event.preventDefault();
128
313
  }
129
- const { dispatchEvent } = deps;
130
- const name = event.currentTarget.dataset.fieldName || event.currentTarget.id.slice("image".length);
314
+ const { store, dispatchEvent, props } = deps;
315
+ const name = event.currentTarget.dataset.fieldName;
316
+
131
317
  dispatchEvent(
132
- new CustomEvent("extra-event", {
318
+ new CustomEvent("form-field-event", {
319
+ bubbles: true,
133
320
  detail: {
134
- name: name,
135
- x: event.clientX,
136
- y: event.clientY,
137
- trigger: event.type,
321
+ name,
322
+ event: event.type,
323
+ values: selectFormValues({ state: store.getState(), props }),
138
324
  },
139
325
  }),
140
326
  );
141
327
  };
142
328
 
143
- export const handleWaveformClick = (deps, payload) => {
329
+ export const handleKeyDown = (deps, payload) => {
330
+ const { store, dispatchEvent, render, props } = deps;
144
331
  const event = payload._event;
145
- if (event.type === "contextmenu") {
332
+
333
+ if (event.key === "Enter" && !event.shiftKey) {
334
+ const target = event.target;
335
+ if (target.tagName === "TEXTAREA" || target.tagName === "RTGL-TEXTAREA") {
336
+ return;
337
+ }
338
+
146
339
  event.preventDefault();
147
- }
148
- const { dispatchEvent } = deps;
149
- const name = event.currentTarget.dataset.fieldName || event.currentTarget.id.slice("waveform".length);
150
- dispatchEvent(
151
- new CustomEvent("extra-event", {
152
- detail: {
153
- name: name,
154
- x: event.clientX,
155
- y: event.clientY,
156
- trigger: event.type,
157
- },
158
- }),
159
- );
160
- };
161
340
 
162
- export const handleSelectAddOption = (deps, payload) => {
163
- const { store, dispatchEvent } = deps;
164
- const event = payload._event;
165
- const name = event.currentTarget.dataset.fieldName || event.currentTarget.id.slice("field".length);
166
- dispatchEvent(
167
- new CustomEvent("action-click", {
168
- detail: {
169
- actionId: 'select-options-add',
170
- name: name,
171
- formValues: store.selectFormValues(),
172
- },
173
- }),
174
- );
341
+ const state = store.getState();
342
+ const form = selectForm({ state, props });
343
+ const actions = form.actions || {};
344
+ const buttons = actions.buttons || [];
345
+
346
+ // Find the first button with validate: true, or the first button
347
+ const validateButton = buttons.find((b) => b.validate);
348
+ const targetButton = validateButton || buttons[0];
349
+
350
+ if (!targetButton) return;
351
+
352
+ const values = selectFormValues({ state, props });
353
+
354
+ if (targetButton.validate) {
355
+ const dataFields = collectAllDataFields(form.fields || []);
356
+ const { valid, errors } = validateForm(dataFields, state.formValues);
357
+ store.setErrors({ errors });
358
+ if (!valid) {
359
+ store.setReactiveMode();
360
+ }
361
+ render();
362
+
363
+ dispatchEvent(
364
+ new CustomEvent("form-action", {
365
+ bubbles: true,
366
+ detail: {
367
+ actionId: targetButton.id,
368
+ values,
369
+ valid,
370
+ errors,
371
+ },
372
+ }),
373
+ );
374
+ } else {
375
+ dispatchEvent(
376
+ new CustomEvent("form-action", {
377
+ bubbles: true,
378
+ detail: {
379
+ actionId: targetButton.id,
380
+ values,
381
+ },
382
+ }),
383
+ );
384
+ }
385
+ }
175
386
  };
176
387
 
177
388
  export const handleTooltipMouseEnter = (deps, payload) => {
178
389
  const { store, render, props } = deps;
179
390
  const event = payload._event;
180
- const fieldName = event.currentTarget.dataset.fieldName || event.currentTarget.id.slice('tooltipIcon'.length);
391
+ const fieldName = event.currentTarget.dataset.fieldName;
181
392
 
182
- // Find the field with matching name to get tooltip content
183
- const form = props.form;
184
- const field = form.fields.find(f => f.name === fieldName);
393
+ const form = selectForm({ state: store.getState(), props });
394
+ const allFields = collectAllDataFields(form.fields || []);
395
+ const field = allFields.find((f) => f.name === fieldName);
185
396
 
186
397
  if (field && field.tooltip) {
187
398
  const rect = event.currentTarget.getBoundingClientRect();
188
399
  store.showTooltip({
189
400
  x: rect.left + rect.width / 2,
190
401
  y: rect.top - 8,
191
- content: field.tooltip.content
402
+ content: typeof field.tooltip === "string" ? field.tooltip : field.tooltip.content || "",
192
403
  });
193
404
  render();
194
405
  }
@@ -199,38 +410,3 @@ export const handleTooltipMouseLeave = (deps) => {
199
410
  store.hideTooltip({});
200
411
  render();
201
412
  };
202
-
203
- export const handleKeyDown = (deps, payload) => {
204
- const { store, dispatchEvent, props } = deps;
205
- const event = payload._event;
206
-
207
- // Handle Enter key to submit form
208
- if (event.key === 'Enter' && !event.shiftKey) {
209
- const target = event.target;
210
- // Don't submit if we're in a textarea (native or custom component)
211
- if (target.tagName === 'TEXTAREA' || target.tagName === 'RTGL-TEXTAREA') {
212
- return;
213
- }
214
-
215
- event.preventDefault();
216
-
217
- // Dispatch action-click event for the first button
218
- const form = props.form || {};
219
- const actions = form.actions || {};
220
- const buttons = actions.buttons || [];
221
-
222
- if (buttons.length > 0) {
223
- const firstButtonId = buttons[0].id;
224
- const formValues = store.selectFormValues();
225
-
226
- dispatchEvent(
227
- new CustomEvent("action-click", {
228
- detail: {
229
- actionId: firstButtonId,
230
- formValues: formValues,
231
- },
232
- }),
233
- );
234
- }
235
- }
236
- };