@pure-ds/core 0.3.18 → 0.4.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/custom-elements.json +12 -9
- package/dist/types/pds.config.d.ts +4 -8
- package/dist/types/pds.config.d.ts.map +1 -1
- package/dist/types/public/assets/js/pds.d.ts +3 -3
- package/dist/types/public/assets/js/pds.d.ts.map +1 -1
- package/dist/types/public/assets/pds/components/pds-jsonform.d.ts +1 -0
- package/dist/types/public/assets/pds/components/pds-jsonform.d.ts.map +1 -1
- package/dist/types/public/assets/pds/components/pds-tabstrip.d.ts +36 -5
- package/dist/types/public/assets/pds/components/pds-tabstrip.d.ts.map +1 -1
- package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
- package/dist/types/src/js/pds-core/pds-ontology.d.ts +186 -12
- package/dist/types/src/js/pds-core/pds-ontology.d.ts.map +1 -1
- package/dist/types/src/js/pds-core/pds-query.d.ts.map +1 -1
- package/package.json +1 -1
- package/public/assets/js/app.js +304 -213
- package/public/assets/js/pds.js +172 -81
- package/public/assets/pds/components/pds-jsonform.js +406 -297
- package/public/assets/pds/components/pds-tabstrip.js +36 -5
- package/public/assets/pds/custom-elements.json +12 -9
- package/public/assets/pds/pds-css-complete.json +1 -1
- package/public/assets/pds/vscode-custom-data.json +4 -4
- package/src/js/pds-core/pds-generator.js +148 -57
- package/src/js/pds-core/pds-ontology.js +803 -256
- package/src/js/pds-core/pds-query.js +582 -571
|
@@ -11,21 +11,21 @@ function getStep(value) {
|
|
|
11
11
|
// Default options for pds-jsonform
|
|
12
12
|
const DEFAULT_OPTIONS = {
|
|
13
13
|
widgets: {
|
|
14
|
-
booleans: "toggle",
|
|
15
|
-
numbers: "input",
|
|
16
|
-
selects: "standard",
|
|
14
|
+
booleans: "toggle", // 'toggle' | 'checkbox'
|
|
15
|
+
numbers: "input", // 'input' | 'range'
|
|
16
|
+
selects: "standard", // 'standard' | 'dropdown'
|
|
17
17
|
},
|
|
18
18
|
layouts: {
|
|
19
|
-
fieldsets: "default",
|
|
20
|
-
arrays: "default",
|
|
19
|
+
fieldsets: "default", // 'default' | 'flex' | 'grid' | 'accordion' | 'tabs' | 'card'
|
|
20
|
+
arrays: "default", // 'default' | 'open' | 'compact'
|
|
21
21
|
},
|
|
22
22
|
enhancements: {
|
|
23
|
-
icons: true,
|
|
24
|
-
datalists: true,
|
|
25
|
-
rangeOutput: true,
|
|
23
|
+
icons: true, // Enable icon-enhanced inputs
|
|
24
|
+
datalists: true, // Enable datalist autocomplete
|
|
25
|
+
rangeOutput: true, // Use .range-output for ranges
|
|
26
26
|
},
|
|
27
27
|
validation: {
|
|
28
|
-
showErrors: true,
|
|
28
|
+
showErrors: true, // Show validation errors inline
|
|
29
29
|
validateOnChange: false, // Validate on every change vs on submit
|
|
30
30
|
},
|
|
31
31
|
};
|
|
@@ -88,6 +88,7 @@ export class SchemaForm extends LitElement {
|
|
|
88
88
|
#data = {};
|
|
89
89
|
#idBase = `sf-${Math.random().toString(36).slice(2)}`;
|
|
90
90
|
#mergedOptions = null;
|
|
91
|
+
#slottedActions = [];
|
|
91
92
|
|
|
92
93
|
constructor() {
|
|
93
94
|
super();
|
|
@@ -102,12 +103,12 @@ export class SchemaForm extends LitElement {
|
|
|
102
103
|
this.hideReset = false;
|
|
103
104
|
this.hideLegend = false;
|
|
104
105
|
this.#installDefaultRenderers();
|
|
105
|
-
|
|
106
|
+
|
|
106
107
|
// Handle submit button clicks in slotted actions
|
|
107
|
-
this.addEventListener(
|
|
108
|
+
this.addEventListener("click", (e) => {
|
|
108
109
|
const button = e.target.closest('button[type="submit"]');
|
|
109
110
|
if (button && this.contains(button)) {
|
|
110
|
-
const form = this.querySelector(
|
|
111
|
+
const form = this.querySelector("form");
|
|
111
112
|
if (form) {
|
|
112
113
|
e.preventDefault();
|
|
113
114
|
form.requestSubmit();
|
|
@@ -123,18 +124,21 @@ export class SchemaForm extends LitElement {
|
|
|
123
124
|
useValidator(fn) {
|
|
124
125
|
this.#validator = fn;
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
+
|
|
127
128
|
// Get values in flat JSON Pointer format
|
|
128
129
|
getValuesFlat() {
|
|
129
130
|
return this.#flattenToPointers(this.#data);
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
+
|
|
132
133
|
#flattenToPointers(obj, prefix = "") {
|
|
133
134
|
const flattened = {};
|
|
134
135
|
for (const [key, value] of Object.entries(obj)) {
|
|
135
136
|
const jsonPointerPath = prefix ? `${prefix}/${key}` : `/${key}`;
|
|
136
137
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
137
|
-
Object.assign(
|
|
138
|
+
Object.assign(
|
|
139
|
+
flattened,
|
|
140
|
+
this.#flattenToPointers(value, jsonPointerPath)
|
|
141
|
+
);
|
|
138
142
|
} else {
|
|
139
143
|
flattened[jsonPointerPath] = value;
|
|
140
144
|
}
|
|
@@ -152,13 +156,21 @@ export class SchemaForm extends LitElement {
|
|
|
152
156
|
return this.#onSubmit(new Event("submit", { cancelable: true }));
|
|
153
157
|
}
|
|
154
158
|
|
|
159
|
+
connectedCallback() {
|
|
160
|
+
super.connectedCallback();
|
|
161
|
+
// Capture slotted actions before first render (Light DOM doesn't support native slots)
|
|
162
|
+
this.#slottedActions = Array.from(
|
|
163
|
+
this.querySelectorAll('[slot="actions"]')
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
155
167
|
// ===== Lit lifecycle =====
|
|
156
168
|
willUpdate(changed) {
|
|
157
169
|
// Merge options when options or jsonSchema changes
|
|
158
170
|
if (changed.has("options") || changed.has("jsonSchema")) {
|
|
159
171
|
this.#mergeOptions();
|
|
160
172
|
}
|
|
161
|
-
|
|
173
|
+
|
|
162
174
|
if (changed.has("jsonSchema")) this.#compile();
|
|
163
175
|
if (changed.has("uiSchema")) this.requestUpdate();
|
|
164
176
|
if (changed.has("values")) {
|
|
@@ -169,7 +181,7 @@ export class SchemaForm extends LitElement {
|
|
|
169
181
|
} else {
|
|
170
182
|
const newData = {};
|
|
171
183
|
for (const [key, value] of Object.entries(v)) {
|
|
172
|
-
if (key.startsWith(
|
|
184
|
+
if (key.startsWith("/")) {
|
|
173
185
|
this.#setByPath(newData, key, value);
|
|
174
186
|
} else {
|
|
175
187
|
newData[key] = value;
|
|
@@ -183,34 +195,37 @@ export class SchemaForm extends LitElement {
|
|
|
183
195
|
#mergeOptions() {
|
|
184
196
|
// Start with default options
|
|
185
197
|
let merged = { ...DEFAULT_OPTIONS };
|
|
186
|
-
|
|
198
|
+
|
|
187
199
|
// Try to get preset options from window.PDS if available
|
|
188
200
|
if (typeof window !== "undefined" && window.PDS?.config?.form?.options) {
|
|
189
|
-
merged = window.PDS.common.deepMerge(
|
|
201
|
+
merged = window.PDS.common.deepMerge(
|
|
202
|
+
merged,
|
|
203
|
+
window.PDS.config.form.options
|
|
204
|
+
);
|
|
190
205
|
}
|
|
191
|
-
|
|
206
|
+
|
|
192
207
|
// Merge instance options
|
|
193
208
|
if (this.options) {
|
|
194
209
|
merged = window.PDS.common.deepMerge(merged, this.options);
|
|
195
210
|
}
|
|
196
|
-
|
|
211
|
+
|
|
197
212
|
this.#mergedOptions = merged;
|
|
198
213
|
}
|
|
199
214
|
|
|
200
215
|
#getOption(path, defaultValue) {
|
|
201
216
|
if (!this.#mergedOptions) this.#mergeOptions();
|
|
202
|
-
|
|
217
|
+
|
|
203
218
|
// Support path-based options like '/address/zip': { ... }
|
|
204
|
-
if (path.startsWith(
|
|
219
|
+
if (path.startsWith("/")) {
|
|
205
220
|
const pathOptions = this.#mergedOptions[path];
|
|
206
221
|
if (pathOptions !== undefined) return pathOptions;
|
|
207
222
|
}
|
|
208
|
-
|
|
223
|
+
|
|
209
224
|
// Support nested option paths like 'widgets.booleans'
|
|
210
|
-
const parts = path.split(
|
|
225
|
+
const parts = path.split(".");
|
|
211
226
|
let current = this.#mergedOptions;
|
|
212
227
|
for (const part of parts) {
|
|
213
|
-
if (current && typeof current ===
|
|
228
|
+
if (current && typeof current === "object" && part in current) {
|
|
214
229
|
current = current[part];
|
|
215
230
|
} else {
|
|
216
231
|
return defaultValue;
|
|
@@ -260,7 +275,7 @@ export class SchemaForm extends LitElement {
|
|
|
260
275
|
if (ui?.["ui:dialog"]) {
|
|
261
276
|
return { kind: "dialog", path, title, schema, ui };
|
|
262
277
|
}
|
|
263
|
-
|
|
278
|
+
|
|
264
279
|
const order = this.#propertyOrder(schema, ui);
|
|
265
280
|
const children = order.map((key) => {
|
|
266
281
|
const childPath = path + "/" + this.#escapeJsonPointer(key);
|
|
@@ -326,7 +341,7 @@ export class SchemaForm extends LitElement {
|
|
|
326
341
|
if (schema.const !== undefined) return "const";
|
|
327
342
|
if (schema.type === "string") {
|
|
328
343
|
// Check for binary/upload content via JSON Schema contentMediaType
|
|
329
|
-
if (schema.contentMediaType || schema.contentEncoding ===
|
|
344
|
+
if (schema.contentMediaType || schema.contentEncoding === "base64") {
|
|
330
345
|
return "upload";
|
|
331
346
|
}
|
|
332
347
|
switch (schema.format) {
|
|
@@ -354,24 +369,23 @@ export class SchemaForm extends LitElement {
|
|
|
354
369
|
case "date-time":
|
|
355
370
|
return "input-datetime";
|
|
356
371
|
default:
|
|
357
|
-
if (
|
|
358
|
-
(schema.maxLength ?? 0) > 160 ||
|
|
359
|
-
ui?.["ui:widget"] === "textarea"
|
|
360
|
-
)
|
|
372
|
+
if ((schema.maxLength ?? 0) > 160 || ui?.["ui:widget"] === "textarea")
|
|
361
373
|
return "textarea";
|
|
362
374
|
return "input-text";
|
|
363
375
|
}
|
|
364
376
|
}
|
|
365
377
|
if (schema.type === "number" || schema.type === "integer") {
|
|
366
378
|
// Check if range widget should be used
|
|
367
|
-
const useRange =
|
|
368
|
-
|
|
369
|
-
|
|
379
|
+
const useRange =
|
|
380
|
+
this.#getOption("widgets.numbers", "input") === "range" ||
|
|
381
|
+
ui?.["ui:widget"] === "range" ||
|
|
382
|
+
ui?.["ui:widget"] === "input-range";
|
|
370
383
|
return useRange ? "input-range" : "input-number";
|
|
371
384
|
}
|
|
372
385
|
if (schema.type === "boolean") {
|
|
373
386
|
// Check if toggle should be used
|
|
374
|
-
const useToggle =
|
|
387
|
+
const useToggle =
|
|
388
|
+
this.#getOption("widgets.booleans", "toggle") === "toggle";
|
|
375
389
|
return useToggle ? "toggle" : "checkbox";
|
|
376
390
|
}
|
|
377
391
|
return "input-text";
|
|
@@ -425,24 +439,21 @@ export class SchemaForm extends LitElement {
|
|
|
425
439
|
${tree ? this.#renderNode(tree) : html`<slot></slot>`}
|
|
426
440
|
${!this.hideActions
|
|
427
441
|
? html`
|
|
428
|
-
<div
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
${this.submitLabel}
|
|
435
|
-
</button>` : nothing}
|
|
436
|
-
|
|
442
|
+
<div class="form-actions">
|
|
443
|
+
${!this.hideSubmit
|
|
444
|
+
? html` <button type="submit" class="btn btn-primary">
|
|
445
|
+
${this.submitLabel}
|
|
446
|
+
</button>`
|
|
447
|
+
: nothing}
|
|
437
448
|
${!this.hideReset
|
|
438
449
|
? html`<button type="reset" class="btn">
|
|
439
450
|
${this.resetLabel}
|
|
440
451
|
</button>`
|
|
441
452
|
: nothing}
|
|
442
|
-
|
|
453
|
+
${this.#slottedActions}
|
|
443
454
|
</div>
|
|
444
455
|
`
|
|
445
|
-
: html`<
|
|
456
|
+
: html`<div class="form-actions">${this.#slottedActions}</div>`}
|
|
446
457
|
</form>
|
|
447
458
|
`;
|
|
448
459
|
}
|
|
@@ -467,39 +478,45 @@ export class SchemaForm extends LitElement {
|
|
|
467
478
|
#renderFieldset(node, context = {}) {
|
|
468
479
|
const legend = node.title ?? "Section";
|
|
469
480
|
const ui = node.ui || this.#uiFor(node.path);
|
|
470
|
-
|
|
481
|
+
|
|
471
482
|
// Check for path-specific options
|
|
472
483
|
const pathOptions = this.#getOption(node.path, {});
|
|
473
|
-
|
|
484
|
+
|
|
474
485
|
// Determine layout mode
|
|
475
|
-
const layout =
|
|
476
|
-
|
|
477
|
-
|
|
486
|
+
const layout =
|
|
487
|
+
ui?.["ui:layout"] ||
|
|
488
|
+
pathOptions.layout ||
|
|
489
|
+
this.#getOption("layouts.fieldsets", "default");
|
|
490
|
+
|
|
478
491
|
// Check for tabs layout
|
|
479
492
|
if (layout === "tabs" || ui?.["ui:tabs"]) {
|
|
480
493
|
return this.#renderFieldsetTabs(node, legend, ui);
|
|
481
494
|
}
|
|
482
|
-
|
|
495
|
+
|
|
483
496
|
// Check for accordion layout
|
|
484
497
|
if (layout === "accordion" || ui?.["ui:accordion"]) {
|
|
485
498
|
return this.#renderFieldsetAccordion(node, legend, ui);
|
|
486
499
|
}
|
|
487
|
-
|
|
500
|
+
|
|
488
501
|
// Check for surface wrapping
|
|
489
502
|
const surface = ui?.["ui:surface"] || pathOptions.surface;
|
|
490
|
-
|
|
503
|
+
|
|
491
504
|
// Build layout classes and inline styles
|
|
492
505
|
const layoutClasses = [];
|
|
493
506
|
let layoutStyle = "";
|
|
494
507
|
const layoutOptions = ui?.["ui:layoutOptions"] || {};
|
|
495
|
-
|
|
508
|
+
|
|
496
509
|
if (layout === "flex") {
|
|
497
510
|
layoutClasses.push("flex");
|
|
498
511
|
if (layoutOptions.wrap) layoutClasses.push("flex-wrap");
|
|
499
512
|
if (layoutOptions.direction === "column") layoutClasses.push("flex-col");
|
|
500
513
|
if (layoutOptions.gap) {
|
|
501
514
|
// Check if gap is a CSS class name (e.g., 'md', 'lg') or a CSS value
|
|
502
|
-
if (
|
|
515
|
+
if (
|
|
516
|
+
layoutOptions.gap.startsWith("var(") ||
|
|
517
|
+
layoutOptions.gap.includes("px") ||
|
|
518
|
+
layoutOptions.gap.includes("rem")
|
|
519
|
+
) {
|
|
503
520
|
layoutStyle += `gap: ${layoutOptions.gap};`;
|
|
504
521
|
} else {
|
|
505
522
|
layoutClasses.push(`gap-${layoutOptions.gap}`);
|
|
@@ -516,45 +533,60 @@ export class SchemaForm extends LitElement {
|
|
|
516
533
|
}
|
|
517
534
|
if (layoutOptions.gap) {
|
|
518
535
|
// Check if gap is a CSS class name (e.g., 'md', 'lg') or a CSS value
|
|
519
|
-
if (
|
|
536
|
+
if (
|
|
537
|
+
layoutOptions.gap.startsWith("var(") ||
|
|
538
|
+
layoutOptions.gap.includes("px") ||
|
|
539
|
+
layoutOptions.gap.includes("rem")
|
|
540
|
+
) {
|
|
520
541
|
layoutStyle += `gap: ${layoutOptions.gap};`;
|
|
521
542
|
} else {
|
|
522
543
|
layoutClasses.push(`gap-${layoutOptions.gap}`);
|
|
523
544
|
}
|
|
524
545
|
}
|
|
525
546
|
}
|
|
526
|
-
|
|
527
|
-
const fieldsetClass =
|
|
528
|
-
|
|
547
|
+
|
|
548
|
+
const fieldsetClass =
|
|
549
|
+
layoutClasses.length > 0 ? layoutClasses.join(" ") : undefined;
|
|
550
|
+
|
|
529
551
|
// Render basic fieldset
|
|
530
552
|
const fieldsetContent = html`
|
|
531
|
-
<fieldset
|
|
532
|
-
|
|
553
|
+
<fieldset
|
|
554
|
+
data-path=${node.path}
|
|
555
|
+
class=${ifDefined(fieldsetClass)}
|
|
556
|
+
style=${ifDefined(layoutStyle || undefined)}
|
|
557
|
+
>
|
|
558
|
+
${!this.hideLegend && !context.hideLegend
|
|
559
|
+
? html`<legend>${legend}</legend>`
|
|
560
|
+
: nothing}
|
|
533
561
|
${node.children.map((child) => this.#renderNode(child, context))}
|
|
534
562
|
</fieldset>
|
|
535
563
|
`;
|
|
536
|
-
|
|
564
|
+
|
|
537
565
|
// Wrap in surface if specified
|
|
538
566
|
if (surface) {
|
|
539
|
-
const surfaceClass =
|
|
540
|
-
|
|
541
|
-
|
|
567
|
+
const surfaceClass =
|
|
568
|
+
surface === "card" || surface === "elevated" || surface === "dialog"
|
|
569
|
+
? `surface ${surface}`
|
|
570
|
+
: "surface";
|
|
542
571
|
return html`<div class=${surfaceClass}>${fieldsetContent}</div>`;
|
|
543
572
|
}
|
|
544
|
-
|
|
573
|
+
|
|
545
574
|
return fieldsetContent;
|
|
546
575
|
}
|
|
547
576
|
|
|
548
577
|
#renderFieldsetTabs(node, legend, ui) {
|
|
549
578
|
const children = node.children || [];
|
|
550
579
|
if (children.length === 0) return nothing;
|
|
551
|
-
|
|
580
|
+
|
|
552
581
|
// Create tab panels from child fields
|
|
553
582
|
return html`
|
|
554
583
|
<pds-tabstrip label=${legend} data-path=${node.path}>
|
|
555
584
|
${children.map((child, idx) => {
|
|
556
585
|
const childTitle = child.title ?? `Tab ${idx + 1}`;
|
|
557
|
-
const childId = `${node.path}-tab-${idx}`.replace(
|
|
586
|
+
const childId = `${node.path}-tab-${idx}`.replace(
|
|
587
|
+
/[^a-zA-Z0-9_-]/g,
|
|
588
|
+
"-"
|
|
589
|
+
);
|
|
558
590
|
return html`
|
|
559
591
|
<pds-tabpanel id=${childId} label=${childTitle}>
|
|
560
592
|
${this.#renderNode(child)}
|
|
@@ -570,13 +602,17 @@ export class SchemaForm extends LitElement {
|
|
|
570
602
|
if (children.length === 0) return nothing;
|
|
571
603
|
const layoutOptions = ui?.["ui:layoutOptions"] || {};
|
|
572
604
|
const openFirst = layoutOptions.openFirst ?? true;
|
|
573
|
-
|
|
605
|
+
|
|
574
606
|
return html`
|
|
575
607
|
<section class="accordion" data-path=${node.path}>
|
|
576
608
|
${children.map((child, idx) => {
|
|
577
609
|
const childTitle = child.title ?? `Section ${idx + 1}`;
|
|
578
|
-
const childId = `${node.path}-acc-${idx}`.replace(
|
|
579
|
-
|
|
610
|
+
const childId = `${node.path}-acc-${idx}`.replace(
|
|
611
|
+
/[^a-zA-Z0-9_-]/g,
|
|
612
|
+
"-"
|
|
613
|
+
);
|
|
614
|
+
const isOpen =
|
|
615
|
+
ui?.["ui:defaultOpen"]?.includes(idx) ?? (openFirst && idx === 0);
|
|
580
616
|
return html`
|
|
581
617
|
<details ?open=${isOpen}>
|
|
582
618
|
<summary id=${childId}>${childTitle}</summary>
|
|
@@ -595,22 +631,27 @@ export class SchemaForm extends LitElement {
|
|
|
595
631
|
const title = node.title ?? "Edit";
|
|
596
632
|
const ui = node.ui || this.#uiFor(path);
|
|
597
633
|
const dialogOpts = ui?.["ui:dialogOptions"] || {};
|
|
598
|
-
const buttonLabel =
|
|
634
|
+
const buttonLabel =
|
|
635
|
+
dialogOpts.buttonLabel || ui?.["ui:dialogButton"] || `Edit ${title}`;
|
|
599
636
|
const dialogTitle = dialogOpts.dialogTitle || title;
|
|
600
|
-
|
|
637
|
+
|
|
601
638
|
const openDialog = async () => {
|
|
602
639
|
// Read current value from this.#data on each open (not captured at render time)
|
|
603
640
|
const currentValue = this.#getByPath(this.#data, path) || {};
|
|
604
|
-
|
|
605
|
-
console.log(
|
|
606
|
-
console.log(
|
|
607
|
-
console.log(
|
|
608
|
-
|
|
609
|
-
this.#emit("pw:dialog-open", {
|
|
610
|
-
|
|
641
|
+
|
|
642
|
+
console.log("Opening dialog for path:", path);
|
|
643
|
+
console.log("Current this.#data:", this.#data);
|
|
644
|
+
console.log("Current value at path:", currentValue);
|
|
645
|
+
|
|
646
|
+
this.#emit("pw:dialog-open", {
|
|
647
|
+
path,
|
|
648
|
+
schema: node.schema,
|
|
649
|
+
value: currentValue,
|
|
650
|
+
});
|
|
651
|
+
|
|
611
652
|
// Create a nested form schema for the dialog
|
|
612
653
|
const dialogSchema = { ...node.schema, title: dialogTitle };
|
|
613
|
-
|
|
654
|
+
|
|
614
655
|
try {
|
|
615
656
|
// Use PDS.ask to show dialog with form - it returns FormData when useForm: true
|
|
616
657
|
const formData = await window.PDS.ask(
|
|
@@ -629,27 +670,40 @@ export class SchemaForm extends LitElement {
|
|
|
629
670
|
size: dialogOpts.size || "lg",
|
|
630
671
|
buttons: {
|
|
631
672
|
ok: { name: dialogOpts.submitLabel || "Save", primary: true },
|
|
632
|
-
cancel: {
|
|
633
|
-
|
|
673
|
+
cancel: {
|
|
674
|
+
name: dialogOpts.cancelLabel || "Cancel",
|
|
675
|
+
cancel: true,
|
|
676
|
+
},
|
|
677
|
+
},
|
|
634
678
|
}
|
|
635
679
|
);
|
|
636
|
-
|
|
680
|
+
|
|
637
681
|
// formData is a FormData object if user clicked OK, null/false if cancelled
|
|
638
682
|
if (formData && formData instanceof FormData) {
|
|
639
683
|
// Convert FormData to nested object structure
|
|
640
684
|
// Note: The nested form generates paths from its own root (e.g., /name, /email)
|
|
641
685
|
// so we don't need to strip a basePath prefix
|
|
642
|
-
const updatedValue = this.#formDataToObject(
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
686
|
+
const updatedValue = this.#formDataToObject(
|
|
687
|
+
formData,
|
|
688
|
+
"",
|
|
689
|
+
dialogSchema
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
console.log("Updating path:", path, "with value:", updatedValue);
|
|
693
|
+
console.log(
|
|
694
|
+
"Before update - this.#data:",
|
|
695
|
+
structuredClone(this.#data)
|
|
696
|
+
);
|
|
697
|
+
|
|
647
698
|
// Update the data at the dialog's path
|
|
648
699
|
this.#setByPath(this.#data, path, updatedValue);
|
|
649
|
-
|
|
650
|
-
console.log(
|
|
651
|
-
|
|
652
|
-
|
|
700
|
+
|
|
701
|
+
console.log(
|
|
702
|
+
"After update - this.#data:",
|
|
703
|
+
structuredClone(this.#data)
|
|
704
|
+
);
|
|
705
|
+
console.log("Verify read back:", this.#getByPath(this.#data, path));
|
|
706
|
+
|
|
653
707
|
this.requestUpdate();
|
|
654
708
|
this.#emit("pw:dialog-submit", { path, value: updatedValue });
|
|
655
709
|
}
|
|
@@ -657,37 +711,43 @@ export class SchemaForm extends LitElement {
|
|
|
657
711
|
console.error("Dialog error:", err);
|
|
658
712
|
}
|
|
659
713
|
};
|
|
660
|
-
|
|
714
|
+
|
|
661
715
|
const buttonIcon = dialogOpts.icon;
|
|
662
|
-
|
|
716
|
+
|
|
663
717
|
return html`
|
|
664
718
|
<div class="dialog-field" data-path=${path}>
|
|
665
719
|
<button type="button" class="btn" @click=${openDialog}>
|
|
666
|
-
${buttonIcon
|
|
720
|
+
${buttonIcon
|
|
721
|
+
? html`<pds-icon icon=${buttonIcon}></pds-icon>`
|
|
722
|
+
: nothing}
|
|
667
723
|
${buttonLabel}
|
|
668
724
|
</button>
|
|
669
|
-
<input
|
|
725
|
+
<input
|
|
726
|
+
type="hidden"
|
|
727
|
+
name=${path}
|
|
728
|
+
.value=${JSON.stringify(this.#getByPath(this.#data, path) || {})}
|
|
729
|
+
/>
|
|
670
730
|
</div>
|
|
671
731
|
`;
|
|
672
732
|
}
|
|
673
|
-
|
|
733
|
+
|
|
674
734
|
// Convert FormData to nested object, handling JSON pointer paths
|
|
675
735
|
#formDataToObject(formData, basePath = "", schema = null) {
|
|
676
736
|
const result = {};
|
|
677
737
|
const arrays = new Map(); // Track array values from checkbox groups
|
|
678
|
-
|
|
738
|
+
|
|
679
739
|
for (const [key, value] of formData.entries()) {
|
|
680
740
|
// Remove basePath prefix if present to get relative path
|
|
681
741
|
let relativePath = key;
|
|
682
742
|
if (basePath && key.startsWith(basePath)) {
|
|
683
743
|
relativePath = key.substring(basePath.length);
|
|
684
744
|
}
|
|
685
|
-
|
|
745
|
+
|
|
686
746
|
// Skip empty paths
|
|
687
747
|
if (!relativePath || relativePath === "/") continue;
|
|
688
|
-
|
|
748
|
+
|
|
689
749
|
// Handle array notation for checkbox groups (path[])
|
|
690
|
-
if (relativePath.endsWith(
|
|
750
|
+
if (relativePath.endsWith("[]")) {
|
|
691
751
|
const arrayPath = relativePath.slice(0, -2);
|
|
692
752
|
if (!arrays.has(arrayPath)) {
|
|
693
753
|
arrays.set(arrayPath, []);
|
|
@@ -695,52 +755,61 @@ export class SchemaForm extends LitElement {
|
|
|
695
755
|
arrays.get(arrayPath).push(value);
|
|
696
756
|
continue;
|
|
697
757
|
}
|
|
698
|
-
|
|
758
|
+
|
|
699
759
|
// Convert value based on schema type if available
|
|
700
760
|
let convertedValue = value;
|
|
701
|
-
const fieldSchema = schema
|
|
702
|
-
|
|
761
|
+
const fieldSchema = schema
|
|
762
|
+
? this.#schemaAtPath(schema, relativePath)
|
|
763
|
+
: this.#schemaAt(relativePath);
|
|
764
|
+
|
|
703
765
|
if (fieldSchema) {
|
|
704
|
-
if (fieldSchema.type ===
|
|
766
|
+
if (fieldSchema.type === "number") {
|
|
705
767
|
convertedValue = parseFloat(value);
|
|
706
|
-
} else if (fieldSchema.type ===
|
|
768
|
+
} else if (fieldSchema.type === "integer") {
|
|
707
769
|
convertedValue = parseInt(value, 10);
|
|
708
|
-
} else if (fieldSchema.type ===
|
|
770
|
+
} else if (fieldSchema.type === "boolean") {
|
|
709
771
|
// Checkbox inputs: if present in FormData, they're checked (true)
|
|
710
772
|
// If not present, they're unchecked (false) - handled below
|
|
711
|
-
convertedValue = value ===
|
|
773
|
+
convertedValue = value === "on" || value === "true" || value === true;
|
|
712
774
|
}
|
|
713
775
|
}
|
|
714
|
-
|
|
776
|
+
|
|
715
777
|
// Set value using JSON pointer path
|
|
716
778
|
this.#setByPath(result, relativePath, convertedValue);
|
|
717
779
|
}
|
|
718
|
-
|
|
780
|
+
|
|
719
781
|
// Add array values from checkbox groups
|
|
720
782
|
for (const [arrayPath, values] of arrays) {
|
|
721
783
|
this.#setByPath(result, arrayPath, values);
|
|
722
784
|
}
|
|
723
|
-
|
|
785
|
+
|
|
724
786
|
// Handle unchecked checkboxes - they won't be in FormData
|
|
725
787
|
// We need to set them to false based on schema
|
|
726
788
|
this.#ensureCheckboxDefaults(result, basePath, schema);
|
|
727
|
-
|
|
789
|
+
|
|
728
790
|
return result;
|
|
729
791
|
}
|
|
730
|
-
|
|
792
|
+
|
|
731
793
|
// Ensure boolean fields that weren't in FormData are set to false
|
|
732
794
|
#ensureCheckboxDefaults(obj, basePath = "", schemaRoot = null) {
|
|
733
|
-
const schema = schemaRoot
|
|
795
|
+
const schema = schemaRoot
|
|
796
|
+
? this.#schemaAtPath(schemaRoot, basePath)
|
|
797
|
+
: this.#schemaAt(basePath);
|
|
734
798
|
if (!schema) return;
|
|
735
|
-
|
|
736
|
-
if (schema.type ===
|
|
799
|
+
|
|
800
|
+
if (schema.type === "object" && schema.properties) {
|
|
737
801
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
738
802
|
const propPath = basePath + "/" + this.#escapeJsonPointer(key);
|
|
739
|
-
const relativePath = propPath.startsWith("/")
|
|
740
|
-
|
|
741
|
-
|
|
803
|
+
const relativePath = propPath.startsWith("/")
|
|
804
|
+
? propPath.substring(1)
|
|
805
|
+
: propPath;
|
|
806
|
+
|
|
807
|
+
if (
|
|
808
|
+
propSchema.type === "boolean" &&
|
|
809
|
+
this.#getByPath(obj, propPath) === undefined
|
|
810
|
+
) {
|
|
742
811
|
this.#setByPath(obj, propPath, false);
|
|
743
|
-
} else if (propSchema.type ===
|
|
812
|
+
} else if (propSchema.type === "object") {
|
|
744
813
|
this.#ensureCheckboxDefaults(obj, propPath, schemaRoot);
|
|
745
814
|
}
|
|
746
815
|
}
|
|
@@ -786,94 +855,122 @@ export class SchemaForm extends LitElement {
|
|
|
786
855
|
const arr = this.#ensureArrayAtPath(path);
|
|
787
856
|
const ui = node.ui || this.#uiFor(path);
|
|
788
857
|
const itemSchema = node.item?.schema;
|
|
789
|
-
|
|
858
|
+
|
|
790
859
|
// Check if this is a simple string array that should use the open group enhancement
|
|
791
|
-
const isSimpleStringArray =
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
860
|
+
const isSimpleStringArray =
|
|
861
|
+
itemSchema?.type === "string" &&
|
|
862
|
+
!itemSchema.format &&
|
|
863
|
+
!itemSchema.enum &&
|
|
864
|
+
(!itemSchema.maxLength || itemSchema.maxLength <= 100);
|
|
865
|
+
|
|
796
866
|
// Check layout preference: use 'open' for simple string arrays by default, or if explicitly set
|
|
797
867
|
let arrayLayout = ui?.["ui:arrayLayout"];
|
|
798
868
|
if (!arrayLayout) {
|
|
799
869
|
// If not explicitly set, use global option with smart default based on array type
|
|
800
|
-
const globalDefault = this.#getOption(
|
|
801
|
-
arrayLayout =
|
|
870
|
+
const globalDefault = this.#getOption("layouts.arrays", "default");
|
|
871
|
+
arrayLayout =
|
|
872
|
+
globalDefault === "default" && isSimpleStringArray
|
|
873
|
+
? "open"
|
|
874
|
+
: globalDefault;
|
|
802
875
|
}
|
|
803
|
-
|
|
804
|
-
const useOpenGroup = arrayLayout ===
|
|
805
|
-
|
|
876
|
+
|
|
877
|
+
const useOpenGroup = arrayLayout === "open" && isSimpleStringArray;
|
|
878
|
+
|
|
806
879
|
// Check if this is a single-selection array (maxItems: 1) for radio group
|
|
807
880
|
const isSingleSelection = node.schema?.maxItems === 1;
|
|
808
881
|
const inputType = isSingleSelection ? "radio" : "checkbox";
|
|
809
|
-
|
|
882
|
+
|
|
810
883
|
if (useOpenGroup) {
|
|
811
884
|
// Render fieldset with data-open to let the enhancement handle UI
|
|
812
885
|
// We sync state after the enhancement runs via MutationObserver
|
|
813
|
-
|
|
886
|
+
|
|
814
887
|
const syncFromDOM = (fieldset) => {
|
|
815
888
|
// Read current state from DOM (after enhancement has modified it)
|
|
816
|
-
const inputs = Array.from(
|
|
889
|
+
const inputs = Array.from(
|
|
890
|
+
fieldset.querySelectorAll(
|
|
891
|
+
'input[type="radio"], input[type="checkbox"]'
|
|
892
|
+
)
|
|
893
|
+
);
|
|
817
894
|
const values = inputs
|
|
818
|
-
.map(input => input.value)
|
|
819
|
-
.filter(v => v && v.trim());
|
|
820
|
-
|
|
895
|
+
.map((input) => input.value)
|
|
896
|
+
.filter((v) => v && v.trim());
|
|
897
|
+
|
|
821
898
|
if (isSingleSelection) {
|
|
822
899
|
// For radio groups, find the checked one
|
|
823
|
-
const checkedInput = inputs.find(input => input.checked);
|
|
824
|
-
this.#setByPath(
|
|
900
|
+
const checkedInput = inputs.find((input) => input.checked);
|
|
901
|
+
this.#setByPath(
|
|
902
|
+
this.#data,
|
|
903
|
+
path,
|
|
904
|
+
checkedInput && checkedInput.value ? [checkedInput.value] : []
|
|
905
|
+
);
|
|
825
906
|
} else {
|
|
826
907
|
// For checkbox groups, all items in DOM are in the array
|
|
827
908
|
this.#setByPath(this.#data, path, values);
|
|
828
909
|
}
|
|
829
|
-
this.#emit("pw:array-change", {
|
|
910
|
+
this.#emit("pw:array-change", {
|
|
911
|
+
path,
|
|
912
|
+
values: this.#getByPath(this.#data, path),
|
|
913
|
+
});
|
|
830
914
|
};
|
|
831
|
-
|
|
915
|
+
|
|
832
916
|
const handleChange = (e) => {
|
|
833
917
|
const fieldset = e.currentTarget;
|
|
834
918
|
if (isSingleSelection) {
|
|
835
919
|
// For radio groups, update to selected value immediately
|
|
836
|
-
const checkedInput = fieldset.querySelector(
|
|
837
|
-
|
|
838
|
-
|
|
920
|
+
const checkedInput = fieldset.querySelector(
|
|
921
|
+
'input[type="radio"]:checked'
|
|
922
|
+
);
|
|
923
|
+
this.#setByPath(
|
|
924
|
+
this.#data,
|
|
925
|
+
path,
|
|
926
|
+
checkedInput && checkedInput.value ? [checkedInput.value] : []
|
|
927
|
+
);
|
|
928
|
+
this.#emit("pw:array-change", {
|
|
929
|
+
path,
|
|
930
|
+
values: this.#getByPath(this.#data, path),
|
|
931
|
+
});
|
|
839
932
|
}
|
|
840
933
|
};
|
|
841
|
-
|
|
934
|
+
|
|
842
935
|
const afterRender = (fieldset) => {
|
|
843
936
|
// Observe DOM changes made by the data-open enhancement
|
|
844
937
|
const observer = new MutationObserver(() => {
|
|
845
938
|
syncFromDOM(fieldset);
|
|
846
939
|
});
|
|
847
|
-
observer.observe(fieldset, {
|
|
848
|
-
childList: true,
|
|
849
|
-
subtree: true
|
|
940
|
+
observer.observe(fieldset, {
|
|
941
|
+
childList: true,
|
|
942
|
+
subtree: true,
|
|
850
943
|
});
|
|
851
|
-
|
|
944
|
+
|
|
852
945
|
// Store observer for cleanup
|
|
853
946
|
if (!fieldset._arrayObserver) {
|
|
854
947
|
fieldset._arrayObserver = observer;
|
|
855
948
|
}
|
|
856
949
|
};
|
|
857
|
-
|
|
950
|
+
|
|
858
951
|
const selectedValue = isSingleSelection && arr.length > 0 ? arr[0] : null;
|
|
859
|
-
|
|
952
|
+
|
|
860
953
|
return html`
|
|
861
|
-
<fieldset
|
|
954
|
+
<fieldset
|
|
862
955
|
role="group"
|
|
863
956
|
data-open
|
|
864
957
|
data-path=${path}
|
|
865
958
|
data-name=${path}
|
|
866
959
|
@change=${handleChange}
|
|
867
|
-
${ref((el) => {
|
|
960
|
+
${ref((el) => {
|
|
961
|
+
if (el) afterRender(el);
|
|
962
|
+
})}
|
|
868
963
|
>
|
|
869
964
|
<legend>${node.title ?? "List"}</legend>
|
|
870
965
|
${arr.map((value, i) => {
|
|
871
966
|
const id = `${path}-${i}`;
|
|
872
|
-
const isChecked = isSingleSelection
|
|
967
|
+
const isChecked = isSingleSelection
|
|
968
|
+
? value === selectedValue
|
|
969
|
+
: false;
|
|
873
970
|
return html`
|
|
874
971
|
<label for=${id}>
|
|
875
972
|
<span data-label>${value}</span>
|
|
876
|
-
<input
|
|
973
|
+
<input
|
|
877
974
|
id=${id}
|
|
878
975
|
type=${inputType}
|
|
879
976
|
name=${path}
|
|
@@ -1014,13 +1111,18 @@ export class SchemaForm extends LitElement {
|
|
|
1014
1111
|
// Wrap with icon if ui:icon is specified and enhancements.icons is enabled
|
|
1015
1112
|
const iconName = ui?.["ui:icon"];
|
|
1016
1113
|
const iconPos = ui?.["ui:iconPosition"] || "start";
|
|
1017
|
-
if (iconName && this.#getOption(
|
|
1018
|
-
const iconClasses =
|
|
1114
|
+
if (iconName && this.#getOption("enhancements.icons", true)) {
|
|
1115
|
+
const iconClasses =
|
|
1116
|
+
iconPos === "end" ? "input-icon input-icon-end" : "input-icon";
|
|
1019
1117
|
controlTpl = html`
|
|
1020
1118
|
<div class=${iconClasses}>
|
|
1021
|
-
${iconPos === "start"
|
|
1119
|
+
${iconPos === "start"
|
|
1120
|
+
? html`<pds-icon icon=${iconName}></pds-icon>`
|
|
1121
|
+
: nothing}
|
|
1022
1122
|
${controlTpl}
|
|
1023
|
-
${iconPos === "end"
|
|
1123
|
+
${iconPos === "end"
|
|
1124
|
+
? html`<pds-icon icon=${iconName}></pds-icon>`
|
|
1125
|
+
: nothing}
|
|
1024
1126
|
</div>
|
|
1025
1127
|
`;
|
|
1026
1128
|
}
|
|
@@ -1040,7 +1142,11 @@ export class SchemaForm extends LitElement {
|
|
|
1040
1142
|
: undefined;
|
|
1041
1143
|
const fieldsetClass = ui?.["ui:class"];
|
|
1042
1144
|
return html`
|
|
1043
|
-
<fieldset
|
|
1145
|
+
<fieldset
|
|
1146
|
+
data-path=${path}
|
|
1147
|
+
role=${ifDefined(role)}
|
|
1148
|
+
class=${ifDefined(fieldsetClass)}
|
|
1149
|
+
>
|
|
1044
1150
|
<legend>${label}</legend>
|
|
1045
1151
|
${controlTpl} ${help ? html`<div data-help>${help}</div>` : nothing}
|
|
1046
1152
|
</fieldset>
|
|
@@ -1054,25 +1160,22 @@ export class SchemaForm extends LitElement {
|
|
|
1054
1160
|
|
|
1055
1161
|
// Add data-toggle for toggle switches
|
|
1056
1162
|
const isToggle = node.widgetKey === "toggle";
|
|
1057
|
-
|
|
1163
|
+
|
|
1058
1164
|
// Add range-output class for range inputs if enabled
|
|
1059
1165
|
const isRange = node.widgetKey === "input-range";
|
|
1060
|
-
const useRangeOutput =
|
|
1166
|
+
const useRangeOutput =
|
|
1167
|
+
isRange && this.#getOption("enhancements.rangeOutput", true);
|
|
1061
1168
|
const labelClass = useRangeOutput ? "range-output" : undefined;
|
|
1062
1169
|
|
|
1063
1170
|
const renderControlAndLabel = (isToggle) => {
|
|
1064
|
-
if(isToggle)
|
|
1065
|
-
return html`${controlTpl} <span data-label>${label}</span>`;
|
|
1171
|
+
if (isToggle) return html`${controlTpl} <span data-label>${label}</span>`;
|
|
1066
1172
|
|
|
1067
1173
|
return html`<span data-label>${label}</span> ${controlTpl}`;
|
|
1174
|
+
};
|
|
1068
1175
|
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
1176
|
return html`
|
|
1072
|
-
<label for=${id} ?data-toggle=${isToggle} class=${ifDefined(labelClass)}>
|
|
1073
|
-
|
|
1177
|
+
<label for=${id} ?data-toggle=${isToggle} class=${ifDefined(labelClass)}>
|
|
1074
1178
|
${renderControlAndLabel(isToggle)}
|
|
1075
|
-
|
|
1076
1179
|
${help ? html`<div data-help>${help}</div>` : nothing}
|
|
1077
1180
|
</label>
|
|
1078
1181
|
`;
|
|
@@ -1149,47 +1252,50 @@ export class SchemaForm extends LitElement {
|
|
|
1149
1252
|
`
|
|
1150
1253
|
);
|
|
1151
1254
|
|
|
1152
|
-
this.defineRenderer(
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
(
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1255
|
+
this.defineRenderer(
|
|
1256
|
+
"input-number",
|
|
1257
|
+
({ id, path, value, attrs, set, schema }) => {
|
|
1258
|
+
const step = attrs.step || getStep(value);
|
|
1259
|
+
return html`
|
|
1260
|
+
<input
|
|
1261
|
+
id=${id}
|
|
1262
|
+
name=${path}
|
|
1263
|
+
type="number"
|
|
1264
|
+
placeholder=${ifDefined(attrs.placeholder)}
|
|
1265
|
+
.value=${value ?? ""}
|
|
1266
|
+
min=${ifDefined(attrs.min)}
|
|
1267
|
+
max=${ifDefined(attrs.max)}
|
|
1268
|
+
step=${ifDefined(step)}
|
|
1269
|
+
?readonly=${!!attrs.readOnly}
|
|
1270
|
+
?required=${!!attrs.required}
|
|
1271
|
+
@input=${(e) => {
|
|
1272
|
+
const v = e.target.value;
|
|
1273
|
+
const numValue =
|
|
1274
|
+
schema.type === "integer" ? parseInt(v, 10) : parseFloat(v);
|
|
1275
|
+
const step =
|
|
1276
|
+
attrs.step ||
|
|
1277
|
+
(numValue % 1 !== 0
|
|
1278
|
+
? `0.${"1".padStart(
|
|
1279
|
+
numValue.toString().split(".")[1]?.length || 0,
|
|
1280
|
+
"0"
|
|
1281
|
+
)}`
|
|
1282
|
+
: "1");
|
|
1283
|
+
e.target.step = step; // Dynamically set step based on value precision
|
|
1284
|
+
if (
|
|
1285
|
+
(attrs.min != null && numValue < attrs.min) ||
|
|
1286
|
+
(attrs.max != null && numValue > attrs.max) ||
|
|
1287
|
+
(attrs.step != null && numValue % parseFloat(attrs.step) !== 0)
|
|
1288
|
+
) {
|
|
1289
|
+
e.target.setCustomValidity("Invalid value");
|
|
1290
|
+
} else {
|
|
1291
|
+
e.target.setCustomValidity("");
|
|
1292
|
+
set(numValue);
|
|
1293
|
+
}
|
|
1294
|
+
}}
|
|
1295
|
+
/>
|
|
1296
|
+
`;
|
|
1297
|
+
}
|
|
1298
|
+
);
|
|
1193
1299
|
|
|
1194
1300
|
// Range input renderer for ui:widget = 'input-range'
|
|
1195
1301
|
this.defineRenderer(
|
|
@@ -1237,8 +1343,9 @@ export class SchemaForm extends LitElement {
|
|
|
1237
1343
|
"input-password",
|
|
1238
1344
|
({ id, path, value, attrs, set, ui }) => {
|
|
1239
1345
|
// Determine autocomplete value based on UI hints or use "current-password" as default
|
|
1240
|
-
const autocomplete =
|
|
1241
|
-
|
|
1346
|
+
const autocomplete =
|
|
1347
|
+
ui?.["ui:autocomplete"] || attrs.autocomplete || "current-password";
|
|
1348
|
+
|
|
1242
1349
|
return html`
|
|
1243
1350
|
<input
|
|
1244
1351
|
id=${id}
|
|
@@ -1371,8 +1478,9 @@ export class SchemaForm extends LitElement {
|
|
|
1371
1478
|
this.defineRenderer(
|
|
1372
1479
|
"select",
|
|
1373
1480
|
({ id, path, value, attrs, set, schema, ui, host }) => {
|
|
1374
|
-
const useDropdown =
|
|
1375
|
-
|
|
1481
|
+
const useDropdown =
|
|
1482
|
+
host.#getOption("widgets.selects", "standard") === "dropdown" ||
|
|
1483
|
+
ui?.["ui:dropdown"] === true;
|
|
1376
1484
|
const enumValues = schema.enum || [];
|
|
1377
1485
|
const enumLabels = schema.enumNames || enumValues;
|
|
1378
1486
|
return html`
|
|
@@ -1386,7 +1494,10 @@ export class SchemaForm extends LitElement {
|
|
|
1386
1494
|
>
|
|
1387
1495
|
<option value="" ?selected=${value == null}>—</option>
|
|
1388
1496
|
${enumValues.map(
|
|
1389
|
-
(v, i) =>
|
|
1497
|
+
(v, i) =>
|
|
1498
|
+
html`<option value=${String(v)}>
|
|
1499
|
+
${String(enumLabels[i])}
|
|
1500
|
+
</option>`
|
|
1390
1501
|
)}
|
|
1391
1502
|
</select>
|
|
1392
1503
|
`;
|
|
@@ -1395,34 +1506,31 @@ export class SchemaForm extends LitElement {
|
|
|
1395
1506
|
|
|
1396
1507
|
// Radio group: returns ONLY the labeled inputs
|
|
1397
1508
|
// Matches PDS pattern: input hidden, label styled as button
|
|
1398
|
-
this.defineRenderer(
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
`;
|
|
1424
|
-
}
|
|
1425
|
-
);
|
|
1509
|
+
this.defineRenderer("radio", ({ id, path, value, attrs, set, schema }) => {
|
|
1510
|
+
const enumValues = schema.enum || [];
|
|
1511
|
+
const enumLabels = schema.enumNames || enumValues;
|
|
1512
|
+
return html`
|
|
1513
|
+
${enumValues.map((v, i) => {
|
|
1514
|
+
const rid = `${id}-${i}`;
|
|
1515
|
+
return html`
|
|
1516
|
+
<label for=${rid}>
|
|
1517
|
+
<input
|
|
1518
|
+
id=${rid}
|
|
1519
|
+
type="radio"
|
|
1520
|
+
name=${path}
|
|
1521
|
+
.value=${String(v)}
|
|
1522
|
+
.checked=${String(value) === String(v)}
|
|
1523
|
+
?required=${!!attrs.required}
|
|
1524
|
+
@change=${(e) => {
|
|
1525
|
+
if (e.target.checked) set(enumValues[i]);
|
|
1526
|
+
}}
|
|
1527
|
+
/>
|
|
1528
|
+
${String(enumLabels[i])}
|
|
1529
|
+
</label>
|
|
1530
|
+
`;
|
|
1531
|
+
})}
|
|
1532
|
+
`;
|
|
1533
|
+
});
|
|
1426
1534
|
|
|
1427
1535
|
// Checkbox group: for multi-select from enum (array type with enum items)
|
|
1428
1536
|
// Shows actual checkboxes (not button-style like radios)
|
|
@@ -1431,7 +1539,8 @@ export class SchemaForm extends LitElement {
|
|
|
1431
1539
|
({ id, path, value, attrs, set, schema }) => {
|
|
1432
1540
|
const selected = Array.isArray(value) ? value : [];
|
|
1433
1541
|
const options = schema.items?.enum || schema.enum || [];
|
|
1434
|
-
const optionLabels =
|
|
1542
|
+
const optionLabels =
|
|
1543
|
+
schema.items?.enumNames || schema.enumNames || options;
|
|
1435
1544
|
|
|
1436
1545
|
return html`
|
|
1437
1546
|
${options.map((v, i) => {
|
|
@@ -1476,45 +1585,41 @@ export class SchemaForm extends LitElement {
|
|
|
1476
1585
|
);
|
|
1477
1586
|
|
|
1478
1587
|
// pds-upload: File upload component
|
|
1479
|
-
this.defineRenderer(
|
|
1480
|
-
"
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
`;
|
|
1495
|
-
}
|
|
1496
|
-
);
|
|
1588
|
+
this.defineRenderer("upload", ({ id, value, attrs, set, ui, path }) => {
|
|
1589
|
+
const uploadOpts = ui?.["ui:options"] || {};
|
|
1590
|
+
return html`
|
|
1591
|
+
<pds-upload
|
|
1592
|
+
id=${id}
|
|
1593
|
+
accept=${ifDefined(uploadOpts.accept)}
|
|
1594
|
+
?multiple=${uploadOpts.multiple ?? false}
|
|
1595
|
+
max-files=${ifDefined(uploadOpts.maxFiles)}
|
|
1596
|
+
max-size=${ifDefined(uploadOpts.maxSize)}
|
|
1597
|
+
label=${ifDefined(uploadOpts.label)}
|
|
1598
|
+
?required=${!!attrs.required}
|
|
1599
|
+
@pw:change=${(e) => set(e.detail.files)}
|
|
1600
|
+
></pds-upload>
|
|
1601
|
+
`;
|
|
1602
|
+
});
|
|
1497
1603
|
|
|
1498
1604
|
// pds-richtext: Rich text editor
|
|
1499
|
-
this.defineRenderer(
|
|
1500
|
-
"
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
);
|
|
1605
|
+
this.defineRenderer("richtext", ({ id, value, attrs, set, ui, path }) => {
|
|
1606
|
+
const richtextOpts = ui?.["ui:options"] || {};
|
|
1607
|
+
return html`
|
|
1608
|
+
<pds-richtext
|
|
1609
|
+
id=${id}
|
|
1610
|
+
name=${path}
|
|
1611
|
+
placeholder=${ifDefined(
|
|
1612
|
+
richtextOpts.placeholder || attrs.placeholder
|
|
1613
|
+
)}
|
|
1614
|
+
.value=${value ?? ""}
|
|
1615
|
+
toolbar=${ifDefined(richtextOpts.toolbar)}
|
|
1616
|
+
?required=${!!attrs.required}
|
|
1617
|
+
?submit-on-enter=${richtextOpts.submitOnEnter ?? false}
|
|
1618
|
+
spellcheck=${richtextOpts.spellcheck ?? true ? "true" : "false"}
|
|
1619
|
+
@input=${(e) => set(e.target.value)}
|
|
1620
|
+
></pds-richtext>
|
|
1621
|
+
`;
|
|
1622
|
+
});
|
|
1518
1623
|
}
|
|
1519
1624
|
|
|
1520
1625
|
// ===== Form submit =====
|
|
@@ -1549,34 +1654,34 @@ export class SchemaForm extends LitElement {
|
|
|
1549
1654
|
// ===== Utilities =====
|
|
1550
1655
|
#uiFor(path) {
|
|
1551
1656
|
if (!this.uiSchema) return undefined;
|
|
1552
|
-
|
|
1657
|
+
|
|
1553
1658
|
// Try exact match first (flat structure)
|
|
1554
1659
|
if (this.uiSchema[path]) return this.uiSchema[path];
|
|
1555
|
-
|
|
1660
|
+
|
|
1556
1661
|
// Try with leading slash
|
|
1557
1662
|
const withSlash = this.#asRel(path);
|
|
1558
1663
|
if (this.uiSchema[withSlash]) return this.uiSchema[withSlash];
|
|
1559
|
-
|
|
1664
|
+
|
|
1560
1665
|
// Try without leading slash for convenience
|
|
1561
1666
|
const withoutSlash = path.startsWith("/") ? path.substring(1) : path;
|
|
1562
1667
|
if (this.uiSchema[withoutSlash]) return this.uiSchema[withoutSlash];
|
|
1563
|
-
|
|
1668
|
+
|
|
1564
1669
|
// Try nested navigation (e.g., userProfile/settings/preferences/theme)
|
|
1565
1670
|
// Skip array indices (numeric parts and wildcard *) when navigating UI schema
|
|
1566
|
-
const parts = path.replace(/^\//,
|
|
1671
|
+
const parts = path.replace(/^\//, "").split("/");
|
|
1567
1672
|
let current = this.uiSchema;
|
|
1568
1673
|
for (const part of parts) {
|
|
1569
1674
|
// Skip numeric array indices and wildcard in UI schema navigation
|
|
1570
|
-
if (/^\d+$/.test(part) || part ===
|
|
1571
|
-
|
|
1572
|
-
if (current && typeof current ===
|
|
1675
|
+
if (/^\d+$/.test(part) || part === "*") continue;
|
|
1676
|
+
|
|
1677
|
+
if (current && typeof current === "object" && part in current) {
|
|
1573
1678
|
current = current[part];
|
|
1574
1679
|
} else {
|
|
1575
1680
|
return undefined;
|
|
1576
1681
|
}
|
|
1577
1682
|
}
|
|
1578
1683
|
// Only return if we found a UI config object (not a nested parent)
|
|
1579
|
-
return current && typeof current ===
|
|
1684
|
+
return current && typeof current === "object" ? current : undefined;
|
|
1580
1685
|
}
|
|
1581
1686
|
#asRel(path) {
|
|
1582
1687
|
return path.startsWith("/") ? path : "/" + path;
|
|
@@ -1598,10 +1703,14 @@ export class SchemaForm extends LitElement {
|
|
|
1598
1703
|
|
|
1599
1704
|
#nativeConstraints(path, schema) {
|
|
1600
1705
|
// Use placeholder if explicitly set, otherwise use first example as placeholder
|
|
1601
|
-
const attrs = {
|
|
1602
|
-
placeholder:
|
|
1706
|
+
const attrs = {
|
|
1707
|
+
placeholder:
|
|
1708
|
+
schema.placeholder ||
|
|
1709
|
+
(schema.examples && schema.examples.length > 0
|
|
1710
|
+
? schema.examples[0]
|
|
1711
|
+
: undefined),
|
|
1603
1712
|
};
|
|
1604
|
-
|
|
1713
|
+
|
|
1605
1714
|
if (schema.type === "string") {
|
|
1606
1715
|
if (schema.minLength != null) attrs.minLength = schema.minLength;
|
|
1607
1716
|
if (schema.maxLength != null) attrs.maxLength = schema.maxLength;
|
|
@@ -1643,7 +1752,7 @@ export class SchemaForm extends LitElement {
|
|
|
1643
1752
|
#schemaAt(path) {
|
|
1644
1753
|
return this.#schemaAtPath(this.jsonSchema, path);
|
|
1645
1754
|
}
|
|
1646
|
-
|
|
1755
|
+
|
|
1647
1756
|
#schemaAtPath(schemaRoot, path) {
|
|
1648
1757
|
let cur = schemaRoot;
|
|
1649
1758
|
for (const seg of path.split("/").filter(Boolean)) {
|