@devjuliovilla/jv-ui 1.5.3 → 1.5.4

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