@mmlogic/components 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +196 -61
  2. package/dist/cjs/format-DBr-GTvS.js +308 -0
  3. package/dist/cjs/loader.cjs.js +1 -1
  4. package/dist/cjs/mosterdcomponents.cjs.js +1 -1
  5. package/dist/cjs/mrd-boolean-field_16.cjs.entry.js +108 -117
  6. package/dist/cjs/mrd-table.cjs.entry.js +318 -62
  7. package/dist/collection/components/mrd-field/mrd-field.js +26 -2
  8. package/dist/collection/components/mrd-file-field/mrd-file-field.js +70 -2
  9. package/dist/collection/components/mrd-file-field/mrd-file-field.scss +13 -0
  10. package/dist/collection/components/mrd-form/mrd-form.js +28 -1
  11. package/dist/collection/components/mrd-image-field/mrd-image-field.js +71 -2
  12. package/dist/collection/components/mrd-image-field/mrd-image-field.scss +26 -2
  13. package/dist/collection/components/mrd-table/mrd-table.js +386 -62
  14. package/dist/collection/components/mrd-table/mrd-table.scss +388 -0
  15. package/dist/collection/dev/app.js +48 -4
  16. package/dist/collection/dev/sprites.svg +55 -0
  17. package/dist/collection/utils/i18n.js +144 -0
  18. package/dist/components/i18n.js +1 -1
  19. package/dist/components/mrd-field2.js +1 -1
  20. package/dist/components/mrd-file-field2.js +1 -1
  21. package/dist/components/mrd-form.js +1 -1
  22. package/dist/components/mrd-image-field2.js +1 -1
  23. package/dist/components/mrd-table.js +1 -1
  24. package/dist/esm/format-EzhfM0uw.js +299 -0
  25. package/dist/esm/loader.js +1 -1
  26. package/dist/esm/mosterdcomponents.js +1 -1
  27. package/dist/esm/mrd-boolean-field_16.entry.js +82 -91
  28. package/dist/esm/mrd-table.entry.js +318 -62
  29. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  30. package/dist/mosterdcomponents/p-27f6947a.entry.js +1 -0
  31. package/dist/mosterdcomponents/p-EzhfM0uw.js +1 -0
  32. package/dist/mosterdcomponents/p-ca5f9919.entry.js +1 -0
  33. package/dist/types/components/mrd-field/mrd-field.d.ts +5 -0
  34. package/dist/types/components/mrd-file-field/mrd-file-field.d.ts +10 -0
  35. package/dist/types/components/mrd-form/mrd-form.d.ts +5 -0
  36. package/dist/types/components/mrd-image-field/mrd-image-field.d.ts +10 -0
  37. package/dist/types/components/mrd-table/mrd-table.d.ts +52 -18
  38. package/dist/types/components.d.ts +53 -3
  39. package/dist/types/utils/cell-renderer.d.ts +27 -0
  40. package/package.json +1 -1
  41. package/dist/cjs/format-CDw-zie_.js +0 -82
  42. package/dist/esm/format-Dt-aHxkM.js +0 -74
  43. package/dist/mosterdcomponents/p-2a8cb2eb.entry.js +0 -1
  44. package/dist/mosterdcomponents/p-Dt-aHxkM.js +0 -1
  45. package/dist/mosterdcomponents/p-baf08615.entry.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { r as registerInstance, c as createEvent, h, H as Host, g as getElement } from './index-_tsCCkAi.js';
2
- import { f as formatTime, a as formatDateTime, b as formatDate, c as formatCurrency, d as formatNumber, e as formatPercentage } from './format-Dt-aHxkM.js';
2
+ import { f as formatTime, a as formatDateTime, b as formatDate, c as formatCurrency, d as formatNumber, e as formatPercentage, t } from './format-EzhfM0uw.js';
3
3
  import { ClientLayoutItemType } from './index.js';
4
4
 
5
5
  class CellRenderer {
@@ -63,22 +63,29 @@ class CellRenderer {
63
63
  }
64
64
  }
65
65
 
66
- const mrdTableScss = () => `.sc-mrd-table-h{display:block;width:100%}.mrd-table__scroll.sc-mrd-table{overflow-y:auto;overflow-x:auto;border:1px solid var(--mrd-border-color);border-radius:var(--mrd-border-radius);overflow-anchor:none}.mrd-table.sc-mrd-table{overflow-x:auto}.mrd-table__table.sc-mrd-table{width:auto;min-width:100%;border-collapse:collapse;font-size:var(--mrd-font-size-sm);color:var(--mrd-color-neutral-900)}.mrd-table__scroll.sc-mrd-table .mrd-table__table.sc-mrd-table{min-width:max-content}.mrd-table__header.sc-mrd-table{position:sticky;top:0;z-index:1;background:var(--mrd-color-white);text-align:left;padding:var(--mrd-space-2) var(--mrd-space-4);border-bottom:2px solid var(--mrd-border-color);color:var(--mrd-color-neutral-600);font-weight:var(--mrd-font-weight-medium);white-space:nowrap;font-size:var(--mrd-font-size-xs);text-transform:uppercase;letter-spacing:0.04em}.mrd-table__header--sortable.sc-mrd-table{cursor:pointer;user-select:none}.mrd-table__header--sortable.sc-mrd-table:hover{background:var(--mrd-color-neutral-50);color:var(--mrd-color-neutral-800)}.mrd-table__header--sorted-asc.sc-mrd-table,.mrd-table__header--sorted-desc.sc-mrd-table{color:var(--mrd-color-primary);border-bottom-color:var(--mrd-color-primary)}.mrd-table__header-label.sc-mrd-table{margin-right:var(--mrd-space-1)}.mrd-table__sort-icon.sc-mrd-table{font-size:0.65rem;opacity:0.4;vertical-align:middle}.mrd-table__header--sorted-asc.sc-mrd-table .mrd-table__sort-icon.sc-mrd-table,.mrd-table__header--sorted-desc.sc-mrd-table .mrd-table__sort-icon.sc-mrd-table{opacity:1;color:var(--mrd-color-primary)}.mrd-table__row.sc-mrd-table{border-bottom:1px solid var(--mrd-border-color)}.mrd-table__row.sc-mrd-table:hover{background:var(--mrd-color-neutral-200) !important}.mrd-table__row--clickable.sc-mrd-table{cursor:pointer}.mrd-table__spacer.sc-mrd-table{border:none}.mrd-table__spacer.sc-mrd-table td.sc-mrd-table{padding:0;border:none}.mrd-table__cell.sc-mrd-table{padding:var(--mrd-space-2) var(--mrd-space-4);vertical-align:top;white-space:nowrap}.mrd-table__cell--numeric.sc-mrd-table{text-align:right;font-variant-numeric:tabular-nums}.mrd-table__row--loading.sc-mrd-table{background:transparent}.mrd-table__cell--placeholder.sc-mrd-table{padding:var(--mrd-space-2) var(--mrd-space-4);border-bottom:1px solid var(--mrd-border-color)}.mrd-table__placeholder-bar.sc-mrd-table{display:block;height:0.75rem;width:55%;border-radius:var(--mrd-border-radius-sm);background:linear-gradient( 90deg, var(--mrd-color-neutral-200) 25%, var(--mrd-color-neutral-100) 50%, var(--mrd-color-neutral-200) 75% );background-size:200% 100%;animation:mrd-shimmer 1.4s ease infinite}@keyframes mrd-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}.mrd-table__empty.sc-mrd-table{padding:var(--mrd-space-4) var(--mrd-space-3);color:var(--mrd-color-neutral-500);font-size:var(--mrd-font-size-sm);text-align:center;margin:0}`;
66
+ const mrdTableScss = () => `.sc-mrd-table-h{display:block;width:100%}.mrd-table__scroll.sc-mrd-table{overflow-y:auto;overflow-x:auto;border:1px solid var(--mrd-border-color);border-radius:var(--mrd-border-radius);overflow-anchor:none}.mrd-table.sc-mrd-table{overflow-x:auto}.mrd-table__table.sc-mrd-table{width:auto;min-width:100%;border-collapse:collapse;font-size:var(--mrd-font-size-sm);color:var(--mrd-color-neutral-900)}.mrd-table__scroll.sc-mrd-table .mrd-table__table.sc-mrd-table{min-width:max-content}.mrd-table__header.sc-mrd-table{position:sticky;top:0;z-index:1;background:var(--mrd-color-white);text-align:left;padding:var(--mrd-space-2) var(--mrd-space-4);border-bottom:2px solid var(--mrd-border-color);color:var(--mrd-color-neutral-600);font-weight:var(--mrd-font-weight-medium);white-space:nowrap;font-size:var(--mrd-font-size-xs);text-transform:uppercase;letter-spacing:0.04em}.mrd-table__header--sortable.sc-mrd-table{cursor:pointer;user-select:none}.mrd-table__header--sortable.sc-mrd-table:hover{background:var(--mrd-color-neutral-50);color:var(--mrd-color-neutral-800)}.mrd-table__header--sorted-asc.sc-mrd-table,.mrd-table__header--sorted-desc.sc-mrd-table{color:var(--mrd-color-primary);border-bottom-color:var(--mrd-color-primary)}.mrd-table__header-label.sc-mrd-table{margin-right:var(--mrd-space-1)}.mrd-table__sort-icon.sc-mrd-table{font-size:0.65rem;opacity:0.4;vertical-align:middle}.mrd-table__header--sorted-asc.sc-mrd-table .mrd-table__sort-icon.sc-mrd-table,.mrd-table__header--sorted-desc.sc-mrd-table .mrd-table__sort-icon.sc-mrd-table{opacity:1;color:var(--mrd-color-primary)}.mrd-table__row.sc-mrd-table{border-bottom:1px solid var(--mrd-border-color)}.mrd-table__row.sc-mrd-table:hover{background:var(--mrd-color-neutral-200) !important}.mrd-table__row--clickable.sc-mrd-table{cursor:pointer}.mrd-table__spacer.sc-mrd-table{border:none}.mrd-table__spacer.sc-mrd-table td.sc-mrd-table{padding:0;border:none}.mrd-table__cell.sc-mrd-table{padding:var(--mrd-space-2) var(--mrd-space-4);vertical-align:top;white-space:nowrap}.mrd-table__cell--numeric.sc-mrd-table{text-align:right;font-variant-numeric:tabular-nums}.mrd-table__row--loading.sc-mrd-table{background:transparent}.mrd-table__cell--placeholder.sc-mrd-table{padding:var(--mrd-space-2) var(--mrd-space-4);border-bottom:1px solid var(--mrd-border-color)}.mrd-table__placeholder-bar.sc-mrd-table{display:block;height:0.75rem;width:55%;border-radius:var(--mrd-border-radius-sm);background:linear-gradient( 90deg, var(--mrd-color-neutral-200) 25%, var(--mrd-color-neutral-100) 50%, var(--mrd-color-neutral-200) 75% );background-size:200% 100%;animation:mrd-shimmer 1.4s ease infinite}@keyframes mrd-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}.mrd-table__toolbar.sc-mrd-table{display:flex;align-items:center;justify-content:space-between;padding-bottom:var(--mrd-space-2)}.mrd-table__toolbar-left.sc-mrd-table,.mrd-table__toolbar-right.sc-mrd-table{display:flex;gap:var(--mrd-space-2);align-items:center}.mrd-table__action.sc-mrd-table{position:relative;display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;background:transparent;border:1px solid transparent;border-radius:var(--mrd-border-radius);cursor:pointer;color:var(--mrd-color-neutral-400);transition:background-color 0.15s, border-color 0.15s, color 0.15s}.mrd-table__action.sc-mrd-table:hover{background-color:var(--mrd-color-neutral-100);border-color:var(--mrd-color-neutral-300);color:var(--mrd-color-neutral-700)}.mrd-table__action.sc-mrd-table:disabled{opacity:0.4;cursor:not-allowed}.mrd-table__action--primary.sc-mrd-table{color:var(--mrd-color-neutral-500)}.mrd-table__action--primary.sc-mrd-table:hover{background:var(--mrd-color-primary);border-color:var(--mrd-color-primary);color:var(--mrd-color-white)}.mrd-table__action--danger.sc-mrd-table{color:var(--mrd-color-error)}.mrd-table__action--danger.sc-mrd-table:hover{background-color:var(--mrd-color-error-light, #fef2f2);border-color:var(--mrd-color-error)}.mrd-table__action-icon.sc-mrd-table{width:1.25rem;height:1.25rem;pointer-events:none;fill:currentColor}.mrd-table__action-tooltip.sc-mrd-table{display:none;position:absolute;bottom:calc(100% + 6px);right:0;padding:var(--mrd-space-1) var(--mrd-space-2);font-size:var(--mrd-font-size-xs);white-space:nowrap;background:var(--mrd-color-tooltip, #fffce1);color:var(--mrd-color-neutral-900);border:1px solid var(--mrd-border-color);border-radius:var(--mrd-border-radius-sm, var(--mrd-border-radius));pointer-events:none;z-index:10}.mrd-table__action.sc-mrd-table:hover .mrd-table__action-tooltip.sc-mrd-table{display:block}.mrd-table__filter-toggle--active.sc-mrd-table{background:var(--mrd-color-primary);border-color:var(--mrd-color-primary);color:var(--mrd-color-white)}.mrd-table__filter-toggle--active.sc-mrd-table:hover{background:var(--mrd-color-primary-dark, var(--mrd-color-primary));border-color:var(--mrd-color-primary-dark, var(--mrd-color-primary));color:var(--mrd-color-white)}.mrd-table__filter-badge.sc-mrd-table{position:absolute;top:-6px;right:-6px;min-width:1.25rem;height:1.25rem;padding:0 3px;background:var(--mrd-color-error, #e53e3e);color:var(--mrd-color-white);border-radius:9999px;font-size:0.65rem;font-weight:var(--mrd-font-weight-medium);line-height:1.25rem;text-align:center;pointer-events:none}.mrd-table__header--filtered.sc-mrd-table{color:var(--mrd-color-primary);border-bottom-color:var(--mrd-color-primary)}.mrd-table__header-filter-btn.sc-mrd-table{display:inline-flex;align-items:center;justify-content:center;margin-left:var(--mrd-space-1);padding:0 3px;background:transparent;border:none;border-radius:3px;cursor:pointer;color:var(--mrd-color-neutral-500);font-size:0.8rem;line-height:1;vertical-align:middle}.mrd-table__header-filter-btn.sc-mrd-table:hover{background:var(--mrd-color-neutral-200);color:var(--mrd-color-neutral-800)}.mrd-table__header-filter-btn--active.sc-mrd-table{color:var(--mrd-color-primary)}.mrd-table__filter-popup.sc-mrd-table{position:fixed;width:280px;background:var(--mrd-color-white);border:1px solid var(--mrd-border-color);border-radius:var(--mrd-border-radius);box-shadow:var(--mrd-shadow-md, 0 4px 12px rgba(0,0,0,.12));z-index:var(--mrd-z-dropdown, 200);font-size:var(--mrd-font-size-sm)}.mrd-table__filter-popup-header.sc-mrd-table{display:flex;align-items:center;justify-content:space-between;padding:var(--mrd-space-2) var(--mrd-space-3);border-bottom:1px solid var(--mrd-border-color)}.mrd-table__filter-popup-title.sc-mrd-table{font-weight:var(--mrd-font-weight-medium);color:var(--mrd-color-neutral-800);font-size:var(--mrd-font-size-sm)}.mrd-table__filter-close.sc-mrd-table{background:transparent;border:none;cursor:pointer;color:var(--mrd-color-neutral-500);font-size:0.9rem;padding:2px 4px;border-radius:3px;line-height:1}.mrd-table__filter-close.sc-mrd-table:hover{background:var(--mrd-color-neutral-100);color:var(--mrd-color-neutral-800)}.mrd-table__filter-section.sc-mrd-table{padding:var(--mrd-space-2) var(--mrd-space-3)}.mrd-table__filter-section-label.sc-mrd-table{font-size:var(--mrd-font-size-xs);font-weight:var(--mrd-font-weight-medium);text-transform:uppercase;letter-spacing:0.04em;color:var(--mrd-color-neutral-500);margin-bottom:var(--mrd-space-2)}.mrd-table__filter-sort-buttons.sc-mrd-table{display:flex;gap:var(--mrd-space-2)}.mrd-table__filter-sort-btn.sc-mrd-table{flex:1;padding:var(--mrd-space-1) var(--mrd-space-2);background:transparent;border:1px solid var(--mrd-border-color);border-radius:var(--mrd-border-radius);cursor:pointer;font-size:var(--mrd-font-size-xs);color:var(--mrd-color-neutral-700)}.mrd-table__filter-sort-btn.sc-mrd-table:hover{background:var(--mrd-color-neutral-100)}.mrd-table__filter-sort-btn--active.sc-mrd-table{background:var(--mrd-color-primary);border-color:var(--mrd-color-primary);color:var(--mrd-color-white)}.mrd-table__filter-divider.sc-mrd-table{height:1px;background:var(--mrd-border-color);margin:0}.mrd-table__filter-editor.sc-mrd-table{display:flex;flex-direction:column;gap:var(--mrd-space-2)}.mrd-table__filter-select.sc-mrd-table,.mrd-table__filter-input.sc-mrd-table{width:100%;padding:var(--mrd-space-1) var(--mrd-space-2);border:1px solid var(--mrd-border-color);border-radius:var(--mrd-border-radius);font-size:var(--mrd-font-size-sm);color:var(--mrd-color-neutral-900);background:var(--mrd-color-white);box-sizing:border-box}.mrd-table__filter-select.sc-mrd-table:focus,.mrd-table__filter-input.sc-mrd-table:focus{outline:none;border-color:var(--mrd-color-primary);box-shadow:0 0 0 2px rgba(0,0,0,.06)}.mrd-table__filter-range.sc-mrd-table{display:flex;align-items:center;gap:var(--mrd-space-1)}.mrd-table__filter-range.sc-mrd-table .mrd-table__filter-input.sc-mrd-table{flex:1;min-width:0}.mrd-table__filter-range-sep.sc-mrd-table{color:var(--mrd-color-neutral-400);flex-shrink:0}.mrd-table__filter-radio-group.sc-mrd-table{display:flex;flex-direction:column;gap:var(--mrd-space-1)}.mrd-table__filter-radio-group--inline.sc-mrd-table{flex-direction:row;gap:var(--mrd-space-3)}.mrd-table__filter-radio-label.sc-mrd-table{display:flex;align-items:center;gap:var(--mrd-space-1);cursor:pointer;font-size:var(--mrd-font-size-sm);color:var(--mrd-color-neutral-800)}.mrd-table__filter-list.sc-mrd-table{display:flex;flex-direction:column;gap:var(--mrd-space-1);max-height:180px;overflow-y:auto}.mrd-table__filter-list-controls.sc-mrd-table{display:flex;gap:var(--mrd-space-2);margin-bottom:var(--mrd-space-1)}.mrd-table__filter-list-btn.sc-mrd-table{font-size:var(--mrd-font-size-xs);color:var(--mrd-color-primary);background:transparent;border:none;cursor:pointer;padding:0;text-decoration:underline}.mrd-table__filter-checkbox-label.sc-mrd-table{display:flex;align-items:center;gap:var(--mrd-space-1);cursor:pointer;font-size:var(--mrd-font-size-sm);color:var(--mrd-color-neutral-800)}.mrd-table__filter-no-support.sc-mrd-table{font-size:var(--mrd-font-size-sm);color:var(--mrd-color-neutral-500);margin:0;font-style:italic}.mrd-table__filter-popup-footer.sc-mrd-table{display:flex;justify-content:flex-end;gap:var(--mrd-space-2);padding:var(--mrd-space-2) var(--mrd-space-3);border-top:1px solid var(--mrd-border-color)}.mrd-table__filter-btn.sc-mrd-table{padding:var(--mrd-space-1) var(--mrd-space-3);border-radius:var(--mrd-border-radius);border:1px solid var(--mrd-border-color);font-size:var(--mrd-font-size-sm);cursor:pointer}.mrd-table__filter-btn--clear.sc-mrd-table{background:transparent;color:var(--mrd-color-neutral-600)}.mrd-table__filter-btn--clear.sc-mrd-table:hover{background:var(--mrd-color-neutral-100)}.mrd-table__filter-btn--apply.sc-mrd-table{background:var(--mrd-color-primary);border-color:var(--mrd-color-primary);color:var(--mrd-color-white)}.mrd-table__filter-btn--apply.sc-mrd-table:hover{background:var(--mrd-color-primary-dark, var(--mrd-color-primary));border-color:var(--mrd-color-primary-dark, var(--mrd-color-primary))}.mrd-table__footer.sc-mrd-table{padding:var(--mrd-space-1) var(--mrd-space-2);font-size:var(--mrd-font-size-xs);color:var(--mrd-color-neutral-500);text-align:right}.mrd-table__empty.sc-mrd-table{padding:var(--mrd-space-4) var(--mrd-space-3);color:var(--mrd-color-neutral-500);font-size:var(--mrd-font-size-sm);text-align:center;margin:0}`;
67
67
 
68
68
  const BUFFER = 10;
69
69
  /** Wacht deze tijd (ms) na het laatste scroll-event voordat pagina's worden
70
70
  * aangevraagd. Pagina's die de gebruiker snel voorbij scrollt worden zo geskipt. */
71
71
  const REQUEST_DEBOUNCE_MS = 150;
72
+ /** Breedte van de filterpopup in px — voor overflow-correctie. */
73
+ const POPUP_WIDTH = 280;
74
+ const TEXT_TYPES = new Set(['TEXT', 'TEXTBLOCK', 'EMAIL', 'HYPERLINK']);
75
+ const NUMERIC_TYPES = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
76
+ const DATE_TYPES = new Set(['DATE', 'DATETIME', 'TIME']);
77
+ const NO_FILTER_TYPES = new Set(['FILE', 'IMAGE']);
72
78
  const MrdTable = class {
73
79
  constructor(hostRef) {
74
80
  registerInstance(this, hostRef);
75
81
  this.mrdLoadPage = createEvent(this, "mrdLoadPage");
76
82
  this.mrdRowClick = createEvent(this, "mrdRowClick");
77
- // ── Debounce internals (geen @State — triggert geen re-render) ─────────────
78
- /** Pagina's die wachten op debounce-flush. */
83
+ this.mrdAction = createEvent(this, "mrdAction");
84
+ this.mrdFilter = createEvent(this, "mrdFilter");
85
+ // ── Non-state internals ────────────────────────────────────────────────────
79
86
  this.pendingPages = new Set();
80
- /** Handle van de actieve debounce-timer. */
81
87
  this.debounceTimer = null;
88
+ this.outsideClickHandler = null;
82
89
  // ── Props ──────────────────────────────────────────────────────────────────
83
90
  this.columns = [];
84
91
  /** Direct rows (non-paginated mode, used when totalElements === 0). */
@@ -95,32 +102,45 @@ const MrdTable = class {
95
102
  /** Initial sort applied on load, e.g. "timestamp,desc" or "name".
96
103
  * Parsed by init() into sortField + sortDir. */
97
104
  this.defaultSort = '';
105
+ /** Toolbar action buttons rendered above the table. */
106
+ this.actions = [];
98
107
  // ── Internal state ─────────────────────────────────────────────────────────
99
- /** Pages injected via setPage(). Always replaced by a new Map to trigger re-render. */
100
108
  this.loadedPages = new Map();
101
- /** Pages already requested via mrdLoadPage (to avoid duplicate events). */
102
109
  this.requestedPages = new Set();
103
- /** Absolute index of the first row currently in the render window. */
104
110
  this.renderStart = 0;
105
- /** Absolute index of the last row currently in the render window. */
106
111
  this.renderEnd = 0;
107
- /** Locked column widths (px) — measured after first page renders, then fixed. */
108
112
  this.colWidths = [];
109
- /** Column currently used for sorting (empty = no sort). */
110
113
  this.sortField = '';
111
- /** Sort direction for sortField. */
112
114
  this.sortDir = 'asc';
115
+ /** Whether the filter UI is visible on column headers. */
116
+ this.filterMode = false;
117
+ /** Active filters keyed by field name. */
118
+ this.activeFilters = new Map();
119
+ /** Field name of the currently open filter popup (null = closed). */
120
+ this.openFilterCol = null;
121
+ /** Filter state being edited in the open popup. */
122
+ this.pendingFilter = null;
123
+ /** Viewport-relative position for the filter popup. */
124
+ this.popupPos = { top: 0, left: 0 };
125
+ /** Current scroll offset of the scroll container — drives pagination footer. */
126
+ this.scrollTop = 0;
113
127
  this.handleScroll = (e) => {
114
128
  const scroller = e.currentTarget;
115
129
  const scrollTop = scroller.scrollTop;
116
130
  const total = this.totalElements;
117
131
  const visStart = Math.floor(scrollTop / this.rowHeight);
118
132
  const visEnd = Math.min(visStart + this.visibleCount(), total - 1);
133
+ this.scrollTop = scrollTop;
119
134
  this.renderStart = Math.max(0, visStart - BUFFER);
120
135
  this.renderEnd = Math.min(total - 1, visEnd + BUFFER);
121
136
  this.requestPagesForWindow(this.renderStart, this.renderEnd);
122
137
  };
123
138
  }
139
+ // ── Prop watchers ─────────────────────────────────────────────────────────
140
+ /** Clamp renderEnd when totalElements shrinks (e.g. after a filter is applied). */
141
+ totalElementsChanged(newVal) {
142
+ this.renderEnd = Math.min(this.renderEnd, Math.max(0, newVal - 1));
143
+ }
124
144
  // ── Public API ─────────────────────────────────────────────────────────────
125
145
  /**
126
146
  * Initialise (or reset) the virtual scroll.
@@ -137,7 +157,6 @@ const MrdTable = class {
137
157
  this.loadedPages = new Map();
138
158
  this.requestedPages = new Set();
139
159
  this.colWidths = [];
140
- // Apply defaultSort prop as the initial sort state.
141
160
  if (this.defaultSort) {
142
161
  const parts = this.defaultSort.split(',');
143
162
  this.sortField = parts[0].trim();
@@ -147,66 +166,105 @@ const MrdTable = class {
147
166
  this.sortField = '';
148
167
  this.sortDir = 'asc';
149
168
  }
169
+ this.scrollTop = 0;
150
170
  this.renderStart = 0;
151
- this.renderEnd = Math.max(0, Math.min(this.visibleCount() + BUFFER, this.totalElements - 1));
152
- // Scroll the container back to the top when switching datasets.
171
+ // No BUFFER on init only request what fits the visible area (page 0).
172
+ // BUFFER is applied during scroll to pre-fetch the next page proactively.
173
+ this.renderEnd = Math.max(0, Math.min(this.visibleCount() - 1, this.totalElements - 1));
153
174
  const scroller = this.el.querySelector('.mrd-table__scroll');
154
175
  if (scroller)
155
176
  scroller.scrollTop = 0;
156
- // Do NOT emit mrdLoadPage here — the host injects page 0 via setPage().
157
177
  }
158
178
  /**
159
179
  * Inject the rows for a given page (0-based).
160
180
  * Creates a new Map reference so Stencil detects the state change.
181
+ *
182
+ * When the page contains fewer rows than pageSize it is the last page.
183
+ * renderEnd is clamped immediately so no loading-placeholder rows appear
184
+ * beyond the actual data — without requiring the host to update totalElements.
161
185
  */
162
186
  async setPage(pageNumber, rows) {
187
+ if (rows.length < this.pageSize) {
188
+ // lastRowIdx is -1 when the page is empty; clamp renderEnd to -1 so the
189
+ // render loop does not execute and no shimmer rows appear.
190
+ const lastRowIdx = pageNumber * this.pageSize + rows.length - 1;
191
+ this.renderEnd = Math.min(this.renderEnd, lastRowIdx);
192
+ }
163
193
  const next = new Map(this.loadedPages);
164
194
  next.set(pageNumber, rows);
165
195
  this.loadedPages = next;
166
196
  }
167
- // ── Private helpers ────────────────────────────────────────────────────────
197
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
198
+ disconnectedCallback() {
199
+ if (this.outsideClickHandler) {
200
+ document.removeEventListener('click', this.outsideClickHandler);
201
+ this.outsideClickHandler = null;
202
+ }
203
+ }
204
+ componentDidRender() {
205
+ if (this.colWidths.length === 0 && this.loadedPages.size > 0 && this.totalElements > 0) {
206
+ const ths = this.el.querySelectorAll('.mrd-table__header');
207
+ if (ths.length > 0) {
208
+ this.colWidths = Array.from(ths).map(th => th.offsetWidth);
209
+ }
210
+ }
211
+ }
212
+ // ── Paging / scroll helpers ────────────────────────────────────────────────
168
213
  visibleCount() {
169
214
  return Math.ceil(this.tableHeight / this.rowHeight);
170
215
  }
171
- /** Returns the current sort value for use in ?sort= query params. */
172
216
  sortParam() {
173
217
  if (!this.sortField)
174
218
  return '';
175
219
  return this.sortDir === 'desc' ? `${this.sortField},desc` : this.sortField;
176
220
  }
177
- /** Called when a header cell is clicked. Toggles direction or sets a new column. */
178
221
  colName(col) {
179
222
  var _a, _b, _c, _d;
180
223
  return (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '';
181
224
  }
182
- handleSortClick(col) {
183
- const name = this.colName(col);
184
- if (this.sortField === name) {
185
- this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
186
- }
187
- else {
188
- this.sortField = name;
189
- this.sortDir = 'asc';
190
- }
191
- // Cancel any pending scroll debounce.
225
+ colDataType(col) {
226
+ var _a, _b;
227
+ if (col.type === 'RELATION')
228
+ return 'RELATION';
229
+ return (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.dataType) !== null && _b !== void 0 ? _b : 'TEXT';
230
+ }
231
+ /** Reset pagination state and scroll to top (used after sort or filter change). */
232
+ resetPages() {
192
233
  if (this.debounceTimer !== null) {
193
234
  clearTimeout(this.debounceTimer);
194
235
  this.debounceTimer = null;
195
236
  }
196
237
  this.pendingPages.clear();
197
- // Wipe all loaded data so the new sort order is fetched fresh.
198
238
  this.loadedPages = new Map();
199
239
  this.requestedPages = new Set();
200
240
  this.colWidths = [];
241
+ this.scrollTop = 0;
201
242
  this.renderStart = 0;
202
- this.renderEnd = Math.max(0, Math.min(this.visibleCount() + BUFFER, this.totalElements - 1));
243
+ // No BUFFER here totalElements may be stale after a filter change.
244
+ // Only request what is visible; BUFFER kicks in during scroll as usual.
245
+ this.renderEnd = Math.max(0, Math.min(this.visibleCount() - 1, this.totalElements - 1));
203
246
  const scroller = this.el.querySelector('.mrd-table__scroll');
204
247
  if (scroller)
205
248
  scroller.scrollTop = 0;
206
- // Emit immediately — no debounce for intentional sort clicks.
249
+ }
250
+ handleSortClick(col) {
251
+ const name = this.colName(col);
252
+ if (this.sortField === name) {
253
+ this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
254
+ }
255
+ else {
256
+ this.sortField = name;
257
+ this.sortDir = 'asc';
258
+ }
259
+ this.resetPages();
260
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
261
+ }
262
+ applySort(col, dir) {
263
+ this.sortField = this.colName(col);
264
+ this.sortDir = dir;
265
+ this.resetPages();
207
266
  this.emitPagesForWindow(this.renderStart, this.renderEnd);
208
267
  }
209
- /** Emits mrdLoadPage immediately for all missing pages in [start, end]. */
210
268
  emitPagesForWindow(start, end) {
211
269
  const firstPage = Math.floor(start / this.pageSize);
212
270
  const lastPage = Math.floor(end / this.pageSize);
@@ -239,13 +297,10 @@ const MrdTable = class {
239
297
  }
240
298
  if (!anyNew)
241
299
  return;
242
- // Reset de timer: wacht tot het scrollen even stopt.
243
300
  if (this.debounceTimer !== null)
244
301
  clearTimeout(this.debounceTimer);
245
302
  this.debounceTimer = setTimeout(() => this.flushPendingPages(), REQUEST_DEBOUNCE_MS);
246
303
  }
247
- /** Emitteert mrdLoadPage alleen voor pagina's die na de debounce-wachttijd
248
- * nog steeds binnen het huidige render-venster vallen. */
249
304
  flushPendingPages() {
250
305
  this.debounceTimer = null;
251
306
  if (this.pendingPages.size === 0)
@@ -253,10 +308,8 @@ const MrdTable = class {
253
308
  const next = new Set(this.requestedPages);
254
309
  let changed = false;
255
310
  for (const page of this.pendingPages) {
256
- // Sla over als pagina inmiddels geladen of al aangevraagd is.
257
311
  if (this.loadedPages.has(page) || next.has(page))
258
312
  continue;
259
- // Sla over als de pagina buiten het huidige venster is geraakt.
260
313
  const pageStart = page * this.pageSize;
261
314
  const pageEnd = pageStart + this.pageSize - 1;
262
315
  if (pageEnd < this.renderStart || pageStart > this.renderEnd)
@@ -269,42 +322,236 @@ const MrdTable = class {
269
322
  if (changed)
270
323
  this.requestedPages = next;
271
324
  }
272
- // ── Lifecycle ──────────────────────────────────────────────────────────────
273
- /** After the first page of data renders, lock column widths so subsequent
274
- * page loads don't cause layout shifts. */
275
- componentDidRender() {
276
- if (this.colWidths.length === 0 && this.loadedPages.size > 0 && this.totalElements > 0) {
277
- const ths = this.el.querySelectorAll('.mrd-table__header');
278
- if (ths.length > 0) {
279
- this.colWidths = Array.from(ths).map(th => th.offsetWidth);
280
- }
325
+ // ── Filter helpers ─────────────────────────────────────────────────────────
326
+ handleFilterToggle() {
327
+ this.filterMode = !this.filterMode;
328
+ if (!this.filterMode)
329
+ this.closeFilterPopup();
330
+ }
331
+ handleFilterOpen(col, e) {
332
+ e.stopPropagation();
333
+ const btn = e.currentTarget;
334
+ const rect = btn.getBoundingClientRect();
335
+ let left = rect.left;
336
+ if (left + POPUP_WIDTH > window.innerWidth - 8)
337
+ left = rect.right - POPUP_WIDTH;
338
+ this.popupPos = { top: rect.bottom + 4, left: Math.max(8, left) };
339
+ const name = this.colName(col);
340
+ const dataType = this.colDataType(col);
341
+ const existing = this.activeFilters.get(name);
342
+ this.pendingFilter = existing ? Object.assign({}, existing) : { field: name, dataType };
343
+ this.openFilterCol = name;
344
+ // Close on outside click — re-register to replace any stale handler
345
+ if (this.outsideClickHandler)
346
+ document.removeEventListener('click', this.outsideClickHandler);
347
+ this.outsideClickHandler = (ev) => {
348
+ const popup = this.el.querySelector('.mrd-table__filter-popup');
349
+ if (popup && !popup.contains(ev.target))
350
+ this.closeFilterPopup();
351
+ };
352
+ document.addEventListener('click', this.outsideClickHandler);
353
+ }
354
+ closeFilterPopup() {
355
+ this.openFilterCol = null;
356
+ this.pendingFilter = null;
357
+ if (this.outsideClickHandler) {
358
+ document.removeEventListener('click', this.outsideClickHandler);
359
+ this.outsideClickHandler = null;
360
+ }
361
+ }
362
+ setPending(key, val) {
363
+ this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { [key]: val });
364
+ }
365
+ togglePendingValue(key, checked) {
366
+ var _a, _b;
367
+ const current = (_b = (_a = this.pendingFilter) === null || _a === void 0 ? void 0 : _a.values) !== null && _b !== void 0 ? _b : [];
368
+ this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { values: checked ? [...current, key] : current.filter(k => k !== key) });
369
+ }
370
+ filterHasValue(f) {
371
+ if (f.operator === 'isEmpty' || f.operator === 'isNotEmpty')
372
+ return true;
373
+ if (f.values !== undefined && f.values.length > 0)
374
+ return true;
375
+ if (f.value != null && f.value !== '')
376
+ return true;
377
+ if (typeof f.value === 'boolean')
378
+ return true;
379
+ if (f.from != null && f.from !== '')
380
+ return true;
381
+ if (f.to != null && f.to !== '')
382
+ return true;
383
+ return false;
384
+ }
385
+ applyFilter() {
386
+ const f = this.pendingFilter;
387
+ if (!(f === null || f === void 0 ? void 0 : f.field)) {
388
+ this.closeFilterPopup();
389
+ return;
390
+ }
391
+ const next = new Map(this.activeFilters);
392
+ if (this.filterHasValue(f)) {
393
+ next.set(f.field, f);
394
+ }
395
+ else {
396
+ next.delete(f.field);
397
+ }
398
+ this.activeFilters = next;
399
+ this.closeFilterPopup();
400
+ this.mrdFilter.emit({ filters: Array.from(this.activeFilters.values()) });
401
+ if (this.totalElements > 0) {
402
+ this.resetPages();
403
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
404
+ }
405
+ }
406
+ clearFilter() {
407
+ const name = this.openFilterCol;
408
+ const next = new Map(this.activeFilters);
409
+ if (name)
410
+ next.delete(name);
411
+ this.activeFilters = next;
412
+ this.closeFilterPopup();
413
+ this.mrdFilter.emit({ filters: Array.from(this.activeFilters.values()) });
414
+ if (this.totalElements > 0) {
415
+ this.resetPages();
416
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
417
+ }
418
+ }
419
+ clearAllFilters() {
420
+ this.activeFilters = new Map();
421
+ this.mrdFilter.emit({ filters: [] });
422
+ if (this.totalElements > 0) {
423
+ this.resetPages();
424
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
425
+ }
426
+ }
427
+ // ── Render: toolbar ────────────────────────────────────────────────────────
428
+ renderToolbar() {
429
+ var _a;
430
+ const filterCount = this.activeFilters.size;
431
+ const hasActions = ((_a = this.actions) === null || _a === void 0 ? void 0 : _a.length) > 0;
432
+ return (h("div", { class: "mrd-table__toolbar" }, h("div", { class: "mrd-table__toolbar-left" }, h("button", { class: `mrd-table__action mrd-table__action--secondary mrd-table__filter-toggle${this.filterMode ? ' mrd-table__filter-toggle--active' : ''}`, onClick: () => this.handleFilterToggle() }, h("svg", { class: "mrd-table__action-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" })), filterCount > 0 && h("span", { class: "mrd-table__filter-badge" }, filterCount), h("span", { class: "mrd-table__action-tooltip" }, this.filterMode ? t('table_filter_hide', this.locale) : t('table_filter', this.locale), filterCount > 0 ? ` (${filterCount} ${t('table_filter_active', this.locale)})` : '')), filterCount > 0 && (h("button", { class: "mrd-table__action mrd-table__action--secondary", onClick: () => this.clearAllFilters() }, h("svg", { class: "mrd-table__action-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" })), h("span", { class: "mrd-table__action-tooltip" }, t('table_filter_clear_all', this.locale))))), hasActions && (h("div", { class: "mrd-table__toolbar-right" }, this.actions.map(a => {
433
+ var _a;
434
+ return (h("button", { class: `mrd-table__action mrd-table__action--${(_a = a.variant) !== null && _a !== void 0 ? _a : 'secondary'}`, disabled: a.disabled, onClick: () => this.mrdAction.emit({ action: a.action }) }, a.icon
435
+ ? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
436
+ : a.label, h("span", { class: "mrd-table__action-tooltip" }, a.label)));
437
+ })))));
438
+ }
439
+ // ── Render: filter popup ───────────────────────────────────────────────────
440
+ renderFilterEditor(col) {
441
+ var _a, _b, _c, _d, _e, _f, _g;
442
+ const pf = (_a = this.pendingFilter) !== null && _a !== void 0 ? _a : {};
443
+ const dataType = this.colDataType(col);
444
+ if (NO_FILTER_TYPES.has(dataType)) {
445
+ return h("p", { class: "mrd-table__filter-no-support" }, t('filter_no_support', this.locale));
446
+ }
447
+ if (dataType === 'BOOLEAN') {
448
+ return (h("div", { class: "mrd-table__filter-radio-group" }, [
449
+ { labelKey: 'filter_all', value: null },
450
+ { labelKey: 'yes', value: true },
451
+ { labelKey: 'no', value: false },
452
+ ].map(opt => (h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `bf-${this.openFilterCol}`, checked: pf.value === opt.value, onChange: () => this.setPending('value', opt.value) }), t(opt.labelKey, this.locale))))));
281
453
  }
454
+ if (dataType === 'LIST') {
455
+ const items = (_c = (_b = col.field) === null || _b === void 0 ? void 0 : _b.listItems) !== null && _c !== void 0 ? _c : [];
456
+ const selected = (_d = pf.values) !== null && _d !== void 0 ? _d : [];
457
+ return (h("div", { class: "mrd-table__filter-list" }, h("div", { class: "mrd-table__filter-list-controls" }, h("button", { class: "mrd-table__filter-list-btn", onClick: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { values: items.map(i => i.key) }); } }, t('filter_select_all', this.locale)), h("button", { class: "mrd-table__filter-list-btn", onClick: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { values: [] }); } }, t('filter_select_none', this.locale))), items.map(item => (h("label", { class: "mrd-table__filter-checkbox-label" }, h("input", { type: "checkbox", checked: selected.includes(item.key), onChange: (e) => this.togglePendingValue(item.key, e.target.checked) }), item.label)))));
458
+ }
459
+ if (TEXT_TYPES.has(dataType) || dataType === 'RELATION') {
460
+ const op = (_e = pf.operator) !== null && _e !== void 0 ? _e : 'startsWith';
461
+ const noInput = op === 'isEmpty' || op === 'isNotEmpty';
462
+ return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => this.setPending('operator', e.target.value) }, [
463
+ { val: 'startsWith', labelKey: 'filter_starts_with' },
464
+ { val: 'equals', labelKey: 'filter_equals' },
465
+ { val: 'isEmpty', labelKey: 'filter_is_empty' },
466
+ { val: 'isNotEmpty', labelKey: 'filter_is_not_empty' },
467
+ ].map(o => h("option", { value: o.val, selected: op === o.val }, t(o.labelKey, this.locale)))), !noInput && (h("input", { type: "text", class: "mrd-table__filter-input", value: String((_f = pf.value) !== null && _f !== void 0 ? _f : ''), placeholder: t('filter_search_value', this.locale), onInput: (e) => this.setPending('value', e.target.value) }))));
468
+ }
469
+ if (NUMERIC_TYPES.has(dataType)) {
470
+ const rangeMode = pf.from !== undefined || pf.to !== undefined;
471
+ return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "number", class: "mrd-table__filter-input", value: pf.value != null ? String(pf.value) : '', onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("span", { class: "mrd-table__filter-range-sep" }, "\u2013"), h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
472
+ }
473
+ if (DATE_TYPES.has(dataType)) {
474
+ const inputType = dataType === 'DATE' ? 'date'
475
+ : dataType === 'DATETIME' ? 'datetime-local'
476
+ : 'time';
477
+ const rangeMode = pf.from !== undefined || pf.to !== undefined;
478
+ return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: inputType, class: "mrd-table__filter-input", value: String((_g = pf.value) !== null && _g !== void 0 ? _g : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
479
+ }
480
+ return null;
481
+ }
482
+ renderFilterPopup() {
483
+ var _a, _b, _c, _d;
484
+ if (!this.openFilterCol || !this.pendingFilter)
485
+ return null;
486
+ const col = this.columns.find(c => this.colName(c) === this.openFilterCol);
487
+ if (!col)
488
+ return null;
489
+ const label = (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : this.openFilterCol;
490
+ const sortActive = this.sortField === this.openFilterCol;
491
+ return (h("div", { class: "mrd-table__filter-popup", style: { top: `${this.popupPos.top}px`, left: `${this.popupPos.left}px` }, onClick: (e) => e.stopPropagation() }, h("div", { class: "mrd-table__filter-popup-header" }, h("span", { class: "mrd-table__filter-popup-title" }, label), h("button", { class: "mrd-table__filter-close", onClick: () => this.closeFilterPopup() }, "\u2715")), h("div", { class: "mrd-table__filter-section" }, h("div", { class: "mrd-table__filter-section-label" }, t('filter_sorting', this.locale)), h("div", { class: "mrd-table__filter-sort-buttons" }, h("button", { class: `mrd-table__filter-sort-btn${sortActive && this.sortDir === 'asc' ? ' mrd-table__filter-sort-btn--active' : ''}`, onClick: () => this.applySort(col, 'asc') }, "\u25B2 ", t('filter_ascending', this.locale)), h("button", { class: `mrd-table__filter-sort-btn${sortActive && this.sortDir === 'desc' ? ' mrd-table__filter-sort-btn--active' : ''}`, onClick: () => this.applySort(col, 'desc') }, "\u25BC ", t('filter_descending', this.locale)))), h("div", { class: "mrd-table__filter-divider" }), h("div", { class: "mrd-table__filter-section" }, h("div", { class: "mrd-table__filter-section-label" }, t('filter_section', this.locale)), this.renderFilterEditor(col)), h("div", { class: "mrd-table__filter-popup-footer" }, h("button", { class: "mrd-table__filter-btn mrd-table__filter-btn--clear", onClick: () => this.clearFilter() }, t('filter_clear', this.locale)), h("button", { class: "mrd-table__filter-btn mrd-table__filter-btn--apply", onClick: () => this.applyFilter() }, t('filter_apply', this.locale)))));
492
+ }
493
+ // ── Render: footer ────────────────────────────────────────────────────────
494
+ renderFooter(rowCount, effectiveTotal) {
495
+ const total = this.totalElements;
496
+ // Non-paginated mode: show plain row count
497
+ if (total === 0) {
498
+ const count = rowCount !== null && rowCount !== void 0 ? rowCount : 0;
499
+ if (count === 0)
500
+ return null;
501
+ return (h("div", { class: "mrd-table__footer" }, count, " ", t('table_of', this.locale), " ", count));
502
+ }
503
+ // Paginated mode: only show once page 0 has loaded (avoids stale total during filter reset)
504
+ if (!this.loadedPages.has(0))
505
+ return null;
506
+ // Use effectiveTotal (derived from actual page lengths) so the counter
507
+ // is correct even when the host has not yet updated totalElements.
508
+ const displayTotal = effectiveTotal !== null && effectiveTotal !== void 0 ? effectiveTotal : total;
509
+ // Compute from/to independently so partial rows at top/bottom are included.
510
+ const from = Math.min(Math.floor(this.scrollTop / this.rowHeight) + 1, displayTotal);
511
+ const to = Math.min(Math.ceil((this.scrollTop + this.tableHeight) / this.rowHeight), displayTotal);
512
+ return (h("div", { class: "mrd-table__footer" }, from, "\u2013", to, " ", t('table_of', this.locale), " ", displayTotal));
282
513
  }
283
514
  // ── Render ─────────────────────────────────────────────────────────────────
284
515
  render() {
285
- var _a, _b;
516
+ var _a, _b, _c;
286
517
  if (!((_a = this.columns) === null || _a === void 0 ? void 0 : _a.length))
287
518
  return null;
288
519
  const numericTypes = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
289
520
  // ── Non-paginated mode ──────────────────────────────────────────────────
290
521
  if (this.totalElements === 0) {
291
- return (h(Host, null, h("div", { class: "mrd-table" }, h("table", { class: "mrd-table__table" }, h("thead", null, h("tr", null, this.columns.map(col => {
522
+ return (h(Host, null, this.renderToolbar(), h("div", { class: "mrd-table" }, h("table", { class: "mrd-table__table" }, h("thead", null, h("tr", null, this.columns.map(col => {
292
523
  var _a, _b, _c, _d;
293
- return (h("th", { class: "mrd-table__header" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''));
524
+ const name = this.colName(col);
525
+ const isFiltered = this.activeFilters.has(name);
526
+ return (h("th", { class: `mrd-table__header${isFiltered ? ' mrd-table__header--filtered' : ''}` }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), this.filterMode && (h("button", { class: `mrd-table__header-filter-btn${isFiltered ? ' mrd-table__header-filter-btn--active' : ''}`, onClick: (e) => this.handleFilterOpen(col, e) }, "\u25BE"))));
294
527
  }))), h("tbody", null, (_b = this.rows) === null || _b === void 0 ? void 0 : _b.map((row, i) => (h("tr", { class: "mrd-table__row mrd-table__row--clickable", style: { background: i % 2 === 0 ? '' : 'var(--mrd-color-neutral-100)' }, onClick: () => this.mrdRowClick.emit(row) }, this.columns.map(col => {
295
528
  var _a, _b;
296
529
  const value = CellRenderer.render(col, row, this.locale);
297
530
  const isNumeric = col.type === 'FIELD' && numericTypes.has((_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.dataType) !== null && _b !== void 0 ? _b : '');
298
531
  return (h("td", { class: `mrd-table__cell${isNumeric ? ' mrd-table__cell--numeric' : ''}` }, value));
299
- })))))), (!this.rows || this.rows.length === 0) && (h("p", { class: "mrd-table__empty" }, "Geen resultaten gevonden.")))));
532
+ })))))), (!this.rows || this.rows.length === 0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale)))), this.renderFooter((_c = this.rows) === null || _c === void 0 ? void 0 : _c.length), this.renderFilterPopup()));
300
533
  }
301
534
  // ── Paginated / virtual-scroll mode ────────────────────────────────────
302
- const total = this.totalElements;
535
+ // Derive the authoritative row count from loaded pages:
536
+ // if any loaded page is shorter than pageSize it is the last page,
537
+ // so the true total cannot exceed (pageNum * pageSize + pageRows.length).
538
+ // This self-corrects without requiring the host to update totalElements.
539
+ let effectiveTotal = this.totalElements;
540
+ for (const [pageNum, pageRows] of this.loadedPages) {
541
+ if (pageRows.length < this.pageSize) {
542
+ effectiveTotal = Math.min(effectiveTotal, pageNum * this.pageSize + pageRows.length);
543
+ }
544
+ }
545
+ // Clamp renderEnd to what we actually know exists (-1 when empty)
546
+ const clampedEnd = Math.min(this.renderEnd, effectiveTotal - 1);
303
547
  const colCount = this.columns.length;
304
548
  const topSpacerHeight = this.renderStart * this.rowHeight;
305
- const bottomSpacerHeight = Math.max(0, (total - 1 - this.renderEnd) * this.rowHeight);
549
+ const bottomSpacerHeight = Math.max(0, (effectiveTotal - 1 - clampedEnd) * this.rowHeight);
550
+ const tableStyle = this.colWidths.length > 0
551
+ ? { tableLayout: 'fixed' }
552
+ : undefined;
306
553
  const renderedRows = [];
307
- for (let i = this.renderStart; i <= this.renderEnd; i++) {
554
+ for (let i = this.renderStart; i <= clampedEnd; i++) {
308
555
  const row = this.getRow(i);
309
556
  if (row === null) {
310
557
  renderedRows.push(h("tr", { class: "mrd-table__row mrd-table__row--loading" }, h("td", { class: "mrd-table__cell--placeholder", colSpan: colCount }, h("span", { class: "mrd-table__placeholder-bar" }))));
@@ -318,17 +565,26 @@ const MrdTable = class {
318
565
  })));
319
566
  }
320
567
  }
321
- const tableStyle = this.colWidths.length > 0
322
- ? { tableLayout: 'fixed' }
323
- : undefined;
324
- return (h(Host, null, h("div", { class: "mrd-table__scroll", style: { height: `${this.tableHeight}px` }, onScroll: this.handleScroll }, h("table", { class: "mrd-table__table", style: tableStyle }, h("thead", null, h("tr", null, this.columns.map((col, idx) => {
568
+ return (h(Host, null, this.renderToolbar(), h("div", { class: "mrd-table__scroll", style: { height: `${this.tableHeight}px` }, onScroll: this.handleScroll }, h("table", { class: "mrd-table__table", style: tableStyle }, h("thead", null, h("tr", null, this.columns.map((col, idx) => {
325
569
  var _a, _b, _c, _d;
326
- const isActive = this.sortField === this.colName(col);
327
- const cls = `mrd-table__header mrd-table__header--sortable${isActive ? ` mrd-table__header--sorted-${this.sortDir}` : ''}`;
328
- return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: () => this.handleSortClick(col) }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, isActive ? (this.sortDir === 'asc' ? '▲' : '▼') : '⇅')));
329
- }))), h("tbody", null, topSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${topSpacerHeight}px` } }, h("td", { colSpan: colCount }))), renderedRows, bottomSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${bottomSpacerHeight}px` } }, h("td", { colSpan: colCount })))))), total === 0 && (h("p", { class: "mrd-table__empty" }, "Geen resultaten gevonden."))));
570
+ const name = this.colName(col);
571
+ const isActive = this.sortField === name;
572
+ const isFiltered = this.activeFilters.has(name);
573
+ const cls = [
574
+ 'mrd-table__header',
575
+ 'mrd-table__header--sortable',
576
+ isActive ? `mrd-table__header--sorted-${this.sortDir}` : '',
577
+ isFiltered ? 'mrd-table__header--filtered' : '',
578
+ ].filter(Boolean).join(' ');
579
+ return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: () => this.handleSortClick(col) }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, isActive ? (this.sortDir === 'asc' ? '▲' : '▼') : '⇅'), this.filterMode && (h("button", { class: `mrd-table__header-filter-btn${isFiltered ? ' mrd-table__header-filter-btn--active' : ''}`, onClick: (e) => { e.stopPropagation(); this.handleFilterOpen(col, e); } }, "\u25BE"))));
580
+ }))), h("tbody", null, topSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${topSpacerHeight}px` } }, h("td", { colSpan: colCount }))), renderedRows, bottomSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${bottomSpacerHeight}px` } }, h("td", { colSpan: colCount })))))), effectiveTotal === 0 && this.loadedPages.has(0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale))), effectiveTotal > 0 && this.renderFooter(undefined, effectiveTotal), this.renderFilterPopup()));
330
581
  }
331
582
  get el() { return getElement(this); }
583
+ static get watchers() { return {
584
+ "totalElements": [{
585
+ "totalElementsChanged": 0
586
+ }]
587
+ }; }
332
588
  };
333
589
  MrdTable.style = mrdTableScss();
334
590
 
@@ -1 +1 @@
1
- import{p as e,b as l}from"./p-_tsCCkAi.js";export{s as setNonce}from"./p-_tsCCkAi.js";import{g as a}from"./p-DQuL1Twl.js";(()=>{const l=import.meta.url,a={};return""!==l&&(a.resourcesUrl=new URL(".",l).href),e(a)})().then((async e=>(await a(),l([["p-baf08615",[[2,"mrd-table",{columns:[16],rows:[16],locale:[1],totalElements:[2,"total-elements"],pageSize:[2,"page-size"],rowHeight:[2,"row-height"],tableHeight:[2,"table-height"],defaultSort:[1,"default-sort"],loadedPages:[32],requestedPages:[32],renderStart:[32],renderEnd:[32],colWidths:[32],sortField:[32],sortDir:[32],init:[64],setPage:[64]}]]],["p-2a8cb2eb",[[2,"mrd-form",{layout:[16],locale:[1],values:[16],referenceHref:[1,"reference-href"],referenceClass:[1,"reference-class"],showCancel:[4,"show-cancel"],formValues:[32],errors:[32],submitted:[32],setFieldValue:[64]},null,{values:[{valuesChanged:0}]}],[2,"mrd-field",{item:[16],locale:[1],value:[16]}],[2,"mrd-boolean-field",{name:[1],label:[1],value:[4],required:[4],disabled:[4],locale:[1],checked:[32]}],[2,"mrd-currency-field",{name:[1],label:[1],value:[16],required:[4],disabled:[4],locale:[1],amountDisplay:[32],currency:[32],error:[32]}],[2,"mrd-date-field",{name:[1],label:[1],value:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-datetime-field",{name:[1],label:[1],value:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-email-field",{name:[1],label:[1],value:[1],placeholder:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-file-field",{name:[1],label:[1],required:[4],disabled:[4],locale:[1],accept:[1],maxSize:[2,"max-size"],fileName:[32],isDragging:[32],error:[32]}],[2,"mrd-hyperlink-field",{name:[1],label:[1],value:[1],placeholder:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-image-field",{name:[1],label:[1],required:[4],disabled:[4],locale:[1],accept:[1],maxSize:[2,"max-size"],previewUrl:[32],fileName:[32],isDragging:[32],error:[32]}],[2,"mrd-list-field",{name:[1],label:[1],value:[1],required:[4],disabled:[4],multiple:[4],locale:[1],listItems:[16],error:[32],selected:[32]}],[2,"mrd-number-field",{name:[1],label:[1],value:[2],placeholder:[1],required:[4],disabled:[4],locale:[1],dataType:[1,"data-type"],decimalPrecision:[2,"decimal-precision"],displayValue:[32],error:[32]}],[2,"mrd-relation-field",{name:[1],label:[1],required:[4],disabled:[4],locale:[1],relatedClass:[1,"related-class"],mostSignificantClass:[1,"most-significant-class"],displayType:[1,"display-type"],editBehavior:[1,"edit-behavior"],commonRelation:[1,"common-relation"],multiple:[4],dropdownValues:[16],value:[1],searchQuery:[32],searchResults:[32],allRecords:[32],isLoading:[32],selectedItems:[32],showResults:[32],error:[32],highlightedIndex:[32],setAllRecords:[64],setSearchResults:[64],setLoading:[64]}],[2,"mrd-text-field",{name:[1],label:[1],value:[1],placeholder:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-textarea-field",{name:[1],label:[1],value:[1],placeholder:[1],required:[4],disabled:[4],locale:[1],error:[32],editorReady:[32]}],[2,"mrd-time-field",{name:[1],label:[1],value:[1],required:[4],disabled:[4],locale:[1],error:[32]}]]]],e))));
1
+ import{p as e,b as l}from"./p-_tsCCkAi.js";export{s as setNonce}from"./p-_tsCCkAi.js";import{g as a}from"./p-DQuL1Twl.js";(()=>{const l=import.meta.url,a={};return""!==l&&(a.resourcesUrl=new URL(".",l).href),e(a)})().then((async e=>(await a(),l([["p-27f6947a",[[2,"mrd-table",{columns:[16],rows:[16],locale:[1],totalElements:[2,"total-elements"],pageSize:[2,"page-size"],rowHeight:[2,"row-height"],tableHeight:[2,"table-height"],defaultSort:[1,"default-sort"],actions:[16],loadedPages:[32],requestedPages:[32],renderStart:[32],renderEnd:[32],colWidths:[32],sortField:[32],sortDir:[32],filterMode:[32],activeFilters:[32],openFilterCol:[32],pendingFilter:[32],popupPos:[32],scrollTop:[32],init:[64],setPage:[64]},null,{totalElements:[{totalElementsChanged:0}]}]]],["p-ca5f9919",[[2,"mrd-form",{layout:[16],locale:[1],values:[16],referenceHref:[1,"reference-href"],referenceClass:[1,"reference-class"],showCancel:[4,"show-cancel"],formValues:[32],errors:[32],submitted:[32],setFieldValue:[64]},null,{values:[{valuesChanged:0}]}],[2,"mrd-field",{item:[16],locale:[1],value:[16]}],[2,"mrd-boolean-field",{name:[1],label:[1],value:[4],required:[4],disabled:[4],locale:[1],checked:[32]}],[2,"mrd-currency-field",{name:[1],label:[1],value:[16],required:[4],disabled:[4],locale:[1],amountDisplay:[32],currency:[32],error:[32]}],[2,"mrd-date-field",{name:[1],label:[1],value:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-datetime-field",{name:[1],label:[1],value:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-email-field",{name:[1],label:[1],value:[1],placeholder:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-file-field",{name:[1],label:[1],value:[16],required:[4],disabled:[4],locale:[1],accept:[1],maxSize:[2,"max-size"],fileName:[32],isDragging:[32],uploading:[32],error:[32]},null,{value:[{valueChanged:0}]}],[2,"mrd-hyperlink-field",{name:[1],label:[1],value:[1],placeholder:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-image-field",{name:[1],label:[1],value:[16],required:[4],disabled:[4],locale:[1],accept:[1],maxSize:[2,"max-size"],previewUrl:[32],fileName:[32],isDragging:[32],uploading:[32],error:[32]},null,{value:[{valueChanged:0}]}],[2,"mrd-list-field",{name:[1],label:[1],value:[1],required:[4],disabled:[4],multiple:[4],locale:[1],listItems:[16],error:[32],selected:[32]}],[2,"mrd-number-field",{name:[1],label:[1],value:[2],placeholder:[1],required:[4],disabled:[4],locale:[1],dataType:[1,"data-type"],decimalPrecision:[2,"decimal-precision"],displayValue:[32],error:[32]}],[2,"mrd-relation-field",{name:[1],label:[1],required:[4],disabled:[4],locale:[1],relatedClass:[1,"related-class"],mostSignificantClass:[1,"most-significant-class"],displayType:[1,"display-type"],editBehavior:[1,"edit-behavior"],commonRelation:[1,"common-relation"],multiple:[4],dropdownValues:[16],value:[1],searchQuery:[32],searchResults:[32],allRecords:[32],isLoading:[32],selectedItems:[32],showResults:[32],error:[32],highlightedIndex:[32],setAllRecords:[64],setSearchResults:[64],setLoading:[64]}],[2,"mrd-text-field",{name:[1],label:[1],value:[1],placeholder:[1],required:[4],disabled:[4],locale:[1],error:[32]}],[2,"mrd-textarea-field",{name:[1],label:[1],value:[1],placeholder:[1],required:[4],disabled:[4],locale:[1],error:[32],editorReady:[32]}],[2,"mrd-time-field",{name:[1],label:[1],value:[1],required:[4],disabled:[4],locale:[1],error:[32]}]]]],e))));