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