@pure-ds/core 0.4.18 → 0.4.19
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} +536 -45
- 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",
|
|
@@ -1045,10 +1353,30 @@ export class SchemaForm extends LitElement {
|
|
|
1045
1353
|
const path = node.path;
|
|
1046
1354
|
const id = this.#idFromPath(path);
|
|
1047
1355
|
const label = node.title ?? this.#titleFromPath(path);
|
|
1048
|
-
|
|
1049
|
-
const required = this.#isRequired(path);
|
|
1356
|
+
let value = this.#getByPath(this.#data, path);
|
|
1050
1357
|
const ui = node.ui || this.#uiFor(path);
|
|
1051
1358
|
|
|
1359
|
+
// Evaluate calculated value if present
|
|
1360
|
+
if (ui?.["ui:calculate"]) {
|
|
1361
|
+
const calculatedValue = this.#evaluateCalculation(ui["ui:calculate"]);
|
|
1362
|
+
// Apply calculated value if field is empty or override not allowed
|
|
1363
|
+
if (value === undefined || value === null || !ui["ui:calculateOverride"]) {
|
|
1364
|
+
value = calculatedValue;
|
|
1365
|
+
// Also update internal data to keep in sync
|
|
1366
|
+
if (this.#getByPath(this.#data, path) !== calculatedValue) {
|
|
1367
|
+
this.#setByPath(this.#data, path, calculatedValue);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Evaluate conditional states
|
|
1373
|
+
const isDisabled = ui?.["ui:disabled"] ||
|
|
1374
|
+
(ui?.["ui:disabledWhen"] && this.#evaluateCondition(ui["ui:disabledWhen"]));
|
|
1375
|
+
const isRequired = this.#isRequired(path) ||
|
|
1376
|
+
(ui?.["ui:requiredWhen"] && this.#evaluateCondition(ui["ui:requiredWhen"]));
|
|
1377
|
+
const isReadonly = ui?.["ui:readonly"] ||
|
|
1378
|
+
(ui?.["ui:calculate"] && !ui["ui:calculateOverride"]);
|
|
1379
|
+
|
|
1052
1380
|
// Override hook before default field render
|
|
1053
1381
|
{
|
|
1054
1382
|
const override = this.#emitCancelable("pw:before-render-field", {
|
|
@@ -1061,23 +1389,45 @@ export class SchemaForm extends LitElement {
|
|
|
1061
1389
|
if (override?.render) return override.render();
|
|
1062
1390
|
}
|
|
1063
1391
|
|
|
1392
|
+
// Build attributes including conditional states
|
|
1393
|
+
const attrs = {
|
|
1394
|
+
...this.#nativeConstraints(path, node.schema),
|
|
1395
|
+
disabled: isDisabled,
|
|
1396
|
+
required: isRequired,
|
|
1397
|
+
readOnly: isReadonly,
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
// Build base render context (shared by ui:render, renderers, and ui:wrapper)
|
|
1401
|
+
const baseContext = {
|
|
1402
|
+
id,
|
|
1403
|
+
path,
|
|
1404
|
+
label,
|
|
1405
|
+
value,
|
|
1406
|
+
required: isRequired,
|
|
1407
|
+
ui,
|
|
1408
|
+
schema: node.schema,
|
|
1409
|
+
get: (p) => this.#getByPath(this.#data, p ?? path),
|
|
1410
|
+
set: (val, p) => this.#assignValue(p ?? path, val),
|
|
1411
|
+
attrs,
|
|
1412
|
+
host: this,
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
// Check for ui:render - completely custom inline renderer (same context as defineRenderer)
|
|
1416
|
+
if (ui?.["ui:render"] && typeof ui["ui:render"] === "function") {
|
|
1417
|
+
const customTpl = ui["ui:render"](baseContext);
|
|
1418
|
+
const before = this.#renderCustomContent(ui?.["ui:before"], baseContext);
|
|
1419
|
+
const after = this.#renderCustomContent(ui?.["ui:after"], baseContext);
|
|
1420
|
+
queueMicrotask(() =>
|
|
1421
|
+
this.#emit("pw:after-render-field", { path, schema: node.schema })
|
|
1422
|
+
);
|
|
1423
|
+
return html`${before}${customTpl}${after}`;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1064
1426
|
// Default renderer lookup: returns ONLY the control markup
|
|
1065
1427
|
const renderer =
|
|
1066
1428
|
this.#renderers.get(node.widgetKey) || this.#renderers.get("*");
|
|
1067
1429
|
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
|
-
})
|
|
1430
|
+
? renderer(baseContext)
|
|
1081
1431
|
: nothing;
|
|
1082
1432
|
|
|
1083
1433
|
// Post-creation tweak
|
|
@@ -1153,12 +1503,47 @@ export class SchemaForm extends LitElement {
|
|
|
1153
1503
|
return html`<span data-label>${label}</span> ${controlTpl}`;
|
|
1154
1504
|
};
|
|
1155
1505
|
|
|
1156
|
-
|
|
1506
|
+
// Build render context for custom content
|
|
1507
|
+
const renderContext = {
|
|
1508
|
+
id,
|
|
1509
|
+
path,
|
|
1510
|
+
label,
|
|
1511
|
+
value,
|
|
1512
|
+
required: isRequired,
|
|
1513
|
+
ui,
|
|
1514
|
+
schema: node.schema,
|
|
1515
|
+
get: (p) => this.#getByPath(this.#data, p ?? path),
|
|
1516
|
+
set: (val, p) => this.#assignValue(p ?? path, val),
|
|
1517
|
+
attrs,
|
|
1518
|
+
host: this,
|
|
1519
|
+
// Additional context for ui:wrapper
|
|
1520
|
+
control: controlTpl,
|
|
1521
|
+
help: help ? html`<div data-help>${help}</div>` : nothing,
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
// Check for ui:wrapper - custom wrapper replaces the entire label structure
|
|
1525
|
+
if (ui?.["ui:wrapper"] && typeof ui["ui:wrapper"] === "function") {
|
|
1526
|
+
const wrapped = ui["ui:wrapper"](renderContext);
|
|
1527
|
+
const before = this.#renderCustomContent(ui?.["ui:before"], renderContext);
|
|
1528
|
+
const after = this.#renderCustomContent(ui?.["ui:after"], renderContext);
|
|
1529
|
+
return html`${before}${wrapped}${after}`;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Standard label wrapper with ui:before/ui:after
|
|
1533
|
+
const before = this.#renderCustomContent(ui?.["ui:before"], renderContext);
|
|
1534
|
+
const after = this.#renderCustomContent(ui?.["ui:after"], renderContext);
|
|
1535
|
+
|
|
1536
|
+
const labelTpl = html`
|
|
1157
1537
|
<label for=${id} ?data-toggle=${isToggle} class=${ifDefined(labelClass)}>
|
|
1158
1538
|
${renderControlAndLabel(isToggle)}
|
|
1159
1539
|
${help ? html`<div data-help>${help}</div>` : nothing}
|
|
1160
1540
|
</label>
|
|
1161
1541
|
`;
|
|
1542
|
+
|
|
1543
|
+
if (before || after) {
|
|
1544
|
+
return html`${before}${labelTpl}${after}`;
|
|
1545
|
+
}
|
|
1546
|
+
return labelTpl;
|
|
1162
1547
|
}
|
|
1163
1548
|
|
|
1164
1549
|
// ===== Default renderers: controls only (no spread arrays) =====
|
|
@@ -1177,6 +1562,7 @@ export class SchemaForm extends LitElement {
|
|
|
1177
1562
|
maxlength=${ifDefined(attrs.maxLength)}
|
|
1178
1563
|
pattern=${ifDefined(attrs.pattern)}
|
|
1179
1564
|
?readonly=${!!attrs.readOnly}
|
|
1565
|
+
?disabled=${!!attrs.disabled}
|
|
1180
1566
|
?required=${!!attrs.required}
|
|
1181
1567
|
autocomplete=${ifDefined(attrs.autocomplete)}
|
|
1182
1568
|
@input=${(e) => set(e.target.value)}
|
|
@@ -1197,6 +1583,7 @@ export class SchemaForm extends LitElement {
|
|
|
1197
1583
|
maxlength=${ifDefined(attrs.maxLength)}
|
|
1198
1584
|
pattern=${ifDefined(attrs.pattern)}
|
|
1199
1585
|
?readonly=${!!attrs.readOnly}
|
|
1586
|
+
?disabled=${!!attrs.disabled}
|
|
1200
1587
|
?required=${!!attrs.required}
|
|
1201
1588
|
autocomplete=${ifDefined(attrs.autocomplete)}
|
|
1202
1589
|
list=${ifDefined(ui?.["ui:datalist"] ? `${id}-datalist` : attrs.list)}
|
|
@@ -1226,6 +1613,7 @@ export class SchemaForm extends LitElement {
|
|
|
1226
1613
|
minlength=${ifDefined(attrs.minLength)}
|
|
1227
1614
|
maxlength=${ifDefined(attrs.maxLength)}
|
|
1228
1615
|
?readonly=${!!attrs.readOnly}
|
|
1616
|
+
?disabled=${!!attrs.disabled}
|
|
1229
1617
|
?required=${!!attrs.required}
|
|
1230
1618
|
@input=${(e) => set(e.target.value)}
|
|
1231
1619
|
></textarea>
|
|
@@ -1247,6 +1635,7 @@ export class SchemaForm extends LitElement {
|
|
|
1247
1635
|
max=${ifDefined(attrs.max)}
|
|
1248
1636
|
step=${ifDefined(step)}
|
|
1249
1637
|
?readonly=${!!attrs.readOnly}
|
|
1638
|
+
?disabled=${!!attrs.disabled}
|
|
1250
1639
|
?required=${!!attrs.required}
|
|
1251
1640
|
@input=${(e) => {
|
|
1252
1641
|
const v = e.target.value;
|
|
@@ -1294,6 +1683,7 @@ export class SchemaForm extends LitElement {
|
|
|
1294
1683
|
max=${max}
|
|
1295
1684
|
step=${step}
|
|
1296
1685
|
.value=${value ?? min}
|
|
1686
|
+
?disabled=${!!attrs.disabled}
|
|
1297
1687
|
@input=${(e) => set(Number(e.target.value))}
|
|
1298
1688
|
/>
|
|
1299
1689
|
<div class="range-bubble" aria-hidden="true">${value ?? min}</div>
|
|
@@ -1312,6 +1702,7 @@ export class SchemaForm extends LitElement {
|
|
|
1312
1702
|
type="email"
|
|
1313
1703
|
.value=${value ?? ""}
|
|
1314
1704
|
?readonly=${!!attrs.readOnly}
|
|
1705
|
+
?disabled=${!!attrs.disabled}
|
|
1315
1706
|
?required=${!!attrs.required}
|
|
1316
1707
|
autocomplete=${ifDefined(attrs.autocomplete)}
|
|
1317
1708
|
@input=${(e) => set(e.target.value)}
|
|
@@ -1336,6 +1727,7 @@ export class SchemaForm extends LitElement {
|
|
|
1336
1727
|
minlength=${ifDefined(attrs.minLength)}
|
|
1337
1728
|
maxlength=${ifDefined(attrs.maxLength)}
|
|
1338
1729
|
?readonly=${!!attrs.readOnly}
|
|
1730
|
+
?disabled=${!!attrs.disabled}
|
|
1339
1731
|
?required=${!!attrs.required}
|
|
1340
1732
|
autocomplete=${autocomplete}
|
|
1341
1733
|
@input=${(e) => set(e.target.value)}
|
|
@@ -1354,6 +1746,7 @@ export class SchemaForm extends LitElement {
|
|
|
1354
1746
|
type="url"
|
|
1355
1747
|
.value=${value ?? ""}
|
|
1356
1748
|
?readonly=${!!attrs.readOnly}
|
|
1749
|
+
?disabled=${!!attrs.disabled}
|
|
1357
1750
|
?required=${!!attrs.required}
|
|
1358
1751
|
@input=${(e) => set(e.target.value)}
|
|
1359
1752
|
/>
|
|
@@ -1372,6 +1765,7 @@ export class SchemaForm extends LitElement {
|
|
|
1372
1765
|
min=${ifDefined(attrs.min)}
|
|
1373
1766
|
max=${ifDefined(attrs.max)}
|
|
1374
1767
|
?readonly=${!!attrs.readOnly}
|
|
1768
|
+
?disabled=${!!attrs.disabled}
|
|
1375
1769
|
?required=${!!attrs.required}
|
|
1376
1770
|
@input=${(e) => set(e.target.value)}
|
|
1377
1771
|
/>
|
|
@@ -1388,6 +1782,7 @@ export class SchemaForm extends LitElement {
|
|
|
1388
1782
|
type="time"
|
|
1389
1783
|
.value=${value ?? ""}
|
|
1390
1784
|
?readonly=${!!attrs.readOnly}
|
|
1785
|
+
?disabled=${!!attrs.disabled}
|
|
1391
1786
|
?required=${!!attrs.required}
|
|
1392
1787
|
@input=${(e) => set(e.target.value)}
|
|
1393
1788
|
/>
|
|
@@ -1404,6 +1799,7 @@ export class SchemaForm extends LitElement {
|
|
|
1404
1799
|
type="color"
|
|
1405
1800
|
.value=${value ?? ""}
|
|
1406
1801
|
?readonly=${!!attrs.readOnly}
|
|
1802
|
+
?disabled=${!!attrs.disabled}
|
|
1407
1803
|
?required=${!!attrs.required}
|
|
1408
1804
|
@input=${(e) => set(e.target.value)}
|
|
1409
1805
|
/>
|
|
@@ -1420,6 +1816,7 @@ export class SchemaForm extends LitElement {
|
|
|
1420
1816
|
type="datetime-local"
|
|
1421
1817
|
.value=${value ?? ""}
|
|
1422
1818
|
?readonly=${!!attrs.readOnly}
|
|
1819
|
+
?disabled=${!!attrs.disabled}
|
|
1423
1820
|
?required=${!!attrs.required}
|
|
1424
1821
|
@input=${(e) => set(e.target.value)}
|
|
1425
1822
|
/>
|
|
@@ -1434,6 +1831,7 @@ export class SchemaForm extends LitElement {
|
|
|
1434
1831
|
name=${path}
|
|
1435
1832
|
type="checkbox"
|
|
1436
1833
|
.checked=${!!value}
|
|
1834
|
+
?disabled=${!!attrs.disabled}
|
|
1437
1835
|
?required=${!!attrs.required}
|
|
1438
1836
|
@change=${(e) => set(!!e.target.checked)}
|
|
1439
1837
|
/>
|
|
@@ -1449,6 +1847,7 @@ export class SchemaForm extends LitElement {
|
|
|
1449
1847
|
name=${path}
|
|
1450
1848
|
type="checkbox"
|
|
1451
1849
|
.checked=${!!value}
|
|
1850
|
+
?disabled=${!!attrs.disabled}
|
|
1452
1851
|
?required=${!!attrs.required}
|
|
1453
1852
|
@change=${(e) => set(!!e.target.checked)}
|
|
1454
1853
|
/>
|
|
@@ -1461,13 +1860,13 @@ export class SchemaForm extends LitElement {
|
|
|
1461
1860
|
const useDropdown =
|
|
1462
1861
|
host.#getOption("widgets.selects", "standard") === "dropdown" ||
|
|
1463
1862
|
ui?.["ui:dropdown"] === true;
|
|
1464
|
-
const enumValues = schema
|
|
1465
|
-
const enumLabels = schema.enumNames || enumValues;
|
|
1863
|
+
const { values: enumValues, labels: enumLabels } = host.#extractEnumOptions(schema);
|
|
1466
1864
|
return html`
|
|
1467
1865
|
<select
|
|
1468
1866
|
id=${id}
|
|
1469
1867
|
name=${path}
|
|
1470
1868
|
.value=${value ?? ""}
|
|
1869
|
+
?disabled=${!!attrs.disabled}
|
|
1471
1870
|
?required=${!!attrs.required}
|
|
1472
1871
|
?data-dropdown=${useDropdown}
|
|
1473
1872
|
@change=${(e) => set(e.target.value)}
|
|
@@ -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;
|
|
@@ -1789,11 +2225,38 @@ export class SchemaForm extends LitElement {
|
|
|
1789
2225
|
|
|
1790
2226
|
#assignValue(path, val) {
|
|
1791
2227
|
this.#setByPath(this.#data, path, val);
|
|
2228
|
+
|
|
2229
|
+
// Apply calculated values for any dependent fields
|
|
2230
|
+
this.#applyCalculatedValues(path);
|
|
2231
|
+
|
|
1792
2232
|
this.requestUpdate();
|
|
1793
2233
|
const validity = { valid: true };
|
|
1794
2234
|
this.#emit("pw:value-change", { name: path, value: val, validity });
|
|
1795
2235
|
}
|
|
1796
2236
|
|
|
2237
|
+
#applyCalculatedValues(changedPath) {
|
|
2238
|
+
// Find fields that depend on the changed path
|
|
2239
|
+
const dependents = this.#dependencies.get(changedPath);
|
|
2240
|
+
if (!dependents) return;
|
|
2241
|
+
|
|
2242
|
+
for (const targetPath of dependents) {
|
|
2243
|
+
const ui = this.#uiFor(targetPath);
|
|
2244
|
+
if (ui?.["ui:calculate"]) {
|
|
2245
|
+
// Check if override is allowed and user has modified the field
|
|
2246
|
+
const allowOverride = ui["ui:calculateOverride"] === true;
|
|
2247
|
+
const currentValue = this.#getByPath(this.#data, targetPath);
|
|
2248
|
+
const calculatedValue = this.#evaluateCalculation(ui["ui:calculate"]);
|
|
2249
|
+
|
|
2250
|
+
// Only update if:
|
|
2251
|
+
// 1. Override is not allowed, OR
|
|
2252
|
+
// 2. Override is allowed but field hasn't been manually modified (still matches previous calc)
|
|
2253
|
+
if (!allowOverride || currentValue === undefined || currentValue === null) {
|
|
2254
|
+
this.#setByPath(this.#data, targetPath, calculatedValue);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
1797
2260
|
#getByPath(obj, path) {
|
|
1798
2261
|
if (!path || path === "") return obj;
|
|
1799
2262
|
const parts = path.split("/").filter(Boolean);
|
|
@@ -1846,6 +2309,34 @@ export class SchemaForm extends LitElement {
|
|
|
1846
2309
|
return key === "radio" || key === "checkbox-group";
|
|
1847
2310
|
}
|
|
1848
2311
|
|
|
2312
|
+
/**
|
|
2313
|
+
* Render custom content from ui:before, ui:after values.
|
|
2314
|
+
* Supports:
|
|
2315
|
+
* - Function: (context) => html`...` - called with render context
|
|
2316
|
+
* - String starting with "slot:": looks up slotted element by name
|
|
2317
|
+
* - null/undefined: returns nothing
|
|
2318
|
+
* @param {Function|string|undefined} content - The content definition
|
|
2319
|
+
* @param {object} context - Render context with path, schema, value, etc.
|
|
2320
|
+
* @returns {TemplateResult|Element|typeof nothing}
|
|
2321
|
+
*/
|
|
2322
|
+
#renderCustomContent(content, context) {
|
|
2323
|
+
if (!content) return nothing;
|
|
2324
|
+
|
|
2325
|
+
// Function: call with context
|
|
2326
|
+
if (typeof content === "function") {
|
|
2327
|
+
return content(context);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// String: slot reference (e.g., "slot:myHeader")
|
|
2331
|
+
if (typeof content === "string" && content.startsWith("slot:")) {
|
|
2332
|
+
const slotName = content.slice(5); // Remove "slot:" prefix
|
|
2333
|
+
const slotEl = this.#slottedContent.get(slotName);
|
|
2334
|
+
return slotEl ? slotEl : nothing;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
return nothing;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
1849
2340
|
// ===== Event helpers =====
|
|
1850
2341
|
#emit(name, detail) {
|
|
1851
2342
|
this.dispatchEvent(
|
|
@@ -1871,4 +2362,4 @@ export class SchemaForm extends LitElement {
|
|
|
1871
2362
|
}
|
|
1872
2363
|
}
|
|
1873
2364
|
|
|
1874
|
-
customElements.define("pds-
|
|
2365
|
+
customElements.define("pds-form", SchemaForm);
|