@pure-ds/core 0.4.18 → 0.4.20
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/CSS-INTELLISENSE-QUICK-REF.md +2 -2
- package/custom-elements.json +174 -170
- package/dist/types/pds.d.ts +2 -2
- 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 → pds-form.d.ts} +10 -9
- package/dist/types/public/assets/pds/components/pds-form.d.ts.map +1 -0
- package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
- package/dist/types/src/node-api/pds-api.d.ts +31 -0
- package/dist/types/src/node-api/pds-api.d.ts.map +1 -0
- package/package.json +1 -1
- package/packages/pds-cli/bin/pds.js +1 -1
- package/public/assets/js/app.js +6 -11
- package/public/assets/js/pds.js +3 -8
- package/public/assets/pds/components/{pds-jsonform.js → pds-form.js} +564 -51
- package/public/assets/pds/custom-elements.json +8 -8
- package/public/assets/pds/vscode-custom-data.json +63 -63
- package/readme.md +4 -4
- package/src/js/pds-core/pds-generator.js +3 -8
- package/src/js/pds-core/pds-ontology.js +5 -5
- package/src/js/pds.d.ts +2 -2
- package/dist/types/public/assets/pds/components/pds-jsonform.d.ts.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, html, nothing, ifDefined, ref } from "#pds/lit";
|
|
1
|
+
import { LitElement, html, nothing, ifDefined, ref, keyed } from "#pds/lit";
|
|
2
2
|
|
|
3
3
|
function getStep(value) {
|
|
4
4
|
if (typeof value === "number") {
|
|
@@ -8,7 +8,7 @@ function getStep(value) {
|
|
|
8
8
|
return "1"; // Default step for integers
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
// Default options for pds-
|
|
11
|
+
// Default options for pds-form
|
|
12
12
|
const DEFAULT_OPTIONS = {
|
|
13
13
|
widgets: {
|
|
14
14
|
booleans: "toggle", // 'toggle' | 'checkbox'
|
|
@@ -31,33 +31,33 @@ const DEFAULT_OPTIONS = {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* <pds-
|
|
34
|
+
* <pds-form>
|
|
35
35
|
*
|
|
36
36
|
* Form Actions:
|
|
37
37
|
* By default, the form includes Submit and Reset buttons inside the <form> element.
|
|
38
38
|
*
|
|
39
39
|
* Usage options:
|
|
40
40
|
* 1. Default buttons:
|
|
41
|
-
* <pds-
|
|
41
|
+
* <pds-form .jsonSchema=${schema}></pds-form>
|
|
42
42
|
*
|
|
43
43
|
* 2. Customize labels:
|
|
44
|
-
* <pds-
|
|
44
|
+
* <pds-form .jsonSchema=${schema} submit-label="Save" reset-label="Clear"></pds-form>
|
|
45
45
|
*
|
|
46
46
|
* 3. Hide reset button:
|
|
47
|
-
* <pds-
|
|
47
|
+
* <pds-form .jsonSchema=${schema} hide-reset></pds-form>
|
|
48
48
|
*
|
|
49
49
|
* 4. Add extra buttons (slot):
|
|
50
|
-
* <pds-
|
|
50
|
+
* <pds-form .jsonSchema=${schema}>
|
|
51
51
|
* <button type="button" slot="actions" @click=${...}>Cancel</button>
|
|
52
|
-
* </pds-
|
|
52
|
+
* </pds-form>
|
|
53
53
|
*
|
|
54
54
|
* 5. Completely custom actions (hides default buttons):
|
|
55
|
-
* <pds-
|
|
55
|
+
* <pds-form .jsonSchema=${schema} hide-actions>
|
|
56
56
|
* <div slot="actions" class="flex gap-md">
|
|
57
57
|
* <button type="submit" class="btn btn-primary">Custom Submit</button>
|
|
58
58
|
* <button type="button" class="btn">Custom Action</button>
|
|
59
59
|
* </div>
|
|
60
|
-
* </pds-
|
|
60
|
+
* </pds-form>
|
|
61
61
|
*/
|
|
62
62
|
export class SchemaForm extends LitElement {
|
|
63
63
|
static properties = {
|
|
@@ -89,6 +89,9 @@ export class SchemaForm extends LitElement {
|
|
|
89
89
|
#idBase = `sf-${Math.random().toString(36).slice(2)}`;
|
|
90
90
|
#mergedOptions = null;
|
|
91
91
|
#slottedActions = [];
|
|
92
|
+
#dependencies = new Map(); // targetPath → Set of sourcePaths that affect it
|
|
93
|
+
#resetKey = 0; // Incremented on reset to force Lit to recreate DOM elements
|
|
94
|
+
#slottedContent = new Map(); // slot name → element for ui:before/ui:after slot references
|
|
92
95
|
|
|
93
96
|
constructor() {
|
|
94
97
|
super();
|
|
@@ -156,12 +159,26 @@ export class SchemaForm extends LitElement {
|
|
|
156
159
|
return this.#onSubmit(new Event("submit", { cancelable: true }));
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
reset() {
|
|
163
|
+
const form = this.renderRoot?.querySelector("form");
|
|
164
|
+
if (form) form.reset(); // Triggers native reset which calls #onReset
|
|
165
|
+
else this.#onReset(new Event("reset")); // Manual fallback
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
connectedCallback() {
|
|
160
169
|
super.connectedCallback();
|
|
161
|
-
// Capture slotted
|
|
170
|
+
// Capture slotted elements before first render (Light DOM doesn't support native slots)
|
|
162
171
|
this.#slottedActions = Array.from(
|
|
163
172
|
this.querySelectorAll('[slot="actions"]')
|
|
164
173
|
);
|
|
174
|
+
// Capture all named slots for ui:before/ui:after references
|
|
175
|
+
this.#slottedContent = new Map();
|
|
176
|
+
this.querySelectorAll('[slot]').forEach((el) => {
|
|
177
|
+
const slotName = el.getAttribute('slot');
|
|
178
|
+
if (slotName && slotName !== 'actions') {
|
|
179
|
+
this.#slottedContent.set(slotName, el);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
165
182
|
}
|
|
166
183
|
|
|
167
184
|
// ===== Lit lifecycle =====
|
|
@@ -234,6 +251,212 @@ export class SchemaForm extends LitElement {
|
|
|
234
251
|
return current !== undefined ? current : defaultValue;
|
|
235
252
|
}
|
|
236
253
|
|
|
254
|
+
// ===== Condition Evaluation =====
|
|
255
|
+
/**
|
|
256
|
+
* Evaluate a condition object against current form data.
|
|
257
|
+
* Supports:
|
|
258
|
+
* - Simple equality: { "/path": "value" }
|
|
259
|
+
* - Operators: { "/path": { "$eq": "value" } }
|
|
260
|
+
* - Multiple conditions (implicit AND): { "/a": "x", "/b": "y" }
|
|
261
|
+
* - Explicit logical: { "$and": [...] }, { "$or": [...] }, { "$not": {...} }
|
|
262
|
+
*
|
|
263
|
+
* Operators: $eq, $ne, $in, $nin, $gt, $gte, $lt, $lte, $exists, $regex
|
|
264
|
+
*/
|
|
265
|
+
#evaluateCondition(condition) {
|
|
266
|
+
if (!condition || typeof condition !== "object") return true;
|
|
267
|
+
|
|
268
|
+
// Handle logical operators
|
|
269
|
+
if ("$and" in condition) {
|
|
270
|
+
return condition.$and.every((c) => this.#evaluateCondition(c));
|
|
271
|
+
}
|
|
272
|
+
if ("$or" in condition) {
|
|
273
|
+
return condition.$or.some((c) => this.#evaluateCondition(c));
|
|
274
|
+
}
|
|
275
|
+
if ("$not" in condition) {
|
|
276
|
+
return !this.#evaluateCondition(condition.$not);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Multiple path conditions = implicit AND
|
|
280
|
+
for (const [key, expected] of Object.entries(condition)) {
|
|
281
|
+
if (key.startsWith("$")) continue; // Skip operators at root
|
|
282
|
+
const actualValue = this.#getByPath(this.#data, key);
|
|
283
|
+
if (!this.#matchValue(actualValue, expected)) return false;
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#matchValue(actual, expected) {
|
|
289
|
+
// Simple equality shorthand
|
|
290
|
+
if (expected === null || typeof expected !== "object" || Array.isArray(expected)) {
|
|
291
|
+
return actual === expected;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Operator object
|
|
295
|
+
for (const [op, operand] of Object.entries(expected)) {
|
|
296
|
+
switch (op) {
|
|
297
|
+
case "$eq":
|
|
298
|
+
if (actual !== operand) return false;
|
|
299
|
+
break;
|
|
300
|
+
case "$ne":
|
|
301
|
+
if (actual === operand) return false;
|
|
302
|
+
break;
|
|
303
|
+
case "$in":
|
|
304
|
+
if (!Array.isArray(operand) || !operand.includes(actual)) return false;
|
|
305
|
+
break;
|
|
306
|
+
case "$nin":
|
|
307
|
+
if (Array.isArray(operand) && operand.includes(actual)) return false;
|
|
308
|
+
break;
|
|
309
|
+
case "$gt":
|
|
310
|
+
if (!(actual > operand)) return false;
|
|
311
|
+
break;
|
|
312
|
+
case "$gte":
|
|
313
|
+
if (!(actual >= operand)) return false;
|
|
314
|
+
break;
|
|
315
|
+
case "$lt":
|
|
316
|
+
if (!(actual < operand)) return false;
|
|
317
|
+
break;
|
|
318
|
+
case "$lte":
|
|
319
|
+
if (!(actual <= operand)) return false;
|
|
320
|
+
break;
|
|
321
|
+
case "$exists":
|
|
322
|
+
if (operand && actual === undefined) return false;
|
|
323
|
+
if (!operand && actual !== undefined) return false;
|
|
324
|
+
break;
|
|
325
|
+
case "$regex": {
|
|
326
|
+
const regex = operand instanceof RegExp ? operand : new RegExp(operand);
|
|
327
|
+
if (!regex.test(String(actual ?? ""))) return false;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
// Unknown operator, ignore
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Evaluate a calculate expression to compute a derived value.
|
|
340
|
+
* Supports:
|
|
341
|
+
* - Path reference: "/path" → returns value at path
|
|
342
|
+
* - Concatenation: { "$concat": ["/firstName", " ", "/lastName"] }
|
|
343
|
+
* - Math: { "$sum": ["/a", "/b"] }, { "$multiply": ["/a", 2] }
|
|
344
|
+
* - Conditional: { "$if": { "cond": {...}, "then": expr, "else": expr } }
|
|
345
|
+
*/
|
|
346
|
+
#evaluateCalculation(expr) {
|
|
347
|
+
if (expr === null || expr === undefined) return undefined;
|
|
348
|
+
|
|
349
|
+
// String path reference
|
|
350
|
+
if (typeof expr === "string") {
|
|
351
|
+
if (expr.startsWith("/")) {
|
|
352
|
+
return this.#getByPath(this.#data, expr);
|
|
353
|
+
}
|
|
354
|
+
return expr; // Literal string
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Number or boolean literal
|
|
358
|
+
if (typeof expr !== "object") return expr;
|
|
359
|
+
|
|
360
|
+
// Array = literal array
|
|
361
|
+
if (Array.isArray(expr)) {
|
|
362
|
+
return expr.map((e) => this.#evaluateCalculation(e));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Operators
|
|
366
|
+
if ("$concat" in expr) {
|
|
367
|
+
return expr.$concat
|
|
368
|
+
.map((e) => this.#evaluateCalculation(e) ?? "")
|
|
369
|
+
.join("");
|
|
370
|
+
}
|
|
371
|
+
if ("$sum" in expr) {
|
|
372
|
+
return expr.$sum.reduce(
|
|
373
|
+
(acc, e) => acc + (Number(this.#evaluateCalculation(e)) || 0),
|
|
374
|
+
0
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
if ("$multiply" in expr) {
|
|
378
|
+
return expr.$multiply.reduce(
|
|
379
|
+
(acc, e) => acc * (Number(this.#evaluateCalculation(e)) || 0),
|
|
380
|
+
1
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if ("$subtract" in expr) {
|
|
384
|
+
const [a, b] = expr.$subtract;
|
|
385
|
+
return (Number(this.#evaluateCalculation(a)) || 0) - (Number(this.#evaluateCalculation(b)) || 0);
|
|
386
|
+
}
|
|
387
|
+
if ("$divide" in expr) {
|
|
388
|
+
const [a, b] = expr.$divide;
|
|
389
|
+
const divisor = Number(this.#evaluateCalculation(b)) || 1;
|
|
390
|
+
return (Number(this.#evaluateCalculation(a)) || 0) / divisor;
|
|
391
|
+
}
|
|
392
|
+
if ("$if" in expr) {
|
|
393
|
+
const { cond, then: thenExpr, else: elseExpr } = expr.$if;
|
|
394
|
+
return this.#evaluateCondition(cond)
|
|
395
|
+
? this.#evaluateCalculation(thenExpr)
|
|
396
|
+
: this.#evaluateCalculation(elseExpr);
|
|
397
|
+
}
|
|
398
|
+
if ("$coalesce" in expr) {
|
|
399
|
+
for (const e of expr.$coalesce) {
|
|
400
|
+
const val = this.#evaluateCalculation(e);
|
|
401
|
+
if (val !== null && val !== undefined && val !== "") return val;
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Unknown object, return as-is
|
|
407
|
+
return expr;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Extract all path dependencies from a condition or calculation expression.
|
|
412
|
+
*/
|
|
413
|
+
#extractDependencies(expr, deps = new Set()) {
|
|
414
|
+
if (!expr) return deps;
|
|
415
|
+
|
|
416
|
+
if (typeof expr === "string" && expr.startsWith("/")) {
|
|
417
|
+
deps.add(expr);
|
|
418
|
+
return deps;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (Array.isArray(expr)) {
|
|
422
|
+
for (const e of expr) this.#extractDependencies(e, deps);
|
|
423
|
+
return deps;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (typeof expr === "object") {
|
|
427
|
+
for (const [key, value] of Object.entries(expr)) {
|
|
428
|
+
if (key.startsWith("/")) {
|
|
429
|
+
deps.add(key);
|
|
430
|
+
}
|
|
431
|
+
this.#extractDependencies(value, deps);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return deps;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Register dependencies for a target path based on its UI conditions.
|
|
440
|
+
*/
|
|
441
|
+
#registerDependencies(targetPath, ui) {
|
|
442
|
+
const deps = new Set();
|
|
443
|
+
|
|
444
|
+
// Extract from all condition properties
|
|
445
|
+
for (const prop of ["ui:visibleWhen", "ui:disabledWhen", "ui:requiredWhen", "ui:calculate"]) {
|
|
446
|
+
if (ui?.[prop]) {
|
|
447
|
+
this.#extractDependencies(ui[prop], deps);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Register reverse mapping: when source changes, target needs update
|
|
452
|
+
for (const sourcePath of deps) {
|
|
453
|
+
if (!this.#dependencies.has(sourcePath)) {
|
|
454
|
+
this.#dependencies.set(sourcePath, new Set());
|
|
455
|
+
}
|
|
456
|
+
this.#dependencies.get(sourcePath).add(targetPath);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
237
460
|
// ===== Schema compilation =====
|
|
238
461
|
#compile() {
|
|
239
462
|
const root = this.jsonSchema;
|
|
@@ -241,6 +464,9 @@ export class SchemaForm extends LitElement {
|
|
|
241
464
|
this.#compiled = null;
|
|
242
465
|
return;
|
|
243
466
|
}
|
|
467
|
+
// Clear dependency graph for fresh build
|
|
468
|
+
this.#dependencies.clear();
|
|
469
|
+
|
|
244
470
|
const resolved =
|
|
245
471
|
this.#emitCancelable("pw:schema-resolve", { schema: root })?.schema ||
|
|
246
472
|
root;
|
|
@@ -252,6 +478,12 @@ export class SchemaForm extends LitElement {
|
|
|
252
478
|
#compileNode(schema, path) {
|
|
253
479
|
const title = schema.title ?? this.#titleFromPath(path);
|
|
254
480
|
const ui = this.#uiFor(path);
|
|
481
|
+
|
|
482
|
+
// Register dependencies for conditional rendering
|
|
483
|
+
if (ui) {
|
|
484
|
+
this.#registerDependencies(path, ui);
|
|
485
|
+
}
|
|
486
|
+
|
|
255
487
|
const custom = this.#emitCancelable("pw:compile-node", {
|
|
256
488
|
path,
|
|
257
489
|
schema,
|
|
@@ -259,7 +491,16 @@ export class SchemaForm extends LitElement {
|
|
|
259
491
|
});
|
|
260
492
|
if (custom?.node) return custom.node;
|
|
261
493
|
|
|
494
|
+
// Check if oneOf/anyOf is used for enum-like selections (all have const)
|
|
495
|
+
// If so, treat as a field with enum options, not as a complex choice
|
|
262
496
|
if (schema.oneOf || schema.anyOf) {
|
|
497
|
+
const { values } = this.#extractEnumOptions(schema);
|
|
498
|
+
// If all options have const values, treat as enum field
|
|
499
|
+
if (values.length > 0) {
|
|
500
|
+
const widgetKey = this.#decideWidget(schema, ui, path);
|
|
501
|
+
return { kind: "field", path, title, schema, ui, widgetKey };
|
|
502
|
+
}
|
|
503
|
+
// Otherwise, treat as complex choice (for actual oneOf/anyOf schemas)
|
|
263
504
|
const choices = (schema.oneOf || schema.anyOf).map((s, i) => ({
|
|
264
505
|
kind: "choice-option",
|
|
265
506
|
index: i,
|
|
@@ -288,8 +529,9 @@ export class SchemaForm extends LitElement {
|
|
|
288
529
|
? { type: "object", properties: {} }
|
|
289
530
|
: schema.items || {};
|
|
290
531
|
|
|
291
|
-
// Special case: array with enum items → checkbox-group
|
|
292
|
-
|
|
532
|
+
// Special case: array with enum/oneOf/anyOf items → checkbox-group
|
|
533
|
+
const { values } = this.#extractEnumOptions(itemSchema);
|
|
534
|
+
if (values.length > 0) {
|
|
293
535
|
return {
|
|
294
536
|
kind: "field",
|
|
295
537
|
path,
|
|
@@ -327,6 +569,49 @@ export class SchemaForm extends LitElement {
|
|
|
327
569
|
return ordered;
|
|
328
570
|
}
|
|
329
571
|
|
|
572
|
+
// Helper to extract enum values and labels from enum, oneOf, or anyOf
|
|
573
|
+
#extractEnumOptions(schema) {
|
|
574
|
+
// Support standard enum array
|
|
575
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
576
|
+
return {
|
|
577
|
+
values: schema.enum,
|
|
578
|
+
labels: schema.enum.map(String)
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Support oneOf pattern: [{ const: value, title: label }, ...]
|
|
583
|
+
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
584
|
+
const values = [];
|
|
585
|
+
const labels = [];
|
|
586
|
+
for (const option of schema.oneOf) {
|
|
587
|
+
if (option.const !== undefined) {
|
|
588
|
+
values.push(option.const);
|
|
589
|
+
labels.push(option.title ?? String(option.const));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (values.length > 0) {
|
|
593
|
+
return { values, labels };
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Support anyOf pattern: [{ const: value, title: label }, ...]
|
|
598
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
599
|
+
const values = [];
|
|
600
|
+
const labels = [];
|
|
601
|
+
for (const option of schema.anyOf) {
|
|
602
|
+
if (option.const !== undefined) {
|
|
603
|
+
values.push(option.const);
|
|
604
|
+
labels.push(option.title ?? String(option.const));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (values.length > 0) {
|
|
608
|
+
return { values, labels };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { values: [], labels: [] };
|
|
613
|
+
}
|
|
614
|
+
|
|
330
615
|
#decideWidget(schema, ui, path) {
|
|
331
616
|
const picked = this.#emitCancelable("pw:choose-widget", {
|
|
332
617
|
path,
|
|
@@ -337,7 +622,8 @@ export class SchemaForm extends LitElement {
|
|
|
337
622
|
if (picked?.widget) return picked.widget;
|
|
338
623
|
// Honor explicit uiSchema widget hints
|
|
339
624
|
if (ui?.["ui:widget"]) return ui["ui:widget"];
|
|
340
|
-
|
|
625
|
+
const { values } = this.#extractEnumOptions(schema);
|
|
626
|
+
if (values.length > 0) return values.length <= 5 ? "radio" : "select";
|
|
341
627
|
if (schema.const !== undefined) return "const";
|
|
342
628
|
if (schema.type === "string") {
|
|
343
629
|
// Check for binary/upload content via JSON Schema contentMediaType
|
|
@@ -432,9 +718,12 @@ export class SchemaForm extends LitElement {
|
|
|
432
718
|
method=${m}
|
|
433
719
|
action=${this.action ?? nothing}
|
|
434
720
|
@submit=${this.#onSubmit}
|
|
721
|
+
@reset=${this.#onReset}
|
|
435
722
|
?disabled=${this.disabled}
|
|
436
723
|
>
|
|
437
|
-
${
|
|
724
|
+
${keyed(this.#resetKey, html`
|
|
725
|
+
${tree ? this.#renderNode(tree) : html`<slot></slot>`}
|
|
726
|
+
`)}
|
|
438
727
|
${!this.hideActions
|
|
439
728
|
? html`
|
|
440
729
|
<div class="form-actions">
|
|
@@ -457,6 +746,16 @@ export class SchemaForm extends LitElement {
|
|
|
457
746
|
}
|
|
458
747
|
|
|
459
748
|
#renderNode(node, context = {}) {
|
|
749
|
+
// Check visibility condition
|
|
750
|
+
const ui = node.ui || this.#uiFor(node.path);
|
|
751
|
+
if (ui?.["ui:visibleWhen"] && !this.#evaluateCondition(ui["ui:visibleWhen"])) {
|
|
752
|
+
return nothing;
|
|
753
|
+
}
|
|
754
|
+
// Also check static ui:hidden
|
|
755
|
+
if (ui?.["ui:hidden"]) {
|
|
756
|
+
return nothing;
|
|
757
|
+
}
|
|
758
|
+
|
|
460
759
|
switch (node.kind) {
|
|
461
760
|
case "fieldset":
|
|
462
761
|
return this.#renderFieldset(node, context);
|
|
@@ -543,15 +842,24 @@ export class SchemaForm extends LitElement {
|
|
|
543
842
|
`;
|
|
544
843
|
|
|
545
844
|
// Wrap in surface if specified
|
|
845
|
+
let result = fieldsetContent;
|
|
546
846
|
if (surface) {
|
|
547
847
|
const surfaceClass =
|
|
548
848
|
surface === "card" || surface === "elevated" || surface === "dialog"
|
|
549
849
|
? `surface ${surface}`
|
|
550
850
|
: "surface";
|
|
551
|
-
|
|
851
|
+
result = html`<div class=${surfaceClass}>${fieldsetContent}</div>`;
|
|
552
852
|
}
|
|
553
853
|
|
|
554
|
-
|
|
854
|
+
// Apply ui:before and ui:after for fieldsets
|
|
855
|
+
const renderContext = { path: node.path, schema: node.schema, ui, node, host: this };
|
|
856
|
+
const before = this.#renderCustomContent(ui?.["ui:before"], renderContext);
|
|
857
|
+
const after = this.#renderCustomContent(ui?.["ui:after"], renderContext);
|
|
858
|
+
|
|
859
|
+
if (before || after) {
|
|
860
|
+
return html`${before}${result}${after}`;
|
|
861
|
+
}
|
|
862
|
+
return result;
|
|
555
863
|
}
|
|
556
864
|
|
|
557
865
|
#renderFieldsetTabs(node, legend, ui) {
|
|
@@ -635,14 +943,14 @@ export class SchemaForm extends LitElement {
|
|
|
635
943
|
try {
|
|
636
944
|
// Use PDS.ask to show dialog with form - it returns FormData when useForm: true
|
|
637
945
|
const formData = await window.PDS.ask(
|
|
638
|
-
html`<pds-
|
|
946
|
+
html`<pds-form
|
|
639
947
|
.jsonSchema=${dialogSchema}
|
|
640
948
|
.values=${currentValue}
|
|
641
949
|
.uiSchema=${this.uiSchema}
|
|
642
950
|
.options=${this.options}
|
|
643
951
|
hide-actions
|
|
644
952
|
hide-legend
|
|
645
|
-
></pds-
|
|
953
|
+
></pds-form>`,
|
|
646
954
|
{
|
|
647
955
|
title: dialogTitle,
|
|
648
956
|
type: "custom",
|
|
@@ -808,6 +1116,7 @@ export class SchemaForm extends LitElement {
|
|
|
808
1116
|
this.#deleteByPathPrefix(this.#data, path + "/");
|
|
809
1117
|
this.requestUpdate();
|
|
810
1118
|
this.#emit("pw:value-change", {
|
|
1119
|
+
path,
|
|
811
1120
|
name: path,
|
|
812
1121
|
value: i,
|
|
813
1122
|
validity: { valid: true },
|
|
@@ -1045,10 +1354,30 @@ export class SchemaForm extends LitElement {
|
|
|
1045
1354
|
const path = node.path;
|
|
1046
1355
|
const id = this.#idFromPath(path);
|
|
1047
1356
|
const label = node.title ?? this.#titleFromPath(path);
|
|
1048
|
-
|
|
1049
|
-
const required = this.#isRequired(path);
|
|
1357
|
+
let value = this.#getByPath(this.#data, path);
|
|
1050
1358
|
const ui = node.ui || this.#uiFor(path);
|
|
1051
1359
|
|
|
1360
|
+
// Evaluate calculated value if present
|
|
1361
|
+
if (ui?.["ui:calculate"]) {
|
|
1362
|
+
const calculatedValue = this.#evaluateCalculation(ui["ui:calculate"]);
|
|
1363
|
+
// Apply calculated value if field is empty or override not allowed
|
|
1364
|
+
if (value === undefined || value === null || !ui["ui:calculateOverride"]) {
|
|
1365
|
+
value = calculatedValue;
|
|
1366
|
+
// Also update internal data to keep in sync
|
|
1367
|
+
if (this.#getByPath(this.#data, path) !== calculatedValue) {
|
|
1368
|
+
this.#setByPath(this.#data, path, calculatedValue);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Evaluate conditional states
|
|
1374
|
+
const isDisabled = ui?.["ui:disabled"] ||
|
|
1375
|
+
(ui?.["ui:disabledWhen"] && this.#evaluateCondition(ui["ui:disabledWhen"]));
|
|
1376
|
+
const isRequired = this.#isRequired(path) ||
|
|
1377
|
+
(ui?.["ui:requiredWhen"] && this.#evaluateCondition(ui["ui:requiredWhen"]));
|
|
1378
|
+
const isReadonly = ui?.["ui:readonly"] ||
|
|
1379
|
+
(ui?.["ui:calculate"] && !ui["ui:calculateOverride"]);
|
|
1380
|
+
|
|
1052
1381
|
// Override hook before default field render
|
|
1053
1382
|
{
|
|
1054
1383
|
const override = this.#emitCancelable("pw:before-render-field", {
|
|
@@ -1061,23 +1390,45 @@ export class SchemaForm extends LitElement {
|
|
|
1061
1390
|
if (override?.render) return override.render();
|
|
1062
1391
|
}
|
|
1063
1392
|
|
|
1393
|
+
// Build attributes including conditional states
|
|
1394
|
+
const attrs = {
|
|
1395
|
+
...this.#nativeConstraints(path, node.schema),
|
|
1396
|
+
disabled: isDisabled,
|
|
1397
|
+
required: isRequired,
|
|
1398
|
+
readOnly: isReadonly,
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// Build base render context (shared by ui:render, renderers, and ui:wrapper)
|
|
1402
|
+
const baseContext = {
|
|
1403
|
+
id,
|
|
1404
|
+
path,
|
|
1405
|
+
label,
|
|
1406
|
+
value,
|
|
1407
|
+
required: isRequired,
|
|
1408
|
+
ui,
|
|
1409
|
+
schema: node.schema,
|
|
1410
|
+
get: (p) => this.#getByPath(this.#data, p ?? path),
|
|
1411
|
+
set: (val, p) => this.#assignValue(p ?? path, val),
|
|
1412
|
+
attrs,
|
|
1413
|
+
host: this,
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
// Check for ui:render - completely custom inline renderer (same context as defineRenderer)
|
|
1417
|
+
if (ui?.["ui:render"] && typeof ui["ui:render"] === "function") {
|
|
1418
|
+
const customTpl = ui["ui:render"](baseContext);
|
|
1419
|
+
const before = this.#renderCustomContent(ui?.["ui:before"], baseContext);
|
|
1420
|
+
const after = this.#renderCustomContent(ui?.["ui:after"], baseContext);
|
|
1421
|
+
queueMicrotask(() =>
|
|
1422
|
+
this.#emit("pw:after-render-field", { path, schema: node.schema })
|
|
1423
|
+
);
|
|
1424
|
+
return html`${before}${customTpl}${after}`;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1064
1427
|
// Default renderer lookup: returns ONLY the control markup
|
|
1065
1428
|
const renderer =
|
|
1066
1429
|
this.#renderers.get(node.widgetKey) || this.#renderers.get("*");
|
|
1067
1430
|
let controlTpl = renderer
|
|
1068
|
-
? renderer(
|
|
1069
|
-
id,
|
|
1070
|
-
path,
|
|
1071
|
-
label,
|
|
1072
|
-
value,
|
|
1073
|
-
required,
|
|
1074
|
-
ui,
|
|
1075
|
-
schema: node.schema,
|
|
1076
|
-
get: (p) => this.#getByPath(this.#data, p ?? path),
|
|
1077
|
-
set: (val, p) => this.#assignValue(p ?? path, val),
|
|
1078
|
-
attrs: this.#nativeConstraints(path, node.schema),
|
|
1079
|
-
host: this,
|
|
1080
|
-
})
|
|
1431
|
+
? renderer(baseContext)
|
|
1081
1432
|
: nothing;
|
|
1082
1433
|
|
|
1083
1434
|
// Post-creation tweak
|
|
@@ -1153,12 +1504,47 @@ export class SchemaForm extends LitElement {
|
|
|
1153
1504
|
return html`<span data-label>${label}</span> ${controlTpl}`;
|
|
1154
1505
|
};
|
|
1155
1506
|
|
|
1156
|
-
|
|
1507
|
+
// Build render context for custom content
|
|
1508
|
+
const renderContext = {
|
|
1509
|
+
id,
|
|
1510
|
+
path,
|
|
1511
|
+
label,
|
|
1512
|
+
value,
|
|
1513
|
+
required: isRequired,
|
|
1514
|
+
ui,
|
|
1515
|
+
schema: node.schema,
|
|
1516
|
+
get: (p) => this.#getByPath(this.#data, p ?? path),
|
|
1517
|
+
set: (val, p) => this.#assignValue(p ?? path, val),
|
|
1518
|
+
attrs,
|
|
1519
|
+
host: this,
|
|
1520
|
+
// Additional context for ui:wrapper
|
|
1521
|
+
control: controlTpl,
|
|
1522
|
+
help: help ? html`<div data-help>${help}</div>` : nothing,
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
// Check for ui:wrapper - custom wrapper replaces the entire label structure
|
|
1526
|
+
if (ui?.["ui:wrapper"] && typeof ui["ui:wrapper"] === "function") {
|
|
1527
|
+
const wrapped = ui["ui:wrapper"](renderContext);
|
|
1528
|
+
const before = this.#renderCustomContent(ui?.["ui:before"], renderContext);
|
|
1529
|
+
const after = this.#renderCustomContent(ui?.["ui:after"], renderContext);
|
|
1530
|
+
return html`${before}${wrapped}${after}`;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Standard label wrapper with ui:before/ui:after
|
|
1534
|
+
const before = this.#renderCustomContent(ui?.["ui:before"], renderContext);
|
|
1535
|
+
const after = this.#renderCustomContent(ui?.["ui:after"], renderContext);
|
|
1536
|
+
|
|
1537
|
+
const labelTpl = html`
|
|
1157
1538
|
<label for=${id} ?data-toggle=${isToggle} class=${ifDefined(labelClass)}>
|
|
1158
1539
|
${renderControlAndLabel(isToggle)}
|
|
1159
1540
|
${help ? html`<div data-help>${help}</div>` : nothing}
|
|
1160
1541
|
</label>
|
|
1161
1542
|
`;
|
|
1543
|
+
|
|
1544
|
+
if (before || after) {
|
|
1545
|
+
return html`${before}${labelTpl}${after}`;
|
|
1546
|
+
}
|
|
1547
|
+
return labelTpl;
|
|
1162
1548
|
}
|
|
1163
1549
|
|
|
1164
1550
|
// ===== Default renderers: controls only (no spread arrays) =====
|
|
@@ -1177,6 +1563,7 @@ export class SchemaForm extends LitElement {
|
|
|
1177
1563
|
maxlength=${ifDefined(attrs.maxLength)}
|
|
1178
1564
|
pattern=${ifDefined(attrs.pattern)}
|
|
1179
1565
|
?readonly=${!!attrs.readOnly}
|
|
1566
|
+
?disabled=${!!attrs.disabled}
|
|
1180
1567
|
?required=${!!attrs.required}
|
|
1181
1568
|
autocomplete=${ifDefined(attrs.autocomplete)}
|
|
1182
1569
|
@input=${(e) => set(e.target.value)}
|
|
@@ -1197,6 +1584,7 @@ export class SchemaForm extends LitElement {
|
|
|
1197
1584
|
maxlength=${ifDefined(attrs.maxLength)}
|
|
1198
1585
|
pattern=${ifDefined(attrs.pattern)}
|
|
1199
1586
|
?readonly=${!!attrs.readOnly}
|
|
1587
|
+
?disabled=${!!attrs.disabled}
|
|
1200
1588
|
?required=${!!attrs.required}
|
|
1201
1589
|
autocomplete=${ifDefined(attrs.autocomplete)}
|
|
1202
1590
|
list=${ifDefined(ui?.["ui:datalist"] ? `${id}-datalist` : attrs.list)}
|
|
@@ -1226,6 +1614,7 @@ export class SchemaForm extends LitElement {
|
|
|
1226
1614
|
minlength=${ifDefined(attrs.minLength)}
|
|
1227
1615
|
maxlength=${ifDefined(attrs.maxLength)}
|
|
1228
1616
|
?readonly=${!!attrs.readOnly}
|
|
1617
|
+
?disabled=${!!attrs.disabled}
|
|
1229
1618
|
?required=${!!attrs.required}
|
|
1230
1619
|
@input=${(e) => set(e.target.value)}
|
|
1231
1620
|
></textarea>
|
|
@@ -1247,6 +1636,7 @@ export class SchemaForm extends LitElement {
|
|
|
1247
1636
|
max=${ifDefined(attrs.max)}
|
|
1248
1637
|
step=${ifDefined(step)}
|
|
1249
1638
|
?readonly=${!!attrs.readOnly}
|
|
1639
|
+
?disabled=${!!attrs.disabled}
|
|
1250
1640
|
?required=${!!attrs.required}
|
|
1251
1641
|
@input=${(e) => {
|
|
1252
1642
|
const v = e.target.value;
|
|
@@ -1294,6 +1684,7 @@ export class SchemaForm extends LitElement {
|
|
|
1294
1684
|
max=${max}
|
|
1295
1685
|
step=${step}
|
|
1296
1686
|
.value=${value ?? min}
|
|
1687
|
+
?disabled=${!!attrs.disabled}
|
|
1297
1688
|
@input=${(e) => set(Number(e.target.value))}
|
|
1298
1689
|
/>
|
|
1299
1690
|
<div class="range-bubble" aria-hidden="true">${value ?? min}</div>
|
|
@@ -1312,6 +1703,7 @@ export class SchemaForm extends LitElement {
|
|
|
1312
1703
|
type="email"
|
|
1313
1704
|
.value=${value ?? ""}
|
|
1314
1705
|
?readonly=${!!attrs.readOnly}
|
|
1706
|
+
?disabled=${!!attrs.disabled}
|
|
1315
1707
|
?required=${!!attrs.required}
|
|
1316
1708
|
autocomplete=${ifDefined(attrs.autocomplete)}
|
|
1317
1709
|
@input=${(e) => set(e.target.value)}
|
|
@@ -1336,6 +1728,7 @@ export class SchemaForm extends LitElement {
|
|
|
1336
1728
|
minlength=${ifDefined(attrs.minLength)}
|
|
1337
1729
|
maxlength=${ifDefined(attrs.maxLength)}
|
|
1338
1730
|
?readonly=${!!attrs.readOnly}
|
|
1731
|
+
?disabled=${!!attrs.disabled}
|
|
1339
1732
|
?required=${!!attrs.required}
|
|
1340
1733
|
autocomplete=${autocomplete}
|
|
1341
1734
|
@input=${(e) => set(e.target.value)}
|
|
@@ -1354,6 +1747,7 @@ export class SchemaForm extends LitElement {
|
|
|
1354
1747
|
type="url"
|
|
1355
1748
|
.value=${value ?? ""}
|
|
1356
1749
|
?readonly=${!!attrs.readOnly}
|
|
1750
|
+
?disabled=${!!attrs.disabled}
|
|
1357
1751
|
?required=${!!attrs.required}
|
|
1358
1752
|
@input=${(e) => set(e.target.value)}
|
|
1359
1753
|
/>
|
|
@@ -1372,6 +1766,7 @@ export class SchemaForm extends LitElement {
|
|
|
1372
1766
|
min=${ifDefined(attrs.min)}
|
|
1373
1767
|
max=${ifDefined(attrs.max)}
|
|
1374
1768
|
?readonly=${!!attrs.readOnly}
|
|
1769
|
+
?disabled=${!!attrs.disabled}
|
|
1375
1770
|
?required=${!!attrs.required}
|
|
1376
1771
|
@input=${(e) => set(e.target.value)}
|
|
1377
1772
|
/>
|
|
@@ -1388,6 +1783,7 @@ export class SchemaForm extends LitElement {
|
|
|
1388
1783
|
type="time"
|
|
1389
1784
|
.value=${value ?? ""}
|
|
1390
1785
|
?readonly=${!!attrs.readOnly}
|
|
1786
|
+
?disabled=${!!attrs.disabled}
|
|
1391
1787
|
?required=${!!attrs.required}
|
|
1392
1788
|
@input=${(e) => set(e.target.value)}
|
|
1393
1789
|
/>
|
|
@@ -1404,6 +1800,7 @@ export class SchemaForm extends LitElement {
|
|
|
1404
1800
|
type="color"
|
|
1405
1801
|
.value=${value ?? ""}
|
|
1406
1802
|
?readonly=${!!attrs.readOnly}
|
|
1803
|
+
?disabled=${!!attrs.disabled}
|
|
1407
1804
|
?required=${!!attrs.required}
|
|
1408
1805
|
@input=${(e) => set(e.target.value)}
|
|
1409
1806
|
/>
|
|
@@ -1420,6 +1817,7 @@ export class SchemaForm extends LitElement {
|
|
|
1420
1817
|
type="datetime-local"
|
|
1421
1818
|
.value=${value ?? ""}
|
|
1422
1819
|
?readonly=${!!attrs.readOnly}
|
|
1820
|
+
?disabled=${!!attrs.disabled}
|
|
1423
1821
|
?required=${!!attrs.required}
|
|
1424
1822
|
@input=${(e) => set(e.target.value)}
|
|
1425
1823
|
/>
|
|
@@ -1434,6 +1832,7 @@ export class SchemaForm extends LitElement {
|
|
|
1434
1832
|
name=${path}
|
|
1435
1833
|
type="checkbox"
|
|
1436
1834
|
.checked=${!!value}
|
|
1835
|
+
?disabled=${!!attrs.disabled}
|
|
1437
1836
|
?required=${!!attrs.required}
|
|
1438
1837
|
@change=${(e) => set(!!e.target.checked)}
|
|
1439
1838
|
/>
|
|
@@ -1449,6 +1848,7 @@ export class SchemaForm extends LitElement {
|
|
|
1449
1848
|
name=${path}
|
|
1450
1849
|
type="checkbox"
|
|
1451
1850
|
.checked=${!!value}
|
|
1851
|
+
?disabled=${!!attrs.disabled}
|
|
1452
1852
|
?required=${!!attrs.required}
|
|
1453
1853
|
@change=${(e) => set(!!e.target.checked)}
|
|
1454
1854
|
/>
|
|
@@ -1461,13 +1861,12 @@ export class SchemaForm extends LitElement {
|
|
|
1461
1861
|
const useDropdown =
|
|
1462
1862
|
host.#getOption("widgets.selects", "standard") === "dropdown" ||
|
|
1463
1863
|
ui?.["ui:dropdown"] === true;
|
|
1464
|
-
const enumValues = schema
|
|
1465
|
-
const enumLabels = schema.enumNames || enumValues;
|
|
1864
|
+
const { values: enumValues, labels: enumLabels } = host.#extractEnumOptions(schema);
|
|
1466
1865
|
return html`
|
|
1467
1866
|
<select
|
|
1468
1867
|
id=${id}
|
|
1469
1868
|
name=${path}
|
|
1470
|
-
|
|
1869
|
+
?disabled=${!!attrs.disabled}
|
|
1471
1870
|
?required=${!!attrs.required}
|
|
1472
1871
|
?data-dropdown=${useDropdown}
|
|
1473
1872
|
@change=${(e) => set(e.target.value)}
|
|
@@ -1475,7 +1874,7 @@ export class SchemaForm extends LitElement {
|
|
|
1475
1874
|
<option value="" ?selected=${value == null}>—</option>
|
|
1476
1875
|
${enumValues.map(
|
|
1477
1876
|
(v, i) =>
|
|
1478
|
-
html`<option value=${String(v)}>
|
|
1877
|
+
html`<option value=${String(v)} ?selected=${String(value) === String(v)}>
|
|
1479
1878
|
${String(enumLabels[i])}
|
|
1480
1879
|
</option>`
|
|
1481
1880
|
)}
|
|
@@ -1486,9 +1885,8 @@ export class SchemaForm extends LitElement {
|
|
|
1486
1885
|
|
|
1487
1886
|
// Radio group: returns ONLY the labeled inputs
|
|
1488
1887
|
// Matches PDS pattern: input hidden, label styled as button
|
|
1489
|
-
this.defineRenderer("radio", ({ id, path, value, attrs, set, schema }) => {
|
|
1490
|
-
const enumValues = schema
|
|
1491
|
-
const enumLabels = schema.enumNames || enumValues;
|
|
1888
|
+
this.defineRenderer("radio", ({ id, path, value, attrs, set, schema, host }) => {
|
|
1889
|
+
const { values: enumValues, labels: enumLabels } = host.#extractEnumOptions(schema);
|
|
1492
1890
|
return html`
|
|
1493
1891
|
${enumValues.map((v, i) => {
|
|
1494
1892
|
const rid = `${id}-${i}`;
|
|
@@ -1516,11 +1914,11 @@ export class SchemaForm extends LitElement {
|
|
|
1516
1914
|
// Shows actual checkboxes (not button-style like radios)
|
|
1517
1915
|
this.defineRenderer(
|
|
1518
1916
|
"checkbox-group",
|
|
1519
|
-
({ id, path, value, attrs, set, schema }) => {
|
|
1917
|
+
({ id, path, value, attrs, set, schema, host }) => {
|
|
1520
1918
|
const selected = Array.isArray(value) ? value : [];
|
|
1521
|
-
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1919
|
+
// For array items, check items schema first, then fallback to root schema
|
|
1920
|
+
const itemSchema = schema.items || schema;
|
|
1921
|
+
const { values: options, labels: optionLabels } = host.#extractEnumOptions(itemSchema);
|
|
1524
1922
|
|
|
1525
1923
|
return html`
|
|
1526
1924
|
${options.map((v, i) => {
|
|
@@ -1631,6 +2029,44 @@ export class SchemaForm extends LitElement {
|
|
|
1631
2029
|
return final;
|
|
1632
2030
|
}
|
|
1633
2031
|
|
|
2032
|
+
// ===== Form reset =====
|
|
2033
|
+
#onReset(e) {
|
|
2034
|
+
// Prevent native reset - we'll handle everything ourselves
|
|
2035
|
+
// Native reset clears DOM inputs but doesn't sync with Lit's reactive state
|
|
2036
|
+
e.preventDefault();
|
|
2037
|
+
|
|
2038
|
+
// Reset internal data to initial values (or empty if no initial values)
|
|
2039
|
+
if (this.values) {
|
|
2040
|
+
const v = this.values;
|
|
2041
|
+
const newData = {};
|
|
2042
|
+
for (const [key, value] of Object.entries(v)) {
|
|
2043
|
+
if (key.startsWith("/")) {
|
|
2044
|
+
this.#setByPath(newData, key, value);
|
|
2045
|
+
} else {
|
|
2046
|
+
newData[key] = value;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
this.#data = newData;
|
|
2050
|
+
} else {
|
|
2051
|
+
this.#data = {};
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// Recompile form - this rebuilds the tree, re-applies defaults,
|
|
2055
|
+
// and rebuilds dependency graph
|
|
2056
|
+
this.#compile();
|
|
2057
|
+
|
|
2058
|
+
// Re-apply calculated values after compile
|
|
2059
|
+
this.#applyCalculatedValues();
|
|
2060
|
+
|
|
2061
|
+
// Emit reset event
|
|
2062
|
+
this.#emit("pw:reset", { data: structuredClone(this.#data) });
|
|
2063
|
+
|
|
2064
|
+
// Increment reset key to force Lit to recreate all DOM elements
|
|
2065
|
+
// This ensures .value bindings properly reflect reset values
|
|
2066
|
+
this.#resetKey++;
|
|
2067
|
+
this.requestUpdate();
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1634
2070
|
// ===== Utilities =====
|
|
1635
2071
|
#uiFor(path) {
|
|
1636
2072
|
if (!this.uiSchema) return undefined;
|
|
@@ -1656,17 +2092,28 @@ export class SchemaForm extends LitElement {
|
|
|
1656
2092
|
const withoutSlash = path.startsWith("/") ? path.substring(1) : path;
|
|
1657
2093
|
if (this.uiSchema[withoutSlash]) return this.uiSchema[withoutSlash];
|
|
1658
2094
|
|
|
1659
|
-
// Try nested navigation (e.g.,
|
|
2095
|
+
// Try nested navigation (e.g., /accountType/companyName)
|
|
1660
2096
|
// Skip array indices (numeric parts and wildcard *) when navigating UI schema
|
|
1661
2097
|
const parts = path.replace(/^\//, "").split("/");
|
|
1662
2098
|
let current = this.uiSchema;
|
|
1663
|
-
for (
|
|
2099
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2100
|
+
const part = parts[i];
|
|
1664
2101
|
// Skip numeric array indices and wildcard in UI schema navigation
|
|
1665
2102
|
if (/^\d+$/.test(part) || part === "*") continue;
|
|
1666
2103
|
|
|
2104
|
+
// Try both with and without leading slash for each segment
|
|
1667
2105
|
if (current && typeof current === "object" && part in current) {
|
|
1668
2106
|
current = current[part];
|
|
2107
|
+
} else if (current && typeof current === "object" && ("/" + part) in current) {
|
|
2108
|
+
current = current["/" + part];
|
|
1669
2109
|
} else {
|
|
2110
|
+
// If this is the first segment, try looking up the full path from root
|
|
2111
|
+
if (i === 0) {
|
|
2112
|
+
const fullPath = "/" + parts.join("/");
|
|
2113
|
+
if (this.uiSchema[fullPath]) {
|
|
2114
|
+
return this.uiSchema[fullPath];
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
1670
2117
|
return undefined;
|
|
1671
2118
|
}
|
|
1672
2119
|
}
|
|
@@ -1789,9 +2236,47 @@ export class SchemaForm extends LitElement {
|
|
|
1789
2236
|
|
|
1790
2237
|
#assignValue(path, val) {
|
|
1791
2238
|
this.#setByPath(this.#data, path, val);
|
|
1792
|
-
|
|
2239
|
+
|
|
2240
|
+
// Apply calculated values for any dependent fields
|
|
2241
|
+
// This will call requestUpdate() if there are dependents
|
|
2242
|
+
const hadDependents = this.#applyCalculatedValues(path);
|
|
2243
|
+
|
|
2244
|
+
// Only request update here if there were no dependents
|
|
2245
|
+
// (to avoid double render)
|
|
2246
|
+
if (!hadDependents) {
|
|
2247
|
+
this.requestUpdate();
|
|
2248
|
+
}
|
|
2249
|
+
|
|
1793
2250
|
const validity = { valid: true };
|
|
1794
|
-
this.#emit("pw:value-change", { name: path, value: val, validity });
|
|
2251
|
+
this.#emit("pw:value-change", { path, name: path, value: val, validity });
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
#applyCalculatedValues(changedPath) {
|
|
2255
|
+
// Find fields that depend on the changed path
|
|
2256
|
+
const dependents = this.#dependencies.get(changedPath);
|
|
2257
|
+
if (!dependents || dependents.size === 0) return false;
|
|
2258
|
+
|
|
2259
|
+
for (const targetPath of dependents) {
|
|
2260
|
+
const ui = this.#uiFor(targetPath);
|
|
2261
|
+
if (ui?.["ui:calculate"]) {
|
|
2262
|
+
// Check if override is allowed and user has modified the field
|
|
2263
|
+
const allowOverride = ui["ui:calculateOverride"] === true;
|
|
2264
|
+
const currentValue = this.#getByPath(this.#data, targetPath);
|
|
2265
|
+
const calculatedValue = this.#evaluateCalculation(ui["ui:calculate"]);
|
|
2266
|
+
|
|
2267
|
+
// Only update if:
|
|
2268
|
+
// 1. Override is not allowed, OR
|
|
2269
|
+
// 2. Override is allowed but field hasn't been manually modified (still matches previous calc)
|
|
2270
|
+
if (!allowOverride || currentValue === undefined || currentValue === null) {
|
|
2271
|
+
this.#setByPath(this.#data, targetPath, calculatedValue);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// Force re-render since there were dependents
|
|
2277
|
+
// This ensures ui:visibleWhen, ui:requiredWhen, ui:disabledWhen get re-evaluated
|
|
2278
|
+
this.requestUpdate();
|
|
2279
|
+
return true; // Indicate that we triggered a re-render
|
|
1795
2280
|
}
|
|
1796
2281
|
|
|
1797
2282
|
#getByPath(obj, path) {
|
|
@@ -1846,6 +2331,34 @@ export class SchemaForm extends LitElement {
|
|
|
1846
2331
|
return key === "radio" || key === "checkbox-group";
|
|
1847
2332
|
}
|
|
1848
2333
|
|
|
2334
|
+
/**
|
|
2335
|
+
* Render custom content from ui:before, ui:after values.
|
|
2336
|
+
* Supports:
|
|
2337
|
+
* - Function: (context) => html`...` - called with render context
|
|
2338
|
+
* - String starting with "slot:": looks up slotted element by name
|
|
2339
|
+
* - null/undefined: returns nothing
|
|
2340
|
+
* @param {Function|string|undefined} content - The content definition
|
|
2341
|
+
* @param {object} context - Render context with path, schema, value, etc.
|
|
2342
|
+
* @returns {TemplateResult|Element|typeof nothing}
|
|
2343
|
+
*/
|
|
2344
|
+
#renderCustomContent(content, context) {
|
|
2345
|
+
if (!content) return nothing;
|
|
2346
|
+
|
|
2347
|
+
// Function: call with context
|
|
2348
|
+
if (typeof content === "function") {
|
|
2349
|
+
return content(context);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// String: slot reference (e.g., "slot:myHeader")
|
|
2353
|
+
if (typeof content === "string" && content.startsWith("slot:")) {
|
|
2354
|
+
const slotName = content.slice(5); // Remove "slot:" prefix
|
|
2355
|
+
const slotEl = this.#slottedContent.get(slotName);
|
|
2356
|
+
return slotEl ? slotEl : nothing;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
return nothing;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
1849
2362
|
// ===== Event helpers =====
|
|
1850
2363
|
#emit(name, detail) {
|
|
1851
2364
|
this.dispatchEvent(
|
|
@@ -1871,4 +2384,4 @@ export class SchemaForm extends LitElement {
|
|
|
1871
2384
|
}
|
|
1872
2385
|
}
|
|
1873
2386
|
|
|
1874
|
-
customElements.define("pds-
|
|
2387
|
+
customElements.define("pds-form", SchemaForm);
|