@wildwinter/app-shell 0.55.0 → 0.57.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/README.md +64 -0
- package/dist/controls.css +81 -0
- package/dist/controls.css.map +1 -1
- package/dist/index.cjs +24 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +1 -1
- package/dist/tokens.css.map +1 -1
- package/dist/value-rows.cjs +279 -10
- package/dist/value-rows.cjs.map +1 -1
- package/dist/value-rows.d.cts +85 -1
- package/dist/value-rows.d.ts +85 -1
- package/dist/value-rows.js +276 -13
- package/dist/value-rows.js.map +1 -1
- package/package.json +1 -1
package/dist/value-rows.d.cts
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
import { PickSource } from '@wildwinter/toolkit/extensions';
|
|
2
2
|
import { FieldDeclLike } from './value-control.cjs';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Whether a node's value for `decl` is set: held, and not the declaration's default. A record counts as its
|
|
6
|
+
* default when each of its fields does and it holds no other key, so `{}` and a record of defaults are both
|
|
7
|
+
* unset; anything else is compared with `defaultOf`, deeply and regardless of key order.
|
|
8
|
+
*/
|
|
9
|
+
declare function isSet(decl: ValueDecl, value: unknown): boolean;
|
|
10
|
+
interface FoldSectionOptions {
|
|
11
|
+
/** The extension's id: the fold is remembered by it. */
|
|
12
|
+
extension: string;
|
|
13
|
+
/** The extension's name, the caption. */
|
|
14
|
+
caption: string;
|
|
15
|
+
/** The node the section is for, by any key unique to it: a row cleared while folded stays while the host
|
|
16
|
+
* keeps drawing this node. */
|
|
17
|
+
node: string;
|
|
18
|
+
/** The extension's fields for the node's kind, in the manifest's order. */
|
|
19
|
+
fields: readonly ValueDecl[];
|
|
20
|
+
/** The node's values for the extension, or undefined where it holds none. */
|
|
21
|
+
entry: Record<string, unknown> | undefined;
|
|
22
|
+
/** One field's row, drawn by the host. */
|
|
23
|
+
row: (decl: ValueDecl) => HTMLElement;
|
|
24
|
+
/** A heading above the first shown field of a group. */
|
|
25
|
+
group: (name: string) => HTMLElement;
|
|
26
|
+
/** Shown above the rows, folded or not (a lock line). */
|
|
27
|
+
lead?: readonly HTMLElement[];
|
|
28
|
+
/** Shown after the rows, folded or not (keys the extension doesn't declare). */
|
|
29
|
+
tail?: readonly HTMLElement[];
|
|
30
|
+
/** The section's own class, beside `shell-fold`. */
|
|
31
|
+
className?: string;
|
|
32
|
+
/** The caption's class, beside `shell-fold-cap`. */
|
|
33
|
+
captionClassName?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ExtensionFold {
|
|
36
|
+
/** Draw one extension's section for one node: the caption and disclosure, then the rows the fold shows. */
|
|
37
|
+
section(o: FoldSectionOptions): HTMLElement;
|
|
38
|
+
/** Whether an extension's section is open (every field). Folded until opened. */
|
|
39
|
+
isOpen(extension: string): boolean;
|
|
40
|
+
setOpen(extension: string, open: boolean): void;
|
|
41
|
+
/** After a redraw: forget the rows kept for nodes it didn't draw, so leaving a node and coming back shows
|
|
42
|
+
* only what is set. A host that redraws its whole view calls it at the end of each, as a row cache's. */
|
|
43
|
+
sweep(): void;
|
|
44
|
+
}
|
|
45
|
+
/** The caption's words: the name, and the count when anything is set. */
|
|
46
|
+
declare function foldCaption(name: string, set: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* The fold for a host's inspector: one per host, kept across its redraws, so the fold stays as left from node
|
|
49
|
+
* to node.
|
|
50
|
+
*/
|
|
51
|
+
declare function extensionFold(): ExtensionFold;
|
|
52
|
+
|
|
53
|
+
/** A map whose values are records: edited in the grid, summarised in the inspector. */
|
|
54
|
+
declare function isRecordMap(decl: ValueDecl): boolean;
|
|
55
|
+
/** A map key's name: a node's from the host, else the key itself. */
|
|
56
|
+
declare function recordMapKeyLabel(decl: ValueDecl, key: string, adapter?: ValueAdapter): string;
|
|
57
|
+
/** A value as words, the way its control shows it: a pick's or a reference's name, flags joined, a boolean as
|
|
58
|
+
* the field's own name. Empty for a value that isn't set. */
|
|
59
|
+
declare function valueText(decl: ValueDecl, value: unknown, adapter?: ValueAdapter): string;
|
|
60
|
+
/** The inspector's summary of a map of records: a line per key, "TAM: smile, nod, WREN", the record's set
|
|
61
|
+
* fields in the manifest's order; a key with nothing set is its name alone. */
|
|
62
|
+
declare function recordMapSummary(decl: ValueDecl, value: unknown, adapter?: ValueAdapter): string[];
|
|
63
|
+
interface RecordMapGridOptions {
|
|
64
|
+
/** The map's declaration: `isRecordMap`. */
|
|
65
|
+
decl: ValueDecl;
|
|
66
|
+
/** The node's value, or undefined where it holds none. */
|
|
67
|
+
value: unknown;
|
|
68
|
+
/** The host's nodes, for a reference key and the cells' picks and references. */
|
|
69
|
+
adapter?: ValueAdapter;
|
|
70
|
+
/** Commit the whole map; undefined clears it. Absent: read-only. */
|
|
71
|
+
onCommit?: (value: unknown) => void;
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
/** The class on each cell's control, as the host's own rows carry. */
|
|
74
|
+
controlClassName?: string;
|
|
75
|
+
}
|
|
76
|
+
interface RecordMapGrid {
|
|
77
|
+
readonly el: HTMLElement;
|
|
78
|
+
update(opts: RecordMapGridOptions): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The grid: a row per key, a column per record field, each cell the row value-rows draws for that field (so
|
|
82
|
+
* picks, their checks and their problems are the same as in the inspector), a remove button per row, and the
|
|
83
|
+
* key adder under it. Keeps its cells across `update`, so a host that redraws on every change keeps the
|
|
84
|
+
* author's place.
|
|
85
|
+
*/
|
|
86
|
+
declare function recordMapGrid(initial: RecordMapGridOptions): RecordMapGrid;
|
|
87
|
+
|
|
4
88
|
/** A declaration of any type, as an extension's manifest writes it. */
|
|
5
89
|
interface ValueDecl extends FieldDeclLike {
|
|
6
90
|
/** The caption; the name when absent. */
|
|
@@ -86,4 +170,4 @@ interface ValueRows {
|
|
|
86
170
|
}
|
|
87
171
|
declare function valueRows(): ValueRows;
|
|
88
172
|
|
|
89
|
-
export { type ValueAdapter, type ValueDecl, type ValueNode, type ValueRowHandle, type ValueRowOptions, type ValueRows, valueRow, valueRows };
|
|
173
|
+
export { type ExtensionFold, type FoldSectionOptions, type RecordMapGrid, type RecordMapGridOptions, type ValueAdapter, type ValueDecl, type ValueNode, type ValueRowHandle, type ValueRowOptions, type ValueRows, extensionFold, foldCaption, isRecordMap, isSet, recordMapGrid, recordMapKeyLabel, recordMapSummary, valueRow, valueRows, valueText };
|
package/dist/value-rows.d.ts
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
import { PickSource } from '@wildwinter/toolkit/extensions';
|
|
2
2
|
import { FieldDeclLike } from './value-control.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Whether a node's value for `decl` is set: held, and not the declaration's default. A record counts as its
|
|
6
|
+
* default when each of its fields does and it holds no other key, so `{}` and a record of defaults are both
|
|
7
|
+
* unset; anything else is compared with `defaultOf`, deeply and regardless of key order.
|
|
8
|
+
*/
|
|
9
|
+
declare function isSet(decl: ValueDecl, value: unknown): boolean;
|
|
10
|
+
interface FoldSectionOptions {
|
|
11
|
+
/** The extension's id: the fold is remembered by it. */
|
|
12
|
+
extension: string;
|
|
13
|
+
/** The extension's name, the caption. */
|
|
14
|
+
caption: string;
|
|
15
|
+
/** The node the section is for, by any key unique to it: a row cleared while folded stays while the host
|
|
16
|
+
* keeps drawing this node. */
|
|
17
|
+
node: string;
|
|
18
|
+
/** The extension's fields for the node's kind, in the manifest's order. */
|
|
19
|
+
fields: readonly ValueDecl[];
|
|
20
|
+
/** The node's values for the extension, or undefined where it holds none. */
|
|
21
|
+
entry: Record<string, unknown> | undefined;
|
|
22
|
+
/** One field's row, drawn by the host. */
|
|
23
|
+
row: (decl: ValueDecl) => HTMLElement;
|
|
24
|
+
/** A heading above the first shown field of a group. */
|
|
25
|
+
group: (name: string) => HTMLElement;
|
|
26
|
+
/** Shown above the rows, folded or not (a lock line). */
|
|
27
|
+
lead?: readonly HTMLElement[];
|
|
28
|
+
/** Shown after the rows, folded or not (keys the extension doesn't declare). */
|
|
29
|
+
tail?: readonly HTMLElement[];
|
|
30
|
+
/** The section's own class, beside `shell-fold`. */
|
|
31
|
+
className?: string;
|
|
32
|
+
/** The caption's class, beside `shell-fold-cap`. */
|
|
33
|
+
captionClassName?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ExtensionFold {
|
|
36
|
+
/** Draw one extension's section for one node: the caption and disclosure, then the rows the fold shows. */
|
|
37
|
+
section(o: FoldSectionOptions): HTMLElement;
|
|
38
|
+
/** Whether an extension's section is open (every field). Folded until opened. */
|
|
39
|
+
isOpen(extension: string): boolean;
|
|
40
|
+
setOpen(extension: string, open: boolean): void;
|
|
41
|
+
/** After a redraw: forget the rows kept for nodes it didn't draw, so leaving a node and coming back shows
|
|
42
|
+
* only what is set. A host that redraws its whole view calls it at the end of each, as a row cache's. */
|
|
43
|
+
sweep(): void;
|
|
44
|
+
}
|
|
45
|
+
/** The caption's words: the name, and the count when anything is set. */
|
|
46
|
+
declare function foldCaption(name: string, set: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* The fold for a host's inspector: one per host, kept across its redraws, so the fold stays as left from node
|
|
49
|
+
* to node.
|
|
50
|
+
*/
|
|
51
|
+
declare function extensionFold(): ExtensionFold;
|
|
52
|
+
|
|
53
|
+
/** A map whose values are records: edited in the grid, summarised in the inspector. */
|
|
54
|
+
declare function isRecordMap(decl: ValueDecl): boolean;
|
|
55
|
+
/** A map key's name: a node's from the host, else the key itself. */
|
|
56
|
+
declare function recordMapKeyLabel(decl: ValueDecl, key: string, adapter?: ValueAdapter): string;
|
|
57
|
+
/** A value as words, the way its control shows it: a pick's or a reference's name, flags joined, a boolean as
|
|
58
|
+
* the field's own name. Empty for a value that isn't set. */
|
|
59
|
+
declare function valueText(decl: ValueDecl, value: unknown, adapter?: ValueAdapter): string;
|
|
60
|
+
/** The inspector's summary of a map of records: a line per key, "TAM: smile, nod, WREN", the record's set
|
|
61
|
+
* fields in the manifest's order; a key with nothing set is its name alone. */
|
|
62
|
+
declare function recordMapSummary(decl: ValueDecl, value: unknown, adapter?: ValueAdapter): string[];
|
|
63
|
+
interface RecordMapGridOptions {
|
|
64
|
+
/** The map's declaration: `isRecordMap`. */
|
|
65
|
+
decl: ValueDecl;
|
|
66
|
+
/** The node's value, or undefined where it holds none. */
|
|
67
|
+
value: unknown;
|
|
68
|
+
/** The host's nodes, for a reference key and the cells' picks and references. */
|
|
69
|
+
adapter?: ValueAdapter;
|
|
70
|
+
/** Commit the whole map; undefined clears it. Absent: read-only. */
|
|
71
|
+
onCommit?: (value: unknown) => void;
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
/** The class on each cell's control, as the host's own rows carry. */
|
|
74
|
+
controlClassName?: string;
|
|
75
|
+
}
|
|
76
|
+
interface RecordMapGrid {
|
|
77
|
+
readonly el: HTMLElement;
|
|
78
|
+
update(opts: RecordMapGridOptions): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The grid: a row per key, a column per record field, each cell the row value-rows draws for that field (so
|
|
82
|
+
* picks, their checks and their problems are the same as in the inspector), a remove button per row, and the
|
|
83
|
+
* key adder under it. Keeps its cells across `update`, so a host that redraws on every change keeps the
|
|
84
|
+
* author's place.
|
|
85
|
+
*/
|
|
86
|
+
declare function recordMapGrid(initial: RecordMapGridOptions): RecordMapGrid;
|
|
87
|
+
|
|
4
88
|
/** A declaration of any type, as an extension's manifest writes it. */
|
|
5
89
|
interface ValueDecl extends FieldDeclLike {
|
|
6
90
|
/** The caption; the name when absent. */
|
|
@@ -86,4 +170,4 @@ interface ValueRows {
|
|
|
86
170
|
}
|
|
87
171
|
declare function valueRows(): ValueRows;
|
|
88
172
|
|
|
89
|
-
export { type ValueAdapter, type ValueDecl, type ValueNode, type ValueRowHandle, type ValueRowOptions, type ValueRows, valueRow, valueRows };
|
|
173
|
+
export { type ExtensionFold, type FoldSectionOptions, type RecordMapGrid, type RecordMapGridOptions, type ValueAdapter, type ValueDecl, type ValueNode, type ValueRowHandle, type ValueRowOptions, type ValueRows, extensionFold, foldCaption, isRecordMap, isSet, recordMapGrid, recordMapKeyLabel, recordMapSummary, valueRow, valueRows, valueText };
|
package/dist/value-rows.js
CHANGED
|
@@ -1,12 +1,275 @@
|
|
|
1
1
|
import { fieldRow, fieldRowFocus } from './chunk-XJM733E6.js';
|
|
2
|
-
import { el, iconBtn, moveItem } from './chunk-W4WC4FO5.js';
|
|
3
|
-
import { pickChoices, valueProblems
|
|
2
|
+
import { el, iconBtn, iconNode, moveItem } from './chunk-W4WC4FO5.js';
|
|
3
|
+
import { stableStringify, defaultOf, pickChoices, valueProblems } from '@wildwinter/toolkit/extensions';
|
|
4
4
|
|
|
5
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6
|
+
function isSet(decl, value) {
|
|
7
|
+
if (value === void 0) return false;
|
|
8
|
+
if (decl.type === "record" && decl.default === void 0 && isObject(value)) {
|
|
9
|
+
const fields = decl.fields ?? [];
|
|
10
|
+
const declared = new Set(fields.map((f) => f.name));
|
|
11
|
+
if (Object.entries(value).some(([k, v]) => !declared.has(k) && v !== void 0)) return true;
|
|
12
|
+
return fields.some((f) => isSet(f, value[f.name]));
|
|
13
|
+
}
|
|
14
|
+
return stableStringify(value) !== stableStringify(defaultOf(decl));
|
|
15
|
+
}
|
|
16
|
+
function foldCaption(name, set) {
|
|
17
|
+
return set > 0 ? `${name} (${set} set)` : name;
|
|
18
|
+
}
|
|
19
|
+
function extensionFold() {
|
|
20
|
+
const open = /* @__PURE__ */ new Set();
|
|
21
|
+
const shown = /* @__PURE__ */ new Map();
|
|
22
|
+
const keyOf = (o) => `${o.extension}\0${o.node}`;
|
|
23
|
+
const visible = (o, setNames) => {
|
|
24
|
+
const was = shown.get(keyOf(o));
|
|
25
|
+
const fields = was ? /* @__PURE__ */ new Set([...was.fields, ...setNames]) : new Set(setNames);
|
|
26
|
+
shown.set(keyOf(o), { fields, used: true });
|
|
27
|
+
return fields;
|
|
28
|
+
};
|
|
29
|
+
const fold = {
|
|
30
|
+
isOpen: (extension) => open.has(extension),
|
|
31
|
+
setOpen(extension, on) {
|
|
32
|
+
if (on) open.add(extension);
|
|
33
|
+
else open.delete(extension);
|
|
34
|
+
for (const key of shown.keys()) if (key.startsWith(`${extension}\0`)) shown.delete(key);
|
|
35
|
+
},
|
|
36
|
+
sweep() {
|
|
37
|
+
for (const [key, entry] of shown) {
|
|
38
|
+
if (!entry.used) shown.delete(key);
|
|
39
|
+
else entry.used = false;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
section(o) {
|
|
43
|
+
const sec = el("div", `shell-fold${o.className ? ` ${o.className}` : ""}`);
|
|
44
|
+
sec.dataset["extension"] = o.extension;
|
|
45
|
+
const cap = el("button", `shell-fold-cap${o.captionClassName ? ` ${o.captionClassName}` : ""}`);
|
|
46
|
+
cap.type = "button";
|
|
47
|
+
const body = el("div", "shell-fold-body");
|
|
48
|
+
sec.append(cap, body);
|
|
49
|
+
const draw = () => {
|
|
50
|
+
const isOpen = open.has(o.extension);
|
|
51
|
+
const setNames = new Set(o.fields.filter((f) => isSet(f, o.entry?.[f.name])).map((f) => f.name));
|
|
52
|
+
const show = isOpen ? void 0 : visible(o, setNames);
|
|
53
|
+
cap.replaceChildren(iconNode(isOpen ? "expanded" : "collapsed"), el("span", "shell-fold-name", foldCaption(o.caption, setNames.size)));
|
|
54
|
+
cap.setAttribute("aria-expanded", String(isOpen));
|
|
55
|
+
cap.dataset["tip"] = isOpen ? `Show only the fields set here` : `Show every ${o.caption} field`;
|
|
56
|
+
sec.classList.toggle("shell-fold-open", isOpen);
|
|
57
|
+
const rows = [...o.lead ?? []];
|
|
58
|
+
let group;
|
|
59
|
+
for (const decl of o.fields) {
|
|
60
|
+
if (show && !show.has(decl.name)) continue;
|
|
61
|
+
const g = decl.group;
|
|
62
|
+
if (g !== void 0 && g !== group) {
|
|
63
|
+
group = g;
|
|
64
|
+
rows.push(o.group(g));
|
|
65
|
+
}
|
|
66
|
+
rows.push(o.row(decl));
|
|
67
|
+
}
|
|
68
|
+
rows.push(...o.tail ?? []);
|
|
69
|
+
body.replaceChildren(...rows);
|
|
70
|
+
};
|
|
71
|
+
cap.addEventListener("click", () => {
|
|
72
|
+
fold.setOpen(o.extension, !open.has(o.extension));
|
|
73
|
+
draw();
|
|
74
|
+
});
|
|
75
|
+
draw();
|
|
76
|
+
return sec;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return fold;
|
|
80
|
+
}
|
|
81
|
+
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
82
|
+
function isRecordMap(decl) {
|
|
83
|
+
return decl.type === "map" && decl.of?.type === "record";
|
|
84
|
+
}
|
|
85
|
+
function recordMapKeyLabel(decl, key, adapter) {
|
|
86
|
+
const kind = decl.key?.type === "reference" ? decl.key.to ?? "" : void 0;
|
|
87
|
+
if (kind === void 0 || !adapter) return key;
|
|
88
|
+
return adapter.nodes(kind).find((n) => n.id === key)?.label ?? adapter.label?.(kind, key) ?? key;
|
|
89
|
+
}
|
|
90
|
+
function valueText(decl, value, adapter) {
|
|
91
|
+
if (!isSet(decl, value)) return "";
|
|
92
|
+
if (decl.type === "pick" && typeof value === "string") {
|
|
93
|
+
const choice = pickChoices(decl, {
|
|
94
|
+
...adapter ? { nodes: (kind) => adapter.nodes(kind) } : {},
|
|
95
|
+
...adapter?.list ? { list: (name) => adapter.list(name) } : {}
|
|
96
|
+
}).find((c) => c.value === value);
|
|
97
|
+
return choice?.label ?? value;
|
|
98
|
+
}
|
|
99
|
+
if (decl.type === "reference" && typeof value === "string" && decl.to !== void 0 && adapter) {
|
|
100
|
+
return adapter.nodes(decl.to).find((n) => n.id === value)?.label ?? adapter.label?.(decl.to, value) ?? value;
|
|
101
|
+
}
|
|
102
|
+
if (decl.type === "boolean") return value === true ? decl.label ?? decl.name : "";
|
|
103
|
+
if (Array.isArray(value)) return value.map(String).join(", ");
|
|
104
|
+
if (isObject2(value)) return JSON.stringify(value);
|
|
105
|
+
return String(value);
|
|
106
|
+
}
|
|
107
|
+
function recordMapSummary(decl, value, adapter) {
|
|
108
|
+
if (!isObject2(value)) return [];
|
|
109
|
+
const fields = decl.of?.fields ?? [];
|
|
110
|
+
return Object.entries(value).map(([key, record]) => {
|
|
111
|
+
const name = recordMapKeyLabel(decl, key, adapter);
|
|
112
|
+
const words = isObject2(record) ? fields.map((f) => valueText(f, record[f.name], adapter)).filter((w) => w !== "") : [];
|
|
113
|
+
return words.length > 0 ? `${name}: ${words.join(", ")}` : name;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function recordMapGrid(initial) {
|
|
117
|
+
let opts = initial;
|
|
118
|
+
const root = el("div", "shell-recordgrid");
|
|
119
|
+
const table = el("table", "shell-recordgrid-table");
|
|
120
|
+
const head = el("thead");
|
|
121
|
+
const body = el("tbody");
|
|
122
|
+
table.append(head, body);
|
|
123
|
+
const adder = el("div", "shell-recordgrid-add");
|
|
124
|
+
const empty = el("p", "shell-recordgrid-empty");
|
|
125
|
+
root.append(table, empty, adder);
|
|
126
|
+
const cells = /* @__PURE__ */ new Map();
|
|
127
|
+
let addKey = "";
|
|
128
|
+
const editable = () => opts.onCommit !== void 0 && opts.disabled !== true;
|
|
129
|
+
const held = () => isObject2(opts.value) ? opts.value : {};
|
|
130
|
+
const commit = (next) => {
|
|
131
|
+
const v = Object.keys(next).length > 0 ? next : void 0;
|
|
132
|
+
opts = { ...opts, value: v };
|
|
133
|
+
draw();
|
|
134
|
+
opts.onCommit?.(v);
|
|
135
|
+
};
|
|
136
|
+
const cell = (key, field, record) => {
|
|
137
|
+
const id = `${key}\0${field.name}`;
|
|
138
|
+
const o = {
|
|
139
|
+
decl: field,
|
|
140
|
+
value: record[field.name],
|
|
141
|
+
...opts.adapter ? { adapter: opts.adapter } : {},
|
|
142
|
+
...opts.disabled !== void 0 ? { disabled: opts.disabled } : {},
|
|
143
|
+
...opts.controlClassName !== void 0 ? { controlClassName: opts.controlClassName } : {},
|
|
144
|
+
...editable() ? {
|
|
145
|
+
onCommit: (v) => {
|
|
146
|
+
const map = held();
|
|
147
|
+
const rec = { ...isObject2(map[key]) ? map[key] : {} };
|
|
148
|
+
if (v === void 0) delete rec[field.name];
|
|
149
|
+
else rec[field.name] = v;
|
|
150
|
+
commit({ ...map, [key]: rec });
|
|
151
|
+
}
|
|
152
|
+
} : {},
|
|
153
|
+
className: "shell-recordgrid-cell"
|
|
154
|
+
};
|
|
155
|
+
const had = cells.get(id);
|
|
156
|
+
if (had && had.el.dataset["type"] === field.type) {
|
|
157
|
+
fieldRowFocus.settle(had.el);
|
|
158
|
+
had.update(o);
|
|
159
|
+
fieldRowFocus.restore(had.el);
|
|
160
|
+
return had.el;
|
|
161
|
+
}
|
|
162
|
+
const handle = valueRow(o);
|
|
163
|
+
handle.el.dataset["type"] = field.type;
|
|
164
|
+
cells.set(id, handle);
|
|
165
|
+
return handle.el;
|
|
166
|
+
};
|
|
167
|
+
const draw = () => {
|
|
168
|
+
const decl = opts.decl;
|
|
169
|
+
const fields = decl.of?.fields ?? [];
|
|
170
|
+
const map = held();
|
|
171
|
+
const keys = Object.keys(map);
|
|
172
|
+
const adapter = opts.adapter;
|
|
173
|
+
const keyKind = decl.key?.type === "reference" ? decl.key.to ?? "" : void 0;
|
|
174
|
+
const keyNodes = keyKind !== void 0 && adapter ? adapter.nodes(keyKind) : void 0;
|
|
175
|
+
const hr = el("tr");
|
|
176
|
+
hr.append(el("th", "shell-recordgrid-keyhead", recordMapKeyHeading(decl)));
|
|
177
|
+
for (const f of fields) {
|
|
178
|
+
const th = el("th", void 0, f.label ?? f.name);
|
|
179
|
+
const help = f.help ?? f.purpose;
|
|
180
|
+
if (help) th.dataset["tip"] = help;
|
|
181
|
+
hr.append(th);
|
|
182
|
+
}
|
|
183
|
+
hr.append(el("th", "shell-recordgrid-tools"));
|
|
184
|
+
head.replaceChildren(hr);
|
|
185
|
+
const used = /* @__PURE__ */ new Set();
|
|
186
|
+
const rows = [];
|
|
187
|
+
for (const key of keys) {
|
|
188
|
+
const record = isObject2(map[key]) ? map[key] : {};
|
|
189
|
+
const tr = el("tr");
|
|
190
|
+
tr.dataset["key"] = key;
|
|
191
|
+
const name = recordMapKeyLabel(decl, key, adapter);
|
|
192
|
+
const keyCell = el("th", "shell-recordgrid-key", name);
|
|
193
|
+
keyCell.setAttribute("scope", "row");
|
|
194
|
+
if (keyNodes !== void 0 && adapter && keyNodes.every((n) => n.id !== key) && adapter.label?.(keyKind, key) === void 0) {
|
|
195
|
+
keyCell.append(el("span", "shell-recordgrid-gone", " (not here any more)"));
|
|
196
|
+
}
|
|
197
|
+
tr.append(keyCell);
|
|
198
|
+
for (const f of fields) {
|
|
199
|
+
used.add(`${key}\0${f.name}`);
|
|
200
|
+
const td = el("td");
|
|
201
|
+
td.append(cell(key, f, record));
|
|
202
|
+
tr.append(td);
|
|
203
|
+
}
|
|
204
|
+
const tools = el("td", "shell-recordgrid-tools");
|
|
205
|
+
if (editable()) tools.append(iconBtn("close", `Remove ${name}`, () => {
|
|
206
|
+
const next = { ...held() };
|
|
207
|
+
delete next[key];
|
|
208
|
+
commit(next);
|
|
209
|
+
}));
|
|
210
|
+
tr.append(tools);
|
|
211
|
+
rows.push(tr);
|
|
212
|
+
}
|
|
213
|
+
for (const id of [...cells.keys()]) if (!used.has(id)) cells.delete(id);
|
|
214
|
+
body.replaceChildren(...rows);
|
|
215
|
+
table.hidden = keys.length === 0;
|
|
216
|
+
empty.textContent = keys.length === 0 ? "None yet." : "";
|
|
217
|
+
empty.hidden = keys.length !== 0;
|
|
218
|
+
adder.replaceChildren();
|
|
219
|
+
if (!editable()) return;
|
|
220
|
+
const add = (k) => {
|
|
221
|
+
if (k === "" || Object.prototype.hasOwnProperty.call(held(), k)) return;
|
|
222
|
+
commit({ ...held(), [k]: defaultOf(decl.of) });
|
|
223
|
+
};
|
|
224
|
+
if (keyNodes !== void 0) {
|
|
225
|
+
const pick = el("select", `field shell-valuerow-key${opts.controlClassName ? ` ${opts.controlClassName}` : ""}`);
|
|
226
|
+
const first = el("option", void 0, "Add...");
|
|
227
|
+
first.value = "";
|
|
228
|
+
pick.append(first);
|
|
229
|
+
for (const n of keyNodes.filter((n2) => !Object.prototype.hasOwnProperty.call(map, n2.id))) {
|
|
230
|
+
const opt = el("option", void 0, n.label);
|
|
231
|
+
opt.value = n.id;
|
|
232
|
+
pick.append(opt);
|
|
233
|
+
}
|
|
234
|
+
pick.disabled = pick.options.length === 1;
|
|
235
|
+
pick.addEventListener("change", () => add(pick.value));
|
|
236
|
+
adder.append(pick);
|
|
237
|
+
} else {
|
|
238
|
+
const input = el("input", "field shell-valuerow-key");
|
|
239
|
+
input.type = "text";
|
|
240
|
+
input.placeholder = "New key";
|
|
241
|
+
input.value = addKey;
|
|
242
|
+
input.addEventListener("input", () => {
|
|
243
|
+
addKey = input.value;
|
|
244
|
+
});
|
|
245
|
+
const go = () => {
|
|
246
|
+
const k = input.value.trim();
|
|
247
|
+
addKey = "";
|
|
248
|
+
add(k);
|
|
249
|
+
};
|
|
250
|
+
input.addEventListener("keydown", (e) => {
|
|
251
|
+
if (e.key === "Enter") go();
|
|
252
|
+
});
|
|
253
|
+
adder.append(input, iconBtn("add", "Add", go));
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
draw();
|
|
257
|
+
return { el: root, update: (o) => {
|
|
258
|
+
opts = o;
|
|
259
|
+
draw();
|
|
260
|
+
} };
|
|
261
|
+
}
|
|
262
|
+
function recordMapKeyHeading(decl) {
|
|
263
|
+
const to = decl.key?.type === "reference" ? decl.key.to : void 0;
|
|
264
|
+
return to ? to.charAt(0).toUpperCase() + to.slice(1) : "Key";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/value-rows.ts
|
|
5
268
|
function nodeLabel(adapter, kind, id) {
|
|
6
269
|
return adapter.nodes(kind).find((n) => n.id === id)?.label ?? adapter.label?.(kind, id);
|
|
7
270
|
}
|
|
8
271
|
var SCALARS = /* @__PURE__ */ new Set(["text", "multiline", "number", "boolean", "enum", "flags", "quality", "string"]);
|
|
9
|
-
var
|
|
272
|
+
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10
273
|
var nameOf = (decl) => decl.label ?? decl.name;
|
|
11
274
|
var asDecl = (d, name) => ({ ...d, name: d.name ?? name });
|
|
12
275
|
function ownProblems(decl, value, adapter) {
|
|
@@ -245,31 +508,31 @@ function structuredRow(initial) {
|
|
|
245
508
|
}, controls));
|
|
246
509
|
};
|
|
247
510
|
if (decl.type === "record") {
|
|
248
|
-
const value =
|
|
511
|
+
const value = isObject3(o.value) ? o.value : void 0;
|
|
249
512
|
const required = value === void 0 ? [] : valueProblems(decl, value).filter((p) => p.code === "required");
|
|
250
513
|
for (const f of decl.fields ?? []) {
|
|
251
514
|
const missing = required.find((p) => p.path === f.name);
|
|
252
515
|
child(`f:${f.name}`, f, value?.[f.name], (v) => {
|
|
253
|
-
const next = { ...
|
|
516
|
+
const next = { ...isObject3(opts.value) ? opts.value : {} };
|
|
254
517
|
if (v === void 0) delete next[f.name];
|
|
255
518
|
else next[f.name] = v;
|
|
256
519
|
commit(Object.keys(next).length > 0 ? next : void 0);
|
|
257
520
|
}, void 0, void 0, missing ? "Required." : void 0);
|
|
258
521
|
}
|
|
259
522
|
} else if (decl.type === "map" && decl.of) {
|
|
260
|
-
const value =
|
|
523
|
+
const value = isObject3(o.value) ? o.value : {};
|
|
261
524
|
const keyNodes = decl.key?.type === "reference" && o.adapter ? o.adapter.nodes(decl.key.to ?? "") : void 0;
|
|
262
525
|
const keyKind = decl.key?.to ?? "";
|
|
263
526
|
const keyLabel = (k) => (o.adapter && keyNodes !== void 0 ? nodeLabel(o.adapter, keyKind, k) : void 0) ?? k;
|
|
264
527
|
for (const k of Object.keys(value)) {
|
|
265
528
|
const remove = editable() ? [iconBtn("close", `Remove ${keyLabel(k)}`, () => {
|
|
266
|
-
const next = { ...
|
|
529
|
+
const next = { ...isObject3(opts.value) ? opts.value : {} };
|
|
267
530
|
delete next[k];
|
|
268
531
|
commit(Object.keys(next).length > 0 ? next : void 0);
|
|
269
532
|
})] : [];
|
|
270
533
|
const gone = keyNodes !== void 0 && o.adapter && nodeLabel(o.adapter, keyKind, k) === void 0 ? `${k} is not here any more.` : void 0;
|
|
271
534
|
child(`k:${k}`, asDecl(decl.of, k), value[k], (v) => {
|
|
272
|
-
const next = { ...
|
|
535
|
+
const next = { ...isObject3(opts.value) ? opts.value : {} };
|
|
273
536
|
next[k] = v === void 0 ? defaultOf(decl.of) : v;
|
|
274
537
|
commit(next);
|
|
275
538
|
}, remove, keyLabel(k), gone);
|
|
@@ -313,7 +576,7 @@ function structuredRow(initial) {
|
|
|
313
576
|
}
|
|
314
577
|
} else if (decl.type === "list" && decl.of) {
|
|
315
578
|
const value = Array.isArray(o.value) ? o.value : [];
|
|
316
|
-
const idOf = (item, i) => decl.id !== void 0 &&
|
|
579
|
+
const idOf = (item, i) => decl.id !== void 0 && isObject3(item) && typeof item[decl.id] === "string" ? `id:${item[decl.id]}` : `i:${i}`;
|
|
317
580
|
value.forEach((item, i) => {
|
|
318
581
|
const set = (next) => commit(next.length > 0 ? next : void 0);
|
|
319
582
|
const controls = editable() ? [
|
|
@@ -327,7 +590,7 @@ function structuredRow(initial) {
|
|
|
327
590
|
}, i === value.length - 1),
|
|
328
591
|
iconBtn("close", "Remove", () => set(value.filter((_, j) => j !== i)), false, true)
|
|
329
592
|
] : [];
|
|
330
|
-
const itemLabel = decl.id !== void 0 &&
|
|
593
|
+
const itemLabel = decl.id !== void 0 && isObject3(item) && typeof item[decl.id] === "string" ? item[decl.id] : `${i + 1}`;
|
|
331
594
|
child(idOf(item, i), asDecl(decl.of, itemLabel), item, (v) => {
|
|
332
595
|
const next = [...Array.isArray(opts.value) ? opts.value : []];
|
|
333
596
|
next[i] = v === void 0 ? defaultOf(decl.of) : v;
|
|
@@ -338,8 +601,8 @@ function structuredRow(initial) {
|
|
|
338
601
|
if (editable()) {
|
|
339
602
|
adder.append(iconBtn("add", "Add", () => {
|
|
340
603
|
const fresh = defaultOf(decl.of);
|
|
341
|
-
if (decl.id !== void 0 &&
|
|
342
|
-
const taken = new Set(value.map((x) =>
|
|
604
|
+
if (decl.id !== void 0 && isObject3(fresh)) {
|
|
605
|
+
const taken = new Set(value.map((x) => isObject3(x) ? x[decl.id] : void 0));
|
|
343
606
|
let n = value.length + 1;
|
|
344
607
|
while (taken.has(`item-${n}`)) n++;
|
|
345
608
|
fresh[decl.id] = `item-${n}`;
|
|
@@ -388,6 +651,6 @@ function valueRows() {
|
|
|
388
651
|
};
|
|
389
652
|
}
|
|
390
653
|
|
|
391
|
-
export { valueRow, valueRows };
|
|
654
|
+
export { extensionFold, foldCaption, isRecordMap, isSet, recordMapGrid, recordMapKeyLabel, recordMapSummary, valueRow, valueRows, valueText };
|
|
392
655
|
//# sourceMappingURL=value-rows.js.map
|
|
393
656
|
//# sourceMappingURL=value-rows.js.map
|