@mmlogic/components 0.1.10 → 0.1.12

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 (31) 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 +29 -110
  6. package/dist/cjs/mrd-table.cjs.entry.js +323 -62
  7. package/dist/collection/components/mrd-table/mrd-table.js +391 -62
  8. package/dist/collection/components/mrd-table/mrd-table.scss +388 -0
  9. package/dist/collection/dev/app.js +48 -4
  10. package/dist/collection/dev/sprites.svg +55 -0
  11. package/dist/collection/utils/i18n.js +144 -0
  12. package/dist/components/i18n.js +1 -1
  13. package/dist/components/mrd-table.js +1 -1
  14. package/dist/esm/format-EzhfM0uw.js +299 -0
  15. package/dist/esm/loader.js +1 -1
  16. package/dist/esm/mosterdcomponents.js +1 -1
  17. package/dist/esm/mrd-boolean-field_16.entry.js +1 -82
  18. package/dist/esm/mrd-table.entry.js +323 -62
  19. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  20. package/dist/mosterdcomponents/p-6832ea50.entry.js +1 -0
  21. package/dist/mosterdcomponents/p-EzhfM0uw.js +1 -0
  22. package/dist/mosterdcomponents/p-ca5f9919.entry.js +1 -0
  23. package/dist/types/components/mrd-table/mrd-table.d.ts +52 -18
  24. package/dist/types/components.d.ts +23 -3
  25. package/dist/types/utils/cell-renderer.d.ts +27 -0
  26. package/package.json +1 -1
  27. package/dist/cjs/format-CDw-zie_.js +0 -82
  28. package/dist/esm/format-Dt-aHxkM.js +0 -74
  29. package/dist/mosterdcomponents/p-Dt-aHxkM.js +0 -1
  30. package/dist/mosterdcomponents/p-baf08615.entry.js +0 -1
  31. package/dist/mosterdcomponents/p-bb567c32.entry.js +0 -1
@@ -1,16 +1,22 @@
1
1
  import { h, Host } from "@stencil/core";
2
2
  import { CellRenderer } from "../../utils/cell-renderer";
3
+ import { t } from "../../utils/i18n";
3
4
  const BUFFER = 10;
4
5
  /** Wacht deze tijd (ms) na het laatste scroll-event voordat pagina's worden
5
6
  * aangevraagd. Pagina's die de gebruiker snel voorbij scrollt worden zo geskipt. */
6
7
  const REQUEST_DEBOUNCE_MS = 150;
8
+ /** Breedte van de filterpopup in px — voor overflow-correctie. */
9
+ const POPUP_WIDTH = 280;
10
+ const TEXT_TYPES = new Set(['TEXT', 'TEXTBLOCK', 'EMAIL', 'HYPERLINK']);
11
+ const NUMERIC_TYPES = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
12
+ const DATE_TYPES = new Set(['DATE', 'DATETIME', 'TIME']);
13
+ const NO_FILTER_TYPES = new Set(['FILE', 'IMAGE']);
7
14
  export class MrdTable {
8
15
  constructor() {
9
- // ── Debounce internals (geen @State — triggert geen re-render) ─────────────
10
- /** Pagina's die wachten op debounce-flush. */
16
+ // ── Non-state internals ────────────────────────────────────────────────────
11
17
  this.pendingPages = new Set();
12
- /** Handle van de actieve debounce-timer. */
13
18
  this.debounceTimer = null;
19
+ this.outsideClickHandler = null;
14
20
  // ── Props ──────────────────────────────────────────────────────────────────
15
21
  this.columns = [];
16
22
  /** Direct rows (non-paginated mode, used when totalElements === 0). */
@@ -27,32 +33,45 @@ export class MrdTable {
27
33
  /** Initial sort applied on load, e.g. "timestamp,desc" or "name".
28
34
  * Parsed by init() into sortField + sortDir. */
29
35
  this.defaultSort = '';
36
+ /** Toolbar action buttons rendered above the table. */
37
+ this.actions = [];
30
38
  // ── Internal state ─────────────────────────────────────────────────────────
31
- /** Pages injected via setPage(). Always replaced by a new Map to trigger re-render. */
32
39
  this.loadedPages = new Map();
33
- /** Pages already requested via mrdLoadPage (to avoid duplicate events). */
34
40
  this.requestedPages = new Set();
35
- /** Absolute index of the first row currently in the render window. */
36
41
  this.renderStart = 0;
37
- /** Absolute index of the last row currently in the render window. */
38
42
  this.renderEnd = 0;
39
- /** Locked column widths (px) — measured after first page renders, then fixed. */
40
43
  this.colWidths = [];
41
- /** Column currently used for sorting (empty = no sort). */
42
44
  this.sortField = '';
43
- /** Sort direction for sortField. */
44
45
  this.sortDir = 'asc';
46
+ /** Whether the filter UI is visible on column headers. */
47
+ this.filterMode = false;
48
+ /** Active filters keyed by field name. */
49
+ this.activeFilters = new Map();
50
+ /** Field name of the currently open filter popup (null = closed). */
51
+ this.openFilterCol = null;
52
+ /** Filter state being edited in the open popup. */
53
+ this.pendingFilter = null;
54
+ /** Viewport-relative position for the filter popup. */
55
+ this.popupPos = { top: 0, left: 0 };
56
+ /** Current scroll offset of the scroll container — drives pagination footer. */
57
+ this.scrollTop = 0;
45
58
  this.handleScroll = (e) => {
46
59
  const scroller = e.currentTarget;
47
60
  const scrollTop = scroller.scrollTop;
48
61
  const total = this.totalElements;
49
62
  const visStart = Math.floor(scrollTop / this.rowHeight);
50
63
  const visEnd = Math.min(visStart + this.visibleCount(), total - 1);
64
+ this.scrollTop = scrollTop;
51
65
  this.renderStart = Math.max(0, visStart - BUFFER);
52
66
  this.renderEnd = Math.min(total - 1, visEnd + BUFFER);
53
67
  this.requestPagesForWindow(this.renderStart, this.renderEnd);
54
68
  };
55
69
  }
70
+ // ── Prop watchers ─────────────────────────────────────────────────────────
71
+ /** Clamp renderEnd when totalElements shrinks (e.g. after a filter is applied). */
72
+ totalElementsChanged(newVal) {
73
+ this.renderEnd = Math.min(this.renderEnd, Math.max(0, newVal - 1));
74
+ }
56
75
  // ── Public API ─────────────────────────────────────────────────────────────
57
76
  /**
58
77
  * Initialise (or reset) the virtual scroll.
@@ -69,7 +88,6 @@ export class MrdTable {
69
88
  this.loadedPages = new Map();
70
89
  this.requestedPages = new Set();
71
90
  this.colWidths = [];
72
- // Apply defaultSort prop as the initial sort state.
73
91
  if (this.defaultSort) {
74
92
  const parts = this.defaultSort.split(',');
75
93
  this.sortField = parts[0].trim();
@@ -79,66 +97,105 @@ export class MrdTable {
79
97
  this.sortField = '';
80
98
  this.sortDir = 'asc';
81
99
  }
100
+ this.scrollTop = 0;
82
101
  this.renderStart = 0;
83
- this.renderEnd = Math.max(0, Math.min(this.visibleCount() + BUFFER, this.totalElements - 1));
84
- // Scroll the container back to the top when switching datasets.
102
+ // No BUFFER on init only request what fits the visible area (page 0).
103
+ // BUFFER is applied during scroll to pre-fetch the next page proactively.
104
+ this.renderEnd = Math.max(0, Math.min(this.visibleCount() - 1, this.totalElements - 1));
85
105
  const scroller = this.el.querySelector('.mrd-table__scroll');
86
106
  if (scroller)
87
107
  scroller.scrollTop = 0;
88
- // Do NOT emit mrdLoadPage here — the host injects page 0 via setPage().
89
108
  }
90
109
  /**
91
110
  * Inject the rows for a given page (0-based).
92
111
  * Creates a new Map reference so Stencil detects the state change.
112
+ *
113
+ * When the page contains fewer rows than pageSize it is the last page.
114
+ * renderEnd is clamped immediately so no loading-placeholder rows appear
115
+ * beyond the actual data — without requiring the host to update totalElements.
93
116
  */
94
117
  async setPage(pageNumber, rows) {
118
+ if (rows.length < this.pageSize) {
119
+ // lastRowIdx is -1 when the page is empty; clamp renderEnd to -1 so the
120
+ // render loop does not execute and no shimmer rows appear.
121
+ const lastRowIdx = pageNumber * this.pageSize + rows.length - 1;
122
+ this.renderEnd = Math.min(this.renderEnd, lastRowIdx);
123
+ }
95
124
  const next = new Map(this.loadedPages);
96
125
  next.set(pageNumber, rows);
97
126
  this.loadedPages = next;
98
127
  }
99
- // ── Private helpers ────────────────────────────────────────────────────────
128
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
129
+ disconnectedCallback() {
130
+ if (this.outsideClickHandler) {
131
+ document.removeEventListener('click', this.outsideClickHandler);
132
+ this.outsideClickHandler = null;
133
+ }
134
+ }
135
+ componentDidRender() {
136
+ if (this.colWidths.length === 0 && this.loadedPages.size > 0 && this.totalElements > 0) {
137
+ const ths = this.el.querySelectorAll('.mrd-table__header');
138
+ if (ths.length > 0) {
139
+ this.colWidths = Array.from(ths).map(th => th.offsetWidth);
140
+ }
141
+ }
142
+ }
143
+ // ── Paging / scroll helpers ────────────────────────────────────────────────
100
144
  visibleCount() {
101
145
  return Math.ceil(this.tableHeight / this.rowHeight);
102
146
  }
103
- /** Returns the current sort value for use in ?sort= query params. */
104
147
  sortParam() {
105
148
  if (!this.sortField)
106
149
  return '';
107
150
  return this.sortDir === 'desc' ? `${this.sortField},desc` : this.sortField;
108
151
  }
109
- /** Called when a header cell is clicked. Toggles direction or sets a new column. */
110
152
  colName(col) {
111
153
  var _a, _b, _c, _d;
112
154
  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 : '';
113
155
  }
114
- handleSortClick(col) {
115
- const name = this.colName(col);
116
- if (this.sortField === name) {
117
- this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
118
- }
119
- else {
120
- this.sortField = name;
121
- this.sortDir = 'asc';
122
- }
123
- // Cancel any pending scroll debounce.
156
+ colDataType(col) {
157
+ var _a, _b;
158
+ if (col.type === 'RELATION')
159
+ return 'RELATION';
160
+ return (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.dataType) !== null && _b !== void 0 ? _b : 'TEXT';
161
+ }
162
+ /** Reset pagination state and scroll to top (used after sort or filter change). */
163
+ resetPages() {
124
164
  if (this.debounceTimer !== null) {
125
165
  clearTimeout(this.debounceTimer);
126
166
  this.debounceTimer = null;
127
167
  }
128
168
  this.pendingPages.clear();
129
- // Wipe all loaded data so the new sort order is fetched fresh.
130
169
  this.loadedPages = new Map();
131
170
  this.requestedPages = new Set();
132
171
  this.colWidths = [];
172
+ this.scrollTop = 0;
133
173
  this.renderStart = 0;
134
- this.renderEnd = Math.max(0, Math.min(this.visibleCount() + BUFFER, this.totalElements - 1));
174
+ // No BUFFER here totalElements may be stale after a filter change.
175
+ // Only request what is visible; BUFFER kicks in during scroll as usual.
176
+ this.renderEnd = Math.max(0, Math.min(this.visibleCount() - 1, this.totalElements - 1));
135
177
  const scroller = this.el.querySelector('.mrd-table__scroll');
136
178
  if (scroller)
137
179
  scroller.scrollTop = 0;
138
- // Emit immediately — no debounce for intentional sort clicks.
180
+ }
181
+ handleSortClick(col) {
182
+ const name = this.colName(col);
183
+ if (this.sortField === name) {
184
+ this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
185
+ }
186
+ else {
187
+ this.sortField = name;
188
+ this.sortDir = 'asc';
189
+ }
190
+ this.resetPages();
191
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
192
+ }
193
+ applySort(col, dir) {
194
+ this.sortField = this.colName(col);
195
+ this.sortDir = dir;
196
+ this.resetPages();
139
197
  this.emitPagesForWindow(this.renderStart, this.renderEnd);
140
198
  }
141
- /** Emits mrdLoadPage immediately for all missing pages in [start, end]. */
142
199
  emitPagesForWindow(start, end) {
143
200
  const firstPage = Math.floor(start / this.pageSize);
144
201
  const lastPage = Math.floor(end / this.pageSize);
@@ -171,13 +228,10 @@ export class MrdTable {
171
228
  }
172
229
  if (!anyNew)
173
230
  return;
174
- // Reset de timer: wacht tot het scrollen even stopt.
175
231
  if (this.debounceTimer !== null)
176
232
  clearTimeout(this.debounceTimer);
177
233
  this.debounceTimer = setTimeout(() => this.flushPendingPages(), REQUEST_DEBOUNCE_MS);
178
234
  }
179
- /** Emitteert mrdLoadPage alleen voor pagina's die na de debounce-wachttijd
180
- * nog steeds binnen het huidige render-venster vallen. */
181
235
  flushPendingPages() {
182
236
  this.debounceTimer = null;
183
237
  if (this.pendingPages.size === 0)
@@ -185,10 +239,8 @@ export class MrdTable {
185
239
  const next = new Set(this.requestedPages);
186
240
  let changed = false;
187
241
  for (const page of this.pendingPages) {
188
- // Sla over als pagina inmiddels geladen of al aangevraagd is.
189
242
  if (this.loadedPages.has(page) || next.has(page))
190
243
  continue;
191
- // Sla over als de pagina buiten het huidige venster is geraakt.
192
244
  const pageStart = page * this.pageSize;
193
245
  const pageEnd = pageStart + this.pageSize - 1;
194
246
  if (pageEnd < this.renderStart || pageStart > this.renderEnd)
@@ -201,42 +253,241 @@ export class MrdTable {
201
253
  if (changed)
202
254
  this.requestedPages = next;
203
255
  }
204
- // ── Lifecycle ──────────────────────────────────────────────────────────────
205
- /** After the first page of data renders, lock column widths so subsequent
206
- * page loads don't cause layout shifts. */
207
- componentDidRender() {
208
- if (this.colWidths.length === 0 && this.loadedPages.size > 0 && this.totalElements > 0) {
209
- const ths = this.el.querySelectorAll('.mrd-table__header');
210
- if (ths.length > 0) {
211
- this.colWidths = Array.from(ths).map(th => th.offsetWidth);
212
- }
256
+ // ── Filter helpers ─────────────────────────────────────────────────────────
257
+ handleFilterToggle() {
258
+ this.filterMode = !this.filterMode;
259
+ if (!this.filterMode)
260
+ this.closeFilterPopup();
261
+ }
262
+ handleFilterOpen(col, e) {
263
+ e.stopPropagation();
264
+ const btn = e.currentTarget;
265
+ const rect = btn.getBoundingClientRect();
266
+ let left = rect.left;
267
+ if (left + POPUP_WIDTH > window.innerWidth - 8)
268
+ left = rect.right - POPUP_WIDTH;
269
+ this.popupPos = { top: rect.bottom + 4, left: Math.max(8, left) };
270
+ const name = this.colName(col);
271
+ const dataType = this.colDataType(col);
272
+ const existing = this.activeFilters.get(name);
273
+ // Set the default operator explicitly so it is present in the ColumnFilter
274
+ // even when the user never touches the operator dropdown.
275
+ const defaultOperator = (TEXT_TYPES.has(dataType) || dataType === 'RELATION')
276
+ ? 'startsWith'
277
+ : undefined;
278
+ this.pendingFilter = existing ? Object.assign({}, existing) : { field: name, dataType, operator: defaultOperator };
279
+ this.openFilterCol = name;
280
+ // Close on outside click — re-register to replace any stale handler
281
+ if (this.outsideClickHandler)
282
+ document.removeEventListener('click', this.outsideClickHandler);
283
+ this.outsideClickHandler = (ev) => {
284
+ const popup = this.el.querySelector('.mrd-table__filter-popup');
285
+ if (popup && !popup.contains(ev.target))
286
+ this.closeFilterPopup();
287
+ };
288
+ document.addEventListener('click', this.outsideClickHandler);
289
+ }
290
+ closeFilterPopup() {
291
+ this.openFilterCol = null;
292
+ this.pendingFilter = null;
293
+ if (this.outsideClickHandler) {
294
+ document.removeEventListener('click', this.outsideClickHandler);
295
+ this.outsideClickHandler = null;
296
+ }
297
+ }
298
+ setPending(key, val) {
299
+ this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { [key]: val });
300
+ }
301
+ togglePendingValue(key, checked) {
302
+ var _a, _b;
303
+ const current = (_b = (_a = this.pendingFilter) === null || _a === void 0 ? void 0 : _a.values) !== null && _b !== void 0 ? _b : [];
304
+ this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { values: checked ? [...current, key] : current.filter(k => k !== key) });
305
+ }
306
+ filterHasValue(f) {
307
+ if (f.operator === 'isEmpty' || f.operator === 'isNotEmpty')
308
+ return true;
309
+ if (f.values !== undefined && f.values.length > 0)
310
+ return true;
311
+ if (f.value != null && f.value !== '')
312
+ return true;
313
+ if (typeof f.value === 'boolean')
314
+ return true;
315
+ if (f.from != null && f.from !== '')
316
+ return true;
317
+ if (f.to != null && f.to !== '')
318
+ return true;
319
+ return false;
320
+ }
321
+ applyFilter() {
322
+ const f = this.pendingFilter;
323
+ if (!(f === null || f === void 0 ? void 0 : f.field)) {
324
+ this.closeFilterPopup();
325
+ return;
326
+ }
327
+ const next = new Map(this.activeFilters);
328
+ if (this.filterHasValue(f)) {
329
+ next.set(f.field, f);
330
+ }
331
+ else {
332
+ next.delete(f.field);
333
+ }
334
+ this.activeFilters = next;
335
+ this.closeFilterPopup();
336
+ this.mrdFilter.emit({ filters: Array.from(this.activeFilters.values()) });
337
+ if (this.totalElements > 0) {
338
+ this.resetPages();
339
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
213
340
  }
214
341
  }
342
+ clearFilter() {
343
+ const name = this.openFilterCol;
344
+ const next = new Map(this.activeFilters);
345
+ if (name)
346
+ next.delete(name);
347
+ this.activeFilters = next;
348
+ this.closeFilterPopup();
349
+ this.mrdFilter.emit({ filters: Array.from(this.activeFilters.values()) });
350
+ if (this.totalElements > 0) {
351
+ this.resetPages();
352
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
353
+ }
354
+ }
355
+ clearAllFilters() {
356
+ this.activeFilters = new Map();
357
+ this.mrdFilter.emit({ filters: [] });
358
+ if (this.totalElements > 0) {
359
+ this.resetPages();
360
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
361
+ }
362
+ }
363
+ // ── Render: toolbar ────────────────────────────────────────────────────────
364
+ renderToolbar() {
365
+ var _a;
366
+ const filterCount = this.activeFilters.size;
367
+ const hasActions = ((_a = this.actions) === null || _a === void 0 ? void 0 : _a.length) > 0;
368
+ 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 => {
369
+ var _a;
370
+ 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
371
+ ? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
372
+ : a.label, h("span", { class: "mrd-table__action-tooltip" }, a.label)));
373
+ })))));
374
+ }
375
+ // ── Render: filter popup ───────────────────────────────────────────────────
376
+ renderFilterEditor(col) {
377
+ var _a, _b, _c, _d, _e, _f, _g;
378
+ const pf = (_a = this.pendingFilter) !== null && _a !== void 0 ? _a : {};
379
+ const dataType = this.colDataType(col);
380
+ if (NO_FILTER_TYPES.has(dataType)) {
381
+ return h("p", { class: "mrd-table__filter-no-support" }, t('filter_no_support', this.locale));
382
+ }
383
+ if (dataType === 'BOOLEAN') {
384
+ return (h("div", { class: "mrd-table__filter-radio-group" }, [
385
+ { labelKey: 'filter_all', value: null },
386
+ { labelKey: 'yes', value: true },
387
+ { labelKey: 'no', value: false },
388
+ ].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))))));
389
+ }
390
+ if (dataType === 'LIST') {
391
+ const items = (_c = (_b = col.field) === null || _b === void 0 ? void 0 : _b.listItems) !== null && _c !== void 0 ? _c : [];
392
+ const selected = (_d = pf.values) !== null && _d !== void 0 ? _d : [];
393
+ 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)))));
394
+ }
395
+ if (TEXT_TYPES.has(dataType) || dataType === 'RELATION') {
396
+ const op = (_e = pf.operator) !== null && _e !== void 0 ? _e : 'startsWith';
397
+ const noInput = op === 'isEmpty' || op === 'isNotEmpty';
398
+ return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => this.setPending('operator', e.target.value) }, [
399
+ { val: 'startsWith', labelKey: 'filter_starts_with' },
400
+ { val: 'equals', labelKey: 'filter_equals' },
401
+ { val: 'isEmpty', labelKey: 'filter_is_empty' },
402
+ { val: 'isNotEmpty', labelKey: 'filter_is_not_empty' },
403
+ ].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) }))));
404
+ }
405
+ if (NUMERIC_TYPES.has(dataType)) {
406
+ const rangeMode = pf.from !== undefined || pf.to !== undefined;
407
+ 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) })))));
408
+ }
409
+ if (DATE_TYPES.has(dataType)) {
410
+ const inputType = dataType === 'DATE' ? 'date'
411
+ : dataType === 'DATETIME' ? 'datetime-local'
412
+ : 'time';
413
+ const rangeMode = pf.from !== undefined || pf.to !== undefined;
414
+ 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) })))));
415
+ }
416
+ return null;
417
+ }
418
+ renderFilterPopup() {
419
+ var _a, _b, _c, _d;
420
+ if (!this.openFilterCol || !this.pendingFilter)
421
+ return null;
422
+ const col = this.columns.find(c => this.colName(c) === this.openFilterCol);
423
+ if (!col)
424
+ return null;
425
+ 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;
426
+ const sortActive = this.sortField === this.openFilterCol;
427
+ 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)))));
428
+ }
429
+ // ── Render: footer ────────────────────────────────────────────────────────
430
+ renderFooter(rowCount, effectiveTotal) {
431
+ const total = this.totalElements;
432
+ // Non-paginated mode: show plain row count
433
+ if (total === 0) {
434
+ const count = rowCount !== null && rowCount !== void 0 ? rowCount : 0;
435
+ if (count === 0)
436
+ return null;
437
+ return (h("div", { class: "mrd-table__footer" }, count, " ", t('table_of', this.locale), " ", count));
438
+ }
439
+ // Paginated mode: only show once page 0 has loaded (avoids stale total during filter reset)
440
+ if (!this.loadedPages.has(0))
441
+ return null;
442
+ // Use effectiveTotal (derived from actual page lengths) so the counter
443
+ // is correct even when the host has not yet updated totalElements.
444
+ const displayTotal = effectiveTotal !== null && effectiveTotal !== void 0 ? effectiveTotal : total;
445
+ // Compute from/to independently so partial rows at top/bottom are included.
446
+ const from = Math.min(Math.floor(this.scrollTop / this.rowHeight) + 1, displayTotal);
447
+ const to = Math.min(Math.ceil((this.scrollTop + this.tableHeight) / this.rowHeight), displayTotal);
448
+ return (h("div", { class: "mrd-table__footer" }, from, "\u2013", to, " ", t('table_of', this.locale), " ", displayTotal));
449
+ }
215
450
  // ── Render ─────────────────────────────────────────────────────────────────
216
451
  render() {
217
- var _a, _b;
452
+ var _a, _b, _c;
218
453
  if (!((_a = this.columns) === null || _a === void 0 ? void 0 : _a.length))
219
454
  return null;
220
455
  const numericTypes = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
221
456
  // ── Non-paginated mode ──────────────────────────────────────────────────
222
457
  if (this.totalElements === 0) {
223
- 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 => {
458
+ 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 => {
224
459
  var _a, _b, _c, _d;
225
- 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 : ''));
460
+ const name = this.colName(col);
461
+ const isFiltered = this.activeFilters.has(name);
462
+ 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"))));
226
463
  }))), 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 => {
227
464
  var _a, _b;
228
465
  const value = CellRenderer.render(col, row, this.locale);
229
466
  const isNumeric = col.type === 'FIELD' && numericTypes.has((_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.dataType) !== null && _b !== void 0 ? _b : '');
230
467
  return (h("td", { class: `mrd-table__cell${isNumeric ? ' mrd-table__cell--numeric' : ''}` }, value));
231
- })))))), (!this.rows || this.rows.length === 0) && (h("p", { class: "mrd-table__empty" }, "Geen resultaten gevonden.")))));
468
+ })))))), (!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()));
232
469
  }
233
470
  // ── Paginated / virtual-scroll mode ────────────────────────────────────
234
- const total = this.totalElements;
471
+ // Derive the authoritative row count from loaded pages:
472
+ // if any loaded page is shorter than pageSize it is the last page,
473
+ // so the true total cannot exceed (pageNum * pageSize + pageRows.length).
474
+ // This self-corrects without requiring the host to update totalElements.
475
+ let effectiveTotal = this.totalElements;
476
+ for (const [pageNum, pageRows] of this.loadedPages) {
477
+ if (pageRows.length < this.pageSize) {
478
+ effectiveTotal = Math.min(effectiveTotal, pageNum * this.pageSize + pageRows.length);
479
+ }
480
+ }
481
+ // Clamp renderEnd to what we actually know exists (-1 when empty)
482
+ const clampedEnd = Math.min(this.renderEnd, effectiveTotal - 1);
235
483
  const colCount = this.columns.length;
236
484
  const topSpacerHeight = this.renderStart * this.rowHeight;
237
- const bottomSpacerHeight = Math.max(0, (total - 1 - this.renderEnd) * this.rowHeight);
485
+ const bottomSpacerHeight = Math.max(0, (effectiveTotal - 1 - clampedEnd) * this.rowHeight);
486
+ const tableStyle = this.colWidths.length > 0
487
+ ? { tableLayout: 'fixed' }
488
+ : undefined;
238
489
  const renderedRows = [];
239
- for (let i = this.renderStart; i <= this.renderEnd; i++) {
490
+ for (let i = this.renderStart; i <= clampedEnd; i++) {
240
491
  const row = this.getRow(i);
241
492
  if (row === null) {
242
493
  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" }))));
@@ -250,15 +501,19 @@ export class MrdTable {
250
501
  })));
251
502
  }
252
503
  }
253
- const tableStyle = this.colWidths.length > 0
254
- ? { tableLayout: 'fixed' }
255
- : undefined;
256
- 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) => {
504
+ 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) => {
257
505
  var _a, _b, _c, _d;
258
- const isActive = this.sortField === this.colName(col);
259
- const cls = `mrd-table__header mrd-table__header--sortable${isActive ? ` mrd-table__header--sorted-${this.sortDir}` : ''}`;
260
- 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' ? '▲' : '▼') : '⇅')));
261
- }))), 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."))));
506
+ const name = this.colName(col);
507
+ const isActive = this.sortField === name;
508
+ const isFiltered = this.activeFilters.has(name);
509
+ const cls = [
510
+ 'mrd-table__header',
511
+ 'mrd-table__header--sortable',
512
+ isActive ? `mrd-table__header--sorted-${this.sortDir}` : '',
513
+ isFiltered ? 'mrd-table__header--filtered' : '',
514
+ ].filter(Boolean).join(' ');
515
+ 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"))));
516
+ }))), 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()));
262
517
  }
263
518
  static get is() { return "mrd-table"; }
264
519
  static get encapsulation() { return "scoped"; }
@@ -441,6 +696,31 @@ export class MrdTable {
441
696
  "reflect": false,
442
697
  "attribute": "default-sort",
443
698
  "defaultValue": "''"
699
+ },
700
+ "actions": {
701
+ "type": "unknown",
702
+ "mutable": false,
703
+ "complexType": {
704
+ "original": "TableAction[]",
705
+ "resolved": "TableAction[]",
706
+ "references": {
707
+ "TableAction": {
708
+ "location": "import",
709
+ "path": "../../utils/cell-renderer",
710
+ "id": "src/utils/cell-renderer.ts::TableAction",
711
+ "referenceLocation": "TableAction"
712
+ }
713
+ }
714
+ },
715
+ "required": false,
716
+ "optional": false,
717
+ "docs": {
718
+ "tags": [],
719
+ "text": "Toolbar action buttons rendered above the table."
720
+ },
721
+ "getter": false,
722
+ "setter": false,
723
+ "defaultValue": "[]"
444
724
  }
445
725
  };
446
726
  }
@@ -452,7 +732,13 @@ export class MrdTable {
452
732
  "renderEnd": {},
453
733
  "colWidths": {},
454
734
  "sortField": {},
455
- "sortDir": {}
735
+ "sortDir": {},
736
+ "filterMode": {},
737
+ "activeFilters": {},
738
+ "openFilterCol": {},
739
+ "pendingFilter": {},
740
+ "popupPos": {},
741
+ "scrollTop": {}
456
742
  };
457
743
  }
458
744
  static get events() {
@@ -491,6 +777,43 @@ export class MrdTable {
491
777
  }
492
778
  }
493
779
  }
780
+ }, {
781
+ "method": "mrdAction",
782
+ "name": "mrdAction",
783
+ "bubbles": true,
784
+ "cancelable": true,
785
+ "composed": true,
786
+ "docs": {
787
+ "tags": [],
788
+ "text": "Fired when a toolbar action button is clicked. Detail contains the action identifier."
789
+ },
790
+ "complexType": {
791
+ "original": "{ action: string }",
792
+ "resolved": "{ action: string; }",
793
+ "references": {}
794
+ }
795
+ }, {
796
+ "method": "mrdFilter",
797
+ "name": "mrdFilter",
798
+ "bubbles": true,
799
+ "cancelable": true,
800
+ "composed": true,
801
+ "docs": {
802
+ "tags": [],
803
+ "text": "Fired when active filters change. Host translates filters to API query params."
804
+ },
805
+ "complexType": {
806
+ "original": "{ filters: ColumnFilter[] }",
807
+ "resolved": "{ filters: ColumnFilter[]; }",
808
+ "references": {
809
+ "ColumnFilter": {
810
+ "location": "import",
811
+ "path": "../../utils/cell-renderer",
812
+ "id": "src/utils/cell-renderer.ts::ColumnFilter",
813
+ "referenceLocation": "ColumnFilter"
814
+ }
815
+ }
816
+ }
494
817
  }];
495
818
  }
496
819
  static get methods() {
@@ -541,11 +864,17 @@ export class MrdTable {
541
864
  "return": "Promise<void>"
542
865
  },
543
866
  "docs": {
544
- "text": "Inject the rows for a given page (0-based).\nCreates a new Map reference so Stencil detects the state change.",
867
+ "text": "Inject the rows for a given page (0-based).\nCreates a new Map reference so Stencil detects the state change.\n\nWhen the page contains fewer rows than pageSize it is the last page.\nrenderEnd is clamped immediately so no loading-placeholder rows appear\nbeyond the actual data \u2014 without requiring the host to update totalElements.",
545
868
  "tags": []
546
869
  }
547
870
  }
548
871
  };
549
872
  }
550
873
  static get elementRef() { return "el"; }
874
+ static get watchers() {
875
+ return [{
876
+ "propName": "totalElements",
877
+ "methodName": "totalElementsChanged"
878
+ }];
879
+ }
551
880
  }