@lilydesignsystem/angular-data-grid 0.9.0 → 0.11.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,25 @@ var DataGrid = class DataGrid {
705
951
  }
706
952
  return ctx;
707
953
  }
954
+ isMovable(column) {
955
+ return column.movable ?? this.movableColumns();
956
+ }
957
+ footerAggregate(columnId, kind) {
958
+ const column = this.resolvedColumns().find((c) => c.id === columnId);
959
+ if (!column) return void 0;
960
+ return aggregateValues(kind, this.sortedEntries().map((e) => defaultAccessor(column, e.row)));
961
+ }
708
962
  constructor() {
709
963
  this.label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
710
964
  this.caption = input(void 0, ...ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []);
711
965
  this.columns = input.required(...ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []);
712
966
  this.rows = input.required(...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
713
967
  this.locale = input(void 0, ...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
968
+ this.emptyValue = input(void 0, ...ngDevMode ? [{ debugName: "emptyValue" }] : /* istanbul ignore next */ []);
969
+ this.warnMissingLabels = input(true, ...ngDevMode ? [{ debugName: "warnMissingLabels" }] : /* istanbul ignore next */ []);
970
+ this.nulls = input("last", ...ngDevMode ? [{ debugName: "nulls" }] : /* istanbul ignore next */ []);
971
+ this.movableColumns = input(false, ...ngDevMode ? [{ debugName: "movableColumns" }] : /* istanbul ignore next */ []);
972
+ this.rowFilter = input(void 0, ...ngDevMode ? [{ debugName: "rowFilter" }] : /* istanbul ignore next */ []);
714
973
  this.rowId = input((_row, index) => String(index), ...ngDevMode ? [{ debugName: "rowId" }] : /* istanbul ignore next */ []);
715
974
  this.selectionMode = input("none", ...ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []);
716
975
  this.pageSize = input(void 0, ...ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []);
@@ -728,10 +987,13 @@ var DataGrid = class DataGrid {
728
987
  this.exportScope = input("view", ...ngDevMode ? [{ debugName: "exportScope" }] : /* istanbul ignore next */ []);
729
988
  this.exportFormulas = input(false, ...ngDevMode ? [{ debugName: "exportFormulas" }] : /* istanbul ignore next */ []);
730
989
  this.exportHandler = input(void 0, ...ngDevMode ? [{ debugName: "exportHandler" }] : /* istanbul ignore next */ []);
990
+ this.copyHandler = input(void 0, ...ngDevMode ? [{ debugName: "copyHandler" }] : /* istanbul ignore next */ []);
731
991
  this.virtualize = input(void 0, ...ngDevMode ? [{ debugName: "virtualize" }] : /* istanbul ignore next */ []);
732
992
  this.className = input("", ...ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []);
733
993
  this.selected = model([], ...ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []);
734
994
  this.filter = model("", ...ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []);
995
+ this.filterField = model("", ...ngDevMode ? [{ debugName: "filterField" }] : /* istanbul ignore next */ []);
996
+ this.thenBy = model([], ...ngDevMode ? [{ debugName: "thenBy" }] : /* istanbul ignore next */ []);
735
997
  this.sort = model({
736
998
  columnId: "",
737
999
  direction: "none"
@@ -743,6 +1005,7 @@ var DataGrid = class DataGrid {
743
1005
  this.fileRemove = output();
744
1006
  this.rowActivate = output();
745
1007
  this.exportFile = output();
1008
+ this.copy = output();
746
1009
  this.gridId = nextDataGridId();
747
1010
  this.minColumnWidth = MIN_COLUMN_WIDTH;
748
1011
  this.rootRef = viewChild.required("rootEl", ...ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []);
@@ -787,36 +1050,50 @@ var DataGrid = class DataGrid {
787
1050
  this.resolvedColumns();
788
1051
  return /* @__PURE__ */ new WeakMap();
789
1052
  }, ...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 */ []);
1053
+ this.isMissing = isMissing;
1054
+ this.filterableColumns = computed(() => this.resolvedColumns().filter((c) => c.filterable !== false), ...ngDevMode ? [{ debugName: "filterableColumns" }] : /* istanbul ignore next */ []);
1055
+ 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
1056
  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)));
1057
+ const terms = searchTerms(this.filter());
1058
+ const rowFilter = this.rowFilter();
1059
+ if (terms.length === 0 && !rowFilter) return this.entries();
1060
+ const index = terms.length > 0 ? this.searchIndex() : null;
1061
+ const field = this.filterField();
1062
+ const only = field ? this.filterableColumns().findIndex((c) => c.id === field) : -1;
1063
+ const rowText = (i) => only >= 0 ? index[i][only] : index[i].join(" ");
1064
+ return this.entries().filter((entry, i) => (!rowFilter || rowFilter(entry.row)) && (!index || terms.every((term) => rowText(i).includes(term))));
799
1065
  }, ...ngDevMode ? [{ debugName: "filteredEntries" }] : /* istanbul ignore next */ []);
1066
+ this.sortKeys = computed(() => [this.sort(), ...this.thenBy()].map((k) => ({
1067
+ column: this.resolvedColumns().find((c) => c.id === k.columnId),
1068
+ dir: k.direction === "ascending" ? 1 : k.direction === "descending" ? -1 : 0
1069
+ })).filter((k) => !!k.column && k.dir !== 0), ...ngDevMode ? [{ debugName: "sortKeys" }] : /* istanbul ignore next */ []);
800
1070
  this.sortedEntries = computed(() => {
801
- const sort = this.sort();
1071
+ const keys = this.sortKeys();
802
1072
  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;
1073
+ if (keys.length === 0) return filtered;
1074
+ const nulls = this.nulls();
1075
+ const collators = keys.map((k) => {
1076
+ const tag = k.column.locale ?? this.locale();
1077
+ return tag ? new Intl.Collator(tag, { numeric: true }) : null;
1078
+ });
808
1079
  const keyed = filtered.map((entry) => ({
809
1080
  entry,
810
- key: this.cellFor(column, entry.row).value
1081
+ keys: keys.map((k) => this.cellFor(k.column, entry.row).value)
811
1082
  }));
812
1083
  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;
1084
+ for (let i = 0; i < keys.length; i++) {
1085
+ const { column, dir } = keys[i];
1086
+ const av = a.keys[i];
1087
+ const bv = b.keys[i];
1088
+ if (av === bv) continue;
1089
+ const aMissing = av === null || av === void 0;
1090
+ const bMissing = bv === null || bv === void 0;
1091
+ if (aMissing || bMissing) return aMissing === bMissing ? 0 : (aMissing ? 1 : -1) * (nulls === "first" ? -1 : 1);
1092
+ const collator = collators[i];
1093
+ 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;
1094
+ if (result !== 0) return result;
1095
+ }
1096
+ return 0;
820
1097
  });
821
1098
  return keyed.map((k) => k.entry);
822
1099
  }, ...ngDevMode ? [{ debugName: "sortedEntries" }] : /* istanbul ignore next */ []);
@@ -883,14 +1160,21 @@ var DataGrid = class DataGrid {
883
1160
  this.someSelected = computed(() => !this.allSelected() && this.sortedEntries().some((e) => this.selectedSet().has(e.id)), ...ngDevMode ? [{ debugName: "someSelected" }] : /* istanbul ignore next */ []);
884
1161
  this.footerContext = computed(() => ({
885
1162
  columns: this.visibleColumns(),
886
- rows: this.pageEntries().map((e) => e.row)
1163
+ rows: this.pageEntries().map((e) => e.row),
1164
+ allRows: this.sortedEntries().map((e) => e.row),
1165
+ aggregate: (columnId, kind) => this.footerAggregate(columnId, kind)
887
1166
  }), ...ngDevMode ? [{ debugName: "footerContext" }] : /* istanbul ignore next */ []);
888
1167
  this.exportColumns = computed(() => this.visibleColumns().filter((c) => c.exportable !== false), ...ngDevMode ? [{ debugName: "exportColumns" }] : /* istanbul ignore next */ []);
889
1168
  this.exportEntries = computed(() => this.exportScope() === "selected" ? this.sortedEntries().filter((e) => this.selectedSet().has(e.id)) : this.sortedEntries(), ...ngDevMode ? [{ debugName: "exportEntries" }] : /* istanbul ignore next */ []);
890
1169
  this.hasExportButtons = computed(() => {
891
1170
  const l = this.labels();
892
- return !!(l.exportCsv || l.exportTsv || l.exportExcel);
1171
+ return !!(l.exportCsv || l.exportTsv || l.exportExcel || l.copy);
893
1172
  }, ...ngDevMode ? [{ debugName: "hasExportButtons" }] : /* istanbul ignore next */ []);
1173
+ this.history = EMPTY_HISTORY;
1174
+ this.typeahead = {
1175
+ prefix: "",
1176
+ at: 0
1177
+ };
894
1178
  effect(() => {
895
1179
  const root = this.rootRef().nativeElement;
896
1180
  const grid = this.isGrid();
@@ -910,6 +1194,30 @@ var DataGrid = class DataGrid {
910
1194
  else table.removeAttribute("aria-busy");
911
1195
  }, { injector: this.injector });
912
1196
  });
1197
+ const reportedLabels = /* @__PURE__ */ new Set();
1198
+ effect(() => {
1199
+ const labels = this.labels();
1200
+ const columns = this.resolvedColumns();
1201
+ const features = {
1202
+ selection: this.selectionMode() !== "none",
1203
+ pagination: !!this.pageSize(),
1204
+ hidableColumns: columns.some((c) => c.hidable),
1205
+ resizableColumns: columns.some((c) => c.resizable),
1206
+ editing: columns.some((c) => c.editable),
1207
+ sorting: columns.some((c) => c.sortable),
1208
+ search: !!labels.search,
1209
+ exporting: !!(labels.exportCsv || labels.exportTsv || labels.exportExcel || labels.copy)
1210
+ };
1211
+ const warn = this.warnMissingLabels();
1212
+ const env = globalThis.process?.env;
1213
+ if (!warn || env?.["NODE_ENV"] === "production") return;
1214
+ const fresh = missingLabels(features, labels).filter((m) => !reportedLabels.has(m.key));
1215
+ if (fresh.length === 0) return;
1216
+ for (const m of fresh) reportedLabels.add(m.key);
1217
+ const controls = fresh.filter((m) => m.kind === "control").map((m) => m.key);
1218
+ const announcements = fresh.filter((m) => m.kind === "announcement").map((m) => m.key);
1219
+ 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("; "));
1220
+ });
913
1221
  effect(() => {
914
1222
  this.pageEntries();
915
1223
  this.visibleColumns();
@@ -930,7 +1238,8 @@ var DataGrid = class DataGrid {
930
1238
  const snapshot = {
931
1239
  columnWidths: this.columnWidths(),
932
1240
  hiddenColumnIds: [...this.hiddenColumnIds()],
933
- sort: this.sort()
1241
+ sort: this.sort(),
1242
+ columnOrder: this.columnOrder()
934
1243
  };
935
1244
  if (!key || typeof localStorage === "undefined") return;
936
1245
  try {
@@ -943,11 +1252,33 @@ var DataGrid = class DataGrid {
943
1252
  }
944
1253
  ariaSortFor(column) {
945
1254
  if (!column.sortable) return null;
946
- return this.sort().columnId === column.id ? this.sort().direction : "none";
1255
+ if (this.sort().columnId === column.id) return this.sort().direction;
1256
+ return this.thenBy().find((k) => k.columnId === column.id)?.direction ?? "none";
1257
+ }
1258
+ sortOrderFor(column) {
1259
+ const keys = this.sortKeys();
1260
+ if (keys.length < 2) return null;
1261
+ const at = keys.findIndex((k) => k.column.id === column.id);
1262
+ return at === -1 ? null : at + 1;
947
1263
  }
948
- toggleSort(column) {
1264
+ toggleSort(column, additive = false) {
949
1265
  if (!column.sortable) return;
950
1266
  const current = this.sort();
1267
+ if (additive && current.direction !== "none" && current.columnId && current.columnId !== column.id) {
1268
+ const thenBy = this.thenBy();
1269
+ const existing = thenBy.find((k) => k.columnId === column.id);
1270
+ const nextThen = !existing ? [...thenBy, {
1271
+ columnId: column.id,
1272
+ direction: "ascending"
1273
+ }] : existing.direction === "ascending" ? thenBy.map((k) => k.columnId === column.id ? {
1274
+ columnId: column.id,
1275
+ direction: "descending"
1276
+ } : k) : thenBy.filter((k) => k.columnId !== column.id);
1277
+ this.thenBy.set(nextThen);
1278
+ this.announce(this.labels().sortAnnouncement?.(column.header, nextThen.find((k) => k.columnId === column.id)?.direction ?? "none"));
1279
+ return;
1280
+ }
1281
+ if (this.thenBy().length > 0) this.thenBy.set([]);
951
1282
  const next = current.columnId !== column.id ? {
952
1283
  columnId: column.id,
953
1284
  direction: "ascending"
@@ -964,8 +1295,20 @@ var DataGrid = class DataGrid {
964
1295
  this.sort.set(next);
965
1296
  this.announce(this.labels().sortAnnouncement?.(column.header, next.direction));
966
1297
  }
967
- onFilterInput(event) {
968
- this.filter.set(event.target.value);
1298
+ setFilter(value) {
1299
+ this.filter.set(value);
1300
+ this.page.set(1);
1301
+ this.announce(this.labels().filterAnnouncement?.(this.filteredEntries().length, this.rows().length));
1302
+ }
1303
+ onFilterKeydown(event) {
1304
+ const e = event;
1305
+ if (e.key === "Escape" && this.filter() !== "") {
1306
+ e.preventDefault();
1307
+ this.setFilter("");
1308
+ }
1309
+ }
1310
+ onFilterFieldChange(event) {
1311
+ this.filterField.set(event.target.value);
969
1312
  this.page.set(1);
970
1313
  this.announce(this.labels().filterAnnouncement?.(this.filteredEntries().length, this.rows().length));
971
1314
  }
@@ -1008,6 +1351,12 @@ var DataGrid = class DataGrid {
1008
1351
  const width = this.widthFor(column);
1009
1352
  if (width) parts.push(`width:${width}px`);
1010
1353
  if (column.align) parts.push(`text-align:${column.align}`);
1354
+ if (column.pinned) {
1355
+ const visible = this.visibleColumns();
1356
+ const i = visible.findIndex((c) => c.id === column.id);
1357
+ const widths = (column.pinned === "start" ? visible.slice(0, i) : visible.slice(i + 1)).filter((c) => c.pinned === column.pinned).map((c) => this.widthFor(c));
1358
+ if (widths.every((w) => w !== void 0)) parts.push(`--data-grid-pinned-offset:${widths.reduce((a, w) => a + (w ?? 0), 0)}px`);
1359
+ }
1011
1360
  return parts.length > 0 ? parts.join(";") : null;
1012
1361
  }
1013
1362
  setWidth(column, width) {
@@ -1125,29 +1474,44 @@ var DataGrid = class DataGrid {
1125
1474
  hasUtcColumn(c) {
1126
1475
  return c.type === "time-zone" && !!c.exportUtc && !!c.referenceDate && !!this.labels().exportUtcHeader;
1127
1476
  }
1128
- runExport(format) {
1477
+ buildExportTable(format) {
1129
1478
  const live = (c) => format === "xlsx" && this.exportFormulas() && c.type === "formula" && !!c.source;
1130
1479
  const headers = [];
1131
1480
  for (const c of this.exportColumns()) {
1132
1481
  headers.push(c.header);
1133
1482
  if (this.hasUtcColumn(c)) headers.push(this.labels().exportUtcHeader(c.header));
1134
1483
  }
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 = {
1484
+ return {
1148
1485
  headers,
1149
- rows
1486
+ rows: this.exportEntries().map((entry) => {
1487
+ const cells = [];
1488
+ for (const c of this.exportColumns()) {
1489
+ const expression = live(c) ? c.source(entry.row) : void 0;
1490
+ cells.push(typeof expression === "string" && expression !== "" && !c.errorOf?.(entry.row) ? {
1491
+ formula: expression,
1492
+ result: this.exportCell(c, entry.row)
1493
+ } : this.exportCell(c, entry.row));
1494
+ if (this.hasUtcColumn(c)) cells.push(zonedUtcTime(this.cellFor(c, entry.row).value, c.referenceDate));
1495
+ }
1496
+ return cells;
1497
+ })
1150
1498
  };
1499
+ }
1500
+ async runCopy() {
1501
+ const table = this.buildExportTable("tsv");
1502
+ const text = toClipboardText(table);
1503
+ const copied = {
1504
+ text,
1505
+ rowCount: table.rows.length
1506
+ };
1507
+ this.copy.emit(copied);
1508
+ if (this.copyHandler()?.(copied) === false) return;
1509
+ const ok = await copyToClipboard(text);
1510
+ this.announce(ok ? this.labels().copyAnnouncement?.(table.rows.length) : this.labels().copyFailed);
1511
+ }
1512
+ runExport(format) {
1513
+ const table = this.buildExportTable(format);
1514
+ const rows = table.rows;
1151
1515
  const name = this.exportFilename();
1152
1516
  const filename = exportFilename(name, format);
1153
1517
  const blob = exportBlob(table, format, { sheetName: name });
@@ -1235,6 +1599,26 @@ var DataGrid = class DataGrid {
1235
1599
  if (column.editor === "date" || column.editor === "time" || column.editor === "select") return draft === "" ? null : draft;
1236
1600
  return draft;
1237
1601
  }
1602
+ stepHistory(direction) {
1603
+ const step = direction === "undo" ? undoEdit(this.history) : redoEdit(this.history);
1604
+ if (!step.record) return;
1605
+ this.history = step.history;
1606
+ const { ownerId, fieldId, value, previous } = step.record;
1607
+ const entry = this.entries().find((e) => e.id === ownerId);
1608
+ const column = this.resolvedColumns().find((c) => c.id === fieldId);
1609
+ if (!entry || !column) return;
1610
+ const restored = direction === "undo" ? previous : value;
1611
+ const replaced = direction === "undo" ? value : previous;
1612
+ this.cellEdit.emit({
1613
+ rowId: entry.id,
1614
+ row: entry.row,
1615
+ columnId: column.id,
1616
+ value: restored,
1617
+ previous: replaced
1618
+ });
1619
+ const text = column.format ? column.format(restored, entry.row) : String(restored ?? "");
1620
+ this.announce((direction === "undo" ? this.labels().undoAnnouncement : this.labels().redoAnnouncement)?.(column.header, text));
1621
+ }
1238
1622
  isStructurallyEqual(a, b) {
1239
1623
  return typeof a === "object" && typeof b === "object" && a !== null && b !== null && JSON.stringify(a) === JSON.stringify(b);
1240
1624
  }
@@ -1255,6 +1639,12 @@ var DataGrid = class DataGrid {
1255
1639
  const previous = column.type === "formula" && column.source ? column.source(entry.row) : defaultAccessor(column, entry.row);
1256
1640
  this.editingState.set(null);
1257
1641
  if (value !== previous && !this.isStructurallyEqual(value, previous)) {
1642
+ this.history = recordEdit(this.history, {
1643
+ ownerId: entry.id,
1644
+ fieldId: column.id,
1645
+ value,
1646
+ previous
1647
+ });
1258
1648
  this.cellEdit.emit({
1259
1649
  rowId: entry.id,
1260
1650
  row: entry.row,
@@ -1423,51 +1813,51 @@ var DataGrid = class DataGrid {
1423
1813
  const activeRow = this.activeRow();
1424
1814
  const activeCol = this.activeCol();
1425
1815
  const colOffset = this.colOffset();
1426
- if (e.altKey && activeRow === -1 && (e.key === "ArrowRight" || e.key === "ArrowLeft")) {
1816
+ const key = logicalKey(e.key, isRtl(cell));
1817
+ if (e.altKey && activeRow === -1 && (key === "ArrowRight" || key === "ArrowLeft")) {
1427
1818
  const column = this.visibleColumns()[activeCol - colOffset];
1428
- if (column?.movable) {
1819
+ if (column && this.isMovable(column)) {
1429
1820
  e.preventDefault();
1430
- const delta = e.key === "ArrowRight" ? 1 : -1;
1821
+ const delta = key === "ArrowRight" ? 1 : -1;
1431
1822
  if (this.moveColumn(column.id, activeCol - colOffset + delta)) this.moveFocus(-1, activeCol + delta);
1432
1823
  return;
1433
1824
  }
1434
1825
  }
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;
1826
+ const historyAction = target === cell && this.resolvedColumns().some((c) => c.editable) ? historyKey(e) : null;
1827
+ if (historyAction) {
1828
+ e.preventDefault();
1829
+ this.stepHistory(historyAction);
1830
+ return;
1831
+ }
1832
+ if (isTypeaheadKey(e) && target === cell && activeRow >= 0) {
1833
+ const column = this.visibleColumns()[activeCol - colOffset];
1834
+ if (column) {
1835
+ const state = nextTypeaheadState(this.typeahead, e.key, Date.now(), 700);
1836
+ const hit = findTypeaheadMatch(this.pageEntries().map((entry) => this.cellFor(column, entry.row).formatted), activeRow, state.prefix);
1837
+ if (hit !== -1) {
1838
+ this.typeahead = state;
1839
+ e.preventDefault();
1840
+ this.moveFocus(hit, activeCol);
1841
+ return;
1842
+ }
1843
+ }
1844
+ }
1845
+ const move = gridMove(e, {
1846
+ row: activeRow,
1847
+ col: activeCol
1848
+ }, {
1849
+ firstRow: -1,
1850
+ lastRow: this.pageEntries().length - 1,
1851
+ lastCol: this.lastCol(),
1852
+ pageStep: this.pageSize() ?? 10,
1853
+ rtl: isRtl(cell)
1854
+ });
1855
+ if (move) {
1856
+ e.preventDefault();
1857
+ this.moveFocus(move.row, move.col);
1858
+ return;
1859
+ }
1860
+ switch (key) {
1471
1861
  case "F2": {
1472
1862
  const entry = this.pageEntries()[Number(cell.dataset["row"])];
1473
1863
  const column = this.visibleColumns()[Number(cell.dataset["col"]) - colOffset];
@@ -1510,6 +1900,7 @@ var DataGrid = class DataGrid {
1510
1900
  if (saved.columnWidths) this.columnWidths.set(saved.columnWidths);
1511
1901
  if (saved.hiddenColumnIds) this.hiddenColumnIds.set(new Set(saved.hiddenColumnIds));
1512
1902
  if (saved.sort) this.sort.set(saved.sort);
1903
+ if (Array.isArray(saved.columnOrder)) this.columnOrder.set(saved.columnOrder);
1513
1904
  }
1514
1905
  } catch {}
1515
1906
  }
@@ -1604,6 +1995,41 @@ var DataGrid = class DataGrid {
1604
1995
  isRequired: false,
1605
1996
  transformFunction: null
1606
1997
  },
1998
+ emptyValue: {
1999
+ classPropertyName: "emptyValue",
2000
+ publicName: "emptyValue",
2001
+ isSignal: true,
2002
+ isRequired: false,
2003
+ transformFunction: null
2004
+ },
2005
+ warnMissingLabels: {
2006
+ classPropertyName: "warnMissingLabels",
2007
+ publicName: "warnMissingLabels",
2008
+ isSignal: true,
2009
+ isRequired: false,
2010
+ transformFunction: null
2011
+ },
2012
+ nulls: {
2013
+ classPropertyName: "nulls",
2014
+ publicName: "nulls",
2015
+ isSignal: true,
2016
+ isRequired: false,
2017
+ transformFunction: null
2018
+ },
2019
+ movableColumns: {
2020
+ classPropertyName: "movableColumns",
2021
+ publicName: "movableColumns",
2022
+ isSignal: true,
2023
+ isRequired: false,
2024
+ transformFunction: null
2025
+ },
2026
+ rowFilter: {
2027
+ classPropertyName: "rowFilter",
2028
+ publicName: "rowFilter",
2029
+ isSignal: true,
2030
+ isRequired: false,
2031
+ transformFunction: null
2032
+ },
1607
2033
  rowId: {
1608
2034
  classPropertyName: "rowId",
1609
2035
  publicName: "rowId",
@@ -1723,6 +2149,13 @@ var DataGrid = class DataGrid {
1723
2149
  isRequired: false,
1724
2150
  transformFunction: null
1725
2151
  },
2152
+ copyHandler: {
2153
+ classPropertyName: "copyHandler",
2154
+ publicName: "copyHandler",
2155
+ isSignal: true,
2156
+ isRequired: false,
2157
+ transformFunction: null
2158
+ },
1726
2159
  virtualize: {
1727
2160
  classPropertyName: "virtualize",
1728
2161
  publicName: "virtualize",
@@ -1751,6 +2184,20 @@ var DataGrid = class DataGrid {
1751
2184
  isRequired: false,
1752
2185
  transformFunction: null
1753
2186
  },
2187
+ filterField: {
2188
+ classPropertyName: "filterField",
2189
+ publicName: "filterField",
2190
+ isSignal: true,
2191
+ isRequired: false,
2192
+ transformFunction: null
2193
+ },
2194
+ thenBy: {
2195
+ classPropertyName: "thenBy",
2196
+ publicName: "thenBy",
2197
+ isSignal: true,
2198
+ isRequired: false,
2199
+ transformFunction: null
2200
+ },
1754
2201
  sort: {
1755
2202
  classPropertyName: "sort",
1756
2203
  publicName: "sort",
@@ -1776,6 +2223,8 @@ var DataGrid = class DataGrid {
1776
2223
  outputs: {
1777
2224
  selected: "selectedChange",
1778
2225
  filter: "filterChange",
2226
+ filterField: "filterFieldChange",
2227
+ thenBy: "thenByChange",
1779
2228
  sort: "sortChange",
1780
2229
  columnOrder: "columnOrderChange",
1781
2230
  page: "pageChange",
@@ -1783,7 +2232,8 @@ var DataGrid = class DataGrid {
1783
2232
  filesSelected: "filesSelected",
1784
2233
  fileRemove: "fileRemove",
1785
2234
  rowActivate: "rowActivate",
1786
- exportFile: "exportFile"
2235
+ exportFile: "exportFile",
2236
+ copy: "copy"
1787
2237
  },
1788
2238
  viewQueries: [{
1789
2239
  propertyName: "rootRef",
@@ -1803,11 +2253,22 @@ var DataGrid = class DataGrid {
1803
2253
  <div #rootEl class="data-grid {{ className() }}">
1804
2254
  @if (labels().search || hidableColumns().length > 0 || hasExportButtons()) {
1805
2255
  <div class="data-grid-toolbar">
2256
+ @if (labels().search && labels().searchField && labels().searchAllFields) {
2257
+ <select class="data-grid-search-field" [attr.aria-label]="labels().searchField" [value]="filterField()" (change)="onFilterFieldChange($event)">
2258
+ <option value="">{{ labels().searchAllFields }}</option>
2259
+ @for (c of filterableColumns(); track c.id) {
2260
+ <option [value]="c.id">{{ c.header }}</option>
2261
+ }
2262
+ </select>
2263
+ }
1806
2264
  @if (labels().search; as searchLabel) {
1807
- <input class="data-grid-search" type="search" [attr.aria-label]="searchLabel" [value]="filter()" (input)="onFilterInput($event)" />
2265
+ <lily-search-input className="data-grid-search" [label]="searchLabel" [value]="filter()" (valueChange)="setFilter($event)" (keydown)="onFilterKeydown($event)" />
1808
2266
  }
1809
2267
  @if (hasExportButtons()) {
1810
2268
  <div class="data-grid-export" role="group" [attr.aria-label]="labels().export ?? null">
2269
+ @if (labels().copy; as copy) {
2270
+ <button type="button" class="data-grid-export-button" data-grid-export-format="copy" [disabled]="exportEntries().length === 0" (click)="runCopy()">{{ copy }}</button>
2271
+ }
1811
2272
  @if (labels().exportCsv; as csv) {
1812
2273
  <button type="button" class="data-grid-export-button" data-grid-export-format="csv" [disabled]="exportEntries().length === 0" (click)="runExport('csv')">{{ csv }}</button>
1813
2274
  }
@@ -1851,16 +2312,26 @@ var DataGrid = class DataGrid {
1851
2312
  }
1852
2313
 
1853
2314
  @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>
2315
+ @if (labels().pagination; as pagination) {
2316
+ <lily-pagination-nav className="data-grid-pagination" [label]="pagination">
2317
+ <ng-container *ngTemplateOutlet="pager" />
2318
+ </lily-pagination-nav>
2319
+ } @else {
2320
+ <div class="data-grid-pagination">
2321
+ <ng-container *ngTemplateOutlet="pager" />
2322
+ </div>
2323
+ }
1859
2324
  }
1860
2325
 
1861
2326
  <p class="data-grid-status" aria-live="polite">{{ statusMessage() }}</p>
1862
2327
  </div>
1863
2328
 
2329
+ <ng-template #pager>
2330
+ <button type="button" class="data-grid-page-previous" [disabled]="clampedPage() <= 1" (click)="goToPage(clampedPage() - 1)">{{ labels().previousPage }}</button>
2331
+ <span class="data-grid-page-status">{{ labels().pageStatus?.(clampedPage(), pageCount(), sortedEntries().length) }}</span>
2332
+ <button type="button" class="data-grid-page-next" [disabled]="clampedPage() >= pageCount()" (click)="goToPage(clampedPage() + 1)">{{ labels().nextPage }}</button>
2333
+ </ng-template>
2334
+
1864
2335
  <ng-template #tableTemplate>
1865
2336
  <lily-data-table
1866
2337
  [label]="label()"
@@ -1908,15 +2379,16 @@ var DataGrid = class DataGrid {
1908
2379
  [attr.data-pinned]="column.pinned ?? null"
1909
2380
  [attr.tabindex]="isGrid() ? (activeRow() === -1 && activeCol() === colOffset() + i ? 0 : -1) : null"
1910
2381
  [attr.aria-sort]="ariaSortFor(column)"
2382
+ [attr.data-sort-order]="sortOrderFor(column)"
1911
2383
  [attr.aria-describedby]="column.type === 'rollup' && labels().rollupDescription ? gridId + '-' + column.id + '-description' : null"
1912
2384
  [attr.style]="cellStyle(column)"
1913
- [attr.draggable]="column.movable ? 'true' : null"
2385
+ [attr.draggable]="isMovable(column) ? 'true' : null"
1914
2386
  (dragstart)="onHeaderDragStart(column, $event)"
1915
2387
  (dragover)="onHeaderDragOver($event)"
1916
2388
  (drop)="onHeaderDrop(column.id, $event)"
1917
2389
  >
1918
2390
  @if (column.sortable) {
1919
- <button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column)">
2391
+ <button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column, $any($event).shiftKey)">
1920
2392
  @if (column.headerCell; as headerTemplate) {
1921
2393
  <ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: column }" />
1922
2394
  } @else {<ng-container>{{ column.header }}</ng-container>}
@@ -1993,6 +2465,7 @@ var DataGrid = class DataGrid {
1993
2465
  [attr.data-cell-type]="column.type ?? null"
1994
2466
  [attr.data-pinned]="column.pinned ?? null"
1995
2467
  [attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
2468
+ [attr.data-empty]="isMissing(ctx.value) ? '' : null"
1996
2469
  [attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
1997
2470
  [attr.style]="cellStyle(column)"
1998
2471
  [lilyDataGridAttrs]="column.cellAttrs?.(ctx)"
@@ -2013,6 +2486,7 @@ var DataGrid = class DataGrid {
2013
2486
  [attr.data-readonly]="column.type === 'rollup' ? '' : null"
2014
2487
  [attr.aria-readonly]="column.type === 'rollup' ? 'true' : null"
2015
2488
  [attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
2489
+ [attr.data-empty]="isMissing(ctx.value) ? '' : null"
2016
2490
  [attr.data-editing]="isEditingCell(entry, column) ? '' : null"
2017
2491
  [attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
2018
2492
  [attr.style]="cellStyle(column)"
@@ -2136,6 +2610,8 @@ var DataGrid = class DataGrid {
2136
2610
  } @else {
2137
2611
  @if (column.cell; as cellTemplate) {
2138
2612
  <ng-container *ngTemplateOutlet="cellTemplate; context: { $implicit: ctx }" />
2613
+ } @else if (isMissing(ctx.value) && (column.emptyValue ?? emptyValue())) {
2614
+ <ng-container>{{ column.emptyValue ?? emptyValue() }}</ng-container>
2139
2615
  } @else if (column.type === 'person') {
2140
2616
  @let people = peopleIn(ctx.value);
2141
2617
  @if (people.length > 0) {
@@ -2280,6 +2756,25 @@ var DataGrid = class DataGrid {
2280
2756
  type: DataTableTH,
2281
2757
  selector: "th[lily-data-table-th]",
2282
2758
  inputs: ["scope", "className"]
2759
+ },
2760
+ {
2761
+ kind: "component",
2762
+ type: PaginationNav,
2763
+ selector: "lily-pagination-nav",
2764
+ inputs: ["label", "className"]
2765
+ },
2766
+ {
2767
+ kind: "component",
2768
+ type: SearchInput,
2769
+ selector: "lily-search-input",
2770
+ inputs: [
2771
+ "label",
2772
+ "className",
2773
+ "value",
2774
+ "required",
2775
+ "disabled"
2776
+ ],
2777
+ outputs: ["valueChange"]
2283
2778
  }
2284
2779
  ],
2285
2780
  changeDetection: i0.ChangeDetectionStrategy.OnPush
@@ -2305,18 +2800,31 @@ i0.ɵɵngDeclareClassMetadata({
2305
2800
  DataTableHead,
2306
2801
  DataTableRow,
2307
2802
  DataTableTD,
2308
- DataTableTH
2803
+ DataTableTH,
2804
+ PaginationNav,
2805
+ SearchInput
2309
2806
  ],
2310
2807
  changeDetection: ChangeDetectionStrategy.OnPush,
2311
2808
  template: `
2312
2809
  <div #rootEl class="data-grid {{ className() }}">
2313
2810
  @if (labels().search || hidableColumns().length > 0 || hasExportButtons()) {
2314
2811
  <div class="data-grid-toolbar">
2812
+ @if (labels().search && labels().searchField && labels().searchAllFields) {
2813
+ <select class="data-grid-search-field" [attr.aria-label]="labels().searchField" [value]="filterField()" (change)="onFilterFieldChange($event)">
2814
+ <option value="">{{ labels().searchAllFields }}</option>
2815
+ @for (c of filterableColumns(); track c.id) {
2816
+ <option [value]="c.id">{{ c.header }}</option>
2817
+ }
2818
+ </select>
2819
+ }
2315
2820
  @if (labels().search; as searchLabel) {
2316
- <input class="data-grid-search" type="search" [attr.aria-label]="searchLabel" [value]="filter()" (input)="onFilterInput($event)" />
2821
+ <lily-search-input className="data-grid-search" [label]="searchLabel" [value]="filter()" (valueChange)="setFilter($event)" (keydown)="onFilterKeydown($event)" />
2317
2822
  }
2318
2823
  @if (hasExportButtons()) {
2319
2824
  <div class="data-grid-export" role="group" [attr.aria-label]="labels().export ?? null">
2825
+ @if (labels().copy; as copy) {
2826
+ <button type="button" class="data-grid-export-button" data-grid-export-format="copy" [disabled]="exportEntries().length === 0" (click)="runCopy()">{{ copy }}</button>
2827
+ }
2320
2828
  @if (labels().exportCsv; as csv) {
2321
2829
  <button type="button" class="data-grid-export-button" data-grid-export-format="csv" [disabled]="exportEntries().length === 0" (click)="runExport('csv')">{{ csv }}</button>
2322
2830
  }
@@ -2360,16 +2868,26 @@ i0.ɵɵngDeclareClassMetadata({
2360
2868
  }
2361
2869
 
2362
2870
  @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>
2871
+ @if (labels().pagination; as pagination) {
2872
+ <lily-pagination-nav className="data-grid-pagination" [label]="pagination">
2873
+ <ng-container *ngTemplateOutlet="pager" />
2874
+ </lily-pagination-nav>
2875
+ } @else {
2876
+ <div class="data-grid-pagination">
2877
+ <ng-container *ngTemplateOutlet="pager" />
2878
+ </div>
2879
+ }
2368
2880
  }
2369
2881
 
2370
2882
  <p class="data-grid-status" aria-live="polite">{{ statusMessage() }}</p>
2371
2883
  </div>
2372
2884
 
2885
+ <ng-template #pager>
2886
+ <button type="button" class="data-grid-page-previous" [disabled]="clampedPage() <= 1" (click)="goToPage(clampedPage() - 1)">{{ labels().previousPage }}</button>
2887
+ <span class="data-grid-page-status">{{ labels().pageStatus?.(clampedPage(), pageCount(), sortedEntries().length) }}</span>
2888
+ <button type="button" class="data-grid-page-next" [disabled]="clampedPage() >= pageCount()" (click)="goToPage(clampedPage() + 1)">{{ labels().nextPage }}</button>
2889
+ </ng-template>
2890
+
2373
2891
  <ng-template #tableTemplate>
2374
2892
  <lily-data-table
2375
2893
  [label]="label()"
@@ -2417,15 +2935,16 @@ i0.ɵɵngDeclareClassMetadata({
2417
2935
  [attr.data-pinned]="column.pinned ?? null"
2418
2936
  [attr.tabindex]="isGrid() ? (activeRow() === -1 && activeCol() === colOffset() + i ? 0 : -1) : null"
2419
2937
  [attr.aria-sort]="ariaSortFor(column)"
2938
+ [attr.data-sort-order]="sortOrderFor(column)"
2420
2939
  [attr.aria-describedby]="column.type === 'rollup' && labels().rollupDescription ? gridId + '-' + column.id + '-description' : null"
2421
2940
  [attr.style]="cellStyle(column)"
2422
- [attr.draggable]="column.movable ? 'true' : null"
2941
+ [attr.draggable]="isMovable(column) ? 'true' : null"
2423
2942
  (dragstart)="onHeaderDragStart(column, $event)"
2424
2943
  (dragover)="onHeaderDragOver($event)"
2425
2944
  (drop)="onHeaderDrop(column.id, $event)"
2426
2945
  >
2427
2946
  @if (column.sortable) {
2428
- <button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column)">
2947
+ <button type="button" class="data-grid-sort-button" data-grid-own [attr.tabindex]="isGrid() ? -1 : null" (click)="toggleSort(column, $any($event).shiftKey)">
2429
2948
  @if (column.headerCell; as headerTemplate) {
2430
2949
  <ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: column }" />
2431
2950
  } @else {<ng-container>{{ column.header }}</ng-container>}
@@ -2502,6 +3021,7 @@ i0.ɵɵngDeclareClassMetadata({
2502
3021
  [attr.data-cell-type]="column.type ?? null"
2503
3022
  [attr.data-pinned]="column.pinned ?? null"
2504
3023
  [attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
3024
+ [attr.data-empty]="isMissing(ctx.value) ? '' : null"
2505
3025
  [attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
2506
3026
  [attr.style]="cellStyle(column)"
2507
3027
  [lilyDataGridAttrs]="column.cellAttrs?.(ctx)"
@@ -2522,6 +3042,7 @@ i0.ɵɵngDeclareClassMetadata({
2522
3042
  [attr.data-readonly]="column.type === 'rollup' ? '' : null"
2523
3043
  [attr.aria-readonly]="column.type === 'rollup' ? 'true' : null"
2524
3044
  [attr.data-error]="column.errorOf?.(entry.row) !== undefined ? '' : null"
3045
+ [attr.data-empty]="isMissing(ctx.value) ? '' : null"
2525
3046
  [attr.data-editing]="isEditingCell(entry, column) ? '' : null"
2526
3047
  [attr.tabindex]="isGrid() ? (activeRow() === item.index && activeCol() === colOffset() + colIndex ? 0 : -1) : null"
2527
3048
  [attr.style]="cellStyle(column)"
@@ -2645,6 +3166,8 @@ i0.ɵɵngDeclareClassMetadata({
2645
3166
  } @else {
2646
3167
  @if (column.cell; as cellTemplate) {
2647
3168
  <ng-container *ngTemplateOutlet="cellTemplate; context: { $implicit: ctx }" />
3169
+ } @else if (isMissing(ctx.value) && (column.emptyValue ?? emptyValue())) {
3170
+ <ng-container>{{ column.emptyValue ?? emptyValue() }}</ng-container>
2648
3171
  } @else if (column.type === 'person') {
2649
3172
  @let people = peopleIn(ctx.value);
2650
3173
  @if (people.length > 0) {
@@ -2774,6 +3297,46 @@ i0.ɵɵngDeclareClassMetadata({
2774
3297
  required: false
2775
3298
  }]
2776
3299
  }],
3300
+ emptyValue: [{
3301
+ type: i0.Input,
3302
+ args: [{
3303
+ isSignal: true,
3304
+ alias: "emptyValue",
3305
+ required: false
3306
+ }]
3307
+ }],
3308
+ warnMissingLabels: [{
3309
+ type: i0.Input,
3310
+ args: [{
3311
+ isSignal: true,
3312
+ alias: "warnMissingLabels",
3313
+ required: false
3314
+ }]
3315
+ }],
3316
+ nulls: [{
3317
+ type: i0.Input,
3318
+ args: [{
3319
+ isSignal: true,
3320
+ alias: "nulls",
3321
+ required: false
3322
+ }]
3323
+ }],
3324
+ movableColumns: [{
3325
+ type: i0.Input,
3326
+ args: [{
3327
+ isSignal: true,
3328
+ alias: "movableColumns",
3329
+ required: false
3330
+ }]
3331
+ }],
3332
+ rowFilter: [{
3333
+ type: i0.Input,
3334
+ args: [{
3335
+ isSignal: true,
3336
+ alias: "rowFilter",
3337
+ required: false
3338
+ }]
3339
+ }],
2777
3340
  rowId: [{
2778
3341
  type: i0.Input,
2779
3342
  args: [{
@@ -2910,6 +3473,14 @@ i0.ɵɵngDeclareClassMetadata({
2910
3473
  required: false
2911
3474
  }]
2912
3475
  }],
3476
+ copyHandler: [{
3477
+ type: i0.Input,
3478
+ args: [{
3479
+ isSignal: true,
3480
+ alias: "copyHandler",
3481
+ required: false
3482
+ }]
3483
+ }],
2913
3484
  virtualize: [{
2914
3485
  type: i0.Input,
2915
3486
  args: [{
@@ -2948,6 +3519,28 @@ i0.ɵɵngDeclareClassMetadata({
2948
3519
  type: i0.Output,
2949
3520
  args: ["filterChange"]
2950
3521
  }],
3522
+ filterField: [{
3523
+ type: i0.Input,
3524
+ args: [{
3525
+ isSignal: true,
3526
+ alias: "filterField",
3527
+ required: false
3528
+ }]
3529
+ }, {
3530
+ type: i0.Output,
3531
+ args: ["filterFieldChange"]
3532
+ }],
3533
+ thenBy: [{
3534
+ type: i0.Input,
3535
+ args: [{
3536
+ isSignal: true,
3537
+ alias: "thenBy",
3538
+ required: false
3539
+ }]
3540
+ }, {
3541
+ type: i0.Output,
3542
+ args: ["thenByChange"]
3543
+ }],
2951
3544
  sort: [{
2952
3545
  type: i0.Input,
2953
3546
  args: [{
@@ -3001,6 +3594,10 @@ i0.ɵɵngDeclareClassMetadata({
3001
3594
  type: i0.Output,
3002
3595
  args: ["exportFile"]
3003
3596
  }],
3597
+ copy: [{
3598
+ type: i0.Output,
3599
+ args: ["copy"]
3600
+ }],
3004
3601
  rootRef: [{
3005
3602
  type: i0.ViewChild,
3006
3603
  args: ["rootEl", { isSignal: true }]
@@ -3011,6 +3608,6 @@ i0.ɵɵngDeclareClassMetadata({
3011
3608
  }]
3012
3609
  }
3013
3610
  });
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 };
3611
+ 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
3612
 
3016
3613
  //# sourceMappingURL=lilydesignsystem-angular-data-grid.mjs.map