@devjuliovilla/jv-ui 1.5.3 → 1.5.5

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.
@@ -17843,47 +17843,47 @@ class JvIconComponent {
17843
17843
  return JV_LUCIDE_ICON_REGISTRY[JV_FALLBACK_ICON_NAME];
17844
17844
  }, ...(ngDevMode ? [{ debugName: "iconDefinition" }] : /* istanbul ignore next */ []));
17845
17845
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17846
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvIconComponent, isStandalone: true, selector: "jv-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, decorative: { classPropertyName: "decorative", publicName: "decorative", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
17847
- <svg
17848
- class="jv-icon"
17849
- [attr.viewBox]="iconDefinition().viewBox"
17850
- [attr.width]="size()"
17851
- [attr.height]="size()"
17852
- [attr.stroke-width]="strokeWidth()"
17853
- [attr.role]="decorative() ? 'presentation' : 'img'"
17854
- [attr.aria-hidden]="decorative() ? 'true' : null"
17855
- [attr.aria-label]="decorative() ? null : ariaLabel() || normalizedName()"
17856
- fill="none"
17857
- stroke="currentColor"
17858
- stroke-linecap="round"
17859
- stroke-linejoin="round"
17860
- >
17861
- @for (element of iconDefinition().elements; track $index) {
17862
- @switch (element.tag) {
17863
- @case ('path') {
17864
- <path [attr.d]="element.attrs['d']" />
17865
- }
17866
- @case ('circle') {
17867
- <circle [attr.cx]="element.attrs['cx']" [attr.cy]="element.attrs['cy']" [attr.r]="element.attrs['r']" />
17868
- }
17869
- @case ('line') {
17870
- <line [attr.x1]="element.attrs['x1']" [attr.x2]="element.attrs['x2']" [attr.y1]="element.attrs['y1']" [attr.y2]="element.attrs['y2']" />
17871
- }
17872
- @case ('polyline') {
17873
- <polyline [attr.points]="element.attrs['points']" />
17874
- }
17875
- @case ('rect') {
17876
- <rect [attr.x]="element.attrs['x']" [attr.y]="element.attrs['y']" [attr.width]="element.attrs['width']" [attr.height]="element.attrs['height']" [attr.rx]="element.attrs['rx']" [attr.ry]="element.attrs['ry']" />
17877
- }
17878
- @case ('ellipse') {
17879
- <ellipse [attr.cx]="element.attrs['cx']" [attr.cy]="element.attrs['cy']" [attr.rx]="element.attrs['rx']" [attr.ry]="element.attrs['ry']" />
17880
- }
17881
- @case ('polygon') {
17882
- <polygon [attr.points]="element.attrs['points']" />
17883
- }
17884
- }
17885
- }
17886
- </svg>
17846
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvIconComponent, isStandalone: true, selector: "jv-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, decorative: { classPropertyName: "decorative", publicName: "decorative", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
17847
+ <svg
17848
+ class="jv-icon"
17849
+ [attr.viewBox]="iconDefinition().viewBox"
17850
+ [attr.width]="size()"
17851
+ [attr.height]="size()"
17852
+ [attr.stroke-width]="strokeWidth()"
17853
+ [attr.role]="decorative() ? 'presentation' : 'img'"
17854
+ [attr.aria-hidden]="decorative() ? 'true' : null"
17855
+ [attr.aria-label]="decorative() ? null : ariaLabel() || normalizedName()"
17856
+ fill="none"
17857
+ stroke="currentColor"
17858
+ stroke-linecap="round"
17859
+ stroke-linejoin="round"
17860
+ >
17861
+ @for (element of iconDefinition().elements; track $index) {
17862
+ @switch (element.tag) {
17863
+ @case ('path') {
17864
+ <path [attr.d]="element.attrs['d']" />
17865
+ }
17866
+ @case ('circle') {
17867
+ <circle [attr.cx]="element.attrs['cx']" [attr.cy]="element.attrs['cy']" [attr.r]="element.attrs['r']" />
17868
+ }
17869
+ @case ('line') {
17870
+ <line [attr.x1]="element.attrs['x1']" [attr.x2]="element.attrs['x2']" [attr.y1]="element.attrs['y1']" [attr.y2]="element.attrs['y2']" />
17871
+ }
17872
+ @case ('polyline') {
17873
+ <polyline [attr.points]="element.attrs['points']" />
17874
+ }
17875
+ @case ('rect') {
17876
+ <rect [attr.x]="element.attrs['x']" [attr.y]="element.attrs['y']" [attr.width]="element.attrs['width']" [attr.height]="element.attrs['height']" [attr.rx]="element.attrs['rx']" [attr.ry]="element.attrs['ry']" />
17877
+ }
17878
+ @case ('ellipse') {
17879
+ <ellipse [attr.cx]="element.attrs['cx']" [attr.cy]="element.attrs['cy']" [attr.rx]="element.attrs['rx']" [attr.ry]="element.attrs['ry']" />
17880
+ }
17881
+ @case ('polygon') {
17882
+ <polygon [attr.points]="element.attrs['points']" />
17883
+ }
17884
+ }
17885
+ }
17886
+ </svg>
17887
17887
  `, isInline: true });
17888
17888
  }
17889
17889
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvIconComponent, decorators: [{
@@ -17891,47 +17891,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
17891
17891
  args: [{
17892
17892
  selector: 'jv-icon',
17893
17893
  standalone: true,
17894
- template: `
17895
- <svg
17896
- class="jv-icon"
17897
- [attr.viewBox]="iconDefinition().viewBox"
17898
- [attr.width]="size()"
17899
- [attr.height]="size()"
17900
- [attr.stroke-width]="strokeWidth()"
17901
- [attr.role]="decorative() ? 'presentation' : 'img'"
17902
- [attr.aria-hidden]="decorative() ? 'true' : null"
17903
- [attr.aria-label]="decorative() ? null : ariaLabel() || normalizedName()"
17904
- fill="none"
17905
- stroke="currentColor"
17906
- stroke-linecap="round"
17907
- stroke-linejoin="round"
17908
- >
17909
- @for (element of iconDefinition().elements; track $index) {
17910
- @switch (element.tag) {
17911
- @case ('path') {
17912
- <path [attr.d]="element.attrs['d']" />
17913
- }
17914
- @case ('circle') {
17915
- <circle [attr.cx]="element.attrs['cx']" [attr.cy]="element.attrs['cy']" [attr.r]="element.attrs['r']" />
17916
- }
17917
- @case ('line') {
17918
- <line [attr.x1]="element.attrs['x1']" [attr.x2]="element.attrs['x2']" [attr.y1]="element.attrs['y1']" [attr.y2]="element.attrs['y2']" />
17919
- }
17920
- @case ('polyline') {
17921
- <polyline [attr.points]="element.attrs['points']" />
17922
- }
17923
- @case ('rect') {
17924
- <rect [attr.x]="element.attrs['x']" [attr.y]="element.attrs['y']" [attr.width]="element.attrs['width']" [attr.height]="element.attrs['height']" [attr.rx]="element.attrs['rx']" [attr.ry]="element.attrs['ry']" />
17925
- }
17926
- @case ('ellipse') {
17927
- <ellipse [attr.cx]="element.attrs['cx']" [attr.cy]="element.attrs['cy']" [attr.rx]="element.attrs['rx']" [attr.ry]="element.attrs['ry']" />
17928
- }
17929
- @case ('polygon') {
17930
- <polygon [attr.points]="element.attrs['points']" />
17931
- }
17932
- }
17933
- }
17934
- </svg>
17894
+ template: `
17895
+ <svg
17896
+ class="jv-icon"
17897
+ [attr.viewBox]="iconDefinition().viewBox"
17898
+ [attr.width]="size()"
17899
+ [attr.height]="size()"
17900
+ [attr.stroke-width]="strokeWidth()"
17901
+ [attr.role]="decorative() ? 'presentation' : 'img'"
17902
+ [attr.aria-hidden]="decorative() ? 'true' : null"
17903
+ [attr.aria-label]="decorative() ? null : ariaLabel() || normalizedName()"
17904
+ fill="none"
17905
+ stroke="currentColor"
17906
+ stroke-linecap="round"
17907
+ stroke-linejoin="round"
17908
+ >
17909
+ @for (element of iconDefinition().elements; track $index) {
17910
+ @switch (element.tag) {
17911
+ @case ('path') {
17912
+ <path [attr.d]="element.attrs['d']" />
17913
+ }
17914
+ @case ('circle') {
17915
+ <circle [attr.cx]="element.attrs['cx']" [attr.cy]="element.attrs['cy']" [attr.r]="element.attrs['r']" />
17916
+ }
17917
+ @case ('line') {
17918
+ <line [attr.x1]="element.attrs['x1']" [attr.x2]="element.attrs['x2']" [attr.y1]="element.attrs['y1']" [attr.y2]="element.attrs['y2']" />
17919
+ }
17920
+ @case ('polyline') {
17921
+ <polyline [attr.points]="element.attrs['points']" />
17922
+ }
17923
+ @case ('rect') {
17924
+ <rect [attr.x]="element.attrs['x']" [attr.y]="element.attrs['y']" [attr.width]="element.attrs['width']" [attr.height]="element.attrs['height']" [attr.rx]="element.attrs['rx']" [attr.ry]="element.attrs['ry']" />
17925
+ }
17926
+ @case ('ellipse') {
17927
+ <ellipse [attr.cx]="element.attrs['cx']" [attr.cy]="element.attrs['cy']" [attr.rx]="element.attrs['rx']" [attr.ry]="element.attrs['ry']" />
17928
+ }
17929
+ @case ('polygon') {
17930
+ <polygon [attr.points]="element.attrs['points']" />
17931
+ }
17932
+ }
17933
+ }
17934
+ </svg>
17935
17935
  `,
17936
17936
  }]
17937
17937
  }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], decorative: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorative", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
@@ -18905,6 +18905,11 @@ const ES = {
18905
18905
  'grid.pageOfTotal': 'Página {page} de {total}',
18906
18906
  'grid.itemsTotal': '{total} registros',
18907
18907
  'grid.itemsShowing': 'Mostrando {start}-{end} de {total}',
18908
+ 'grid.exportCsv': 'CSV',
18909
+ 'grid.exportExcel': 'Excel',
18910
+ 'grid.filterPlaceholder': 'Filtrar...',
18911
+ 'grid.filterForColumn': 'Filtrar por ${column}',
18912
+ 'grid.actionsLabel': 'Acciones',
18908
18913
  'pagination.pageLabel': 'Página {page}',
18909
18914
  'pagination.itemsShowing': '{start}-{end} de {total}',
18910
18915
  'pagination.of': 'de',
@@ -18935,6 +18940,11 @@ const EN = {
18935
18940
  'grid.pageOfTotal': 'Page {page} of {total}',
18936
18941
  'grid.itemsTotal': '{total} items',
18937
18942
  'grid.itemsShowing': 'Showing {start}-{end} of {total}',
18943
+ 'grid.exportCsv': 'CSV',
18944
+ 'grid.exportExcel': 'Excel',
18945
+ 'grid.filterPlaceholder': 'Filter...',
18946
+ 'grid.filterForColumn': 'Filter by ${column}',
18947
+ 'grid.actionsLabel': 'Actions',
18938
18948
  'pagination.pageLabel': 'Page {page}',
18939
18949
  'pagination.itemsShowing': '{start}-{end} of {total}',
18940
18950
  'pagination.of': 'of',
@@ -20193,6 +20203,17 @@ const JV_GRID_DEFAULT_OPTIONS = {
20193
20203
  noResultsMessage: '',
20194
20204
  searchPlaceholder: '',
20195
20205
  ariaLabel: '',
20206
+ serverSide: false,
20207
+ totalItems: 0,
20208
+ stickyColumns: false,
20209
+ resizableColumns: false,
20210
+ reorderableColumns: false,
20211
+ editable: false,
20212
+ virtualScroll: false,
20213
+ virtualScrollRowHeight: 48,
20214
+ exportable: false,
20215
+ columnFilters: false,
20216
+ rowDoubleClick: false,
20196
20217
  };
20197
20218
 
20198
20219
  let gridIdSequence = 0;
@@ -20206,11 +20227,16 @@ class JvGridComponent {
20206
20227
  trackBy = input(null, ...(ngDevMode ? [{ debugName: "trackBy" }] : /* istanbul ignore next */ []));
20207
20228
  selectedIds = input([], ...(ngDevMode ? [{ debugName: "selectedIds" }] : /* istanbul ignore next */ []));
20208
20229
  rowClick = output();
20230
+ rowDoubleClick = output();
20209
20231
  actionClick = output();
20210
20232
  selectionChange = output();
20211
20233
  pageChange = output();
20212
20234
  searchChange = output();
20213
20235
  sortChange = output();
20236
+ columnFilter = output();
20237
+ columnResize = output();
20238
+ columnReorder = output();
20239
+ rowEdit = output();
20214
20240
  effectiveOptions = computed(() => ({ ...JV_GRID_DEFAULT_OPTIONS, ...this.options() }), ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : /* istanbul ignore next */ []));
20215
20241
  idKey = computed(() => {
20216
20242
  const tb = this.trackBy();
@@ -20220,7 +20246,18 @@ class JvGridComponent {
20220
20246
  }, ...(ngDevMode ? [{ debugName: "idKey" }] : /* istanbul ignore next */ []));
20221
20247
  resolvedOptions = this.effectiveOptions;
20222
20248
  gridLabel = computed(() => this.resolvedOptions().ariaLabel || `Grid ${this.gridId}`, ...(ngDevMode ? [{ debugName: "gridLabel" }] : /* istanbul ignore next */ []));
20223
- visibleColumns = computed(() => this.columns().filter(col => !col.hidden), ...(ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []));
20249
+ columnOrder = signal([], ...(ngDevMode ? [{ debugName: "columnOrder" }] : /* istanbul ignore next */ []));
20250
+ colFilters = signal({}, ...(ngDevMode ? [{ debugName: "colFilters" }] : /* istanbul ignore next */ []));
20251
+ columnWidths = signal({}, ...(ngDevMode ? [{ debugName: "columnWidths" }] : /* istanbul ignore next */ []));
20252
+ visibleColumns = computed(() => {
20253
+ const cols = this.columns().filter(col => !col.hidden);
20254
+ const order = this.columnOrder();
20255
+ if (order.length > 0) {
20256
+ return order.map(key => cols.find(c => this.colKey(c) === key)).filter(Boolean);
20257
+ }
20258
+ return cols;
20259
+ }, ...(ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []));
20260
+ groupedHeaders = computed(() => this.columns().filter(col => col.children && col.children.length > 0), ...(ngDevMode ? [{ debugName: "groupedHeaders" }] : /* istanbul ignore next */ []));
20224
20261
  internalSelectedIds = signal([], ...(ngDevMode ? [{ debugName: "internalSelectedIds" }] : /* istanbul ignore next */ []));
20225
20262
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
20226
20263
  sortState = signal({ columnKey: '', direction: null }, ...(ngDevMode ? [{ debugName: "sortState" }] : /* istanbul ignore next */ []));
@@ -20236,23 +20273,51 @@ class JvGridComponent {
20236
20273
  const ids = fromInput.length > 0 ? fromInput : fromInternal;
20237
20274
  return new Set(ids);
20238
20275
  }, ...(ngDevMode ? [{ debugName: "selectedIdSet" }] : /* istanbul ignore next */ []));
20276
+ hasActiveFilters = computed(() => Object.values(this.colFilters()).some(f => f.value.trim().length > 0), ...(ngDevMode ? [{ debugName: "hasActiveFilters" }] : /* istanbul ignore next */ []));
20239
20277
  filteredData = computed(() => {
20278
+ if (this.effectiveOptions().serverSide)
20279
+ return this.data();
20240
20280
  let items = this.data();
20241
20281
  const term = this.searchTerm().trim().toLowerCase();
20242
- if (!term)
20243
- return items;
20244
- return items.filter(row => {
20245
- return this.visibleColumns().some(col => {
20246
- if (col.searchable === false)
20247
- return false;
20248
- const value = this.getRawCellValue(row, col);
20249
- if (value == null)
20250
- return false;
20251
- return String(value).toLowerCase().includes(term);
20282
+ if (term) {
20283
+ items = items.filter(row => {
20284
+ return this.visibleColumns().some(col => {
20285
+ if (col.searchable === false)
20286
+ return false;
20287
+ const value = this.getRawCellValue(row, col);
20288
+ if (value == null)
20289
+ return false;
20290
+ return String(value).toLowerCase().includes(term);
20291
+ });
20252
20292
  });
20253
- });
20293
+ }
20294
+ const filters = this.colFilters();
20295
+ const hasFilters = Object.values(filters).some(f => f.value.trim().length > 0);
20296
+ if (hasFilters) {
20297
+ items = items.filter(row => {
20298
+ return Object.entries(filters).every(([key, filter]) => {
20299
+ if (!filter.value.trim())
20300
+ return true;
20301
+ const col = this.columns().find(c => this.colKey(c) === key);
20302
+ if (!col || col.filterable === false)
20303
+ return true;
20304
+ const value = String(this.getRawCellValue(row, col) ?? '').toLowerCase();
20305
+ const fv = filter.value.toLowerCase();
20306
+ switch (filter.operator) {
20307
+ case 'contains': return value.includes(fv);
20308
+ case 'equals': return value === fv;
20309
+ case 'startsWith': return value.startsWith(fv);
20310
+ case 'endsWith': return value.endsWith(fv);
20311
+ default: return value.includes(fv);
20312
+ }
20313
+ });
20314
+ });
20315
+ }
20316
+ return items;
20254
20317
  }, ...(ngDevMode ? [{ debugName: "filteredData" }] : /* istanbul ignore next */ []));
20255
20318
  sortedData = computed(() => {
20319
+ if (this.effectiveOptions().serverSide)
20320
+ return this.filteredData();
20256
20321
  let items = this.filteredData();
20257
20322
  const { columnKey, direction } = this.sortState();
20258
20323
  if (!columnKey || !direction)
@@ -20268,18 +20333,50 @@ class JvGridComponent {
20268
20333
  return direction === 'asc' ? cmp : -cmp;
20269
20334
  });
20270
20335
  }, ...(ngDevMode ? [{ debugName: "sortedData" }] : /* istanbul ignore next */ []));
20271
- totalItems = computed(() => this.sortedData().length, ...(ngDevMode ? [{ debugName: "totalItems" }] : /* istanbul ignore next */ []));
20272
- totalPages = computed(() => Math.max(1, Math.ceil(this.totalItems() / this.pageSize())), ...(ngDevMode ? [{ debugName: "totalPages" }] : /* istanbul ignore next */ []));
20273
- pagedData = computed(() => {
20336
+ effectiveTotalItems = computed(() => {
20337
+ if (this.effectiveOptions().serverSide) {
20338
+ return this.effectiveOptions().totalItems ?? this.data().length;
20339
+ }
20340
+ return this.sortedData().length;
20341
+ }, ...(ngDevMode ? [{ debugName: "effectiveTotalItems" }] : /* istanbul ignore next */ []));
20342
+ effectiveTotalPages = computed(() => Math.max(1, Math.ceil(this.effectiveTotalItems() / this.pageSize())), ...(ngDevMode ? [{ debugName: "effectiveTotalPages" }] : /* istanbul ignore next */ []));
20343
+ displayData = computed(() => {
20344
+ if (this.effectiveOptions().virtualScroll) {
20345
+ return this.sortedData();
20346
+ }
20347
+ if (!this.effectiveOptions().pageable)
20348
+ return this.sortedData();
20274
20349
  const items = this.sortedData();
20275
20350
  const ps = this.pageSize();
20276
- const page = Math.min(this.pageIndex(), this.totalPages() - 1);
20351
+ const page = Math.min(this.pageIndex(), this.effectiveTotalPages() - 1);
20277
20352
  return items.slice(page * ps, page * ps + ps);
20278
- }, ...(ngDevMode ? [{ debugName: "pagedData" }] : /* istanbul ignore next */ []));
20279
- pageStart = computed(() => this.totalItems() === 0 ? 0 : this.pageIndex() * this.pageSize() + 1, ...(ngDevMode ? [{ debugName: "pageStart" }] : /* istanbul ignore next */ []));
20280
- pageEnd = computed(() => Math.min((this.pageIndex() + 1) * this.pageSize(), this.totalItems()), ...(ngDevMode ? [{ debugName: "pageEnd" }] : /* istanbul ignore next */ []));
20353
+ }, ...(ngDevMode ? [{ debugName: "displayData" }] : /* istanbul ignore next */ []));
20354
+ virtualScroll = computed(() => this.effectiveOptions().virtualScroll && this.effectiveOptions().virtualScrollRowHeight, ...(ngDevMode ? [{ debugName: "virtualScroll" }] : /* istanbul ignore next */ []));
20355
+ virtualStartIndex = signal(0, ...(ngDevMode ? [{ debugName: "virtualStartIndex" }] : /* istanbul ignore next */ []));
20356
+ virtualEndIndex = signal(0, ...(ngDevMode ? [{ debugName: "virtualEndIndex" }] : /* istanbul ignore next */ []));
20357
+ virtualDisplayData = computed(() => {
20358
+ if (!this.effectiveOptions().virtualScroll)
20359
+ return this.displayData();
20360
+ const items = this.sortedData();
20361
+ const start = this.virtualStartIndex();
20362
+ const end = this.virtualEndIndex();
20363
+ if (start >= items.length)
20364
+ return [];
20365
+ return items.slice(start, Math.min(end, items.length));
20366
+ }, ...(ngDevMode ? [{ debugName: "virtualDisplayData" }] : /* istanbul ignore next */ []));
20367
+ virtualTotalHeight = computed(() => {
20368
+ const count = this.effectiveOptions().serverSide
20369
+ ? this.effectiveTotalItems()
20370
+ : this.sortedData().length;
20371
+ return count * (this.effectiveOptions().virtualScrollRowHeight ?? 48);
20372
+ }, ...(ngDevMode ? [{ debugName: "virtualTotalHeight" }] : /* istanbul ignore next */ []));
20373
+ virtualOffsetY = computed(() => {
20374
+ return this.virtualStartIndex() * (this.effectiveOptions().virtualScrollRowHeight ?? 48);
20375
+ }, ...(ngDevMode ? [{ debugName: "virtualOffsetY" }] : /* istanbul ignore next */ []));
20376
+ pageStart = computed(() => this.effectiveTotalItems() === 0 ? 0 : this.pageIndex() * this.pageSize() + 1, ...(ngDevMode ? [{ debugName: "pageStart" }] : /* istanbul ignore next */ []));
20377
+ pageEnd = computed(() => Math.min((this.pageIndex() + 1) * this.pageSize(), this.effectiveTotalItems()), ...(ngDevMode ? [{ debugName: "pageEnd" }] : /* istanbul ignore next */ []));
20281
20378
  pageRange = computed(() => {
20282
- const total = this.totalPages();
20379
+ const total = this.effectiveTotalPages();
20283
20380
  const current = this.pageIndex();
20284
20381
  const range = [];
20285
20382
  const start = Math.max(0, current - 2);
@@ -20289,11 +20386,11 @@ class JvGridComponent {
20289
20386
  return range;
20290
20387
  }, ...(ngDevMode ? [{ debugName: "pageRange" }] : /* istanbul ignore next */ []));
20291
20388
  allSelected = computed(() => {
20292
- const rows = this.pagedData();
20389
+ const rows = this.displayData();
20293
20390
  return rows.length > 0 && rows.every(r => this.selectedIdSet().has(this.getRowId(r)));
20294
20391
  }, ...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
20295
20392
  someSelected = computed(() => {
20296
- const rows = this.pagedData();
20393
+ const rows = this.displayData();
20297
20394
  return rows.some(r => this.selectedIdSet().has(this.getRowId(r))) && !this.allSelected();
20298
20395
  }, ...(ngDevMode ? [{ debugName: "someSelected" }] : /* istanbul ignore next */ []));
20299
20396
  colspan = computed(() => {
@@ -20305,6 +20402,7 @@ class JvGridComponent {
20305
20402
  return count;
20306
20403
  }, ...(ngDevMode ? [{ debugName: "colspan" }] : /* istanbul ignore next */ []));
20307
20404
  densityClass = computed(() => `jv-grid-density--${this.resolvedOptions().density ?? 'normal'}`, ...(ngDevMode ? [{ debugName: "densityClass" }] : /* istanbul ignore next */ []));
20405
+ editState = signal(null, ...(ngDevMode ? [{ debugName: "editState" }] : /* istanbul ignore next */ []));
20308
20406
  t = (key, params) => this.translationService.translate(key, params);
20309
20407
  getRowId(row, index) {
20310
20408
  const idKey = this.idKey();
@@ -20326,6 +20424,10 @@ class JvGridComponent {
20326
20424
  getCellValue(row, col) {
20327
20425
  return this.getRawCellValue(row, col);
20328
20426
  }
20427
+ getEditValue(row, col) {
20428
+ const val = this.getCellValue(row, col);
20429
+ return val == null ? '' : String(val);
20430
+ }
20329
20431
  formatValue(value, col) {
20330
20432
  if (value == null)
20331
20433
  return '';
@@ -20353,6 +20455,38 @@ class JvGridComponent {
20353
20455
  return `${col.header} - ${this.t('grid.sortDesc')}`;
20354
20456
  return `${col.header} - ${this.t('grid.sortAsc')} ${this.t('grid.sortDesc')}`;
20355
20457
  }
20458
+ getColumnWidth(col) {
20459
+ return this.columnWidths()[this.colKey(col)] ?? (col.width || undefined);
20460
+ }
20461
+ isStickyLeft(col) {
20462
+ return !!(this.resolvedOptions().stickyColumns && col.sticky === 'left');
20463
+ }
20464
+ isStickyRight(col) {
20465
+ return !!(this.resolvedOptions().stickyColumns && col.sticky === 'right');
20466
+ }
20467
+ isColumnResizable(col) {
20468
+ return this.resolvedOptions().resizableColumns || col.resizable === true;
20469
+ }
20470
+ isCellEditable(col) {
20471
+ return (this.resolvedOptions().editable || col.editable === true) && col.editable !== false;
20472
+ }
20473
+ groupColspan(group) {
20474
+ return group.children?.length ?? 1;
20475
+ }
20476
+ isEditingCell(row, col) {
20477
+ const es = this.editState();
20478
+ return es !== null && es.rowId === this.getRowId(row) && es.colKey === this.colKey(col);
20479
+ }
20480
+ isRowEditing(row) {
20481
+ const es = this.editState();
20482
+ return es !== null && es.rowId === this.getRowId(row);
20483
+ }
20484
+ getEditOptions(col) {
20485
+ return (col.editOptions || []).map(opt => ({
20486
+ label: opt.label,
20487
+ value: String(opt.value),
20488
+ }));
20489
+ }
20356
20490
  findColumn(key) {
20357
20491
  return this.columns().find(c => this.colKey(c) === key) ?? { key, header: key };
20358
20492
  }
@@ -20385,7 +20519,7 @@ class JvGridComponent {
20385
20519
  this.searchChange.emit(value);
20386
20520
  }
20387
20521
  goToPage(page) {
20388
- if (page >= 0 && page < this.totalPages()) {
20522
+ if (page >= 0 && page < this.effectiveTotalPages()) {
20389
20523
  this.pageIndex.set(page);
20390
20524
  this.pageChange.emit({ pageIndex: page, pageSize: this.pageSize() });
20391
20525
  }
@@ -20408,7 +20542,7 @@ class JvGridComponent {
20408
20542
  toggleSelectAll(checked) {
20409
20543
  this.internalSelectedIds.update(ids => {
20410
20544
  const set = new Set(ids);
20411
- const rows = this.pagedData();
20545
+ const rows = this.displayData();
20412
20546
  rows.forEach(r => {
20413
20547
  if (checked)
20414
20548
  set.add(this.getRowId(r));
@@ -20428,9 +20562,23 @@ class JvGridComponent {
20428
20562
  this.rowClick.emit(event);
20429
20563
  }
20430
20564
  }
20565
+ onRowDblClick(row) {
20566
+ if (this.resolvedOptions().rowDoubleClick) {
20567
+ this.rowDoubleClick.emit(row);
20568
+ }
20569
+ }
20431
20570
  onActionClick(action, row) {
20432
20571
  this.actionClick.emit({ actionId: action.id, row });
20433
20572
  }
20573
+ onColumnFilterChange(col, value) {
20574
+ const key = this.colKey(col);
20575
+ this.colFilters.update(f => ({
20576
+ ...f,
20577
+ [key]: { key, value, operator: 'contains' },
20578
+ }));
20579
+ this.pageIndex.set(0);
20580
+ this.columnFilter.emit({ columnKey: key, value });
20581
+ }
20434
20582
  getRowLabel(row, index) {
20435
20583
  const label = this.visibleColumns()
20436
20584
  .slice(0, 2)
@@ -20439,20 +20587,160 @@ class JvGridComponent {
20439
20587
  .join(' - ');
20440
20588
  return `${label}${this.isSelected(row) ? ` (${this.t('grid.selected')})` : ''}`;
20441
20589
  }
20590
+ startEdit(row, col, event) {
20591
+ if (!this.isCellEditable(col))
20592
+ return;
20593
+ if (col.type === 'boolean')
20594
+ return;
20595
+ event.preventDefault();
20596
+ this.editState.set({ rowId: this.getRowId(row), colKey: this.colKey(col) });
20597
+ }
20598
+ commitEdit(row, col, value) {
20599
+ const colKey = this.colKey(col);
20600
+ if (this.editState()?.colKey === colKey) {
20601
+ this.rowEdit.emit({ row, column: col, value });
20602
+ this.editState.set(null);
20603
+ }
20604
+ }
20605
+ cancelEdit() {
20606
+ this.editState.set(null);
20607
+ }
20608
+ onEditValueChange(row, col, value) {
20609
+ this.commitEdit(row, col, value);
20610
+ }
20611
+ // Column resizing
20612
+ resizeData = null;
20613
+ onResizeStart(event, col, _colIdx) {
20614
+ event.preventDefault();
20615
+ const th = event.target.closest('th');
20616
+ const startWidth = th.offsetWidth;
20617
+ this.resizeData = { col, startX: event.clientX, startWidth };
20618
+ document.addEventListener('mousemove', this.onResizeMove);
20619
+ document.addEventListener('mouseup', this.onResizeEnd);
20620
+ }
20621
+ onResizeMove = (event) => {
20622
+ if (!this.resizeData)
20623
+ return;
20624
+ const diff = event.clientX - this.resizeData.startX;
20625
+ const newWidth = Math.max(30, this.resizeData.startWidth + diff);
20626
+ this.columnWidths.update(w => ({
20627
+ ...w,
20628
+ [this.colKey(this.resizeData.col)]: `${newWidth}px`,
20629
+ }));
20630
+ };
20631
+ onResizeEnd = () => {
20632
+ if (this.resizeData) {
20633
+ const key = this.colKey(this.resizeData.col);
20634
+ const w = this.columnWidths()[key];
20635
+ this.columnResize.emit({ columnKey: key, width: w });
20636
+ }
20637
+ this.resizeData = null;
20638
+ document.removeEventListener('mousemove', this.onResizeMove);
20639
+ document.removeEventListener('mouseup', this.onResizeEnd);
20640
+ };
20641
+ // Column reordering
20642
+ dragIndex = null;
20643
+ onDragStart(event, colIdx) {
20644
+ if (!this.resolvedOptions().reorderableColumns)
20645
+ return;
20646
+ this.dragIndex = colIdx;
20647
+ event.dataTransfer.effectAllowed = 'move';
20648
+ }
20649
+ onDragOver(event, _colIdx) {
20650
+ if (!this.resolvedOptions().reorderableColumns || this.dragIndex === null)
20651
+ return;
20652
+ event.preventDefault();
20653
+ event.dataTransfer.dropEffect = 'move';
20654
+ }
20655
+ onDrop(event, targetIdx) {
20656
+ event.preventDefault();
20657
+ if (!this.resolvedOptions().reorderableColumns || this.dragIndex === null)
20658
+ return;
20659
+ if (this.dragIndex === targetIdx)
20660
+ return;
20661
+ const cols = this.visibleColumns().map(c => this.colKey(c));
20662
+ const [moved] = cols.splice(this.dragIndex, 1);
20663
+ cols.splice(targetIdx, 0, moved);
20664
+ this.columnOrder.set(cols);
20665
+ this.columnReorder.emit({ columnKey: moved, newIndex: targetIdx });
20666
+ this.dragIndex = null;
20667
+ }
20668
+ onDragEnd(_event) {
20669
+ this.dragIndex = null;
20670
+ }
20671
+ // Virtual scrolling
20672
+ onVirtualScroll(target) {
20673
+ if (!this.effectiveOptions().virtualScroll)
20674
+ return;
20675
+ const rowHeight = this.effectiveOptions().virtualScrollRowHeight ?? 48;
20676
+ const buffer = 5;
20677
+ const scrollTop = target.scrollTop;
20678
+ const clientHeight = target.clientHeight;
20679
+ const start = Math.max(0, Math.floor(scrollTop / rowHeight) - buffer);
20680
+ const end = Math.min(this.sortedData().length, Math.ceil((scrollTop + clientHeight) / rowHeight) + buffer);
20681
+ this.virtualStartIndex.set(start);
20682
+ this.virtualEndIndex.set(end);
20683
+ }
20684
+ // Export
20685
+ exportCsv() {
20686
+ const cols = this.visibleColumns().map(c => ({ key: this.colKey(c), header: c.header }));
20687
+ const allData = this.effectiveOptions().serverSide ? this.data() : this.sortedData();
20688
+ const headerRow = cols.map(c => `"${c.header}"`).join(',');
20689
+ const dataRows = allData.map(row => cols.map(col => {
20690
+ const val = row[col.key];
20691
+ if (val == null)
20692
+ return '';
20693
+ return `"${String(val).replace(/"/g, '""')}"`;
20694
+ }).join(','));
20695
+ const csv = [headerRow, ...dataRows].join('\r\n');
20696
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
20697
+ const url = URL.createObjectURL(blob);
20698
+ const a = document.createElement('a');
20699
+ a.href = url;
20700
+ a.download = 'grid-export.csv';
20701
+ a.click();
20702
+ URL.revokeObjectURL(url);
20703
+ }
20704
+ exportExcel() {
20705
+ const cols = this.visibleColumns().map(c => ({ key: this.colKey(c), header: c.header }));
20706
+ const allData = this.effectiveOptions().serverSide ? this.data() : this.sortedData();
20707
+ let html = '<table>';
20708
+ html += '<thead><tr>' + cols.map(c => `<th>${c.header}</th>`).join('') + '</tr></thead>';
20709
+ html += '<tbody>';
20710
+ for (const row of allData) {
20711
+ html += '<tr>' + cols.map(col => `<td>${row[col.key] ?? ''}</td>`).join('') + '</tr>';
20712
+ }
20713
+ html += '</tbody></table>';
20714
+ const blob = new Blob([html], { type: 'application/vnd.ms-excel' });
20715
+ const url = URL.createObjectURL(blob);
20716
+ const a = document.createElement('a');
20717
+ a.href = url;
20718
+ a.download = 'grid-export.xls';
20719
+ a.click();
20720
+ URL.revokeObjectURL(url);
20721
+ }
20442
20722
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
20443
- 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", actionClick: "actionClick", selectionChange: "selectionChange", pageChange: "pageChange", searchChange: "searchChange", sortChange: "sortChange" }, ngImport: i0, template: `
20723
+ 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: `
20444
20724
  <div class="jv-grid" role="region" [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()">
20445
- @if (resolvedOptions().searchable) {
20725
+ @if (resolvedOptions().searchable || resolvedOptions().exportable) {
20446
20726
  <div class="jv-grid-toolbar">
20447
- <div class="jv-grid-search">
20448
- <jv-icon name="search" [size]="16" />
20449
- <jv-input
20450
- inputId="grid-search"
20451
- [placeholder]="t('grid.searchPlaceholder')"
20452
- [ngModel]="searchTerm()"
20453
- (ngModelChange)="onSearchInput($event)"
20454
- />
20455
- </div>
20727
+ @if (resolvedOptions().searchable) {
20728
+ <div class="jv-grid-search">
20729
+ <jv-icon name="search" [size]="16" />
20730
+ <jv-input
20731
+ inputId="grid-search"
20732
+ [placeholder]="t('grid.searchPlaceholder')"
20733
+ [ngModel]="searchTerm()"
20734
+ (ngModelChange)="onSearchInput($event)"
20735
+ />
20736
+ </div>
20737
+ }
20738
+ @if (resolvedOptions().exportable) {
20739
+ <div class="jv-grid-export-actions">
20740
+ <jv-button variant="outline" icon="download" (click)="exportCsv()">{{ t('grid.exportCsv') }}</jv-button>
20741
+ <jv-button variant="outline" icon="file-spreadsheet" (click)="exportExcel()">{{ t('grid.exportExcel') }}</jv-button>
20742
+ </div>
20743
+ }
20456
20744
  </div>
20457
20745
  }
20458
20746
 
@@ -20462,17 +20750,67 @@ class JvGridComponent {
20462
20750
  <span>{{ t('grid.loading') }}</span>
20463
20751
  </div>
20464
20752
  } @else {
20465
- <div class="jv-grid-table-wrap">
20753
+ <div
20754
+ class="jv-grid-table-wrap"
20755
+ [class.jv-grid-virtual]="resolvedOptions().virtualScroll"
20756
+ #tableWrap
20757
+ (scroll)="onVirtualScroll($any($event.target))"
20758
+ >
20759
+ @if (resolvedOptions().virtualScroll) {
20760
+ <div class="jv-grid-virtual-spacer" [style.height.px]="virtualTotalHeight()"></div>
20761
+ }
20466
20762
  <table
20467
20763
  class="jv-grid-table"
20468
20764
  role="grid"
20469
20765
  [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()"
20766
+ [attr.aria-rowcount]="effectiveTotalItems()"
20767
+ [attr.aria-colcount]="visibleColumns().length"
20470
20768
  [class]="densityClass()"
20769
+ [style.position]="resolvedOptions().virtualScroll ? 'relative' : null"
20770
+ [style.top.px]="virtualOffsetY()"
20471
20771
  >
20472
20772
  <caption class="sr-only">{{ resolvedOptions().ariaLabel || gridLabel() }}</caption>
20773
+
20774
+ @let hasGroupedHeaders = groupedHeaders().length > 0;
20775
+
20473
20776
  <thead class="jv-grid-thead">
20777
+ @if (hasGroupedHeaders) {
20778
+ <tr>
20779
+ @if (resolvedOptions().selectable) {
20780
+ <th class="jv-grid-th jv-grid-th-select" scope="col" [attr.rowspan]="2">
20781
+ <input
20782
+ type="checkbox"
20783
+ class="jv-grid-checkbox"
20784
+ [checked]="allSelected()"
20785
+ (change)="toggleSelectAll($any($event.target).checked)"
20786
+ [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
20787
+ [indeterminate]="someSelected()"
20788
+ />
20789
+ </th>
20790
+ }
20791
+ @for (group of groupedHeaders(); track colKey(group)) {
20792
+ <th
20793
+ class="jv-grid-th"
20794
+ [class]="group.headerClass || ''"
20795
+ [class.jv-grid-th-sticky-left]="isStickyLeft(group)"
20796
+ [class.jv-grid-th-sticky-right]="isStickyRight(group)"
20797
+ [attr.colspan]="groupColspan(group)"
20798
+ scope="colgroup"
20799
+ [style.text-align]="group.align ?? 'start'"
20800
+ >
20801
+ <div class="jv-grid-th-content">{{ group.header }}</div>
20802
+ </th>
20803
+ }
20804
+ @if (actions().length > 0) {
20805
+ <th class="jv-grid-th jv-grid-th-actions" scope="col" [attr.rowspan]="2">
20806
+ <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
20807
+ </th>
20808
+ }
20809
+ </tr>
20810
+ }
20811
+
20474
20812
  <tr>
20475
- @if (resolvedOptions().selectable) {
20813
+ @if (!hasGroupedHeaders && resolvedOptions().selectable) {
20476
20814
  <th class="jv-grid-th jv-grid-th-select" scope="col">
20477
20815
  <input
20478
20816
  type="checkbox"
@@ -20484,13 +20822,18 @@ class JvGridComponent {
20484
20822
  />
20485
20823
  </th>
20486
20824
  }
20487
- @for (col of visibleColumns(); track colKey(col)) {
20825
+ @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
20488
20826
  <th
20489
20827
  class="jv-grid-th"
20490
20828
  [class]="col.headerClass || ''"
20491
20829
  [class.jv-grid-th-sortable]="col.sortable !== false && resolvedOptions().sortable"
20830
+ [class.jv-grid-th-sticky-left]="isStickyLeft(col)"
20831
+ [class.jv-grid-th-sticky-right]="isStickyRight(col)"
20832
+ [class.jv-grid-th-reorderable]="resolvedOptions().reorderableColumns"
20492
20833
  [style.text-align]="col.align ?? 'start'"
20493
- [style.width]="col.width || undefined"
20834
+ [style.width]="getColumnWidth(col)"
20835
+ [style.min-width]="col.minWidth || undefined"
20836
+ [style.max-width]="col.maxWidth || undefined"
20494
20837
  scope="col"
20495
20838
  [attr.aria-sort]="getAriaSort(col)"
20496
20839
  [attr.aria-label]="getSortLabel(col)"
@@ -20498,6 +20841,11 @@ class JvGridComponent {
20498
20841
  (click)="toggleSort(col)"
20499
20842
  (keydown.enter)="toggleSort(col)"
20500
20843
  (keydown.space)="toggleSort(col, $event)"
20844
+ draggable="{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}"
20845
+ (dragstart)="onDragStart($event, colIdx)"
20846
+ (dragover)="onDragOver($event, colIdx)"
20847
+ (drop)="onDrop($event, colIdx)"
20848
+ (dragend)="onDragEnd($event)"
20501
20849
  >
20502
20850
  <div class="jv-grid-th-content">
20503
20851
  <span>{{ col.header }}</span>
@@ -20510,45 +20858,78 @@ class JvGridComponent {
20510
20858
  </span>
20511
20859
  }
20512
20860
  </div>
20861
+ @if (isColumnResizable(col)) {
20862
+ <div
20863
+ class="jv-grid-resize-handle"
20864
+ (mousedown)="onResizeStart($event, col, colIdx)"
20865
+ ></div>
20866
+ }
20513
20867
  </th>
20514
20868
  }
20515
- @if (actions().length > 0) {
20869
+ @if (!hasGroupedHeaders && actions().length > 0) {
20516
20870
  <th class="jv-grid-th jv-grid-th-actions" scope="col">
20517
20871
  <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
20518
20872
  </th>
20519
20873
  }
20520
20874
  </tr>
20875
+
20876
+ @if (resolvedOptions().columnFilters) {
20877
+ <tr class="jv-grid-filter-row">
20878
+ @if (resolvedOptions().selectable) {
20879
+ <th class="jv-grid-th jv-grid-th-select"></th>
20880
+ }
20881
+ @for (col of visibleColumns(); track colKey(col)) {
20882
+ <th class="jv-grid-th jv-grid-th-filter">
20883
+ @if (col.filterable !== false) {
20884
+ <input
20885
+ class="jv-grid-filter-input"
20886
+ [placeholder]="t('grid.filterPlaceholder')"
20887
+ [attr.aria-label]="t('grid.filterForColumn', { column: col.header })"
20888
+ [ngModel]="colFilters()[colKey(col)]?.value ?? ''"
20889
+ (ngModelChange)="onColumnFilterChange(col, $event)"
20890
+ />
20891
+ }
20892
+ </th>
20893
+ }
20894
+ @if (actions().length > 0) {
20895
+ <th class="jv-grid-th jv-grid-th-actions"></th>
20896
+ }
20897
+ </tr>
20898
+ }
20521
20899
  </thead>
20900
+
20522
20901
  <tbody class="jv-grid-tbody">
20523
- @if (pagedData().length === 0 && !searchTerm().trim()) {
20902
+ @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {
20524
20903
  <tr>
20525
- <td
20526
- class="jv-grid-empty"
20527
- [attr.colspan]="colspan()"
20528
- >
20904
+ <td class="jv-grid-empty" [attr.colspan]="colspan()">
20529
20905
  {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}
20530
20906
  </td>
20531
20907
  </tr>
20532
- } @else if (pagedData().length === 0 && searchTerm().trim()) {
20908
+ } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {
20533
20909
  <tr>
20534
- <td
20535
- class="jv-grid-empty"
20536
- [attr.colspan]="colspan()"
20537
- >
20910
+ <td class="jv-grid-empty" [attr.colspan]="colspan()">
20538
20911
  {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}
20539
20912
  </td>
20540
20913
  </tr>
20541
20914
  } @else {
20542
- @for (row of pagedData(); track getRowId(row, $index)) {
20915
+ @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {
20543
20916
  <tr
20544
20917
  class="jv-grid-tr"
20918
+ role="row"
20919
+ [class.jv-grid-tr-editing]="isRowEditing(row)"
20920
+ [class.jv-grid-tr-selected]="isSelected(row)"
20921
+ [attr.aria-selected]="resolvedOptions().selectable ? isSelected(row) : null"
20545
20922
  (click)="onRowClick(row)"
20923
+ (dblclick)="onRowDblClick(row)"
20546
20924
  (keydown.enter)="onRowClick(row)"
20547
20925
  (keydown.space)="onRowClick($event, row)"
20548
- [tabindex]="0"
20926
+ [tabindex]="-1"
20549
20927
  >
20550
20928
  @if (resolvedOptions().selectable) {
20551
- <td class="jv-grid-td jv-grid-td-select">
20929
+ <td
20930
+ class="jv-grid-td jv-grid-td-select"
20931
+ [class.jv-grid-td-sticky-left]="resolvedOptions().stickyColumns"
20932
+ >
20552
20933
  <input
20553
20934
  type="checkbox"
20554
20935
  class="jv-grid-checkbox"
@@ -20563,24 +20944,61 @@ class JvGridComponent {
20563
20944
  <td
20564
20945
  class="jv-grid-td"
20565
20946
  [class]="col.cellClass || ''"
20947
+ [class.jv-grid-td-sticky-left]="isStickyLeft(col)"
20948
+ [class.jv-grid-td-sticky-right]="isStickyRight(col)"
20949
+ [class.jv-grid-td-editable]="isCellEditable(col)"
20566
20950
  [style.text-align]="col.align ?? 'start'"
20951
+ (dblclick)="startEdit(row, col, $event)"
20952
+ (keydown.enter)="startEdit(row, col, $event)"
20953
+ [tabindex]="isCellEditable(col) ? -1 : null"
20567
20954
  >
20568
- @let value = getCellValue(row, col);
20569
- @if (col.type === 'boolean') {
20570
- @if (value) {
20571
- <span class="jv-grid-boolean jv-grid-boolean-true" aria-label="true">✓</span>
20955
+ @if (isEditingCell(row, col)) {
20956
+ @if (col.editType === 'select' && col.editOptions) {
20957
+ <jv-select
20958
+ [options]="getEditOptions(col)"
20959
+ [modelValue]="getEditValue(row, col)"
20960
+ (selectionChange)="onEditValueChange(row, col, $event)"
20961
+ />
20962
+ } @else if (col.editType === 'boolean') {
20963
+ <input
20964
+ type="checkbox"
20965
+ class="jv-grid-checkbox"
20966
+ [checked]="getCellValue(row, col) === true"
20967
+ (change)="commitEdit(row, col, $any($event.target).checked)"
20968
+ [attr.aria-label]="col.header"
20969
+ />
20572
20970
  } @else {
20573
- <span class="jv-grid-boolean jv-grid-boolean-false" aria-label="false">✗</span>
20971
+ <input
20972
+ class="jv-grid-edit-input"
20973
+ [type]="col.editType === 'number' ? 'number' : 'text'"
20974
+ [value]="getEditValue(row, col)"
20975
+ (blur)="commitEdit(row, col, $any($event.target).value)"
20976
+ (keydown.enter)="commitEdit(row, col, $any($event.target).value)"
20977
+ (keydown.escape)="cancelEdit()"
20978
+ autofocus
20979
+ />
20574
20980
  }
20575
- } @else if (col.format) {
20576
- {{ col.format(value, row) }}
20577
20981
  } @else {
20578
- {{ formatValue(value, col) }}
20982
+ @let value = getCellValue(row, col);
20983
+ @if (col.type === 'boolean') {
20984
+ @if (value) {
20985
+ <span class="jv-grid-boolean jv-grid-boolean-true" aria-label="true">✓</span>
20986
+ } @else {
20987
+ <span class="jv-grid-boolean jv-grid-boolean-false" aria-label="false">✗</span>
20988
+ }
20989
+ } @else if (col.format) {
20990
+ {{ col.format(value, row) }}
20991
+ } @else {
20992
+ {{ formatValue(value, col) }}
20993
+ }
20579
20994
  }
20580
20995
  </td>
20581
20996
  }
20582
20997
  @if (actions().length > 0) {
20583
- <td class="jv-grid-td jv-grid-td-actions">
20998
+ <td
20999
+ class="jv-grid-td jv-grid-td-actions"
21000
+ [class.jv-grid-td-sticky-right]="resolvedOptions().stickyColumns"
21001
+ >
20584
21002
  <div class="jv-grid-actions-group">
20585
21003
  @for (action of actions(); track action.id) {
20586
21004
  <jv-button
@@ -20588,8 +21006,9 @@ class JvGridComponent {
20588
21006
  [icon]="action.icon ?? null"
20589
21007
  [disabled]="action.disabled?.(row) ?? false"
20590
21008
  [attr.aria-label]="action.label"
21009
+ [title]="action.label"
20591
21010
  (click)="onActionClick(action, row); $event.stopPropagation()"
20592
- >{{ action.label }}</jv-button>
21011
+ ></jv-button>
20593
21012
  }
20594
21013
  </div>
20595
21014
  </td>
@@ -20602,10 +21021,10 @@ class JvGridComponent {
20602
21021
  </div>
20603
21022
  }
20604
21023
 
20605
- @if (resolvedOptions().pageable && totalPages() > 1 && !resolvedOptions().loading) {
20606
- <nav class="jv-grid-pagination" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
20607
- <div class="jv-grid-pagination-info">
20608
- {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: totalItems() }) }}
21024
+ @if (resolvedOptions().pageable && effectiveTotalPages() > 1 && !resolvedOptions().loading) {
21025
+ <nav class="jv-grid-pagination" role="navigation" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
21026
+ <div class="jv-grid-pagination-info" aria-live="polite" aria-atomic="true">
21027
+ {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: effectiveTotalItems() }) }}
20609
21028
  </div>
20610
21029
  <div class="jv-grid-pagination-controls">
20611
21030
  <jv-button
@@ -20626,45 +21045,53 @@ class JvGridComponent {
20626
21045
  <jv-button
20627
21046
  [variant]="p === pageIndex() ? 'primary' : 'outline'"
20628
21047
  [attr.aria-current]="p === pageIndex() ? 'page' : null"
20629
- [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: totalPages() })"
21048
+ [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })"
20630
21049
  (click)="goToPage(p)"
20631
21050
  >{{ p + 1 }}</jv-button>
20632
21051
  }
20633
21052
  <jv-button
20634
21053
  variant="outline"
20635
21054
  icon="chevron-right"
20636
- [disabled]="pageIndex() >= totalPages() - 1"
21055
+ [disabled]="pageIndex() >= effectiveTotalPages() - 1"
20637
21056
  (click)="goToPage(pageIndex() + 1)"
20638
21057
  [attr.aria-label]="t('grid.pageNext')"
20639
21058
  ></jv-button>
20640
21059
  <jv-button
20641
21060
  variant="outline"
20642
21061
  icon="chevrons-right"
20643
- [disabled]="pageIndex() >= totalPages() - 1"
20644
- (click)="goToPage(totalPages() - 1)"
21062
+ [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21063
+ (click)="goToPage(effectiveTotalPages() - 1)"
20645
21064
  [attr.aria-label]="t('grid.pageLast')"
20646
21065
  ></jv-button>
20647
21066
  </div>
20648
21067
  </nav>
20649
21068
  }
20650
21069
  </div>
20651
- `, isInline: true, styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;gap:var(--jv-spacing-md)}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-table-wrap{overflow-x:auto;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md)}.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}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.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-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-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)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { 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"] }] });
21070
+ `, 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-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", "inputId", "modelValue"], outputs: ["selectionChange"] }] });
20652
21071
  }
20653
21072
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvGridComponent, decorators: [{
20654
21073
  type: Component,
20655
- args: [{ selector: 'jv-grid', standalone: true, imports: [FormsModule, JvButtonComponent, JvIconComponent, JvInputComponent], template: `
21074
+ args: [{ selector: 'jv-grid', standalone: true, imports: [FormsModule, JvButtonComponent, JvIconComponent, JvInputComponent, JvSelectComponent], template: `
20656
21075
  <div class="jv-grid" role="region" [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()">
20657
- @if (resolvedOptions().searchable) {
21076
+ @if (resolvedOptions().searchable || resolvedOptions().exportable) {
20658
21077
  <div class="jv-grid-toolbar">
20659
- <div class="jv-grid-search">
20660
- <jv-icon name="search" [size]="16" />
20661
- <jv-input
20662
- inputId="grid-search"
20663
- [placeholder]="t('grid.searchPlaceholder')"
20664
- [ngModel]="searchTerm()"
20665
- (ngModelChange)="onSearchInput($event)"
20666
- />
20667
- </div>
21078
+ @if (resolvedOptions().searchable) {
21079
+ <div class="jv-grid-search">
21080
+ <jv-icon name="search" [size]="16" />
21081
+ <jv-input
21082
+ inputId="grid-search"
21083
+ [placeholder]="t('grid.searchPlaceholder')"
21084
+ [ngModel]="searchTerm()"
21085
+ (ngModelChange)="onSearchInput($event)"
21086
+ />
21087
+ </div>
21088
+ }
21089
+ @if (resolvedOptions().exportable) {
21090
+ <div class="jv-grid-export-actions">
21091
+ <jv-button variant="outline" icon="download" (click)="exportCsv()">{{ t('grid.exportCsv') }}</jv-button>
21092
+ <jv-button variant="outline" icon="file-spreadsheet" (click)="exportExcel()">{{ t('grid.exportExcel') }}</jv-button>
21093
+ </div>
21094
+ }
20668
21095
  </div>
20669
21096
  }
20670
21097
 
@@ -20674,17 +21101,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
20674
21101
  <span>{{ t('grid.loading') }}</span>
20675
21102
  </div>
20676
21103
  } @else {
20677
- <div class="jv-grid-table-wrap">
21104
+ <div
21105
+ class="jv-grid-table-wrap"
21106
+ [class.jv-grid-virtual]="resolvedOptions().virtualScroll"
21107
+ #tableWrap
21108
+ (scroll)="onVirtualScroll($any($event.target))"
21109
+ >
21110
+ @if (resolvedOptions().virtualScroll) {
21111
+ <div class="jv-grid-virtual-spacer" [style.height.px]="virtualTotalHeight()"></div>
21112
+ }
20678
21113
  <table
20679
21114
  class="jv-grid-table"
20680
21115
  role="grid"
20681
21116
  [attr.aria-label]="resolvedOptions().ariaLabel || gridLabel()"
21117
+ [attr.aria-rowcount]="effectiveTotalItems()"
21118
+ [attr.aria-colcount]="visibleColumns().length"
20682
21119
  [class]="densityClass()"
21120
+ [style.position]="resolvedOptions().virtualScroll ? 'relative' : null"
21121
+ [style.top.px]="virtualOffsetY()"
20683
21122
  >
20684
21123
  <caption class="sr-only">{{ resolvedOptions().ariaLabel || gridLabel() }}</caption>
21124
+
21125
+ @let hasGroupedHeaders = groupedHeaders().length > 0;
21126
+
20685
21127
  <thead class="jv-grid-thead">
21128
+ @if (hasGroupedHeaders) {
21129
+ <tr>
21130
+ @if (resolvedOptions().selectable) {
21131
+ <th class="jv-grid-th jv-grid-th-select" scope="col" [attr.rowspan]="2">
21132
+ <input
21133
+ type="checkbox"
21134
+ class="jv-grid-checkbox"
21135
+ [checked]="allSelected()"
21136
+ (change)="toggleSelectAll($any($event.target).checked)"
21137
+ [attr.aria-label]="allSelected() ? t('grid.deselectAll') : t('grid.selectAll')"
21138
+ [indeterminate]="someSelected()"
21139
+ />
21140
+ </th>
21141
+ }
21142
+ @for (group of groupedHeaders(); track colKey(group)) {
21143
+ <th
21144
+ class="jv-grid-th"
21145
+ [class]="group.headerClass || ''"
21146
+ [class.jv-grid-th-sticky-left]="isStickyLeft(group)"
21147
+ [class.jv-grid-th-sticky-right]="isStickyRight(group)"
21148
+ [attr.colspan]="groupColspan(group)"
21149
+ scope="colgroup"
21150
+ [style.text-align]="group.align ?? 'start'"
21151
+ >
21152
+ <div class="jv-grid-th-content">{{ group.header }}</div>
21153
+ </th>
21154
+ }
21155
+ @if (actions().length > 0) {
21156
+ <th class="jv-grid-th jv-grid-th-actions" scope="col" [attr.rowspan]="2">
21157
+ <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
21158
+ </th>
21159
+ }
21160
+ </tr>
21161
+ }
21162
+
20686
21163
  <tr>
20687
- @if (resolvedOptions().selectable) {
21164
+ @if (!hasGroupedHeaders && resolvedOptions().selectable) {
20688
21165
  <th class="jv-grid-th jv-grid-th-select" scope="col">
20689
21166
  <input
20690
21167
  type="checkbox"
@@ -20696,13 +21173,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
20696
21173
  />
20697
21174
  </th>
20698
21175
  }
20699
- @for (col of visibleColumns(); track colKey(col)) {
21176
+ @for (col of visibleColumns(); track colKey(col); let colIdx = $index) {
20700
21177
  <th
20701
21178
  class="jv-grid-th"
20702
21179
  [class]="col.headerClass || ''"
20703
21180
  [class.jv-grid-th-sortable]="col.sortable !== false && resolvedOptions().sortable"
21181
+ [class.jv-grid-th-sticky-left]="isStickyLeft(col)"
21182
+ [class.jv-grid-th-sticky-right]="isStickyRight(col)"
21183
+ [class.jv-grid-th-reorderable]="resolvedOptions().reorderableColumns"
20704
21184
  [style.text-align]="col.align ?? 'start'"
20705
- [style.width]="col.width || undefined"
21185
+ [style.width]="getColumnWidth(col)"
21186
+ [style.min-width]="col.minWidth || undefined"
21187
+ [style.max-width]="col.maxWidth || undefined"
20706
21188
  scope="col"
20707
21189
  [attr.aria-sort]="getAriaSort(col)"
20708
21190
  [attr.aria-label]="getSortLabel(col)"
@@ -20710,6 +21192,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
20710
21192
  (click)="toggleSort(col)"
20711
21193
  (keydown.enter)="toggleSort(col)"
20712
21194
  (keydown.space)="toggleSort(col, $event)"
21195
+ draggable="{{ resolvedOptions().reorderableColumns ? 'true' : 'false' }}"
21196
+ (dragstart)="onDragStart($event, colIdx)"
21197
+ (dragover)="onDragOver($event, colIdx)"
21198
+ (drop)="onDrop($event, colIdx)"
21199
+ (dragend)="onDragEnd($event)"
20713
21200
  >
20714
21201
  <div class="jv-grid-th-content">
20715
21202
  <span>{{ col.header }}</span>
@@ -20722,45 +21209,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
20722
21209
  </span>
20723
21210
  }
20724
21211
  </div>
21212
+ @if (isColumnResizable(col)) {
21213
+ <div
21214
+ class="jv-grid-resize-handle"
21215
+ (mousedown)="onResizeStart($event, col, colIdx)"
21216
+ ></div>
21217
+ }
20725
21218
  </th>
20726
21219
  }
20727
- @if (actions().length > 0) {
21220
+ @if (!hasGroupedHeaders && actions().length > 0) {
20728
21221
  <th class="jv-grid-th jv-grid-th-actions" scope="col">
20729
21222
  <span class="sr-only">{{ t('grid.actionsLabel', { label: '' }) }}</span>
20730
21223
  </th>
20731
21224
  }
20732
21225
  </tr>
21226
+
21227
+ @if (resolvedOptions().columnFilters) {
21228
+ <tr class="jv-grid-filter-row">
21229
+ @if (resolvedOptions().selectable) {
21230
+ <th class="jv-grid-th jv-grid-th-select"></th>
21231
+ }
21232
+ @for (col of visibleColumns(); track colKey(col)) {
21233
+ <th class="jv-grid-th jv-grid-th-filter">
21234
+ @if (col.filterable !== false) {
21235
+ <input
21236
+ class="jv-grid-filter-input"
21237
+ [placeholder]="t('grid.filterPlaceholder')"
21238
+ [attr.aria-label]="t('grid.filterForColumn', { column: col.header })"
21239
+ [ngModel]="colFilters()[colKey(col)]?.value ?? ''"
21240
+ (ngModelChange)="onColumnFilterChange(col, $event)"
21241
+ />
21242
+ }
21243
+ </th>
21244
+ }
21245
+ @if (actions().length > 0) {
21246
+ <th class="jv-grid-th jv-grid-th-actions"></th>
21247
+ }
21248
+ </tr>
21249
+ }
20733
21250
  </thead>
21251
+
20734
21252
  <tbody class="jv-grid-tbody">
20735
- @if (pagedData().length === 0 && !searchTerm().trim()) {
21253
+ @if (displayData().length === 0 && !searchTerm().trim() && !hasActiveFilters()) {
20736
21254
  <tr>
20737
- <td
20738
- class="jv-grid-empty"
20739
- [attr.colspan]="colspan()"
20740
- >
21255
+ <td class="jv-grid-empty" [attr.colspan]="colspan()">
20741
21256
  {{ resolvedOptions().emptyMessage || t('grid.emptyMessage') }}
20742
21257
  </td>
20743
21258
  </tr>
20744
- } @else if (pagedData().length === 0 && searchTerm().trim()) {
21259
+ } @else if (displayData().length === 0 && (searchTerm().trim() || hasActiveFilters())) {
20745
21260
  <tr>
20746
- <td
20747
- class="jv-grid-empty"
20748
- [attr.colspan]="colspan()"
20749
- >
21261
+ <td class="jv-grid-empty" [attr.colspan]="colspan()">
20750
21262
  {{ resolvedOptions().noResultsMessage || t('grid.noResults') }}
20751
21263
  </td>
20752
21264
  </tr>
20753
21265
  } @else {
20754
- @for (row of pagedData(); track getRowId(row, $index)) {
21266
+ @for (row of displayData(); track getRowId(row, $index); let rowIdx = $index) {
20755
21267
  <tr
20756
21268
  class="jv-grid-tr"
21269
+ role="row"
21270
+ [class.jv-grid-tr-editing]="isRowEditing(row)"
21271
+ [class.jv-grid-tr-selected]="isSelected(row)"
21272
+ [attr.aria-selected]="resolvedOptions().selectable ? isSelected(row) : null"
20757
21273
  (click)="onRowClick(row)"
21274
+ (dblclick)="onRowDblClick(row)"
20758
21275
  (keydown.enter)="onRowClick(row)"
20759
21276
  (keydown.space)="onRowClick($event, row)"
20760
- [tabindex]="0"
21277
+ [tabindex]="-1"
20761
21278
  >
20762
21279
  @if (resolvedOptions().selectable) {
20763
- <td class="jv-grid-td jv-grid-td-select">
21280
+ <td
21281
+ class="jv-grid-td jv-grid-td-select"
21282
+ [class.jv-grid-td-sticky-left]="resolvedOptions().stickyColumns"
21283
+ >
20764
21284
  <input
20765
21285
  type="checkbox"
20766
21286
  class="jv-grid-checkbox"
@@ -20775,24 +21295,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
20775
21295
  <td
20776
21296
  class="jv-grid-td"
20777
21297
  [class]="col.cellClass || ''"
21298
+ [class.jv-grid-td-sticky-left]="isStickyLeft(col)"
21299
+ [class.jv-grid-td-sticky-right]="isStickyRight(col)"
21300
+ [class.jv-grid-td-editable]="isCellEditable(col)"
20778
21301
  [style.text-align]="col.align ?? 'start'"
21302
+ (dblclick)="startEdit(row, col, $event)"
21303
+ (keydown.enter)="startEdit(row, col, $event)"
21304
+ [tabindex]="isCellEditable(col) ? -1 : null"
20779
21305
  >
20780
- @let value = getCellValue(row, col);
20781
- @if (col.type === 'boolean') {
20782
- @if (value) {
20783
- <span class="jv-grid-boolean jv-grid-boolean-true" aria-label="true">✓</span>
21306
+ @if (isEditingCell(row, col)) {
21307
+ @if (col.editType === 'select' && col.editOptions) {
21308
+ <jv-select
21309
+ [options]="getEditOptions(col)"
21310
+ [modelValue]="getEditValue(row, col)"
21311
+ (selectionChange)="onEditValueChange(row, col, $event)"
21312
+ />
21313
+ } @else if (col.editType === 'boolean') {
21314
+ <input
21315
+ type="checkbox"
21316
+ class="jv-grid-checkbox"
21317
+ [checked]="getCellValue(row, col) === true"
21318
+ (change)="commitEdit(row, col, $any($event.target).checked)"
21319
+ [attr.aria-label]="col.header"
21320
+ />
20784
21321
  } @else {
20785
- <span class="jv-grid-boolean jv-grid-boolean-false" aria-label="false">✗</span>
21322
+ <input
21323
+ class="jv-grid-edit-input"
21324
+ [type]="col.editType === 'number' ? 'number' : 'text'"
21325
+ [value]="getEditValue(row, col)"
21326
+ (blur)="commitEdit(row, col, $any($event.target).value)"
21327
+ (keydown.enter)="commitEdit(row, col, $any($event.target).value)"
21328
+ (keydown.escape)="cancelEdit()"
21329
+ autofocus
21330
+ />
20786
21331
  }
20787
- } @else if (col.format) {
20788
- {{ col.format(value, row) }}
20789
21332
  } @else {
20790
- {{ formatValue(value, col) }}
21333
+ @let value = getCellValue(row, col);
21334
+ @if (col.type === 'boolean') {
21335
+ @if (value) {
21336
+ <span class="jv-grid-boolean jv-grid-boolean-true" aria-label="true">✓</span>
21337
+ } @else {
21338
+ <span class="jv-grid-boolean jv-grid-boolean-false" aria-label="false">✗</span>
21339
+ }
21340
+ } @else if (col.format) {
21341
+ {{ col.format(value, row) }}
21342
+ } @else {
21343
+ {{ formatValue(value, col) }}
21344
+ }
20791
21345
  }
20792
21346
  </td>
20793
21347
  }
20794
21348
  @if (actions().length > 0) {
20795
- <td class="jv-grid-td jv-grid-td-actions">
21349
+ <td
21350
+ class="jv-grid-td jv-grid-td-actions"
21351
+ [class.jv-grid-td-sticky-right]="resolvedOptions().stickyColumns"
21352
+ >
20796
21353
  <div class="jv-grid-actions-group">
20797
21354
  @for (action of actions(); track action.id) {
20798
21355
  <jv-button
@@ -20800,8 +21357,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
20800
21357
  [icon]="action.icon ?? null"
20801
21358
  [disabled]="action.disabled?.(row) ?? false"
20802
21359
  [attr.aria-label]="action.label"
21360
+ [title]="action.label"
20803
21361
  (click)="onActionClick(action, row); $event.stopPropagation()"
20804
- >{{ action.label }}</jv-button>
21362
+ ></jv-button>
20805
21363
  }
20806
21364
  </div>
20807
21365
  </td>
@@ -20814,10 +21372,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
20814
21372
  </div>
20815
21373
  }
20816
21374
 
20817
- @if (resolvedOptions().pageable && totalPages() > 1 && !resolvedOptions().loading) {
20818
- <nav class="jv-grid-pagination" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
20819
- <div class="jv-grid-pagination-info">
20820
- {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: totalItems() }) }}
21375
+ @if (resolvedOptions().pageable && effectiveTotalPages() > 1 && !resolvedOptions().loading) {
21376
+ <nav class="jv-grid-pagination" role="navigation" [attr.aria-label]="t('grid.paginationLabel', { label: '' })">
21377
+ <div class="jv-grid-pagination-info" aria-live="polite" aria-atomic="true">
21378
+ {{ t('grid.itemsShowing', { start: pageStart(), end: pageEnd(), total: effectiveTotalItems() }) }}
20821
21379
  </div>
20822
21380
  <div class="jv-grid-pagination-controls">
20823
21381
  <jv-button
@@ -20838,30 +21396,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
20838
21396
  <jv-button
20839
21397
  [variant]="p === pageIndex() ? 'primary' : 'outline'"
20840
21398
  [attr.aria-current]="p === pageIndex() ? 'page' : null"
20841
- [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: totalPages() })"
21399
+ [attr.aria-label]="t('grid.pageOfTotal', { page: p + 1, total: effectiveTotalPages() })"
20842
21400
  (click)="goToPage(p)"
20843
21401
  >{{ p + 1 }}</jv-button>
20844
21402
  }
20845
21403
  <jv-button
20846
21404
  variant="outline"
20847
21405
  icon="chevron-right"
20848
- [disabled]="pageIndex() >= totalPages() - 1"
21406
+ [disabled]="pageIndex() >= effectiveTotalPages() - 1"
20849
21407
  (click)="goToPage(pageIndex() + 1)"
20850
21408
  [attr.aria-label]="t('grid.pageNext')"
20851
21409
  ></jv-button>
20852
21410
  <jv-button
20853
21411
  variant="outline"
20854
21412
  icon="chevrons-right"
20855
- [disabled]="pageIndex() >= totalPages() - 1"
20856
- (click)="goToPage(totalPages() - 1)"
21413
+ [disabled]="pageIndex() >= effectiveTotalPages() - 1"
21414
+ (click)="goToPage(effectiveTotalPages() - 1)"
20857
21415
  [attr.aria-label]="t('grid.pageLast')"
20858
21416
  ></jv-button>
20859
21417
  </div>
20860
21418
  </nav>
20861
21419
  }
20862
21420
  </div>
20863
- `, styles: [".jv-grid{display:flex;flex-direction:column;gap:var(--jv-spacing-md)}.jv-grid-toolbar{display:flex;align-items:center;gap:var(--jv-spacing-md)}.jv-grid-search{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-grid-table-wrap{overflow-x:auto;border:1px solid var(--jv-color-border);border-radius:var(--jv-radius-md)}.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}.jv-grid-th-sortable{cursor:pointer}.jv-grid-th-sortable:hover{background:var(--jv-color-surface)}.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-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-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)}\n"] }]
20864
- }], 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"] }], 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"] }] } });
21421
+ `, 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-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"] }]
21422
+ }], 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"] }] } });
20865
21423
 
20866
21424
  class JvPaginationComponent {
20867
21425
  translationService = inject(JvTranslationService);
@@ -20900,130 +21458,130 @@ class JvPaginationComponent {
20900
21458
  this.pageChange.emit({ pageIndex: 0, pageSize: Number(value) });
20901
21459
  }
20902
21460
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvPaginationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
20903
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvPaginationComponent, isStandalone: true, selector: "jv-pagination", inputs: { totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: true, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, maxVisiblePages: { classPropertyName: "maxVisiblePages", publicName: "maxVisiblePages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange" }, ngImport: i0, template: `
20904
- <nav class="jv-pagination" role="navigation" [attr.aria-label]="t('pagination.pageLabel', { page: pageIndex() + 1 })">
20905
- <div class="jv-pagination-info">
20906
- <span>{{ t('pagination.itemsShowing', { start: pageStart(), end: pageEnd(), total: totalItems() }) }}</span>
20907
- </div>
20908
-
20909
- <div class="jv-pagination-controls">
20910
- <div class="jv-pagination-size">
20911
- <label class="jv-pagination-size-label">{{ t('pagination.itemsPerPage') }}</label>
20912
- <jv-select
20913
- [options]="pageSizeOptionsList()"
20914
- [modelValue]="pageSizeStr()"
20915
- (selectionChange)="onPageSizeChange($event)"
20916
- />
20917
- </div>
20918
-
20919
- <div class="jv-pagination-buttons">
20920
- <jv-button
20921
- variant="outline"
20922
- icon="chevrons-left"
20923
- [disabled]="pageIndex() === 0"
20924
- (click)="goToPage(0)"
20925
- [attr.aria-label]="t('pagination.firstPage')"
20926
- />
20927
-
20928
- <jv-button
20929
- variant="outline"
20930
- icon="chevron-left"
20931
- [disabled]="pageIndex() === 0"
20932
- (click)="goToPage(pageIndex() - 1)"
20933
- [attr.aria-label]="t('pagination.prevPage')"
20934
- />
20935
-
20936
- @for (p of pageRange(); track p) {
20937
- <jv-button
20938
- [variant]="p === pageIndex() ? 'primary' : 'outline'"
20939
- [attr.aria-current]="p === pageIndex() ? 'page' : null"
20940
- [attr.aria-label]="t('pagination.pageLabel', { page: p + 1 })"
20941
- (click)="goToPage(p)"
20942
- >{{ p + 1 }}</jv-button>
20943
- }
20944
-
20945
- <jv-button
20946
- variant="outline"
20947
- icon="chevron-right"
20948
- [disabled]="pageIndex() >= totalPages() - 1"
20949
- (click)="goToPage(pageIndex() + 1)"
20950
- [attr.aria-label]="t('pagination.nextPage')"
20951
- />
20952
-
20953
- <jv-button
20954
- variant="outline"
20955
- icon="chevrons-right"
20956
- [disabled]="pageIndex() >= totalPages() - 1"
20957
- (click)="goToPage(totalPages() - 1)"
20958
- [attr.aria-label]="t('pagination.lastPage')"
20959
- />
20960
- </div>
20961
- </div>
20962
- </nav>
21461
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: JvPaginationComponent, isStandalone: true, selector: "jv-pagination", inputs: { totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: true, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, maxVisiblePages: { classPropertyName: "maxVisiblePages", publicName: "maxVisiblePages", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange" }, ngImport: i0, template: `
21462
+ <nav class="jv-pagination" role="navigation" [attr.aria-label]="t('pagination.pageLabel', { page: pageIndex() + 1 })">
21463
+ <div class="jv-pagination-info">
21464
+ <span>{{ t('pagination.itemsShowing', { start: pageStart(), end: pageEnd(), total: totalItems() }) }}</span>
21465
+ </div>
21466
+
21467
+ <div class="jv-pagination-controls">
21468
+ <div class="jv-pagination-size">
21469
+ <label class="jv-pagination-size-label">{{ t('pagination.itemsPerPage') }}</label>
21470
+ <jv-select
21471
+ [options]="pageSizeOptionsList()"
21472
+ [modelValue]="pageSizeStr()"
21473
+ (selectionChange)="onPageSizeChange($event)"
21474
+ />
21475
+ </div>
21476
+
21477
+ <div class="jv-pagination-buttons">
21478
+ <jv-button
21479
+ variant="outline"
21480
+ icon="chevrons-left"
21481
+ [disabled]="pageIndex() === 0"
21482
+ (click)="goToPage(0)"
21483
+ [attr.aria-label]="t('pagination.firstPage')"
21484
+ />
21485
+
21486
+ <jv-button
21487
+ variant="outline"
21488
+ icon="chevron-left"
21489
+ [disabled]="pageIndex() === 0"
21490
+ (click)="goToPage(pageIndex() - 1)"
21491
+ [attr.aria-label]="t('pagination.prevPage')"
21492
+ />
21493
+
21494
+ @for (p of pageRange(); track p) {
21495
+ <jv-button
21496
+ [variant]="p === pageIndex() ? 'primary' : 'outline'"
21497
+ [attr.aria-current]="p === pageIndex() ? 'page' : null"
21498
+ [attr.aria-label]="t('pagination.pageLabel', { page: p + 1 })"
21499
+ (click)="goToPage(p)"
21500
+ >{{ p + 1 }}</jv-button>
21501
+ }
21502
+
21503
+ <jv-button
21504
+ variant="outline"
21505
+ icon="chevron-right"
21506
+ [disabled]="pageIndex() >= totalPages() - 1"
21507
+ (click)="goToPage(pageIndex() + 1)"
21508
+ [attr.aria-label]="t('pagination.nextPage')"
21509
+ />
21510
+
21511
+ <jv-button
21512
+ variant="outline"
21513
+ icon="chevrons-right"
21514
+ [disabled]="pageIndex() >= totalPages() - 1"
21515
+ (click)="goToPage(totalPages() - 1)"
21516
+ [attr.aria-label]="t('pagination.lastPage')"
21517
+ />
21518
+ </div>
21519
+ </div>
21520
+ </nav>
20963
21521
  `, isInline: true, styles: [".jv-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-pagination-size{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-pagination-size-label{font-size:.875rem;color:var(--jv-color-foreground-muted);white-space:nowrap}.jv-pagination-size jv-select{width:auto;min-width:4.5rem}.jv-pagination-buttons{display:flex;align-items:center;gap:var(--jv-spacing-xs)}\n"], dependencies: [{ kind: "component", type: JvButtonComponent, selector: "jv-button", inputs: ["type", "variant", "icon", "iconPosition", "loading", "disabled"] }, { kind: "component", type: JvSelectComponent, selector: "jv-select", inputs: ["options", "placeholder", "required", "invalid", "describedBy", "inputId", "modelValue"], outputs: ["selectionChange"] }] });
20964
21522
  }
20965
21523
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JvPaginationComponent, decorators: [{
20966
21524
  type: Component,
20967
- args: [{ selector: 'jv-pagination', standalone: true, imports: [JvButtonComponent, JvSelectComponent], template: `
20968
- <nav class="jv-pagination" role="navigation" [attr.aria-label]="t('pagination.pageLabel', { page: pageIndex() + 1 })">
20969
- <div class="jv-pagination-info">
20970
- <span>{{ t('pagination.itemsShowing', { start: pageStart(), end: pageEnd(), total: totalItems() }) }}</span>
20971
- </div>
20972
-
20973
- <div class="jv-pagination-controls">
20974
- <div class="jv-pagination-size">
20975
- <label class="jv-pagination-size-label">{{ t('pagination.itemsPerPage') }}</label>
20976
- <jv-select
20977
- [options]="pageSizeOptionsList()"
20978
- [modelValue]="pageSizeStr()"
20979
- (selectionChange)="onPageSizeChange($event)"
20980
- />
20981
- </div>
20982
-
20983
- <div class="jv-pagination-buttons">
20984
- <jv-button
20985
- variant="outline"
20986
- icon="chevrons-left"
20987
- [disabled]="pageIndex() === 0"
20988
- (click)="goToPage(0)"
20989
- [attr.aria-label]="t('pagination.firstPage')"
20990
- />
20991
-
20992
- <jv-button
20993
- variant="outline"
20994
- icon="chevron-left"
20995
- [disabled]="pageIndex() === 0"
20996
- (click)="goToPage(pageIndex() - 1)"
20997
- [attr.aria-label]="t('pagination.prevPage')"
20998
- />
20999
-
21000
- @for (p of pageRange(); track p) {
21001
- <jv-button
21002
- [variant]="p === pageIndex() ? 'primary' : 'outline'"
21003
- [attr.aria-current]="p === pageIndex() ? 'page' : null"
21004
- [attr.aria-label]="t('pagination.pageLabel', { page: p + 1 })"
21005
- (click)="goToPage(p)"
21006
- >{{ p + 1 }}</jv-button>
21007
- }
21008
-
21009
- <jv-button
21010
- variant="outline"
21011
- icon="chevron-right"
21012
- [disabled]="pageIndex() >= totalPages() - 1"
21013
- (click)="goToPage(pageIndex() + 1)"
21014
- [attr.aria-label]="t('pagination.nextPage')"
21015
- />
21016
-
21017
- <jv-button
21018
- variant="outline"
21019
- icon="chevrons-right"
21020
- [disabled]="pageIndex() >= totalPages() - 1"
21021
- (click)="goToPage(totalPages() - 1)"
21022
- [attr.aria-label]="t('pagination.lastPage')"
21023
- />
21024
- </div>
21025
- </div>
21026
- </nav>
21525
+ args: [{ selector: 'jv-pagination', standalone: true, imports: [JvButtonComponent, JvSelectComponent], template: `
21526
+ <nav class="jv-pagination" role="navigation" [attr.aria-label]="t('pagination.pageLabel', { page: pageIndex() + 1 })">
21527
+ <div class="jv-pagination-info">
21528
+ <span>{{ t('pagination.itemsShowing', { start: pageStart(), end: pageEnd(), total: totalItems() }) }}</span>
21529
+ </div>
21530
+
21531
+ <div class="jv-pagination-controls">
21532
+ <div class="jv-pagination-size">
21533
+ <label class="jv-pagination-size-label">{{ t('pagination.itemsPerPage') }}</label>
21534
+ <jv-select
21535
+ [options]="pageSizeOptionsList()"
21536
+ [modelValue]="pageSizeStr()"
21537
+ (selectionChange)="onPageSizeChange($event)"
21538
+ />
21539
+ </div>
21540
+
21541
+ <div class="jv-pagination-buttons">
21542
+ <jv-button
21543
+ variant="outline"
21544
+ icon="chevrons-left"
21545
+ [disabled]="pageIndex() === 0"
21546
+ (click)="goToPage(0)"
21547
+ [attr.aria-label]="t('pagination.firstPage')"
21548
+ />
21549
+
21550
+ <jv-button
21551
+ variant="outline"
21552
+ icon="chevron-left"
21553
+ [disabled]="pageIndex() === 0"
21554
+ (click)="goToPage(pageIndex() - 1)"
21555
+ [attr.aria-label]="t('pagination.prevPage')"
21556
+ />
21557
+
21558
+ @for (p of pageRange(); track p) {
21559
+ <jv-button
21560
+ [variant]="p === pageIndex() ? 'primary' : 'outline'"
21561
+ [attr.aria-current]="p === pageIndex() ? 'page' : null"
21562
+ [attr.aria-label]="t('pagination.pageLabel', { page: p + 1 })"
21563
+ (click)="goToPage(p)"
21564
+ >{{ p + 1 }}</jv-button>
21565
+ }
21566
+
21567
+ <jv-button
21568
+ variant="outline"
21569
+ icon="chevron-right"
21570
+ [disabled]="pageIndex() >= totalPages() - 1"
21571
+ (click)="goToPage(pageIndex() + 1)"
21572
+ [attr.aria-label]="t('pagination.nextPage')"
21573
+ />
21574
+
21575
+ <jv-button
21576
+ variant="outline"
21577
+ icon="chevrons-right"
21578
+ [disabled]="pageIndex() >= totalPages() - 1"
21579
+ (click)="goToPage(totalPages() - 1)"
21580
+ [attr.aria-label]="t('pagination.lastPage')"
21581
+ />
21582
+ </div>
21583
+ </div>
21584
+ </nav>
21027
21585
  `, styles: [".jv-pagination{display:flex;align-items:center;justify-content:space-between;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-pagination-info{font-size:.875rem;color:var(--jv-color-foreground-muted)}.jv-pagination-controls{display:flex;align-items:center;gap:var(--jv-spacing-md);flex-wrap:wrap}.jv-pagination-size{display:flex;align-items:center;gap:var(--jv-spacing-sm)}.jv-pagination-size-label{font-size:.875rem;color:var(--jv-color-foreground-muted);white-space:nowrap}.jv-pagination-size jv-select{width:auto;min-width:4.5rem}.jv-pagination-buttons{display:flex;align-items:center;gap:var(--jv-spacing-xs)}\n"] }]
21028
21586
  }], propDecorators: { totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: true }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }], maxVisiblePages: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxVisiblePages", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }] } });
21029
21587