@lilydesignsystem/angular-data-grid 0.0.0-stage → 0.9.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 +61 -2
- package/fesm2022/lilydesignsystem-angular-data-grid.mjs +3016 -0
- package/fesm2022/lilydesignsystem-angular-data-grid.mjs.map +1 -0
- package/package.json +47 -3
- package/types/lilydesignsystem-angular-data-grid.d.ts +811 -0
- package/types/lilydesignsystem-angular-data-grid.d.ts.map +1 -0
|
@@ -0,0 +1,3016 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { ChangeDetectionStrategy, Component, Directive, ElementRef, Injector, afterNextRender, computed, effect, inject, input, model, output, signal, untracked, viewChild } from "@angular/core";
|
|
3
|
+
import { NgTemplateOutlet } from "@angular/common";
|
|
4
|
+
import { DataTable, DataTableBody, DataTableFoot, DataTableHead, DataTableRow, DataTableTD, DataTableTH } from "@lilydesignsystem/angular-headless";
|
|
5
|
+
function isExportFormula(value) {
|
|
6
|
+
return !!value && typeof value === "object" && !(value instanceof Date) && typeof value.formula === "string" && "result" in value;
|
|
7
|
+
}
|
|
8
|
+
const MIME_TYPES = {
|
|
9
|
+
csv: "text/csv;charset=utf-8",
|
|
10
|
+
tsv: "text/tab-separated-values;charset=utf-8",
|
|
11
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
12
|
+
};
|
|
13
|
+
const FORMULA_START = /^[=+\-@\t\r]/;
|
|
14
|
+
function cellText(value, safe = true) {
|
|
15
|
+
if (value === null || value === void 0) return "";
|
|
16
|
+
if (isExportFormula(value)) return cellText(value.result, safe);
|
|
17
|
+
if (value instanceof Date) {
|
|
18
|
+
if (Number.isNaN(value.getTime())) return "";
|
|
19
|
+
const iso = value.toISOString();
|
|
20
|
+
return iso.endsWith("T00:00:00.000Z") ? iso.slice(0, 10) : iso;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "";
|
|
23
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
24
|
+
const text = String(value);
|
|
25
|
+
return safe && FORMULA_START.test(text) ? `'${text}` : text;
|
|
26
|
+
}
|
|
27
|
+
function quoteField(text, delimiter) {
|
|
28
|
+
return text.includes(delimiter) || /["\r\n]/.test(text) ? `"${text.replace(/"/g, "\"\"")}"` : text;
|
|
29
|
+
}
|
|
30
|
+
function toDelimited(table, options = {}) {
|
|
31
|
+
const { delimiter = ",", header = true, bom = false, safe = true, eol = "\r\n" } = options;
|
|
32
|
+
const lines = [];
|
|
33
|
+
if (header) lines.push(table.headers.map((h) => quoteField(cellText(h, safe), delimiter)).join(delimiter));
|
|
34
|
+
for (const row of table.rows) lines.push(table.headers.map((_, i) => quoteField(cellText(row[i], safe), delimiter)).join(delimiter));
|
|
35
|
+
return (bom ? "" : "") + lines.join(eol) + (lines.length > 0 ? eol : "");
|
|
36
|
+
}
|
|
37
|
+
function toCsv(table, options = {}) {
|
|
38
|
+
return toDelimited(table, {
|
|
39
|
+
...options,
|
|
40
|
+
delimiter: ","
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function toTsv(table, options = {}) {
|
|
44
|
+
return toDelimited(table, {
|
|
45
|
+
...options,
|
|
46
|
+
delimiter: " "
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const encoder = new TextEncoder();
|
|
50
|
+
function xmlEscape(text) {
|
|
51
|
+
return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
52
|
+
}
|
|
53
|
+
function columnName(index) {
|
|
54
|
+
let n = index;
|
|
55
|
+
let name = "";
|
|
56
|
+
do {
|
|
57
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
58
|
+
n = Math.floor(n / 26) - 1;
|
|
59
|
+
} while (n >= 0);
|
|
60
|
+
return name;
|
|
61
|
+
}
|
|
62
|
+
function excelSerial(date) {
|
|
63
|
+
return date.getTime() / 864e5 + 25569;
|
|
64
|
+
}
|
|
65
|
+
function sheetNameFor(name) {
|
|
66
|
+
return name.replace(/[[\]:*?/\\]/g, " ").replace(/\s+/g, " ").trim().slice(0, 31) || "Sheet1";
|
|
67
|
+
}
|
|
68
|
+
const STYLES_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"><numFmts count=\"2\"><numFmt numFmtId=\"164\" formatCode=\"yyyy\\-mm\\-dd\"/><numFmt numFmtId=\"165\" formatCode=\"yyyy\\-mm\\-dd\\ hh:mm:ss\"/></numFmts><fonts count=\"2\"><font><sz val=\"11\"/><name val=\"Calibri\"/></font><font><b/><sz val=\"11\"/><name val=\"Calibri\"/></font></fonts><fills count=\"2\"><fill><patternFill patternType=\"none\"/></fill><fill><patternFill patternType=\"gray125\"/></fill></fills><borders count=\"1\"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/></cellStyleXfs><cellXfs count=\"4\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/><xf numFmtId=\"164\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyNumberFormat=\"1\"/><xf numFmtId=\"165\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyNumberFormat=\"1\"/><xf numFmtId=\"0\" fontId=\"1\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyFont=\"1\"/></cellXfs><cellStyles count=\"1\"><cellStyle name=\"Normal\" xfId=\"0\" builtinId=\"0\"/></cellStyles></styleSheet>";
|
|
69
|
+
function formulaXml(ref, value) {
|
|
70
|
+
const expression = xmlEscape(value.formula.replace(/^=/, ""));
|
|
71
|
+
const result = value.result;
|
|
72
|
+
if (typeof result === "number" && Number.isFinite(result)) return `<c r="${ref}"><f>${expression}</f><v>${result}</v></c>`;
|
|
73
|
+
if (typeof result === "boolean") return `<c r="${ref}" t="b"><f>${expression}</f><v>${result ? 1 : 0}</v></c>`;
|
|
74
|
+
if (result instanceof Date && !Number.isNaN(result.getTime())) return `<c r="${ref}" s="${result.getTime() % 864e5 !== 0 ? 2 : 1}"><f>${expression}</f><v>${excelSerial(result)}</v></c>`;
|
|
75
|
+
if (result === null || result === void 0) return `<c r="${ref}"><f>${expression}</f></c>`;
|
|
76
|
+
return `<c r="${ref}" t="str"><f>${expression}</f><v>${xmlEscape(String(result))}</v></c>`;
|
|
77
|
+
}
|
|
78
|
+
function cellXml(ref, value, headerStyle = false) {
|
|
79
|
+
if (value === null || value === void 0) return "";
|
|
80
|
+
if (isExportFormula(value)) return formulaXml(ref, value);
|
|
81
|
+
const s = headerStyle ? " s=\"3\"" : "";
|
|
82
|
+
if (value instanceof Date) {
|
|
83
|
+
if (Number.isNaN(value.getTime())) return "";
|
|
84
|
+
return `<c r="${ref}" s="${value.getTime() % 864e5 !== 0 ? 2 : 1}"><v>${excelSerial(value)}</v></c>`;
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === "number") return Number.isFinite(value) ? `<c r="${ref}"${s}><v>${value}</v></c>` : "";
|
|
87
|
+
if (typeof value === "boolean") return `<c r="${ref}"${s} t="b"><v>${value ? 1 : 0}</v></c>`;
|
|
88
|
+
return `<c r="${ref}"${s} t="inlineStr"><is><t xml:space="preserve">${xmlEscape(String(value))}</t></is></c>`;
|
|
89
|
+
}
|
|
90
|
+
function sheetXml(table, header) {
|
|
91
|
+
const rows = [];
|
|
92
|
+
let rowNumber = 0;
|
|
93
|
+
if (header) {
|
|
94
|
+
rowNumber += 1;
|
|
95
|
+
rows.push(`<row r="${rowNumber}">${table.headers.map((h, i) => cellXml(`${columnName(i)}${rowNumber}`, h, true)).join("")}</row>`);
|
|
96
|
+
}
|
|
97
|
+
for (const row of table.rows) {
|
|
98
|
+
rowNumber += 1;
|
|
99
|
+
const cells = table.headers.map((_, i) => cellXml(`${columnName(i)}${rowNumber}`, row[i])).join("");
|
|
100
|
+
rows.push(`<row r="${rowNumber}">${cells}</row>`);
|
|
101
|
+
}
|
|
102
|
+
return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">" + (header && table.headers.length > 0 ? "<sheetViews><sheetView workbookViewId=\"0\"><pane ySplit=\"1\" topLeftCell=\"A2\" activePane=\"bottomLeft\" state=\"frozen\"/></sheetView></sheetViews>" : "") + `<sheetData>${rows.join("")}</sheetData></worksheet>`;
|
|
103
|
+
}
|
|
104
|
+
function toXlsx(table, options = {}) {
|
|
105
|
+
const { sheetName = "Sheet1", header = true } = options;
|
|
106
|
+
const files = {
|
|
107
|
+
"[Content_Types].xml": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\"><Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/><Default Extension=\"xml\" ContentType=\"application/xml\"/><Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/><Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/><Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/></Types>",
|
|
108
|
+
"_rels/.rels": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/></Relationships>",
|
|
109
|
+
"xl/workbook.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="${xmlEscape(sheetNameFor(sheetName))}" sheetId="1" r:id="rId1"/></sheets></workbook>`,
|
|
110
|
+
"xl/_rels/workbook.xml.rels": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/><Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/></Relationships>",
|
|
111
|
+
"xl/styles.xml": STYLES_XML,
|
|
112
|
+
"xl/worksheets/sheet1.xml": sheetXml(table, header)
|
|
113
|
+
};
|
|
114
|
+
return zipStore(Object.entries(files).map(([name, text]) => ({
|
|
115
|
+
name,
|
|
116
|
+
data: encoder.encode(text)
|
|
117
|
+
})));
|
|
118
|
+
}
|
|
119
|
+
let crcTable;
|
|
120
|
+
function crc32(bytes) {
|
|
121
|
+
if (!crcTable) {
|
|
122
|
+
crcTable = /* @__PURE__ */ new Uint32Array(256);
|
|
123
|
+
for (let n = 0; n < 256; n++) {
|
|
124
|
+
let c = n;
|
|
125
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
126
|
+
crcTable[n] = c >>> 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let crc = 4294967295;
|
|
130
|
+
for (let i = 0; i < bytes.length; i++) crc = crcTable[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
131
|
+
return (crc ^ 4294967295) >>> 0;
|
|
132
|
+
}
|
|
133
|
+
const DOS_TIME = 0;
|
|
134
|
+
const DOS_DATE = 33;
|
|
135
|
+
function zipStore(entries) {
|
|
136
|
+
const parts = [];
|
|
137
|
+
const central = [];
|
|
138
|
+
let offset = 0;
|
|
139
|
+
for (const { name, data } of entries) {
|
|
140
|
+
const nameBytes = encoder.encode(name);
|
|
141
|
+
const crc = crc32(data);
|
|
142
|
+
const local = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(30));
|
|
143
|
+
local.setUint32(0, 67324752, true);
|
|
144
|
+
local.setUint16(4, 20, true);
|
|
145
|
+
local.setUint16(6, 2048, true);
|
|
146
|
+
local.setUint16(8, 0, true);
|
|
147
|
+
local.setUint16(10, DOS_TIME, true);
|
|
148
|
+
local.setUint16(12, DOS_DATE, true);
|
|
149
|
+
local.setUint32(14, crc, true);
|
|
150
|
+
local.setUint32(18, data.length, true);
|
|
151
|
+
local.setUint32(22, data.length, true);
|
|
152
|
+
local.setUint16(26, nameBytes.length, true);
|
|
153
|
+
local.setUint16(28, 0, true);
|
|
154
|
+
parts.push(new Uint8Array(local.buffer), nameBytes, data);
|
|
155
|
+
const entry = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(46));
|
|
156
|
+
entry.setUint32(0, 33639248, true);
|
|
157
|
+
entry.setUint16(4, 20, true);
|
|
158
|
+
entry.setUint16(6, 20, true);
|
|
159
|
+
entry.setUint16(8, 2048, true);
|
|
160
|
+
entry.setUint16(10, 0, true);
|
|
161
|
+
entry.setUint16(12, DOS_TIME, true);
|
|
162
|
+
entry.setUint16(14, DOS_DATE, true);
|
|
163
|
+
entry.setUint32(16, crc, true);
|
|
164
|
+
entry.setUint32(20, data.length, true);
|
|
165
|
+
entry.setUint32(24, data.length, true);
|
|
166
|
+
entry.setUint16(28, nameBytes.length, true);
|
|
167
|
+
entry.setUint32(42, offset, true);
|
|
168
|
+
central.push(new Uint8Array(entry.buffer), nameBytes);
|
|
169
|
+
offset += 30 + nameBytes.length + data.length;
|
|
170
|
+
}
|
|
171
|
+
const centralSize = central.reduce((n, p) => n + p.length, 0);
|
|
172
|
+
const end = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(22));
|
|
173
|
+
end.setUint32(0, 101010256, true);
|
|
174
|
+
end.setUint16(8, entries.length, true);
|
|
175
|
+
end.setUint16(10, entries.length, true);
|
|
176
|
+
end.setUint32(12, centralSize, true);
|
|
177
|
+
end.setUint32(16, offset, true);
|
|
178
|
+
const all = [
|
|
179
|
+
...parts,
|
|
180
|
+
...central,
|
|
181
|
+
new Uint8Array(end.buffer)
|
|
182
|
+
];
|
|
183
|
+
const out = new Uint8Array(all.reduce((n, p) => n + p.length, 0));
|
|
184
|
+
let at = 0;
|
|
185
|
+
for (const p of all) {
|
|
186
|
+
out.set(p, at);
|
|
187
|
+
at += p.length;
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function exportBlob(table, format, options = {}) {
|
|
192
|
+
if (format === "xlsx") return new Blob([toXlsx(table, options)], { type: MIME_TYPES.xlsx });
|
|
193
|
+
const text = toDelimited(table, {
|
|
194
|
+
delimiter: format === "tsv" ? " " : ",",
|
|
195
|
+
header: options.header,
|
|
196
|
+
bom: true
|
|
197
|
+
});
|
|
198
|
+
return new Blob([text], { type: MIME_TYPES[format] });
|
|
199
|
+
}
|
|
200
|
+
function exportFilename(base, format) {
|
|
201
|
+
const cleaned = base.replace(/[\\/:*?"<>|\u0000-\u001F]/g, "-").trim() || "export";
|
|
202
|
+
return cleaned.toLowerCase().endsWith(`.${format}`) ? cleaned : `${cleaned}.${format}`;
|
|
203
|
+
}
|
|
204
|
+
function downloadBlob(blob, filename) {
|
|
205
|
+
if (typeof document === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) return;
|
|
206
|
+
const url = URL.createObjectURL(blob);
|
|
207
|
+
const link = document.createElement("a");
|
|
208
|
+
link.href = url;
|
|
209
|
+
link.download = filename;
|
|
210
|
+
link.style.display = "none";
|
|
211
|
+
document.body.appendChild(link);
|
|
212
|
+
link.click();
|
|
213
|
+
link.remove();
|
|
214
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
215
|
+
}
|
|
216
|
+
const SAFE_SCHEME = /^(https?:|mailto:|tel:)/i;
|
|
217
|
+
const RELATIVE = /^(\/(?!\/)|\.\.?\/|#|\?)/;
|
|
218
|
+
function safeHref(href) {
|
|
219
|
+
if (typeof href !== "string") return null;
|
|
220
|
+
const text = href.trim();
|
|
221
|
+
if (!text) return null;
|
|
222
|
+
if (/[\u0000-\u001F\u007F]/.test(text)) return null;
|
|
223
|
+
return SAFE_SCHEME.test(text) || RELATIVE.test(text) ? text : null;
|
|
224
|
+
}
|
|
225
|
+
function linkParts(value) {
|
|
226
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
227
|
+
if (typeof value === "string") return { href: value };
|
|
228
|
+
if (typeof value === "object" && typeof value.href === "string") return value;
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
function linkLabel(value) {
|
|
232
|
+
const link = linkParts(value);
|
|
233
|
+
return link ? link.label ?? link.href : "";
|
|
234
|
+
}
|
|
235
|
+
function isCivilDate(text) {
|
|
236
|
+
if (typeof text !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(text)) return false;
|
|
237
|
+
const [y, m, d] = text.split("-").map(Number);
|
|
238
|
+
const date = new Date(Date.UTC(y, m - 1, d));
|
|
239
|
+
return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;
|
|
240
|
+
}
|
|
241
|
+
function isCivilTime(text) {
|
|
242
|
+
if (typeof text !== "string") return false;
|
|
243
|
+
const match = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(text);
|
|
244
|
+
return !!match && Number(match[1]) < 24 && Number(match[2]) < 60 && (match[3] === void 0 || Number(match[3]) < 60);
|
|
245
|
+
}
|
|
246
|
+
function civilDateToDate(text) {
|
|
247
|
+
if (text instanceof Date) return Number.isNaN(text.getTime()) ? null : text;
|
|
248
|
+
return isCivilDate(text) ? /* @__PURE__ */ new Date(`${text}T00:00:00Z`) : null;
|
|
249
|
+
}
|
|
250
|
+
const isEmpty = (value) => value === null || value === void 0 || value === "";
|
|
251
|
+
const OFFSET = /^([+-])(\d{2}):?(\d{2})$/;
|
|
252
|
+
function zoneOffsetMinutes(timeZone, instantMs) {
|
|
253
|
+
const bare = OFFSET.exec(timeZone);
|
|
254
|
+
if (bare) return (bare[1] === "-" ? -1 : 1) * (Number(bare[2]) * 60 + Number(bare[3]));
|
|
255
|
+
try {
|
|
256
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
257
|
+
timeZone,
|
|
258
|
+
hourCycle: "h23",
|
|
259
|
+
year: "numeric",
|
|
260
|
+
month: "2-digit",
|
|
261
|
+
day: "2-digit",
|
|
262
|
+
hour: "2-digit",
|
|
263
|
+
minute: "2-digit",
|
|
264
|
+
second: "2-digit"
|
|
265
|
+
}).formatToParts(new Date(instantMs));
|
|
266
|
+
const n = (type) => Number(parts.find((x) => x.type === type)?.value);
|
|
267
|
+
return (Date.UTC(n("year"), n("month") - 1, n("day"), n("hour") % 24, n("minute"), n("second")) - Math.floor(instantMs / 1e3) * 1e3) / 6e4;
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function isValidTimeZone(timeZone) {
|
|
273
|
+
return typeof timeZone === "string" && timeZone !== "" && zoneOffsetMinutes(timeZone, 0) !== null;
|
|
274
|
+
}
|
|
275
|
+
function isZonedTime(value) {
|
|
276
|
+
return !!value && typeof value === "object" && isCivilTime(value.time) && isValidTimeZone(value.timeZone);
|
|
277
|
+
}
|
|
278
|
+
function zonedMinutes(value, referenceDate) {
|
|
279
|
+
if (!isZonedTime(value)) return null;
|
|
280
|
+
const [h, m, sec] = value.time.split(":").map(Number);
|
|
281
|
+
const local = h * 60 + m + (sec || 0) / 60;
|
|
282
|
+
if (!referenceDate || !isCivilDate(referenceDate)) return local;
|
|
283
|
+
const [y, mo, d] = referenceDate.split("-").map(Number);
|
|
284
|
+
const guess = Date.UTC(y, mo - 1, d, h, m, sec || 0);
|
|
285
|
+
const offset = zoneOffsetMinutes(value.timeZone, guess);
|
|
286
|
+
if (offset === null) return local;
|
|
287
|
+
return guess / 6e4 - offset;
|
|
288
|
+
}
|
|
289
|
+
function zonedUtcTime(value, referenceDate) {
|
|
290
|
+
if (!referenceDate || !isCivilDate(referenceDate)) return null;
|
|
291
|
+
const minutes = zonedMinutes(value, referenceDate);
|
|
292
|
+
if (minutes === null) return null;
|
|
293
|
+
const day = (Math.round(minutes) % 1440 + 1440) % 1440;
|
|
294
|
+
return `${String(Math.floor(day / 60)).padStart(2, "0")}:${String(day % 60).padStart(2, "0")}`;
|
|
295
|
+
}
|
|
296
|
+
function zoneLabel(timeZone, locale, referenceDate) {
|
|
297
|
+
if (!locale || !referenceDate || !isCivilDate(referenceDate) || OFFSET.test(timeZone)) return timeZone;
|
|
298
|
+
try {
|
|
299
|
+
return new Intl.DateTimeFormat(locale, {
|
|
300
|
+
timeZone,
|
|
301
|
+
timeZoneName: "short"
|
|
302
|
+
}).formatToParts(/* @__PURE__ */ new Date(`${referenceDate}T12:00:00Z`)).find((x) => x.type === "timeZoneName")?.value ?? timeZone;
|
|
303
|
+
} catch {
|
|
304
|
+
return timeZone;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function peopleOf(value) {
|
|
308
|
+
return (Array.isArray(value) ? value : isEmpty(value) ? [] : [value]).filter((p) => !!p && typeof p === "object" && typeof p.id === "string");
|
|
309
|
+
}
|
|
310
|
+
const personName = (person) => person.name ? person.name : person.id;
|
|
311
|
+
function filesOf(value) {
|
|
312
|
+
return (Array.isArray(value) ? value : []).filter((f) => !!f && typeof f === "object" && typeof f.id === "string" && typeof f.name === "string");
|
|
313
|
+
}
|
|
314
|
+
function aggregateValues(kind, values) {
|
|
315
|
+
const present = values.filter((v) => !isEmpty(v));
|
|
316
|
+
const numbers = present.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
317
|
+
switch (kind) {
|
|
318
|
+
case "count": return values.length;
|
|
319
|
+
case "countUnique": return present.length === 0 ? null : new Set(present.map((v) => typeof v === "object" ? JSON.stringify(v) : String(v))).size;
|
|
320
|
+
case "sum": return numbers.length === 0 ? null : numbers.reduce((a, b) => a + b, 0);
|
|
321
|
+
case "avg": return numbers.length === 0 ? null : numbers.reduce((a, b) => a + b, 0) / numbers.length;
|
|
322
|
+
case "median": {
|
|
323
|
+
if (numbers.length === 0) return null;
|
|
324
|
+
const sorted = numbers.slice().sort((a, b) => a - b);
|
|
325
|
+
const mid = sorted.length >> 1;
|
|
326
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
327
|
+
}
|
|
328
|
+
case "min":
|
|
329
|
+
case "max": {
|
|
330
|
+
const pool = numbers.length === present.length ? numbers : present.filter((v) => typeof v === "string");
|
|
331
|
+
if (pool.length === 0) return null;
|
|
332
|
+
return pool.reduce((a, b) => (kind === "min" ? b < a : b > a) ? b : a);
|
|
333
|
+
}
|
|
334
|
+
case "range": return numbers.length === 0 ? null : Math.max(...numbers) - Math.min(...numbers);
|
|
335
|
+
case "percentTrue": {
|
|
336
|
+
const booleans = values.filter((v) => typeof v === "boolean");
|
|
337
|
+
return booleans.length === 0 ? null : booleans.filter(Boolean).length / booleans.length * 100;
|
|
338
|
+
}
|
|
339
|
+
case "percentEmpty": return values.length === 0 ? null : (values.length - present.length) / values.length * 100;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function isInvalidForType(column, value) {
|
|
343
|
+
if (isEmpty(value)) return false;
|
|
344
|
+
switch (column.type) {
|
|
345
|
+
case "number": return typeof value !== "number" || !Number.isFinite(value);
|
|
346
|
+
case "link": return safeHref(linkParts(value)?.href) === null;
|
|
347
|
+
case "date": return !isCivilDate(value);
|
|
348
|
+
case "time": return !isCivilTime(value);
|
|
349
|
+
case "time-zone": return !isZonedTime(value);
|
|
350
|
+
case "select":
|
|
351
|
+
case "radio": return !(column.options ?? []).some((o) => o.id === value);
|
|
352
|
+
case "person": return peopleOf(value).length === 0;
|
|
353
|
+
default: return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const optionLabel = (column, id) => {
|
|
357
|
+
const found = (column.options ?? []).find((o) => o.id === id);
|
|
358
|
+
return found ? found.label : isEmpty(id) ? "" : String(id);
|
|
359
|
+
};
|
|
360
|
+
function resultFormatter(column, locale) {
|
|
361
|
+
const type = column.resultType ?? "text";
|
|
362
|
+
if (type === "number") {
|
|
363
|
+
const formatter = locale || column.numberFormat ? new Intl.NumberFormat(locale, column.numberFormat) : null;
|
|
364
|
+
return (v) => typeof v === "number" && Number.isFinite(v) ? formatter ? formatter.format(v) : String(v) : isEmpty(v) ? "" : String(v);
|
|
365
|
+
}
|
|
366
|
+
if (type === "date") {
|
|
367
|
+
const formatter = locale || column.dateFormat ? new Intl.DateTimeFormat(locale, {
|
|
368
|
+
dateStyle: "medium",
|
|
369
|
+
...column.dateFormat,
|
|
370
|
+
timeZone: "UTC"
|
|
371
|
+
}) : null;
|
|
372
|
+
return (v) => {
|
|
373
|
+
if (isEmpty(v)) return "";
|
|
374
|
+
const date = civilDateToDate(v);
|
|
375
|
+
return date ? formatter ? formatter.format(date) : date.toISOString().slice(0, 10) : String(v);
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return (v) => isEmpty(v) ? "" : String(v);
|
|
379
|
+
}
|
|
380
|
+
function typeDefaults(column, gridLocale) {
|
|
381
|
+
const locale = column.locale ?? gridLocale;
|
|
382
|
+
switch (column.type) {
|
|
383
|
+
case "text": return {
|
|
384
|
+
align: "start",
|
|
385
|
+
editor: "text",
|
|
386
|
+
...locale ? { compare: (a, b) => new Intl.Collator(locale).compare(String(a), String(b)) } : {}
|
|
387
|
+
};
|
|
388
|
+
case "number": {
|
|
389
|
+
const formatter = locale || column.numberFormat ? new Intl.NumberFormat(locale, column.numberFormat) : null;
|
|
390
|
+
return {
|
|
391
|
+
align: "end",
|
|
392
|
+
editor: "number",
|
|
393
|
+
...formatter ? { format: (v) => typeof v === "number" && Number.isFinite(v) ? formatter.format(v) : "" } : {}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
case "link": return {
|
|
397
|
+
align: "start",
|
|
398
|
+
editor: "link",
|
|
399
|
+
format: (v) => linkLabel(v),
|
|
400
|
+
filterText: (v) => {
|
|
401
|
+
const link = linkParts(v);
|
|
402
|
+
return link ? `${link.label ?? ""} ${link.href}`.trim() : "";
|
|
403
|
+
},
|
|
404
|
+
compare: (a, b) => {
|
|
405
|
+
const x = linkLabel(a);
|
|
406
|
+
const y = linkLabel(b);
|
|
407
|
+
return locale ? new Intl.Collator(locale).compare(x, y) : x < y ? -1 : x > y ? 1 : 0;
|
|
408
|
+
},
|
|
409
|
+
exportValue: (row) => linkLabel(column.accessor ? column.accessor(row) : row[column.id]),
|
|
410
|
+
parse: (draft, row) => {
|
|
411
|
+
const previous = linkParts(column.accessor ? column.accessor(row) : row[column.id]);
|
|
412
|
+
const href = draft.trim();
|
|
413
|
+
if (!href) return null;
|
|
414
|
+
return previous && typeof previous === "object" && (previous.label !== void 0 || previous.external !== void 0) ? {
|
|
415
|
+
...previous,
|
|
416
|
+
href
|
|
417
|
+
} : href;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
case "date": {
|
|
421
|
+
const formatter = locale || column.dateFormat ? new Intl.DateTimeFormat(locale, {
|
|
422
|
+
dateStyle: "medium",
|
|
423
|
+
...column.dateFormat,
|
|
424
|
+
timeZone: "UTC"
|
|
425
|
+
}) : null;
|
|
426
|
+
return {
|
|
427
|
+
align: "start",
|
|
428
|
+
editor: "date",
|
|
429
|
+
format: (v) => {
|
|
430
|
+
if (isEmpty(v)) return "";
|
|
431
|
+
const date = civilDateToDate(v);
|
|
432
|
+
if (!date) return String(v);
|
|
433
|
+
return formatter ? formatter.format(date) : date.toISOString().slice(0, 10);
|
|
434
|
+
},
|
|
435
|
+
exportValue: (row) => civilDateToDate(column.accessor ? column.accessor(row) : row[column.id])
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
case "time": {
|
|
439
|
+
const formatter = locale || column.timeFormat ? new Intl.DateTimeFormat(locale, {
|
|
440
|
+
timeStyle: "short",
|
|
441
|
+
...column.timeFormat,
|
|
442
|
+
timeZone: "UTC"
|
|
443
|
+
}) : null;
|
|
444
|
+
return {
|
|
445
|
+
align: "start",
|
|
446
|
+
editor: "time",
|
|
447
|
+
format: (v) => {
|
|
448
|
+
if (isEmpty(v)) return "";
|
|
449
|
+
if (!isCivilTime(v)) return String(v);
|
|
450
|
+
return formatter ? formatter.format(/* @__PURE__ */ new Date(`1970-01-01T${v.length === 5 ? `${v}:00` : v}Z`)) : v;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
case "time-zone": {
|
|
455
|
+
const formatter = locale || column.timeFormat ? new Intl.DateTimeFormat(locale, {
|
|
456
|
+
timeStyle: "short",
|
|
457
|
+
...column.timeFormat,
|
|
458
|
+
timeZone: "UTC"
|
|
459
|
+
}) : null;
|
|
460
|
+
const text = (v) => {
|
|
461
|
+
if (isEmpty(v)) return "";
|
|
462
|
+
if (!isZonedTime(v)) return String(v);
|
|
463
|
+
return `${formatter ? formatter.format(/* @__PURE__ */ new Date(`1970-01-01T${v.time.length === 5 ? `${v.time}:00` : v.time}Z`)) : v.time} ${zoneLabel(v.timeZone, locale, column.referenceDate)}`;
|
|
464
|
+
};
|
|
465
|
+
return {
|
|
466
|
+
align: "start",
|
|
467
|
+
editor: "time-zone",
|
|
468
|
+
format: text,
|
|
469
|
+
compare: (a, b) => (zonedMinutes(a, column.referenceDate) ?? 0) - (zonedMinutes(b, column.referenceDate) ?? 0),
|
|
470
|
+
exportValue: (row) => {
|
|
471
|
+
const v = column.accessor ? column.accessor(row) : row[column.id];
|
|
472
|
+
return isZonedTime(v) ? `${v.time} ${v.timeZone}` : null;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
case "person": {
|
|
477
|
+
const names = (v) => peopleOf(v).map(personName);
|
|
478
|
+
const collator = locale ? new Intl.Collator(locale) : null;
|
|
479
|
+
const order = (a, b) => collator ? collator.compare(a, b) : a < b ? -1 : a > b ? 1 : 0;
|
|
480
|
+
return {
|
|
481
|
+
align: "start",
|
|
482
|
+
editor: "person",
|
|
483
|
+
format: (v) => names(v).join("; "),
|
|
484
|
+
filterText: (v) => peopleOf(v).map((p) => `${personName(p)} ${p.email ?? ""}`.trim()).join(" "),
|
|
485
|
+
compare: (a, b) => {
|
|
486
|
+
const x = names(a);
|
|
487
|
+
const y = names(b);
|
|
488
|
+
return order(x[0] ?? "", y[0] ?? "") || x.length - y.length;
|
|
489
|
+
},
|
|
490
|
+
exportValue: (row) => {
|
|
491
|
+
const value = column.accessor ? column.accessor(row) : row[column.id];
|
|
492
|
+
const joined = (column.exportAs === "email" ? peopleOf(value).map((p) => p.email ?? "") : names(value)).join("; ");
|
|
493
|
+
return joined.replace(/[;\s]/g, "") === "" ? null : joined;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
case "file": {
|
|
498
|
+
const names = (v) => filesOf(v).map((f) => f.name);
|
|
499
|
+
const collator = locale ? new Intl.Collator(locale) : null;
|
|
500
|
+
const order = (a, b) => collator ? collator.compare(a, b) : a < b ? -1 : a > b ? 1 : 0;
|
|
501
|
+
return {
|
|
502
|
+
align: "start",
|
|
503
|
+
format: (v) => names(v).join("; "),
|
|
504
|
+
filterText: (v) => names(v).join(" "),
|
|
505
|
+
compare: (a, b) => {
|
|
506
|
+
const x = names(a);
|
|
507
|
+
const y = names(b);
|
|
508
|
+
return order(x[0] ?? "", y[0] ?? "") || x.length - y.length;
|
|
509
|
+
},
|
|
510
|
+
exportValue: (row) => {
|
|
511
|
+
const joined = names(column.accessor ? column.accessor(row) : row[column.id]).join("; ");
|
|
512
|
+
return joined === "" ? null : joined;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
case "select":
|
|
517
|
+
case "radio": {
|
|
518
|
+
const order = new Map((column.options ?? []).map((o, i) => [o.id, i]));
|
|
519
|
+
return {
|
|
520
|
+
align: "start",
|
|
521
|
+
editor: column.type === "select" ? "select" : "radio",
|
|
522
|
+
format: (v) => optionLabel(column, v),
|
|
523
|
+
filterText: (v) => optionLabel(column, v),
|
|
524
|
+
compare: column.type === "radio" || column.sortBy === "order" ? (a, b) => (order.get(a) ?? Infinity) - (order.get(b) ?? Infinity) : (a, b) => locale ? new Intl.Collator(locale).compare(optionLabel(column, a), optionLabel(column, b)) : optionLabel(column, a) < optionLabel(column, b) ? -1 : optionLabel(column, a) > optionLabel(column, b) ? 1 : 0,
|
|
525
|
+
exportValue: (row) => {
|
|
526
|
+
const id = column.accessor ? column.accessor(row) : row[column.id];
|
|
527
|
+
return column.exportAs === "id" ? isEmpty(id) ? null : id : isEmpty(id) ? null : optionLabel(column, id);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
default: return {};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function resolveColumn(column, gridLocale, rows = []) {
|
|
535
|
+
if (!column.type) return column;
|
|
536
|
+
const defaults = typeDefaults(column, gridLocale);
|
|
537
|
+
const own = Object.fromEntries(Object.entries(column).filter(([, v]) => v !== void 0));
|
|
538
|
+
const resolved = {
|
|
539
|
+
...defaults,
|
|
540
|
+
...own
|
|
541
|
+
};
|
|
542
|
+
const locale = column.locale ?? gridLocale;
|
|
543
|
+
if (column.type === "rollup") {
|
|
544
|
+
const relation = column.relation ?? (column.accessor ? (row) => column.accessor(row) : (row) => row[column.id]);
|
|
545
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
546
|
+
const format = resultFormatter(column, locale);
|
|
547
|
+
resolved.accessor = (row) => {
|
|
548
|
+
if (cache.has(row)) return cache.get(row);
|
|
549
|
+
const related = relation(row);
|
|
550
|
+
const values = Array.isArray(related) ? related : [];
|
|
551
|
+
const result = column.reduce ? column.reduce(values, row) : aggregateValues(column.aggregate ?? "count", values);
|
|
552
|
+
cache.set(row, result);
|
|
553
|
+
return result;
|
|
554
|
+
};
|
|
555
|
+
resolved.editable = false;
|
|
556
|
+
resolved.align = column.align ?? (column.resultType === "number" ? "end" : "start");
|
|
557
|
+
if (column.format === void 0) resolved.format = (v) => format(v);
|
|
558
|
+
if (column.exportValue === void 0) resolved.exportValue = (row) => {
|
|
559
|
+
const v = resolved.accessor(row);
|
|
560
|
+
return column.resultType === "date" ? civilDateToDate(v) : isEmpty(v) ? null : v;
|
|
561
|
+
};
|
|
562
|
+
} else if (column.type === "formula") {
|
|
563
|
+
const source = column.accessor ?? ((row) => row[column.id]);
|
|
564
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
565
|
+
const format = resultFormatter(column, locale);
|
|
566
|
+
const result = (row) => {
|
|
567
|
+
let found = cache.get(row);
|
|
568
|
+
if (!found) {
|
|
569
|
+
const expression = source(row);
|
|
570
|
+
if (isEmpty(expression)) found = { value: null };
|
|
571
|
+
else if (typeof column.evaluate !== "function") found = { value: expression };
|
|
572
|
+
else try {
|
|
573
|
+
found = column.evaluate(String(expression), {
|
|
574
|
+
row,
|
|
575
|
+
rows,
|
|
576
|
+
columnId: column.id
|
|
577
|
+
});
|
|
578
|
+
} catch (error) {
|
|
579
|
+
found = { error: error instanceof Error ? error.message : String(error) };
|
|
580
|
+
}
|
|
581
|
+
cache.set(row, found);
|
|
582
|
+
}
|
|
583
|
+
return found;
|
|
584
|
+
};
|
|
585
|
+
const valueOf = (row) => {
|
|
586
|
+
const r = result(row);
|
|
587
|
+
return "error" in r ? null : r.value;
|
|
588
|
+
};
|
|
589
|
+
resolved.source = source;
|
|
590
|
+
resolved.errorOf = (row) => {
|
|
591
|
+
const r = result(row);
|
|
592
|
+
return "error" in r ? r.error : void 0;
|
|
593
|
+
};
|
|
594
|
+
resolved.accessor = valueOf;
|
|
595
|
+
resolved.align = column.align ?? (column.resultType === "number" ? "end" : "start");
|
|
596
|
+
if (column.format === void 0) resolved.format = (v, row) => {
|
|
597
|
+
const message = resolved.errorOf(row);
|
|
598
|
+
return message !== void 0 ? message : format(v);
|
|
599
|
+
};
|
|
600
|
+
if (column.exportValue === void 0) resolved.exportValue = (row) => {
|
|
601
|
+
const message = resolved.errorOf(row);
|
|
602
|
+
if (message !== void 0) return message;
|
|
603
|
+
const v = valueOf(row);
|
|
604
|
+
return column.resultType === "date" ? civilDateToDate(v) : isEmpty(v) ? null : v;
|
|
605
|
+
};
|
|
606
|
+
if (column.filterText === void 0) resolved.filterText = (v, row) => resolved.format(v, row);
|
|
607
|
+
}
|
|
608
|
+
return resolved;
|
|
609
|
+
}
|
|
610
|
+
const WIDGET_SELECTOR = "a[href], button, input, select, textarea, summary, [contenteditable=\"true\"]";
|
|
611
|
+
const MIN_COLUMN_WIDTH = 40;
|
|
612
|
+
const RESIZE_STEP = 16;
|
|
613
|
+
let uid = 0;
|
|
614
|
+
function nextDataGridId() {
|
|
615
|
+
uid += 1;
|
|
616
|
+
return `data-grid-${uid}`;
|
|
617
|
+
}
|
|
618
|
+
function defaultAccessor(column, row) {
|
|
619
|
+
return column.accessor ? column.accessor(row) : row[column.id];
|
|
620
|
+
}
|
|
621
|
+
function formatCell(column, row) {
|
|
622
|
+
const value = defaultAccessor(column, row);
|
|
623
|
+
if (column.format) return column.format(value, row);
|
|
624
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
625
|
+
}
|
|
626
|
+
var DataGridAttrs = class DataGridAttrs {
|
|
627
|
+
constructor() {
|
|
628
|
+
this.lilyDataGridAttrs = input(void 0, ...ngDevMode ? [{ debugName: "lilyDataGridAttrs" }] : /* istanbul ignore next */ []);
|
|
629
|
+
this.host = inject(ElementRef);
|
|
630
|
+
this.applied = [];
|
|
631
|
+
effect(() => {
|
|
632
|
+
const next = this.lilyDataGridAttrs() ?? {};
|
|
633
|
+
for (const name of this.applied) if (!(name in next)) this.host.nativeElement.removeAttribute(name);
|
|
634
|
+
for (const [name, value] of Object.entries(next)) this.host.nativeElement.setAttribute(name, value);
|
|
635
|
+
this.applied = Object.keys(next);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
static {
|
|
639
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
640
|
+
minVersion: "12.0.0",
|
|
641
|
+
version: "22.2.3",
|
|
642
|
+
ngImport: i0,
|
|
643
|
+
type: DataGridAttrs,
|
|
644
|
+
deps: [],
|
|
645
|
+
target: i0.ɵɵFactoryTarget.Directive
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
static {
|
|
649
|
+
this.ɵdir = i0.ɵɵngDeclareDirective({
|
|
650
|
+
minVersion: "17.1.0",
|
|
651
|
+
version: "22.2.3",
|
|
652
|
+
type: DataGridAttrs,
|
|
653
|
+
isStandalone: true,
|
|
654
|
+
selector: "[lilyDataGridAttrs]",
|
|
655
|
+
inputs: { lilyDataGridAttrs: {
|
|
656
|
+
classPropertyName: "lilyDataGridAttrs",
|
|
657
|
+
publicName: "lilyDataGridAttrs",
|
|
658
|
+
isSignal: true,
|
|
659
|
+
isRequired: false,
|
|
660
|
+
transformFunction: null
|
|
661
|
+
} },
|
|
662
|
+
ngImport: i0
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
667
|
+
minVersion: "12.0.0",
|
|
668
|
+
version: "22.2.3",
|
|
669
|
+
ngImport: i0,
|
|
670
|
+
type: DataGridAttrs,
|
|
671
|
+
decorators: [{
|
|
672
|
+
type: Directive,
|
|
673
|
+
args: [{
|
|
674
|
+
selector: "[lilyDataGridAttrs]",
|
|
675
|
+
standalone: true
|
|
676
|
+
}]
|
|
677
|
+
}],
|
|
678
|
+
ctorParameters: () => [],
|
|
679
|
+
propDecorators: { lilyDataGridAttrs: [{
|
|
680
|
+
type: i0.Input,
|
|
681
|
+
args: [{
|
|
682
|
+
isSignal: true,
|
|
683
|
+
alias: "lilyDataGridAttrs",
|
|
684
|
+
required: false
|
|
685
|
+
}]
|
|
686
|
+
}] }
|
|
687
|
+
});
|
|
688
|
+
var DataGrid = class DataGrid {
|
|
689
|
+
cellFor(column, row) {
|
|
690
|
+
const cache = this.cellCache();
|
|
691
|
+
let perRow = cache.get(row);
|
|
692
|
+
if (!perRow) {
|
|
693
|
+
perRow = /* @__PURE__ */ new Map();
|
|
694
|
+
cache.set(row, perRow);
|
|
695
|
+
}
|
|
696
|
+
let ctx = perRow.get(column.id);
|
|
697
|
+
if (!ctx) {
|
|
698
|
+
ctx = {
|
|
699
|
+
value: defaultAccessor(column, row),
|
|
700
|
+
formatted: formatCell(column, row),
|
|
701
|
+
row,
|
|
702
|
+
column
|
|
703
|
+
};
|
|
704
|
+
perRow.set(column.id, ctx);
|
|
705
|
+
}
|
|
706
|
+
return ctx;
|
|
707
|
+
}
|
|
708
|
+
constructor() {
|
|
709
|
+
this.label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
|
|
710
|
+
this.caption = input(void 0, ...ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []);
|
|
711
|
+
this.columns = input.required(...ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []);
|
|
712
|
+
this.rows = input.required(...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
|
|
713
|
+
this.locale = input(void 0, ...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
|
|
714
|
+
this.rowId = input((_row, index) => String(index), ...ngDevMode ? [{ debugName: "rowId" }] : /* istanbul ignore next */ []);
|
|
715
|
+
this.selectionMode = input("none", ...ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []);
|
|
716
|
+
this.pageSize = input(void 0, ...ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []);
|
|
717
|
+
this.storageKey = input(void 0, ...ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []);
|
|
718
|
+
this.labels = input({}, ...ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []);
|
|
719
|
+
this.mode = input("grid", ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
|
|
720
|
+
this.rowClass = input(void 0, ...ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []);
|
|
721
|
+
this.rowAttrs = input(void 0, ...ngDevMode ? [{ debugName: "rowAttrs" }] : /* istanbul ignore next */ []);
|
|
722
|
+
this.activateRows = input(false, ...ngDevMode ? [{ debugName: "activateRows" }] : /* istanbul ignore next */ []);
|
|
723
|
+
this.empty = input(void 0, ...ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []);
|
|
724
|
+
this.footer = input(void 0, ...ngDevMode ? [{ debugName: "footer" }] : /* istanbul ignore next */ []);
|
|
725
|
+
this.busy = input(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
|
|
726
|
+
this.scroll = input(true, ...ngDevMode ? [{ debugName: "scroll" }] : /* istanbul ignore next */ []);
|
|
727
|
+
this.exportFilename = input("data-grid", ...ngDevMode ? [{ debugName: "exportFilename" }] : /* istanbul ignore next */ []);
|
|
728
|
+
this.exportScope = input("view", ...ngDevMode ? [{ debugName: "exportScope" }] : /* istanbul ignore next */ []);
|
|
729
|
+
this.exportFormulas = input(false, ...ngDevMode ? [{ debugName: "exportFormulas" }] : /* istanbul ignore next */ []);
|
|
730
|
+
this.exportHandler = input(void 0, ...ngDevMode ? [{ debugName: "exportHandler" }] : /* istanbul ignore next */ []);
|
|
731
|
+
this.virtualize = input(void 0, ...ngDevMode ? [{ debugName: "virtualize" }] : /* istanbul ignore next */ []);
|
|
732
|
+
this.className = input("", ...ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []);
|
|
733
|
+
this.selected = model([], ...ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []);
|
|
734
|
+
this.filter = model("", ...ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []);
|
|
735
|
+
this.sort = model({
|
|
736
|
+
columnId: "",
|
|
737
|
+
direction: "none"
|
|
738
|
+
}, ...ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []);
|
|
739
|
+
this.columnOrder = model([], ...ngDevMode ? [{ debugName: "columnOrder" }] : /* istanbul ignore next */ []);
|
|
740
|
+
this.page = model(1, ...ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []);
|
|
741
|
+
this.cellEdit = output();
|
|
742
|
+
this.filesSelected = output();
|
|
743
|
+
this.fileRemove = output();
|
|
744
|
+
this.rowActivate = output();
|
|
745
|
+
this.exportFile = output();
|
|
746
|
+
this.gridId = nextDataGridId();
|
|
747
|
+
this.minColumnWidth = MIN_COLUMN_WIDTH;
|
|
748
|
+
this.rootRef = viewChild.required("rootEl", ...ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []);
|
|
749
|
+
this.scrollRef = viewChild("scrollEl", ...ngDevMode ? [{ debugName: "scrollRef" }] : /* istanbul ignore next */ []);
|
|
750
|
+
this.injector = inject(Injector);
|
|
751
|
+
this.statusMessage = signal("", ...ngDevMode ? [{ debugName: "statusMessage" }] : /* istanbul ignore next */ []);
|
|
752
|
+
this.hiddenColumnIds = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "hiddenColumnIds" }] : /* istanbul ignore next */ []);
|
|
753
|
+
this.columnWidths = signal({}, ...ngDevMode ? [{ debugName: "columnWidths" }] : /* istanbul ignore next */ []);
|
|
754
|
+
this.focusedRow = signal(-1, ...ngDevMode ? [{ debugName: "focusedRow" }] : /* istanbul ignore next */ []);
|
|
755
|
+
this.focusedCol = signal(0, ...ngDevMode ? [{ debugName: "focusedCol" }] : /* istanbul ignore next */ []);
|
|
756
|
+
this.lastSelectedIndex = -1;
|
|
757
|
+
this.editingState = signal(null, ...ngDevMode ? [{ debugName: "editingState" }] : /* istanbul ignore next */ []);
|
|
758
|
+
this.scrollTop = signal(0, ...ngDevMode ? [{ debugName: "scrollTop" }] : /* istanbul ignore next */ []);
|
|
759
|
+
this.draggingColumnId = null;
|
|
760
|
+
this.resolvedColumns = computed(() => this.columns().map((c) => resolveColumn(c, this.locale(), this.rows())), ...ngDevMode ? [{ debugName: "resolvedColumns" }] : /* istanbul ignore next */ []);
|
|
761
|
+
this.isGrid = computed(() => this.mode() === "grid", ...ngDevMode ? [{ debugName: "isGrid" }] : /* istanbul ignore next */ []);
|
|
762
|
+
this.hasSelection = computed(() => this.selectionMode() !== "none", ...ngDevMode ? [{ debugName: "hasSelection" }] : /* istanbul ignore next */ []);
|
|
763
|
+
this.colOffset = computed(() => this.hasSelection() ? 1 : 0, ...ngDevMode ? [{ debugName: "colOffset" }] : /* istanbul ignore next */ []);
|
|
764
|
+
this.orderedColumns = computed(() => {
|
|
765
|
+
const order = this.columnOrder();
|
|
766
|
+
const columns = this.resolvedColumns();
|
|
767
|
+
if (order.length === 0) return columns;
|
|
768
|
+
const rank = (id) => {
|
|
769
|
+
const i = order.indexOf(id);
|
|
770
|
+
return i === -1 ? order.length : i;
|
|
771
|
+
};
|
|
772
|
+
return columns.map((c, i) => ({
|
|
773
|
+
c,
|
|
774
|
+
i
|
|
775
|
+
})).sort((a, b) => rank(a.c.id) - rank(b.c.id) || a.i - b.i).map((x) => x.c);
|
|
776
|
+
}, ...ngDevMode ? [{ debugName: "orderedColumns" }] : /* istanbul ignore next */ []);
|
|
777
|
+
this.visibleColumns = computed(() => this.orderedColumns().filter((c) => !this.hiddenColumnIds().has(c.id)), ...ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []);
|
|
778
|
+
this.hidableColumns = computed(() => this.resolvedColumns().filter((c) => c.hidable), ...ngDevMode ? [{ debugName: "hidableColumns" }] : /* istanbul ignore next */ []);
|
|
779
|
+
this.entries = computed(() => {
|
|
780
|
+
const rowId = this.rowId();
|
|
781
|
+
return this.rows().map((row, index) => ({
|
|
782
|
+
row,
|
|
783
|
+
id: rowId(row, index)
|
|
784
|
+
}));
|
|
785
|
+
}, ...ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []);
|
|
786
|
+
this.cellCache = computed(() => {
|
|
787
|
+
this.resolvedColumns();
|
|
788
|
+
return /* @__PURE__ */ new WeakMap();
|
|
789
|
+
}, ...ngDevMode ? [{ debugName: "cellCache" }] : /* istanbul ignore next */ []);
|
|
790
|
+
this.searchIndex = computed(() => {
|
|
791
|
+
const filterable = this.resolvedColumns().filter((c) => c.filterable !== false);
|
|
792
|
+
return this.entries().map((entry) => filterable.map((column) => (column.filterText ? column.filterText(this.cellFor(column, entry.row).value, entry.row) : this.cellFor(column, entry.row).formatted).toLowerCase()));
|
|
793
|
+
}, ...ngDevMode ? [{ debugName: "searchIndex" }] : /* istanbul ignore next */ []);
|
|
794
|
+
this.filteredEntries = computed(() => {
|
|
795
|
+
const text = this.filter().trim().toLowerCase();
|
|
796
|
+
if (!text) return this.entries();
|
|
797
|
+
const index = this.searchIndex();
|
|
798
|
+
return this.entries().filter((_entry, i) => index[i].some((value) => value.includes(text)));
|
|
799
|
+
}, ...ngDevMode ? [{ debugName: "filteredEntries" }] : /* istanbul ignore next */ []);
|
|
800
|
+
this.sortedEntries = computed(() => {
|
|
801
|
+
const sort = this.sort();
|
|
802
|
+
const filtered = this.filteredEntries();
|
|
803
|
+
if (sort.direction === "none" || !sort.columnId) return filtered;
|
|
804
|
+
const column = this.resolvedColumns().find((c) => c.id === sort.columnId);
|
|
805
|
+
if (!column) return filtered;
|
|
806
|
+
const dir = sort.direction === "ascending" ? 1 : -1;
|
|
807
|
+
const compare = column.compare;
|
|
808
|
+
const keyed = filtered.map((entry) => ({
|
|
809
|
+
entry,
|
|
810
|
+
key: this.cellFor(column, entry.row).value
|
|
811
|
+
}));
|
|
812
|
+
keyed.sort((a, b) => {
|
|
813
|
+
const av = a.key;
|
|
814
|
+
const bv = b.key;
|
|
815
|
+
if (av === bv) return 0;
|
|
816
|
+
if (av === null || av === void 0) return 1;
|
|
817
|
+
if (bv === null || bv === void 0) return -1;
|
|
818
|
+
if (compare) return compare(av, bv) * dir;
|
|
819
|
+
return av > bv ? dir : -dir;
|
|
820
|
+
});
|
|
821
|
+
return keyed.map((k) => k.entry);
|
|
822
|
+
}, ...ngDevMode ? [{ debugName: "sortedEntries" }] : /* istanbul ignore next */ []);
|
|
823
|
+
this.pageCount = computed(() => {
|
|
824
|
+
const size = this.pageSize();
|
|
825
|
+
return size ? Math.max(1, Math.ceil(this.sortedEntries().length / size)) : 1;
|
|
826
|
+
}, ...ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []);
|
|
827
|
+
this.clampedPage = computed(() => Math.min(Math.max(this.page(), 1), this.pageCount()), ...ngDevMode ? [{ debugName: "clampedPage" }] : /* istanbul ignore next */ []);
|
|
828
|
+
this.pageStart = computed(() => {
|
|
829
|
+
const size = this.pageSize();
|
|
830
|
+
return size ? (this.clampedPage() - 1) * size : 0;
|
|
831
|
+
}, ...ngDevMode ? [{ debugName: "pageStart" }] : /* istanbul ignore next */ []);
|
|
832
|
+
this.pageEntries = computed(() => {
|
|
833
|
+
const size = this.pageSize();
|
|
834
|
+
return size ? this.sortedEntries().slice(this.pageStart(), this.pageStart() + size) : this.sortedEntries();
|
|
835
|
+
}, ...ngDevMode ? [{ debugName: "pageEntries" }] : /* istanbul ignore next */ []);
|
|
836
|
+
this.lastCol = computed(() => this.colOffset() + this.visibleColumns().length - 1, ...ngDevMode ? [{ debugName: "lastCol" }] : /* istanbul ignore next */ []);
|
|
837
|
+
this.activeRow = computed(() => Math.max(-1, Math.min(this.focusedRow(), this.pageEntries().length - 1)), ...ngDevMode ? [{ debugName: "activeRow" }] : /* istanbul ignore next */ []);
|
|
838
|
+
this.activeCol = computed(() => Math.max(0, Math.min(this.focusedCol(), this.lastCol())), ...ngDevMode ? [{ debugName: "activeCol" }] : /* istanbul ignore next */ []);
|
|
839
|
+
this.indexed = computed(() => !!this.pageSize() || !!this.virtualize(), ...ngDevMode ? [{ debugName: "indexed" }] : /* istanbul ignore next */ []);
|
|
840
|
+
this.rowCount = computed(() => this.pageSize() ? this.sortedEntries().length + 1 : this.virtualize() ? this.pageEntries().length + 1 : void 0, ...ngDevMode ? [{ debugName: "rowCount" }] : /* istanbul ignore next */ []);
|
|
841
|
+
this.renderItems = computed(() => {
|
|
842
|
+
const total = this.pageEntries().length;
|
|
843
|
+
const v = this.virtualize();
|
|
844
|
+
if (!v) return this.pageEntries().map((_, index) => ({
|
|
845
|
+
kind: "row",
|
|
846
|
+
index
|
|
847
|
+
}));
|
|
848
|
+
const overscan = v.overscan ?? 3;
|
|
849
|
+
const top = this.scrollTop();
|
|
850
|
+
const start = Math.max(0, Math.floor(top / v.rowHeight) - overscan);
|
|
851
|
+
const end = Math.min(total, Math.ceil((top + v.height) / v.rowHeight) + overscan);
|
|
852
|
+
const indices = /* @__PURE__ */ new Set();
|
|
853
|
+
for (let i = start; i < end; i++) indices.add(i);
|
|
854
|
+
const active = this.activeRow();
|
|
855
|
+
if (active >= 0 && active < total) indices.add(active);
|
|
856
|
+
const items = [];
|
|
857
|
+
let previous = -1;
|
|
858
|
+
for (const index of [...indices].sort((a, b) => a - b)) {
|
|
859
|
+
if (index - previous > 1) items.push({
|
|
860
|
+
kind: "spacer",
|
|
861
|
+
rows: index - previous - 1,
|
|
862
|
+
key: `spacer-${previous}`
|
|
863
|
+
});
|
|
864
|
+
items.push({
|
|
865
|
+
kind: "row",
|
|
866
|
+
index
|
|
867
|
+
});
|
|
868
|
+
previous = index;
|
|
869
|
+
}
|
|
870
|
+
if (total - 1 - previous > 0) items.push({
|
|
871
|
+
kind: "spacer",
|
|
872
|
+
rows: total - 1 - previous,
|
|
873
|
+
key: `spacer-${previous}`
|
|
874
|
+
});
|
|
875
|
+
return items;
|
|
876
|
+
}, ...ngDevMode ? [{ debugName: "renderItems" }] : /* istanbul ignore next */ []);
|
|
877
|
+
this.scrollStyle = computed(() => {
|
|
878
|
+
const v = this.virtualize();
|
|
879
|
+
return v ? `height:${v.height}px;overflow:auto` : null;
|
|
880
|
+
}, ...ngDevMode ? [{ debugName: "scrollStyle" }] : /* istanbul ignore next */ []);
|
|
881
|
+
this.selectedSet = computed(() => new Set(this.selected()), ...ngDevMode ? [{ debugName: "selectedSet" }] : /* istanbul ignore next */ []);
|
|
882
|
+
this.allSelected = computed(() => this.sortedEntries().length > 0 && this.sortedEntries().every((e) => this.selectedSet().has(e.id)), ...ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []);
|
|
883
|
+
this.someSelected = computed(() => !this.allSelected() && this.sortedEntries().some((e) => this.selectedSet().has(e.id)), ...ngDevMode ? [{ debugName: "someSelected" }] : /* istanbul ignore next */ []);
|
|
884
|
+
this.footerContext = computed(() => ({
|
|
885
|
+
columns: this.visibleColumns(),
|
|
886
|
+
rows: this.pageEntries().map((e) => e.row)
|
|
887
|
+
}), ...ngDevMode ? [{ debugName: "footerContext" }] : /* istanbul ignore next */ []);
|
|
888
|
+
this.exportColumns = computed(() => this.visibleColumns().filter((c) => c.exportable !== false), ...ngDevMode ? [{ debugName: "exportColumns" }] : /* istanbul ignore next */ []);
|
|
889
|
+
this.exportEntries = computed(() => this.exportScope() === "selected" ? this.sortedEntries().filter((e) => this.selectedSet().has(e.id)) : this.sortedEntries(), ...ngDevMode ? [{ debugName: "exportEntries" }] : /* istanbul ignore next */ []);
|
|
890
|
+
this.hasExportButtons = computed(() => {
|
|
891
|
+
const l = this.labels();
|
|
892
|
+
return !!(l.exportCsv || l.exportTsv || l.exportExcel);
|
|
893
|
+
}, ...ngDevMode ? [{ debugName: "hasExportButtons" }] : /* istanbul ignore next */ []);
|
|
894
|
+
effect(() => {
|
|
895
|
+
const root = this.rootRef().nativeElement;
|
|
896
|
+
const grid = this.isGrid();
|
|
897
|
+
const count = this.rowCount();
|
|
898
|
+
const busy = this.busy();
|
|
899
|
+
this.sortedEntries();
|
|
900
|
+
this.pageEntries();
|
|
901
|
+
this.visibleColumns();
|
|
902
|
+
this.scroll();
|
|
903
|
+
afterNextRender(() => {
|
|
904
|
+
const table = root.querySelector("table.data-table");
|
|
905
|
+
if (!table) return;
|
|
906
|
+
table.setAttribute("role", grid ? "grid" : "table");
|
|
907
|
+
if (grid && count !== void 0) table.setAttribute("aria-rowcount", String(count));
|
|
908
|
+
else table.removeAttribute("aria-rowcount");
|
|
909
|
+
if (busy) table.setAttribute("aria-busy", "true");
|
|
910
|
+
else table.removeAttribute("aria-busy");
|
|
911
|
+
}, { injector: this.injector });
|
|
912
|
+
});
|
|
913
|
+
effect(() => {
|
|
914
|
+
this.pageEntries();
|
|
915
|
+
this.visibleColumns();
|
|
916
|
+
this.isGrid();
|
|
917
|
+
this.scrollTop();
|
|
918
|
+
this.editingState();
|
|
919
|
+
afterNextRender(() => this.normaliseWidgets(), { injector: this.injector });
|
|
920
|
+
});
|
|
921
|
+
let restored = false;
|
|
922
|
+
effect(() => {
|
|
923
|
+
const key = this.storageKey();
|
|
924
|
+
if (!key || restored) return;
|
|
925
|
+
restored = true;
|
|
926
|
+
untracked(() => this.restoreView(key));
|
|
927
|
+
});
|
|
928
|
+
effect(() => {
|
|
929
|
+
const key = this.storageKey();
|
|
930
|
+
const snapshot = {
|
|
931
|
+
columnWidths: this.columnWidths(),
|
|
932
|
+
hiddenColumnIds: [...this.hiddenColumnIds()],
|
|
933
|
+
sort: this.sort()
|
|
934
|
+
};
|
|
935
|
+
if (!key || typeof localStorage === "undefined") return;
|
|
936
|
+
try {
|
|
937
|
+
localStorage.setItem(key, JSON.stringify(snapshot));
|
|
938
|
+
} catch {}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
announce(message) {
|
|
942
|
+
if (message) this.statusMessage.set(message);
|
|
943
|
+
}
|
|
944
|
+
ariaSortFor(column) {
|
|
945
|
+
if (!column.sortable) return null;
|
|
946
|
+
return this.sort().columnId === column.id ? this.sort().direction : "none";
|
|
947
|
+
}
|
|
948
|
+
toggleSort(column) {
|
|
949
|
+
if (!column.sortable) return;
|
|
950
|
+
const current = this.sort();
|
|
951
|
+
const next = current.columnId !== column.id ? {
|
|
952
|
+
columnId: column.id,
|
|
953
|
+
direction: "ascending"
|
|
954
|
+
} : current.direction === "ascending" ? {
|
|
955
|
+
columnId: column.id,
|
|
956
|
+
direction: "descending"
|
|
957
|
+
} : current.direction === "descending" ? {
|
|
958
|
+
columnId: "",
|
|
959
|
+
direction: "none"
|
|
960
|
+
} : {
|
|
961
|
+
columnId: column.id,
|
|
962
|
+
direction: "ascending"
|
|
963
|
+
};
|
|
964
|
+
this.sort.set(next);
|
|
965
|
+
this.announce(this.labels().sortAnnouncement?.(column.header, next.direction));
|
|
966
|
+
}
|
|
967
|
+
onFilterInput(event) {
|
|
968
|
+
this.filter.set(event.target.value);
|
|
969
|
+
this.page.set(1);
|
|
970
|
+
this.announce(this.labels().filterAnnouncement?.(this.filteredEntries().length, this.rows().length));
|
|
971
|
+
}
|
|
972
|
+
isSelected(id) {
|
|
973
|
+
return this.selectedSet().has(id);
|
|
974
|
+
}
|
|
975
|
+
setSelected(next) {
|
|
976
|
+
this.selected.set(next);
|
|
977
|
+
this.announce(this.labels().selectionAnnouncement?.(next.length));
|
|
978
|
+
}
|
|
979
|
+
onRowSelectClick(index, event) {
|
|
980
|
+
const e = event;
|
|
981
|
+
this.toggleRow(index, e);
|
|
982
|
+
}
|
|
983
|
+
toggleRow(index, event) {
|
|
984
|
+
const sorted = this.sortedEntries();
|
|
985
|
+
const id = sorted[index].id;
|
|
986
|
+
const selected = this.selected();
|
|
987
|
+
const mode = this.selectionMode();
|
|
988
|
+
if (mode === "single") this.setSelected(this.isSelected(id) ? [] : [id]);
|
|
989
|
+
else if (mode === "multiple") {
|
|
990
|
+
if (event?.shiftKey && this.lastSelectedIndex >= 0) {
|
|
991
|
+
const [start, end] = this.lastSelectedIndex < index ? [this.lastSelectedIndex, index] : [index, this.lastSelectedIndex];
|
|
992
|
+
const range = sorted.slice(start, end + 1).map((e) => e.id);
|
|
993
|
+
this.setSelected([.../* @__PURE__ */ new Set([...selected, ...range])]);
|
|
994
|
+
} else if (event?.ctrlKey || event?.metaKey) this.setSelected(this.isSelected(id) ? selected.filter((x) => x !== id) : [...selected, id]);
|
|
995
|
+
else this.setSelected(this.isSelected(id) && selected.length === 1 ? [] : [id]);
|
|
996
|
+
}
|
|
997
|
+
this.lastSelectedIndex = index;
|
|
998
|
+
}
|
|
999
|
+
toggleSelectAll() {
|
|
1000
|
+
if (this.allSelected()) this.setSelected([]);
|
|
1001
|
+
else this.setSelected(this.sortedEntries().map((e) => e.id));
|
|
1002
|
+
}
|
|
1003
|
+
widthFor(column) {
|
|
1004
|
+
return this.columnWidths()[column.id] ?? column.width;
|
|
1005
|
+
}
|
|
1006
|
+
cellStyle(column) {
|
|
1007
|
+
const parts = [];
|
|
1008
|
+
const width = this.widthFor(column);
|
|
1009
|
+
if (width) parts.push(`width:${width}px`);
|
|
1010
|
+
if (column.align) parts.push(`text-align:${column.align}`);
|
|
1011
|
+
return parts.length > 0 ? parts.join(";") : null;
|
|
1012
|
+
}
|
|
1013
|
+
setWidth(column, width) {
|
|
1014
|
+
this.columnWidths.update((current) => ({
|
|
1015
|
+
...current,
|
|
1016
|
+
[column.id]: Math.max(MIN_COLUMN_WIDTH, width)
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
onResizeKeydown(column, event) {
|
|
1020
|
+
const e = event;
|
|
1021
|
+
e.stopPropagation();
|
|
1022
|
+
const current = this.widthFor(column) ?? 120;
|
|
1023
|
+
if (e.key === "ArrowRight") {
|
|
1024
|
+
e.preventDefault();
|
|
1025
|
+
this.setWidth(column, current + RESIZE_STEP);
|
|
1026
|
+
} else if (e.key === "ArrowLeft") {
|
|
1027
|
+
e.preventDefault();
|
|
1028
|
+
this.setWidth(column, current - RESIZE_STEP);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
onResizePointerDown(column, event) {
|
|
1032
|
+
const e = event;
|
|
1033
|
+
e.preventDefault();
|
|
1034
|
+
const startX = e.clientX;
|
|
1035
|
+
const target = e.target;
|
|
1036
|
+
const startWidth = this.widthFor(column) ?? target.closest("th")?.getBoundingClientRect().width ?? 120;
|
|
1037
|
+
target.setPointerCapture?.(e.pointerId);
|
|
1038
|
+
const onMove = (moveEvent) => this.setWidth(column, startWidth + (moveEvent.clientX - startX));
|
|
1039
|
+
const onUp = () => {
|
|
1040
|
+
window.removeEventListener("pointermove", onMove);
|
|
1041
|
+
window.removeEventListener("pointerup", onUp);
|
|
1042
|
+
};
|
|
1043
|
+
window.addEventListener("pointermove", onMove);
|
|
1044
|
+
window.addEventListener("pointerup", onUp);
|
|
1045
|
+
}
|
|
1046
|
+
toggleColumnVisibility(columnId) {
|
|
1047
|
+
this.hiddenColumnIds.update((current) => {
|
|
1048
|
+
const next = new Set(current);
|
|
1049
|
+
if (next.has(columnId)) next.delete(columnId);
|
|
1050
|
+
else next.add(columnId);
|
|
1051
|
+
return next;
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
goToPage(next) {
|
|
1055
|
+
const target = Math.min(Math.max(next, 1), this.pageCount());
|
|
1056
|
+
if (target === this.clampedPage()) return;
|
|
1057
|
+
this.page.set(target);
|
|
1058
|
+
this.announce(this.labels().pageAnnouncement?.(target, this.pageCount()));
|
|
1059
|
+
}
|
|
1060
|
+
moveColumn(columnId, toVisibleIndex) {
|
|
1061
|
+
const ids = this.visibleColumns().map((c) => c.id);
|
|
1062
|
+
const from = ids.indexOf(columnId);
|
|
1063
|
+
const to = Math.min(Math.max(toVisibleIndex, 0), ids.length - 1);
|
|
1064
|
+
if (from === -1 || from === to) return false;
|
|
1065
|
+
ids.splice(to, 0, ...ids.splice(from, 1));
|
|
1066
|
+
const visibleSet = new Set(ids);
|
|
1067
|
+
let next = 0;
|
|
1068
|
+
this.columnOrder.set(this.orderedColumns().map((c) => visibleSet.has(c.id) ? ids[next++] : c.id));
|
|
1069
|
+
const column = this.resolvedColumns().find((c) => c.id === columnId);
|
|
1070
|
+
this.announce(this.labels().columnMoveAnnouncement?.(column.header, to + 1, ids.length));
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
onHeaderDragStart(column, event) {
|
|
1074
|
+
if (!column.movable) return;
|
|
1075
|
+
this.draggingColumnId = column.id;
|
|
1076
|
+
event.dataTransfer?.setData("text/plain", column.id);
|
|
1077
|
+
}
|
|
1078
|
+
onHeaderDragOver(event) {
|
|
1079
|
+
if (this.draggingColumnId) event.preventDefault();
|
|
1080
|
+
}
|
|
1081
|
+
onHeaderDrop(targetId, event) {
|
|
1082
|
+
event.preventDefault();
|
|
1083
|
+
const id = this.draggingColumnId;
|
|
1084
|
+
this.draggingColumnId = null;
|
|
1085
|
+
if (!id || id === targetId) return;
|
|
1086
|
+
const to = this.visibleColumns().findIndex((c) => c.id === targetId);
|
|
1087
|
+
if (to >= 0) this.moveColumn(id, to);
|
|
1088
|
+
}
|
|
1089
|
+
onScroll(event) {
|
|
1090
|
+
if (this.virtualize()) this.scrollTop.set(event.currentTarget.scrollTop);
|
|
1091
|
+
}
|
|
1092
|
+
revealRow(index) {
|
|
1093
|
+
const v = this.virtualize();
|
|
1094
|
+
if (!v || index < 0) return;
|
|
1095
|
+
const top = index * v.rowHeight;
|
|
1096
|
+
const current = this.scrollTop();
|
|
1097
|
+
let next = current;
|
|
1098
|
+
if (top < current) next = top;
|
|
1099
|
+
else if (top + v.rowHeight > current + v.height) next = top + v.rowHeight - v.height;
|
|
1100
|
+
if (next === current) return;
|
|
1101
|
+
this.scrollTop.set(next);
|
|
1102
|
+
const el = this.scrollRef()?.nativeElement;
|
|
1103
|
+
if (el) el.scrollTop = next;
|
|
1104
|
+
}
|
|
1105
|
+
focusActiveCell() {
|
|
1106
|
+
afterNextRender(() => {
|
|
1107
|
+
this.rootRef().nativeElement.querySelector(".data-table-th[tabindex=\"0\"], .data-table-td[tabindex=\"0\"]")?.focus({ preventScroll: true });
|
|
1108
|
+
}, { injector: this.injector });
|
|
1109
|
+
}
|
|
1110
|
+
moveFocus(row, col) {
|
|
1111
|
+
const nextRow = Math.min(Math.max(row, -1), this.pageEntries().length - 1);
|
|
1112
|
+
this.focusedRow.set(nextRow);
|
|
1113
|
+
this.focusedCol.set(Math.min(Math.max(col, 0), this.lastCol()));
|
|
1114
|
+
this.revealRow(nextRow);
|
|
1115
|
+
this.focusActiveCell();
|
|
1116
|
+
}
|
|
1117
|
+
exportCell(column, row) {
|
|
1118
|
+
if (column.exportValue) return column.exportValue(row);
|
|
1119
|
+
const ctx = this.cellFor(column, row);
|
|
1120
|
+
const value = ctx.value;
|
|
1121
|
+
if (value === null || value === void 0) return null;
|
|
1122
|
+
if (typeof value === "number" || typeof value === "boolean" || value instanceof Date) return value;
|
|
1123
|
+
return ctx.formatted;
|
|
1124
|
+
}
|
|
1125
|
+
hasUtcColumn(c) {
|
|
1126
|
+
return c.type === "time-zone" && !!c.exportUtc && !!c.referenceDate && !!this.labels().exportUtcHeader;
|
|
1127
|
+
}
|
|
1128
|
+
runExport(format) {
|
|
1129
|
+
const live = (c) => format === "xlsx" && this.exportFormulas() && c.type === "formula" && !!c.source;
|
|
1130
|
+
const headers = [];
|
|
1131
|
+
for (const c of this.exportColumns()) {
|
|
1132
|
+
headers.push(c.header);
|
|
1133
|
+
if (this.hasUtcColumn(c)) headers.push(this.labels().exportUtcHeader(c.header));
|
|
1134
|
+
}
|
|
1135
|
+
const rows = this.exportEntries().map((entry) => {
|
|
1136
|
+
const cells = [];
|
|
1137
|
+
for (const c of this.exportColumns()) {
|
|
1138
|
+
const expression = live(c) ? c.source(entry.row) : void 0;
|
|
1139
|
+
cells.push(typeof expression === "string" && expression !== "" && !c.errorOf?.(entry.row) ? {
|
|
1140
|
+
formula: expression,
|
|
1141
|
+
result: this.exportCell(c, entry.row)
|
|
1142
|
+
} : this.exportCell(c, entry.row));
|
|
1143
|
+
if (this.hasUtcColumn(c)) cells.push(zonedUtcTime(this.cellFor(c, entry.row).value, c.referenceDate));
|
|
1144
|
+
}
|
|
1145
|
+
return cells;
|
|
1146
|
+
});
|
|
1147
|
+
const table = {
|
|
1148
|
+
headers,
|
|
1149
|
+
rows
|
|
1150
|
+
};
|
|
1151
|
+
const name = this.exportFilename();
|
|
1152
|
+
const filename = exportFilename(name, format);
|
|
1153
|
+
const blob = exportBlob(table, format, { sheetName: name });
|
|
1154
|
+
const file = {
|
|
1155
|
+
format,
|
|
1156
|
+
filename,
|
|
1157
|
+
rowCount: rows.length,
|
|
1158
|
+
blob
|
|
1159
|
+
};
|
|
1160
|
+
this.exportFile.emit(file);
|
|
1161
|
+
if (this.exportHandler()?.(file) === false) return;
|
|
1162
|
+
downloadBlob(blob, filename);
|
|
1163
|
+
this.announce(this.labels().exportAnnouncement?.(format, rows.length, filename));
|
|
1164
|
+
}
|
|
1165
|
+
onFileDragOver(column, event) {
|
|
1166
|
+
if (column.type !== "file" || !column.editable) return;
|
|
1167
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
1168
|
+
}
|
|
1169
|
+
onFileDrop(entry, column, event) {
|
|
1170
|
+
if (column.type !== "file" || !column.editable) return;
|
|
1171
|
+
const dropped = Array.from(event.dataTransfer?.files ?? []);
|
|
1172
|
+
if (dropped.length === 0) return;
|
|
1173
|
+
event.preventDefault();
|
|
1174
|
+
const files = column.multiple === false ? dropped.slice(0, 1) : dropped;
|
|
1175
|
+
this.filesSelected.emit({
|
|
1176
|
+
rowId: entry.id,
|
|
1177
|
+
row: entry.row,
|
|
1178
|
+
columnId: column.id,
|
|
1179
|
+
files
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
onFileInput(entry, column, event) {
|
|
1183
|
+
const input = event.target;
|
|
1184
|
+
const chosen = Array.from(input.files ?? []);
|
|
1185
|
+
if (chosen.length > 0) this.filesSelected.emit({
|
|
1186
|
+
rowId: entry.id,
|
|
1187
|
+
row: entry.row,
|
|
1188
|
+
columnId: column.id,
|
|
1189
|
+
files: chosen
|
|
1190
|
+
});
|
|
1191
|
+
input.value = "";
|
|
1192
|
+
}
|
|
1193
|
+
isEditable(column) {
|
|
1194
|
+
if (!column.editable) return false;
|
|
1195
|
+
if (column.type === "rollup" || column.type === "file") return false;
|
|
1196
|
+
if (column.type === "person" && !column.personEditor) return false;
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
isEditingCell(entry, column) {
|
|
1200
|
+
const e = this.editingState();
|
|
1201
|
+
return !!e && e.rowId === entry.id && e.columnId === column.id;
|
|
1202
|
+
}
|
|
1203
|
+
startEdit(entry, column) {
|
|
1204
|
+
if (!this.isEditable(column)) return;
|
|
1205
|
+
const raw = column.type === "formula" && column.source ? column.source(entry.row) : defaultAccessor(column, entry.row);
|
|
1206
|
+
if (column.type === "time-zone") {
|
|
1207
|
+
const zoned = isZonedTime(raw) ? raw : null;
|
|
1208
|
+
this.editingState.set({
|
|
1209
|
+
rowId: entry.id,
|
|
1210
|
+
columnId: column.id,
|
|
1211
|
+
draft: zoned?.time ?? "",
|
|
1212
|
+
zone: zoned?.timeZone ?? column.timeZones?.[0]?.id ?? ""
|
|
1213
|
+
});
|
|
1214
|
+
} else {
|
|
1215
|
+
const text = column.type === "link" ? linkParts(raw)?.href ?? "" : raw === null || raw === void 0 ? "" : String(raw);
|
|
1216
|
+
this.editingState.set({
|
|
1217
|
+
rowId: entry.id,
|
|
1218
|
+
columnId: column.id,
|
|
1219
|
+
draft: text
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
afterNextRender(() => {
|
|
1223
|
+
const editor = this.rootRef().nativeElement.querySelector(".data-grid-cell-editor");
|
|
1224
|
+
editor?.focus();
|
|
1225
|
+
if (editor instanceof HTMLInputElement && editor.type === "text") editor.select();
|
|
1226
|
+
}, { injector: this.injector });
|
|
1227
|
+
}
|
|
1228
|
+
parseDraft(column, row, draft, zone) {
|
|
1229
|
+
if (column.parse) return column.parse(draft, row);
|
|
1230
|
+
if (column.type === "time-zone") return draft === "" ? null : {
|
|
1231
|
+
time: draft,
|
|
1232
|
+
timeZone: zone ?? ""
|
|
1233
|
+
};
|
|
1234
|
+
if (column.editor === "number") return draft.trim() === "" ? null : Number(draft);
|
|
1235
|
+
if (column.editor === "date" || column.editor === "time" || column.editor === "select") return draft === "" ? null : draft;
|
|
1236
|
+
return draft;
|
|
1237
|
+
}
|
|
1238
|
+
isStructurallyEqual(a, b) {
|
|
1239
|
+
return typeof a === "object" && typeof b === "object" && a !== null && b !== null && JSON.stringify(a) === JSON.stringify(b);
|
|
1240
|
+
}
|
|
1241
|
+
commitValue(entry, column, value) {
|
|
1242
|
+
const message = column.validate?.(value, entry.row);
|
|
1243
|
+
const invalidType = isInvalidForType(column, value);
|
|
1244
|
+
if (message || invalidType) {
|
|
1245
|
+
const text = message ?? this.labels().invalidValue?.(column.header, column.type ?? "") ?? "";
|
|
1246
|
+
const current = this.editingState();
|
|
1247
|
+
if (current) this.editingState.set({
|
|
1248
|
+
...current,
|
|
1249
|
+
error: text || void 0,
|
|
1250
|
+
invalid: true
|
|
1251
|
+
});
|
|
1252
|
+
this.announce(this.labels().editError?.(column.header, text));
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
const previous = column.type === "formula" && column.source ? column.source(entry.row) : defaultAccessor(column, entry.row);
|
|
1256
|
+
this.editingState.set(null);
|
|
1257
|
+
if (value !== previous && !this.isStructurallyEqual(value, previous)) {
|
|
1258
|
+
this.cellEdit.emit({
|
|
1259
|
+
rowId: entry.id,
|
|
1260
|
+
row: entry.row,
|
|
1261
|
+
columnId: column.id,
|
|
1262
|
+
value,
|
|
1263
|
+
previous
|
|
1264
|
+
});
|
|
1265
|
+
this.announce(this.labels().editAnnouncement?.(column.header, column.format ? column.format(value, entry.row) : String(value ?? "")));
|
|
1266
|
+
}
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
commitEdit() {
|
|
1270
|
+
const current = this.editingState();
|
|
1271
|
+
if (!current) return;
|
|
1272
|
+
const entry = this.entries().find((e) => e.id === current.rowId);
|
|
1273
|
+
const column = this.resolvedColumns().find((c) => c.id === current.columnId);
|
|
1274
|
+
if (!entry || !column) {
|
|
1275
|
+
this.editingState.set(null);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (this.commitValue(entry, column, this.parseDraft(column, entry.row, current.draft, current.zone))) this.focusActiveCell();
|
|
1279
|
+
}
|
|
1280
|
+
cancelEdit() {
|
|
1281
|
+
const current = this.editingState();
|
|
1282
|
+
if (!current) return;
|
|
1283
|
+
this.editingState.set(null);
|
|
1284
|
+
const column = this.resolvedColumns().find((c) => c.id === current.columnId);
|
|
1285
|
+
if (column) this.announce(this.labels().editCancelAnnouncement?.(column.header));
|
|
1286
|
+
this.focusActiveCell();
|
|
1287
|
+
}
|
|
1288
|
+
onEditorKeydown(event) {
|
|
1289
|
+
const e = event;
|
|
1290
|
+
e.stopPropagation();
|
|
1291
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
1292
|
+
if (e.key === "Enter") {
|
|
1293
|
+
e.preventDefault();
|
|
1294
|
+
this.commitEdit();
|
|
1295
|
+
} else if (e.key === "Escape") {
|
|
1296
|
+
e.preventDefault();
|
|
1297
|
+
this.cancelEdit();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
onEditorBlur() {
|
|
1301
|
+
if (this.editingState()) this.commitEdit();
|
|
1302
|
+
}
|
|
1303
|
+
onZonedFocusout(event) {
|
|
1304
|
+
const e = event;
|
|
1305
|
+
if (this.editingState() && !e.currentTarget.contains(e.relatedTarget)) this.commitEdit();
|
|
1306
|
+
}
|
|
1307
|
+
setDraft(event) {
|
|
1308
|
+
const current = this.editingState();
|
|
1309
|
+
if (current) this.editingState.set({
|
|
1310
|
+
...current,
|
|
1311
|
+
draft: event.target.value,
|
|
1312
|
+
error: void 0,
|
|
1313
|
+
invalid: false
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
setZone(event) {
|
|
1317
|
+
const current = this.editingState();
|
|
1318
|
+
if (current) this.editingState.set({
|
|
1319
|
+
...current,
|
|
1320
|
+
zone: event.target.value,
|
|
1321
|
+
error: void 0,
|
|
1322
|
+
invalid: false
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
personEditorContext(entry, column, ctx) {
|
|
1326
|
+
return {
|
|
1327
|
+
value: ctx.value,
|
|
1328
|
+
commit: (v) => void this.commitValue(entry, column, v),
|
|
1329
|
+
cancel: () => this.cancelEdit()
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
activateFocusedCell() {
|
|
1333
|
+
if (this.activeCol() === 0 && this.hasSelection()) {
|
|
1334
|
+
if (this.activeRow() === -1) {
|
|
1335
|
+
if (this.selectionMode() === "multiple") this.toggleSelectAll();
|
|
1336
|
+
} else this.toggleRow(this.pageStart() + this.activeRow());
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const column = this.visibleColumns()[this.activeCol() - this.colOffset()];
|
|
1340
|
+
if (!column) return;
|
|
1341
|
+
if (this.activeRow() === -1) this.toggleSort(column);
|
|
1342
|
+
}
|
|
1343
|
+
widgetsIn(cell) {
|
|
1344
|
+
return Array.from(cell.querySelectorAll(WIDGET_SELECTOR)).filter((el) => !el.closest("[data-grid-own]"));
|
|
1345
|
+
}
|
|
1346
|
+
setWidgetTabindex(cell, value) {
|
|
1347
|
+
for (const widget of this.widgetsIn(cell)) widget.setAttribute("tabindex", value);
|
|
1348
|
+
}
|
|
1349
|
+
normaliseWidgets() {
|
|
1350
|
+
if (!this.isGrid()) return;
|
|
1351
|
+
for (const cell of this.rootRef().nativeElement.querySelectorAll(".data-table-body [data-row][data-col]")) {
|
|
1352
|
+
if (cell.getAttribute("data-grid-entered") === "true") continue;
|
|
1353
|
+
this.setWidgetTabindex(cell, "-1");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
enterCell(cell) {
|
|
1357
|
+
const first = this.widgetsIn(cell)[0];
|
|
1358
|
+
if (!first) return false;
|
|
1359
|
+
cell.setAttribute("data-grid-entered", "true");
|
|
1360
|
+
this.setWidgetTabindex(cell, "0");
|
|
1361
|
+
first.focus();
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
leaveCell(cell) {
|
|
1365
|
+
if (cell.getAttribute("data-grid-entered") !== "true") return;
|
|
1366
|
+
cell.removeAttribute("data-grid-entered");
|
|
1367
|
+
this.setWidgetTabindex(cell, "-1");
|
|
1368
|
+
}
|
|
1369
|
+
onGridFocusin(event) {
|
|
1370
|
+
if (!this.isGrid()) return;
|
|
1371
|
+
const target = event.target;
|
|
1372
|
+
const cell = target.closest("[data-row][data-col]");
|
|
1373
|
+
if (!cell) return;
|
|
1374
|
+
this.focusedRow.set(Number(cell.dataset["row"]));
|
|
1375
|
+
this.focusedCol.set(Number(cell.dataset["col"]));
|
|
1376
|
+
if (target !== cell && !target.closest("[data-grid-own]") && cell.closest(".data-table-body")) {
|
|
1377
|
+
cell.setAttribute("data-grid-entered", "true");
|
|
1378
|
+
this.setWidgetTabindex(cell, "0");
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
onGridFocusout(event) {
|
|
1382
|
+
if (!this.isGrid()) return;
|
|
1383
|
+
const e = event;
|
|
1384
|
+
const cell = e.target.closest("[data-row][data-col]");
|
|
1385
|
+
if (!cell) return;
|
|
1386
|
+
const next = e.relatedTarget;
|
|
1387
|
+
if (next && cell.contains(next)) return;
|
|
1388
|
+
this.leaveCell(cell);
|
|
1389
|
+
}
|
|
1390
|
+
onGridClick(event) {
|
|
1391
|
+
if (!this.activateRows()) return;
|
|
1392
|
+
const target = event.target;
|
|
1393
|
+
const cell = target.closest("[data-row][data-col]");
|
|
1394
|
+
if (!cell || cell.closest(".data-table-head")) return;
|
|
1395
|
+
if (target.closest(WIDGET_SELECTOR) || target.closest("[data-grid-own]")) return;
|
|
1396
|
+
const entry = this.pageEntries()[Number(cell.dataset["row"])];
|
|
1397
|
+
if (entry) this.rowActivate.emit({
|
|
1398
|
+
row: entry.row,
|
|
1399
|
+
id: entry.id
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
onGridDblclick(event) {
|
|
1403
|
+
const target = event.target;
|
|
1404
|
+
const cell = target.closest("[data-row][data-col]");
|
|
1405
|
+
if (!cell || cell.closest(".data-table-head") || target.closest("input, button, select, textarea")) return;
|
|
1406
|
+
const entry = this.pageEntries()[Number(cell.dataset["row"])];
|
|
1407
|
+
const column = this.visibleColumns()[Number(cell.dataset["col"]) - this.colOffset()];
|
|
1408
|
+
if (entry && column && this.isEditable(column)) this.startEdit(entry, column);
|
|
1409
|
+
}
|
|
1410
|
+
onGridKeydown(event) {
|
|
1411
|
+
if (!this.isGrid()) return;
|
|
1412
|
+
const e = event;
|
|
1413
|
+
const target = e.target;
|
|
1414
|
+
const cell = target.closest("[data-row][data-col]");
|
|
1415
|
+
if (!cell) return;
|
|
1416
|
+
if (target !== cell && !target.closest("[data-grid-own]")) {
|
|
1417
|
+
if (e.key === "Escape") {
|
|
1418
|
+
e.preventDefault();
|
|
1419
|
+
cell.focus({ preventScroll: true });
|
|
1420
|
+
}
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const activeRow = this.activeRow();
|
|
1424
|
+
const activeCol = this.activeCol();
|
|
1425
|
+
const colOffset = this.colOffset();
|
|
1426
|
+
if (e.altKey && activeRow === -1 && (e.key === "ArrowRight" || e.key === "ArrowLeft")) {
|
|
1427
|
+
const column = this.visibleColumns()[activeCol - colOffset];
|
|
1428
|
+
if (column?.movable) {
|
|
1429
|
+
e.preventDefault();
|
|
1430
|
+
const delta = e.key === "ArrowRight" ? 1 : -1;
|
|
1431
|
+
if (this.moveColumn(column.id, activeCol - colOffset + delta)) this.moveFocus(-1, activeCol + delta);
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const size = this.pageSize();
|
|
1436
|
+
switch (e.key) {
|
|
1437
|
+
case "ArrowRight":
|
|
1438
|
+
e.preventDefault();
|
|
1439
|
+
this.moveFocus(activeRow, activeCol + 1);
|
|
1440
|
+
break;
|
|
1441
|
+
case "ArrowLeft":
|
|
1442
|
+
e.preventDefault();
|
|
1443
|
+
this.moveFocus(activeRow, activeCol - 1);
|
|
1444
|
+
break;
|
|
1445
|
+
case "ArrowDown":
|
|
1446
|
+
e.preventDefault();
|
|
1447
|
+
this.moveFocus(activeRow + 1, activeCol);
|
|
1448
|
+
break;
|
|
1449
|
+
case "ArrowUp":
|
|
1450
|
+
e.preventDefault();
|
|
1451
|
+
this.moveFocus(activeRow - 1, activeCol);
|
|
1452
|
+
break;
|
|
1453
|
+
case "Home":
|
|
1454
|
+
e.preventDefault();
|
|
1455
|
+
if (e.ctrlKey || e.metaKey) this.moveFocus(-1, 0);
|
|
1456
|
+
else this.moveFocus(activeRow, 0);
|
|
1457
|
+
break;
|
|
1458
|
+
case "End":
|
|
1459
|
+
e.preventDefault();
|
|
1460
|
+
if (e.ctrlKey || e.metaKey) this.moveFocus(this.pageEntries().length - 1, this.lastCol());
|
|
1461
|
+
else this.moveFocus(activeRow, this.lastCol());
|
|
1462
|
+
break;
|
|
1463
|
+
case "PageDown":
|
|
1464
|
+
e.preventDefault();
|
|
1465
|
+
this.moveFocus(activeRow + (size ?? 10), activeCol);
|
|
1466
|
+
break;
|
|
1467
|
+
case "PageUp":
|
|
1468
|
+
e.preventDefault();
|
|
1469
|
+
this.moveFocus(activeRow - (size ?? 10), activeCol);
|
|
1470
|
+
break;
|
|
1471
|
+
case "F2": {
|
|
1472
|
+
const entry = this.pageEntries()[Number(cell.dataset["row"])];
|
|
1473
|
+
const column = this.visibleColumns()[Number(cell.dataset["col"]) - colOffset];
|
|
1474
|
+
if (target === cell && entry && column && this.isEditable(column)) {
|
|
1475
|
+
e.preventDefault();
|
|
1476
|
+
this.startEdit(entry, column);
|
|
1477
|
+
}
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
case "Enter":
|
|
1481
|
+
case " ":
|
|
1482
|
+
e.preventDefault();
|
|
1483
|
+
if (e.key === "Enter" && target === cell && cell.closest(".data-table-body")) {
|
|
1484
|
+
const entry = this.pageEntries()[Number(cell.dataset["row"])];
|
|
1485
|
+
const column = this.visibleColumns()[Number(cell.dataset["col"]) - colOffset];
|
|
1486
|
+
if (entry && column && this.isEditable(column)) {
|
|
1487
|
+
this.startEdit(entry, column);
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (target === cell && cell.closest(".data-table-body") && !cell.querySelector("[data-grid-own]")) {
|
|
1492
|
+
if (this.enterCell(cell)) break;
|
|
1493
|
+
if (e.key === "Enter" && this.activateRows()) {
|
|
1494
|
+
const entry = this.pageEntries()[Number(cell.dataset["row"])];
|
|
1495
|
+
if (entry) this.rowActivate.emit({
|
|
1496
|
+
row: entry.row,
|
|
1497
|
+
id: entry.id
|
|
1498
|
+
});
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
this.activateFocusedCell();
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
restoreView(key) {
|
|
1506
|
+
try {
|
|
1507
|
+
const raw = localStorage.getItem(key);
|
|
1508
|
+
if (raw) {
|
|
1509
|
+
const saved = JSON.parse(raw);
|
|
1510
|
+
if (saved.columnWidths) this.columnWidths.set(saved.columnWidths);
|
|
1511
|
+
if (saved.hiddenColumnIds) this.hiddenColumnIds.set(new Set(saved.hiddenColumnIds));
|
|
1512
|
+
if (saved.sort) this.sort.set(saved.sort);
|
|
1513
|
+
}
|
|
1514
|
+
} catch {}
|
|
1515
|
+
}
|
|
1516
|
+
rowLabelOf(entry) {
|
|
1517
|
+
const first = this.visibleColumns()[0];
|
|
1518
|
+
return first ? this.cellFor(first, entry.row).formatted : entry.id;
|
|
1519
|
+
}
|
|
1520
|
+
cellClassOf(column, ctx) {
|
|
1521
|
+
return (typeof column.cellClass === "function" ? column.cellClass(ctx) : column.cellClass) ?? "";
|
|
1522
|
+
}
|
|
1523
|
+
peopleIn(value) {
|
|
1524
|
+
return peopleOf(value);
|
|
1525
|
+
}
|
|
1526
|
+
shownPeople(column, value) {
|
|
1527
|
+
const people = peopleOf(value);
|
|
1528
|
+
return column.maxPeople !== void 0 ? people.slice(0, Math.max(0, column.maxPeople)) : people;
|
|
1529
|
+
}
|
|
1530
|
+
nameOf(person) {
|
|
1531
|
+
return personName(person);
|
|
1532
|
+
}
|
|
1533
|
+
filesIn(value) {
|
|
1534
|
+
return filesOf(value);
|
|
1535
|
+
}
|
|
1536
|
+
linkIn(value) {
|
|
1537
|
+
return linkParts(value);
|
|
1538
|
+
}
|
|
1539
|
+
hrefOf(href) {
|
|
1540
|
+
return href === void 0 ? null : safeHref(href);
|
|
1541
|
+
}
|
|
1542
|
+
stringOf(value) {
|
|
1543
|
+
return String(value);
|
|
1544
|
+
}
|
|
1545
|
+
ungroupedOptions(column) {
|
|
1546
|
+
return (column.options ?? []).filter((o) => !o.group);
|
|
1547
|
+
}
|
|
1548
|
+
groupsOf(column) {
|
|
1549
|
+
return [...new Set((column.options ?? []).filter((o) => o.group).map((o) => o.group))];
|
|
1550
|
+
}
|
|
1551
|
+
optionsInGroup(column, group) {
|
|
1552
|
+
return (column.options ?? []).filter((o) => o.group === group);
|
|
1553
|
+
}
|
|
1554
|
+
static {
|
|
1555
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
1556
|
+
minVersion: "12.0.0",
|
|
1557
|
+
version: "22.2.3",
|
|
1558
|
+
ngImport: i0,
|
|
1559
|
+
type: DataGrid,
|
|
1560
|
+
deps: [],
|
|
1561
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
static {
|
|
1565
|
+
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1566
|
+
minVersion: "17.0.0",
|
|
1567
|
+
version: "22.2.3",
|
|
1568
|
+
type: DataGrid,
|
|
1569
|
+
isStandalone: true,
|
|
1570
|
+
selector: "lily-data-grid",
|
|
1571
|
+
inputs: {
|
|
1572
|
+
label: {
|
|
1573
|
+
classPropertyName: "label",
|
|
1574
|
+
publicName: "label",
|
|
1575
|
+
isSignal: true,
|
|
1576
|
+
isRequired: true,
|
|
1577
|
+
transformFunction: null
|
|
1578
|
+
},
|
|
1579
|
+
caption: {
|
|
1580
|
+
classPropertyName: "caption",
|
|
1581
|
+
publicName: "caption",
|
|
1582
|
+
isSignal: true,
|
|
1583
|
+
isRequired: false,
|
|
1584
|
+
transformFunction: null
|
|
1585
|
+
},
|
|
1586
|
+
columns: {
|
|
1587
|
+
classPropertyName: "columns",
|
|
1588
|
+
publicName: "columns",
|
|
1589
|
+
isSignal: true,
|
|
1590
|
+
isRequired: true,
|
|
1591
|
+
transformFunction: null
|
|
1592
|
+
},
|
|
1593
|
+
rows: {
|
|
1594
|
+
classPropertyName: "rows",
|
|
1595
|
+
publicName: "rows",
|
|
1596
|
+
isSignal: true,
|
|
1597
|
+
isRequired: true,
|
|
1598
|
+
transformFunction: null
|
|
1599
|
+
},
|
|
1600
|
+
locale: {
|
|
1601
|
+
classPropertyName: "locale",
|
|
1602
|
+
publicName: "locale",
|
|
1603
|
+
isSignal: true,
|
|
1604
|
+
isRequired: false,
|
|
1605
|
+
transformFunction: null
|
|
1606
|
+
},
|
|
1607
|
+
rowId: {
|
|
1608
|
+
classPropertyName: "rowId",
|
|
1609
|
+
publicName: "rowId",
|
|
1610
|
+
isSignal: true,
|
|
1611
|
+
isRequired: false,
|
|
1612
|
+
transformFunction: null
|
|
1613
|
+
},
|
|
1614
|
+
selectionMode: {
|
|
1615
|
+
classPropertyName: "selectionMode",
|
|
1616
|
+
publicName: "selectionMode",
|
|
1617
|
+
isSignal: true,
|
|
1618
|
+
isRequired: false,
|
|
1619
|
+
transformFunction: null
|
|
1620
|
+
},
|
|
1621
|
+
pageSize: {
|
|
1622
|
+
classPropertyName: "pageSize",
|
|
1623
|
+
publicName: "pageSize",
|
|
1624
|
+
isSignal: true,
|
|
1625
|
+
isRequired: false,
|
|
1626
|
+
transformFunction: null
|
|
1627
|
+
},
|
|
1628
|
+
storageKey: {
|
|
1629
|
+
classPropertyName: "storageKey",
|
|
1630
|
+
publicName: "storageKey",
|
|
1631
|
+
isSignal: true,
|
|
1632
|
+
isRequired: false,
|
|
1633
|
+
transformFunction: null
|
|
1634
|
+
},
|
|
1635
|
+
labels: {
|
|
1636
|
+
classPropertyName: "labels",
|
|
1637
|
+
publicName: "labels",
|
|
1638
|
+
isSignal: true,
|
|
1639
|
+
isRequired: false,
|
|
1640
|
+
transformFunction: null
|
|
1641
|
+
},
|
|
1642
|
+
mode: {
|
|
1643
|
+
classPropertyName: "mode",
|
|
1644
|
+
publicName: "mode",
|
|
1645
|
+
isSignal: true,
|
|
1646
|
+
isRequired: false,
|
|
1647
|
+
transformFunction: null
|
|
1648
|
+
},
|
|
1649
|
+
rowClass: {
|
|
1650
|
+
classPropertyName: "rowClass",
|
|
1651
|
+
publicName: "rowClass",
|
|
1652
|
+
isSignal: true,
|
|
1653
|
+
isRequired: false,
|
|
1654
|
+
transformFunction: null
|
|
1655
|
+
},
|
|
1656
|
+
rowAttrs: {
|
|
1657
|
+
classPropertyName: "rowAttrs",
|
|
1658
|
+
publicName: "rowAttrs",
|
|
1659
|
+
isSignal: true,
|
|
1660
|
+
isRequired: false,
|
|
1661
|
+
transformFunction: null
|
|
1662
|
+
},
|
|
1663
|
+
activateRows: {
|
|
1664
|
+
classPropertyName: "activateRows",
|
|
1665
|
+
publicName: "activateRows",
|
|
1666
|
+
isSignal: true,
|
|
1667
|
+
isRequired: false,
|
|
1668
|
+
transformFunction: null
|
|
1669
|
+
},
|
|
1670
|
+
empty: {
|
|
1671
|
+
classPropertyName: "empty",
|
|
1672
|
+
publicName: "empty",
|
|
1673
|
+
isSignal: true,
|
|
1674
|
+
isRequired: false,
|
|
1675
|
+
transformFunction: null
|
|
1676
|
+
},
|
|
1677
|
+
footer: {
|
|
1678
|
+
classPropertyName: "footer",
|
|
1679
|
+
publicName: "footer",
|
|
1680
|
+
isSignal: true,
|
|
1681
|
+
isRequired: false,
|
|
1682
|
+
transformFunction: null
|
|
1683
|
+
},
|
|
1684
|
+
busy: {
|
|
1685
|
+
classPropertyName: "busy",
|
|
1686
|
+
publicName: "busy",
|
|
1687
|
+
isSignal: true,
|
|
1688
|
+
isRequired: false,
|
|
1689
|
+
transformFunction: null
|
|
1690
|
+
},
|
|
1691
|
+
scroll: {
|
|
1692
|
+
classPropertyName: "scroll",
|
|
1693
|
+
publicName: "scroll",
|
|
1694
|
+
isSignal: true,
|
|
1695
|
+
isRequired: false,
|
|
1696
|
+
transformFunction: null
|
|
1697
|
+
},
|
|
1698
|
+
exportFilename: {
|
|
1699
|
+
classPropertyName: "exportFilename",
|
|
1700
|
+
publicName: "exportFilename",
|
|
1701
|
+
isSignal: true,
|
|
1702
|
+
isRequired: false,
|
|
1703
|
+
transformFunction: null
|
|
1704
|
+
},
|
|
1705
|
+
exportScope: {
|
|
1706
|
+
classPropertyName: "exportScope",
|
|
1707
|
+
publicName: "exportScope",
|
|
1708
|
+
isSignal: true,
|
|
1709
|
+
isRequired: false,
|
|
1710
|
+
transformFunction: null
|
|
1711
|
+
},
|
|
1712
|
+
exportFormulas: {
|
|
1713
|
+
classPropertyName: "exportFormulas",
|
|
1714
|
+
publicName: "exportFormulas",
|
|
1715
|
+
isSignal: true,
|
|
1716
|
+
isRequired: false,
|
|
1717
|
+
transformFunction: null
|
|
1718
|
+
},
|
|
1719
|
+
exportHandler: {
|
|
1720
|
+
classPropertyName: "exportHandler",
|
|
1721
|
+
publicName: "exportHandler",
|
|
1722
|
+
isSignal: true,
|
|
1723
|
+
isRequired: false,
|
|
1724
|
+
transformFunction: null
|
|
1725
|
+
},
|
|
1726
|
+
virtualize: {
|
|
1727
|
+
classPropertyName: "virtualize",
|
|
1728
|
+
publicName: "virtualize",
|
|
1729
|
+
isSignal: true,
|
|
1730
|
+
isRequired: false,
|
|
1731
|
+
transformFunction: null
|
|
1732
|
+
},
|
|
1733
|
+
className: {
|
|
1734
|
+
classPropertyName: "className",
|
|
1735
|
+
publicName: "className",
|
|
1736
|
+
isSignal: true,
|
|
1737
|
+
isRequired: false,
|
|
1738
|
+
transformFunction: null
|
|
1739
|
+
},
|
|
1740
|
+
selected: {
|
|
1741
|
+
classPropertyName: "selected",
|
|
1742
|
+
publicName: "selected",
|
|
1743
|
+
isSignal: true,
|
|
1744
|
+
isRequired: false,
|
|
1745
|
+
transformFunction: null
|
|
1746
|
+
},
|
|
1747
|
+
filter: {
|
|
1748
|
+
classPropertyName: "filter",
|
|
1749
|
+
publicName: "filter",
|
|
1750
|
+
isSignal: true,
|
|
1751
|
+
isRequired: false,
|
|
1752
|
+
transformFunction: null
|
|
1753
|
+
},
|
|
1754
|
+
sort: {
|
|
1755
|
+
classPropertyName: "sort",
|
|
1756
|
+
publicName: "sort",
|
|
1757
|
+
isSignal: true,
|
|
1758
|
+
isRequired: false,
|
|
1759
|
+
transformFunction: null
|
|
1760
|
+
},
|
|
1761
|
+
columnOrder: {
|
|
1762
|
+
classPropertyName: "columnOrder",
|
|
1763
|
+
publicName: "columnOrder",
|
|
1764
|
+
isSignal: true,
|
|
1765
|
+
isRequired: false,
|
|
1766
|
+
transformFunction: null
|
|
1767
|
+
},
|
|
1768
|
+
page: {
|
|
1769
|
+
classPropertyName: "page",
|
|
1770
|
+
publicName: "page",
|
|
1771
|
+
isSignal: true,
|
|
1772
|
+
isRequired: false,
|
|
1773
|
+
transformFunction: null
|
|
1774
|
+
}
|
|
1775
|
+
},
|
|
1776
|
+
outputs: {
|
|
1777
|
+
selected: "selectedChange",
|
|
1778
|
+
filter: "filterChange",
|
|
1779
|
+
sort: "sortChange",
|
|
1780
|
+
columnOrder: "columnOrderChange",
|
|
1781
|
+
page: "pageChange",
|
|
1782
|
+
cellEdit: "cellEdit",
|
|
1783
|
+
filesSelected: "filesSelected",
|
|
1784
|
+
fileRemove: "fileRemove",
|
|
1785
|
+
rowActivate: "rowActivate",
|
|
1786
|
+
exportFile: "exportFile"
|
|
1787
|
+
},
|
|
1788
|
+
viewQueries: [{
|
|
1789
|
+
propertyName: "rootRef",
|
|
1790
|
+
first: true,
|
|
1791
|
+
predicate: ["rootEl"],
|
|
1792
|
+
descendants: true,
|
|
1793
|
+
isSignal: true
|
|
1794
|
+
}, {
|
|
1795
|
+
propertyName: "scrollRef",
|
|
1796
|
+
first: true,
|
|
1797
|
+
predicate: ["scrollEl"],
|
|
1798
|
+
descendants: true,
|
|
1799
|
+
isSignal: true
|
|
1800
|
+
}],
|
|
1801
|
+
ngImport: i0,
|
|
1802
|
+
template: `
|
|
1803
|
+
<div #rootEl class="data-grid {{ className() }}">
|
|
1804
|
+
@if (labels().search || hidableColumns().length > 0 || hasExportButtons()) {
|
|
1805
|
+
<div class="data-grid-toolbar">
|
|
1806
|
+
@if (labels().search; as searchLabel) {
|
|
1807
|
+
<input class="data-grid-search" type="search" [attr.aria-label]="searchLabel" [value]="filter()" (input)="onFilterInput($event)" />
|
|
1808
|
+
}
|
|
1809
|
+
@if (hasExportButtons()) {
|
|
1810
|
+
<div class="data-grid-export" role="group" [attr.aria-label]="labels().export ?? null">
|
|
1811
|
+
@if (labels().exportCsv; as csv) {
|
|
1812
|
+
<button type="button" class="data-grid-export-button" data-grid-export-format="csv" [disabled]="exportEntries().length === 0" (click)="runExport('csv')">{{ csv }}</button>
|
|
1813
|
+
}
|
|
1814
|
+
@if (labels().exportTsv; as tsv) {
|
|
1815
|
+
<button type="button" class="data-grid-export-button" data-grid-export-format="tsv" [disabled]="exportEntries().length === 0" (click)="runExport('tsv')">{{ tsv }}</button>
|
|
1816
|
+
}
|
|
1817
|
+
@if (labels().exportExcel; as excel) {
|
|
1818
|
+
<button type="button" class="data-grid-export-button" data-grid-export-format="xlsx" [disabled]="exportEntries().length === 0" (click)="runExport('xlsx')">{{ excel }}</button>
|
|
1819
|
+
}
|
|
1820
|
+
</div>
|
|
1821
|
+
}
|
|
1822
|
+
@if (hidableColumns().length > 0 && labels().columnVisibility; as visibilityLabel) {
|
|
1823
|
+
<fieldset class="data-grid-column-visibility">
|
|
1824
|
+
<legend>{{ visibilityLabel }}</legend>
|
|
1825
|
+
@for (column of hidableColumns(); track column.id) {
|
|
1826
|
+
<label>
|
|
1827
|
+
<input type="checkbox" [checked]="!hiddenColumnIds().has(column.id)" (change)="toggleColumnVisibility(column.id)" />
|
|
1828
|
+
{{ labels().columnVisibilityOption?.(column.header) ?? column.header }}
|
|
1829
|
+
</label>
|
|
1830
|
+
}
|
|
1831
|
+
</fieldset>
|
|
1832
|
+
}
|
|
1833
|
+
</div>
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
@if (scroll()) {
|
|
1837
|
+
<!-- A scrollable region must be reachable by keyboard (WCAG 2.1.1); in grid mode a roving cell is already a tab stop. -->
|
|
1838
|
+
<div
|
|
1839
|
+
#scrollEl
|
|
1840
|
+
class="data-grid-scroll"
|
|
1841
|
+
role="region"
|
|
1842
|
+
[attr.aria-label]="label()"
|
|
1843
|
+
[attr.tabindex]="isGrid() ? null : 0"
|
|
1844
|
+
[attr.style]="scrollStyle()"
|
|
1845
|
+
(scroll)="onScroll($event)"
|
|
1846
|
+
>
|
|
1847
|
+
<ng-container *ngTemplateOutlet="tableTemplate" />
|
|
1848
|
+
</div>
|
|
1849
|
+
} @else {
|
|
1850
|
+
<ng-container *ngTemplateOutlet="tableTemplate" />
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
@if (pageSize()) {
|
|
1854
|
+
<div class="data-grid-pagination">
|
|
1855
|
+
<button type="button" class="data-grid-page-previous" [disabled]="clampedPage() <= 1" (click)="goToPage(clampedPage() - 1)">{{ labels().previousPage }}</button>
|
|
1856
|
+
<span class="data-grid-page-status">{{ labels().pageStatus?.(clampedPage(), pageCount(), sortedEntries().length) }}</span>
|
|
1857
|
+
<button type="button" class="data-grid-page-next" [disabled]="clampedPage() >= pageCount()" (click)="goToPage(clampedPage() + 1)">{{ labels().nextPage }}</button>
|
|
1858
|
+
</div>
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
<p class="data-grid-status" aria-live="polite">{{ statusMessage() }}</p>
|
|
1862
|
+
</div>
|
|
1863
|
+
|
|
1864
|
+
<ng-template #tableTemplate>
|
|
1865
|
+
<lily-data-table
|
|
1866
|
+
[label]="label()"
|
|
1867
|
+
[className]="''"
|
|
1868
|
+
(keydown)="onGridKeydown($event)"
|
|
1869
|
+
(focusin)="onGridFocusin($event)"
|
|
1870
|
+
(focusout)="onGridFocusout($event)"
|
|
1871
|
+
(click)="onGridClick($event)"
|
|
1872
|
+
(dblclick)="onGridDblclick($event)"
|
|
1873
|
+
>
|
|
1874
|
+
@if (caption()) {
|
|
1875
|
+
<caption>{{ caption() }}</caption>
|
|
1876
|
+
}
|
|
1877
|
+
<thead lily-data-table-head>
|
|
1878
|
+
<tr lily-data-table-row [attr.aria-rowindex]="indexed() ? 1 : null">
|
|
1879
|
+
@if (hasSelection()) {
|
|
1880
|
+
<th
|
|
1881
|
+
lily-data-table-th
|
|
1882
|
+
data-row="-1"
|
|
1883
|
+
data-col="0"
|
|
1884
|
+
[attr.tabindex]="isGrid() ? (activeRow() === -1 && activeCol() === 0 ? 0 : -1) : null"
|
|
1885
|
+
>
|
|
1886
|
+
@if (selectionMode() === 'multiple') {
|
|
1887
|
+
<input
|
|
1888
|
+
type="checkbox"
|
|
1889
|
+
data-grid-own
|
|
1890
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
1891
|
+
[attr.aria-label]="labels().selectAll ?? null"
|
|
1892
|
+
[checked]="allSelected()"
|
|
1893
|
+
[indeterminate]="someSelected()"
|
|
1894
|
+
(click)="toggleSelectAll()"
|
|
1895
|
+
/>
|
|
1896
|
+
} @else {
|
|
1897
|
+
<span class="data-grid-selection-header-label">{{ labels().selectionColumn }}</span>
|
|
1898
|
+
}
|
|
1899
|
+
</th>
|
|
1900
|
+
}
|
|
1901
|
+
@for (column of visibleColumns(); track column.id; let i = $index) {
|
|
1902
|
+
<th
|
|
1903
|
+
lily-data-table-th
|
|
1904
|
+
data-row="-1"
|
|
1905
|
+
[attr.data-col]="colOffset() + i"
|
|
1906
|
+
[attr.data-align]="column.align ?? null"
|
|
1907
|
+
[attr.data-cell-type]="column.type ?? null"
|
|
1908
|
+
[attr.data-pinned]="column.pinned ?? null"
|
|
1909
|
+
[attr.tabindex]="isGrid() ? (activeRow() === -1 && activeCol() === colOffset() + i ? 0 : -1) : null"
|
|
1910
|
+
[attr.aria-sort]="ariaSortFor(column)"
|
|
1911
|
+
[attr.aria-describedby]="column.type === 'rollup' && labels().rollupDescription ? gridId + '-' + column.id + '-description' : null"
|
|
1912
|
+
[attr.style]="cellStyle(column)"
|
|
1913
|
+
[attr.draggable]="column.movable ? 'true' : null"
|
|
1914
|
+
(dragstart)="onHeaderDragStart(column, $event)"
|
|
1915
|
+
(dragover)="onHeaderDragOver($event)"
|
|
1916
|
+
(drop)="onHeaderDrop(column.id, $event)"
|
|
1917
|
+
>
|
|
1918
|
+
@if (column.sortable) {
|
|
1919
|
+
<button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column)">
|
|
1920
|
+
@if (column.headerCell; as headerTemplate) {
|
|
1921
|
+
<ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: column }" />
|
|
1922
|
+
} @else {<ng-container>{{ column.header }}</ng-container>}
|
|
1923
|
+
</button>
|
|
1924
|
+
} @else if (column.headerCell; as headerTemplate) {
|
|
1925
|
+
<ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: column }" />
|
|
1926
|
+
} @else {<ng-container>{{ column.header }}</ng-container>}
|
|
1927
|
+
@if (column.type === 'rollup' && labels().rollupDescription; as describe) {
|
|
1928
|
+
<span class="data-grid-visually-hidden" [id]="gridId + '-' + column.id + '-description'">{{ describe(column.header) }}</span>
|
|
1929
|
+
}
|
|
1930
|
+
@if (column.resizable) {
|
|
1931
|
+
<!-- WAI-ARIA "window splitter" pattern: role="separator" plus tabindex/aria-valuenow is how a resize handle becomes focusable and operable. -->
|
|
1932
|
+
<span
|
|
1933
|
+
class="data-grid-resize-handle"
|
|
1934
|
+
role="separator"
|
|
1935
|
+
data-grid-own
|
|
1936
|
+
aria-orientation="vertical"
|
|
1937
|
+
[attr.aria-label]="labels().resizeHandle?.(column.header) ?? null"
|
|
1938
|
+
[attr.aria-valuenow]="widthFor(column) ?? 120"
|
|
1939
|
+
[attr.aria-valuemin]="minColumnWidth"
|
|
1940
|
+
tabindex="0"
|
|
1941
|
+
(keydown)="onResizeKeydown(column, $event)"
|
|
1942
|
+
(pointerdown)="onResizePointerDown(column, $event)"
|
|
1943
|
+
></span>
|
|
1944
|
+
}
|
|
1945
|
+
</th>
|
|
1946
|
+
}
|
|
1947
|
+
</tr>
|
|
1948
|
+
</thead>
|
|
1949
|
+
<tbody lily-data-table-body>
|
|
1950
|
+
@for (item of renderItems(); track item.kind === 'row' ? pageEntries()[item.index].id : item.key) {
|
|
1951
|
+
@if (item.kind === 'spacer') {
|
|
1952
|
+
<tr class="data-grid-spacer" aria-hidden="true" [attr.style]="'height:' + item.rows * (virtualize()?.rowHeight ?? 0) + 'px'">
|
|
1953
|
+
<td [attr.colspan]="lastCol() + 1"></td>
|
|
1954
|
+
</tr>
|
|
1955
|
+
} @else {
|
|
1956
|
+
@let entry = pageEntries()[item.index];
|
|
1957
|
+
<tr
|
|
1958
|
+
lily-data-table-row
|
|
1959
|
+
[className]="rowClass()?.(entry.row) ?? ''"
|
|
1960
|
+
[attr.aria-selected]="hasSelection() ? isSelected(entry.id) : null"
|
|
1961
|
+
[attr.aria-rowindex]="indexed() ? pageStart() + item.index + 2 : null"
|
|
1962
|
+
[lilyDataGridAttrs]="rowAttrs()?.(entry.row)"
|
|
1963
|
+
>
|
|
1964
|
+
@if (hasSelection()) {
|
|
1965
|
+
<td
|
|
1966
|
+
lily-data-table-td
|
|
1967
|
+
[attr.role]="isGrid() ? 'gridcell' : 'cell'"
|
|
1968
|
+
[attr.data-row]="item.index"
|
|
1969
|
+
data-col="0"
|
|
1970
|
+
[attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === 0 ? 0 : -1) : null"
|
|
1971
|
+
>
|
|
1972
|
+
<input
|
|
1973
|
+
[type]="selectionMode() === 'single' ? 'radio' : 'checkbox'"
|
|
1974
|
+
data-grid-own
|
|
1975
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
1976
|
+
[attr.aria-label]="labels().selectRow?.(rowLabelOf(entry)) ?? null"
|
|
1977
|
+
[checked]="isSelected(entry.id)"
|
|
1978
|
+
(click)="onRowSelectClick(pageStart() + item.index, $event)"
|
|
1979
|
+
/>
|
|
1980
|
+
</td>
|
|
1981
|
+
}
|
|
1982
|
+
@for (column of visibleColumns(); track column.id; let colIndex = $index) {
|
|
1983
|
+
@let ctx = cellFor(column, entry.row);
|
|
1984
|
+
@if (column.rowHeader) {
|
|
1985
|
+
<th
|
|
1986
|
+
lily-data-table-th
|
|
1987
|
+
scope="row"
|
|
1988
|
+
role="rowheader"
|
|
1989
|
+
[className]="cellClassOf(column, ctx)"
|
|
1990
|
+
[attr.data-row]="item.index"
|
|
1991
|
+
[attr.data-col]="colOffset() + colIndex"
|
|
1992
|
+
[attr.data-align]="column.align ?? null"
|
|
1993
|
+
[attr.data-cell-type]="column.type ?? null"
|
|
1994
|
+
[attr.data-pinned]="column.pinned ?? null"
|
|
1995
|
+
[attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
|
|
1996
|
+
[attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
|
|
1997
|
+
[attr.style]="cellStyle(column)"
|
|
1998
|
+
[lilyDataGridAttrs]="column.cellAttrs?.(ctx)"
|
|
1999
|
+
>
|
|
2000
|
+
<ng-container *ngTemplateOutlet="cellBody; context: { entry: entry, column: column, ctx: ctx }" />
|
|
2001
|
+
</th>
|
|
2002
|
+
} @else {
|
|
2003
|
+
<td
|
|
2004
|
+
lily-data-table-td
|
|
2005
|
+
[className]="cellClassOf(column, ctx)"
|
|
2006
|
+
[attr.role]="isGrid() ? 'gridcell' : 'cell'"
|
|
2007
|
+
[attr.data-row]="item.index"
|
|
2008
|
+
[attr.data-col]="colOffset() + colIndex"
|
|
2009
|
+
[attr.data-align]="column.align ?? null"
|
|
2010
|
+
[attr.data-cell-type]="column.type ?? null"
|
|
2011
|
+
[attr.data-pinned]="column.pinned ?? null"
|
|
2012
|
+
[attr.data-editable]="isEditable(column) ? '' : null"
|
|
2013
|
+
[attr.data-readonly]="column.type === 'rollup' ? '' : null"
|
|
2014
|
+
[attr.aria-readonly]="column.type === 'rollup' ? 'true' : null"
|
|
2015
|
+
[attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
|
|
2016
|
+
[attr.data-editing]="isEditingCell(entry, column) ? '' : null"
|
|
2017
|
+
[attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
|
|
2018
|
+
[attr.style]="cellStyle(column)"
|
|
2019
|
+
[lilyDataGridAttrs]="column.cellAttrs?.(ctx)"
|
|
2020
|
+
(dragover)="onFileDragOver(column, $event)"
|
|
2021
|
+
(drop)="onFileDrop(entry, column, $event)"
|
|
2022
|
+
>
|
|
2023
|
+
<ng-container *ngTemplateOutlet="cellBody; context: { entry: entry, column: column, ctx: ctx }" />
|
|
2024
|
+
</td>
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
</tr>
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
@if (pageEntries().length === 0 && empty(); as emptyTemplate) {
|
|
2031
|
+
<tr lily-data-table-row className="data-grid-empty-row">
|
|
2032
|
+
<td lily-data-table-td className="data-grid-empty" [attr.colspan]="lastCol() + 1" [attr.role]="isGrid() ? 'gridcell' : 'cell'">
|
|
2033
|
+
<ng-container *ngTemplateOutlet="emptyTemplate" />
|
|
2034
|
+
</td>
|
|
2035
|
+
</tr>
|
|
2036
|
+
}
|
|
2037
|
+
</tbody>
|
|
2038
|
+
@if (footer(); as footerTemplate) {
|
|
2039
|
+
<tfoot lily-data-table-foot>
|
|
2040
|
+
<ng-container *ngTemplateOutlet="footerTemplate; context: { $implicit: footerContext() }" />
|
|
2041
|
+
</tfoot>
|
|
2042
|
+
}
|
|
2043
|
+
</lily-data-table>
|
|
2044
|
+
</ng-template>
|
|
2045
|
+
|
|
2046
|
+
<ng-template #cellBody let-entry="entry" let-column="column" let-ctx="ctx">
|
|
2047
|
+
@if (isEditingCell(entry, column)) {
|
|
2048
|
+
@let editing = editingState()!;
|
|
2049
|
+
@if (column.editor === 'person' && column.personEditor) {
|
|
2050
|
+
<ng-container *ngTemplateOutlet="column.personEditor; context: { $implicit: personEditorContext(entry, column, ctx) }" />
|
|
2051
|
+
} @else if (column.editor === 'time-zone') {
|
|
2052
|
+
<span
|
|
2053
|
+
class="data-grid-zoned-editor"
|
|
2054
|
+
role="group"
|
|
2055
|
+
[attr.aria-label]="labels().cellEditor?.(column.header) ?? column.header"
|
|
2056
|
+
(focusout)="onZonedFocusout($event)"
|
|
2057
|
+
>
|
|
2058
|
+
<input
|
|
2059
|
+
class="data-grid-cell-editor"
|
|
2060
|
+
type="time"
|
|
2061
|
+
step="1"
|
|
2062
|
+
[attr.aria-label]="labels().cellEditor?.(column.header) ?? column.header"
|
|
2063
|
+
[attr.aria-invalid]="editing.error || editing.invalid ? 'true' : null"
|
|
2064
|
+
[attr.aria-describedby]="editing.error ? entry.id + '-' + column.id + '-error' : null"
|
|
2065
|
+
[value]="editing.draft"
|
|
2066
|
+
(input)="setDraft($event)"
|
|
2067
|
+
(keydown)="onEditorKeydown($event)"
|
|
2068
|
+
/>
|
|
2069
|
+
<select
|
|
2070
|
+
class="data-grid-cell-editor data-grid-zone-editor"
|
|
2071
|
+
[attr.aria-label]="labels().timeZone?.(column.header) ?? column.header"
|
|
2072
|
+
(change)="setZone($event)"
|
|
2073
|
+
(keydown)="onEditorKeydown($event)"
|
|
2074
|
+
>
|
|
2075
|
+
@for (option of column.timeZones ?? []; track option.id) {
|
|
2076
|
+
<option [value]="option.id" [selected]="option.id === editing.zone">{{ option.label }}</option>
|
|
2077
|
+
}
|
|
2078
|
+
</select>
|
|
2079
|
+
</span>
|
|
2080
|
+
} @else if (column.editor === 'select') {
|
|
2081
|
+
<select
|
|
2082
|
+
class="data-grid-cell-editor"
|
|
2083
|
+
[attr.aria-label]="labels().cellEditor?.(column.header) ?? column.header"
|
|
2084
|
+
[attr.aria-invalid]="editing.error || editing.invalid ? 'true' : null"
|
|
2085
|
+
[attr.aria-describedby]="editing.error ? entry.id + '-' + column.id + '-error' : null"
|
|
2086
|
+
(change)="setDraft($event)"
|
|
2087
|
+
(keydown)="onEditorKeydown($event)"
|
|
2088
|
+
(blur)="onEditorBlur()"
|
|
2089
|
+
>
|
|
2090
|
+
<option value="" [selected]="editing.draft === ''"></option>
|
|
2091
|
+
@for (option of ungroupedOptions(column); track option.id) {
|
|
2092
|
+
<option [value]="option.id" [disabled]="option.disabled ?? false" [selected]="option.id === editing.draft">{{ option.label }}</option>
|
|
2093
|
+
}
|
|
2094
|
+
@for (group of groupsOf(column); track group) {
|
|
2095
|
+
<optgroup [label]="group">
|
|
2096
|
+
@for (option of optionsInGroup(column, group); track option.id) {
|
|
2097
|
+
<option [value]="option.id" [disabled]="option.disabled ?? false" [selected]="option.id === editing.draft">{{ option.label }}</option>
|
|
2098
|
+
}
|
|
2099
|
+
</optgroup>
|
|
2100
|
+
}
|
|
2101
|
+
</select>
|
|
2102
|
+
} @else {
|
|
2103
|
+
<input
|
|
2104
|
+
class="data-grid-cell-editor"
|
|
2105
|
+
[type]="column.editor === 'number' ? 'number' : column.editor === 'date' ? 'date' : column.editor === 'time' ? 'time' : 'text'"
|
|
2106
|
+
[attr.step]="column.editor === 'time' ? 1 : null"
|
|
2107
|
+
[attr.inputmode]="column.editor === 'number' ? 'decimal' : null"
|
|
2108
|
+
[attr.aria-label]="(column.type === 'formula' ? labels().formulaEditor?.(column.header) : undefined) ?? labels().cellEditor?.(column.header) ?? column.header"
|
|
2109
|
+
[attr.aria-invalid]="editing.error || editing.invalid ? 'true' : null"
|
|
2110
|
+
[attr.aria-describedby]="editing.error ? entry.id + '-' + column.id + '-error' : null"
|
|
2111
|
+
[value]="editing.draft"
|
|
2112
|
+
(input)="setDraft($event)"
|
|
2113
|
+
(keydown)="onEditorKeydown($event)"
|
|
2114
|
+
(blur)="onEditorBlur()"
|
|
2115
|
+
/>
|
|
2116
|
+
}
|
|
2117
|
+
@if (editing.error) {
|
|
2118
|
+
<span class="data-grid-cell-error" [id]="entry.id + '-' + column.id + '-error'">{{ editing.error }}</span>
|
|
2119
|
+
}
|
|
2120
|
+
} @else if (column.type === 'radio' && column.editable) {
|
|
2121
|
+
<fieldset class="data-grid-radio-group" data-grid-own-group>
|
|
2122
|
+
<legend class="data-grid-radio-legend">{{ labels().radioGroup?.(column.header, rowLabelOf(entry)) ?? column.header }}</legend>
|
|
2123
|
+
@for (option of column.options ?? []; track option.id) {
|
|
2124
|
+
<label class="data-grid-radio-option">
|
|
2125
|
+
<input
|
|
2126
|
+
type="radio"
|
|
2127
|
+
[name]="gridId + '-' + entry.id + '-' + column.id"
|
|
2128
|
+
[value]="option.id"
|
|
2129
|
+
[checked]="ctx.value === option.id"
|
|
2130
|
+
[disabled]="option.disabled ?? false"
|
|
2131
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2132
|
+
(change)="commitValue(entry, column, option.id)"
|
|
2133
|
+
/>{{ option.label }}</label>
|
|
2134
|
+
}
|
|
2135
|
+
</fieldset>
|
|
2136
|
+
} @else {
|
|
2137
|
+
@if (column.cell; as cellTemplate) {
|
|
2138
|
+
<ng-container *ngTemplateOutlet="cellTemplate; context: { $implicit: ctx }" />
|
|
2139
|
+
} @else if (column.type === 'person') {
|
|
2140
|
+
@let people = peopleIn(ctx.value);
|
|
2141
|
+
@if (people.length > 0) {
|
|
2142
|
+
@let shown = shownPeople(column, ctx.value);
|
|
2143
|
+
<ul class="data-grid-people">
|
|
2144
|
+
@for (person of shown; track person.id) {
|
|
2145
|
+
<li class="data-grid-person">
|
|
2146
|
+
@if (column.avatar; as avatarTemplate) {
|
|
2147
|
+
<span class="data-grid-avatar" aria-hidden="true"><ng-container *ngTemplateOutlet="avatarTemplate; context: { $implicit: person }" /></span>
|
|
2148
|
+
}
|
|
2149
|
+
<span class="data-grid-person-name">{{ nameOf(person) }}</span>
|
|
2150
|
+
</li>
|
|
2151
|
+
}
|
|
2152
|
+
@if (shown.length < people.length && labels().morePeople; as more) {
|
|
2153
|
+
<li class="data-grid-people-more">{{ more(people.length - shown.length) }}</li>
|
|
2154
|
+
}
|
|
2155
|
+
</ul>
|
|
2156
|
+
}
|
|
2157
|
+
} @else if (column.type === 'file') {
|
|
2158
|
+
@let files = filesIn(ctx.value);
|
|
2159
|
+
@if (files.length > 0) {
|
|
2160
|
+
<ul class="data-grid-files">
|
|
2161
|
+
@for (file of files; track file.id) {
|
|
2162
|
+
<li class="data-grid-file">
|
|
2163
|
+
@if (hrefOf(file.href); as fileHref) {
|
|
2164
|
+
<a class="data-grid-link" [href]="fileHref">{{ file.name }}</a>
|
|
2165
|
+
} @else {
|
|
2166
|
+
<span class="data-grid-file-name">{{ file.name }}</span>
|
|
2167
|
+
}
|
|
2168
|
+
@if (file.size !== undefined && column.formatSize; as sizeText) {
|
|
2169
|
+
{{ ' ' }}<span class="data-grid-file-size">{{ column.formatSize(file.size) }}</span>
|
|
2170
|
+
}
|
|
2171
|
+
@if (column.editable && labels().removeFile; as removeLabel) {
|
|
2172
|
+
<button
|
|
2173
|
+
type="button"
|
|
2174
|
+
class="data-grid-file-remove"
|
|
2175
|
+
data-grid-own
|
|
2176
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2177
|
+
[attr.aria-label]="removeLabel(file.name)"
|
|
2178
|
+
(click)="fileRemove.emit({ rowId: entry.id, row: entry.row, columnId: column.id, fileId: file.id })"
|
|
2179
|
+
></button>
|
|
2180
|
+
}
|
|
2181
|
+
</li>
|
|
2182
|
+
}
|
|
2183
|
+
</ul>
|
|
2184
|
+
}
|
|
2185
|
+
@if (column.editable) {
|
|
2186
|
+
<input
|
|
2187
|
+
type="file"
|
|
2188
|
+
class="data-grid-file-input"
|
|
2189
|
+
[attr.aria-label]="labels().fileInput?.(column.header, rowLabelOf(entry)) ?? column.header"
|
|
2190
|
+
[multiple]="column.multiple !== false"
|
|
2191
|
+
[attr.accept]="column.accept ?? null"
|
|
2192
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2193
|
+
(change)="onFileInput(entry, column, $event)"
|
|
2194
|
+
/>
|
|
2195
|
+
}
|
|
2196
|
+
} @else if (column.type === 'link') {
|
|
2197
|
+
@let link = linkIn(ctx.value);
|
|
2198
|
+
@if (link) {
|
|
2199
|
+
@if (hrefOf(link.href); as href) {
|
|
2200
|
+
<a class="data-grid-link" [href]="href" [attr.target]="link.external ? '_blank' : null" [attr.rel]="link.external ? 'noopener noreferrer' : null">{{ link.label ?? link.href }}@if (link.external && labels().opensInNewTab; as newTab) {{{ ' ' }}<span class="data-grid-visually-hidden">{{ newTab(link.label ?? link.href) }}</span>}</a>
|
|
2201
|
+
} @else {
|
|
2202
|
+
<!-- An address with an unsafe scheme is shown as text, never as a link. -->
|
|
2203
|
+
<span class="data-grid-link data-grid-link-unsafe">{{ link.label ?? link.href }}</span>
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
} @else if (column.type === 'date' || column.type === 'time') {
|
|
2207
|
+
@if (ctx.formatted !== '') {
|
|
2208
|
+
<time [attr.datetime]="stringOf(ctx.value)">{{ ctx.formatted }}</time>
|
|
2209
|
+
}
|
|
2210
|
+
} @else {<ng-container>{{ ctx.formatted }}</ng-container>}
|
|
2211
|
+
@if (isEditable(column) && column.type !== 'radio' && labels().editCell; as editLabel) {
|
|
2212
|
+
<button
|
|
2213
|
+
type="button"
|
|
2214
|
+
class="data-grid-edit-button"
|
|
2215
|
+
data-grid-own
|
|
2216
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2217
|
+
[attr.aria-label]="editLabel(column.header, rowLabelOf(entry))"
|
|
2218
|
+
(click)="startEdit(entry, column)"
|
|
2219
|
+
></button>
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
</ng-template>
|
|
2223
|
+
`,
|
|
2224
|
+
isInline: true,
|
|
2225
|
+
dependencies: [
|
|
2226
|
+
{
|
|
2227
|
+
kind: "directive",
|
|
2228
|
+
type: NgTemplateOutlet,
|
|
2229
|
+
selector: "[ngTemplateOutlet]",
|
|
2230
|
+
inputs: [
|
|
2231
|
+
"ngTemplateOutletContext",
|
|
2232
|
+
"ngTemplateOutlet",
|
|
2233
|
+
"ngTemplateOutletInjector"
|
|
2234
|
+
]
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
kind: "directive",
|
|
2238
|
+
type: DataGridAttrs,
|
|
2239
|
+
selector: "[lilyDataGridAttrs]",
|
|
2240
|
+
inputs: ["lilyDataGridAttrs"]
|
|
2241
|
+
},
|
|
2242
|
+
{
|
|
2243
|
+
kind: "component",
|
|
2244
|
+
type: DataTable,
|
|
2245
|
+
selector: "lily-data-table",
|
|
2246
|
+
inputs: ["label", "className"]
|
|
2247
|
+
},
|
|
2248
|
+
{
|
|
2249
|
+
kind: "component",
|
|
2250
|
+
type: DataTableBody,
|
|
2251
|
+
selector: "tbody[lily-data-table-body]",
|
|
2252
|
+
inputs: ["className"]
|
|
2253
|
+
},
|
|
2254
|
+
{
|
|
2255
|
+
kind: "component",
|
|
2256
|
+
type: DataTableFoot,
|
|
2257
|
+
selector: "tfoot[lily-data-table-foot]",
|
|
2258
|
+
inputs: ["className"]
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
kind: "component",
|
|
2262
|
+
type: DataTableHead,
|
|
2263
|
+
selector: "thead[lily-data-table-head]",
|
|
2264
|
+
inputs: ["className"]
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
kind: "component",
|
|
2268
|
+
type: DataTableRow,
|
|
2269
|
+
selector: "tr[lily-data-table-row]",
|
|
2270
|
+
inputs: ["className"]
|
|
2271
|
+
},
|
|
2272
|
+
{
|
|
2273
|
+
kind: "component",
|
|
2274
|
+
type: DataTableTD,
|
|
2275
|
+
selector: "td[lily-data-table-td]",
|
|
2276
|
+
inputs: ["className"]
|
|
2277
|
+
},
|
|
2278
|
+
{
|
|
2279
|
+
kind: "component",
|
|
2280
|
+
type: DataTableTH,
|
|
2281
|
+
selector: "th[lily-data-table-th]",
|
|
2282
|
+
inputs: ["scope", "className"]
|
|
2283
|
+
}
|
|
2284
|
+
],
|
|
2285
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2290
|
+
minVersion: "12.0.0",
|
|
2291
|
+
version: "22.2.3",
|
|
2292
|
+
ngImport: i0,
|
|
2293
|
+
type: DataGrid,
|
|
2294
|
+
decorators: [{
|
|
2295
|
+
type: Component,
|
|
2296
|
+
args: [{
|
|
2297
|
+
selector: "lily-data-grid",
|
|
2298
|
+
standalone: true,
|
|
2299
|
+
imports: [
|
|
2300
|
+
NgTemplateOutlet,
|
|
2301
|
+
DataGridAttrs,
|
|
2302
|
+
DataTable,
|
|
2303
|
+
DataTableBody,
|
|
2304
|
+
DataTableFoot,
|
|
2305
|
+
DataTableHead,
|
|
2306
|
+
DataTableRow,
|
|
2307
|
+
DataTableTD,
|
|
2308
|
+
DataTableTH
|
|
2309
|
+
],
|
|
2310
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2311
|
+
template: `
|
|
2312
|
+
<div #rootEl class="data-grid {{ className() }}">
|
|
2313
|
+
@if (labels().search || hidableColumns().length > 0 || hasExportButtons()) {
|
|
2314
|
+
<div class="data-grid-toolbar">
|
|
2315
|
+
@if (labels().search; as searchLabel) {
|
|
2316
|
+
<input class="data-grid-search" type="search" [attr.aria-label]="searchLabel" [value]="filter()" (input)="onFilterInput($event)" />
|
|
2317
|
+
}
|
|
2318
|
+
@if (hasExportButtons()) {
|
|
2319
|
+
<div class="data-grid-export" role="group" [attr.aria-label]="labels().export ?? null">
|
|
2320
|
+
@if (labels().exportCsv; as csv) {
|
|
2321
|
+
<button type="button" class="data-grid-export-button" data-grid-export-format="csv" [disabled]="exportEntries().length === 0" (click)="runExport('csv')">{{ csv }}</button>
|
|
2322
|
+
}
|
|
2323
|
+
@if (labels().exportTsv; as tsv) {
|
|
2324
|
+
<button type="button" class="data-grid-export-button" data-grid-export-format="tsv" [disabled]="exportEntries().length === 0" (click)="runExport('tsv')">{{ tsv }}</button>
|
|
2325
|
+
}
|
|
2326
|
+
@if (labels().exportExcel; as excel) {
|
|
2327
|
+
<button type="button" class="data-grid-export-button" data-grid-export-format="xlsx" [disabled]="exportEntries().length === 0" (click)="runExport('xlsx')">{{ excel }}</button>
|
|
2328
|
+
}
|
|
2329
|
+
</div>
|
|
2330
|
+
}
|
|
2331
|
+
@if (hidableColumns().length > 0 && labels().columnVisibility; as visibilityLabel) {
|
|
2332
|
+
<fieldset class="data-grid-column-visibility">
|
|
2333
|
+
<legend>{{ visibilityLabel }}</legend>
|
|
2334
|
+
@for (column of hidableColumns(); track column.id) {
|
|
2335
|
+
<label>
|
|
2336
|
+
<input type="checkbox" [checked]="!hiddenColumnIds().has(column.id)" (change)="toggleColumnVisibility(column.id)" />
|
|
2337
|
+
{{ labels().columnVisibilityOption?.(column.header) ?? column.header }}
|
|
2338
|
+
</label>
|
|
2339
|
+
}
|
|
2340
|
+
</fieldset>
|
|
2341
|
+
}
|
|
2342
|
+
</div>
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
@if (scroll()) {
|
|
2346
|
+
<!-- A scrollable region must be reachable by keyboard (WCAG 2.1.1); in grid mode a roving cell is already a tab stop. -->
|
|
2347
|
+
<div
|
|
2348
|
+
#scrollEl
|
|
2349
|
+
class="data-grid-scroll"
|
|
2350
|
+
role="region"
|
|
2351
|
+
[attr.aria-label]="label()"
|
|
2352
|
+
[attr.tabindex]="isGrid() ? null : 0"
|
|
2353
|
+
[attr.style]="scrollStyle()"
|
|
2354
|
+
(scroll)="onScroll($event)"
|
|
2355
|
+
>
|
|
2356
|
+
<ng-container *ngTemplateOutlet="tableTemplate" />
|
|
2357
|
+
</div>
|
|
2358
|
+
} @else {
|
|
2359
|
+
<ng-container *ngTemplateOutlet="tableTemplate" />
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
@if (pageSize()) {
|
|
2363
|
+
<div class="data-grid-pagination">
|
|
2364
|
+
<button type="button" class="data-grid-page-previous" [disabled]="clampedPage() <= 1" (click)="goToPage(clampedPage() - 1)">{{ labels().previousPage }}</button>
|
|
2365
|
+
<span class="data-grid-page-status">{{ labels().pageStatus?.(clampedPage(), pageCount(), sortedEntries().length) }}</span>
|
|
2366
|
+
<button type="button" class="data-grid-page-next" [disabled]="clampedPage() >= pageCount()" (click)="goToPage(clampedPage() + 1)">{{ labels().nextPage }}</button>
|
|
2367
|
+
</div>
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
<p class="data-grid-status" aria-live="polite">{{ statusMessage() }}</p>
|
|
2371
|
+
</div>
|
|
2372
|
+
|
|
2373
|
+
<ng-template #tableTemplate>
|
|
2374
|
+
<lily-data-table
|
|
2375
|
+
[label]="label()"
|
|
2376
|
+
[className]="''"
|
|
2377
|
+
(keydown)="onGridKeydown($event)"
|
|
2378
|
+
(focusin)="onGridFocusin($event)"
|
|
2379
|
+
(focusout)="onGridFocusout($event)"
|
|
2380
|
+
(click)="onGridClick($event)"
|
|
2381
|
+
(dblclick)="onGridDblclick($event)"
|
|
2382
|
+
>
|
|
2383
|
+
@if (caption()) {
|
|
2384
|
+
<caption>{{ caption() }}</caption>
|
|
2385
|
+
}
|
|
2386
|
+
<thead lily-data-table-head>
|
|
2387
|
+
<tr lily-data-table-row [attr.aria-rowindex]="indexed() ? 1 : null">
|
|
2388
|
+
@if (hasSelection()) {
|
|
2389
|
+
<th
|
|
2390
|
+
lily-data-table-th
|
|
2391
|
+
data-row="-1"
|
|
2392
|
+
data-col="0"
|
|
2393
|
+
[attr.tabindex]="isGrid() ? (activeRow() === -1 && activeCol() === 0 ? 0 : -1) : null"
|
|
2394
|
+
>
|
|
2395
|
+
@if (selectionMode() === 'multiple') {
|
|
2396
|
+
<input
|
|
2397
|
+
type="checkbox"
|
|
2398
|
+
data-grid-own
|
|
2399
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2400
|
+
[attr.aria-label]="labels().selectAll ?? null"
|
|
2401
|
+
[checked]="allSelected()"
|
|
2402
|
+
[indeterminate]="someSelected()"
|
|
2403
|
+
(click)="toggleSelectAll()"
|
|
2404
|
+
/>
|
|
2405
|
+
} @else {
|
|
2406
|
+
<span class="data-grid-selection-header-label">{{ labels().selectionColumn }}</span>
|
|
2407
|
+
}
|
|
2408
|
+
</th>
|
|
2409
|
+
}
|
|
2410
|
+
@for (column of visibleColumns(); track column.id; let i = $index) {
|
|
2411
|
+
<th
|
|
2412
|
+
lily-data-table-th
|
|
2413
|
+
data-row="-1"
|
|
2414
|
+
[attr.data-col]="colOffset() + i"
|
|
2415
|
+
[attr.data-align]="column.align ?? null"
|
|
2416
|
+
[attr.data-cell-type]="column.type ?? null"
|
|
2417
|
+
[attr.data-pinned]="column.pinned ?? null"
|
|
2418
|
+
[attr.tabindex]="isGrid() ? (activeRow() === -1 && activeCol() === colOffset() + i ? 0 : -1) : null"
|
|
2419
|
+
[attr.aria-sort]="ariaSortFor(column)"
|
|
2420
|
+
[attr.aria-describedby]="column.type === 'rollup' && labels().rollupDescription ? gridId + '-' + column.id + '-description' : null"
|
|
2421
|
+
[attr.style]="cellStyle(column)"
|
|
2422
|
+
[attr.draggable]="column.movable ? 'true' : null"
|
|
2423
|
+
(dragstart)="onHeaderDragStart(column, $event)"
|
|
2424
|
+
(dragover)="onHeaderDragOver($event)"
|
|
2425
|
+
(drop)="onHeaderDrop(column.id, $event)"
|
|
2426
|
+
>
|
|
2427
|
+
@if (column.sortable) {
|
|
2428
|
+
<button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column)">
|
|
2429
|
+
@if (column.headerCell; as headerTemplate) {
|
|
2430
|
+
<ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: column }" />
|
|
2431
|
+
} @else {<ng-container>{{ column.header }}</ng-container>}
|
|
2432
|
+
</button>
|
|
2433
|
+
} @else if (column.headerCell; as headerTemplate) {
|
|
2434
|
+
<ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: column }" />
|
|
2435
|
+
} @else {<ng-container>{{ column.header }}</ng-container>}
|
|
2436
|
+
@if (column.type === 'rollup' && labels().rollupDescription; as describe) {
|
|
2437
|
+
<span class="data-grid-visually-hidden" [id]="gridId + '-' + column.id + '-description'">{{ describe(column.header) }}</span>
|
|
2438
|
+
}
|
|
2439
|
+
@if (column.resizable) {
|
|
2440
|
+
<!-- WAI-ARIA "window splitter" pattern: role="separator" plus tabindex/aria-valuenow is how a resize handle becomes focusable and operable. -->
|
|
2441
|
+
<span
|
|
2442
|
+
class="data-grid-resize-handle"
|
|
2443
|
+
role="separator"
|
|
2444
|
+
data-grid-own
|
|
2445
|
+
aria-orientation="vertical"
|
|
2446
|
+
[attr.aria-label]="labels().resizeHandle?.(column.header) ?? null"
|
|
2447
|
+
[attr.aria-valuenow]="widthFor(column) ?? 120"
|
|
2448
|
+
[attr.aria-valuemin]="minColumnWidth"
|
|
2449
|
+
tabindex="0"
|
|
2450
|
+
(keydown)="onResizeKeydown(column, $event)"
|
|
2451
|
+
(pointerdown)="onResizePointerDown(column, $event)"
|
|
2452
|
+
></span>
|
|
2453
|
+
}
|
|
2454
|
+
</th>
|
|
2455
|
+
}
|
|
2456
|
+
</tr>
|
|
2457
|
+
</thead>
|
|
2458
|
+
<tbody lily-data-table-body>
|
|
2459
|
+
@for (item of renderItems(); track item.kind === 'row' ? pageEntries()[item.index].id : item.key) {
|
|
2460
|
+
@if (item.kind === 'spacer') {
|
|
2461
|
+
<tr class="data-grid-spacer" aria-hidden="true" [attr.style]="'height:' + item.rows * (virtualize()?.rowHeight ?? 0) + 'px'">
|
|
2462
|
+
<td [attr.colspan]="lastCol() + 1"></td>
|
|
2463
|
+
</tr>
|
|
2464
|
+
} @else {
|
|
2465
|
+
@let entry = pageEntries()[item.index];
|
|
2466
|
+
<tr
|
|
2467
|
+
lily-data-table-row
|
|
2468
|
+
[className]="rowClass()?.(entry.row) ?? ''"
|
|
2469
|
+
[attr.aria-selected]="hasSelection() ? isSelected(entry.id) : null"
|
|
2470
|
+
[attr.aria-rowindex]="indexed() ? pageStart() + item.index + 2 : null"
|
|
2471
|
+
[lilyDataGridAttrs]="rowAttrs()?.(entry.row)"
|
|
2472
|
+
>
|
|
2473
|
+
@if (hasSelection()) {
|
|
2474
|
+
<td
|
|
2475
|
+
lily-data-table-td
|
|
2476
|
+
[attr.role]="isGrid() ? 'gridcell' : 'cell'"
|
|
2477
|
+
[attr.data-row]="item.index"
|
|
2478
|
+
data-col="0"
|
|
2479
|
+
[attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === 0 ? 0 : -1) : null"
|
|
2480
|
+
>
|
|
2481
|
+
<input
|
|
2482
|
+
[type]="selectionMode() === 'single' ? 'radio' : 'checkbox'"
|
|
2483
|
+
data-grid-own
|
|
2484
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2485
|
+
[attr.aria-label]="labels().selectRow?.(rowLabelOf(entry)) ?? null"
|
|
2486
|
+
[checked]="isSelected(entry.id)"
|
|
2487
|
+
(click)="onRowSelectClick(pageStart() + item.index, $event)"
|
|
2488
|
+
/>
|
|
2489
|
+
</td>
|
|
2490
|
+
}
|
|
2491
|
+
@for (column of visibleColumns(); track column.id; let colIndex = $index) {
|
|
2492
|
+
@let ctx = cellFor(column, entry.row);
|
|
2493
|
+
@if (column.rowHeader) {
|
|
2494
|
+
<th
|
|
2495
|
+
lily-data-table-th
|
|
2496
|
+
scope="row"
|
|
2497
|
+
role="rowheader"
|
|
2498
|
+
[className]="cellClassOf(column, ctx)"
|
|
2499
|
+
[attr.data-row]="item.index"
|
|
2500
|
+
[attr.data-col]="colOffset() + colIndex"
|
|
2501
|
+
[attr.data-align]="column.align ?? null"
|
|
2502
|
+
[attr.data-cell-type]="column.type ?? null"
|
|
2503
|
+
[attr.data-pinned]="column.pinned ?? null"
|
|
2504
|
+
[attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
|
|
2505
|
+
[attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
|
|
2506
|
+
[attr.style]="cellStyle(column)"
|
|
2507
|
+
[lilyDataGridAttrs]="column.cellAttrs?.(ctx)"
|
|
2508
|
+
>
|
|
2509
|
+
<ng-container *ngTemplateOutlet="cellBody; context: { entry: entry, column: column, ctx: ctx }" />
|
|
2510
|
+
</th>
|
|
2511
|
+
} @else {
|
|
2512
|
+
<td
|
|
2513
|
+
lily-data-table-td
|
|
2514
|
+
[className]="cellClassOf(column, ctx)"
|
|
2515
|
+
[attr.role]="isGrid() ? 'gridcell' : 'cell'"
|
|
2516
|
+
[attr.data-row]="item.index"
|
|
2517
|
+
[attr.data-col]="colOffset() + colIndex"
|
|
2518
|
+
[attr.data-align]="column.align ?? null"
|
|
2519
|
+
[attr.data-cell-type]="column.type ?? null"
|
|
2520
|
+
[attr.data-pinned]="column.pinned ?? null"
|
|
2521
|
+
[attr.data-editable]="isEditable(column) ? '' : null"
|
|
2522
|
+
[attr.data-readonly]="column.type === 'rollup' ? '' : null"
|
|
2523
|
+
[attr.aria-readonly]="column.type === 'rollup' ? 'true' : null"
|
|
2524
|
+
[attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
|
|
2525
|
+
[attr.data-editing]="isEditingCell(entry, column) ? '' : null"
|
|
2526
|
+
[attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
|
|
2527
|
+
[attr.style]="cellStyle(column)"
|
|
2528
|
+
[lilyDataGridAttrs]="column.cellAttrs?.(ctx)"
|
|
2529
|
+
(dragover)="onFileDragOver(column, $event)"
|
|
2530
|
+
(drop)="onFileDrop(entry, column, $event)"
|
|
2531
|
+
>
|
|
2532
|
+
<ng-container *ngTemplateOutlet="cellBody; context: { entry: entry, column: column, ctx: ctx }" />
|
|
2533
|
+
</td>
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
</tr>
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
@if (pageEntries().length === 0 && empty(); as emptyTemplate) {
|
|
2540
|
+
<tr lily-data-table-row className="data-grid-empty-row">
|
|
2541
|
+
<td lily-data-table-td className="data-grid-empty" [attr.colspan]="lastCol() + 1" [attr.role]="isGrid() ? 'gridcell' : 'cell'">
|
|
2542
|
+
<ng-container *ngTemplateOutlet="emptyTemplate" />
|
|
2543
|
+
</td>
|
|
2544
|
+
</tr>
|
|
2545
|
+
}
|
|
2546
|
+
</tbody>
|
|
2547
|
+
@if (footer(); as footerTemplate) {
|
|
2548
|
+
<tfoot lily-data-table-foot>
|
|
2549
|
+
<ng-container *ngTemplateOutlet="footerTemplate; context: { $implicit: footerContext() }" />
|
|
2550
|
+
</tfoot>
|
|
2551
|
+
}
|
|
2552
|
+
</lily-data-table>
|
|
2553
|
+
</ng-template>
|
|
2554
|
+
|
|
2555
|
+
<ng-template #cellBody let-entry="entry" let-column="column" let-ctx="ctx">
|
|
2556
|
+
@if (isEditingCell(entry, column)) {
|
|
2557
|
+
@let editing = editingState()!;
|
|
2558
|
+
@if (column.editor === 'person' && column.personEditor) {
|
|
2559
|
+
<ng-container *ngTemplateOutlet="column.personEditor; context: { $implicit: personEditorContext(entry, column, ctx) }" />
|
|
2560
|
+
} @else if (column.editor === 'time-zone') {
|
|
2561
|
+
<span
|
|
2562
|
+
class="data-grid-zoned-editor"
|
|
2563
|
+
role="group"
|
|
2564
|
+
[attr.aria-label]="labels().cellEditor?.(column.header) ?? column.header"
|
|
2565
|
+
(focusout)="onZonedFocusout($event)"
|
|
2566
|
+
>
|
|
2567
|
+
<input
|
|
2568
|
+
class="data-grid-cell-editor"
|
|
2569
|
+
type="time"
|
|
2570
|
+
step="1"
|
|
2571
|
+
[attr.aria-label]="labels().cellEditor?.(column.header) ?? column.header"
|
|
2572
|
+
[attr.aria-invalid]="editing.error || editing.invalid ? 'true' : null"
|
|
2573
|
+
[attr.aria-describedby]="editing.error ? entry.id + '-' + column.id + '-error' : null"
|
|
2574
|
+
[value]="editing.draft"
|
|
2575
|
+
(input)="setDraft($event)"
|
|
2576
|
+
(keydown)="onEditorKeydown($event)"
|
|
2577
|
+
/>
|
|
2578
|
+
<select
|
|
2579
|
+
class="data-grid-cell-editor data-grid-zone-editor"
|
|
2580
|
+
[attr.aria-label]="labels().timeZone?.(column.header) ?? column.header"
|
|
2581
|
+
(change)="setZone($event)"
|
|
2582
|
+
(keydown)="onEditorKeydown($event)"
|
|
2583
|
+
>
|
|
2584
|
+
@for (option of column.timeZones ?? []; track option.id) {
|
|
2585
|
+
<option [value]="option.id" [selected]="option.id === editing.zone">{{ option.label }}</option>
|
|
2586
|
+
}
|
|
2587
|
+
</select>
|
|
2588
|
+
</span>
|
|
2589
|
+
} @else if (column.editor === 'select') {
|
|
2590
|
+
<select
|
|
2591
|
+
class="data-grid-cell-editor"
|
|
2592
|
+
[attr.aria-label]="labels().cellEditor?.(column.header) ?? column.header"
|
|
2593
|
+
[attr.aria-invalid]="editing.error || editing.invalid ? 'true' : null"
|
|
2594
|
+
[attr.aria-describedby]="editing.error ? entry.id + '-' + column.id + '-error' : null"
|
|
2595
|
+
(change)="setDraft($event)"
|
|
2596
|
+
(keydown)="onEditorKeydown($event)"
|
|
2597
|
+
(blur)="onEditorBlur()"
|
|
2598
|
+
>
|
|
2599
|
+
<option value="" [selected]="editing.draft === ''"></option>
|
|
2600
|
+
@for (option of ungroupedOptions(column); track option.id) {
|
|
2601
|
+
<option [value]="option.id" [disabled]="option.disabled ?? false" [selected]="option.id === editing.draft">{{ option.label }}</option>
|
|
2602
|
+
}
|
|
2603
|
+
@for (group of groupsOf(column); track group) {
|
|
2604
|
+
<optgroup [label]="group">
|
|
2605
|
+
@for (option of optionsInGroup(column, group); track option.id) {
|
|
2606
|
+
<option [value]="option.id" [disabled]="option.disabled ?? false" [selected]="option.id === editing.draft">{{ option.label }}</option>
|
|
2607
|
+
}
|
|
2608
|
+
</optgroup>
|
|
2609
|
+
}
|
|
2610
|
+
</select>
|
|
2611
|
+
} @else {
|
|
2612
|
+
<input
|
|
2613
|
+
class="data-grid-cell-editor"
|
|
2614
|
+
[type]="column.editor === 'number' ? 'number' : column.editor === 'date' ? 'date' : column.editor === 'time' ? 'time' : 'text'"
|
|
2615
|
+
[attr.step]="column.editor === 'time' ? 1 : null"
|
|
2616
|
+
[attr.inputmode]="column.editor === 'number' ? 'decimal' : null"
|
|
2617
|
+
[attr.aria-label]="(column.type === 'formula' ? labels().formulaEditor?.(column.header) : undefined) ?? labels().cellEditor?.(column.header) ?? column.header"
|
|
2618
|
+
[attr.aria-invalid]="editing.error || editing.invalid ? 'true' : null"
|
|
2619
|
+
[attr.aria-describedby]="editing.error ? entry.id + '-' + column.id + '-error' : null"
|
|
2620
|
+
[value]="editing.draft"
|
|
2621
|
+
(input)="setDraft($event)"
|
|
2622
|
+
(keydown)="onEditorKeydown($event)"
|
|
2623
|
+
(blur)="onEditorBlur()"
|
|
2624
|
+
/>
|
|
2625
|
+
}
|
|
2626
|
+
@if (editing.error) {
|
|
2627
|
+
<span class="data-grid-cell-error" [id]="entry.id + '-' + column.id + '-error'">{{ editing.error }}</span>
|
|
2628
|
+
}
|
|
2629
|
+
} @else if (column.type === 'radio' && column.editable) {
|
|
2630
|
+
<fieldset class="data-grid-radio-group" data-grid-own-group>
|
|
2631
|
+
<legend class="data-grid-radio-legend">{{ labels().radioGroup?.(column.header, rowLabelOf(entry)) ?? column.header }}</legend>
|
|
2632
|
+
@for (option of column.options ?? []; track option.id) {
|
|
2633
|
+
<label class="data-grid-radio-option">
|
|
2634
|
+
<input
|
|
2635
|
+
type="radio"
|
|
2636
|
+
[name]="gridId + '-' + entry.id + '-' + column.id"
|
|
2637
|
+
[value]="option.id"
|
|
2638
|
+
[checked]="ctx.value === option.id"
|
|
2639
|
+
[disabled]="option.disabled ?? false"
|
|
2640
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2641
|
+
(change)="commitValue(entry, column, option.id)"
|
|
2642
|
+
/>{{ option.label }}</label>
|
|
2643
|
+
}
|
|
2644
|
+
</fieldset>
|
|
2645
|
+
} @else {
|
|
2646
|
+
@if (column.cell; as cellTemplate) {
|
|
2647
|
+
<ng-container *ngTemplateOutlet="cellTemplate; context: { $implicit: ctx }" />
|
|
2648
|
+
} @else if (column.type === 'person') {
|
|
2649
|
+
@let people = peopleIn(ctx.value);
|
|
2650
|
+
@if (people.length > 0) {
|
|
2651
|
+
@let shown = shownPeople(column, ctx.value);
|
|
2652
|
+
<ul class="data-grid-people">
|
|
2653
|
+
@for (person of shown; track person.id) {
|
|
2654
|
+
<li class="data-grid-person">
|
|
2655
|
+
@if (column.avatar; as avatarTemplate) {
|
|
2656
|
+
<span class="data-grid-avatar" aria-hidden="true"><ng-container *ngTemplateOutlet="avatarTemplate; context: { $implicit: person }" /></span>
|
|
2657
|
+
}
|
|
2658
|
+
<span class="data-grid-person-name">{{ nameOf(person) }}</span>
|
|
2659
|
+
</li>
|
|
2660
|
+
}
|
|
2661
|
+
@if (shown.length < people.length && labels().morePeople; as more) {
|
|
2662
|
+
<li class="data-grid-people-more">{{ more(people.length - shown.length) }}</li>
|
|
2663
|
+
}
|
|
2664
|
+
</ul>
|
|
2665
|
+
}
|
|
2666
|
+
} @else if (column.type === 'file') {
|
|
2667
|
+
@let files = filesIn(ctx.value);
|
|
2668
|
+
@if (files.length > 0) {
|
|
2669
|
+
<ul class="data-grid-files">
|
|
2670
|
+
@for (file of files; track file.id) {
|
|
2671
|
+
<li class="data-grid-file">
|
|
2672
|
+
@if (hrefOf(file.href); as fileHref) {
|
|
2673
|
+
<a class="data-grid-link" [href]="fileHref">{{ file.name }}</a>
|
|
2674
|
+
} @else {
|
|
2675
|
+
<span class="data-grid-file-name">{{ file.name }}</span>
|
|
2676
|
+
}
|
|
2677
|
+
@if (file.size !== undefined && column.formatSize; as sizeText) {
|
|
2678
|
+
{{ ' ' }}<span class="data-grid-file-size">{{ column.formatSize(file.size) }}</span>
|
|
2679
|
+
}
|
|
2680
|
+
@if (column.editable && labels().removeFile; as removeLabel) {
|
|
2681
|
+
<button
|
|
2682
|
+
type="button"
|
|
2683
|
+
class="data-grid-file-remove"
|
|
2684
|
+
data-grid-own
|
|
2685
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2686
|
+
[attr.aria-label]="removeLabel(file.name)"
|
|
2687
|
+
(click)="fileRemove.emit({ rowId: entry.id, row: entry.row, columnId: column.id, fileId: file.id })"
|
|
2688
|
+
></button>
|
|
2689
|
+
}
|
|
2690
|
+
</li>
|
|
2691
|
+
}
|
|
2692
|
+
</ul>
|
|
2693
|
+
}
|
|
2694
|
+
@if (column.editable) {
|
|
2695
|
+
<input
|
|
2696
|
+
type="file"
|
|
2697
|
+
class="data-grid-file-input"
|
|
2698
|
+
[attr.aria-label]="labels().fileInput?.(column.header, rowLabelOf(entry)) ?? column.header"
|
|
2699
|
+
[multiple]="column.multiple !== false"
|
|
2700
|
+
[attr.accept]="column.accept ?? null"
|
|
2701
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2702
|
+
(change)="onFileInput(entry, column, $event)"
|
|
2703
|
+
/>
|
|
2704
|
+
}
|
|
2705
|
+
} @else if (column.type === 'link') {
|
|
2706
|
+
@let link = linkIn(ctx.value);
|
|
2707
|
+
@if (link) {
|
|
2708
|
+
@if (hrefOf(link.href); as href) {
|
|
2709
|
+
<a class="data-grid-link" [href]="href" [attr.target]="link.external ? '_blank' : null" [attr.rel]="link.external ? 'noopener noreferrer' : null">{{ link.label ?? link.href }}@if (link.external && labels().opensInNewTab; as newTab) {{{ ' ' }}<span class="data-grid-visually-hidden">{{ newTab(link.label ?? link.href) }}</span>}</a>
|
|
2710
|
+
} @else {
|
|
2711
|
+
<!-- An address with an unsafe scheme is shown as text, never as a link. -->
|
|
2712
|
+
<span class="data-grid-link data-grid-link-unsafe">{{ link.label ?? link.href }}</span>
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
} @else if (column.type === 'date' || column.type === 'time') {
|
|
2716
|
+
@if (ctx.formatted !== '') {
|
|
2717
|
+
<time [attr.datetime]="stringOf(ctx.value)">{{ ctx.formatted }}</time>
|
|
2718
|
+
}
|
|
2719
|
+
} @else {<ng-container>{{ ctx.formatted }}</ng-container>}
|
|
2720
|
+
@if (isEditable(column) && column.type !== 'radio' && labels().editCell; as editLabel) {
|
|
2721
|
+
<button
|
|
2722
|
+
type="button"
|
|
2723
|
+
class="data-grid-edit-button"
|
|
2724
|
+
data-grid-own
|
|
2725
|
+
[attr.tabindex]="isGrid() ? -1 : null"
|
|
2726
|
+
[attr.aria-label]="editLabel(column.header, rowLabelOf(entry))"
|
|
2727
|
+
(click)="startEdit(entry, column)"
|
|
2728
|
+
></button>
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
</ng-template>
|
|
2732
|
+
`
|
|
2733
|
+
}]
|
|
2734
|
+
}],
|
|
2735
|
+
ctorParameters: () => [],
|
|
2736
|
+
propDecorators: {
|
|
2737
|
+
label: [{
|
|
2738
|
+
type: i0.Input,
|
|
2739
|
+
args: [{
|
|
2740
|
+
isSignal: true,
|
|
2741
|
+
alias: "label",
|
|
2742
|
+
required: true
|
|
2743
|
+
}]
|
|
2744
|
+
}],
|
|
2745
|
+
caption: [{
|
|
2746
|
+
type: i0.Input,
|
|
2747
|
+
args: [{
|
|
2748
|
+
isSignal: true,
|
|
2749
|
+
alias: "caption",
|
|
2750
|
+
required: false
|
|
2751
|
+
}]
|
|
2752
|
+
}],
|
|
2753
|
+
columns: [{
|
|
2754
|
+
type: i0.Input,
|
|
2755
|
+
args: [{
|
|
2756
|
+
isSignal: true,
|
|
2757
|
+
alias: "columns",
|
|
2758
|
+
required: true
|
|
2759
|
+
}]
|
|
2760
|
+
}],
|
|
2761
|
+
rows: [{
|
|
2762
|
+
type: i0.Input,
|
|
2763
|
+
args: [{
|
|
2764
|
+
isSignal: true,
|
|
2765
|
+
alias: "rows",
|
|
2766
|
+
required: true
|
|
2767
|
+
}]
|
|
2768
|
+
}],
|
|
2769
|
+
locale: [{
|
|
2770
|
+
type: i0.Input,
|
|
2771
|
+
args: [{
|
|
2772
|
+
isSignal: true,
|
|
2773
|
+
alias: "locale",
|
|
2774
|
+
required: false
|
|
2775
|
+
}]
|
|
2776
|
+
}],
|
|
2777
|
+
rowId: [{
|
|
2778
|
+
type: i0.Input,
|
|
2779
|
+
args: [{
|
|
2780
|
+
isSignal: true,
|
|
2781
|
+
alias: "rowId",
|
|
2782
|
+
required: false
|
|
2783
|
+
}]
|
|
2784
|
+
}],
|
|
2785
|
+
selectionMode: [{
|
|
2786
|
+
type: i0.Input,
|
|
2787
|
+
args: [{
|
|
2788
|
+
isSignal: true,
|
|
2789
|
+
alias: "selectionMode",
|
|
2790
|
+
required: false
|
|
2791
|
+
}]
|
|
2792
|
+
}],
|
|
2793
|
+
pageSize: [{
|
|
2794
|
+
type: i0.Input,
|
|
2795
|
+
args: [{
|
|
2796
|
+
isSignal: true,
|
|
2797
|
+
alias: "pageSize",
|
|
2798
|
+
required: false
|
|
2799
|
+
}]
|
|
2800
|
+
}],
|
|
2801
|
+
storageKey: [{
|
|
2802
|
+
type: i0.Input,
|
|
2803
|
+
args: [{
|
|
2804
|
+
isSignal: true,
|
|
2805
|
+
alias: "storageKey",
|
|
2806
|
+
required: false
|
|
2807
|
+
}]
|
|
2808
|
+
}],
|
|
2809
|
+
labels: [{
|
|
2810
|
+
type: i0.Input,
|
|
2811
|
+
args: [{
|
|
2812
|
+
isSignal: true,
|
|
2813
|
+
alias: "labels",
|
|
2814
|
+
required: false
|
|
2815
|
+
}]
|
|
2816
|
+
}],
|
|
2817
|
+
mode: [{
|
|
2818
|
+
type: i0.Input,
|
|
2819
|
+
args: [{
|
|
2820
|
+
isSignal: true,
|
|
2821
|
+
alias: "mode",
|
|
2822
|
+
required: false
|
|
2823
|
+
}]
|
|
2824
|
+
}],
|
|
2825
|
+
rowClass: [{
|
|
2826
|
+
type: i0.Input,
|
|
2827
|
+
args: [{
|
|
2828
|
+
isSignal: true,
|
|
2829
|
+
alias: "rowClass",
|
|
2830
|
+
required: false
|
|
2831
|
+
}]
|
|
2832
|
+
}],
|
|
2833
|
+
rowAttrs: [{
|
|
2834
|
+
type: i0.Input,
|
|
2835
|
+
args: [{
|
|
2836
|
+
isSignal: true,
|
|
2837
|
+
alias: "rowAttrs",
|
|
2838
|
+
required: false
|
|
2839
|
+
}]
|
|
2840
|
+
}],
|
|
2841
|
+
activateRows: [{
|
|
2842
|
+
type: i0.Input,
|
|
2843
|
+
args: [{
|
|
2844
|
+
isSignal: true,
|
|
2845
|
+
alias: "activateRows",
|
|
2846
|
+
required: false
|
|
2847
|
+
}]
|
|
2848
|
+
}],
|
|
2849
|
+
empty: [{
|
|
2850
|
+
type: i0.Input,
|
|
2851
|
+
args: [{
|
|
2852
|
+
isSignal: true,
|
|
2853
|
+
alias: "empty",
|
|
2854
|
+
required: false
|
|
2855
|
+
}]
|
|
2856
|
+
}],
|
|
2857
|
+
footer: [{
|
|
2858
|
+
type: i0.Input,
|
|
2859
|
+
args: [{
|
|
2860
|
+
isSignal: true,
|
|
2861
|
+
alias: "footer",
|
|
2862
|
+
required: false
|
|
2863
|
+
}]
|
|
2864
|
+
}],
|
|
2865
|
+
busy: [{
|
|
2866
|
+
type: i0.Input,
|
|
2867
|
+
args: [{
|
|
2868
|
+
isSignal: true,
|
|
2869
|
+
alias: "busy",
|
|
2870
|
+
required: false
|
|
2871
|
+
}]
|
|
2872
|
+
}],
|
|
2873
|
+
scroll: [{
|
|
2874
|
+
type: i0.Input,
|
|
2875
|
+
args: [{
|
|
2876
|
+
isSignal: true,
|
|
2877
|
+
alias: "scroll",
|
|
2878
|
+
required: false
|
|
2879
|
+
}]
|
|
2880
|
+
}],
|
|
2881
|
+
exportFilename: [{
|
|
2882
|
+
type: i0.Input,
|
|
2883
|
+
args: [{
|
|
2884
|
+
isSignal: true,
|
|
2885
|
+
alias: "exportFilename",
|
|
2886
|
+
required: false
|
|
2887
|
+
}]
|
|
2888
|
+
}],
|
|
2889
|
+
exportScope: [{
|
|
2890
|
+
type: i0.Input,
|
|
2891
|
+
args: [{
|
|
2892
|
+
isSignal: true,
|
|
2893
|
+
alias: "exportScope",
|
|
2894
|
+
required: false
|
|
2895
|
+
}]
|
|
2896
|
+
}],
|
|
2897
|
+
exportFormulas: [{
|
|
2898
|
+
type: i0.Input,
|
|
2899
|
+
args: [{
|
|
2900
|
+
isSignal: true,
|
|
2901
|
+
alias: "exportFormulas",
|
|
2902
|
+
required: false
|
|
2903
|
+
}]
|
|
2904
|
+
}],
|
|
2905
|
+
exportHandler: [{
|
|
2906
|
+
type: i0.Input,
|
|
2907
|
+
args: [{
|
|
2908
|
+
isSignal: true,
|
|
2909
|
+
alias: "exportHandler",
|
|
2910
|
+
required: false
|
|
2911
|
+
}]
|
|
2912
|
+
}],
|
|
2913
|
+
virtualize: [{
|
|
2914
|
+
type: i0.Input,
|
|
2915
|
+
args: [{
|
|
2916
|
+
isSignal: true,
|
|
2917
|
+
alias: "virtualize",
|
|
2918
|
+
required: false
|
|
2919
|
+
}]
|
|
2920
|
+
}],
|
|
2921
|
+
className: [{
|
|
2922
|
+
type: i0.Input,
|
|
2923
|
+
args: [{
|
|
2924
|
+
isSignal: true,
|
|
2925
|
+
alias: "className",
|
|
2926
|
+
required: false
|
|
2927
|
+
}]
|
|
2928
|
+
}],
|
|
2929
|
+
selected: [{
|
|
2930
|
+
type: i0.Input,
|
|
2931
|
+
args: [{
|
|
2932
|
+
isSignal: true,
|
|
2933
|
+
alias: "selected",
|
|
2934
|
+
required: false
|
|
2935
|
+
}]
|
|
2936
|
+
}, {
|
|
2937
|
+
type: i0.Output,
|
|
2938
|
+
args: ["selectedChange"]
|
|
2939
|
+
}],
|
|
2940
|
+
filter: [{
|
|
2941
|
+
type: i0.Input,
|
|
2942
|
+
args: [{
|
|
2943
|
+
isSignal: true,
|
|
2944
|
+
alias: "filter",
|
|
2945
|
+
required: false
|
|
2946
|
+
}]
|
|
2947
|
+
}, {
|
|
2948
|
+
type: i0.Output,
|
|
2949
|
+
args: ["filterChange"]
|
|
2950
|
+
}],
|
|
2951
|
+
sort: [{
|
|
2952
|
+
type: i0.Input,
|
|
2953
|
+
args: [{
|
|
2954
|
+
isSignal: true,
|
|
2955
|
+
alias: "sort",
|
|
2956
|
+
required: false
|
|
2957
|
+
}]
|
|
2958
|
+
}, {
|
|
2959
|
+
type: i0.Output,
|
|
2960
|
+
args: ["sortChange"]
|
|
2961
|
+
}],
|
|
2962
|
+
columnOrder: [{
|
|
2963
|
+
type: i0.Input,
|
|
2964
|
+
args: [{
|
|
2965
|
+
isSignal: true,
|
|
2966
|
+
alias: "columnOrder",
|
|
2967
|
+
required: false
|
|
2968
|
+
}]
|
|
2969
|
+
}, {
|
|
2970
|
+
type: i0.Output,
|
|
2971
|
+
args: ["columnOrderChange"]
|
|
2972
|
+
}],
|
|
2973
|
+
page: [{
|
|
2974
|
+
type: i0.Input,
|
|
2975
|
+
args: [{
|
|
2976
|
+
isSignal: true,
|
|
2977
|
+
alias: "page",
|
|
2978
|
+
required: false
|
|
2979
|
+
}]
|
|
2980
|
+
}, {
|
|
2981
|
+
type: i0.Output,
|
|
2982
|
+
args: ["pageChange"]
|
|
2983
|
+
}],
|
|
2984
|
+
cellEdit: [{
|
|
2985
|
+
type: i0.Output,
|
|
2986
|
+
args: ["cellEdit"]
|
|
2987
|
+
}],
|
|
2988
|
+
filesSelected: [{
|
|
2989
|
+
type: i0.Output,
|
|
2990
|
+
args: ["filesSelected"]
|
|
2991
|
+
}],
|
|
2992
|
+
fileRemove: [{
|
|
2993
|
+
type: i0.Output,
|
|
2994
|
+
args: ["fileRemove"]
|
|
2995
|
+
}],
|
|
2996
|
+
rowActivate: [{
|
|
2997
|
+
type: i0.Output,
|
|
2998
|
+
args: ["rowActivate"]
|
|
2999
|
+
}],
|
|
3000
|
+
exportFile: [{
|
|
3001
|
+
type: i0.Output,
|
|
3002
|
+
args: ["exportFile"]
|
|
3003
|
+
}],
|
|
3004
|
+
rootRef: [{
|
|
3005
|
+
type: i0.ViewChild,
|
|
3006
|
+
args: ["rootEl", { isSignal: true }]
|
|
3007
|
+
}],
|
|
3008
|
+
scrollRef: [{
|
|
3009
|
+
type: i0.ViewChild,
|
|
3010
|
+
args: ["scrollEl", { isSignal: true }]
|
|
3011
|
+
}]
|
|
3012
|
+
}
|
|
3013
|
+
});
|
|
3014
|
+
export { DataGrid, DataGridAttrs, MIME_TYPES, aggregateValues, cellText, civilDateToDate, columnName, crc32, downloadBlob, exportBlob, exportFilename, filesOf, isCivilDate, isCivilTime, isExportFormula, isInvalidForType, isValidTimeZone, isZonedTime, linkLabel, linkParts, nextDataGridId, peopleOf, personName, resolveColumn, safeHref, toCsv, toDelimited, toTsv, toXlsx, zipStore, zonedMinutes, zonedUtcTime };
|
|
3015
|
+
|
|
3016
|
+
//# sourceMappingURL=lilydesignsystem-angular-data-grid.mjs.map
|