@lilydesignsystem/angular-data-grid 0.9.0 → 0.10.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.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from "@angular/core";
2
2
  import { ChangeDetectionStrategy, Component, Directive, ElementRef, Injector, afterNextRender, computed, effect, inject, input, model, output, signal, untracked, viewChild } from "@angular/core";
3
3
  import { NgTemplateOutlet } from "@angular/common";
4
- import { DataTable, DataTableBody, DataTableFoot, DataTableHead, DataTableRow, DataTableTD, DataTableTH } from "@lilydesignsystem/angular-headless";
4
+ import { DataTable, DataTableBody, DataTableFoot, DataTableHead, DataTableRow, DataTableTD, DataTableTH, PaginationNav, SearchInput } from "@lilydesignsystem/angular-headless";
5
5
  function isExportFormula(value) {
6
6
  return !!value && typeof value === "object" && !(value instanceof Date) && typeof value.formula === "string" && "result" in value;
7
7
  }
@@ -213,6 +213,251 @@ function downloadBlob(blob, filename) {
213
213
  link.remove();
214
214
  setTimeout(() => URL.revokeObjectURL(url), 1e3);
215
215
  }
216
+ function toClipboardText(table) {
217
+ return toTsv(table, { bom: false });
218
+ }
219
+ async function copyToClipboard(text) {
220
+ try {
221
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
222
+ await navigator.clipboard.writeText(text);
223
+ return true;
224
+ }
225
+ } catch {}
226
+ try {
227
+ if (typeof document === "undefined") return false;
228
+ const area = document.createElement("textarea");
229
+ area.value = text;
230
+ area.setAttribute("readonly", "");
231
+ area.setAttribute("aria-hidden", "true");
232
+ area.style.position = "fixed";
233
+ area.style.opacity = "0";
234
+ document.body.appendChild(area);
235
+ area.select();
236
+ const ok = typeof document.execCommand === "function" && document.execCommand("copy");
237
+ area.remove();
238
+ return !!ok;
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+ function foldText(text) {
244
+ return text.normalize("NFD").replace(/\p{M}+/gu, "").toLowerCase();
245
+ }
246
+ function searchTerms(query) {
247
+ return foldText(query).split(/\s+/).filter(Boolean);
248
+ }
249
+ function matchesSearch(text, query) {
250
+ const terms = searchTerms(query);
251
+ if (terms.length === 0) return true;
252
+ const folded = foldText(text);
253
+ return terms.every((term) => folded.includes(term));
254
+ }
255
+ function highlightSegments(text, query) {
256
+ const terms = searchTerms(query);
257
+ if (terms.length === 0 || text === "") return [{
258
+ text,
259
+ match: false
260
+ }];
261
+ const chars = Array.from(text);
262
+ const folded = chars.map((c) => foldText(c));
263
+ if (folded.some((f) => f.length !== 1)) return [{
264
+ text,
265
+ match: false
266
+ }];
267
+ const flat = folded.join("");
268
+ const marked = new Array(chars.length).fill(false);
269
+ for (const term of terms) for (let at = flat.indexOf(term); at !== -1; at = flat.indexOf(term, at + 1)) for (let i = at; i < at + term.length; i++) marked[i] = true;
270
+ const out = [];
271
+ chars.forEach((c, i) => {
272
+ const last = out[out.length - 1];
273
+ if (last && last.match === marked[i]) last.text += c;
274
+ else out.push({
275
+ text: c,
276
+ match: marked[i]
277
+ });
278
+ });
279
+ return out;
280
+ }
281
+ function isRtl(element) {
282
+ if (!element) return false;
283
+ const attr = element.closest("[dir]")?.getAttribute("dir")?.toLowerCase();
284
+ if (attr === "rtl") return true;
285
+ if (attr === "ltr") return false;
286
+ if (typeof getComputedStyle === "function") return getComputedStyle(element).direction === "rtl";
287
+ return false;
288
+ }
289
+ function logicalKey(key, rtl) {
290
+ if (!rtl) return key;
291
+ if (key === "ArrowLeft") return "ArrowRight";
292
+ if (key === "ArrowRight") return "ArrowLeft";
293
+ return key;
294
+ }
295
+ const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
296
+ function gridMove(event, at, nav) {
297
+ const key = logicalKey(event.key, nav.rtl ?? false);
298
+ const corner = !!(event.ctrlKey || event.metaKey);
299
+ let row = at.row;
300
+ let col = at.col;
301
+ switch (key) {
302
+ case "ArrowRight":
303
+ col += 1;
304
+ break;
305
+ case "ArrowLeft":
306
+ col -= 1;
307
+ break;
308
+ case "ArrowDown":
309
+ row += 1;
310
+ break;
311
+ case "ArrowUp":
312
+ row -= 1;
313
+ break;
314
+ case "PageDown":
315
+ row += nav.pageStep;
316
+ break;
317
+ case "PageUp":
318
+ row -= nav.pageStep;
319
+ break;
320
+ case "Home":
321
+ if (corner) [row, col] = [nav.firstRow, 0];
322
+ else if (nav.homeEnd === "column") row = nav.firstRow;
323
+ else col = 0;
324
+ break;
325
+ case "End":
326
+ if (corner) [row, col] = [nav.lastRow, nav.lastCol];
327
+ else if (nav.homeEnd === "column") row = nav.lastRow;
328
+ else col = nav.lastCol;
329
+ break;
330
+ default: return null;
331
+ }
332
+ return {
333
+ row: clamp(row, nav.firstRow, nav.lastRow),
334
+ col: clamp(col, 0, nav.lastCol)
335
+ };
336
+ }
337
+ const CONTROL_LABELS = {
338
+ selection: [
339
+ "selectAll",
340
+ "selectionColumn",
341
+ "selectRow"
342
+ ],
343
+ pagination: [
344
+ "previousPage",
345
+ "nextPage",
346
+ "pageStatus"
347
+ ],
348
+ hidableColumns: ["columnVisibility", "columnVisibilityOption"],
349
+ resizableColumns: ["resizeHandle"],
350
+ editing: ["editCell", "cellEditor"],
351
+ search: ["search"],
352
+ exporting: ["export"]
353
+ };
354
+ const ANNOUNCEMENT_LABELS = {
355
+ sorting: ["sortAnnouncement"],
356
+ search: ["filterAnnouncement"],
357
+ selection: ["selectionAnnouncement"],
358
+ pagination: ["pageAnnouncement"],
359
+ editing: [
360
+ "editAnnouncement",
361
+ "editCancelAnnouncement",
362
+ "editError",
363
+ "undoAnnouncement",
364
+ "redoAnnouncement"
365
+ ],
366
+ exporting: [
367
+ "exportAnnouncement",
368
+ "copyAnnouncement",
369
+ "copyFailed"
370
+ ]
371
+ };
372
+ function requiredLabels(features) {
373
+ const out = [];
374
+ const add = (table, kind) => {
375
+ for (const feature of Object.keys(table)) {
376
+ if (!features[feature]) continue;
377
+ for (const key of table[feature]) if (!out.some((o) => o.key === key)) out.push({
378
+ key,
379
+ kind
380
+ });
381
+ }
382
+ };
383
+ add(CONTROL_LABELS, "control");
384
+ add(ANNOUNCEMENT_LABELS, "announcement");
385
+ return out;
386
+ }
387
+ function missingLabels(features, labels) {
388
+ return requiredLabels(features).filter(({ key }) => {
389
+ const value = labels?.[key];
390
+ return value === void 0 || value === null || value === "";
391
+ });
392
+ }
393
+ const EMPTY_HISTORY = {
394
+ undo: [],
395
+ redo: []
396
+ };
397
+ const HISTORY_LIMIT = 100;
398
+ function recordEdit(history, record, limit = 100) {
399
+ const undo = [...history.undo, record];
400
+ return {
401
+ undo: undo.length > limit ? undo.slice(undo.length - limit) : undo,
402
+ redo: []
403
+ };
404
+ }
405
+ function undoEdit(history) {
406
+ const record = history.undo[history.undo.length - 1];
407
+ if (!record) return {
408
+ history,
409
+ record: void 0
410
+ };
411
+ return {
412
+ history: {
413
+ undo: history.undo.slice(0, -1),
414
+ redo: [...history.redo, record]
415
+ },
416
+ record
417
+ };
418
+ }
419
+ function redoEdit(history) {
420
+ const record = history.redo[history.redo.length - 1];
421
+ if (!record) return {
422
+ history,
423
+ record: void 0
424
+ };
425
+ return {
426
+ history: {
427
+ undo: [...history.undo, record],
428
+ redo: history.redo.slice(0, -1)
429
+ },
430
+ record
431
+ };
432
+ }
433
+ function historyKey(event) {
434
+ if (event.altKey || !(event.ctrlKey || event.metaKey)) return null;
435
+ const key = event.key.toLowerCase();
436
+ if (key === "z") return event.shiftKey ? "redo" : "undo";
437
+ if (key === "y" && !event.shiftKey) return "redo";
438
+ return null;
439
+ }
440
+ const TYPEAHEAD_RESET_MS = 700;
441
+ function isTypeaheadKey(event) {
442
+ return event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.altKey && !event.metaKey;
443
+ }
444
+ function nextTypeaheadState(previous, key, now, resetMs = 700) {
445
+ return {
446
+ prefix: (previous.prefix !== "" && now - previous.at <= resetMs ? previous.prefix : "") + key,
447
+ at: now
448
+ };
449
+ }
450
+ function findTypeaheadMatch(labels, from, prefix) {
451
+ const folded = foldText(prefix);
452
+ if (folded === "" || labels.length === 0) return -1;
453
+ const needle = folded.length > 1 && [...folded].every((c) => c === folded[0]) ? folded[0] : folded;
454
+ const startOffset = needle.length > 1 ? 0 : 1;
455
+ for (let step = 0; step < labels.length; step++) {
456
+ const index = (from + startOffset + step + labels.length) % labels.length;
457
+ if (foldText(labels[index]).startsWith(needle)) return index;
458
+ }
459
+ return -1;
460
+ }
216
461
  const SAFE_SCHEME = /^(https?:|mailto:|tel:)/i;
217
462
  const RELATIVE = /^(\/(?!\/)|\.\.?\/|#|\?)/;
218
463
  function safeHref(href) {
@@ -607,6 +852,7 @@ function resolveColumn(column, gridLocale, rows = []) {
607
852
  }
608
853
  return resolved;
609
854
  }
855
+ const isMissing = (value) => value === null || value === void 0 || value === "";
610
856
  const WIDGET_SELECTOR = "a[href], button, input, select, textarea, summary, [contenteditable=\"true\"]";
611
857
  const MIN_COLUMN_WIDTH = 40;
612
858
  const RESIZE_STEP = 16;
@@ -705,12 +951,20 @@ var DataGrid = class DataGrid {
705
951
  }
706
952
  return ctx;
707
953
  }
954
+ isMovable(column) {
955
+ return column.movable ?? this.movableColumns();
956
+ }
708
957
  constructor() {
709
958
  this.label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
710
959
  this.caption = input(void 0, ...ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []);
711
960
  this.columns = input.required(...ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []);
712
961
  this.rows = input.required(...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
713
962
  this.locale = input(void 0, ...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
963
+ this.emptyValue = input(void 0, ...ngDevMode ? [{ debugName: "emptyValue" }] : /* istanbul ignore next */ []);
964
+ this.warnMissingLabels = input(true, ...ngDevMode ? [{ debugName: "warnMissingLabels" }] : /* istanbul ignore next */ []);
965
+ this.nulls = input("last", ...ngDevMode ? [{ debugName: "nulls" }] : /* istanbul ignore next */ []);
966
+ this.movableColumns = input(false, ...ngDevMode ? [{ debugName: "movableColumns" }] : /* istanbul ignore next */ []);
967
+ this.rowFilter = input(void 0, ...ngDevMode ? [{ debugName: "rowFilter" }] : /* istanbul ignore next */ []);
714
968
  this.rowId = input((_row, index) => String(index), ...ngDevMode ? [{ debugName: "rowId" }] : /* istanbul ignore next */ []);
715
969
  this.selectionMode = input("none", ...ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []);
716
970
  this.pageSize = input(void 0, ...ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []);
@@ -728,10 +982,13 @@ var DataGrid = class DataGrid {
728
982
  this.exportScope = input("view", ...ngDevMode ? [{ debugName: "exportScope" }] : /* istanbul ignore next */ []);
729
983
  this.exportFormulas = input(false, ...ngDevMode ? [{ debugName: "exportFormulas" }] : /* istanbul ignore next */ []);
730
984
  this.exportHandler = input(void 0, ...ngDevMode ? [{ debugName: "exportHandler" }] : /* istanbul ignore next */ []);
985
+ this.copyHandler = input(void 0, ...ngDevMode ? [{ debugName: "copyHandler" }] : /* istanbul ignore next */ []);
731
986
  this.virtualize = input(void 0, ...ngDevMode ? [{ debugName: "virtualize" }] : /* istanbul ignore next */ []);
732
987
  this.className = input("", ...ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []);
733
988
  this.selected = model([], ...ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []);
734
989
  this.filter = model("", ...ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []);
990
+ this.filterField = model("", ...ngDevMode ? [{ debugName: "filterField" }] : /* istanbul ignore next */ []);
991
+ this.thenBy = model([], ...ngDevMode ? [{ debugName: "thenBy" }] : /* istanbul ignore next */ []);
735
992
  this.sort = model({
736
993
  columnId: "",
737
994
  direction: "none"
@@ -743,6 +1000,7 @@ var DataGrid = class DataGrid {
743
1000
  this.fileRemove = output();
744
1001
  this.rowActivate = output();
745
1002
  this.exportFile = output();
1003
+ this.copy = output();
746
1004
  this.gridId = nextDataGridId();
747
1005
  this.minColumnWidth = MIN_COLUMN_WIDTH;
748
1006
  this.rootRef = viewChild.required("rootEl", ...ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []);
@@ -787,36 +1045,50 @@ var DataGrid = class DataGrid {
787
1045
  this.resolvedColumns();
788
1046
  return /* @__PURE__ */ new WeakMap();
789
1047
  }, ...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 */ []);
1048
+ this.isMissing = isMissing;
1049
+ this.filterableColumns = computed(() => this.resolvedColumns().filter((c) => c.filterable !== false), ...ngDevMode ? [{ debugName: "filterableColumns" }] : /* istanbul ignore next */ []);
1050
+ this.searchIndex = computed(() => this.entries().map((entry) => this.filterableColumns().map((column) => foldText(column.filterText ? column.filterText(this.cellFor(column, entry.row).value, entry.row) : this.cellFor(column, entry.row).formatted))), ...ngDevMode ? [{ debugName: "searchIndex" }] : /* istanbul ignore next */ []);
794
1051
  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)));
1052
+ const terms = searchTerms(this.filter());
1053
+ const rowFilter = this.rowFilter();
1054
+ if (terms.length === 0 && !rowFilter) return this.entries();
1055
+ const index = terms.length > 0 ? this.searchIndex() : null;
1056
+ const field = this.filterField();
1057
+ const only = field ? this.filterableColumns().findIndex((c) => c.id === field) : -1;
1058
+ const rowText = (i) => only >= 0 ? index[i][only] : index[i].join(" ");
1059
+ return this.entries().filter((entry, i) => (!rowFilter || rowFilter(entry.row)) && (!index || terms.every((term) => rowText(i).includes(term))));
799
1060
  }, ...ngDevMode ? [{ debugName: "filteredEntries" }] : /* istanbul ignore next */ []);
1061
+ this.sortKeys = computed(() => [this.sort(), ...this.thenBy()].map((k) => ({
1062
+ column: this.resolvedColumns().find((c) => c.id === k.columnId),
1063
+ dir: k.direction === "ascending" ? 1 : k.direction === "descending" ? -1 : 0
1064
+ })).filter((k) => !!k.column && k.dir !== 0), ...ngDevMode ? [{ debugName: "sortKeys" }] : /* istanbul ignore next */ []);
800
1065
  this.sortedEntries = computed(() => {
801
- const sort = this.sort();
1066
+ const keys = this.sortKeys();
802
1067
  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;
1068
+ if (keys.length === 0) return filtered;
1069
+ const nulls = this.nulls();
1070
+ const collators = keys.map((k) => {
1071
+ const tag = k.column.locale ?? this.locale();
1072
+ return tag ? new Intl.Collator(tag, { numeric: true }) : null;
1073
+ });
808
1074
  const keyed = filtered.map((entry) => ({
809
1075
  entry,
810
- key: this.cellFor(column, entry.row).value
1076
+ keys: keys.map((k) => this.cellFor(k.column, entry.row).value)
811
1077
  }));
812
1078
  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;
1079
+ for (let i = 0; i < keys.length; i++) {
1080
+ const { column, dir } = keys[i];
1081
+ const av = a.keys[i];
1082
+ const bv = b.keys[i];
1083
+ if (av === bv) continue;
1084
+ const aMissing = av === null || av === void 0;
1085
+ const bMissing = bv === null || bv === void 0;
1086
+ if (aMissing || bMissing) return aMissing === bMissing ? 0 : (aMissing ? 1 : -1) * (nulls === "first" ? -1 : 1);
1087
+ const collator = collators[i];
1088
+ const result = column.compare ? column.compare(av, bv) * dir : collator && typeof av === "string" && typeof bv === "string" ? collator.compare(av, bv) * dir : av > bv ? dir : -dir;
1089
+ if (result !== 0) return result;
1090
+ }
1091
+ return 0;
820
1092
  });
821
1093
  return keyed.map((k) => k.entry);
822
1094
  }, ...ngDevMode ? [{ debugName: "sortedEntries" }] : /* istanbul ignore next */ []);
@@ -889,8 +1161,13 @@ var DataGrid = class DataGrid {
889
1161
  this.exportEntries = computed(() => this.exportScope() === "selected" ? this.sortedEntries().filter((e) => this.selectedSet().has(e.id)) : this.sortedEntries(), ...ngDevMode ? [{ debugName: "exportEntries" }] : /* istanbul ignore next */ []);
890
1162
  this.hasExportButtons = computed(() => {
891
1163
  const l = this.labels();
892
- return !!(l.exportCsv || l.exportTsv || l.exportExcel);
1164
+ return !!(l.exportCsv || l.exportTsv || l.exportExcel || l.copy);
893
1165
  }, ...ngDevMode ? [{ debugName: "hasExportButtons" }] : /* istanbul ignore next */ []);
1166
+ this.history = EMPTY_HISTORY;
1167
+ this.typeahead = {
1168
+ prefix: "",
1169
+ at: 0
1170
+ };
894
1171
  effect(() => {
895
1172
  const root = this.rootRef().nativeElement;
896
1173
  const grid = this.isGrid();
@@ -910,6 +1187,30 @@ var DataGrid = class DataGrid {
910
1187
  else table.removeAttribute("aria-busy");
911
1188
  }, { injector: this.injector });
912
1189
  });
1190
+ const reportedLabels = /* @__PURE__ */ new Set();
1191
+ effect(() => {
1192
+ const labels = this.labels();
1193
+ const columns = this.resolvedColumns();
1194
+ const features = {
1195
+ selection: this.selectionMode() !== "none",
1196
+ pagination: !!this.pageSize(),
1197
+ hidableColumns: columns.some((c) => c.hidable),
1198
+ resizableColumns: columns.some((c) => c.resizable),
1199
+ editing: columns.some((c) => c.editable),
1200
+ sorting: columns.some((c) => c.sortable),
1201
+ search: !!labels.search,
1202
+ exporting: !!(labels.exportCsv || labels.exportTsv || labels.exportExcel || labels.copy)
1203
+ };
1204
+ const warn = this.warnMissingLabels();
1205
+ const env = globalThis.process?.env;
1206
+ if (!warn || env?.["NODE_ENV"] === "production") return;
1207
+ const fresh = missingLabels(features, labels).filter((m) => !reportedLabels.has(m.key));
1208
+ if (fresh.length === 0) return;
1209
+ for (const m of fresh) reportedLabels.add(m.key);
1210
+ const controls = fresh.filter((m) => m.kind === "control").map((m) => m.key);
1211
+ const announcements = fresh.filter((m) => m.kind === "announcement").map((m) => m.key);
1212
+ console.warn(`[data-grid] "${this.label()}": labels left out — ` + [controls.length ? `controls with no name: ${controls.join(", ")}` : "", announcements.length ? `silent announcements: ${announcements.join(", ")}` : ""].filter(Boolean).join("; "));
1213
+ });
913
1214
  effect(() => {
914
1215
  this.pageEntries();
915
1216
  this.visibleColumns();
@@ -943,11 +1244,33 @@ var DataGrid = class DataGrid {
943
1244
  }
944
1245
  ariaSortFor(column) {
945
1246
  if (!column.sortable) return null;
946
- return this.sort().columnId === column.id ? this.sort().direction : "none";
1247
+ if (this.sort().columnId === column.id) return this.sort().direction;
1248
+ return this.thenBy().find((k) => k.columnId === column.id)?.direction ?? "none";
1249
+ }
1250
+ sortOrderFor(column) {
1251
+ const keys = this.sortKeys();
1252
+ if (keys.length < 2) return null;
1253
+ const at = keys.findIndex((k) => k.column.id === column.id);
1254
+ return at === -1 ? null : at + 1;
947
1255
  }
948
- toggleSort(column) {
1256
+ toggleSort(column, additive = false) {
949
1257
  if (!column.sortable) return;
950
1258
  const current = this.sort();
1259
+ if (additive && current.direction !== "none" && current.columnId && current.columnId !== column.id) {
1260
+ const thenBy = this.thenBy();
1261
+ const existing = thenBy.find((k) => k.columnId === column.id);
1262
+ const nextThen = !existing ? [...thenBy, {
1263
+ columnId: column.id,
1264
+ direction: "ascending"
1265
+ }] : existing.direction === "ascending" ? thenBy.map((k) => k.columnId === column.id ? {
1266
+ columnId: column.id,
1267
+ direction: "descending"
1268
+ } : k) : thenBy.filter((k) => k.columnId !== column.id);
1269
+ this.thenBy.set(nextThen);
1270
+ this.announce(this.labels().sortAnnouncement?.(column.header, nextThen.find((k) => k.columnId === column.id)?.direction ?? "none"));
1271
+ return;
1272
+ }
1273
+ if (this.thenBy().length > 0) this.thenBy.set([]);
951
1274
  const next = current.columnId !== column.id ? {
952
1275
  columnId: column.id,
953
1276
  direction: "ascending"
@@ -964,8 +1287,20 @@ var DataGrid = class DataGrid {
964
1287
  this.sort.set(next);
965
1288
  this.announce(this.labels().sortAnnouncement?.(column.header, next.direction));
966
1289
  }
967
- onFilterInput(event) {
968
- this.filter.set(event.target.value);
1290
+ setFilter(value) {
1291
+ this.filter.set(value);
1292
+ this.page.set(1);
1293
+ this.announce(this.labels().filterAnnouncement?.(this.filteredEntries().length, this.rows().length));
1294
+ }
1295
+ onFilterKeydown(event) {
1296
+ const e = event;
1297
+ if (e.key === "Escape" && this.filter() !== "") {
1298
+ e.preventDefault();
1299
+ this.setFilter("");
1300
+ }
1301
+ }
1302
+ onFilterFieldChange(event) {
1303
+ this.filterField.set(event.target.value);
969
1304
  this.page.set(1);
970
1305
  this.announce(this.labels().filterAnnouncement?.(this.filteredEntries().length, this.rows().length));
971
1306
  }
@@ -1125,29 +1460,44 @@ var DataGrid = class DataGrid {
1125
1460
  hasUtcColumn(c) {
1126
1461
  return c.type === "time-zone" && !!c.exportUtc && !!c.referenceDate && !!this.labels().exportUtcHeader;
1127
1462
  }
1128
- runExport(format) {
1463
+ buildExportTable(format) {
1129
1464
  const live = (c) => format === "xlsx" && this.exportFormulas() && c.type === "formula" && !!c.source;
1130
1465
  const headers = [];
1131
1466
  for (const c of this.exportColumns()) {
1132
1467
  headers.push(c.header);
1133
1468
  if (this.hasUtcColumn(c)) headers.push(this.labels().exportUtcHeader(c.header));
1134
1469
  }
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 = {
1470
+ return {
1148
1471
  headers,
1149
- rows
1472
+ rows: this.exportEntries().map((entry) => {
1473
+ const cells = [];
1474
+ for (const c of this.exportColumns()) {
1475
+ const expression = live(c) ? c.source(entry.row) : void 0;
1476
+ cells.push(typeof expression === "string" && expression !== "" && !c.errorOf?.(entry.row) ? {
1477
+ formula: expression,
1478
+ result: this.exportCell(c, entry.row)
1479
+ } : this.exportCell(c, entry.row));
1480
+ if (this.hasUtcColumn(c)) cells.push(zonedUtcTime(this.cellFor(c, entry.row).value, c.referenceDate));
1481
+ }
1482
+ return cells;
1483
+ })
1484
+ };
1485
+ }
1486
+ async runCopy() {
1487
+ const table = this.buildExportTable("tsv");
1488
+ const text = toClipboardText(table);
1489
+ const copied = {
1490
+ text,
1491
+ rowCount: table.rows.length
1150
1492
  };
1493
+ this.copy.emit(copied);
1494
+ if (this.copyHandler()?.(copied) === false) return;
1495
+ const ok = await copyToClipboard(text);
1496
+ this.announce(ok ? this.labels().copyAnnouncement?.(table.rows.length) : this.labels().copyFailed);
1497
+ }
1498
+ runExport(format) {
1499
+ const table = this.buildExportTable(format);
1500
+ const rows = table.rows;
1151
1501
  const name = this.exportFilename();
1152
1502
  const filename = exportFilename(name, format);
1153
1503
  const blob = exportBlob(table, format, { sheetName: name });
@@ -1235,6 +1585,26 @@ var DataGrid = class DataGrid {
1235
1585
  if (column.editor === "date" || column.editor === "time" || column.editor === "select") return draft === "" ? null : draft;
1236
1586
  return draft;
1237
1587
  }
1588
+ stepHistory(direction) {
1589
+ const step = direction === "undo" ? undoEdit(this.history) : redoEdit(this.history);
1590
+ if (!step.record) return;
1591
+ this.history = step.history;
1592
+ const { ownerId, fieldId, value, previous } = step.record;
1593
+ const entry = this.entries().find((e) => e.id === ownerId);
1594
+ const column = this.resolvedColumns().find((c) => c.id === fieldId);
1595
+ if (!entry || !column) return;
1596
+ const restored = direction === "undo" ? previous : value;
1597
+ const replaced = direction === "undo" ? value : previous;
1598
+ this.cellEdit.emit({
1599
+ rowId: entry.id,
1600
+ row: entry.row,
1601
+ columnId: column.id,
1602
+ value: restored,
1603
+ previous: replaced
1604
+ });
1605
+ const text = column.format ? column.format(restored, entry.row) : String(restored ?? "");
1606
+ this.announce((direction === "undo" ? this.labels().undoAnnouncement : this.labels().redoAnnouncement)?.(column.header, text));
1607
+ }
1238
1608
  isStructurallyEqual(a, b) {
1239
1609
  return typeof a === "object" && typeof b === "object" && a !== null && b !== null && JSON.stringify(a) === JSON.stringify(b);
1240
1610
  }
@@ -1255,6 +1625,12 @@ var DataGrid = class DataGrid {
1255
1625
  const previous = column.type === "formula" && column.source ? column.source(entry.row) : defaultAccessor(column, entry.row);
1256
1626
  this.editingState.set(null);
1257
1627
  if (value !== previous && !this.isStructurallyEqual(value, previous)) {
1628
+ this.history = recordEdit(this.history, {
1629
+ ownerId: entry.id,
1630
+ fieldId: column.id,
1631
+ value,
1632
+ previous
1633
+ });
1258
1634
  this.cellEdit.emit({
1259
1635
  rowId: entry.id,
1260
1636
  row: entry.row,
@@ -1423,51 +1799,51 @@ var DataGrid = class DataGrid {
1423
1799
  const activeRow = this.activeRow();
1424
1800
  const activeCol = this.activeCol();
1425
1801
  const colOffset = this.colOffset();
1426
- if (e.altKey && activeRow === -1 && (e.key === "ArrowRight" || e.key === "ArrowLeft")) {
1802
+ const key = logicalKey(e.key, isRtl(cell));
1803
+ if (e.altKey && activeRow === -1 && (key === "ArrowRight" || key === "ArrowLeft")) {
1427
1804
  const column = this.visibleColumns()[activeCol - colOffset];
1428
- if (column?.movable) {
1805
+ if (column && this.isMovable(column)) {
1429
1806
  e.preventDefault();
1430
- const delta = e.key === "ArrowRight" ? 1 : -1;
1807
+ const delta = key === "ArrowRight" ? 1 : -1;
1431
1808
  if (this.moveColumn(column.id, activeCol - colOffset + delta)) this.moveFocus(-1, activeCol + delta);
1432
1809
  return;
1433
1810
  }
1434
1811
  }
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;
1812
+ const historyAction = target === cell && this.resolvedColumns().some((c) => c.editable) ? historyKey(e) : null;
1813
+ if (historyAction) {
1814
+ e.preventDefault();
1815
+ this.stepHistory(historyAction);
1816
+ return;
1817
+ }
1818
+ if (isTypeaheadKey(e) && target === cell && activeRow >= 0) {
1819
+ const column = this.visibleColumns()[activeCol - colOffset];
1820
+ if (column) {
1821
+ const state = nextTypeaheadState(this.typeahead, e.key, Date.now(), 700);
1822
+ const hit = findTypeaheadMatch(this.pageEntries().map((entry) => this.cellFor(column, entry.row).formatted), activeRow, state.prefix);
1823
+ if (hit !== -1) {
1824
+ this.typeahead = state;
1825
+ e.preventDefault();
1826
+ this.moveFocus(hit, activeCol);
1827
+ return;
1828
+ }
1829
+ }
1830
+ }
1831
+ const move = gridMove(e, {
1832
+ row: activeRow,
1833
+ col: activeCol
1834
+ }, {
1835
+ firstRow: -1,
1836
+ lastRow: this.pageEntries().length - 1,
1837
+ lastCol: this.lastCol(),
1838
+ pageStep: this.pageSize() ?? 10,
1839
+ rtl: isRtl(cell)
1840
+ });
1841
+ if (move) {
1842
+ e.preventDefault();
1843
+ this.moveFocus(move.row, move.col);
1844
+ return;
1845
+ }
1846
+ switch (key) {
1471
1847
  case "F2": {
1472
1848
  const entry = this.pageEntries()[Number(cell.dataset["row"])];
1473
1849
  const column = this.visibleColumns()[Number(cell.dataset["col"]) - colOffset];
@@ -1604,6 +1980,41 @@ var DataGrid = class DataGrid {
1604
1980
  isRequired: false,
1605
1981
  transformFunction: null
1606
1982
  },
1983
+ emptyValue: {
1984
+ classPropertyName: "emptyValue",
1985
+ publicName: "emptyValue",
1986
+ isSignal: true,
1987
+ isRequired: false,
1988
+ transformFunction: null
1989
+ },
1990
+ warnMissingLabels: {
1991
+ classPropertyName: "warnMissingLabels",
1992
+ publicName: "warnMissingLabels",
1993
+ isSignal: true,
1994
+ isRequired: false,
1995
+ transformFunction: null
1996
+ },
1997
+ nulls: {
1998
+ classPropertyName: "nulls",
1999
+ publicName: "nulls",
2000
+ isSignal: true,
2001
+ isRequired: false,
2002
+ transformFunction: null
2003
+ },
2004
+ movableColumns: {
2005
+ classPropertyName: "movableColumns",
2006
+ publicName: "movableColumns",
2007
+ isSignal: true,
2008
+ isRequired: false,
2009
+ transformFunction: null
2010
+ },
2011
+ rowFilter: {
2012
+ classPropertyName: "rowFilter",
2013
+ publicName: "rowFilter",
2014
+ isSignal: true,
2015
+ isRequired: false,
2016
+ transformFunction: null
2017
+ },
1607
2018
  rowId: {
1608
2019
  classPropertyName: "rowId",
1609
2020
  publicName: "rowId",
@@ -1723,6 +2134,13 @@ var DataGrid = class DataGrid {
1723
2134
  isRequired: false,
1724
2135
  transformFunction: null
1725
2136
  },
2137
+ copyHandler: {
2138
+ classPropertyName: "copyHandler",
2139
+ publicName: "copyHandler",
2140
+ isSignal: true,
2141
+ isRequired: false,
2142
+ transformFunction: null
2143
+ },
1726
2144
  virtualize: {
1727
2145
  classPropertyName: "virtualize",
1728
2146
  publicName: "virtualize",
@@ -1751,6 +2169,20 @@ var DataGrid = class DataGrid {
1751
2169
  isRequired: false,
1752
2170
  transformFunction: null
1753
2171
  },
2172
+ filterField: {
2173
+ classPropertyName: "filterField",
2174
+ publicName: "filterField",
2175
+ isSignal: true,
2176
+ isRequired: false,
2177
+ transformFunction: null
2178
+ },
2179
+ thenBy: {
2180
+ classPropertyName: "thenBy",
2181
+ publicName: "thenBy",
2182
+ isSignal: true,
2183
+ isRequired: false,
2184
+ transformFunction: null
2185
+ },
1754
2186
  sort: {
1755
2187
  classPropertyName: "sort",
1756
2188
  publicName: "sort",
@@ -1776,6 +2208,8 @@ var DataGrid = class DataGrid {
1776
2208
  outputs: {
1777
2209
  selected: "selectedChange",
1778
2210
  filter: "filterChange",
2211
+ filterField: "filterFieldChange",
2212
+ thenBy: "thenByChange",
1779
2213
  sort: "sortChange",
1780
2214
  columnOrder: "columnOrderChange",
1781
2215
  page: "pageChange",
@@ -1783,7 +2217,8 @@ var DataGrid = class DataGrid {
1783
2217
  filesSelected: "filesSelected",
1784
2218
  fileRemove: "fileRemove",
1785
2219
  rowActivate: "rowActivate",
1786
- exportFile: "exportFile"
2220
+ exportFile: "exportFile",
2221
+ copy: "copy"
1787
2222
  },
1788
2223
  viewQueries: [{
1789
2224
  propertyName: "rootRef",
@@ -1803,11 +2238,22 @@ var DataGrid = class DataGrid {
1803
2238
  <div #rootEl class="data-grid {{ className() }}">
1804
2239
  @if (labels().search || hidableColumns().length > 0 || hasExportButtons()) {
1805
2240
  <div class="data-grid-toolbar">
2241
+ @if (labels().search && labels().searchField && labels().searchAllFields) {
2242
+ <select class="data-grid-search-field" [attr.aria-label]="labels().searchField" [value]="filterField()" (change)="onFilterFieldChange($event)">
2243
+ <option value="">{{ labels().searchAllFields }}</option>
2244
+ @for (c of filterableColumns(); track c.id) {
2245
+ <option [value]="c.id">{{ c.header }}</option>
2246
+ }
2247
+ </select>
2248
+ }
1806
2249
  @if (labels().search; as searchLabel) {
1807
- <input class="data-grid-search" type="search" [attr.aria-label]="searchLabel" [value]="filter()" (input)="onFilterInput($event)" />
2250
+ <lily-search-input className="data-grid-search" [label]="searchLabel" [value]="filter()" (valueChange)="setFilter($event)" (keydown)="onFilterKeydown($event)" />
1808
2251
  }
1809
2252
  @if (hasExportButtons()) {
1810
2253
  <div class="data-grid-export" role="group" [attr.aria-label]="labels().export ?? null">
2254
+ @if (labels().copy; as copy) {
2255
+ <button type="button" class="data-grid-export-button" data-grid-export-format="copy" [disabled]="exportEntries().length === 0" (click)="runCopy()">{{ copy }}</button>
2256
+ }
1811
2257
  @if (labels().exportCsv; as csv) {
1812
2258
  <button type="button" class="data-grid-export-button" data-grid-export-format="csv" [disabled]="exportEntries().length === 0" (click)="runExport('csv')">{{ csv }}</button>
1813
2259
  }
@@ -1851,16 +2297,26 @@ var DataGrid = class DataGrid {
1851
2297
  }
1852
2298
 
1853
2299
  @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>
2300
+ @if (labels().pagination; as pagination) {
2301
+ <lily-pagination-nav className="data-grid-pagination" [label]="pagination">
2302
+ <ng-container *ngTemplateOutlet="pager" />
2303
+ </lily-pagination-nav>
2304
+ } @else {
2305
+ <div class="data-grid-pagination">
2306
+ <ng-container *ngTemplateOutlet="pager" />
2307
+ </div>
2308
+ }
1859
2309
  }
1860
2310
 
1861
2311
  <p class="data-grid-status" aria-live="polite">{{ statusMessage() }}</p>
1862
2312
  </div>
1863
2313
 
2314
+ <ng-template #pager>
2315
+ <button type="button" class="data-grid-page-previous" [disabled]="clampedPage() <= 1" (click)="goToPage(clampedPage() - 1)">{{ labels().previousPage }}</button>
2316
+ <span class="data-grid-page-status">{{ labels().pageStatus?.(clampedPage(), pageCount(), sortedEntries().length) }}</span>
2317
+ <button type="button" class="data-grid-page-next" [disabled]="clampedPage() >= pageCount()" (click)="goToPage(clampedPage() + 1)">{{ labels().nextPage }}</button>
2318
+ </ng-template>
2319
+
1864
2320
  <ng-template #tableTemplate>
1865
2321
  <lily-data-table
1866
2322
  [label]="label()"
@@ -1908,15 +2364,16 @@ var DataGrid = class DataGrid {
1908
2364
  [attr.data-pinned]="column.pinned ?? null"
1909
2365
  [attr.tabindex]="isGrid() ? (activeRow() === -1 && activeCol() === colOffset() + i ? 0 : -1) : null"
1910
2366
  [attr.aria-sort]="ariaSortFor(column)"
2367
+ [attr.data-sort-order]="sortOrderFor(column)"
1911
2368
  [attr.aria-describedby]="column.type === 'rollup' && labels().rollupDescription ? gridId + '-' + column.id + '-description' : null"
1912
2369
  [attr.style]="cellStyle(column)"
1913
- [attr.draggable]="column.movable ? 'true' : null"
2370
+ [attr.draggable]="isMovable(column) ? 'true' : null"
1914
2371
  (dragstart)="onHeaderDragStart(column, $event)"
1915
2372
  (dragover)="onHeaderDragOver($event)"
1916
2373
  (drop)="onHeaderDrop(column.id, $event)"
1917
2374
  >
1918
2375
  @if (column.sortable) {
1919
- <button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column)">
2376
+ <button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column, $any($event).shiftKey)">
1920
2377
  @if (column.headerCell; as headerTemplate) {
1921
2378
  <ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: column }" />
1922
2379
  } @else {<ng-container>{{ column.header }}</ng-container>}
@@ -1993,6 +2450,7 @@ var DataGrid = class DataGrid {
1993
2450
  [attr.data-cell-type]="column.type ?? null"
1994
2451
  [attr.data-pinned]="column.pinned ?? null"
1995
2452
  [attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
2453
+ [attr.data-empty]="isMissing(ctx.value) ? '' : null"
1996
2454
  [attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
1997
2455
  [attr.style]="cellStyle(column)"
1998
2456
  [lilyDataGridAttrs]="column.cellAttrs?.(ctx)"
@@ -2013,6 +2471,7 @@ var DataGrid = class DataGrid {
2013
2471
  [attr.data-readonly]="column.type === 'rollup' ? '' : null"
2014
2472
  [attr.aria-readonly]="column.type === 'rollup' ? 'true' : null"
2015
2473
  [attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
2474
+ [attr.data-empty]="isMissing(ctx.value) ? '' : null"
2016
2475
  [attr.data-editing]="isEditingCell(entry, column) ? '' : null"
2017
2476
  [attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
2018
2477
  [attr.style]="cellStyle(column)"
@@ -2136,6 +2595,8 @@ var DataGrid = class DataGrid {
2136
2595
  } @else {
2137
2596
  @if (column.cell; as cellTemplate) {
2138
2597
  <ng-container *ngTemplateOutlet="cellTemplate; context: { $implicit: ctx }" />
2598
+ } @else if (isMissing(ctx.value) && (column.emptyValue ?? emptyValue())) {
2599
+ <ng-container>{{ column.emptyValue ?? emptyValue() }}</ng-container>
2139
2600
  } @else if (column.type === 'person') {
2140
2601
  @let people = peopleIn(ctx.value);
2141
2602
  @if (people.length > 0) {
@@ -2280,6 +2741,25 @@ var DataGrid = class DataGrid {
2280
2741
  type: DataTableTH,
2281
2742
  selector: "th[lily-data-table-th]",
2282
2743
  inputs: ["scope", "className"]
2744
+ },
2745
+ {
2746
+ kind: "component",
2747
+ type: PaginationNav,
2748
+ selector: "lily-pagination-nav",
2749
+ inputs: ["label", "className"]
2750
+ },
2751
+ {
2752
+ kind: "component",
2753
+ type: SearchInput,
2754
+ selector: "lily-search-input",
2755
+ inputs: [
2756
+ "label",
2757
+ "className",
2758
+ "value",
2759
+ "required",
2760
+ "disabled"
2761
+ ],
2762
+ outputs: ["valueChange"]
2283
2763
  }
2284
2764
  ],
2285
2765
  changeDetection: i0.ChangeDetectionStrategy.OnPush
@@ -2305,18 +2785,31 @@ i0.ɵɵngDeclareClassMetadata({
2305
2785
  DataTableHead,
2306
2786
  DataTableRow,
2307
2787
  DataTableTD,
2308
- DataTableTH
2788
+ DataTableTH,
2789
+ PaginationNav,
2790
+ SearchInput
2309
2791
  ],
2310
2792
  changeDetection: ChangeDetectionStrategy.OnPush,
2311
2793
  template: `
2312
2794
  <div #rootEl class="data-grid {{ className() }}">
2313
2795
  @if (labels().search || hidableColumns().length > 0 || hasExportButtons()) {
2314
2796
  <div class="data-grid-toolbar">
2797
+ @if (labels().search && labels().searchField && labels().searchAllFields) {
2798
+ <select class="data-grid-search-field" [attr.aria-label]="labels().searchField" [value]="filterField()" (change)="onFilterFieldChange($event)">
2799
+ <option value="">{{ labels().searchAllFields }}</option>
2800
+ @for (c of filterableColumns(); track c.id) {
2801
+ <option [value]="c.id">{{ c.header }}</option>
2802
+ }
2803
+ </select>
2804
+ }
2315
2805
  @if (labels().search; as searchLabel) {
2316
- <input class="data-grid-search" type="search" [attr.aria-label]="searchLabel" [value]="filter()" (input)="onFilterInput($event)" />
2806
+ <lily-search-input className="data-grid-search" [label]="searchLabel" [value]="filter()" (valueChange)="setFilter($event)" (keydown)="onFilterKeydown($event)" />
2317
2807
  }
2318
2808
  @if (hasExportButtons()) {
2319
2809
  <div class="data-grid-export" role="group" [attr.aria-label]="labels().export ?? null">
2810
+ @if (labels().copy; as copy) {
2811
+ <button type="button" class="data-grid-export-button" data-grid-export-format="copy" [disabled]="exportEntries().length === 0" (click)="runCopy()">{{ copy }}</button>
2812
+ }
2320
2813
  @if (labels().exportCsv; as csv) {
2321
2814
  <button type="button" class="data-grid-export-button" data-grid-export-format="csv" [disabled]="exportEntries().length === 0" (click)="runExport('csv')">{{ csv }}</button>
2322
2815
  }
@@ -2360,16 +2853,26 @@ i0.ɵɵngDeclareClassMetadata({
2360
2853
  }
2361
2854
 
2362
2855
  @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>
2856
+ @if (labels().pagination; as pagination) {
2857
+ <lily-pagination-nav className="data-grid-pagination" [label]="pagination">
2858
+ <ng-container *ngTemplateOutlet="pager" />
2859
+ </lily-pagination-nav>
2860
+ } @else {
2861
+ <div class="data-grid-pagination">
2862
+ <ng-container *ngTemplateOutlet="pager" />
2863
+ </div>
2864
+ }
2368
2865
  }
2369
2866
 
2370
2867
  <p class="data-grid-status" aria-live="polite">{{ statusMessage() }}</p>
2371
2868
  </div>
2372
2869
 
2870
+ <ng-template #pager>
2871
+ <button type="button" class="data-grid-page-previous" [disabled]="clampedPage() <= 1" (click)="goToPage(clampedPage() - 1)">{{ labels().previousPage }}</button>
2872
+ <span class="data-grid-page-status">{{ labels().pageStatus?.(clampedPage(), pageCount(), sortedEntries().length) }}</span>
2873
+ <button type="button" class="data-grid-page-next" [disabled]="clampedPage() >= pageCount()" (click)="goToPage(clampedPage() + 1)">{{ labels().nextPage }}</button>
2874
+ </ng-template>
2875
+
2373
2876
  <ng-template #tableTemplate>
2374
2877
  <lily-data-table
2375
2878
  [label]="label()"
@@ -2417,15 +2920,16 @@ i0.ɵɵngDeclareClassMetadata({
2417
2920
  [attr.data-pinned]="column.pinned ?? null"
2418
2921
  [attr.tabindex]="isGrid() ? (activeRow() === -1 && activeCol() === colOffset() + i ? 0 : -1) : null"
2419
2922
  [attr.aria-sort]="ariaSortFor(column)"
2923
+ [attr.data-sort-order]="sortOrderFor(column)"
2420
2924
  [attr.aria-describedby]="column.type === 'rollup' && labels().rollupDescription ? gridId + '-' + column.id + '-description' : null"
2421
2925
  [attr.style]="cellStyle(column)"
2422
- [attr.draggable]="column.movable ? 'true' : null"
2926
+ [attr.draggable]="isMovable(column) ? 'true' : null"
2423
2927
  (dragstart)="onHeaderDragStart(column, $event)"
2424
2928
  (dragover)="onHeaderDragOver($event)"
2425
2929
  (drop)="onHeaderDrop(column.id, $event)"
2426
2930
  >
2427
2931
  @if (column.sortable) {
2428
- <button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column)">
2932
+ <button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column, $any($event).shiftKey)">
2429
2933
  @if (column.headerCell; as headerTemplate) {
2430
2934
  <ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: column }" />
2431
2935
  } @else {<ng-container>{{ column.header }}</ng-container>}
@@ -2502,6 +3006,7 @@ i0.ɵɵngDeclareClassMetadata({
2502
3006
  [attr.data-cell-type]="column.type ?? null"
2503
3007
  [attr.data-pinned]="column.pinned ?? null"
2504
3008
  [attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
3009
+ [attr.data-empty]="isMissing(ctx.value) ? '' : null"
2505
3010
  [attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
2506
3011
  [attr.style]="cellStyle(column)"
2507
3012
  [lilyDataGridAttrs]="column.cellAttrs?.(ctx)"
@@ -2522,6 +3027,7 @@ i0.ɵɵngDeclareClassMetadata({
2522
3027
  [attr.data-readonly]="column.type === 'rollup' ? '' : null"
2523
3028
  [attr.aria-readonly]="column.type === 'rollup' ? 'true' : null"
2524
3029
  [attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
3030
+ [attr.data-empty]="isMissing(ctx.value) ? '' : null"
2525
3031
  [attr.data-editing]="isEditingCell(entry, column) ? '' : null"
2526
3032
  [attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
2527
3033
  [attr.style]="cellStyle(column)"
@@ -2645,6 +3151,8 @@ i0.ɵɵngDeclareClassMetadata({
2645
3151
  } @else {
2646
3152
  @if (column.cell; as cellTemplate) {
2647
3153
  <ng-container *ngTemplateOutlet="cellTemplate; context: { $implicit: ctx }" />
3154
+ } @else if (isMissing(ctx.value) && (column.emptyValue ?? emptyValue())) {
3155
+ <ng-container>{{ column.emptyValue ?? emptyValue() }}</ng-container>
2648
3156
  } @else if (column.type === 'person') {
2649
3157
  @let people = peopleIn(ctx.value);
2650
3158
  @if (people.length > 0) {
@@ -2774,6 +3282,46 @@ i0.ɵɵngDeclareClassMetadata({
2774
3282
  required: false
2775
3283
  }]
2776
3284
  }],
3285
+ emptyValue: [{
3286
+ type: i0.Input,
3287
+ args: [{
3288
+ isSignal: true,
3289
+ alias: "emptyValue",
3290
+ required: false
3291
+ }]
3292
+ }],
3293
+ warnMissingLabels: [{
3294
+ type: i0.Input,
3295
+ args: [{
3296
+ isSignal: true,
3297
+ alias: "warnMissingLabels",
3298
+ required: false
3299
+ }]
3300
+ }],
3301
+ nulls: [{
3302
+ type: i0.Input,
3303
+ args: [{
3304
+ isSignal: true,
3305
+ alias: "nulls",
3306
+ required: false
3307
+ }]
3308
+ }],
3309
+ movableColumns: [{
3310
+ type: i0.Input,
3311
+ args: [{
3312
+ isSignal: true,
3313
+ alias: "movableColumns",
3314
+ required: false
3315
+ }]
3316
+ }],
3317
+ rowFilter: [{
3318
+ type: i0.Input,
3319
+ args: [{
3320
+ isSignal: true,
3321
+ alias: "rowFilter",
3322
+ required: false
3323
+ }]
3324
+ }],
2777
3325
  rowId: [{
2778
3326
  type: i0.Input,
2779
3327
  args: [{
@@ -2910,6 +3458,14 @@ i0.ɵɵngDeclareClassMetadata({
2910
3458
  required: false
2911
3459
  }]
2912
3460
  }],
3461
+ copyHandler: [{
3462
+ type: i0.Input,
3463
+ args: [{
3464
+ isSignal: true,
3465
+ alias: "copyHandler",
3466
+ required: false
3467
+ }]
3468
+ }],
2913
3469
  virtualize: [{
2914
3470
  type: i0.Input,
2915
3471
  args: [{
@@ -2948,6 +3504,28 @@ i0.ɵɵngDeclareClassMetadata({
2948
3504
  type: i0.Output,
2949
3505
  args: ["filterChange"]
2950
3506
  }],
3507
+ filterField: [{
3508
+ type: i0.Input,
3509
+ args: [{
3510
+ isSignal: true,
3511
+ alias: "filterField",
3512
+ required: false
3513
+ }]
3514
+ }, {
3515
+ type: i0.Output,
3516
+ args: ["filterFieldChange"]
3517
+ }],
3518
+ thenBy: [{
3519
+ type: i0.Input,
3520
+ args: [{
3521
+ isSignal: true,
3522
+ alias: "thenBy",
3523
+ required: false
3524
+ }]
3525
+ }, {
3526
+ type: i0.Output,
3527
+ args: ["thenByChange"]
3528
+ }],
2951
3529
  sort: [{
2952
3530
  type: i0.Input,
2953
3531
  args: [{
@@ -3001,6 +3579,10 @@ i0.ɵɵngDeclareClassMetadata({
3001
3579
  type: i0.Output,
3002
3580
  args: ["exportFile"]
3003
3581
  }],
3582
+ copy: [{
3583
+ type: i0.Output,
3584
+ args: ["copy"]
3585
+ }],
3004
3586
  rootRef: [{
3005
3587
  type: i0.ViewChild,
3006
3588
  args: ["rootEl", { isSignal: true }]
@@ -3011,6 +3593,6 @@ i0.ɵɵngDeclareClassMetadata({
3011
3593
  }]
3012
3594
  }
3013
3595
  });
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 };
3596
+ export { ANNOUNCEMENT_LABELS, CONTROL_LABELS, DataGrid, DataGridAttrs, EMPTY_HISTORY, HISTORY_LIMIT, MIME_TYPES, TYPEAHEAD_RESET_MS, aggregateValues, cellText, civilDateToDate, columnName, copyToClipboard, crc32, downloadBlob, exportBlob, exportFilename, filesOf, findTypeaheadMatch, foldText, highlightSegments, historyKey, isCivilDate, isCivilTime, isExportFormula, isInvalidForType, isTypeaheadKey, isValidTimeZone, isZonedTime, linkLabel, linkParts, matchesSearch, missingLabels, nextDataGridId, nextTypeaheadState, peopleOf, personName, recordEdit, redoEdit, requiredLabels, resolveColumn, safeHref, searchTerms, toClipboardText, toCsv, toDelimited, toTsv, toXlsx, undoEdit, zipStore, zonedMinutes, zonedUtcTime };
3015
3597
 
3016
3598
  //# sourceMappingURL=lilydesignsystem-angular-data-grid.mjs.map