@devjuliovilla/jv-ui 1.15.0 → 1.16.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.
Files changed (47) hide show
  1. package/README.md +24 -0
  2. package/fesm2022/devjuliovilla-jv-ui.mjs +328 -977
  3. package/fesm2022/devjuliovilla-jv-ui.mjs.map +1 -1
  4. package/package.json +85 -1
  5. package/secondary/auth-pages.d.ts +1 -0
  6. package/secondary/auth-pages.mjs +1 -0
  7. package/secondary/buttons.d.ts +1 -0
  8. package/secondary/buttons.mjs +1 -0
  9. package/secondary/calendar.d.ts +1 -0
  10. package/secondary/calendar.mjs +1 -0
  11. package/secondary/collapsible.d.ts +1 -0
  12. package/secondary/collapsible.mjs +1 -0
  13. package/secondary/config.d.ts +1 -0
  14. package/secondary/config.mjs +1 -0
  15. package/secondary/containers.d.ts +1 -0
  16. package/secondary/containers.mjs +1 -0
  17. package/secondary/core.d.ts +1 -0
  18. package/secondary/core.mjs +1 -0
  19. package/secondary/dialogs.d.ts +1 -0
  20. package/secondary/dialogs.mjs +1 -0
  21. package/secondary/feedback.d.ts +1 -0
  22. package/secondary/feedback.mjs +1 -0
  23. package/secondary/forms.d.ts +1 -0
  24. package/secondary/forms.mjs +1 -0
  25. package/secondary/gallery.d.ts +1 -0
  26. package/secondary/gallery.mjs +1 -0
  27. package/secondary/grid.d.ts +1 -0
  28. package/secondary/grid.mjs +1 -0
  29. package/secondary/i18n.d.ts +1 -0
  30. package/secondary/i18n.mjs +1 -0
  31. package/secondary/icons.d.ts +1 -0
  32. package/secondary/icons.mjs +1 -0
  33. package/secondary/layout.d.ts +1 -0
  34. package/secondary/layout.mjs +1 -0
  35. package/secondary/list.d.ts +1 -0
  36. package/secondary/list.mjs +1 -0
  37. package/secondary/pagination.d.ts +1 -0
  38. package/secondary/pagination.mjs +1 -0
  39. package/secondary/services.d.ts +1 -0
  40. package/secondary/services.mjs +1 -0
  41. package/secondary/slider.d.ts +1 -0
  42. package/secondary/slider.mjs +1 -0
  43. package/secondary/tabs.d.ts +1 -0
  44. package/secondary/tabs.mjs +1 -0
  45. package/secondary/tooltip.d.ts +1 -0
  46. package/secondary/tooltip.mjs +1 -0
  47. package/types/devjuliovilla-jv-ui.d.ts +0 -4
@@ -24583,6 +24583,266 @@ function exportGridToExcel(header, rows, filename = 'export.xlsx') {
24583
24583
  downloadBlob(blob, filename);
24584
24584
  }
24585
24585
 
24586
+ function flattenGridColumns(columns) {
24587
+ return columns.flatMap((column) => column.children?.length ? flattenGridColumns(column.children) : [column]);
24588
+ }
24589
+ function getVisibleGridColumns(columns, columnOrder) {
24590
+ const visibleColumns = flattenGridColumns(columns).filter((column) => !column.hidden);
24591
+ if (columnOrder.length === 0)
24592
+ return visibleColumns;
24593
+ return columnOrder
24594
+ .map((key) => visibleColumns.find((column) => String(column.key) === key))
24595
+ .filter((column) => Boolean(column));
24596
+ }
24597
+ function getGroupedGridColumns(columns) {
24598
+ return columns.filter((column) => column.children && column.children.length > 0);
24599
+ }
24600
+
24601
+ function getCellKeyboardTarget(key, rowIndex, columnIndex, rowCount, columnCount) {
24602
+ if (key === 'ArrowUp' && rowIndex === 0) {
24603
+ return { kind: 'header', row: 0, column: columnIndex };
24604
+ }
24605
+ const movement = {
24606
+ ArrowDown: [1, 0],
24607
+ ArrowLeft: [0, -1],
24608
+ ArrowRight: [0, 1],
24609
+ ArrowUp: [-1, 0],
24610
+ };
24611
+ const delta = movement[key];
24612
+ if (!delta)
24613
+ return null;
24614
+ const target = { row: rowIndex + delta[0], column: columnIndex + delta[1] };
24615
+ if (target.row < 0 ||
24616
+ target.row >= rowCount ||
24617
+ target.column < 0 ||
24618
+ target.column >= columnCount) {
24619
+ return null;
24620
+ }
24621
+ return { kind: 'cell', ...target };
24622
+ }
24623
+ function getHeaderKeyboardTarget(key, columnIndex, columnCount) {
24624
+ if (key === 'ArrowDown')
24625
+ return { kind: 'cell', row: 0, column: columnIndex };
24626
+ const delta = key === 'ArrowLeft' ? -1 : key === 'ArrowRight' ? 1 : 0;
24627
+ if (!delta)
24628
+ return null;
24629
+ const targetColumn = columnIndex + delta;
24630
+ if (targetColumn < 0 || targetColumn >= columnCount)
24631
+ return null;
24632
+ return { kind: 'header', row: 0, column: targetColumn };
24633
+ }
24634
+
24635
+ function getGridEditType(column) {
24636
+ if (column.editType)
24637
+ return column.editType;
24638
+ switch (column.type) {
24639
+ case 'integer':
24640
+ case 'number':
24641
+ case 'currency':
24642
+ case 'percent':
24643
+ return 'number';
24644
+ case 'date':
24645
+ return 'date';
24646
+ case 'time':
24647
+ return 'time';
24648
+ case 'datetime':
24649
+ return 'datetime';
24650
+ case 'boolean':
24651
+ return 'boolean';
24652
+ default:
24653
+ return 'text';
24654
+ }
24655
+ }
24656
+ function getGridInputType(column) {
24657
+ switch (getGridEditType(column)) {
24658
+ case 'number':
24659
+ return 'number';
24660
+ case 'date':
24661
+ return 'date';
24662
+ case 'time':
24663
+ return 'time';
24664
+ case 'datetime':
24665
+ return 'datetime-local';
24666
+ default:
24667
+ return 'text';
24668
+ }
24669
+ }
24670
+ function isGridBooleanTrue(value) {
24671
+ return value === true || value === 'true' || value === 1 || value === '1';
24672
+ }
24673
+ function getGridEditValue(value, column, toDate) {
24674
+ const editType = getGridEditType(column);
24675
+ if (editType === 'date')
24676
+ return toGridDateInputValue(value, toDate);
24677
+ if (editType === 'datetime')
24678
+ return toGridDateTimeInputValue(value, toDate);
24679
+ if (editType === 'time')
24680
+ return toGridTimeInputValue(value, toDate);
24681
+ return value == null ? '' : String(value);
24682
+ }
24683
+ function normalizeGridEditValue(column, value, toDate) {
24684
+ switch (getGridEditType(column)) {
24685
+ case 'number': {
24686
+ if (value === '' || value == null)
24687
+ return null;
24688
+ const number = Number(value);
24689
+ return Number.isFinite(number) ? number : null;
24690
+ }
24691
+ case 'date':
24692
+ return toGridDateInputValue(value, toDate) || null;
24693
+ case 'datetime': {
24694
+ if (value === '' || value == null)
24695
+ return null;
24696
+ const date = new Date(String(value));
24697
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
24698
+ }
24699
+ case 'time': {
24700
+ if (value === '' || value == null)
24701
+ return null;
24702
+ const time = String(value);
24703
+ return /^\d{2}:\d{2}$/.test(time) ? `${time}:00` : time;
24704
+ }
24705
+ case 'boolean':
24706
+ return Boolean(value);
24707
+ default:
24708
+ return value;
24709
+ }
24710
+ }
24711
+ function toGridDateInputValue(value, toDate) {
24712
+ if (typeof value === 'string') {
24713
+ const match = value.match(/^\d{4}-\d{2}-\d{2}/);
24714
+ if (match)
24715
+ return match[0];
24716
+ }
24717
+ const date = toDate(value);
24718
+ if (!date)
24719
+ return '';
24720
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;
24721
+ }
24722
+ function toGridDateTimeInputValue(value, toDate) {
24723
+ const date = toDate(value);
24724
+ if (!date)
24725
+ return '';
24726
+ const pad = (part) => String(part).padStart(2, '0');
24727
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
24728
+ }
24729
+ function toGridTimeInputValue(value, toDate) {
24730
+ if (typeof value === 'string' && /^\d{2}:\d{2}(:\d{2})?$/.test(value))
24731
+ return value.slice(0, 5);
24732
+ const date = toDate(value);
24733
+ if (!date)
24734
+ return '';
24735
+ return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
24736
+ }
24737
+
24738
+ function filterGridData(data, columns, searchTerm, filters, getColumnKey, getRawCellValue, getSearchValue) {
24739
+ let items = data;
24740
+ const term = searchTerm.trim().toLowerCase();
24741
+ if (term) {
24742
+ items = items.filter((row) => columns.some((column) => {
24743
+ if (column.searchable === false)
24744
+ return false;
24745
+ const value = getRawCellValue(row, column);
24746
+ if (value == null)
24747
+ return false;
24748
+ return getSearchValue(value, column).includes(term);
24749
+ }));
24750
+ }
24751
+ const hasFilters = Object.values(filters).some((filter) => filter.value.trim().length > 0);
24752
+ if (!hasFilters)
24753
+ return items;
24754
+ return items.filter((row) => Object.entries(filters).every(([key, filter]) => {
24755
+ if (!filter.value.trim())
24756
+ return true;
24757
+ const column = columns.find((candidate) => getColumnKey(candidate) === key);
24758
+ if (!column || column.filterable === false)
24759
+ return true;
24760
+ const value = getSearchValue(getRawCellValue(row, column), column);
24761
+ const filterValue = filter.value.toLowerCase();
24762
+ switch (filter.operator) {
24763
+ case 'contains':
24764
+ return value.includes(filterValue);
24765
+ case 'equals':
24766
+ return value === filterValue;
24767
+ case 'startsWith':
24768
+ return value.startsWith(filterValue);
24769
+ case 'endsWith':
24770
+ return value.endsWith(filterValue);
24771
+ default:
24772
+ return value.includes(filterValue);
24773
+ }
24774
+ }));
24775
+ }
24776
+ function sortGridData(data, sortState, findColumn, getRawCellValue, getComparableValue) {
24777
+ const { columnKey, direction } = sortState;
24778
+ if (!columnKey || !direction)
24779
+ return data;
24780
+ const column = findColumn(columnKey);
24781
+ return [...data].sort((a, b) => {
24782
+ const valueA = getComparableValue(getRawCellValue(a, column), column);
24783
+ const valueB = getComparableValue(getRawCellValue(b, column), column);
24784
+ if (valueA == null)
24785
+ return 1;
24786
+ if (valueB == null)
24787
+ return -1;
24788
+ const comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
24789
+ return direction === 'asc' ? comparison : -comparison;
24790
+ });
24791
+ }
24792
+ function paginateGridData(data, pageIndex, pageSize, totalPages) {
24793
+ const page = Math.min(pageIndex, totalPages - 1);
24794
+ return data.slice(page * pageSize, page * pageSize + pageSize);
24795
+ }
24796
+
24797
+ function resolveGridSelection(controlledIds, internalIds) {
24798
+ return new Set(controlledIds ?? internalIds);
24799
+ }
24800
+ function areAllGridRowsSelected(rows, selectedIds, getRowId) {
24801
+ return rows.length > 0 && rows.every((row) => selectedIds.has(getRowId(row)));
24802
+ }
24803
+ function areSomeGridRowsSelected(rows, selectedIds, getRowId) {
24804
+ return (rows.some((row) => selectedIds.has(getRowId(row))) &&
24805
+ !areAllGridRowsSelected(rows, selectedIds, getRowId));
24806
+ }
24807
+ function toggleGridRowSelection(selectedIds, rowId) {
24808
+ const next = new Set(selectedIds);
24809
+ if (next.has(rowId))
24810
+ next.delete(rowId);
24811
+ else
24812
+ next.add(rowId);
24813
+ return Array.from(next);
24814
+ }
24815
+ function toggleGridRowsSelection(selectedIds, rows, checked, getRowId) {
24816
+ const next = new Set(selectedIds);
24817
+ rows.forEach((row) => {
24818
+ const id = getRowId(row);
24819
+ if (checked)
24820
+ next.add(id);
24821
+ else
24822
+ next.delete(id);
24823
+ });
24824
+ return Array.from(next);
24825
+ }
24826
+
24827
+ function getGridVirtualRange(scrollTop, clientHeight, rowHeight, itemCount, buffer = 5) {
24828
+ const start = Math.max(0, Math.floor(scrollTop / rowHeight) - buffer);
24829
+ const end = Math.min(itemCount, Math.ceil((scrollTop + clientHeight) / rowHeight) + buffer);
24830
+ return { start, end };
24831
+ }
24832
+ function getGridVirtualSlice(items, start, end, enabled) {
24833
+ if (!enabled)
24834
+ return items;
24835
+ if (start >= items.length)
24836
+ return [];
24837
+ return items.slice(start, Math.min(end, items.length));
24838
+ }
24839
+ function getGridVirtualTotalHeight(itemCount, rowHeight) {
24840
+ return itemCount * rowHeight;
24841
+ }
24842
+ function getGridVirtualOffset(start, rowHeight) {
24843
+ return start * rowHeight;
24844
+ }
24845
+
24586
24846
  let gridIdSequence = 0;
24587
24847
  class JvGridComponent {
24588
24848
  gridId = `jv-grid-${++gridIdSequence}`;
@@ -24608,7 +24868,10 @@ class JvGridComponent {
24608
24868
  columnResize = output();
24609
24869
  columnReorder = output();
24610
24870
  rowEdit = output();
24611
- effectiveOptions = computed(() => ({ ...JV_GRID_DEFAULT_OPTIONS, ...this.options() }), ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : /* istanbul ignore next */ []));
24871
+ effectiveOptions = computed(() => ({
24872
+ ...JV_GRID_DEFAULT_OPTIONS,
24873
+ ...this.options(),
24874
+ }), ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : /* istanbul ignore next */ []));
24612
24875
  idKey = computed(() => {
24613
24876
  const tb = this.trackBy();
24614
24877
  if (typeof tb === 'string')
@@ -24622,14 +24885,9 @@ class JvGridComponent {
24622
24885
  columnWidths = signal({}, ...(ngDevMode ? [{ debugName: "columnWidths" }] : /* istanbul ignore next */ []));
24623
24886
  activeCell = signal({ row: 0, column: 0 }, ...(ngDevMode ? [{ debugName: "activeCell" }] : /* istanbul ignore next */ []));
24624
24887
  visibleColumns = computed(() => {
24625
- const cols = this.flattenColumns(this.columns()).filter(col => !col.hidden);
24626
- const order = this.columnOrder();
24627
- if (order.length > 0) {
24628
- return order.map(key => cols.find(c => this.colKey(c) === key)).filter(Boolean);
24629
- }
24630
- return cols;
24888
+ return getVisibleGridColumns(this.columns(), this.columnOrder());
24631
24889
  }, ...(ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []));
24632
- groupedHeaders = computed(() => this.columns().filter(col => col.children && col.children.length > 0), ...(ngDevMode ? [{ debugName: "groupedHeaders" }] : /* istanbul ignore next */ []));
24890
+ groupedHeaders = computed(() => getGroupedGridColumns(this.columns()), ...(ngDevMode ? [{ debugName: "groupedHeaders" }] : /* istanbul ignore next */ []));
24633
24891
  internalSelectedIds = signal([], ...(ngDevMode ? [{ debugName: "internalSelectedIds" }] : /* istanbul ignore next */ []));
24634
24892
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
24635
24893
  sortState = signal({ columnKey: '', direction: null }, ...(ngDevMode ? [{ debugName: "sortState" }] : /* istanbul ignore next */ []));
@@ -24638,8 +24896,8 @@ class JvGridComponent {
24638
24896
  pageSize = computed(() => this.internalPageSize() ?? this.effectiveOptions().pageSize ?? 20, ...(ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []));
24639
24897
  pageSizeStr = computed(() => String(this.pageSize()), ...(ngDevMode ? [{ debugName: "pageSizeStr" }] : /* istanbul ignore next */ []));
24640
24898
  pageSizeOptions = computed(() => (this.effectiveOptions().pageSizeOptions ?? [])
24641
- .filter(size => Number.isInteger(size) && size > 0)
24642
- .map(size => ({ label: String(size), value: String(size) })), ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : /* istanbul ignore next */ []));
24899
+ .filter((size) => Number.isInteger(size) && size > 0)
24900
+ .map((size) => ({ label: String(size), value: String(size) })), ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : /* istanbul ignore next */ []));
24643
24901
  pageSizeInputSync = effect(() => {
24644
24902
  void this.options().pageSize;
24645
24903
  this.internalPageSize.set(null);
@@ -24654,71 +24912,18 @@ class JvGridComponent {
24654
24912
  this.pageIndex.set(Math.max(0, lastPage));
24655
24913
  }, ...(ngDevMode ? [{ debugName: "pageBoundsGuard" }] : /* istanbul ignore next */ []));
24656
24914
  selectedIdSet = computed(() => {
24657
- const fromInput = this.selectedIds();
24658
- const fromInternal = this.internalSelectedIds();
24659
- const ids = fromInput !== null ? fromInput : fromInternal;
24660
- return new Set(ids);
24915
+ return resolveGridSelection(this.selectedIds(), this.internalSelectedIds());
24661
24916
  }, ...(ngDevMode ? [{ debugName: "selectedIdSet" }] : /* istanbul ignore next */ []));
24662
- hasActiveFilters = computed(() => Object.values(this.colFilters()).some(f => f.value.trim().length > 0), ...(ngDevMode ? [{ debugName: "hasActiveFilters" }] : /* istanbul ignore next */ []));
24917
+ hasActiveFilters = computed(() => Object.values(this.colFilters()).some((f) => f.value.trim().length > 0), ...(ngDevMode ? [{ debugName: "hasActiveFilters" }] : /* istanbul ignore next */ []));
24663
24918
  filteredData = computed(() => {
24664
24919
  if (this.effectiveOptions().serverSide)
24665
24920
  return this.data();
24666
- let items = this.data();
24667
- const term = this.searchTerm().trim().toLowerCase();
24668
- if (term) {
24669
- items = items.filter(row => {
24670
- return this.visibleColumns().some(col => {
24671
- if (col.searchable === false)
24672
- return false;
24673
- const value = this.getRawCellValue(row, col);
24674
- if (value == null)
24675
- return false;
24676
- return this.getSearchValue(value, col).includes(term);
24677
- });
24678
- });
24679
- }
24680
- const filters = this.colFilters();
24681
- const hasFilters = Object.values(filters).some(f => f.value.trim().length > 0);
24682
- if (hasFilters) {
24683
- items = items.filter(row => {
24684
- return Object.entries(filters).every(([key, filter]) => {
24685
- if (!filter.value.trim())
24686
- return true;
24687
- const col = this.columns().find(c => this.colKey(c) === key);
24688
- if (!col || col.filterable === false)
24689
- return true;
24690
- const value = this.getSearchValue(this.getRawCellValue(row, col), col);
24691
- const fv = filter.value.toLowerCase();
24692
- switch (filter.operator) {
24693
- case 'contains': return value.includes(fv);
24694
- case 'equals': return value === fv;
24695
- case 'startsWith': return value.startsWith(fv);
24696
- case 'endsWith': return value.endsWith(fv);
24697
- default: return value.includes(fv);
24698
- }
24699
- });
24700
- });
24701
- }
24702
- return items;
24921
+ return filterGridData(this.data(), this.visibleColumns(), this.searchTerm(), this.colFilters(), (column) => this.colKey(column), (row, column) => this.getRawCellValue(row, column), (value, column) => this.getSearchValue(value, column));
24703
24922
  }, ...(ngDevMode ? [{ debugName: "filteredData" }] : /* istanbul ignore next */ []));
24704
24923
  sortedData = computed(() => {
24705
24924
  if (this.effectiveOptions().serverSide)
24706
24925
  return this.filteredData();
24707
- const items = this.filteredData();
24708
- const { columnKey, direction } = this.sortState();
24709
- if (!columnKey || !direction)
24710
- return items;
24711
- return [...items].sort((a, b) => {
24712
- const col = this.findColumn(columnKey);
24713
- const va = this.getComparableValue(this.getRawCellValue(a, col), col);
24714
- const vb = this.getComparableValue(this.getRawCellValue(b, col), col);
24715
- if (va == null)
24716
- return 1;
24717
- if (vb == null)
24718
- return -1;
24719
- const cmp = va < vb ? -1 : va > vb ? 1 : 0;
24720
- return direction === 'asc' ? cmp : -cmp;
24721
- });
24926
+ return sortGridData(this.filteredData(), this.sortState(), (key) => this.findColumn(key), (row, column) => this.getRawCellValue(row, column), (value, column) => this.getComparableValue(value, column));
24722
24927
  }, ...(ngDevMode ? [{ debugName: "sortedData" }] : /* istanbul ignore next */ []));
24723
24928
  effectiveTotalItems = computed(() => {
24724
24929
  if (this.effectiveOptions().serverSide) {
@@ -24732,10 +24937,7 @@ class JvGridComponent {
24732
24937
  return this.virtualDisplayData();
24733
24938
  if (!this.effectiveOptions().pageable)
24734
24939
  return this.sortedData();
24735
- const items = this.sortedData();
24736
- const ps = this.pageSize();
24737
- const page = Math.min(this.pageIndex(), this.effectiveTotalPages() - 1);
24738
- return items.slice(page * ps, page * ps + ps);
24940
+ return paginateGridData(this.sortedData(), this.pageIndex(), this.pageSize(), this.effectiveTotalPages());
24739
24941
  }, ...(ngDevMode ? [{ debugName: "displayData" }] : /* istanbul ignore next */ []));
24740
24942
  virtualScrollEnabled = computed(() => this.effectiveOptions().virtualScroll && !this.effectiveOptions().serverSide, ...(ngDevMode ? [{ debugName: "virtualScrollEnabled" }] : /* istanbul ignore next */ []));
24741
24943
  virtualStartIndex = signal(0, ...(ngDevMode ? [{ debugName: "virtualStartIndex" }] : /* istanbul ignore next */ []));
@@ -24746,23 +24948,16 @@ class JvGridComponent {
24746
24948
  this.virtualEndIndex.set(Math.min(20, length));
24747
24949
  }, ...(ngDevMode ? [{ debugName: "virtualRangeResetter" }] : /* istanbul ignore next */ []));
24748
24950
  virtualDisplayData = computed(() => {
24749
- if (!this.virtualScrollEnabled())
24750
- return this.sortedData();
24751
- const items = this.sortedData();
24752
- const start = this.virtualStartIndex();
24753
- const end = this.virtualEndIndex();
24754
- if (start >= items.length)
24755
- return [];
24756
- return items.slice(start, Math.min(end, items.length));
24951
+ return getGridVirtualSlice(this.sortedData(), this.virtualStartIndex(), this.virtualEndIndex(), !!this.virtualScrollEnabled());
24757
24952
  }, ...(ngDevMode ? [{ debugName: "virtualDisplayData" }] : /* istanbul ignore next */ []));
24758
24953
  virtualTotalHeight = computed(() => {
24759
24954
  const count = this.effectiveOptions().serverSide
24760
24955
  ? this.effectiveTotalItems()
24761
24956
  : this.sortedData().length;
24762
- return count * (this.effectiveOptions().virtualScrollRowHeight ?? 48);
24957
+ return getGridVirtualTotalHeight(count, this.effectiveOptions().virtualScrollRowHeight ?? 48);
24763
24958
  }, ...(ngDevMode ? [{ debugName: "virtualTotalHeight" }] : /* istanbul ignore next */ []));
24764
24959
  virtualOffsetY = computed(() => {
24765
- return this.virtualStartIndex() * (this.effectiveOptions().virtualScrollRowHeight ?? 48);
24960
+ return getGridVirtualOffset(this.virtualStartIndex(), this.effectiveOptions().virtualScrollRowHeight ?? 48);
24766
24961
  }, ...(ngDevMode ? [{ debugName: "virtualOffsetY" }] : /* istanbul ignore next */ []));
24767
24962
  pageStart = computed(() => this.effectiveTotalItems() === 0 ? 0 : this.pageIndex() * this.pageSize() + 1, ...(ngDevMode ? [{ debugName: "pageStart" }] : /* istanbul ignore next */ []));
24768
24963
  pageEnd = computed(() => Math.min((this.pageIndex() + 1) * this.pageSize(), this.effectiveTotalItems()), ...(ngDevMode ? [{ debugName: "pageEnd" }] : /* istanbul ignore next */ []));
@@ -24778,11 +24973,11 @@ class JvGridComponent {
24778
24973
  }, ...(ngDevMode ? [{ debugName: "pageRange" }] : /* istanbul ignore next */ []));
24779
24974
  allSelected = computed(() => {
24780
24975
  const rows = this.displayData();
24781
- return rows.length > 0 && rows.every(r => this.selectedIdSet().has(this.getRowId(r)));
24976
+ return areAllGridRowsSelected(rows, this.selectedIdSet(), (row) => this.getRowId(row));
24782
24977
  }, ...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
24783
24978
  someSelected = computed(() => {
24784
24979
  const rows = this.displayData();
24785
- return rows.some(r => this.selectedIdSet().has(this.getRowId(r))) && !this.allSelected();
24980
+ return areSomeGridRowsSelected(rows, this.selectedIdSet(), (row) => this.getRowId(row));
24786
24981
  }, ...(ngDevMode ? [{ debugName: "someSelected" }] : /* istanbul ignore next */ []));
24787
24982
  colspan = computed(() => {
24788
24983
  let count = this.visibleColumns().length;
@@ -24800,9 +24995,6 @@ class JvGridComponent {
24800
24995
  const active = this.activeCell();
24801
24996
  return active.row === rowIndex && active.column === columnIndex ? 0 : -1;
24802
24997
  }
24803
- flattenColumns(columns) {
24804
- return columns.flatMap(col => col.children?.length ? this.flattenColumns(col.children) : [col]);
24805
- }
24806
24998
  onCellFocus(rowIndex, columnIndex) {
24807
24999
  if (this.resolvedOptions().keyboardNavigation) {
24808
25000
  this.activeCell.set({ row: rowIndex, column: columnIndex });
@@ -24812,34 +25004,26 @@ class JvGridComponent {
24812
25004
  if (!this.resolvedOptions().keyboardNavigation)
24813
25005
  return;
24814
25006
  const target = event.target;
24815
- if (target !== event.currentTarget && target.closest('button, input, select, textarea, [contenteditable="true"]')) {
25007
+ if (target !== event.currentTarget &&
25008
+ target.closest('button, input, select, textarea, [contenteditable="true"]')) {
24816
25009
  return;
24817
25010
  }
24818
- const movement = {
24819
- ArrowDown: [1, 0],
24820
- ArrowLeft: [0, -1],
24821
- ArrowRight: [0, 1],
24822
- };
24823
- if (event.key === 'ArrowUp' && rowIndex === 0) {
25011
+ const targetPosition = getCellKeyboardTarget(event.key, rowIndex, columnIndex, this.displayData().length, this.visibleColumns().length);
25012
+ if (!targetPosition)
25013
+ return;
25014
+ if (targetPosition.kind === 'header') {
24824
25015
  event.preventDefault();
24825
- this.activeCell.set({ row: 0, column: columnIndex });
25016
+ this.activeCell.set({ row: targetPosition.row, column: targetPosition.column });
24826
25017
  const table = event.currentTarget.closest('table');
24827
- table?.querySelector(`[data-column-index="${columnIndex}"][role="columnheader"]`)?.focus();
25018
+ table
25019
+ ?.querySelector(`[data-column-index="${targetPosition.column}"][role="columnheader"]`)
25020
+ ?.focus();
24828
25021
  return;
24829
25022
  }
24830
- const delta = event.key === 'ArrowUp' ? [-1, 0] : movement[event.key];
24831
- if (!delta)
24832
- return;
24833
- const nextRow = rowIndex + delta[0];
24834
- const nextColumn = columnIndex + delta[1];
24835
- const rows = this.displayData();
24836
- const columns = this.visibleColumns();
24837
- if (nextRow < 0 || nextRow >= rows.length || nextColumn < 0 || nextColumn >= columns.length)
24838
- return;
24839
25023
  event.preventDefault();
24840
- this.activeCell.set({ row: nextRow, column: nextColumn });
25024
+ this.activeCell.set({ row: targetPosition.row, column: targetPosition.column });
24841
25025
  const table = event.currentTarget.closest('table');
24842
- const nextCell = table?.querySelector(`.jv-grid-keyboard-cell[data-row-index="${nextRow}"][data-column-index="${nextColumn}"]`);
25026
+ const nextCell = table?.querySelector(`.jv-grid-keyboard-cell[data-row-index="${targetPosition.row}"][data-column-index="${targetPosition.column}"]`);
24843
25027
  nextCell?.focus();
24844
25028
  }
24845
25029
  onCellF2(event, rowIndex, columnIndex) {
@@ -24861,7 +25045,10 @@ class JvGridComponent {
24861
25045
  onHeaderKeydown(event, columnIndex) {
24862
25046
  if (!this.resolvedOptions().keyboardNavigation)
24863
25047
  return;
24864
- if (event.key === 'ArrowDown') {
25048
+ const targetPosition = getHeaderKeyboardTarget(event.key, columnIndex, this.visibleColumns().length);
25049
+ if (!targetPosition)
25050
+ return;
25051
+ if (targetPosition.kind === 'cell') {
24865
25052
  const table = event.currentTarget.closest('table');
24866
25053
  const firstCell = table?.querySelector(`.jv-grid-keyboard-cell[data-row-index="0"][data-column-index="${columnIndex}"]`);
24867
25054
  if (firstCell) {
@@ -24871,16 +25058,12 @@ class JvGridComponent {
24871
25058
  }
24872
25059
  return;
24873
25060
  }
24874
- const delta = event.key === 'ArrowLeft' ? -1 : event.key === 'ArrowRight' ? 1 : 0;
24875
- if (!delta)
24876
- return;
24877
- const nextColumn = columnIndex + delta;
24878
- if (nextColumn < 0 || nextColumn >= this.visibleColumns().length)
24879
- return;
24880
25061
  event.preventDefault();
24881
- this.activeCell.set({ row: 0, column: nextColumn });
25062
+ this.activeCell.set({ row: targetPosition.row, column: targetPosition.column });
24882
25063
  const table = event.currentTarget.closest('table');
24883
- table?.querySelector(`[data-column-index="${nextColumn}"][role="columnheader"]`)?.focus();
25064
+ table
25065
+ ?.querySelector(`[data-column-index="${targetPosition.column}"][role="columnheader"]`)
25066
+ ?.focus();
24884
25067
  }
24885
25068
  editState = signal(null, ...(ngDevMode ? [{ debugName: "editState" }] : /* istanbul ignore next */ []));
24886
25069
  t = (key, params) => this.translationService.translate(key, params);
@@ -24892,7 +25075,7 @@ class JvGridComponent {
24892
25075
  const tb = this.trackBy();
24893
25076
  if (typeof tb === 'function')
24894
25077
  return tb(row, index ?? 0);
24895
- if (row && typeof row === 'object' && ('id' in row)) {
25078
+ if (row && typeof row === 'object' && 'id' in row) {
24896
25079
  const id = record['id'];
24897
25080
  if (typeof id === 'string' || typeof id === 'number')
24898
25081
  return id;
@@ -24912,106 +25095,22 @@ class JvGridComponent {
24912
25095
  return this.getRawCellValue(row, col);
24913
25096
  }
24914
25097
  getEditValue(row, col) {
24915
- const val = this.getCellValue(row, col);
24916
- const editType = this.editType(col);
24917
- if (editType === 'date')
24918
- return this.toDateInputValue(val);
24919
- if (editType === 'datetime')
24920
- return this.toDateTimeInputValue(val);
24921
- if (editType === 'time')
24922
- return this.toTimeInputValue(val);
24923
- return val == null ? '' : String(val);
25098
+ return getGridEditValue(this.getCellValue(row, col), col, (value) => this.formatService.toDate(value));
24924
25099
  }
24925
25100
  getBooleanEditValue(row, col) {
24926
- const value = this.getCellValue(row, col);
24927
- return value === true || value === 'true' || value === 1 || value === '1';
25101
+ return isGridBooleanTrue(this.getCellValue(row, col));
24928
25102
  }
24929
25103
  isBooleanTrue(value) {
24930
- return value === true || value === 'true' || value === 1 || value === '1';
25104
+ return isGridBooleanTrue(value);
24931
25105
  }
24932
25106
  editType(col) {
24933
- if (col.editType)
24934
- return col.editType;
24935
- switch (col.type) {
24936
- case 'integer':
24937
- case 'number':
24938
- case 'currency':
24939
- case 'percent':
24940
- return 'number';
24941
- case 'date':
24942
- return 'date';
24943
- case 'time':
24944
- return 'time';
24945
- case 'datetime':
24946
- return 'datetime';
24947
- case 'boolean':
24948
- return 'boolean';
24949
- default:
24950
- return 'text';
24951
- }
25107
+ return getGridEditType(col);
24952
25108
  }
24953
25109
  inputType(col) {
24954
- switch (this.editType(col)) {
24955
- case 'number': return 'number';
24956
- case 'date': return 'date';
24957
- case 'time': return 'time';
24958
- case 'datetime': return 'datetime-local';
24959
- default: return 'text';
24960
- }
25110
+ return getGridInputType(col);
24961
25111
  }
24962
25112
  normalizeEditValue(col, value) {
24963
- switch (this.editType(col)) {
24964
- case 'number': {
24965
- if (value === '' || value == null)
24966
- return null;
24967
- const number = Number(value);
24968
- return Number.isFinite(number) ? number : null;
24969
- }
24970
- case 'date':
24971
- return this.toDateInputValue(value) || null;
24972
- case 'datetime': {
24973
- if (value === '' || value == null)
24974
- return null;
24975
- const date = new Date(String(value));
24976
- return Number.isNaN(date.getTime()) ? null : date.toISOString();
24977
- }
24978
- case 'time': {
24979
- if (value === '' || value == null)
24980
- return null;
24981
- const time = String(value);
24982
- return /^\d{2}:\d{2}$/.test(time) ? `${time}:00` : time;
24983
- }
24984
- case 'boolean':
24985
- return Boolean(value);
24986
- default:
24987
- return value;
24988
- }
24989
- }
24990
- toDateInputValue(value) {
24991
- if (typeof value === 'string') {
24992
- const match = value.match(/^\d{4}-\d{2}-\d{2}/);
24993
- if (match)
24994
- return match[0];
24995
- }
24996
- const date = this.formatService.toDate(value);
24997
- if (!date)
24998
- return '';
24999
- return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;
25000
- }
25001
- toDateTimeInputValue(value) {
25002
- const date = this.formatService.toDate(value);
25003
- if (!date)
25004
- return '';
25005
- const pad = (part) => String(part).padStart(2, '0');
25006
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
25007
- }
25008
- toTimeInputValue(value) {
25009
- if (typeof value === 'string' && /^\d{2}:\d{2}(:\d{2})?$/.test(value))
25010
- return value.slice(0, 5);
25011
- const date = this.formatService.toDate(value);
25012
- if (!date)
25013
- return '';
25014
- return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
25113
+ return normalizeGridEditValue(col, value, (input) => this.formatService.toDate(input));
25015
25114
  }
25016
25115
  formatValue(value, col, _row) {
25017
25116
  if (value == null)
@@ -25098,7 +25197,7 @@ class JvGridComponent {
25098
25197
  return (this.resolvedOptions().editable || col.editable === true) && col.editable !== false;
25099
25198
  }
25100
25199
  groupColspan(group) {
25101
- return this.flattenColumns(group.children ?? []).filter(col => !col.hidden).length || 1;
25200
+ return flattenGridColumns(group.children ?? []).filter((col) => !col.hidden).length || 1;
25102
25201
  }
25103
25202
  isEditingCell(row, col) {
25104
25203
  const es = this.editState();
@@ -25109,13 +25208,13 @@ class JvGridComponent {
25109
25208
  return es !== null && es.rowId === this.getRowId(row);
25110
25209
  }
25111
25210
  getEditOptions(col) {
25112
- return (col.editOptions || []).map(opt => ({
25211
+ return (col.editOptions || []).map((opt) => ({
25113
25212
  label: opt.label,
25114
25213
  value: String(opt.value),
25115
25214
  }));
25116
25215
  }
25117
25216
  findColumn(key) {
25118
- return this.visibleColumns().find(c => this.colKey(c) === key) ?? { key, header: key };
25217
+ return this.visibleColumns().find((c) => this.colKey(c) === key) ?? { key, header: key };
25119
25218
  }
25120
25219
  toggleSort(col, event) {
25121
25220
  if (col.sortable === false || !this.resolvedOptions().sortable)
@@ -25153,13 +25252,7 @@ class JvGridComponent {
25153
25252
  return this.selectedIdSet().has(this.getRowId(row));
25154
25253
  }
25155
25254
  toggleRow(row) {
25156
- const id = this.getRowId(row);
25157
- const set = new Set(this.selectedIdSet());
25158
- if (set.has(id))
25159
- set.delete(id);
25160
- else
25161
- set.add(id);
25162
- const nextIds = Array.from(set);
25255
+ const nextIds = toggleGridRowSelection(this.selectedIdSet(), this.getRowId(row));
25163
25256
  if (this.selectedIds() === null)
25164
25257
  this.internalSelectedIds.set(nextIds);
25165
25258
  this.selectionChange.emit(nextIds);
@@ -25173,15 +25266,7 @@ class JvGridComponent {
25173
25266
  this.pageChange.emit({ pageIndex: 0, pageSize });
25174
25267
  }
25175
25268
  toggleSelectAll(checked) {
25176
- const set = new Set(this.selectedIdSet());
25177
- const rows = this.displayData();
25178
- rows.forEach(r => {
25179
- if (checked)
25180
- set.add(this.getRowId(r));
25181
- else
25182
- set.delete(this.getRowId(r));
25183
- });
25184
- const nextIds = Array.from(set);
25269
+ const nextIds = toggleGridRowsSelection(this.selectedIdSet(), this.displayData(), checked, (row) => this.getRowId(row));
25185
25270
  if (this.selectedIds() === null)
25186
25271
  this.internalSelectedIds.set(nextIds);
25187
25272
  this.selectionChange.emit(nextIds);
@@ -25205,7 +25290,7 @@ class JvGridComponent {
25205
25290
  }
25206
25291
  onColumnFilterChange(col, value) {
25207
25292
  const key = this.colKey(col);
25208
- this.colFilters.update(f => ({
25293
+ this.colFilters.update((f) => ({
25209
25294
  ...f,
25210
25295
  [key]: { key, value, operator: 'contains' },
25211
25296
  }));
@@ -25215,8 +25300,8 @@ class JvGridComponent {
25215
25300
  getRowLabel(row, _index) {
25216
25301
  const label = this.visibleColumns()
25217
25302
  .slice(0, 2)
25218
- .map(col => this.getCellValue(row, col))
25219
- .filter(v => v != null)
25303
+ .map((col) => this.getCellValue(row, col))
25304
+ .filter((v) => v != null)
25220
25305
  .join(' - ');
25221
25306
  return `${label}${this.isSelected(row) ? ` (${this.t('grid.selected')})` : ''}`;
25222
25307
  }
@@ -25250,7 +25335,7 @@ class JvGridComponent {
25250
25335
  this.editState.set(null);
25251
25336
  }
25252
25337
  onEditValueChange(row, col, value) {
25253
- const option = col.editOptions?.find(item => String(item.value) === value);
25338
+ const option = col.editOptions?.find((item) => String(item.value) === value);
25254
25339
  this.commitEdit(row, col, option?.value ?? value);
25255
25340
  }
25256
25341
  // Column resizing
@@ -25275,7 +25360,7 @@ class JvGridComponent {
25275
25360
  return;
25276
25361
  const diff = event.clientX - this.resizeData.startX;
25277
25362
  const newWidth = Math.max(30, this.resizeData.startWidth + diff);
25278
- this.columnWidths.update(w => ({
25363
+ this.columnWidths.update((w) => ({
25279
25364
  ...w,
25280
25365
  [this.colKey(this.resizeData.col)]: `${newWidth}px`,
25281
25366
  }));
@@ -25310,7 +25395,7 @@ class JvGridComponent {
25310
25395
  return;
25311
25396
  if (this.dragIndex === targetIdx)
25312
25397
  return;
25313
- const cols = this.visibleColumns().map(c => this.colKey(c));
25398
+ const cols = this.visibleColumns().map((c) => this.colKey(c));
25314
25399
  const [moved] = cols.splice(this.dragIndex, 1);
25315
25400
  cols.splice(targetIdx, 0, moved);
25316
25401
  this.columnOrder.set(cols);
@@ -25325,13 +25410,9 @@ class JvGridComponent {
25325
25410
  if (!this.virtualScrollEnabled())
25326
25411
  return;
25327
25412
  const rowHeight = this.effectiveOptions().virtualScrollRowHeight ?? 48;
25328
- const buffer = 5;
25329
- const scrollTop = target.scrollTop;
25330
- const clientHeight = target.clientHeight;
25331
- const start = Math.max(0, Math.floor(scrollTop / rowHeight) - buffer);
25332
- const end = Math.min(this.sortedData().length, Math.ceil((scrollTop + clientHeight) / rowHeight) + buffer);
25333
- this.virtualStartIndex.set(start);
25334
- this.virtualEndIndex.set(end);
25413
+ const range = getGridVirtualRange(target.scrollTop, target.clientHeight, rowHeight, this.sortedData().length);
25414
+ this.virtualStartIndex.set(range.start);
25415
+ this.virtualEndIndex.set(range.end);
25335
25416
  }
25336
25417
  // Export
25337
25418
  exportCsv() {
@@ -25354,753 +25435,23 @@ class JvGridComponent {
25354
25435
  return configuredName.replace(/\.(csv|xlsx)$/i, '') + `.${extension}`;
25355
25436
  }
25356
25437
  buildExportMatrix() {
25357
- const cols = this.visibleColumns().map(c => ({ key: this.colKey(c), header: c.header, column: c }));
25438
+ const cols = this.visibleColumns().map((c) => ({
25439
+ key: this.colKey(c),
25440
+ header: c.header,
25441
+ column: c,
25442
+ }));
25358
25443
  const allData = this.effectiveOptions().serverSide ? this.data() : this.sortedData();
25359
25444
  return {
25360
- header: cols.map(c => c.header),
25361
- rows: allData.map(row => cols.map(col => this.getExportValue(row[col.key], col.column))),
25445
+ header: cols.map((c) => c.header),
25446
+ rows: allData.map((row) => cols.map((col) => this.getExportValue(row[col.key], col.column))),
25362
25447
  };
25363
25448
  }
25364
25449
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
25365
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvGridComponent, isStandalone: true, selector: "jv-grid", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowClick: "rowClick", rowDoubleClick: "rowDoubleClick", actionClick: "actionClick", selectionChange: "selectionChange", pageChange: "pageChange", searchChange: "searchChange", sortChange: "sortChange", columnFilter: "columnFilter", columnResize: "columnResize", columnReorder: "columnReorder", rowEdit: "rowEdit" }, ngImport: i0, template: `
25366
- <div class="jv-grid" role="region" [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()">
25367
- @if (resolvedOptions().searchable || resolvedOptions().exportable) {
25368
- <div class="jv-grid-toolbar">
25369
- @if (resolvedOptions().searchable) {
25370
- <div class="jv-grid-search">
25371
- <jv-icon name="search" [size]="16" />
25372
- <jv-input
25373
- [inputId]="gridSearchInputId"
25374
- [placeholder]="t('grid.searchPlaceholder')"
25375
- [ngModel]="searchTerm()"
25376
- (ngModelChange)="onSearchInput($event)"
25377
- />
25378
- </div>
25379
- }
25380
- @if (resolvedOptions().exportable) {
25381
- <div class="jv-grid-export-actions">
25382
- <jv-button variant="outline" icon="download" (click)="exportCsv()">{{ t('grid.exportCsv') }}</jv-button>
25383
- <jv-button variant="outline" icon="file-spreadsheet" (click)="exportExcel()">{{ t('grid.exportExcel') }}</jv-button>
25384
- </div>
25385
- }
25386
- </div>
25387
- }
25388
-
25389
- @if (resolvedOptions().loading) {
25390
- <div class="jv-grid-loading" role="status" aria-live="polite">
25391
- <span class="jv-grid-spinner" aria-hidden="true"></span>
25392
- <span>{{ t('grid.loading') }}</span>
25393
- </div>
25394
- } @else {
25395
- <div
25396
- class="jv-grid-table-wrap"
25397
- [class.jv-grid-virtual]="virtualScrollEnabled()"
25398
- #tableWrap
25399
- (scroll)="onVirtualScroll($any($event.target))"
25400
- >
25401
- @if (virtualScrollEnabled()) {
25402
- <div class="jv-grid-virtual-spacer" [style.height.px]="virtualTotalHeight()"></div>
25403
- }
25404
- <table
25405
- class="jv-grid-table"
25406
- role="grid"
25407
- [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()"
25408
- [attr.aria-rowcount]="effectiveTotalItems()"
25409
- [attr.aria-colcount]="colspan()"
25410
- [class]="densityClass()"
25411
- [style.position]="virtualScrollEnabled() ? 'relative' : null"
25412
- [style.top.px]="virtualOffsetY()"
25413
- >
25414
- <caption class="sr-only">{{ resolvedOptions().ariaLabel || gridLabel() }}</caption>
25415
-
25416
- @let hasGroupedHeaders = groupedHeaders().length > 0;
25417
-
25418
- <thead class="jv-grid-thead">
25419
- @if (hasGroupedHeaders) {
25420
- <tr>
25421
- @if (resolvedOptions().selectable) {
25422
- <th class="jv-grid-th jv-grid-th-select" scope="col" [attr.rowspan]="2">
25423
- <input
25424
- type="checkbox"
25425
- class="jv-grid-checkbox"
25426
- [checked]="allSelected()"
25427
- (change)="toggleSelectAll($any($event.target).checked)"
25428
- [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
25429
- [indeterminate]="someSelected()"
25430
- />
25431
- </th>
25432
- }
25433
- @for (group of groupedHeaders(); track colKey(group)) {
25434
- <th
25435
- class="jv-grid-th"
25436
- [class]="group.headerClass || ''"
25437
- [class.jv-grid-th-sticky-left]="isStickyLeft(group)"
25438
- [class.jv-grid-th-sticky-right]="isStickyRight(group)"
25439
- [attr.colspan]="groupColspan(group)"
25440
- scope="colgroup"
25441
- [style.text-align]="group.align ?? 'start'"
25442
- >
25443
- <div class="jv-grid-th-content">{{ group.header }}</div>
25444
- </th>
25445
- }
25446
- @if (actions().length > 0) {
25447
- <th class="jv-grid-th jv-grid-th-actions" scope="col" [attr.rowspan]="2">
25448
- <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
25449
- </th>
25450
- }
25451
- </tr>
25452
- }
25453
-
25454
- <tr>
25455
- @if (!hasGroupedHeaders && resolvedOptions().selectable) {
25456
- <th class="jv-grid-th jv-grid-th-select" scope="col">
25457
- <input
25458
- type="checkbox"
25459
- class="jv-grid-checkbox"
25460
- [checked]="allSelected()"
25461
- (change)="toggleSelectAll($any($event.target).checked)"
25462
- [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
25463
- [indeterminate]="someSelected()"
25464
- />
25465
- </th>
25466
- }
25467
- @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
25468
- <th
25469
- class="jv-grid-th"
25470
- [class]="col.headerClass || ''"
25471
- [class.jv-grid-th-sortable]="col.sortable !== false && resolvedOptions().sortable"
25472
- [class.jv-grid-th-sticky-left]="isStickyLeft(col)"
25473
- [class.jv-grid-th-sticky-right]="isStickyRight(col)"
25474
- [class.jv-grid-th-reorderable]="resolvedOptions().reorderableColumns"
25475
- [style.text-align]="col.align ?? 'start'"
25476
- [style.width]="getColumnWidth(col)"
25477
- [style.min-width]="col.minWidth || undefined"
25478
- [style.max-width]="col.maxWidth || undefined"
25479
- scope="col"
25480
- role="columnheader"
25481
- [attr.data-column-index]="colIdx"
25482
- [attr.aria-sort]="getAriaSort(col)"
25483
- [attr.aria-label]="getSortLabel(col)"
25484
- [tabindex]="resolvedOptions().keyboardNavigation ? (activeCell().column === colIdx ? 0 : -1) : (col.sortable !== false && resolvedOptions().sortable ? 0 : -1)"
25485
- (click)="toggleSort(col)"
25486
- (focus)="onHeaderFocus(colIdx)"
25487
- (keydown)="onHeaderKeydown($event, colIdx)"
25488
- (keydown.enter)="toggleSort(col)"
25489
- (keydown.space)="toggleSort(col, $event)"
25490
- draggable="{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}"
25491
- (dragstart)="onDragStart($event, colIdx)"
25492
- (dragover)="onDragOver($event, colIdx)"
25493
- (drop)="onDrop($event, colIdx)"
25494
- (dragend)="onDragEnd($event)"
25495
- >
25496
- <div class="jv-grid-th-content">
25497
- <span>{{ col.header }}</span>
25498
- @if (col.sortable !== false && resolvedOptions().sortable) {
25499
- <span class="jv-grid-sort-icon" aria-hidden="true">
25500
- @if (sortState().columnKey === colKey(col)) {
25501
- @if (sortState().direction === 'asc') { ▲ }
25502
- @if (sortState().direction === 'desc') { ▼ }
25503
- }
25504
- </span>
25505
- }
25506
- </div>
25507
- @if (isColumnResizable(col)) {
25508
- <div
25509
- class="jv-grid-resize-handle"
25510
- (mousedown)="onResizeStart($event, col, colIdx)"
25511
- ></div>
25512
- }
25513
- </th>
25514
- }
25515
- @if (!hasGroupedHeaders && actions().length > 0) {
25516
- <th class="jv-grid-th jv-grid-th-actions" scope="col">
25517
- <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
25518
- </th>
25519
- }
25520
- </tr>
25521
-
25522
- @if (resolvedOptions().columnFilters) {
25523
- <tr class="jv-grid-filter-row">
25524
- @if (resolvedOptions().selectable) {
25525
- <th class="jv-grid-th jv-grid-th-select"></th>
25526
- }
25527
- @for (col of visibleColumns(); track colKey(col)) {
25528
- <th class="jv-grid-th jv-grid-th-filter">
25529
- @if (col.filterable !== false) {
25530
- <input
25531
- class="jv-grid-filter-input"
25532
- [placeholder]="t('grid.filterPlaceholder')"
25533
- [attr.aria-label]="t('grid.filterForColumn', { column: col.header })"
25534
- [ngModel]="colFilters()[colKey(col)]?.value ?? ''"
25535
- (ngModelChange)="onColumnFilterChange(col, $event)"
25536
- />
25537
- }
25538
- </th>
25539
- }
25540
- @if (actions().length > 0) {
25541
- <th class="jv-grid-th jv-grid-th-actions"></th>
25542
- }
25543
- </tr>
25544
- }
25545
- </thead>
25546
-
25547
- <tbody class="jv-grid-tbody">
25548
- @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {
25549
- <tr>
25550
- <td class="jv-grid-empty" [attr.colspan]="colspan()">
25551
- {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}
25552
- </td>
25553
- </tr>
25554
- } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {
25555
- <tr>
25556
- <td class="jv-grid-empty" [attr.colspan]="colspan()">
25557
- {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}
25558
- </td>
25559
- </tr>
25560
- } @else {
25561
- @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {
25562
- <tr
25563
- class="jv-grid-tr"
25564
- role="row"
25565
- [class.jv-grid-tr-editing]="isRowEditing(row)"
25566
- [class.jv-grid-tr-selected]="isSelected(row)"
25567
- [attr.aria-selected]="resolvedOptions().selectable ? isSelected(row) : null"
25568
- (click)="onRowClick(row)"
25569
- (dblclick)="onRowDblClick(row)"
25570
- (keydown.enter)="onRowClick(row)"
25571
- (keydown.space)="onRowClick($event, row)"
25572
- [tabindex]="-1"
25573
- >
25574
- @if (resolvedOptions().selectable) {
25575
- <td
25576
- class="jv-grid-td jv-grid-td-select"
25577
- [class.jv-grid-td-sticky-left]="resolvedOptions().stickyColumns"
25578
- >
25579
- <input
25580
- type="checkbox"
25581
- class="jv-grid-checkbox"
25582
- [checked]="isSelected(row)"
25583
- (change)="toggleRow(row); $event.stopPropagation()"
25584
- (keydown.space)="$event.stopPropagation()"
25585
- [attr.aria-label]="getRowLabel(row, $index)"
25586
- />
25587
- </td>
25588
- }
25589
- @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
25590
- <td
25591
- class="jv-grid-td"
25592
- [attr.role]="resolvedOptions().keyboardNavigation ? 'gridcell' : null"
25593
- [class]="col.cellClass || ''"
25594
- [class.jv-grid-td-sticky-left]="isStickyLeft(col)"
25595
- [class.jv-grid-td-sticky-right]="isStickyRight(col)"
25596
- [class.jv-grid-td-editable]="isCellEditable(col)"
25597
- [style.text-align]="col.align ?? 'start'"
25598
- (dblclick)="startEdit(row, col, $event)"
25599
- (keydown.enter)="startEdit(row, col, $event)"
25600
- [attr.data-row-index]="rowIdx"
25601
- [attr.data-column-index]="colIdx"
25602
- [class.jv-grid-keyboard-cell]="resolvedOptions().keyboardNavigation"
25603
- [attr.tabindex]="cellTabIndex(rowIdx, colIdx, col)"
25604
- (focus)="onCellFocus(rowIdx, colIdx)"
25605
- (keydown.f2)="onCellF2($event, rowIdx, colIdx)"
25606
- (keydown)="onCellKeydown($event, rowIdx, colIdx)"
25607
- >
25608
- @if (isEditingCell(row, col)) {
25609
- @if (editType(col) === 'select' && col.editOptions) {
25610
- <jv-select
25611
- [options]="getEditOptions(col)"
25612
- [modelValue]="getEditValue(row, col)"
25613
- (selectionChange)="onEditValueChange(row, col, $event)"
25614
- />
25615
- } @else if (editType(col) === 'boolean') {
25616
- <input
25617
- type="checkbox"
25618
- class="jv-grid-checkbox"
25619
- [checked]="getBooleanEditValue(row, col)"
25620
- (change)="commitEdit(row, col, normalizeEditValue(col, $any($event.target).checked))"
25621
- (keydown.enter)="commitEditFromKeyboard($event, row, col, normalizeEditValue(col, $any($event.target).checked))"
25622
- (keydown.escape)="cancelEdit()"
25623
- [attr.aria-label]="col.header"
25624
- />
25625
- } @else {
25626
- <input
25627
- class="jv-grid-edit-input"
25628
- [type]="inputType(col)"
25629
- [value]="getEditValue(row, col)"
25630
- (blur)="commitEdit(row, col, normalizeEditValue(col, $any($event.target).value))"
25631
- (keydown.enter)="commitEditFromKeyboard($event, row, col, normalizeEditValue(col, $any($event.target).value))"
25632
- (keydown.escape)="cancelEdit()"
25633
- />
25634
- }
25635
- } @else {
25636
- @let value = getCellValue(row, col);
25637
- @if (col.type === 'boolean') {
25638
- @if (isBooleanTrue(value)) {
25639
- <span class="jv-grid-boolean jv-grid-boolean-true" [attr.aria-label]="t('grid.booleanYes')">{{ t('grid.booleanYes') }}</span>
25640
- } @else {
25641
- <span class="jv-grid-boolean jv-grid-boolean-false" [attr.aria-label]="t('grid.booleanNo')">{{ t('grid.booleanNo') }}</span>
25642
- }
25643
- } @else if (col.format) {
25644
- {{ col.format(value, row) }}
25645
- } @else {
25646
- {{ formatValue(value, col, row) }}
25647
- }
25648
- }
25649
- </td>
25650
- }
25651
- @if (actions().length > 0) {
25652
- <td
25653
- class="jv-grid-td jv-grid-td-actions"
25654
- [class.jv-grid-td-sticky-right]="resolvedOptions().stickyColumns"
25655
- >
25656
- <div class="jv-grid-actions-group">
25657
- @for (action of actions(); track action.id) {
25658
- <jv-button
25659
- [variant]="actionVariant(action.variant)"
25660
- [icon]="action.icon ?? null"
25661
- [disabled]="action.disabled?.(row) ?? false"
25662
- [attr.aria-label]="action.label"
25663
- [title]="action.label"
25664
- (click)="onActionClick(action, row); $event.stopPropagation()"
25665
- ></jv-button>
25666
- }
25667
- </div>
25668
- </td>
25669
- }
25670
- </tr>
25671
- }
25672
- }
25673
- </tbody>
25674
- </table>
25675
- </div>
25676
- }
25677
-
25678
- @if (resolvedOptions().pageable && !virtualScrollEnabled() && effectiveTotalPages() > 1 && !resolvedOptions().loading) {
25679
- <nav class="jv-grid-pagination" role="navigation" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
25680
- <div class="jv-grid-pagination-info" aria-live="polite" aria-atomic="true">
25681
- {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: effectiveTotalItems() }) }}
25682
- </div>
25683
- <div class="jv-grid-pagination-controls">
25684
- @if (pageSizeOptions().length > 0) {
25685
- <jv-select
25686
- [options]="pageSizeOptions()"
25687
- [modelValue]="pageSizeStr()"
25688
- [ariaLabel]="t('grid.itemsPerPage')"
25689
- (selectionChange)="onPageSizeChange($event)"
25690
- />
25691
- }
25692
- <jv-button
25693
- variant="outline"
25694
- icon="chevrons-left"
25695
- [disabled]="pageIndex() === 0"
25696
- (click)="goToPage(0)"
25697
- [attr.aria-label]="t('grid.pageFirst')"
25698
- ></jv-button>
25699
- <jv-button
25700
- variant="outline"
25701
- icon="chevron-left"
25702
- [disabled]="pageIndex() === 0"
25703
- (click)="goToPage(pageIndex() - 1)"
25704
- [attr.aria-label]="t('grid.pagePrev')"
25705
- ></jv-button>
25706
- @for (p of pageRange(); track p) {
25707
- <jv-button
25708
- [variant]="p === pageIndex() ? 'primary' : 'outline'"
25709
- [attr.aria-current]="p === pageIndex() ? 'page' : null"
25710
- [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })"
25711
- (click)="goToPage(p)"
25712
- >{{ p + 1 }}</jv-button>
25713
- }
25714
- <jv-button
25715
- variant="outline"
25716
- icon="chevron-right"
25717
- [disabled]="pageIndex() >= effectiveTotalPages() - 1"
25718
- (click)="goToPage(pageIndex() + 1)"
25719
- [attr.aria-label]="t('grid.pageNext')"
25720
- ></jv-button>
25721
- <jv-button
25722
- variant="outline"
25723
- icon="chevrons-right"
25724
- [disabled]="pageIndex() >= effectiveTotalPages() - 1"
25725
- (click)="goToPage(effectiveTotalPages() - 1)"
25726
- [attr.aria-label]="t('grid.pageLast')"
25727
- ></jv-button>
25728
- </div>
25729
- </nav>
25730
- }
25731
- </div>
25732
- `, isInline: true, styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-export-actions{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-table-wrap{overflow-x:auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);position:relative}.jv-grid-virtual{max-height:600px}.jv-grid-virtual-spacer{pointer-events:none}.jv-grid-table{width:100%;border-collapse:collapse;background:var(--jv-color-surface)}.jv-grid-thead{background:var(--jv-color-surface-muted)}.jv-grid-th{padding:var(--jv-spacing-sm) var(--jv-spacing-md);font-weight:600;font-size:.875rem;color:var(--jv-color-foreground-muted);text-align:start;white-space:nowrap;border-bottom:1px solid var(--jv-color-border);-webkit-user-select:none;user-select:none;position:relative}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.jv-grid-th-reorderable{cursor:grab}.jv-grid-th-reorderable:active{cursor:grabbing}.jv-grid-th-sticky-left{position:sticky;left:0;z-index:2;background:var(--jv-color-surface-muted);border-right:1px solid var(--jv-color-border)}.jv-grid-th-sticky-right{position:sticky;right:0;z-index:2;background:var(--jv-color-surface-muted);border-left:1px solid var(--jv-color-border)}.jv-grid-th-content{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-sort-icon{font-size:.75rem;line-height:1}.jv-grid-th-select,.jv-grid-td-select{width:3rem;text-align:center}.jv-grid-th-actions,.jv-grid-td-actions{width:1%;white-space:nowrap}.jv-grid-td{padding:var(--jv-spacing-sm) var(--jv-spacing-md);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);border-bottom:1px solid var(--jv-color-border)}.jv-grid-keyboard-cell:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-td-sticky-left{position:sticky;left:0;z-index:1;background:var(--jv-color-surface);border-right:1px solid var(--jv-color-border)}.jv-grid-td-sticky-right{position:sticky;right:0;z-index:1;background:var(--jv-color-surface);border-left:1px solid var(--jv-color-border)}.jv-grid-t:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr{transition:background-color .12s ease}.jv-grid-tr:hover{background:var(--jv-color-surface-muted)}.jv-grid-tr:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr-editing{background:var(--jv-color-surface-highlight, rgba(59, 130, 246, .05))}.jv-grid-tr-selected{background:var(--jv-color-primary-light, rgba(59, 130, 246, .08))}.jv-grid-checkbox{width:1rem;height:1rem;accent-color:var(--jv-color-primary);cursor:pointer}.jv-grid-boolean{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:999px;font-size:.75rem;font-weight:700}.jv-grid-boolean-true{background:var(--jv-color-success);color:#fff}.jv-grid-boolean-false{background:var(--jv-color-danger);color:#fff}.jv-grid-empty{padding:var(--jv-spacing-xl) var(--jv-spacing-md);text-align:center;color:var(--jv-color-foreground-muted);font-style:italic}.jv-grid-actions-group{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-loading{display:flex;align-items:center;justify-content:center;gap:var(--jv-spacing-md);padding:var(--jv-spacing-xl);color:var(--jv-color-foreground-muted)}.jv-grid-spinner{width:1.25rem;height:1.25rem;border:2px solid var(--jv-color-border);border-right-color:var(--jv-color-primary);border-radius:999px;animation:jv-grid-spin .8s linear infinite}@keyframes jv-grid-spin{to{transform:rotate(360deg)}}.jv-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-grid-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-xs);flex-wrap:wrap}@media(max-width:640px){.jv-grid-pagination{flex-direction:column;align-items:stretch;gap:var(--jv-spacing-sm)}.jv-grid-pagination-info{text-align:center}.jv-grid-pagination-controls{justify-content:center}.jv-grid-toolbar{flex-direction:column;align-items:stretch}.jv-grid-search{width:100%}.jv-grid-export-actions{justify-content:stretch}.jv-grid-export-actions jv-button{flex:1}}.jv-grid-resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;background:transparent;z-index:3}.jv-grid-resize-handle:hover,.jv-grid-resize-handle:active{background:var(--jv-color-primary)}.jv-grid-filter-row .jv-grid-th{padding:var(--jv-spacing-xs) var(--jv-spacing-sm)}.jv-grid-filter-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:.8rem;box-sizing:border-box}.jv-grid-td-editable{cursor:pointer}.jv-grid-td-editable:hover{box-shadow:inset 0 0 0 1px var(--jv-color-primary)}.jv-grid-edit-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-primary);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);box-sizing:border-box}.jv-grid-edit-input:focus{outline:2px solid var(--jv-color-primary);outline-offset:-1px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: JvButtonComponent, selector: "jv-button", inputs: ["type", "variant", "icon", "iconPosition", "loading", "disabled"] }, { kind: "component", type: JvIconComponent, selector: "jv-icon", inputs: ["name", "size", "strokeWidth", "decorative", "ariaLabel"] }, { kind: "component", type: JvInputComponent, selector: "jv-input", inputs: ["type", "placeholder", "required", "invalid", "describedBy", "inputId"] }, { kind: "component", type: JvSelectComponent, selector: "jv-select", inputs: ["options", "placeholder", "required", "invalid", "describedBy", "ariaLabel", "inputId", "modelValue"], outputs: ["selectionChange"] }] });
25450
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvGridComponent, isStandalone: true, selector: "jv-grid", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowClick: "rowClick", rowDoubleClick: "rowDoubleClick", actionClick: "actionClick", selectionChange: "selectionChange", pageChange: "pageChange", searchChange: "searchChange", sortChange: "sortChange", columnFilter: "columnFilter", columnResize: "columnResize", columnReorder: "columnReorder", rowEdit: "rowEdit" }, ngImport: i0, template: "<div class=\"jv-grid\" role=\"region\" [attr.aria-label]=\"resolvedOptions().ariaLabel || gridLabel()\">\n @if (resolvedOptions().searchable || resolvedOptions().exportable) {\n <div class=\"jv-grid-toolbar\">\n @if (resolvedOptions().searchable) {\n <div class=\"jv-grid-search\">\n <jv-icon name=\"search\" [size]=\"16\" />\n <jv-input\n [inputId]=\"gridSearchInputId\"\n [placeholder]=\"t('grid.searchPlaceholder')\"\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"onSearchInput($event)\"\n />\n </div>\n }\n @if (resolvedOptions().exportable) {\n <div class=\"jv-grid-export-actions\">\n <jv-button variant=\"outline\" icon=\"download\" (click)=\"exportCsv()\">{{\n t('grid.exportCsv')\n }}</jv-button>\n <jv-button variant=\"outline\" icon=\"file-spreadsheet\" (click)=\"exportExcel()\">{{\n t('grid.exportExcel')\n }}</jv-button>\n </div>\n }\n </div>\n }\n\n @if (resolvedOptions().loading) {\n <div class=\"jv-grid-loading\" role=\"status\" aria-live=\"polite\">\n <span class=\"jv-grid-spinner\" aria-hidden=\"true\"></span>\n <span>{{ t('grid.loading') }}</span>\n </div>\n } @else {\n <div\n class=\"jv-grid-table-wrap\"\n [class.jv-grid-virtual]=\"virtualScrollEnabled()\"\n #tableWrap\n (scroll)=\"onVirtualScroll($any($event.target))\"\n >\n @if (virtualScrollEnabled()) {\n <div class=\"jv-grid-virtual-spacer\" [style.height.px]=\"virtualTotalHeight()\"></div>\n }\n <table\n class=\"jv-grid-table\"\n role=\"grid\"\n [attr.aria-label]=\"resolvedOptions().ariaLabel || gridLabel()\"\n [attr.aria-rowcount]=\"effectiveTotalItems()\"\n [attr.aria-colcount]=\"colspan()\"\n [class]=\"densityClass()\"\n [style.position]=\"virtualScrollEnabled() ? 'relative' : null\"\n [style.top.px]=\"virtualOffsetY()\"\n >\n <caption class=\"sr-only\">\n {{\n resolvedOptions().ariaLabel || gridLabel()\n }}\n </caption>\n\n @let hasGroupedHeaders = groupedHeaders().length > 0;\n\n <thead class=\"jv-grid-thead\">\n @if (hasGroupedHeaders) {\n <tr>\n @if (resolvedOptions().selectable) {\n <th class=\"jv-grid-th jv-grid-th-select\" scope=\"col\" [attr.rowspan]=\"2\">\n <input\n type=\"checkbox\"\n class=\"jv-grid-checkbox\"\n [checked]=\"allSelected()\"\n (change)=\"toggleSelectAll($any($event.target).checked)\"\n [attr.aria-label]=\"allSelected() ? t('grid.deselectAll') : t('grid.selectAll')\"\n [indeterminate]=\"someSelected()\"\n />\n </th>\n }\n @for (group of groupedHeaders(); track colKey(group)) {\n <th\n class=\"jv-grid-th\"\n [class]=\"group.headerClass || ''\"\n [class.jv-grid-th-sticky-left]=\"isStickyLeft(group)\"\n [class.jv-grid-th-sticky-right]=\"isStickyRight(group)\"\n [attr.colspan]=\"groupColspan(group)\"\n scope=\"colgroup\"\n [style.text-align]=\"group.align ?? 'start'\"\n >\n <div class=\"jv-grid-th-content\">{{ group.header }}</div>\n </th>\n }\n @if (actions().length > 0) {\n <th class=\"jv-grid-th jv-grid-th-actions\" scope=\"col\" [attr.rowspan]=\"2\">\n <span class=\"sr-only\">{{ t('grid.actionsLabel', { label: '' }) }}</span>\n </th>\n }\n </tr>\n }\n\n <tr>\n @if (!hasGroupedHeaders && resolvedOptions().selectable) {\n <th class=\"jv-grid-th jv-grid-th-select\" scope=\"col\">\n <input\n type=\"checkbox\"\n class=\"jv-grid-checkbox\"\n [checked]=\"allSelected()\"\n (change)=\"toggleSelectAll($any($event.target).checked)\"\n [attr.aria-label]=\"allSelected() ? t('grid.deselectAll') : t('grid.selectAll')\"\n [indeterminate]=\"someSelected()\"\n />\n </th>\n }\n @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {\n <th\n class=\"jv-grid-th\"\n [class]=\"col.headerClass || ''\"\n [class.jv-grid-th-sortable]=\"col.sortable !== false && resolvedOptions().sortable\"\n [class.jv-grid-th-sticky-left]=\"isStickyLeft(col)\"\n [class.jv-grid-th-sticky-right]=\"isStickyRight(col)\"\n [class.jv-grid-th-reorderable]=\"resolvedOptions().reorderableColumns\"\n [style.text-align]=\"col.align ?? 'start'\"\n [style.width]=\"getColumnWidth(col)\"\n [style.min-width]=\"col.minWidth || undefined\"\n [style.max-width]=\"col.maxWidth || undefined\"\n scope=\"col\"\n role=\"columnheader\"\n [attr.data-column-index]=\"colIdx\"\n [attr.aria-sort]=\"getAriaSort(col)\"\n [attr.aria-label]=\"getSortLabel(col)\"\n [tabindex]=\"\n resolvedOptions().keyboardNavigation\n ? activeCell().column === colIdx\n ? 0\n : -1\n : col.sortable !== false && resolvedOptions().sortable\n ? 0\n : -1\n \"\n (click)=\"toggleSort(col)\"\n (focus)=\"onHeaderFocus(colIdx)\"\n (keydown)=\"onHeaderKeydown($event, colIdx)\"\n (keydown.enter)=\"toggleSort(col)\"\n (keydown.space)=\"toggleSort(col, $event)\"\n draggable=\"{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}\"\n (dragstart)=\"onDragStart($event, colIdx)\"\n (dragover)=\"onDragOver($event, colIdx)\"\n (drop)=\"onDrop($event, colIdx)\"\n (dragend)=\"onDragEnd($event)\"\n >\n <div class=\"jv-grid-th-content\">\n <span>{{ col.header }}</span>\n @if (col.sortable !== false && resolvedOptions().sortable) {\n <span class=\"jv-grid-sort-icon\" aria-hidden=\"true\">\n @if (sortState().columnKey === colKey(col)) {\n @if (sortState().direction === 'asc') {\n \u25B2\n }\n @if (sortState().direction === 'desc') {\n \u25BC\n }\n }\n </span>\n }\n </div>\n @if (isColumnResizable(col)) {\n <div\n class=\"jv-grid-resize-handle\"\n (mousedown)=\"onResizeStart($event, col, colIdx)\"\n ></div>\n }\n </th>\n }\n @if (!hasGroupedHeaders && actions().length > 0) {\n <th class=\"jv-grid-th jv-grid-th-actions\" scope=\"col\">\n <span class=\"sr-only\">{{ t('grid.actionsLabel', { label: '' }) }}</span>\n </th>\n }\n </tr>\n\n @if (resolvedOptions().columnFilters) {\n <tr class=\"jv-grid-filter-row\">\n @if (resolvedOptions().selectable) {\n <th class=\"jv-grid-th jv-grid-th-select\"></th>\n }\n @for (col of visibleColumns(); track colKey(col)) {\n <th class=\"jv-grid-th jv-grid-th-filter\">\n @if (col.filterable !== false) {\n <input\n class=\"jv-grid-filter-input\"\n [placeholder]=\"t('grid.filterPlaceholder')\"\n [attr.aria-label]=\"t('grid.filterForColumn', { column: col.header })\"\n [ngModel]=\"colFilters()[colKey(col)]?.value ?? ''\"\n (ngModelChange)=\"onColumnFilterChange(col, $event)\"\n />\n }\n </th>\n }\n @if (actions().length > 0) {\n <th class=\"jv-grid-th jv-grid-th-actions\"></th>\n }\n </tr>\n }\n </thead>\n\n <tbody class=\"jv-grid-tbody\">\n @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {\n <tr>\n <td class=\"jv-grid-empty\" [attr.colspan]=\"colspan()\">\n {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}\n </td>\n </tr>\n } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {\n <tr>\n <td class=\"jv-grid-empty\" [attr.colspan]=\"colspan()\">\n {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}\n </td>\n </tr>\n } @else {\n @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {\n <tr\n class=\"jv-grid-tr\"\n role=\"row\"\n [class.jv-grid-tr-editing]=\"isRowEditing(row)\"\n [class.jv-grid-tr-selected]=\"isSelected(row)\"\n [attr.aria-selected]=\"resolvedOptions().selectable ? isSelected(row) : null\"\n (click)=\"onRowClick(row)\"\n (dblclick)=\"onRowDblClick(row)\"\n (keydown.enter)=\"onRowClick(row)\"\n (keydown.space)=\"onRowClick($event, row)\"\n [tabindex]=\"-1\"\n >\n @if (resolvedOptions().selectable) {\n <td\n class=\"jv-grid-td jv-grid-td-select\"\n [class.jv-grid-td-sticky-left]=\"resolvedOptions().stickyColumns\"\n >\n <input\n type=\"checkbox\"\n class=\"jv-grid-checkbox\"\n [checked]=\"isSelected(row)\"\n (change)=\"toggleRow(row); $event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"getRowLabel(row, $index)\"\n />\n </td>\n }\n @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {\n <td\n class=\"jv-grid-td\"\n [attr.role]=\"resolvedOptions().keyboardNavigation ? 'gridcell' : null\"\n [class]=\"col.cellClass || ''\"\n [class.jv-grid-td-sticky-left]=\"isStickyLeft(col)\"\n [class.jv-grid-td-sticky-right]=\"isStickyRight(col)\"\n [class.jv-grid-td-editable]=\"isCellEditable(col)\"\n [style.text-align]=\"col.align ?? 'start'\"\n (dblclick)=\"startEdit(row, col, $event)\"\n (keydown.enter)=\"startEdit(row, col, $event)\"\n [attr.data-row-index]=\"rowIdx\"\n [attr.data-column-index]=\"colIdx\"\n [class.jv-grid-keyboard-cell]=\"resolvedOptions().keyboardNavigation\"\n [attr.tabindex]=\"cellTabIndex(rowIdx, colIdx, col)\"\n (focus)=\"onCellFocus(rowIdx, colIdx)\"\n (keydown.f2)=\"onCellF2($event, rowIdx, colIdx)\"\n (keydown)=\"onCellKeydown($event, rowIdx, colIdx)\"\n >\n @if (isEditingCell(row, col)) {\n @if (editType(col) === 'select' && col.editOptions) {\n <jv-select\n [options]=\"getEditOptions(col)\"\n [modelValue]=\"getEditValue(row, col)\"\n (selectionChange)=\"onEditValueChange(row, col, $event)\"\n />\n } @else if (editType(col) === 'boolean') {\n <input\n type=\"checkbox\"\n class=\"jv-grid-checkbox\"\n [checked]=\"getBooleanEditValue(row, col)\"\n (change)=\"\n commitEdit(\n row,\n col,\n normalizeEditValue(col, $any($event.target).checked)\n )\n \"\n (keydown.enter)=\"\n commitEditFromKeyboard(\n $event,\n row,\n col,\n normalizeEditValue(col, $any($event.target).checked)\n )\n \"\n (keydown.escape)=\"cancelEdit()\"\n [attr.aria-label]=\"col.header\"\n />\n } @else {\n <input\n class=\"jv-grid-edit-input\"\n [type]=\"inputType(col)\"\n [value]=\"getEditValue(row, col)\"\n (blur)=\"\n commitEdit(row, col, normalizeEditValue(col, $any($event.target).value))\n \"\n (keydown.enter)=\"\n commitEditFromKeyboard(\n $event,\n row,\n col,\n normalizeEditValue(col, $any($event.target).value)\n )\n \"\n (keydown.escape)=\"cancelEdit()\"\n />\n }\n } @else {\n @let value = getCellValue(row, col);\n @if (col.type === 'boolean') {\n @if (isBooleanTrue(value)) {\n <span\n class=\"jv-grid-boolean jv-grid-boolean-true\"\n [attr.aria-label]=\"t('grid.booleanYes')\"\n >{{ t('grid.booleanYes') }}</span\n >\n } @else {\n <span\n class=\"jv-grid-boolean jv-grid-boolean-false\"\n [attr.aria-label]=\"t('grid.booleanNo')\"\n >{{ t('grid.booleanNo') }}</span\n >\n }\n } @else if (col.format) {\n {{ col.format(value, row) }}\n } @else {\n {{ formatValue(value, col, row) }}\n }\n }\n </td>\n }\n @if (actions().length > 0) {\n <td\n class=\"jv-grid-td jv-grid-td-actions\"\n [class.jv-grid-td-sticky-right]=\"resolvedOptions().stickyColumns\"\n >\n <div class=\"jv-grid-actions-group\">\n @for (action of actions(); track action.id) {\n <jv-button\n [variant]=\"actionVariant(action.variant)\"\n [icon]=\"action.icon ?? null\"\n [disabled]=\"action.disabled?.(row) ?? false\"\n [attr.aria-label]=\"action.label\"\n [title]=\"action.label\"\n (click)=\"onActionClick(action, row); $event.stopPropagation()\"\n ></jv-button>\n }\n </div>\n </td>\n }\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n }\n\n @if (\n resolvedOptions().pageable &&\n !virtualScrollEnabled() &&\n effectiveTotalPages() > 1 &&\n !resolvedOptions().loading\n ) {\n <nav\n class=\"jv-grid-pagination\"\n role=\"navigation\"\n [attr.aria-label]=\"t('grid.paginationLabel', { label: '' })\"\n >\n <div class=\"jv-grid-pagination-info\" aria-live=\"polite\" aria-atomic=\"true\">\n {{\n t('grid.itemsShowing', {\n start: pageStart(),\n end: pageEnd(),\n total: effectiveTotalItems(),\n })\n }}\n </div>\n <div class=\"jv-grid-pagination-controls\">\n @if (pageSizeOptions().length > 0) {\n <jv-select\n [options]=\"pageSizeOptions()\"\n [modelValue]=\"pageSizeStr()\"\n [ariaLabel]=\"t('grid.itemsPerPage')\"\n (selectionChange)=\"onPageSizeChange($event)\"\n />\n }\n <jv-button\n variant=\"outline\"\n icon=\"chevrons-left\"\n [disabled]=\"pageIndex() === 0\"\n (click)=\"goToPage(0)\"\n [attr.aria-label]=\"t('grid.pageFirst')\"\n ></jv-button>\n <jv-button\n variant=\"outline\"\n icon=\"chevron-left\"\n [disabled]=\"pageIndex() === 0\"\n (click)=\"goToPage(pageIndex() - 1)\"\n [attr.aria-label]=\"t('grid.pagePrev')\"\n ></jv-button>\n @for (p of pageRange(); track p) {\n <jv-button\n [variant]=\"p === pageIndex() ? 'primary' : 'outline'\"\n [attr.aria-current]=\"p === pageIndex() ? 'page' : null\"\n [attr.aria-label]=\"t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })\"\n (click)=\"goToPage(p)\"\n >{{ p + 1 }}</jv-button\n >\n }\n <jv-button\n variant=\"outline\"\n icon=\"chevron-right\"\n [disabled]=\"pageIndex() >= effectiveTotalPages() - 1\"\n (click)=\"goToPage(pageIndex() + 1)\"\n [attr.aria-label]=\"t('grid.pageNext')\"\n ></jv-button>\n <jv-button\n variant=\"outline\"\n icon=\"chevrons-right\"\n [disabled]=\"pageIndex() >= effectiveTotalPages() - 1\"\n (click)=\"goToPage(effectiveTotalPages() - 1)\"\n [attr.aria-label]=\"t('grid.pageLast')\"\n ></jv-button>\n </div>\n </nav>\n }\n</div>\n", styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-export-actions{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-table-wrap{overflow-x:auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);position:relative}.jv-grid-virtual{max-height:600px}.jv-grid-virtual-spacer{pointer-events:none}.jv-grid-table{width:100%;border-collapse:collapse;background:var(--jv-color-surface)}.jv-grid-thead{background:var(--jv-color-surface-muted)}.jv-grid-th{padding:var(--jv-spacing-sm) var(--jv-spacing-md);font-weight:600;font-size:.875rem;color:var(--jv-color-foreground-muted);text-align:start;white-space:nowrap;border-bottom:1px solid var(--jv-color-border);-webkit-user-select:none;user-select:none;position:relative}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.jv-grid-th-reorderable{cursor:grab}.jv-grid-th-reorderable:active{cursor:grabbing}.jv-grid-th-sticky-left{position:sticky;left:0;z-index:2;background:var(--jv-color-surface-muted);border-right:1px solid var(--jv-color-border)}.jv-grid-th-sticky-right{position:sticky;right:0;z-index:2;background:var(--jv-color-surface-muted);border-left:1px solid var(--jv-color-border)}.jv-grid-th-content{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-sort-icon{font-size:.75rem;line-height:1}.jv-grid-th-select,.jv-grid-td-select{width:3rem;text-align:center}.jv-grid-th-actions,.jv-grid-td-actions{width:1%;white-space:nowrap}.jv-grid-td{padding:var(--jv-spacing-sm) var(--jv-spacing-md);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);border-bottom:1px solid var(--jv-color-border)}.jv-grid-keyboard-cell:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-td-sticky-left{position:sticky;left:0;z-index:1;background:var(--jv-color-surface);border-right:1px solid var(--jv-color-border)}.jv-grid-td-sticky-right{position:sticky;right:0;z-index:1;background:var(--jv-color-surface);border-left:1px solid var(--jv-color-border)}.jv-grid-t:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr{transition:background-color .12s ease}.jv-grid-tr:hover{background:var(--jv-color-surface-muted)}.jv-grid-tr:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr-editing{background:var(--jv-color-surface-highlight, rgba(59, 130, 246, .05))}.jv-grid-tr-selected{background:var(--jv-color-primary-light, rgba(59, 130, 246, .08))}.jv-grid-checkbox{width:1rem;height:1rem;accent-color:var(--jv-color-primary);cursor:pointer}.jv-grid-boolean{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:999px;font-size:.75rem;font-weight:700}.jv-grid-boolean-true{background:var(--jv-color-success);color:#fff}.jv-grid-boolean-false{background:var(--jv-color-danger);color:#fff}.jv-grid-empty{padding:var(--jv-spacing-xl) var(--jv-spacing-md);text-align:center;color:var(--jv-color-foreground-muted);font-style:italic}.jv-grid-actions-group{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-loading{display:flex;align-items:center;justify-content:center;gap:var(--jv-spacing-md);padding:var(--jv-spacing-xl);color:var(--jv-color-foreground-muted)}.jv-grid-spinner{width:1.25rem;height:1.25rem;border:2px solid var(--jv-color-border);border-right-color:var(--jv-color-primary);border-radius:999px;animation:jv-grid-spin .8s linear infinite}@keyframes jv-grid-spin{to{transform:rotate(360deg)}}.jv-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-grid-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-xs);flex-wrap:wrap}@media(max-width:640px){.jv-grid-pagination{flex-direction:column;align-items:stretch;gap:var(--jv-spacing-sm)}.jv-grid-pagination-info{text-align:center}.jv-grid-pagination-controls{justify-content:center}.jv-grid-toolbar{flex-direction:column;align-items:stretch}.jv-grid-search{width:100%}.jv-grid-export-actions{justify-content:stretch}.jv-grid-export-actions jv-button{flex:1}}.jv-grid-resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;background:transparent;z-index:3}.jv-grid-resize-handle:hover,.jv-grid-resize-handle:active{background:var(--jv-color-primary)}.jv-grid-filter-row .jv-grid-th{padding:var(--jv-spacing-xs) var(--jv-spacing-sm)}.jv-grid-filter-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:.8rem;box-sizing:border-box}.jv-grid-td-editable{cursor:pointer}.jv-grid-td-editable:hover{box-shadow:inset 0 0 0 1px var(--jv-color-primary)}.jv-grid-edit-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-primary);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);box-sizing:border-box}.jv-grid-edit-input:focus{outline:2px solid var(--jv-color-primary);outline-offset:-1px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: JvButtonComponent, selector: "jv-button", inputs: ["type", "variant", "icon", "iconPosition", "loading", "disabled"] }, { kind: "component", type: JvIconComponent, selector: "jv-icon", inputs: ["name", "size", "strokeWidth", "decorative", "ariaLabel"] }, { kind: "component", type: JvInputComponent, selector: "jv-input", inputs: ["type", "placeholder", "required", "invalid", "describedBy", "inputId"] }, { kind: "component", type: JvSelectComponent, selector: "jv-select", inputs: ["options", "placeholder", "required", "invalid", "describedBy", "ariaLabel", "inputId", "modelValue"], outputs: ["selectionChange"] }] });
25733
25451
  }
25734
25452
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvGridComponent, decorators: [{
25735
25453
  type: Component,
25736
- args: [{ selector: 'jv-grid', standalone: true, imports: [FormsModule, JvButtonComponent, JvIconComponent, JvInputComponent, JvSelectComponent], template: `
25737
- <div class="jv-grid" role="region" [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()">
25738
- @if (resolvedOptions().searchable || resolvedOptions().exportable) {
25739
- <div class="jv-grid-toolbar">
25740
- @if (resolvedOptions().searchable) {
25741
- <div class="jv-grid-search">
25742
- <jv-icon name="search" [size]="16" />
25743
- <jv-input
25744
- [inputId]="gridSearchInputId"
25745
- [placeholder]="t('grid.searchPlaceholder')"
25746
- [ngModel]="searchTerm()"
25747
- (ngModelChange)="onSearchInput($event)"
25748
- />
25749
- </div>
25750
- }
25751
- @if (resolvedOptions().exportable) {
25752
- <div class="jv-grid-export-actions">
25753
- <jv-button variant="outline" icon="download" (click)="exportCsv()">{{ t('grid.exportCsv') }}</jv-button>
25754
- <jv-button variant="outline" icon="file-spreadsheet" (click)="exportExcel()">{{ t('grid.exportExcel') }}</jv-button>
25755
- </div>
25756
- }
25757
- </div>
25758
- }
25759
-
25760
- @if (resolvedOptions().loading) {
25761
- <div class="jv-grid-loading" role="status" aria-live="polite">
25762
- <span class="jv-grid-spinner" aria-hidden="true"></span>
25763
- <span>{{ t('grid.loading') }}</span>
25764
- </div>
25765
- } @else {
25766
- <div
25767
- class="jv-grid-table-wrap"
25768
- [class.jv-grid-virtual]="virtualScrollEnabled()"
25769
- #tableWrap
25770
- (scroll)="onVirtualScroll($any($event.target))"
25771
- >
25772
- @if (virtualScrollEnabled()) {
25773
- <div class="jv-grid-virtual-spacer" [style.height.px]="virtualTotalHeight()"></div>
25774
- }
25775
- <table
25776
- class="jv-grid-table"
25777
- role="grid"
25778
- [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()"
25779
- [attr.aria-rowcount]="effectiveTotalItems()"
25780
- [attr.aria-colcount]="colspan()"
25781
- [class]="densityClass()"
25782
- [style.position]="virtualScrollEnabled() ? 'relative' : null"
25783
- [style.top.px]="virtualOffsetY()"
25784
- >
25785
- <caption class="sr-only">{{ resolvedOptions().ariaLabel || gridLabel() }}</caption>
25786
-
25787
- @let hasGroupedHeaders = groupedHeaders().length > 0;
25788
-
25789
- <thead class="jv-grid-thead">
25790
- @if (hasGroupedHeaders) {
25791
- <tr>
25792
- @if (resolvedOptions().selectable) {
25793
- <th class="jv-grid-th jv-grid-th-select" scope="col" [attr.rowspan]="2">
25794
- <input
25795
- type="checkbox"
25796
- class="jv-grid-checkbox"
25797
- [checked]="allSelected()"
25798
- (change)="toggleSelectAll($any($event.target).checked)"
25799
- [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
25800
- [indeterminate]="someSelected()"
25801
- />
25802
- </th>
25803
- }
25804
- @for (group of groupedHeaders(); track colKey(group)) {
25805
- <th
25806
- class="jv-grid-th"
25807
- [class]="group.headerClass || ''"
25808
- [class.jv-grid-th-sticky-left]="isStickyLeft(group)"
25809
- [class.jv-grid-th-sticky-right]="isStickyRight(group)"
25810
- [attr.colspan]="groupColspan(group)"
25811
- scope="colgroup"
25812
- [style.text-align]="group.align ?? 'start'"
25813
- >
25814
- <div class="jv-grid-th-content">{{ group.header }}</div>
25815
- </th>
25816
- }
25817
- @if (actions().length > 0) {
25818
- <th class="jv-grid-th jv-grid-th-actions" scope="col" [attr.rowspan]="2">
25819
- <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
25820
- </th>
25821
- }
25822
- </tr>
25823
- }
25824
-
25825
- <tr>
25826
- @if (!hasGroupedHeaders && resolvedOptions().selectable) {
25827
- <th class="jv-grid-th jv-grid-th-select" scope="col">
25828
- <input
25829
- type="checkbox"
25830
- class="jv-grid-checkbox"
25831
- [checked]="allSelected()"
25832
- (change)="toggleSelectAll($any($event.target).checked)"
25833
- [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
25834
- [indeterminate]="someSelected()"
25835
- />
25836
- </th>
25837
- }
25838
- @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
25839
- <th
25840
- class="jv-grid-th"
25841
- [class]="col.headerClass || ''"
25842
- [class.jv-grid-th-sortable]="col.sortable !== false && resolvedOptions().sortable"
25843
- [class.jv-grid-th-sticky-left]="isStickyLeft(col)"
25844
- [class.jv-grid-th-sticky-right]="isStickyRight(col)"
25845
- [class.jv-grid-th-reorderable]="resolvedOptions().reorderableColumns"
25846
- [style.text-align]="col.align ?? 'start'"
25847
- [style.width]="getColumnWidth(col)"
25848
- [style.min-width]="col.minWidth || undefined"
25849
- [style.max-width]="col.maxWidth || undefined"
25850
- scope="col"
25851
- role="columnheader"
25852
- [attr.data-column-index]="colIdx"
25853
- [attr.aria-sort]="getAriaSort(col)"
25854
- [attr.aria-label]="getSortLabel(col)"
25855
- [tabindex]="resolvedOptions().keyboardNavigation ? (activeCell().column === colIdx ? 0 : -1) : (col.sortable !== false && resolvedOptions().sortable ? 0 : -1)"
25856
- (click)="toggleSort(col)"
25857
- (focus)="onHeaderFocus(colIdx)"
25858
- (keydown)="onHeaderKeydown($event, colIdx)"
25859
- (keydown.enter)="toggleSort(col)"
25860
- (keydown.space)="toggleSort(col, $event)"
25861
- draggable="{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}"
25862
- (dragstart)="onDragStart($event, colIdx)"
25863
- (dragover)="onDragOver($event, colIdx)"
25864
- (drop)="onDrop($event, colIdx)"
25865
- (dragend)="onDragEnd($event)"
25866
- >
25867
- <div class="jv-grid-th-content">
25868
- <span>{{ col.header }}</span>
25869
- @if (col.sortable !== false && resolvedOptions().sortable) {
25870
- <span class="jv-grid-sort-icon" aria-hidden="true">
25871
- @if (sortState().columnKey === colKey(col)) {
25872
- @if (sortState().direction === 'asc') { ▲ }
25873
- @if (sortState().direction === 'desc') { ▼ }
25874
- }
25875
- </span>
25876
- }
25877
- </div>
25878
- @if (isColumnResizable(col)) {
25879
- <div
25880
- class="jv-grid-resize-handle"
25881
- (mousedown)="onResizeStart($event, col, colIdx)"
25882
- ></div>
25883
- }
25884
- </th>
25885
- }
25886
- @if (!hasGroupedHeaders && actions().length > 0) {
25887
- <th class="jv-grid-th jv-grid-th-actions" scope="col">
25888
- <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
25889
- </th>
25890
- }
25891
- </tr>
25892
-
25893
- @if (resolvedOptions().columnFilters) {
25894
- <tr class="jv-grid-filter-row">
25895
- @if (resolvedOptions().selectable) {
25896
- <th class="jv-grid-th jv-grid-th-select"></th>
25897
- }
25898
- @for (col of visibleColumns(); track colKey(col)) {
25899
- <th class="jv-grid-th jv-grid-th-filter">
25900
- @if (col.filterable !== false) {
25901
- <input
25902
- class="jv-grid-filter-input"
25903
- [placeholder]="t('grid.filterPlaceholder')"
25904
- [attr.aria-label]="t('grid.filterForColumn', { column: col.header })"
25905
- [ngModel]="colFilters()[colKey(col)]?.value ?? ''"
25906
- (ngModelChange)="onColumnFilterChange(col, $event)"
25907
- />
25908
- }
25909
- </th>
25910
- }
25911
- @if (actions().length > 0) {
25912
- <th class="jv-grid-th jv-grid-th-actions"></th>
25913
- }
25914
- </tr>
25915
- }
25916
- </thead>
25917
-
25918
- <tbody class="jv-grid-tbody">
25919
- @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {
25920
- <tr>
25921
- <td class="jv-grid-empty" [attr.colspan]="colspan()">
25922
- {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}
25923
- </td>
25924
- </tr>
25925
- } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {
25926
- <tr>
25927
- <td class="jv-grid-empty" [attr.colspan]="colspan()">
25928
- {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}
25929
- </td>
25930
- </tr>
25931
- } @else {
25932
- @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {
25933
- <tr
25934
- class="jv-grid-tr"
25935
- role="row"
25936
- [class.jv-grid-tr-editing]="isRowEditing(row)"
25937
- [class.jv-grid-tr-selected]="isSelected(row)"
25938
- [attr.aria-selected]="resolvedOptions().selectable ? isSelected(row) : null"
25939
- (click)="onRowClick(row)"
25940
- (dblclick)="onRowDblClick(row)"
25941
- (keydown.enter)="onRowClick(row)"
25942
- (keydown.space)="onRowClick($event, row)"
25943
- [tabindex]="-1"
25944
- >
25945
- @if (resolvedOptions().selectable) {
25946
- <td
25947
- class="jv-grid-td jv-grid-td-select"
25948
- [class.jv-grid-td-sticky-left]="resolvedOptions().stickyColumns"
25949
- >
25950
- <input
25951
- type="checkbox"
25952
- class="jv-grid-checkbox"
25953
- [checked]="isSelected(row)"
25954
- (change)="toggleRow(row); $event.stopPropagation()"
25955
- (keydown.space)="$event.stopPropagation()"
25956
- [attr.aria-label]="getRowLabel(row, $index)"
25957
- />
25958
- </td>
25959
- }
25960
- @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
25961
- <td
25962
- class="jv-grid-td"
25963
- [attr.role]="resolvedOptions().keyboardNavigation ? 'gridcell' : null"
25964
- [class]="col.cellClass || ''"
25965
- [class.jv-grid-td-sticky-left]="isStickyLeft(col)"
25966
- [class.jv-grid-td-sticky-right]="isStickyRight(col)"
25967
- [class.jv-grid-td-editable]="isCellEditable(col)"
25968
- [style.text-align]="col.align ?? 'start'"
25969
- (dblclick)="startEdit(row, col, $event)"
25970
- (keydown.enter)="startEdit(row, col, $event)"
25971
- [attr.data-row-index]="rowIdx"
25972
- [attr.data-column-index]="colIdx"
25973
- [class.jv-grid-keyboard-cell]="resolvedOptions().keyboardNavigation"
25974
- [attr.tabindex]="cellTabIndex(rowIdx, colIdx, col)"
25975
- (focus)="onCellFocus(rowIdx, colIdx)"
25976
- (keydown.f2)="onCellF2($event, rowIdx, colIdx)"
25977
- (keydown)="onCellKeydown($event, rowIdx, colIdx)"
25978
- >
25979
- @if (isEditingCell(row, col)) {
25980
- @if (editType(col) === 'select' && col.editOptions) {
25981
- <jv-select
25982
- [options]="getEditOptions(col)"
25983
- [modelValue]="getEditValue(row, col)"
25984
- (selectionChange)="onEditValueChange(row, col, $event)"
25985
- />
25986
- } @else if (editType(col) === 'boolean') {
25987
- <input
25988
- type="checkbox"
25989
- class="jv-grid-checkbox"
25990
- [checked]="getBooleanEditValue(row, col)"
25991
- (change)="commitEdit(row, col, normalizeEditValue(col, $any($event.target).checked))"
25992
- (keydown.enter)="commitEditFromKeyboard($event, row, col, normalizeEditValue(col, $any($event.target).checked))"
25993
- (keydown.escape)="cancelEdit()"
25994
- [attr.aria-label]="col.header"
25995
- />
25996
- } @else {
25997
- <input
25998
- class="jv-grid-edit-input"
25999
- [type]="inputType(col)"
26000
- [value]="getEditValue(row, col)"
26001
- (blur)="commitEdit(row, col, normalizeEditValue(col, $any($event.target).value))"
26002
- (keydown.enter)="commitEditFromKeyboard($event, row, col, normalizeEditValue(col, $any($event.target).value))"
26003
- (keydown.escape)="cancelEdit()"
26004
- />
26005
- }
26006
- } @else {
26007
- @let value = getCellValue(row, col);
26008
- @if (col.type === 'boolean') {
26009
- @if (isBooleanTrue(value)) {
26010
- <span class="jv-grid-boolean jv-grid-boolean-true" [attr.aria-label]="t('grid.booleanYes')">{{ t('grid.booleanYes') }}</span>
26011
- } @else {
26012
- <span class="jv-grid-boolean jv-grid-boolean-false" [attr.aria-label]="t('grid.booleanNo')">{{ t('grid.booleanNo') }}</span>
26013
- }
26014
- } @else if (col.format) {
26015
- {{ col.format(value, row) }}
26016
- } @else {
26017
- {{ formatValue(value, col, row) }}
26018
- }
26019
- }
26020
- </td>
26021
- }
26022
- @if (actions().length > 0) {
26023
- <td
26024
- class="jv-grid-td jv-grid-td-actions"
26025
- [class.jv-grid-td-sticky-right]="resolvedOptions().stickyColumns"
26026
- >
26027
- <div class="jv-grid-actions-group">
26028
- @for (action of actions(); track action.id) {
26029
- <jv-button
26030
- [variant]="actionVariant(action.variant)"
26031
- [icon]="action.icon ?? null"
26032
- [disabled]="action.disabled?.(row) ?? false"
26033
- [attr.aria-label]="action.label"
26034
- [title]="action.label"
26035
- (click)="onActionClick(action, row); $event.stopPropagation()"
26036
- ></jv-button>
26037
- }
26038
- </div>
26039
- </td>
26040
- }
26041
- </tr>
26042
- }
26043
- }
26044
- </tbody>
26045
- </table>
26046
- </div>
26047
- }
26048
-
26049
- @if (resolvedOptions().pageable && !virtualScrollEnabled() && effectiveTotalPages() > 1 && !resolvedOptions().loading) {
26050
- <nav class="jv-grid-pagination" role="navigation" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
26051
- <div class="jv-grid-pagination-info" aria-live="polite" aria-atomic="true">
26052
- {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: effectiveTotalItems() }) }}
26053
- </div>
26054
- <div class="jv-grid-pagination-controls">
26055
- @if (pageSizeOptions().length > 0) {
26056
- <jv-select
26057
- [options]="pageSizeOptions()"
26058
- [modelValue]="pageSizeStr()"
26059
- [ariaLabel]="t('grid.itemsPerPage')"
26060
- (selectionChange)="onPageSizeChange($event)"
26061
- />
26062
- }
26063
- <jv-button
26064
- variant="outline"
26065
- icon="chevrons-left"
26066
- [disabled]="pageIndex() === 0"
26067
- (click)="goToPage(0)"
26068
- [attr.aria-label]="t('grid.pageFirst')"
26069
- ></jv-button>
26070
- <jv-button
26071
- variant="outline"
26072
- icon="chevron-left"
26073
- [disabled]="pageIndex() === 0"
26074
- (click)="goToPage(pageIndex() - 1)"
26075
- [attr.aria-label]="t('grid.pagePrev')"
26076
- ></jv-button>
26077
- @for (p of pageRange(); track p) {
26078
- <jv-button
26079
- [variant]="p === pageIndex() ? 'primary' : 'outline'"
26080
- [attr.aria-current]="p === pageIndex() ? 'page' : null"
26081
- [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })"
26082
- (click)="goToPage(p)"
26083
- >{{ p + 1 }}</jv-button>
26084
- }
26085
- <jv-button
26086
- variant="outline"
26087
- icon="chevron-right"
26088
- [disabled]="pageIndex() >= effectiveTotalPages() - 1"
26089
- (click)="goToPage(pageIndex() + 1)"
26090
- [attr.aria-label]="t('grid.pageNext')"
26091
- ></jv-button>
26092
- <jv-button
26093
- variant="outline"
26094
- icon="chevrons-right"
26095
- [disabled]="pageIndex() >= effectiveTotalPages() - 1"
26096
- (click)="goToPage(effectiveTotalPages() - 1)"
26097
- [attr.aria-label]="t('grid.pageLast')"
26098
- ></jv-button>
26099
- </div>
26100
- </nav>
26101
- }
26102
- </div>
26103
- `, styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-export-actions{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-table-wrap{overflow-x:auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);position:relative}.jv-grid-virtual{max-height:600px}.jv-grid-virtual-spacer{pointer-events:none}.jv-grid-table{width:100%;border-collapse:collapse;background:var(--jv-color-surface)}.jv-grid-thead{background:var(--jv-color-surface-muted)}.jv-grid-th{padding:var(--jv-spacing-sm) var(--jv-spacing-md);font-weight:600;font-size:.875rem;color:var(--jv-color-foreground-muted);text-align:start;white-space:nowrap;border-bottom:1px solid var(--jv-color-border);-webkit-user-select:none;user-select:none;position:relative}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.jv-grid-th-reorderable{cursor:grab}.jv-grid-th-reorderable:active{cursor:grabbing}.jv-grid-th-sticky-left{position:sticky;left:0;z-index:2;background:var(--jv-color-surface-muted);border-right:1px solid var(--jv-color-border)}.jv-grid-th-sticky-right{position:sticky;right:0;z-index:2;background:var(--jv-color-surface-muted);border-left:1px solid var(--jv-color-border)}.jv-grid-th-content{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-sort-icon{font-size:.75rem;line-height:1}.jv-grid-th-select,.jv-grid-td-select{width:3rem;text-align:center}.jv-grid-th-actions,.jv-grid-td-actions{width:1%;white-space:nowrap}.jv-grid-td{padding:var(--jv-spacing-sm) var(--jv-spacing-md);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);border-bottom:1px solid var(--jv-color-border)}.jv-grid-keyboard-cell:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-td-sticky-left{position:sticky;left:0;z-index:1;background:var(--jv-color-surface);border-right:1px solid var(--jv-color-border)}.jv-grid-td-sticky-right{position:sticky;right:0;z-index:1;background:var(--jv-color-surface);border-left:1px solid var(--jv-color-border)}.jv-grid-t:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr{transition:background-color .12s ease}.jv-grid-tr:hover{background:var(--jv-color-surface-muted)}.jv-grid-tr:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr-editing{background:var(--jv-color-surface-highlight, rgba(59, 130, 246, .05))}.jv-grid-tr-selected{background:var(--jv-color-primary-light, rgba(59, 130, 246, .08))}.jv-grid-checkbox{width:1rem;height:1rem;accent-color:var(--jv-color-primary);cursor:pointer}.jv-grid-boolean{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:999px;font-size:.75rem;font-weight:700}.jv-grid-boolean-true{background:var(--jv-color-success);color:#fff}.jv-grid-boolean-false{background:var(--jv-color-danger);color:#fff}.jv-grid-empty{padding:var(--jv-spacing-xl) var(--jv-spacing-md);text-align:center;color:var(--jv-color-foreground-muted);font-style:italic}.jv-grid-actions-group{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-loading{display:flex;align-items:center;justify-content:center;gap:var(--jv-spacing-md);padding:var(--jv-spacing-xl);color:var(--jv-color-foreground-muted)}.jv-grid-spinner{width:1.25rem;height:1.25rem;border:2px solid var(--jv-color-border);border-right-color:var(--jv-color-primary);border-radius:999px;animation:jv-grid-spin .8s linear infinite}@keyframes jv-grid-spin{to{transform:rotate(360deg)}}.jv-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-grid-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-xs);flex-wrap:wrap}@media(max-width:640px){.jv-grid-pagination{flex-direction:column;align-items:stretch;gap:var(--jv-spacing-sm)}.jv-grid-pagination-info{text-align:center}.jv-grid-pagination-controls{justify-content:center}.jv-grid-toolbar{flex-direction:column;align-items:stretch}.jv-grid-search{width:100%}.jv-grid-export-actions{justify-content:stretch}.jv-grid-export-actions jv-button{flex:1}}.jv-grid-resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;background:transparent;z-index:3}.jv-grid-resize-handle:hover,.jv-grid-resize-handle:active{background:var(--jv-color-primary)}.jv-grid-filter-row .jv-grid-th{padding:var(--jv-spacing-xs) var(--jv-spacing-sm)}.jv-grid-filter-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:.8rem;box-sizing:border-box}.jv-grid-td-editable{cursor:pointer}.jv-grid-td-editable:hover{box-shadow:inset 0 0 0 1px var(--jv-color-primary)}.jv-grid-edit-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-primary);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);box-sizing:border-box}.jv-grid-edit-input:focus{outline:2px solid var(--jv-color-primary);outline-offset:-1px}\n"] }]
25454
+ args: [{ selector: 'jv-grid', standalone: true, imports: [FormsModule, JvButtonComponent, JvIconComponent, JvInputComponent, JvSelectComponent], template: "<div class=\"jv-grid\" role=\"region\" [attr.aria-label]=\"resolvedOptions().ariaLabel || gridLabel()\">\n @if (resolvedOptions().searchable || resolvedOptions().exportable) {\n <div class=\"jv-grid-toolbar\">\n @if (resolvedOptions().searchable) {\n <div class=\"jv-grid-search\">\n <jv-icon name=\"search\" [size]=\"16\" />\n <jv-input\n [inputId]=\"gridSearchInputId\"\n [placeholder]=\"t('grid.searchPlaceholder')\"\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"onSearchInput($event)\"\n />\n </div>\n }\n @if (resolvedOptions().exportable) {\n <div class=\"jv-grid-export-actions\">\n <jv-button variant=\"outline\" icon=\"download\" (click)=\"exportCsv()\">{{\n t('grid.exportCsv')\n }}</jv-button>\n <jv-button variant=\"outline\" icon=\"file-spreadsheet\" (click)=\"exportExcel()\">{{\n t('grid.exportExcel')\n }}</jv-button>\n </div>\n }\n </div>\n }\n\n @if (resolvedOptions().loading) {\n <div class=\"jv-grid-loading\" role=\"status\" aria-live=\"polite\">\n <span class=\"jv-grid-spinner\" aria-hidden=\"true\"></span>\n <span>{{ t('grid.loading') }}</span>\n </div>\n } @else {\n <div\n class=\"jv-grid-table-wrap\"\n [class.jv-grid-virtual]=\"virtualScrollEnabled()\"\n #tableWrap\n (scroll)=\"onVirtualScroll($any($event.target))\"\n >\n @if (virtualScrollEnabled()) {\n <div class=\"jv-grid-virtual-spacer\" [style.height.px]=\"virtualTotalHeight()\"></div>\n }\n <table\n class=\"jv-grid-table\"\n role=\"grid\"\n [attr.aria-label]=\"resolvedOptions().ariaLabel || gridLabel()\"\n [attr.aria-rowcount]=\"effectiveTotalItems()\"\n [attr.aria-colcount]=\"colspan()\"\n [class]=\"densityClass()\"\n [style.position]=\"virtualScrollEnabled() ? 'relative' : null\"\n [style.top.px]=\"virtualOffsetY()\"\n >\n <caption class=\"sr-only\">\n {{\n resolvedOptions().ariaLabel || gridLabel()\n }}\n </caption>\n\n @let hasGroupedHeaders = groupedHeaders().length > 0;\n\n <thead class=\"jv-grid-thead\">\n @if (hasGroupedHeaders) {\n <tr>\n @if (resolvedOptions().selectable) {\n <th class=\"jv-grid-th jv-grid-th-select\" scope=\"col\" [attr.rowspan]=\"2\">\n <input\n type=\"checkbox\"\n class=\"jv-grid-checkbox\"\n [checked]=\"allSelected()\"\n (change)=\"toggleSelectAll($any($event.target).checked)\"\n [attr.aria-label]=\"allSelected() ? t('grid.deselectAll') : t('grid.selectAll')\"\n [indeterminate]=\"someSelected()\"\n />\n </th>\n }\n @for (group of groupedHeaders(); track colKey(group)) {\n <th\n class=\"jv-grid-th\"\n [class]=\"group.headerClass || ''\"\n [class.jv-grid-th-sticky-left]=\"isStickyLeft(group)\"\n [class.jv-grid-th-sticky-right]=\"isStickyRight(group)\"\n [attr.colspan]=\"groupColspan(group)\"\n scope=\"colgroup\"\n [style.text-align]=\"group.align ?? 'start'\"\n >\n <div class=\"jv-grid-th-content\">{{ group.header }}</div>\n </th>\n }\n @if (actions().length > 0) {\n <th class=\"jv-grid-th jv-grid-th-actions\" scope=\"col\" [attr.rowspan]=\"2\">\n <span class=\"sr-only\">{{ t('grid.actionsLabel', { label: '' }) }}</span>\n </th>\n }\n </tr>\n }\n\n <tr>\n @if (!hasGroupedHeaders && resolvedOptions().selectable) {\n <th class=\"jv-grid-th jv-grid-th-select\" scope=\"col\">\n <input\n type=\"checkbox\"\n class=\"jv-grid-checkbox\"\n [checked]=\"allSelected()\"\n (change)=\"toggleSelectAll($any($event.target).checked)\"\n [attr.aria-label]=\"allSelected() ? t('grid.deselectAll') : t('grid.selectAll')\"\n [indeterminate]=\"someSelected()\"\n />\n </th>\n }\n @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {\n <th\n class=\"jv-grid-th\"\n [class]=\"col.headerClass || ''\"\n [class.jv-grid-th-sortable]=\"col.sortable !== false && resolvedOptions().sortable\"\n [class.jv-grid-th-sticky-left]=\"isStickyLeft(col)\"\n [class.jv-grid-th-sticky-right]=\"isStickyRight(col)\"\n [class.jv-grid-th-reorderable]=\"resolvedOptions().reorderableColumns\"\n [style.text-align]=\"col.align ?? 'start'\"\n [style.width]=\"getColumnWidth(col)\"\n [style.min-width]=\"col.minWidth || undefined\"\n [style.max-width]=\"col.maxWidth || undefined\"\n scope=\"col\"\n role=\"columnheader\"\n [attr.data-column-index]=\"colIdx\"\n [attr.aria-sort]=\"getAriaSort(col)\"\n [attr.aria-label]=\"getSortLabel(col)\"\n [tabindex]=\"\n resolvedOptions().keyboardNavigation\n ? activeCell().column === colIdx\n ? 0\n : -1\n : col.sortable !== false && resolvedOptions().sortable\n ? 0\n : -1\n \"\n (click)=\"toggleSort(col)\"\n (focus)=\"onHeaderFocus(colIdx)\"\n (keydown)=\"onHeaderKeydown($event, colIdx)\"\n (keydown.enter)=\"toggleSort(col)\"\n (keydown.space)=\"toggleSort(col, $event)\"\n draggable=\"{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}\"\n (dragstart)=\"onDragStart($event, colIdx)\"\n (dragover)=\"onDragOver($event, colIdx)\"\n (drop)=\"onDrop($event, colIdx)\"\n (dragend)=\"onDragEnd($event)\"\n >\n <div class=\"jv-grid-th-content\">\n <span>{{ col.header }}</span>\n @if (col.sortable !== false && resolvedOptions().sortable) {\n <span class=\"jv-grid-sort-icon\" aria-hidden=\"true\">\n @if (sortState().columnKey === colKey(col)) {\n @if (sortState().direction === 'asc') {\n \u25B2\n }\n @if (sortState().direction === 'desc') {\n \u25BC\n }\n }\n </span>\n }\n </div>\n @if (isColumnResizable(col)) {\n <div\n class=\"jv-grid-resize-handle\"\n (mousedown)=\"onResizeStart($event, col, colIdx)\"\n ></div>\n }\n </th>\n }\n @if (!hasGroupedHeaders && actions().length > 0) {\n <th class=\"jv-grid-th jv-grid-th-actions\" scope=\"col\">\n <span class=\"sr-only\">{{ t('grid.actionsLabel', { label: '' }) }}</span>\n </th>\n }\n </tr>\n\n @if (resolvedOptions().columnFilters) {\n <tr class=\"jv-grid-filter-row\">\n @if (resolvedOptions().selectable) {\n <th class=\"jv-grid-th jv-grid-th-select\"></th>\n }\n @for (col of visibleColumns(); track colKey(col)) {\n <th class=\"jv-grid-th jv-grid-th-filter\">\n @if (col.filterable !== false) {\n <input\n class=\"jv-grid-filter-input\"\n [placeholder]=\"t('grid.filterPlaceholder')\"\n [attr.aria-label]=\"t('grid.filterForColumn', { column: col.header })\"\n [ngModel]=\"colFilters()[colKey(col)]?.value ?? ''\"\n (ngModelChange)=\"onColumnFilterChange(col, $event)\"\n />\n }\n </th>\n }\n @if (actions().length > 0) {\n <th class=\"jv-grid-th jv-grid-th-actions\"></th>\n }\n </tr>\n }\n </thead>\n\n <tbody class=\"jv-grid-tbody\">\n @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {\n <tr>\n <td class=\"jv-grid-empty\" [attr.colspan]=\"colspan()\">\n {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}\n </td>\n </tr>\n } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {\n <tr>\n <td class=\"jv-grid-empty\" [attr.colspan]=\"colspan()\">\n {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}\n </td>\n </tr>\n } @else {\n @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {\n <tr\n class=\"jv-grid-tr\"\n role=\"row\"\n [class.jv-grid-tr-editing]=\"isRowEditing(row)\"\n [class.jv-grid-tr-selected]=\"isSelected(row)\"\n [attr.aria-selected]=\"resolvedOptions().selectable ? isSelected(row) : null\"\n (click)=\"onRowClick(row)\"\n (dblclick)=\"onRowDblClick(row)\"\n (keydown.enter)=\"onRowClick(row)\"\n (keydown.space)=\"onRowClick($event, row)\"\n [tabindex]=\"-1\"\n >\n @if (resolvedOptions().selectable) {\n <td\n class=\"jv-grid-td jv-grid-td-select\"\n [class.jv-grid-td-sticky-left]=\"resolvedOptions().stickyColumns\"\n >\n <input\n type=\"checkbox\"\n class=\"jv-grid-checkbox\"\n [checked]=\"isSelected(row)\"\n (change)=\"toggleRow(row); $event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"getRowLabel(row, $index)\"\n />\n </td>\n }\n @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {\n <td\n class=\"jv-grid-td\"\n [attr.role]=\"resolvedOptions().keyboardNavigation ? 'gridcell' : null\"\n [class]=\"col.cellClass || ''\"\n [class.jv-grid-td-sticky-left]=\"isStickyLeft(col)\"\n [class.jv-grid-td-sticky-right]=\"isStickyRight(col)\"\n [class.jv-grid-td-editable]=\"isCellEditable(col)\"\n [style.text-align]=\"col.align ?? 'start'\"\n (dblclick)=\"startEdit(row, col, $event)\"\n (keydown.enter)=\"startEdit(row, col, $event)\"\n [attr.data-row-index]=\"rowIdx\"\n [attr.data-column-index]=\"colIdx\"\n [class.jv-grid-keyboard-cell]=\"resolvedOptions().keyboardNavigation\"\n [attr.tabindex]=\"cellTabIndex(rowIdx, colIdx, col)\"\n (focus)=\"onCellFocus(rowIdx, colIdx)\"\n (keydown.f2)=\"onCellF2($event, rowIdx, colIdx)\"\n (keydown)=\"onCellKeydown($event, rowIdx, colIdx)\"\n >\n @if (isEditingCell(row, col)) {\n @if (editType(col) === 'select' && col.editOptions) {\n <jv-select\n [options]=\"getEditOptions(col)\"\n [modelValue]=\"getEditValue(row, col)\"\n (selectionChange)=\"onEditValueChange(row, col, $event)\"\n />\n } @else if (editType(col) === 'boolean') {\n <input\n type=\"checkbox\"\n class=\"jv-grid-checkbox\"\n [checked]=\"getBooleanEditValue(row, col)\"\n (change)=\"\n commitEdit(\n row,\n col,\n normalizeEditValue(col, $any($event.target).checked)\n )\n \"\n (keydown.enter)=\"\n commitEditFromKeyboard(\n $event,\n row,\n col,\n normalizeEditValue(col, $any($event.target).checked)\n )\n \"\n (keydown.escape)=\"cancelEdit()\"\n [attr.aria-label]=\"col.header\"\n />\n } @else {\n <input\n class=\"jv-grid-edit-input\"\n [type]=\"inputType(col)\"\n [value]=\"getEditValue(row, col)\"\n (blur)=\"\n commitEdit(row, col, normalizeEditValue(col, $any($event.target).value))\n \"\n (keydown.enter)=\"\n commitEditFromKeyboard(\n $event,\n row,\n col,\n normalizeEditValue(col, $any($event.target).value)\n )\n \"\n (keydown.escape)=\"cancelEdit()\"\n />\n }\n } @else {\n @let value = getCellValue(row, col);\n @if (col.type === 'boolean') {\n @if (isBooleanTrue(value)) {\n <span\n class=\"jv-grid-boolean jv-grid-boolean-true\"\n [attr.aria-label]=\"t('grid.booleanYes')\"\n >{{ t('grid.booleanYes') }}</span\n >\n } @else {\n <span\n class=\"jv-grid-boolean jv-grid-boolean-false\"\n [attr.aria-label]=\"t('grid.booleanNo')\"\n >{{ t('grid.booleanNo') }}</span\n >\n }\n } @else if (col.format) {\n {{ col.format(value, row) }}\n } @else {\n {{ formatValue(value, col, row) }}\n }\n }\n </td>\n }\n @if (actions().length > 0) {\n <td\n class=\"jv-grid-td jv-grid-td-actions\"\n [class.jv-grid-td-sticky-right]=\"resolvedOptions().stickyColumns\"\n >\n <div class=\"jv-grid-actions-group\">\n @for (action of actions(); track action.id) {\n <jv-button\n [variant]=\"actionVariant(action.variant)\"\n [icon]=\"action.icon ?? null\"\n [disabled]=\"action.disabled?.(row) ?? false\"\n [attr.aria-label]=\"action.label\"\n [title]=\"action.label\"\n (click)=\"onActionClick(action, row); $event.stopPropagation()\"\n ></jv-button>\n }\n </div>\n </td>\n }\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n }\n\n @if (\n resolvedOptions().pageable &&\n !virtualScrollEnabled() &&\n effectiveTotalPages() > 1 &&\n !resolvedOptions().loading\n ) {\n <nav\n class=\"jv-grid-pagination\"\n role=\"navigation\"\n [attr.aria-label]=\"t('grid.paginationLabel', { label: '' })\"\n >\n <div class=\"jv-grid-pagination-info\" aria-live=\"polite\" aria-atomic=\"true\">\n {{\n t('grid.itemsShowing', {\n start: pageStart(),\n end: pageEnd(),\n total: effectiveTotalItems(),\n })\n }}\n </div>\n <div class=\"jv-grid-pagination-controls\">\n @if (pageSizeOptions().length > 0) {\n <jv-select\n [options]=\"pageSizeOptions()\"\n [modelValue]=\"pageSizeStr()\"\n [ariaLabel]=\"t('grid.itemsPerPage')\"\n (selectionChange)=\"onPageSizeChange($event)\"\n />\n }\n <jv-button\n variant=\"outline\"\n icon=\"chevrons-left\"\n [disabled]=\"pageIndex() === 0\"\n (click)=\"goToPage(0)\"\n [attr.aria-label]=\"t('grid.pageFirst')\"\n ></jv-button>\n <jv-button\n variant=\"outline\"\n icon=\"chevron-left\"\n [disabled]=\"pageIndex() === 0\"\n (click)=\"goToPage(pageIndex() - 1)\"\n [attr.aria-label]=\"t('grid.pagePrev')\"\n ></jv-button>\n @for (p of pageRange(); track p) {\n <jv-button\n [variant]=\"p === pageIndex() ? 'primary' : 'outline'\"\n [attr.aria-current]=\"p === pageIndex() ? 'page' : null\"\n [attr.aria-label]=\"t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })\"\n (click)=\"goToPage(p)\"\n >{{ p + 1 }}</jv-button\n >\n }\n <jv-button\n variant=\"outline\"\n icon=\"chevron-right\"\n [disabled]=\"pageIndex() >= effectiveTotalPages() - 1\"\n (click)=\"goToPage(pageIndex() + 1)\"\n [attr.aria-label]=\"t('grid.pageNext')\"\n ></jv-button>\n <jv-button\n variant=\"outline\"\n icon=\"chevrons-right\"\n [disabled]=\"pageIndex() >= effectiveTotalPages() - 1\"\n (click)=\"goToPage(effectiveTotalPages() - 1)\"\n [attr.aria-label]=\"t('grid.pageLast')\"\n ></jv-button>\n </div>\n </nav>\n }\n</div>\n", styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-export-actions{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-table-wrap{overflow-x:auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md);position:relative}.jv-grid-virtual{max-height:600px}.jv-grid-virtual-spacer{pointer-events:none}.jv-grid-table{width:100%;border-collapse:collapse;background:var(--jv-color-surface)}.jv-grid-thead{background:var(--jv-color-surface-muted)}.jv-grid-th{padding:var(--jv-spacing-sm) var(--jv-spacing-md);font-weight:600;font-size:.875rem;color:var(--jv-color-foreground-muted);text-align:start;white-space:nowrap;border-bottom:1px solid var(--jv-color-border);-webkit-user-select:none;user-select:none;position:relative}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.jv-grid-th-reorderable{cursor:grab}.jv-grid-th-reorderable:active{cursor:grabbing}.jv-grid-th-sticky-left{position:sticky;left:0;z-index:2;background:var(--jv-color-surface-muted);border-right:1px solid var(--jv-color-border)}.jv-grid-th-sticky-right{position:sticky;right:0;z-index:2;background:var(--jv-color-surface-muted);border-left:1px solid var(--jv-color-border)}.jv-grid-th-content{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-sort-icon{font-size:.75rem;line-height:1}.jv-grid-th-select,.jv-grid-td-select{width:3rem;text-align:center}.jv-grid-th-actions,.jv-grid-td-actions{width:1%;white-space:nowrap}.jv-grid-td{padding:var(--jv-spacing-sm) var(--jv-spacing-md);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);border-bottom:1px solid var(--jv-color-border)}.jv-grid-keyboard-cell:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-td-sticky-left{position:sticky;left:0;z-index:1;background:var(--jv-color-surface);border-right:1px solid var(--jv-color-border)}.jv-grid-td-sticky-right{position:sticky;right:0;z-index:1;background:var(--jv-color-surface);border-left:1px solid var(--jv-color-border)}.jv-grid-t:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr{transition:background-color .12s ease}.jv-grid-tr:hover{background:var(--jv-color-surface-muted)}.jv-grid-tr:focus-visible{outline:2px solid var(--jv-color-primary);outline-offset:-2px}.jv-grid-tr-editing{background:var(--jv-color-surface-highlight, rgba(59, 130, 246, .05))}.jv-grid-tr-selected{background:var(--jv-color-primary-light, rgba(59, 130, 246, .08))}.jv-grid-checkbox{width:1rem;height:1rem;accent-color:var(--jv-color-primary);cursor:pointer}.jv-grid-boolean{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:999px;font-size:.75rem;font-weight:700}.jv-grid-boolean-true{background:var(--jv-color-success);color:#fff}.jv-grid-boolean-false{background:var(--jv-color-danger);color:#fff}.jv-grid-empty{padding:var(--jv-spacing-xl) var(--jv-spacing-md);text-align:center;color:var(--jv-color-foreground-muted);font-style:italic}.jv-grid-actions-group{display:flex;align-items:center;gap:var(--jv-spacing-xs)}.jv-grid-loading{display:flex;align-items:center;justify-content:center;gap:var(--jv-spacing-md);padding:var(--jv-spacing-xl);color:var(--jv-color-foreground-muted)}.jv-grid-spinner{width:1.25rem;height:1.25rem;border:2px solid var(--jv-color-border);border-right-color:var(--jv-color-primary);border-radius:999px;animation:jv-grid-spin .8s linear infinite}@keyframes jv-grid-spin{to{transform:rotate(360deg)}}.jv-grid-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-grid-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-grid-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-xs);flex-wrap:wrap}@media(max-width:640px){.jv-grid-pagination{flex-direction:column;align-items:stretch;gap:var(--jv-spacing-sm)}.jv-grid-pagination-info{text-align:center}.jv-grid-pagination-controls{justify-content:center}.jv-grid-toolbar{flex-direction:column;align-items:stretch}.jv-grid-search{width:100%}.jv-grid-export-actions{justify-content:stretch}.jv-grid-export-actions jv-button{flex:1}}.jv-grid-resize-handle{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;background:transparent;z-index:3}.jv-grid-resize-handle:hover,.jv-grid-resize-handle:active{background:var(--jv-color-primary)}.jv-grid-filter-row .jv-grid-th{padding:var(--jv-spacing-xs) var(--jv-spacing-sm)}.jv-grid-filter-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:.8rem;box-sizing:border-box}.jv-grid-td-editable{cursor:pointer}.jv-grid-td-editable:hover{box-shadow:inset 0 0 0 1px var(--jv-color-primary)}.jv-grid-edit-input{width:100%;min-height:var(--jv-density-control-height);padding:0 var(--jv-spacing-sm);border:1px solid var(--jv-color-primary);border-radius:var(--jv-radius-sm);background:var(--jv-color-surface);color:var(--jv-color-foreground);font-size:var(--jv-density-font-size);box-sizing:border-box}.jv-grid-edit-input:focus{outline:2px solid var(--jv-color-primary);outline-offset:-1px}\n"] }]
26104
25455
  }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], selectedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIds", required: false }] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], actionClick: [{ type: i0.Output, args: ["actionClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], columnFilter: [{ type: i0.Output, args: ["columnFilter"] }], columnResize: [{ type: i0.Output, args: ["columnResize"] }], columnReorder: [{ type: i0.Output, args: ["columnReorder"] }], rowEdit: [{ type: i0.Output, args: ["rowEdit"] }] } });
26105
25456
 
26106
25457
  class JvPaginationComponent {