@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,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,236 @@ 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
+ this.pendingFilter = existing ? Object.assign({}, existing) : { field: name, dataType };
274
+ this.openFilterCol = name;
275
+ // Close on outside click — re-register to replace any stale handler
276
+ if (this.outsideClickHandler)
277
+ document.removeEventListener('click', this.outsideClickHandler);
278
+ this.outsideClickHandler = (ev) => {
279
+ const popup = this.el.querySelector('.mrd-table__filter-popup');
280
+ if (popup && !popup.contains(ev.target))
281
+ this.closeFilterPopup();
282
+ };
283
+ document.addEventListener('click', this.outsideClickHandler);
284
+ }
285
+ closeFilterPopup() {
286
+ this.openFilterCol = null;
287
+ this.pendingFilter = null;
288
+ if (this.outsideClickHandler) {
289
+ document.removeEventListener('click', this.outsideClickHandler);
290
+ this.outsideClickHandler = null;
291
+ }
292
+ }
293
+ setPending(key, val) {
294
+ this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { [key]: val });
295
+ }
296
+ togglePendingValue(key, checked) {
297
+ var _a, _b;
298
+ const current = (_b = (_a = this.pendingFilter) === null || _a === void 0 ? void 0 : _a.values) !== null && _b !== void 0 ? _b : [];
299
+ this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { values: checked ? [...current, key] : current.filter(k => k !== key) });
300
+ }
301
+ filterHasValue(f) {
302
+ if (f.operator === 'isEmpty' || f.operator === 'isNotEmpty')
303
+ return true;
304
+ if (f.values !== undefined && f.values.length > 0)
305
+ return true;
306
+ if (f.value != null && f.value !== '')
307
+ return true;
308
+ if (typeof f.value === 'boolean')
309
+ return true;
310
+ if (f.from != null && f.from !== '')
311
+ return true;
312
+ if (f.to != null && f.to !== '')
313
+ return true;
314
+ return false;
315
+ }
316
+ applyFilter() {
317
+ const f = this.pendingFilter;
318
+ if (!(f === null || f === void 0 ? void 0 : f.field)) {
319
+ this.closeFilterPopup();
320
+ return;
321
+ }
322
+ const next = new Map(this.activeFilters);
323
+ if (this.filterHasValue(f)) {
324
+ next.set(f.field, f);
325
+ }
326
+ else {
327
+ next.delete(f.field);
328
+ }
329
+ this.activeFilters = next;
330
+ this.closeFilterPopup();
331
+ this.mrdFilter.emit({ filters: Array.from(this.activeFilters.values()) });
332
+ if (this.totalElements > 0) {
333
+ this.resetPages();
334
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
335
+ }
336
+ }
337
+ clearFilter() {
338
+ const name = this.openFilterCol;
339
+ const next = new Map(this.activeFilters);
340
+ if (name)
341
+ next.delete(name);
342
+ this.activeFilters = next;
343
+ this.closeFilterPopup();
344
+ this.mrdFilter.emit({ filters: Array.from(this.activeFilters.values()) });
345
+ if (this.totalElements > 0) {
346
+ this.resetPages();
347
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
348
+ }
349
+ }
350
+ clearAllFilters() {
351
+ this.activeFilters = new Map();
352
+ this.mrdFilter.emit({ filters: [] });
353
+ if (this.totalElements > 0) {
354
+ this.resetPages();
355
+ this.emitPagesForWindow(this.renderStart, this.renderEnd);
356
+ }
357
+ }
358
+ // ── Render: toolbar ────────────────────────────────────────────────────────
359
+ renderToolbar() {
360
+ var _a;
361
+ const filterCount = this.activeFilters.size;
362
+ const hasActions = ((_a = this.actions) === null || _a === void 0 ? void 0 : _a.length) > 0;
363
+ 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 => {
364
+ var _a;
365
+ 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
366
+ ? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
367
+ : a.label, h("span", { class: "mrd-table__action-tooltip" }, a.label)));
368
+ })))));
369
+ }
370
+ // ── Render: filter popup ───────────────────────────────────────────────────
371
+ renderFilterEditor(col) {
372
+ var _a, _b, _c, _d, _e, _f, _g;
373
+ const pf = (_a = this.pendingFilter) !== null && _a !== void 0 ? _a : {};
374
+ const dataType = this.colDataType(col);
375
+ if (NO_FILTER_TYPES.has(dataType)) {
376
+ return h("p", { class: "mrd-table__filter-no-support" }, t('filter_no_support', this.locale));
377
+ }
378
+ if (dataType === 'BOOLEAN') {
379
+ return (h("div", { class: "mrd-table__filter-radio-group" }, [
380
+ { labelKey: 'filter_all', value: null },
381
+ { labelKey: 'yes', value: true },
382
+ { labelKey: 'no', value: false },
383
+ ].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))))));
213
384
  }
385
+ if (dataType === 'LIST') {
386
+ const items = (_c = (_b = col.field) === null || _b === void 0 ? void 0 : _b.listItems) !== null && _c !== void 0 ? _c : [];
387
+ const selected = (_d = pf.values) !== null && _d !== void 0 ? _d : [];
388
+ 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)))));
389
+ }
390
+ if (TEXT_TYPES.has(dataType) || dataType === 'RELATION') {
391
+ const op = (_e = pf.operator) !== null && _e !== void 0 ? _e : 'startsWith';
392
+ const noInput = op === 'isEmpty' || op === 'isNotEmpty';
393
+ return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => this.setPending('operator', e.target.value) }, [
394
+ { val: 'startsWith', labelKey: 'filter_starts_with' },
395
+ { val: 'equals', labelKey: 'filter_equals' },
396
+ { val: 'isEmpty', labelKey: 'filter_is_empty' },
397
+ { val: 'isNotEmpty', labelKey: 'filter_is_not_empty' },
398
+ ].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) }))));
399
+ }
400
+ if (NUMERIC_TYPES.has(dataType)) {
401
+ const rangeMode = pf.from !== undefined || pf.to !== undefined;
402
+ 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) })))));
403
+ }
404
+ if (DATE_TYPES.has(dataType)) {
405
+ const inputType = dataType === 'DATE' ? 'date'
406
+ : dataType === 'DATETIME' ? 'datetime-local'
407
+ : 'time';
408
+ const rangeMode = pf.from !== undefined || pf.to !== undefined;
409
+ 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) })))));
410
+ }
411
+ return null;
412
+ }
413
+ renderFilterPopup() {
414
+ var _a, _b, _c, _d;
415
+ if (!this.openFilterCol || !this.pendingFilter)
416
+ return null;
417
+ const col = this.columns.find(c => this.colName(c) === this.openFilterCol);
418
+ if (!col)
419
+ return null;
420
+ 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;
421
+ const sortActive = this.sortField === this.openFilterCol;
422
+ 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)))));
423
+ }
424
+ // ── Render: footer ────────────────────────────────────────────────────────
425
+ renderFooter(rowCount, effectiveTotal) {
426
+ const total = this.totalElements;
427
+ // Non-paginated mode: show plain row count
428
+ if (total === 0) {
429
+ const count = rowCount !== null && rowCount !== void 0 ? rowCount : 0;
430
+ if (count === 0)
431
+ return null;
432
+ return (h("div", { class: "mrd-table__footer" }, count, " ", t('table_of', this.locale), " ", count));
433
+ }
434
+ // Paginated mode: only show once page 0 has loaded (avoids stale total during filter reset)
435
+ if (!this.loadedPages.has(0))
436
+ return null;
437
+ // Use effectiveTotal (derived from actual page lengths) so the counter
438
+ // is correct even when the host has not yet updated totalElements.
439
+ const displayTotal = effectiveTotal !== null && effectiveTotal !== void 0 ? effectiveTotal : total;
440
+ // Compute from/to independently so partial rows at top/bottom are included.
441
+ const from = Math.min(Math.floor(this.scrollTop / this.rowHeight) + 1, displayTotal);
442
+ const to = Math.min(Math.ceil((this.scrollTop + this.tableHeight) / this.rowHeight), displayTotal);
443
+ return (h("div", { class: "mrd-table__footer" }, from, "\u2013", to, " ", t('table_of', this.locale), " ", displayTotal));
214
444
  }
215
445
  // ── Render ─────────────────────────────────────────────────────────────────
216
446
  render() {
217
- var _a, _b;
447
+ var _a, _b, _c;
218
448
  if (!((_a = this.columns) === null || _a === void 0 ? void 0 : _a.length))
219
449
  return null;
220
450
  const numericTypes = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
221
451
  // ── Non-paginated mode ──────────────────────────────────────────────────
222
452
  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 => {
453
+ 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
454
  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 : ''));
455
+ const name = this.colName(col);
456
+ const isFiltered = this.activeFilters.has(name);
457
+ 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
458
  }))), 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
459
  var _a, _b;
228
460
  const value = CellRenderer.render(col, row, this.locale);
229
461
  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
462
  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.")))));
463
+ })))))), (!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
464
  }
233
465
  // ── Paginated / virtual-scroll mode ────────────────────────────────────
234
- const total = this.totalElements;
466
+ // Derive the authoritative row count from loaded pages:
467
+ // if any loaded page is shorter than pageSize it is the last page,
468
+ // so the true total cannot exceed (pageNum * pageSize + pageRows.length).
469
+ // This self-corrects without requiring the host to update totalElements.
470
+ let effectiveTotal = this.totalElements;
471
+ for (const [pageNum, pageRows] of this.loadedPages) {
472
+ if (pageRows.length < this.pageSize) {
473
+ effectiveTotal = Math.min(effectiveTotal, pageNum * this.pageSize + pageRows.length);
474
+ }
475
+ }
476
+ // Clamp renderEnd to what we actually know exists (-1 when empty)
477
+ const clampedEnd = Math.min(this.renderEnd, effectiveTotal - 1);
235
478
  const colCount = this.columns.length;
236
479
  const topSpacerHeight = this.renderStart * this.rowHeight;
237
- const bottomSpacerHeight = Math.max(0, (total - 1 - this.renderEnd) * this.rowHeight);
480
+ const bottomSpacerHeight = Math.max(0, (effectiveTotal - 1 - clampedEnd) * this.rowHeight);
481
+ const tableStyle = this.colWidths.length > 0
482
+ ? { tableLayout: 'fixed' }
483
+ : undefined;
238
484
  const renderedRows = [];
239
- for (let i = this.renderStart; i <= this.renderEnd; i++) {
485
+ for (let i = this.renderStart; i <= clampedEnd; i++) {
240
486
  const row = this.getRow(i);
241
487
  if (row === null) {
242
488
  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 +496,19 @@ export class MrdTable {
250
496
  })));
251
497
  }
252
498
  }
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) => {
499
+ 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
500
  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."))));
501
+ const name = this.colName(col);
502
+ const isActive = this.sortField === name;
503
+ const isFiltered = this.activeFilters.has(name);
504
+ const cls = [
505
+ 'mrd-table__header',
506
+ 'mrd-table__header--sortable',
507
+ isActive ? `mrd-table__header--sorted-${this.sortDir}` : '',
508
+ isFiltered ? 'mrd-table__header--filtered' : '',
509
+ ].filter(Boolean).join(' ');
510
+ 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"))));
511
+ }))), 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
512
  }
263
513
  static get is() { return "mrd-table"; }
264
514
  static get encapsulation() { return "scoped"; }
@@ -441,6 +691,31 @@ export class MrdTable {
441
691
  "reflect": false,
442
692
  "attribute": "default-sort",
443
693
  "defaultValue": "''"
694
+ },
695
+ "actions": {
696
+ "type": "unknown",
697
+ "mutable": false,
698
+ "complexType": {
699
+ "original": "TableAction[]",
700
+ "resolved": "TableAction[]",
701
+ "references": {
702
+ "TableAction": {
703
+ "location": "import",
704
+ "path": "../../utils/cell-renderer",
705
+ "id": "src/utils/cell-renderer.ts::TableAction",
706
+ "referenceLocation": "TableAction"
707
+ }
708
+ }
709
+ },
710
+ "required": false,
711
+ "optional": false,
712
+ "docs": {
713
+ "tags": [],
714
+ "text": "Toolbar action buttons rendered above the table."
715
+ },
716
+ "getter": false,
717
+ "setter": false,
718
+ "defaultValue": "[]"
444
719
  }
445
720
  };
446
721
  }
@@ -452,7 +727,13 @@ export class MrdTable {
452
727
  "renderEnd": {},
453
728
  "colWidths": {},
454
729
  "sortField": {},
455
- "sortDir": {}
730
+ "sortDir": {},
731
+ "filterMode": {},
732
+ "activeFilters": {},
733
+ "openFilterCol": {},
734
+ "pendingFilter": {},
735
+ "popupPos": {},
736
+ "scrollTop": {}
456
737
  };
457
738
  }
458
739
  static get events() {
@@ -491,6 +772,43 @@ export class MrdTable {
491
772
  }
492
773
  }
493
774
  }
775
+ }, {
776
+ "method": "mrdAction",
777
+ "name": "mrdAction",
778
+ "bubbles": true,
779
+ "cancelable": true,
780
+ "composed": true,
781
+ "docs": {
782
+ "tags": [],
783
+ "text": "Fired when a toolbar action button is clicked. Detail contains the action identifier."
784
+ },
785
+ "complexType": {
786
+ "original": "{ action: string }",
787
+ "resolved": "{ action: string; }",
788
+ "references": {}
789
+ }
790
+ }, {
791
+ "method": "mrdFilter",
792
+ "name": "mrdFilter",
793
+ "bubbles": true,
794
+ "cancelable": true,
795
+ "composed": true,
796
+ "docs": {
797
+ "tags": [],
798
+ "text": "Fired when active filters change. Host translates filters to API query params."
799
+ },
800
+ "complexType": {
801
+ "original": "{ filters: ColumnFilter[] }",
802
+ "resolved": "{ filters: ColumnFilter[]; }",
803
+ "references": {
804
+ "ColumnFilter": {
805
+ "location": "import",
806
+ "path": "../../utils/cell-renderer",
807
+ "id": "src/utils/cell-renderer.ts::ColumnFilter",
808
+ "referenceLocation": "ColumnFilter"
809
+ }
810
+ }
811
+ }
494
812
  }];
495
813
  }
496
814
  static get methods() {
@@ -541,11 +859,17 @@ export class MrdTable {
541
859
  "return": "Promise<void>"
542
860
  },
543
861
  "docs": {
544
- "text": "Inject the rows for a given page (0-based).\nCreates a new Map reference so Stencil detects the state change.",
862
+ "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
863
  "tags": []
546
864
  }
547
865
  }
548
866
  };
549
867
  }
550
868
  static get elementRef() { return "el"; }
869
+ static get watchers() {
870
+ return [{
871
+ "propName": "totalElements",
872
+ "methodName": "totalElementsChanged"
873
+ }];
874
+ }
551
875
  }