@mmlogic/components 0.5.6 → 0.5.8
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.
- package/dist/cjs/{cell-renderer-Coy-aD3k.js → cell-renderer-pwUGEDql.js} +8 -24
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/mosterdcomponents.cjs.js +1 -1
- package/dist/cjs/mrd-boolean-field_25.cjs.entry.js +195 -52
- package/dist/cjs/mrd-merge-view.cjs.entry.js +1 -1
- package/dist/collection/components/table/mrd-table/mrd-table.css +52 -22
- package/dist/collection/components/table/mrd-table/mrd-table.js +180 -45
- package/dist/collection/components/table/mrd-table/table-parts.js +6 -3
- package/dist/collection/components/table/mrd-table/virtual-scroll.js +8 -3
- package/dist/collection/utils/i18n.js +8 -24
- package/dist/components/i18n.js +1 -1
- package/dist/components/mrd-table2.js +1 -1
- package/dist/esm/{cell-renderer-Dd_PKH0_.js → cell-renderer-B8rJpnq8.js} +8 -24
- package/dist/esm/loader.js +1 -1
- package/dist/esm/mosterdcomponents.js +1 -1
- package/dist/esm/mrd-boolean-field_25.entry.js +195 -52
- package/dist/esm/mrd-merge-view.entry.js +1 -1
- package/dist/mosterdcomponents/cell-renderer-B8u5BCK-.js.map +1 -0
- package/dist/mosterdcomponents/client-layout-Bc42CfJB.js.map +1 -0
- package/dist/mosterdcomponents/document-attachments-BufPmMRv.js.map +1 -0
- package/dist/mosterdcomponents/field-helpers-CUJsHr1V.js.map +1 -0
- package/dist/mosterdcomponents/file-upload-common-DdTDuxEU.js.map +1 -0
- package/dist/mosterdcomponents/format-gcecItcD.js.map +1 -0
- package/dist/mosterdcomponents/i18n-Ba7_jO9I.js.map +1 -0
- package/dist/mosterdcomponents/index-1ayBojyN.js.map +1 -0
- package/dist/mosterdcomponents/index-DhffneUF.js.map +1 -0
- package/dist/mosterdcomponents/index.esm.js.map +1 -0
- package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
- package/dist/mosterdcomponents/mosterdcomponents.esm.js.map +1 -0
- package/dist/mosterdcomponents/{p-53b00783.entry.js → p-6f6c8e53.entry.js} +2 -2
- package/dist/mosterdcomponents/{p-1fc897f4.entry.js → p-e540de6a.entry.js} +1 -1
- package/dist/mosterdcomponents/p-rTM1tU_M.js +1 -0
- package/dist/mosterdcomponents/purify.es-_duHfPgC.js.map +1 -0
- package/dist/mosterdcomponents/query-params-BZbQoeYF.js.map +1 -0
- package/dist/mosterdcomponents/quill-C9pgw_k-.js.map +1 -0
- package/dist/mosterdcomponents/validation-CjjTt66G.js.map +1 -0
- package/dist/types/components/table/mrd-table/mrd-table.d.ts +53 -5
- package/dist/types/components/table/mrd-table/table-parts.d.ts +0 -2
- package/dist/types/components/table/mrd-table/virtual-scroll.d.ts +6 -1
- package/package.json +1 -1
- package/dist/mosterdcomponents/p-CGm4ff1O.js +0 -1
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
color: var(--mrd-color-neutral-600);
|
|
35
35
|
font-weight: var(--mrd-font-weight-medium);
|
|
36
36
|
white-space: nowrap;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
text-overflow: ellipsis;
|
|
37
39
|
font-size: var(--mrd-font-size-xs);
|
|
38
40
|
text-transform: uppercase;
|
|
39
41
|
letter-spacing: 0.04em;
|
|
@@ -57,6 +59,41 @@
|
|
|
57
59
|
.mrd-table__header-label {
|
|
58
60
|
margin-right: var(--mrd-space-1);
|
|
59
61
|
}
|
|
62
|
+
.mrd-table__col-resizer {
|
|
63
|
+
position: absolute;
|
|
64
|
+
top: 0;
|
|
65
|
+
right: 0;
|
|
66
|
+
width: 9px;
|
|
67
|
+
height: 100%;
|
|
68
|
+
cursor: col-resize;
|
|
69
|
+
touch-action: none;
|
|
70
|
+
user-select: none;
|
|
71
|
+
}
|
|
72
|
+
.mrd-table__col-resizer::after {
|
|
73
|
+
content: "";
|
|
74
|
+
position: absolute;
|
|
75
|
+
top: 25%;
|
|
76
|
+
right: 4px;
|
|
77
|
+
width: 1px;
|
|
78
|
+
height: 50%;
|
|
79
|
+
background: var(--mrd-border-color);
|
|
80
|
+
}
|
|
81
|
+
.mrd-table__col-resizer:hover::after {
|
|
82
|
+
width: 2px;
|
|
83
|
+
right: 3px;
|
|
84
|
+
background: var(--mrd-color-primary);
|
|
85
|
+
}
|
|
86
|
+
.mrd-table__col-resizer--active::after {
|
|
87
|
+
width: 2px;
|
|
88
|
+
right: 3px;
|
|
89
|
+
height: 100%;
|
|
90
|
+
top: 0;
|
|
91
|
+
background: var(--mrd-color-primary);
|
|
92
|
+
}
|
|
93
|
+
.mrd-table--resizing {
|
|
94
|
+
cursor: col-resize;
|
|
95
|
+
user-select: none;
|
|
96
|
+
}
|
|
60
97
|
.mrd-table__sort-icon {
|
|
61
98
|
font-size: 0.85rem;
|
|
62
99
|
opacity: 0.4;
|
|
@@ -99,6 +136,8 @@
|
|
|
99
136
|
padding: var(--mrd-space-2) var(--mrd-space-4);
|
|
100
137
|
vertical-align: top;
|
|
101
138
|
white-space: nowrap;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
text-overflow: ellipsis;
|
|
102
141
|
}
|
|
103
142
|
.mrd-table__cell--numeric {
|
|
104
143
|
text-align: right;
|
|
@@ -313,31 +352,20 @@
|
|
|
313
352
|
background: var(--mrd-color-neutral-50);
|
|
314
353
|
color: var(--mrd-color-neutral-900);
|
|
315
354
|
}
|
|
316
|
-
.mrd-table__filter-
|
|
317
|
-
|
|
355
|
+
.mrd-table__filter-clear {
|
|
356
|
+
width: auto;
|
|
357
|
+
gap: var(--mrd-space-1);
|
|
358
|
+
padding: 0 var(--mrd-space-2);
|
|
359
|
+
color: var(--mrd-color-primary);
|
|
318
360
|
border-color: var(--mrd-color-primary);
|
|
319
|
-
color: var(--mrd-color-white);
|
|
320
361
|
}
|
|
321
|
-
.mrd-table__filter-
|
|
322
|
-
|
|
323
|
-
border-color: var(--mrd-color-primary
|
|
324
|
-
color: var(--mrd-color-white);
|
|
362
|
+
.mrd-table__filter-clear:hover {
|
|
363
|
+
color: var(--mrd-color-primary);
|
|
364
|
+
border-color: var(--mrd-color-primary);
|
|
325
365
|
}
|
|
326
|
-
.mrd-table__filter-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
right: -6px;
|
|
330
|
-
min-width: 1.25rem;
|
|
331
|
-
height: 1.25rem;
|
|
332
|
-
padding: 0 3px;
|
|
333
|
-
background: var(--mrd-color-error, #e53e3e);
|
|
334
|
-
color: var(--mrd-color-white);
|
|
335
|
-
border-radius: 9999px;
|
|
336
|
-
font-size: 0.65rem;
|
|
337
|
-
font-weight: var(--mrd-font-weight-medium);
|
|
338
|
-
line-height: 1.25rem;
|
|
339
|
-
text-align: center;
|
|
340
|
-
pointer-events: none;
|
|
366
|
+
.mrd-table__filter-clear-label {
|
|
367
|
+
font-size: var(--mrd-font-size-sm);
|
|
368
|
+
white-space: nowrap;
|
|
341
369
|
}
|
|
342
370
|
.mrd-table__filter-popup {
|
|
343
371
|
position: fixed;
|
|
@@ -550,6 +578,8 @@
|
|
|
550
578
|
font-weight: var(--mrd-font-weight-medium);
|
|
551
579
|
font-variant-numeric: tabular-nums;
|
|
552
580
|
white-space: nowrap;
|
|
581
|
+
overflow: hidden;
|
|
582
|
+
text-overflow: ellipsis;
|
|
553
583
|
border-top: 2px solid var(--mrd-border-color);
|
|
554
584
|
}
|
|
555
585
|
.mrd-table__totals-cell--numeric {
|
|
@@ -9,6 +9,10 @@ import { REQUEST_DEBOUNCE_MS, REQUEST_TIMEOUT_MS, MAX_RETRY_ATTEMPTS, visibleCou
|
|
|
9
9
|
const STALE_CHECK_INTERVAL_MS = 2000;
|
|
10
10
|
/** Breedte van de filterpopup in px — voor overflow-correctie. */
|
|
11
11
|
const POPUP_WIDTH = 280;
|
|
12
|
+
/** Smallest column width (px) a user can drag a column down to. */
|
|
13
|
+
const MIN_COL_WIDTH = 48;
|
|
14
|
+
/** Upper bound (px) for a double-click auto-fit, so one huge value can't blow up the layout. */
|
|
15
|
+
const MAX_AUTOFIT_WIDTH = 600;
|
|
12
16
|
export class MrdTable {
|
|
13
17
|
constructor() {
|
|
14
18
|
// ── Non-state internals ────────────────────────────────────────────────────
|
|
@@ -25,6 +29,13 @@ export class MrdTable {
|
|
|
25
29
|
this.viewPopoverClickHandler = null;
|
|
26
30
|
/** Guards against scheduling more than one column-width measurement per freeze. */
|
|
27
31
|
this.colWidthMeasureScheduled = false;
|
|
32
|
+
/** Drag origin of the running column resize (null = no drag in progress). */
|
|
33
|
+
this.resizeOrigin = null;
|
|
34
|
+
/** True once the user has dragged (or auto-fitted) a column — see colWidthsUserSet usages. */
|
|
35
|
+
this.colWidthsUserSet = false;
|
|
36
|
+
/** Set once the host drives this table through init()/setPage(). Never cleared: it has to
|
|
37
|
+
* survive resetPages(), which empties loadedPages — see the isPaginated getter. */
|
|
38
|
+
this.paginationActivated = false;
|
|
28
39
|
// ── Props ──────────────────────────────────────────────────────────────────
|
|
29
40
|
/** The VIEW or RELATED_VIEW layout item. Contains view config, dataClass, fromClass, actions etc. */
|
|
30
41
|
this.item = null;
|
|
@@ -54,10 +65,10 @@ export class MrdTable {
|
|
|
54
65
|
this.renderStart = 0;
|
|
55
66
|
this.renderEnd = 0;
|
|
56
67
|
this.colWidths = [];
|
|
68
|
+
/** Index of the column currently being dragged by its resize handle (null = not resizing). */
|
|
69
|
+
this.resizingCol = null;
|
|
57
70
|
this.sortField = '';
|
|
58
71
|
this.sortDir = 'asc';
|
|
59
|
-
/** Whether the filter UI is visible on column headers. */
|
|
60
|
-
this.filterMode = false;
|
|
61
72
|
/** Active filters keyed by field name. */
|
|
62
73
|
this.activeFilters = new Map();
|
|
63
74
|
/** Field name of the currently open filter popup (null = closed). */
|
|
@@ -94,15 +105,24 @@ export class MrdTable {
|
|
|
94
105
|
};
|
|
95
106
|
}
|
|
96
107
|
// ── Prop watchers ─────────────────────────────────────────────────────────
|
|
97
|
-
/** Clamp renderEnd when totalElements shrinks (e.g. after a filter is applied).
|
|
108
|
+
/** Clamp renderEnd when totalElements shrinks (e.g. after a filter is applied).
|
|
109
|
+
* 0 means "unknown" (hosts that rely on minKnownTotal never set it, and some reset it
|
|
110
|
+
* while a filter is being applied) — clamping to it would collapse the window to a
|
|
111
|
+
* single row, so leave the window alone and let the render pass clamp it. */
|
|
98
112
|
totalElementsChanged(newVal) {
|
|
99
|
-
|
|
113
|
+
if (newVal > 0)
|
|
114
|
+
this.renderEnd = Math.min(this.renderEnd, newVal - 1);
|
|
100
115
|
}
|
|
101
116
|
/** Reset to primary view when the item prop is replaced from outside. */
|
|
102
117
|
itemChanged(newVal) {
|
|
103
118
|
var _a, _b;
|
|
104
119
|
this.activeViewIdx = 0;
|
|
105
120
|
this.applyDefaultSort((_b = (_a = newVal === null || newVal === void 0 ? void 0 : newVal.view) === null || _a === void 0 ? void 0 : _a.defaultSort) !== null && _b !== void 0 ? _b : '');
|
|
121
|
+
// The columns getter derives from item/activeViewIdx: locked widths belong to the old
|
|
122
|
+
// column set and would otherwise be applied by index to the new one.
|
|
123
|
+
this.colWidths = [];
|
|
124
|
+
this.colWidthMeasureScheduled = false;
|
|
125
|
+
this.colWidthsUserSet = false;
|
|
106
126
|
}
|
|
107
127
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
108
128
|
componentWillLoad() {
|
|
@@ -137,6 +157,7 @@ export class MrdTable {
|
|
|
137
157
|
this.retryAttempts.clear();
|
|
138
158
|
this.colWidths = [];
|
|
139
159
|
this.colWidthMeasureScheduled = false;
|
|
160
|
+
this.colWidthsUserSet = false;
|
|
140
161
|
this.scrollTop = 0;
|
|
141
162
|
this.renderStart = 0;
|
|
142
163
|
// Always fill the visible viewport on init — totalElements may be stale from a
|
|
@@ -149,6 +170,7 @@ export class MrdTable {
|
|
|
149
170
|
this.aggregationsTotal = null;
|
|
150
171
|
this.aggregationsPending = true;
|
|
151
172
|
this.minKnownTotal = 0;
|
|
173
|
+
this.paginationActivated = true;
|
|
152
174
|
// Always request page 0 — totalElements may be unknown (0) on first load.
|
|
153
175
|
this.emitLoadPage(0);
|
|
154
176
|
this.requestedPages = new Set([0]);
|
|
@@ -165,6 +187,7 @@ export class MrdTable {
|
|
|
165
187
|
* detection even when rows.length === pageSize (exact multiple of page size).
|
|
166
188
|
*/
|
|
167
189
|
async setPage(pageNumber, rows, hasNext) {
|
|
190
|
+
this.paginationActivated = true;
|
|
168
191
|
const isLastPage = hasNext !== undefined ? !hasNext : rows.length < this.pageSize;
|
|
169
192
|
if (isLastPage) {
|
|
170
193
|
// lastRowIdx is -1 when the page is empty; clamp renderEnd to -1 so the
|
|
@@ -181,6 +204,13 @@ export class MrdTable {
|
|
|
181
204
|
const next = new Map(this.loadedPages);
|
|
182
205
|
next.set(pageNumber, rows);
|
|
183
206
|
this.loadedPages = next;
|
|
207
|
+
// Repair a window that renders nothing. renderEnd is only ever clamped *down* (here and
|
|
208
|
+
// in totalElementsChanged), so a window computed while the total was still unknown stays
|
|
209
|
+
// collapsed and the rows sit in loadedPages without ever reaching the screen until the
|
|
210
|
+
// user scrolls. Stays collapsed when the data really is empty (minKnownTotal 0).
|
|
211
|
+
if (this.renderEnd < this.renderStart) {
|
|
212
|
+
this.renderEnd = Math.min(this.renderStart + this.visibleCount() - 1, this.minKnownTotal - 1);
|
|
213
|
+
}
|
|
184
214
|
this.requestedAt.delete(pageNumber);
|
|
185
215
|
this.retryAttempts.delete(pageNumber);
|
|
186
216
|
if (this.failedPages.has(pageNumber)) {
|
|
@@ -231,23 +261,123 @@ export class MrdTable {
|
|
|
231
261
|
// re-render *during* the render cycle if done synchronously here. Defer it to a
|
|
232
262
|
// writeTask so the state mutation happens outside the render pass (avoids the
|
|
233
263
|
// Stencil "state/prop changed during rendering" warning and potential loops).
|
|
264
|
+
// NB: the freeze must not depend on the totalElements prop — hosts may omit it
|
|
265
|
+
// and rely on minKnownTotal, in which case the widths would never be locked and
|
|
266
|
+
// every incoming page would re-flow all columns.
|
|
234
267
|
if (this.colWidths.length === 0 &&
|
|
235
268
|
!this.colWidthMeasureScheduled &&
|
|
236
|
-
this.loadedPages.size > 0
|
|
237
|
-
this.totalElements > 0) {
|
|
269
|
+
this.loadedPages.size > 0) {
|
|
238
270
|
this.colWidthMeasureScheduled = true;
|
|
239
271
|
writeTask(() => {
|
|
240
272
|
this.colWidthMeasureScheduled = false;
|
|
241
273
|
// Re-check: state may have changed (e.g. a reset) between scheduling and running.
|
|
242
274
|
if (this.colWidths.length !== 0)
|
|
243
275
|
return;
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
this.colWidths =
|
|
247
|
-
}
|
|
276
|
+
const measured = this.measureColWidths();
|
|
277
|
+
if (measured)
|
|
278
|
+
this.colWidths = measured;
|
|
248
279
|
});
|
|
249
280
|
}
|
|
250
281
|
}
|
|
282
|
+
// ── Column resizing (drag the header border, Excel-style) ──────────────────
|
|
283
|
+
/** Current rendered header widths, or null when they can't be trusted (header not laid out yet). */
|
|
284
|
+
measureColWidths() {
|
|
285
|
+
const ths = this.el.querySelectorAll('.mrd-table__header');
|
|
286
|
+
if (ths.length === 0 || ths.length !== this.columns.length)
|
|
287
|
+
return null;
|
|
288
|
+
return Array.from(ths).map(th => Math.round(th.getBoundingClientRect().width));
|
|
289
|
+
}
|
|
290
|
+
/** Re-reads the rendered header widths into colWidths so a drag starts from what is
|
|
291
|
+
* actually on screen — and locks the columns if they weren't locked yet.
|
|
292
|
+
*
|
|
293
|
+
* A table narrower than its container is stretched by `minWidth: 100%`, so the rendered
|
|
294
|
+
* columns are wider than the auto-measured values held in colWidths. Dragging from those
|
|
295
|
+
* stale values moved the grabbed column to the wrong width, and dropping the stretch (which
|
|
296
|
+
* a hand-set width does) then re-laid out every other column at once. */
|
|
297
|
+
syncColWidths() {
|
|
298
|
+
const measured = this.measureColWidths();
|
|
299
|
+
if (measured) {
|
|
300
|
+
this.colWidths = measured;
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return this.colWidths.length === this.columns.length;
|
|
304
|
+
}
|
|
305
|
+
setColWidth(idx, width) {
|
|
306
|
+
const next = [...this.colWidths];
|
|
307
|
+
next[idx] = Math.max(MIN_COL_WIDTH, Math.round(width));
|
|
308
|
+
this.colWidths = next;
|
|
309
|
+
this.colWidthsUserSet = true;
|
|
310
|
+
}
|
|
311
|
+
handleResizeStart(e, idx) {
|
|
312
|
+
var _a, _b;
|
|
313
|
+
// Stop the header's own click/sort handling; the drag owns this gesture.
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
e.stopPropagation();
|
|
316
|
+
if (!this.syncColWidths())
|
|
317
|
+
return;
|
|
318
|
+
this.resizeOrigin = { idx, startX: e.clientX, startWidth: this.colWidths[idx] };
|
|
319
|
+
this.resizingCol = idx;
|
|
320
|
+
// Pointer capture keeps move/up events on the handle even when the cursor runs
|
|
321
|
+
// ahead of it — no document-level listeners to register and clean up.
|
|
322
|
+
(_b = (_a = e.currentTarget).setPointerCapture) === null || _b === void 0 ? void 0 : _b.call(_a, e.pointerId);
|
|
323
|
+
}
|
|
324
|
+
handleResizeMove(e) {
|
|
325
|
+
if (!this.resizeOrigin)
|
|
326
|
+
return;
|
|
327
|
+
const { idx, startX, startWidth } = this.resizeOrigin;
|
|
328
|
+
this.setColWidth(idx, startWidth + (e.clientX - startX));
|
|
329
|
+
}
|
|
330
|
+
handleResizeEnd(e) {
|
|
331
|
+
var _a, _b;
|
|
332
|
+
if (!this.resizeOrigin)
|
|
333
|
+
return;
|
|
334
|
+
this.resizeOrigin = null;
|
|
335
|
+
this.resizingCol = null;
|
|
336
|
+
(_b = (_a = e.currentTarget).releasePointerCapture) === null || _b === void 0 ? void 0 : _b.call(_a, e.pointerId);
|
|
337
|
+
}
|
|
338
|
+
/** Double-click on the handle: widen the column to fit its widest *rendered* cell.
|
|
339
|
+
* Only rows currently in the virtual-scroll window are measured — off-screen pages
|
|
340
|
+
* aren't in the DOM at all, so this is "fit what you can see", not "fit all data". */
|
|
341
|
+
handleResizeAutoFit(e, idx) {
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
e.stopPropagation();
|
|
344
|
+
if (!this.syncColWidths())
|
|
345
|
+
return;
|
|
346
|
+
// .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
|
|
347
|
+
// colSpan cell (without that class) which would otherwise measure as the full table width.
|
|
348
|
+
const cells = this.el.querySelectorAll(`.mrd-table__header:nth-child(${idx + 1}), .mrd-table__cell:nth-child(${idx + 1})`);
|
|
349
|
+
let widest = 0;
|
|
350
|
+
cells.forEach(cell => {
|
|
351
|
+
// scrollWidth is the untruncated content width; cells clip with overflow: hidden.
|
|
352
|
+
const style = getComputedStyle(cell);
|
|
353
|
+
const padding = parseFloat(style.paddingLeft || '0') + parseFloat(style.paddingRight || '0');
|
|
354
|
+
widest = Math.max(widest, cell.scrollWidth + padding);
|
|
355
|
+
});
|
|
356
|
+
if (widest > 0)
|
|
357
|
+
this.setColWidth(idx, Math.min(widest, MAX_AUTOFIT_WIDTH));
|
|
358
|
+
}
|
|
359
|
+
/** Inline table style once the column widths are locked, undefined while still auto-layout.
|
|
360
|
+
* Once locked the table gets an explicit pixel width (the sum of the columns) instead of
|
|
361
|
+
* the stylesheet's `min-width: max-content`. max-content is re-derived from the cell
|
|
362
|
+
* contents, so every page that scrolled in with a longer value widened the table and
|
|
363
|
+
* re-proportioned all columns — the jitter this replaces.
|
|
364
|
+
* `minWidth: 100%` keeps an auto-measured table stretched to its container, but is dropped
|
|
365
|
+
* once the user resizes by hand: with it, the space freed by narrowing a column is
|
|
366
|
+
* redistributed over all columns, so a drag would partly spring back. */
|
|
367
|
+
lockedTableStyle() {
|
|
368
|
+
if (this.colWidths.length === 0)
|
|
369
|
+
return undefined;
|
|
370
|
+
const total = this.colWidths.reduce((sum, w) => sum + (w || 0), 0);
|
|
371
|
+
return Object.assign({ tableLayout: 'fixed', width: `${total}px` }, (this.colWidthsUserSet ? {} : { minWidth: '100%' }));
|
|
372
|
+
}
|
|
373
|
+
/** The resize handle rendered in the right edge of every header cell. */
|
|
374
|
+
renderColResizer(idx) {
|
|
375
|
+
const cls = 'mrd-table__col-resizer' + (this.resizingCol === idx ? ' mrd-table__col-resizer--active' : '');
|
|
376
|
+
return (h("span", { class: cls, role: "separator", "aria-orientation": "vertical", title: t('column_resize', this.locale), "on-pointerdown": (e) => this.handleResizeStart(e, idx), "on-pointermove": (e) => this.handleResizeMove(e), "on-pointerup": (e) => this.handleResizeEnd(e), "on-pointercancel": (e) => this.handleResizeEnd(e), "on-dblclick": (e) => this.handleResizeAutoFit(e, idx),
|
|
377
|
+
// The click that follows the drag targets this handle; swallow it so it never
|
|
378
|
+
// bubbles to the header and triggers a sort or the filter popup.
|
|
379
|
+
onClick: (e) => e.stopPropagation() }));
|
|
380
|
+
}
|
|
251
381
|
// ── Paging / scroll helpers ────────────────────────────────────────────────
|
|
252
382
|
visibleCount() {
|
|
253
383
|
return visibleCount(this.tableHeight, this.rowHeight);
|
|
@@ -331,6 +461,17 @@ export class MrdTable {
|
|
|
331
461
|
activeFilters: this.activeFilters.values(),
|
|
332
462
|
});
|
|
333
463
|
}
|
|
464
|
+
/** True when the table is in paginated / virtual-scroll mode rather than driven by the
|
|
465
|
+
* rows prop. Mirrors the branch taken in render().
|
|
466
|
+
*
|
|
467
|
+
* The filter handlers gate their reload on this. They used to test minKnownTotal, which is
|
|
468
|
+
* 0 after a filter returned no rows at all — so clearing that filter reloaded nothing and
|
|
469
|
+
* the table stayed empty. Hosts that omit totalElements have the same problem, hence the
|
|
470
|
+
* sticky flag rather than loadedPages.size: resetPages() clears the loaded pages, and the
|
|
471
|
+
* mode must not flip back to the rows prop (and its "no results") while page 0 is in flight. */
|
|
472
|
+
get isPaginated() {
|
|
473
|
+
return this.totalElements > 0 || this.paginationActivated;
|
|
474
|
+
}
|
|
334
475
|
get columns() {
|
|
335
476
|
var _a, _b, _c;
|
|
336
477
|
return ((_c = (_b = (_a = this.allViews[this.activeViewIdx]) === null || _a === void 0 ? void 0 : _a.view) === null || _b === void 0 ? void 0 : _b.values) !== null && _c !== void 0 ? _c : []);
|
|
@@ -412,8 +553,13 @@ export class MrdTable {
|
|
|
412
553
|
this.failedPages = new Set();
|
|
413
554
|
this.requestedAt.clear();
|
|
414
555
|
this.retryAttempts.clear();
|
|
415
|
-
|
|
416
|
-
|
|
556
|
+
// A sort or filter change keeps the same columns, so hand-set widths survive it —
|
|
557
|
+
// re-measuring would throw away what the user just dragged. Auto-measured widths are
|
|
558
|
+
// dropped so they can be re-derived from the new first page.
|
|
559
|
+
if (!this.colWidthsUserSet) {
|
|
560
|
+
this.colWidths = [];
|
|
561
|
+
this.colWidthMeasureScheduled = false;
|
|
562
|
+
}
|
|
417
563
|
this.scrollTop = 0;
|
|
418
564
|
this.renderStart = 0;
|
|
419
565
|
this.minKnownTotal = 0;
|
|
@@ -424,18 +570,8 @@ export class MrdTable {
|
|
|
424
570
|
if (scroller)
|
|
425
571
|
scroller.scrollTop = 0;
|
|
426
572
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (this.sortField === name) {
|
|
430
|
-
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
this.sortField = name;
|
|
434
|
-
this.sortDir = 'asc';
|
|
435
|
-
}
|
|
436
|
-
this.resetPages();
|
|
437
|
-
this.emitPagesForWindow(this.renderStart, this.renderEnd);
|
|
438
|
-
}
|
|
573
|
+
/** Sorting runs through the column popup's sort section (applySort); there is no
|
|
574
|
+
* separate click-to-sort mode any more. */
|
|
439
575
|
applySort(col, dir) {
|
|
440
576
|
this.sortField = this.colName(col);
|
|
441
577
|
this.sortDir = dir;
|
|
@@ -541,11 +677,6 @@ export class MrdTable {
|
|
|
541
677
|
this.requestedPages = requested;
|
|
542
678
|
}
|
|
543
679
|
// ── Filter helpers ─────────────────────────────────────────────────────────
|
|
544
|
-
handleFilterToggle() {
|
|
545
|
-
this.filterMode = !this.filterMode;
|
|
546
|
-
if (!this.filterMode)
|
|
547
|
-
this.closeFilterPopup();
|
|
548
|
-
}
|
|
549
680
|
handleFilterOpen(col, e) {
|
|
550
681
|
e.stopPropagation();
|
|
551
682
|
const btn = e.currentTarget;
|
|
@@ -652,7 +783,7 @@ export class MrdTable {
|
|
|
652
783
|
this.aggregations = null;
|
|
653
784
|
this.aggregationsTotal = null;
|
|
654
785
|
this.aggregationsPending = true;
|
|
655
|
-
if (this.
|
|
786
|
+
if (this.isPaginated) {
|
|
656
787
|
this.resetPages();
|
|
657
788
|
this.emitPagesForWindow(this.renderStart, this.renderEnd);
|
|
658
789
|
}
|
|
@@ -667,7 +798,7 @@ export class MrdTable {
|
|
|
667
798
|
this.aggregations = null;
|
|
668
799
|
this.aggregationsTotal = null;
|
|
669
800
|
this.aggregationsPending = true;
|
|
670
|
-
if (this.
|
|
801
|
+
if (this.isPaginated) {
|
|
671
802
|
this.resetPages();
|
|
672
803
|
this.emitPagesForWindow(this.renderStart, this.renderEnd);
|
|
673
804
|
}
|
|
@@ -677,7 +808,7 @@ export class MrdTable {
|
|
|
677
808
|
this.aggregations = null;
|
|
678
809
|
this.aggregationsTotal = null;
|
|
679
810
|
this.aggregationsPending = true;
|
|
680
|
-
if (this.
|
|
811
|
+
if (this.isPaginated) {
|
|
681
812
|
this.resetPages();
|
|
682
813
|
this.emitPagesForWindow(this.renderStart, this.renderEnd);
|
|
683
814
|
}
|
|
@@ -713,8 +844,7 @@ export class MrdTable {
|
|
|
713
844
|
// ── Render: toolbar ────────────────────────────────────────────────────────
|
|
714
845
|
renderToolbar() {
|
|
715
846
|
var _a;
|
|
716
|
-
return (h(Toolbar, { locale: this.locale,
|
|
717
|
-
onFilterToggle: () => this.handleFilterToggle(),
|
|
847
|
+
return (h(Toolbar, { locale: this.locale, filterCount: this.activeFilters.size, actions: this.tableActions, views: this.allViews, activeViewIdx: this.activeViewIdx, viewPopoverOpen: this.viewPopoverOpen, createPickerOpen: this.createPickerOpen, createTypes: (_a = this.item) === null || _a === void 0 ? void 0 : _a.createTypes, cb: {
|
|
718
848
|
onClearAllFilters: () => this.clearAllFilters(),
|
|
719
849
|
onViewSwitch: idx => this.handleViewSwitch(idx),
|
|
720
850
|
onToggleViewPopover: e => this.toggleViewPopover(e),
|
|
@@ -747,7 +877,7 @@ export class MrdTable {
|
|
|
747
877
|
// ── Render: footer ────────────────────────────────────────────────────────
|
|
748
878
|
renderFooter(rowCount, effectiveTotal, isTotalKnown = true) {
|
|
749
879
|
// Non-paginated mode: totalElements=0 and no paginated data loaded yet
|
|
750
|
-
if (this.
|
|
880
|
+
if (!this.isPaginated) {
|
|
751
881
|
const count = rowCount !== null && rowCount !== void 0 ? rowCount : 0;
|
|
752
882
|
if (count === 0)
|
|
753
883
|
return null;
|
|
@@ -781,17 +911,18 @@ export class MrdTable {
|
|
|
781
911
|
// ── Non-paginated mode ──────────────────────────────────────────────────
|
|
782
912
|
// Only enter non-paginated mode when totalElements is 0 AND no paginated data
|
|
783
913
|
// has been loaded yet — prevents the wrong branch when the host omits totalElements.
|
|
784
|
-
if (this.
|
|
785
|
-
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 => {
|
|
914
|
+
if (!this.isPaginated) {
|
|
915
|
+
return (h(Host, { class: { 'mrd-table--resizing': this.resizingCol !== null } }, this.renderToolbar(), h("div", { class: "mrd-table" }, h("table", { class: "mrd-table__table", style: this.lockedTableStyle() }, h("thead", null, h("tr", null, this.columns.map((col, idx) => {
|
|
786
916
|
var _a;
|
|
787
917
|
const name = this.colName(col);
|
|
788
918
|
const isFiltered = this.activeFilters.has(name);
|
|
919
|
+
const isInteractive = !NON_INTERACTIVE_TYPES.has(this.colDataType(col));
|
|
789
920
|
const cls = [
|
|
790
921
|
'mrd-table__header',
|
|
791
922
|
isFiltered ? 'mrd-table__header--filtered' : '',
|
|
792
|
-
|
|
923
|
+
isInteractive ? 'mrd-table__header--sortable' : '',
|
|
793
924
|
].filter(Boolean).join(' ');
|
|
794
|
-
return (h("th", { class: cls,
|
|
925
|
+
return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: isInteractive ? (e) => this.handleFilterOpen(col, e) : undefined }, h("span", { class: "mrd-table__header-label" }, (_a = col.label) !== null && _a !== void 0 ? _a : ''), isFiltered && h(FilterIcon, null), this.renderColResizer(idx)));
|
|
795
926
|
}))), 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 => this.renderCell(col, row)))))), this.renderTotalsRow()), (!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(), this.renderTextblockModal(), this.renderJsonModal()));
|
|
796
927
|
}
|
|
797
928
|
// ── Paginated / virtual-scroll mode ────────────────────────────────────
|
|
@@ -805,9 +936,13 @@ export class MrdTable {
|
|
|
805
936
|
const colCount = this.columns.length;
|
|
806
937
|
const topSpacerHeight = this.renderStart * this.rowHeight;
|
|
807
938
|
const bottomSpacerHeight = Math.max(0, (effectiveTotal - 1 - clampedEnd) * this.rowHeight);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
939
|
+
// Once the widths are frozen the table gets an explicit pixel width (the sum of the
|
|
940
|
+
// locked columns) instead of the stylesheet's `min-width: max-content`. max-content is
|
|
941
|
+
// re-derived from the cell contents, so every page that scrolled in with a longer value
|
|
942
|
+
// widened the table and re-proportioned all columns — the jitter this replaces.
|
|
943
|
+
// `minWidth: 100%` keeps a narrow table stretched to the container (and still responsive
|
|
944
|
+
// to container resizes) without ever consulting the content again.
|
|
945
|
+
const tableStyle = this.lockedTableStyle();
|
|
811
946
|
const renderedRows = [];
|
|
812
947
|
for (let i = this.renderStart; i <= clampedEnd; i++) {
|
|
813
948
|
const row = this.getRow(i);
|
|
@@ -832,7 +967,7 @@ export class MrdTable {
|
|
|
832
967
|
}) }, this.columns.map(col => this.renderCell(col, row))));
|
|
833
968
|
}
|
|
834
969
|
}
|
|
835
|
-
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) => {
|
|
970
|
+
return (h(Host, { class: { 'mrd-table--resizing': this.resizingCol !== 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) => {
|
|
836
971
|
var _a;
|
|
837
972
|
const name = this.colName(col);
|
|
838
973
|
const isActive = this.sortField === name;
|
|
@@ -844,7 +979,7 @@ export class MrdTable {
|
|
|
844
979
|
isActive ? `mrd-table__header--sorted-${this.sortDir}` : '',
|
|
845
980
|
isFiltered ? 'mrd-table__header--filtered' : '',
|
|
846
981
|
].filter(Boolean).join(' ');
|
|
847
|
-
return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: isInteractive ? ((e) => this.
|
|
982
|
+
return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: isInteractive ? ((e) => this.handleFilterOpen(col, e)) : undefined }, h("span", { class: "mrd-table__header-label" }, (_a = col.label) !== null && _a !== void 0 ? _a : ''), isInteractive && isActive && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, this.sortDir === 'asc' ? '▲' : '▼')), isInteractive && !isActive && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, "\u21C5")), isInteractive && isFiltered && h(FilterIcon, null), this.renderColResizer(idx)));
|
|
848
983
|
}))), 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 })))), this.renderTotalsRow())), effectiveTotal === 0 && this.loadedPages.has(0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale))), effectiveTotal > 0 && this.renderFooter(undefined, effectiveTotal, this.isTotalKnown()), this.renderFilterPopup(), this.renderTextblockModal(), this.renderJsonModal()));
|
|
849
984
|
}
|
|
850
985
|
renderTextblockModal() {
|
|
@@ -1066,9 +1201,9 @@ export class MrdTable {
|
|
|
1066
1201
|
"renderStart": {},
|
|
1067
1202
|
"renderEnd": {},
|
|
1068
1203
|
"colWidths": {},
|
|
1204
|
+
"resizingCol": {},
|
|
1069
1205
|
"sortField": {},
|
|
1070
1206
|
"sortDir": {},
|
|
1071
|
-
"filterMode": {},
|
|
1072
1207
|
"activeFilters": {},
|
|
1073
1208
|
"openFilterCol": {},
|
|
1074
1209
|
"pendingFilter": {},
|
|
@@ -42,7 +42,10 @@ export const TableCell = ({ col, row, locale, onDownload, onOpenTextblock, onOpe
|
|
|
42
42
|
return (h("td", { class: "mrd-table__cell" }, preview, h("button", { class: "mrd-table__textblock-btn", onClick: (e) => { e.stopPropagation(); onOpenTextblock(full); }, "aria-label": t('textblock_show_more', locale) }, "\u22EF")));
|
|
43
43
|
}
|
|
44
44
|
const value = CellRenderer.render(col, row, locale);
|
|
45
|
-
return (
|
|
45
|
+
return (
|
|
46
|
+
// title: columns are locked to the width measured on page 0, so a longer value on a
|
|
47
|
+
// later page is ellipsised rather than widening the column — the tooltip keeps it readable.
|
|
48
|
+
h("td", { class: `mrd-table__cell${isNumeric ? ' mrd-table__cell--numeric' : ''}`, title: value || undefined }, value));
|
|
46
49
|
};
|
|
47
50
|
/** Formats the aggregation total for one column, '' when not applicable. */
|
|
48
51
|
export function formatAggregationValue(col, aggregations, locale) {
|
|
@@ -84,11 +87,11 @@ export const JsonModal = ({ html, locale, onClose }) => {
|
|
|
84
87
|
return null;
|
|
85
88
|
return (h("div", { class: "mrd-table__modal-backdrop", onClick: onClose, role: "dialog", "aria-modal": "true" }, h("div", { class: "mrd-table__modal", onClick: (e) => e.stopPropagation() }, h("button", { class: "mrd-table__modal-close", onClick: onClose, "aria-label": t('close', locale) }, "\u2715"), h("pre", { class: "mrd-table__modal-json", innerHTML: html }))));
|
|
86
89
|
};
|
|
87
|
-
export const Toolbar = ({ locale,
|
|
90
|
+
export const Toolbar = ({ locale, filterCount, actions, views, activeViewIdx, viewPopoverOpen, createPickerOpen, createTypes, cb }) => {
|
|
88
91
|
var _a;
|
|
89
92
|
const hasActions = actions.length > 0;
|
|
90
93
|
const hasViewSwitcher = views.length > 1;
|
|
91
|
-
return (h("div", { class: "mrd-table__toolbar" }, h("div", { class: "mrd-table__toolbar-left" },
|
|
94
|
+
return (h("div", { class: "mrd-table__toolbar" }, h("div", { class: "mrd-table__toolbar-left" }, filterCount > 0 && (h("button", { class: "mrd-table__action mrd-table__action--secondary mrd-table__filter-clear", onClick: cb.onClearAllFilters }, 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__filter-clear-label" }, t('table_filter_clear_all', locale), " (", filterCount, ")")))), hasViewSwitcher && (h("div", { class: "mrd-table__toolbar-center" }, h("div", { class: "mrd-table__view-switcher" }, h("button", { class: "mrd-table__view-arrow", "aria-label": "Previous view", onClick: () => cb.onViewSwitch((activeViewIdx - 1 + views.length) % views.length) }, "\u25C0"), h("div", { class: "mrd-table__view-title-wrap" }, h("button", { class: "mrd-table__view-title", onClick: (e) => cb.onToggleViewPopover(e) }, (_a = views[activeViewIdx]) === null || _a === void 0 ? void 0 : _a.label), viewPopoverOpen && (h("div", { class: "mrd-table__view-popover", onClick: (e) => e.stopPropagation() }, views.map((v, i) => (h("button", { key: String(i), class: `mrd-table__view-popover-item${i === activeViewIdx ? ' mrd-table__view-popover-item--active' : ''}`, onClick: () => {
|
|
92
95
|
if (i !== activeViewIdx)
|
|
93
96
|
cb.onViewSwitch(i);
|
|
94
97
|
cb.onCloseViewPopover();
|
|
@@ -14,13 +14,18 @@ export const MAX_RETRY_ATTEMPTS = 3;
|
|
|
14
14
|
export function visibleCount(tableHeight, rowHeight) {
|
|
15
15
|
return Math.ceil(tableHeight / rowHeight);
|
|
16
16
|
}
|
|
17
|
-
/** Render window (with buffer) for the given scroll offset.
|
|
17
|
+
/** Render window (with buffer) for the given scroll offset.
|
|
18
|
+
* total <= 0 means "not known yet" (no totalElements prop, no aggregation total and no page
|
|
19
|
+
* loaded — e.g. right after a filter reset), *not* "no rows". Clamping to total - 1 would
|
|
20
|
+
* collapse the window to renderEnd = -1, so nothing renders and no page is ever requested;
|
|
21
|
+
* the window is left unclamped instead and the render pass clamps it against
|
|
22
|
+
* effectiveTotal once real data has landed. */
|
|
18
23
|
export function computeWindow(scrollTop, rowHeight, tableHeight, total) {
|
|
19
24
|
const visStart = Math.floor(scrollTop / rowHeight);
|
|
20
|
-
const
|
|
25
|
+
const unclamped = visStart + visibleCount(tableHeight, rowHeight) + BUFFER;
|
|
21
26
|
return {
|
|
22
27
|
renderStart: Math.max(0, visStart - BUFFER),
|
|
23
|
-
renderEnd: Math.min(total - 1,
|
|
28
|
+
renderEnd: total > 0 ? Math.min(total - 1, unclamped) : unclamped,
|
|
24
29
|
};
|
|
25
30
|
}
|
|
26
31
|
/** First/last page (0-based) covering the given row window. */
|