@mk-kit/ui 0.34.0
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/LICENSE +21 -0
- package/README.md +115 -0
- package/block-editor/README.md +254 -0
- package/fesm2022/mk-kit-ui-block-editor.mjs +2158 -0
- package/fesm2022/mk-kit-ui-block-editor.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-button.mjs +81 -0
- package/fesm2022/mk-kit-ui-button.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-checkbox.mjs +136 -0
- package/fesm2022/mk-kit-ui-checkbox.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-chip.mjs +122 -0
- package/fesm2022/mk-kit-ui-chip.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-context-menu.mjs +144 -0
- package/fesm2022/mk-kit-ui-context-menu.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-core.mjs +1576 -0
- package/fesm2022/mk-kit-ui-core.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-data.mjs +6055 -0
- package/fesm2022/mk-kit-ui-data.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-datetime.mjs +3409 -0
- package/fesm2022/mk-kit-ui-datetime.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-directives.mjs +1779 -0
- package/fesm2022/mk-kit-ui-directives.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-dnd.mjs +1073 -0
- package/fesm2022/mk-kit-ui-dnd.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-feedback.mjs +2426 -0
- package/fesm2022/mk-kit-ui-feedback.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-forms.mjs +9208 -0
- package/fesm2022/mk-kit-ui-forms.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-icon.mjs +470 -0
- package/fesm2022/mk-kit-ui-icon.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-media.mjs +896 -0
- package/fesm2022/mk-kit-ui-media.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-navigation.mjs +2542 -0
- package/fesm2022/mk-kit-ui-navigation.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-rich-text.mjs +565 -0
- package/fesm2022/mk-kit-ui-rich-text.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-table.mjs +1378 -0
- package/fesm2022/mk-kit-ui-table.mjs.map +1 -0
- package/fesm2022/mk-kit-ui.mjs +32 -0
- package/fesm2022/mk-kit-ui.mjs.map +1 -0
- package/package.json +130 -0
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.js +113 -0
- package/schematics/ng-add/schema.json +22 -0
- package/schematics/package.json +3 -0
- package/styles/mk-kit.css +750 -0
- package/types/mk-kit-ui-block-editor.d.ts +292 -0
- package/types/mk-kit-ui-button.d.ts +40 -0
- package/types/mk-kit-ui-checkbox.d.ts +62 -0
- package/types/mk-kit-ui-chip.d.ts +59 -0
- package/types/mk-kit-ui-context-menu.d.ts +57 -0
- package/types/mk-kit-ui-core.d.ts +1105 -0
- package/types/mk-kit-ui-data.d.ts +2580 -0
- package/types/mk-kit-ui-datetime.d.ts +1171 -0
- package/types/mk-kit-ui-directives.d.ts +807 -0
- package/types/mk-kit-ui-dnd.d.ts +423 -0
- package/types/mk-kit-ui-feedback.d.ts +1270 -0
- package/types/mk-kit-ui-forms.d.ts +3586 -0
- package/types/mk-kit-ui-icon.d.ts +108 -0
- package/types/mk-kit-ui-media.d.ts +549 -0
- package/types/mk-kit-ui-navigation.d.ts +1169 -0
- package/types/mk-kit-ui-rich-text.d.ts +187 -0
- package/types/mk-kit-ui-table.d.ts +739 -0
- package/types/mk-kit-ui.d.ts +17 -0
|
@@ -0,0 +1,1378 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, TemplateRef, Directive, input, ElementRef, Injector, PLATFORM_ID, DestroyRef, signal, effect, afterNextRender, booleanAttribute, numberAttribute, model, output, computed, viewChild, contentChild, contentChildren, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { DOCUMENT, isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
|
|
4
|
+
import { MkLiveAnnouncer, MK_I18N, mkUniqueId } from '@mk-kit/ui/core';
|
|
5
|
+
import { MkCheckbox } from '@mk-kit/ui/checkbox';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Marks an `<ng-template>` as the expandable detail content for {@link MkTable}
|
|
9
|
+
* rows. The template's implicit context is the row object, so consumers can
|
|
10
|
+
* destructure it with `let-row`:
|
|
11
|
+
*
|
|
12
|
+
* ```html
|
|
13
|
+
* <mk-table [columns]="cols" [data]="rows()" expandable>
|
|
14
|
+
* <ng-template mkTableRowDetail let-row>
|
|
15
|
+
* <dl>… {{ row.notes }} …</dl>
|
|
16
|
+
* </ng-template>
|
|
17
|
+
* </mk-table>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
class MkTableRowDetail {
|
|
21
|
+
/** The projected detail template, rendered once per expanded row. */
|
|
22
|
+
template = inject(TemplateRef);
|
|
23
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTableRowDetail, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
24
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: MkTableRowDetail, isStandalone: true, selector: "[mkTableRowDetail]", ngImport: i0 });
|
|
25
|
+
}
|
|
26
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTableRowDetail, decorators: [{
|
|
27
|
+
type: Directive,
|
|
28
|
+
args: [{
|
|
29
|
+
selector: '[mkTableRowDetail]',
|
|
30
|
+
}]
|
|
31
|
+
}] });
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Marks an `<ng-template>` as the renderer for one column's cells, named by the
|
|
35
|
+
* column `key`. Without it a cell can only be text — `MkTableColumn.format`
|
|
36
|
+
* returns a string — so anything richer (a status tag, an avatar, a progress
|
|
37
|
+
* bar, an action button) meant abandoning `mk-table` for a hand-rolled
|
|
38
|
+
* `<table>`.
|
|
39
|
+
*
|
|
40
|
+
* The value is the template's implicit context and the row is available as
|
|
41
|
+
* `let-row`:
|
|
42
|
+
*
|
|
43
|
+
* ```html
|
|
44
|
+
* <mk-table [columns]="cols" [data]="rows()">
|
|
45
|
+
* <ng-template mkTableCell="status" let-value let-row="row">
|
|
46
|
+
* <mk-tag [tone]="toneFor(value)">{{ label(value) }}</mk-tag>
|
|
47
|
+
* </ng-template>
|
|
48
|
+
* </mk-table>
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* A column with both a template and a `format` uses the template; `format`
|
|
52
|
+
* still applies to sorting/export paths that need a string.
|
|
53
|
+
*/
|
|
54
|
+
class MkTableCell {
|
|
55
|
+
/** Column key whose cells this template renders. */
|
|
56
|
+
mkTableCell = input.required(/* @ts-ignore */
|
|
57
|
+
...(ngDevMode ? [{ debugName: "mkTableCell" }] : /* istanbul ignore next */ []));
|
|
58
|
+
/** The projected template, rendered once per cell in that column. */
|
|
59
|
+
template = inject(TemplateRef);
|
|
60
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTableCell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
61
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkTableCell, isStandalone: true, selector: "[mkTableCell]", inputs: { mkTableCell: { classPropertyName: "mkTableCell", publicName: "mkTableCell", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
62
|
+
}
|
|
63
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTableCell, decorators: [{
|
|
64
|
+
type: Directive,
|
|
65
|
+
args: [{
|
|
66
|
+
selector: '[mkTableCell]',
|
|
67
|
+
}]
|
|
68
|
+
}], propDecorators: { mkTableCell: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkTableCell", required: true }] }] } });
|
|
69
|
+
|
|
70
|
+
/** Hard floor (px) for column resize when a column sets no `minWidth`. */
|
|
71
|
+
const MIN_COL_WIDTH = 60;
|
|
72
|
+
/** Upper bound advertised on resize separators (`aria-valuemax`). */
|
|
73
|
+
const MAX_COL_WIDTH = 2000;
|
|
74
|
+
/**
|
|
75
|
+
* Table — a themed data table built on a native `<table>` for accessibility.
|
|
76
|
+
* Supply `columns` and `data`; opt into sortable columns, sticky header,
|
|
77
|
+
* zebra striping, hover and density. Sorting is fully keyboard operable
|
|
78
|
+
* (Enter/Space on a header) and announces changes via {@link MkLiveAnnouncer}.
|
|
79
|
+
*
|
|
80
|
+
* ```html
|
|
81
|
+
* <mk-table
|
|
82
|
+
* [columns]="columns"
|
|
83
|
+
* [data]="rows()"
|
|
84
|
+
* stickyHeader
|
|
85
|
+
* zebra
|
|
86
|
+
* (sortChange)="onSort($event)"
|
|
87
|
+
* (rowClick)="open($event)" />
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
class MkTable {
|
|
91
|
+
announcer = inject(MkLiveAnnouncer);
|
|
92
|
+
i18n = inject(MK_I18N);
|
|
93
|
+
document = inject(DOCUMENT);
|
|
94
|
+
host = inject(ElementRef);
|
|
95
|
+
injector = inject(Injector);
|
|
96
|
+
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
97
|
+
destroyRef = inject(DestroyRef);
|
|
98
|
+
/**
|
|
99
|
+
* True while the table is narrower than {@link stackAt} and rendering cards.
|
|
100
|
+
*
|
|
101
|
+
* Driven by the element's own width rather than the viewport's: the question
|
|
102
|
+
* "do these columns fit" is about the space the table HAS, not the size of
|
|
103
|
+
* the screen. A table in a sidebar or a dialog should stack while the window
|
|
104
|
+
* around it is enormous.
|
|
105
|
+
*/
|
|
106
|
+
stacked = signal(false, /* @ts-ignore */
|
|
107
|
+
...(ngDevMode ? [{ debugName: "stacked" }] : /* istanbul ignore next */ []));
|
|
108
|
+
constructor() {
|
|
109
|
+
// Keep the sticky group-header offset in sync with the rendered thead
|
|
110
|
+
// height (it shifts with density, sticky mode and grouping itself).
|
|
111
|
+
effect(() => {
|
|
112
|
+
this.stickyHeader();
|
|
113
|
+
this.density();
|
|
114
|
+
this.groupBy();
|
|
115
|
+
afterNextRender({ read: () => this.applyGroupTop() }, { injector: this.injector });
|
|
116
|
+
});
|
|
117
|
+
// Watch the host's width against `stackAt`. ResizeObserver rather than
|
|
118
|
+
// matchMedia because the trigger is the element's width, not the window's.
|
|
119
|
+
// Skipped entirely on the server and wherever the API is missing, leaving
|
|
120
|
+
// `stacked` false — the grid is the safe fallback, since it renders the
|
|
121
|
+
// same data with nothing dropped.
|
|
122
|
+
afterNextRender({
|
|
123
|
+
read: () => {
|
|
124
|
+
if (!this.isBrowser || typeof ResizeObserver === 'undefined')
|
|
125
|
+
return;
|
|
126
|
+
const el = this.host.nativeElement;
|
|
127
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
128
|
+
const limit = this.stackAt();
|
|
129
|
+
this.stacked.set(limit > 0 && entry.contentRect.width < limit);
|
|
130
|
+
});
|
|
131
|
+
observer.observe(el);
|
|
132
|
+
this.destroyRef.onDestroy(() => observer.disconnect());
|
|
133
|
+
},
|
|
134
|
+
}, { injector: this.injector });
|
|
135
|
+
}
|
|
136
|
+
/** Measures the thead and exposes it as the group rows' sticky offset. */
|
|
137
|
+
applyGroupTop() {
|
|
138
|
+
if (this.groupBy() == null)
|
|
139
|
+
return;
|
|
140
|
+
const thead = this.host.nativeElement.querySelector('thead');
|
|
141
|
+
const h = this.stickyHeader() && thead ? thead.getBoundingClientRect().height : 0;
|
|
142
|
+
this.host.nativeElement.style.setProperty('--_group-top', `${Math.round(h)}px`);
|
|
143
|
+
}
|
|
144
|
+
/** Column definitions (order = display order). */
|
|
145
|
+
columns = input.required(/* @ts-ignore */
|
|
146
|
+
...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
147
|
+
/** Row objects to render. */
|
|
148
|
+
data = input([], /* @ts-ignore */
|
|
149
|
+
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
150
|
+
/** Pin the header to the top of the scroll container. */
|
|
151
|
+
stickyHeader = input(false, { ...(ngDevMode ? { debugName: "stickyHeader" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
152
|
+
/** Alternate row background for readability. */
|
|
153
|
+
zebra = input(false, { ...(ngDevMode ? { debugName: "zebra" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
154
|
+
/** Highlight rows on hover. */
|
|
155
|
+
hover = input(true, { ...(ngDevMode ? { debugName: "hover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
156
|
+
/** Row density. */
|
|
157
|
+
density = input('comfortable', /* @ts-ignore */
|
|
158
|
+
...(ngDevMode ? [{ debugName: "density" }] : /* istanbul ignore next */ []));
|
|
159
|
+
/**
|
|
160
|
+
* Width in px below which each row renders as a CARD instead of a table row.
|
|
161
|
+
* `0` (default) never stacks.
|
|
162
|
+
*
|
|
163
|
+
* Measured on the table's own container, not the viewport — a table in a
|
|
164
|
+
* narrow sidebar should stack on a desktop, and a table on a tablet in
|
|
165
|
+
* landscape should not. Per-column behaviour is set with
|
|
166
|
+
* {@link MkTableColumn.stack}.
|
|
167
|
+
*
|
|
168
|
+
* A grid cannot survive a phone: eight columns become eight unreadable
|
|
169
|
+
* slivers, and horizontal scrolling loses the row you were reading. Cards
|
|
170
|
+
* keep one record together and put its header beside each value.
|
|
171
|
+
*/
|
|
172
|
+
stackAt = input(0, { ...(ngDevMode ? { debugName: "stackAt" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
173
|
+
/** Style rows as clickable and emit `rowClick`. */
|
|
174
|
+
clickableRows = input(false, { ...(ngDevMode ? { debugName: "clickableRows" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
175
|
+
/** Message shown when there are no rows. */
|
|
176
|
+
emptyMessage = input(this.i18n.noData, /* @ts-ignore */
|
|
177
|
+
...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
|
|
178
|
+
/** Render a leading checkbox column for row selection. */
|
|
179
|
+
selectable = input(false, { ...(ngDevMode ? { debugName: "selectable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
180
|
+
/**
|
|
181
|
+
* Two-way bound array of selected rows. Use `[(selected)]` to bind, or read
|
|
182
|
+
* `selectionChange`. Rows are compared by {@link trackKey} when set, else by
|
|
183
|
+
* referential identity.
|
|
184
|
+
*/
|
|
185
|
+
selected = model([], /* @ts-ignore */
|
|
186
|
+
...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
187
|
+
/**
|
|
188
|
+
* Property name identifying a row for selection equality. When omitted rows
|
|
189
|
+
* are matched by reference.
|
|
190
|
+
*/
|
|
191
|
+
trackKey = input(/* @ts-ignore */
|
|
192
|
+
...(ngDevMode ? [undefined, { debugName: "trackKey" }] : /* istanbul ignore next */ []));
|
|
193
|
+
/**
|
|
194
|
+
* Optional per-row CSS class: called with each row, the returned string is
|
|
195
|
+
* appended to the row's class list (falsy → none). For state the consumer
|
|
196
|
+
* owns — an "active in the side panel" highlight, an unread accent — that
|
|
197
|
+
* `selectable`'s own selected style doesn't cover.
|
|
198
|
+
*/
|
|
199
|
+
rowClass = input(null, /* @ts-ignore */
|
|
200
|
+
...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
|
|
201
|
+
/** Resolved class for a row (empty string when no `rowClass` is set). */
|
|
202
|
+
rowClassFor(row) {
|
|
203
|
+
return this.rowClass()?.(row) ?? '';
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Render a leading expander column. Each row can reveal a detail panel
|
|
207
|
+
* supplied via an `<ng-template mkTableRowDetail let-row>`.
|
|
208
|
+
*/
|
|
209
|
+
expandable = input(false, { ...(ngDevMode ? { debugName: "expandable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
210
|
+
/** Allow only one row expanded at a time (accordion). */
|
|
211
|
+
singleExpand = input(false, { ...(ngDevMode ? { debugName: "singleExpand" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
212
|
+
/** Enable drag-to-resize on columns marked `resizable` (data-grid pro). */
|
|
213
|
+
resizableColumns = input(false, { ...(ngDevMode ? { debugName: "resizableColumns" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
214
|
+
/** Enable drag-to-reorder of column headers (data-grid pro). */
|
|
215
|
+
reorderableColumns = input(false, { ...(ngDevMode ? { debugName: "reorderableColumns" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
216
|
+
/**
|
|
217
|
+
* Group rows by a column key or an accessor. Renders a collapsible group
|
|
218
|
+
* header row (sticky, with a row count) above each group. Sorting still
|
|
219
|
+
* applies within groups; groups follow their first row's sorted position.
|
|
220
|
+
*/
|
|
221
|
+
groupBy = input(null, /* @ts-ignore */
|
|
222
|
+
...(ngDevMode ? [{ debugName: "groupBy" }] : /* istanbul ignore next */ []));
|
|
223
|
+
/** Formats a group header label; defaults to `String(value)`. */
|
|
224
|
+
groupLabel = input(null, /* @ts-ignore */
|
|
225
|
+
...(ngDevMode ? [{ debugName: "groupLabel" }] : /* istanbul ignore next */ []));
|
|
226
|
+
/** Emitted when the sort column/direction changes. */
|
|
227
|
+
sortChange = output();
|
|
228
|
+
/** Emitted when a row is clicked (enable via `clickableRows`). */
|
|
229
|
+
rowClick = output();
|
|
230
|
+
/** Emitted with the new selection whenever it changes (enable via `selectable`). */
|
|
231
|
+
selectionChange = output();
|
|
232
|
+
/** Emitted with the currently expanded rows whenever they change. */
|
|
233
|
+
expandedChange = output();
|
|
234
|
+
/** Emitted when a column is resized (px). */
|
|
235
|
+
columnResize = output();
|
|
236
|
+
/** Emitted with the new column key order after a reorder. */
|
|
237
|
+
columnReorder = output();
|
|
238
|
+
/** Emitted when an inline-editable cell is saved. */
|
|
239
|
+
cellEdit = output();
|
|
240
|
+
/** Emitted when a group header is expanded or collapsed. */
|
|
241
|
+
groupToggle = output();
|
|
242
|
+
/** User-set column widths (px), keyed by column key. */
|
|
243
|
+
colWidths = signal({}, /* @ts-ignore */
|
|
244
|
+
...(ngDevMode ? [{ debugName: "colWidths" }] : /* istanbul ignore next */ []));
|
|
245
|
+
/** User-set column order (keys); `null` = the input order. */
|
|
246
|
+
colOrder = signal(null, /* @ts-ignore */
|
|
247
|
+
...(ngDevMode ? [{ debugName: "colOrder" }] : /* istanbul ignore next */ []));
|
|
248
|
+
/** The cell currently being inline-edited. */
|
|
249
|
+
editing = signal(null, /* @ts-ignore */
|
|
250
|
+
...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
|
|
251
|
+
/** Columns in display order, honouring any user reordering. */
|
|
252
|
+
orderedColumns = computed(() => {
|
|
253
|
+
const cols = this.columns();
|
|
254
|
+
const order = this.colOrder();
|
|
255
|
+
if (!order)
|
|
256
|
+
return cols;
|
|
257
|
+
const byKey = new Map(cols.map((c) => [c.key, c]));
|
|
258
|
+
const ordered = order.map((k) => byKey.get(k)).filter((c) => !!c);
|
|
259
|
+
// Append any columns not present in the saved order (e.g. newly added).
|
|
260
|
+
const seen = new Set(order);
|
|
261
|
+
for (const c of cols)
|
|
262
|
+
if (!seen.has(c.key))
|
|
263
|
+
ordered.push(c);
|
|
264
|
+
return ordered;
|
|
265
|
+
}, /* @ts-ignore */
|
|
266
|
+
...(ngDevMode ? [{ debugName: "orderedColumns" }] : /* istanbul ignore next */ []));
|
|
267
|
+
// ── Stacked (card) layout ────────────────────────────────────────────────
|
|
268
|
+
// Three slots, so a card reads as a record rather than as a form: a heading
|
|
269
|
+
// line, labelled fields, and actions along the bottom. Columns keep their
|
|
270
|
+
// configured order within each slot.
|
|
271
|
+
/** Columns forming the card's heading line. */
|
|
272
|
+
stackTitleColumns = computed(() => this.orderedColumns().filter((c) => c.stack === 'title'), /* @ts-ignore */
|
|
273
|
+
...(ngDevMode ? [{ debugName: "stackTitleColumns" }] : /* istanbul ignore next */ []));
|
|
274
|
+
/** Columns rendered as `label / value` rows in the card body. */
|
|
275
|
+
stackFieldColumns = computed(() => this.orderedColumns().filter((c) => !c.stack), /* @ts-ignore */
|
|
276
|
+
...(ngDevMode ? [{ debugName: "stackFieldColumns" }] : /* istanbul ignore next */ []));
|
|
277
|
+
/** Columns pinned to the bottom of the card, unlabelled. */
|
|
278
|
+
stackFooterColumns = computed(() => this.orderedColumns().filter((c) => c.stack === 'footer'), /* @ts-ignore */
|
|
279
|
+
...(ngDevMode ? [{ debugName: "stackFooterColumns" }] : /* istanbul ignore next */ []));
|
|
280
|
+
/**
|
|
281
|
+
* Whether a stacked cell should show its column header as a label.
|
|
282
|
+
*
|
|
283
|
+
* An empty header means the column never had a name to show — an actions or
|
|
284
|
+
* chevron column — and an empty label box would just be a gap the reader has
|
|
285
|
+
* to account for.
|
|
286
|
+
*/
|
|
287
|
+
hasStackLabel(col) {
|
|
288
|
+
return !!col.header?.trim();
|
|
289
|
+
}
|
|
290
|
+
/** The rendered width for a column, if the user resized it. */
|
|
291
|
+
colStyleWidth(col) {
|
|
292
|
+
// A card has one column, so a per-column width — configured or dragged —
|
|
293
|
+
// would pin the value box to a grid width that no longer exists.
|
|
294
|
+
if (this.stacked())
|
|
295
|
+
return null;
|
|
296
|
+
const w = this.colWidths()[col.key];
|
|
297
|
+
if (w != null)
|
|
298
|
+
return `${w}px`;
|
|
299
|
+
return col.width ?? null;
|
|
300
|
+
}
|
|
301
|
+
/** Sticky offsets for every pinned column, computed once per layout change. */
|
|
302
|
+
pinnedOffsets = computed(() => {
|
|
303
|
+
const map = new Map();
|
|
304
|
+
const cols = this.orderedColumns();
|
|
305
|
+
const widths = this.colWidths();
|
|
306
|
+
// Account for the leading select / expander columns at the inline start.
|
|
307
|
+
let left = (this.selectable() ? 44 : 0) + (this.expandable() ? 44 : 0);
|
|
308
|
+
for (const c of cols) {
|
|
309
|
+
if (c.pinned !== 'left')
|
|
310
|
+
continue;
|
|
311
|
+
map.set(c.key, left);
|
|
312
|
+
left += widths[c.key] ?? this.numericWidth(c);
|
|
313
|
+
}
|
|
314
|
+
let right = 0;
|
|
315
|
+
for (let i = cols.length - 1; i >= 0; i--) {
|
|
316
|
+
const c = cols[i];
|
|
317
|
+
if (c.pinned !== 'right')
|
|
318
|
+
continue;
|
|
319
|
+
map.set(c.key, right);
|
|
320
|
+
right += widths[c.key] ?? this.numericWidth(c);
|
|
321
|
+
}
|
|
322
|
+
return map;
|
|
323
|
+
}, /* @ts-ignore */
|
|
324
|
+
...(ngDevMode ? [{ debugName: "pinnedOffsets" }] : /* istanbul ignore next */ []));
|
|
325
|
+
/** Sticky offset (px) for a pinned column. */
|
|
326
|
+
pinnedOffset(col) {
|
|
327
|
+
return this.pinnedOffsets().get(col.key) ?? 0;
|
|
328
|
+
}
|
|
329
|
+
/** Pinning freezes a column against horizontal scroll. Cards do not scroll
|
|
330
|
+
* sideways, so both the class and its inline offset are suppressed. */
|
|
331
|
+
isPinned(col, side) {
|
|
332
|
+
return !this.stacked() && col.pinned === side;
|
|
333
|
+
}
|
|
334
|
+
numericWidth(col) {
|
|
335
|
+
const w = col.width ? parseInt(col.width, 10) : NaN;
|
|
336
|
+
return Number.isFinite(w) ? w : 150;
|
|
337
|
+
}
|
|
338
|
+
// --- Column resize --------------------------------------------------------
|
|
339
|
+
resizeKey = null;
|
|
340
|
+
resizeStartX = 0;
|
|
341
|
+
resizeStartW = 0;
|
|
342
|
+
resizeMin = MIN_COL_WIDTH;
|
|
343
|
+
/** +1 in LTR, -1 in RTL — dragging toward the inline-end always widens. */
|
|
344
|
+
resizeSign = 1;
|
|
345
|
+
/** Whether the table currently renders right-to-left (SSR-safe). */
|
|
346
|
+
isRtl() {
|
|
347
|
+
const view = this.document.defaultView;
|
|
348
|
+
if (!view)
|
|
349
|
+
return false;
|
|
350
|
+
return view.getComputedStyle(this.host.nativeElement).direction === 'rtl';
|
|
351
|
+
}
|
|
352
|
+
resizeRaf = null;
|
|
353
|
+
pendingResizeX = 0;
|
|
354
|
+
/** Begin a drag-resize from a header handle. */
|
|
355
|
+
startResize(event, col) {
|
|
356
|
+
if (!this.resizableColumns() || !col.resizable)
|
|
357
|
+
return;
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
event.stopPropagation();
|
|
360
|
+
const th = event.target.closest('th');
|
|
361
|
+
this.resizeKey = col.key;
|
|
362
|
+
this.resizeStartX = event.clientX;
|
|
363
|
+
this.resizeSign = this.isRtl() ? -1 : 1;
|
|
364
|
+
this.resizeStartW =
|
|
365
|
+
this.colWidths()[col.key] ?? th?.getBoundingClientRect().width ?? this.numericWidth(col);
|
|
366
|
+
this.resizeMin = col.minWidth ?? MIN_COL_WIDTH;
|
|
367
|
+
this.document.addEventListener('pointermove', this.onResizeMove);
|
|
368
|
+
this.document.addEventListener('pointerup', this.onResizeEnd);
|
|
369
|
+
this.document.addEventListener('pointercancel', this.onResizeEnd);
|
|
370
|
+
}
|
|
371
|
+
/** rAF-coalesced: at most one width write (and CD pass) per frame. */
|
|
372
|
+
onResizeMove = (event) => {
|
|
373
|
+
if (!this.resizeKey)
|
|
374
|
+
return;
|
|
375
|
+
this.pendingResizeX = event.clientX;
|
|
376
|
+
if (this.resizeRaf != null)
|
|
377
|
+
return;
|
|
378
|
+
this.resizeRaf = this.document.defaultView?.requestAnimationFrame(() => {
|
|
379
|
+
this.resizeRaf = null;
|
|
380
|
+
this.applyPendingResize();
|
|
381
|
+
}) ?? null;
|
|
382
|
+
};
|
|
383
|
+
applyPendingResize() {
|
|
384
|
+
if (!this.resizeKey)
|
|
385
|
+
return;
|
|
386
|
+
const width = Math.max(this.resizeMin, Math.round(this.resizeStartW +
|
|
387
|
+
this.resizeSign * (this.pendingResizeX - this.resizeStartX)));
|
|
388
|
+
this.colWidths.update((w) => ({ ...w, [this.resizeKey]: width }));
|
|
389
|
+
}
|
|
390
|
+
onResizeEnd = () => {
|
|
391
|
+
if (this.resizeRaf != null) {
|
|
392
|
+
// Flush (don't drop) the last pending move so a fast drag lands exactly.
|
|
393
|
+
this.document.defaultView?.cancelAnimationFrame(this.resizeRaf);
|
|
394
|
+
this.resizeRaf = null;
|
|
395
|
+
this.applyPendingResize();
|
|
396
|
+
}
|
|
397
|
+
if (this.resizeKey) {
|
|
398
|
+
const col = this.columns().find((c) => c.key === this.resizeKey);
|
|
399
|
+
const width = this.colWidths()[this.resizeKey] ?? Math.round(this.resizeStartW);
|
|
400
|
+
this.columnResize.emit({ key: this.resizeKey, width });
|
|
401
|
+
if (col)
|
|
402
|
+
this.announcer.announce(this.i18n.columnWidth(col.header, width));
|
|
403
|
+
}
|
|
404
|
+
this.resizeKey = null;
|
|
405
|
+
this.document.removeEventListener('pointermove', this.onResizeMove);
|
|
406
|
+
this.document.removeEventListener('pointerup', this.onResizeEnd);
|
|
407
|
+
this.document.removeEventListener('pointercancel', this.onResizeEnd);
|
|
408
|
+
};
|
|
409
|
+
/** Keyboard column resize on the focused separator (APG window splitter). */
|
|
410
|
+
onResizeKeydown(event, col) {
|
|
411
|
+
if (!this.resizableColumns() || !col.resizable)
|
|
412
|
+
return;
|
|
413
|
+
const step = event.shiftKey ? 1 : 10;
|
|
414
|
+
let delta = 0;
|
|
415
|
+
if (event.key === 'ArrowLeft')
|
|
416
|
+
delta = -step;
|
|
417
|
+
else if (event.key === 'ArrowRight')
|
|
418
|
+
delta = step;
|
|
419
|
+
else
|
|
420
|
+
return;
|
|
421
|
+
// Mirror in RTL so pressing toward the inline-end always grows the column.
|
|
422
|
+
if (this.isRtl())
|
|
423
|
+
delta = -delta;
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
event.stopPropagation();
|
|
426
|
+
const min = col.minWidth ?? MIN_COL_WIDTH;
|
|
427
|
+
const th = event.target.closest('th');
|
|
428
|
+
const current = this.colWidths()[col.key] ??
|
|
429
|
+
th?.getBoundingClientRect().width ??
|
|
430
|
+
this.numericWidth(col);
|
|
431
|
+
const width = Math.max(min, Math.round(current + delta));
|
|
432
|
+
this.colWidths.update((w) => ({ ...w, [col.key]: width }));
|
|
433
|
+
this.columnResize.emit({ key: col.key, width });
|
|
434
|
+
this.announcer.announce(this.i18n.columnWidth(col.header, width));
|
|
435
|
+
}
|
|
436
|
+
/** The current width of a column, for the separator's aria-valuenow. */
|
|
437
|
+
resizeValueNow(col) {
|
|
438
|
+
return Math.round(this.colWidths()[col.key] ?? this.numericWidth(col));
|
|
439
|
+
}
|
|
440
|
+
/** The resize floor for a column, for the separator's aria-valuemin. */
|
|
441
|
+
resizeValueMin(col) {
|
|
442
|
+
return col.minWidth ?? MIN_COL_WIDTH;
|
|
443
|
+
}
|
|
444
|
+
/** Advertised resize ceiling (aria-valuemax) — a sane constant bound. */
|
|
445
|
+
resizeValueMax = MAX_COL_WIDTH;
|
|
446
|
+
ngOnDestroy() {
|
|
447
|
+
if (this.resizeRaf != null) {
|
|
448
|
+
this.document.defaultView?.cancelAnimationFrame(this.resizeRaf);
|
|
449
|
+
this.resizeRaf = null;
|
|
450
|
+
}
|
|
451
|
+
this.document.removeEventListener('pointermove', this.onResizeMove);
|
|
452
|
+
this.document.removeEventListener('pointerup', this.onResizeEnd);
|
|
453
|
+
this.document.removeEventListener('pointercancel', this.onResizeEnd);
|
|
454
|
+
}
|
|
455
|
+
// --- Column reorder (native drag) -----------------------------------------
|
|
456
|
+
dragKey = signal(null, /* @ts-ignore */
|
|
457
|
+
...(ngDevMode ? [{ debugName: "dragKey" }] : /* istanbul ignore next */ []));
|
|
458
|
+
onColDragStart(event, col) {
|
|
459
|
+
if (!this.reorderableColumns())
|
|
460
|
+
return;
|
|
461
|
+
this.dragKey.set(col.key);
|
|
462
|
+
event.dataTransfer?.setData('text/plain', col.key);
|
|
463
|
+
if (event.dataTransfer)
|
|
464
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
465
|
+
}
|
|
466
|
+
onColDragOver(event) {
|
|
467
|
+
if (this.reorderableColumns() && this.dragKey())
|
|
468
|
+
event.preventDefault();
|
|
469
|
+
}
|
|
470
|
+
onColDrop(event, target) {
|
|
471
|
+
const from = this.dragKey();
|
|
472
|
+
this.dragKey.set(null);
|
|
473
|
+
if (!from || from === target.key)
|
|
474
|
+
return;
|
|
475
|
+
event.preventDefault();
|
|
476
|
+
const order = this.orderedColumns().map((c) => c.key);
|
|
477
|
+
const toIdx = order.indexOf(target.key);
|
|
478
|
+
this.moveColumn(from, toIdx);
|
|
479
|
+
}
|
|
480
|
+
onColDragEnd() {
|
|
481
|
+
this.dragKey.set(null);
|
|
482
|
+
}
|
|
483
|
+
/** Move a column to `toIdx` in the display order and announce it. */
|
|
484
|
+
moveColumn(key, toIdx) {
|
|
485
|
+
const order = this.orderedColumns().map((c) => c.key);
|
|
486
|
+
const fromIdx = order.indexOf(key);
|
|
487
|
+
if (fromIdx < 0 || toIdx < 0 || toIdx >= order.length || fromIdx === toIdx) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
order.splice(toIdx, 0, order.splice(fromIdx, 1)[0]);
|
|
491
|
+
this.colOrder.set(order);
|
|
492
|
+
this.columnReorder.emit(order);
|
|
493
|
+
const col = this.columns().find((c) => c.key === key);
|
|
494
|
+
if (col) {
|
|
495
|
+
this.announcer.announce(this.i18n.columnMoved(col.header, toIdx + 1, order.length));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/** Keyboard column reorder: Alt+Arrow moves the focused header. */
|
|
499
|
+
onReorderKeydown(event, col) {
|
|
500
|
+
if (!this.reorderableColumns() || col.pinned || !event.altKey)
|
|
501
|
+
return;
|
|
502
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
|
|
503
|
+
return;
|
|
504
|
+
// Swallow the combo even when the move is a no-op (first column moved
|
|
505
|
+
// further left, last moved right) — otherwise Alt+Arrow falls through to
|
|
506
|
+
// the browser's history Back/Forward and navigates away from the grid.
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
const order = this.orderedColumns().map((c) => c.key);
|
|
509
|
+
const idx = order.indexOf(col.key);
|
|
510
|
+
if (event.key === 'ArrowLeft' && idx > 0) {
|
|
511
|
+
this.moveColumn(col.key, idx - 1);
|
|
512
|
+
}
|
|
513
|
+
else if (event.key === 'ArrowRight' && idx < order.length - 1) {
|
|
514
|
+
this.moveColumn(col.key, idx + 1);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// --- Inline cell edit -----------------------------------------------------
|
|
518
|
+
isEditing(index, col) {
|
|
519
|
+
const e = this.editing();
|
|
520
|
+
return !!e && e.index === index && e.key === col.key;
|
|
521
|
+
}
|
|
522
|
+
/** The cell element being edited, so focus can be restored after. */
|
|
523
|
+
editingCell = null;
|
|
524
|
+
/** The inline-edit input, focused once it renders. */
|
|
525
|
+
editInput = viewChild('editInput', /* @ts-ignore */
|
|
526
|
+
...(ngDevMode ? [{ debugName: "editInput" }] : /* istanbul ignore next */ []));
|
|
527
|
+
startEdit(index, col, event) {
|
|
528
|
+
if (!col.editable)
|
|
529
|
+
return;
|
|
530
|
+
event?.stopPropagation();
|
|
531
|
+
this.editingCell =
|
|
532
|
+
event?.target?.closest('td') ??
|
|
533
|
+
null;
|
|
534
|
+
this.editing.set({ index, key: col.key });
|
|
535
|
+
// Explicit focus once the input exists — a dynamically inserted
|
|
536
|
+
// `autofocus` attribute is not honoured after initial page load.
|
|
537
|
+
afterNextRender(() => this.editInput()?.nativeElement.focus(), {
|
|
538
|
+
injector: this.injector,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/** Keyboard path into edit mode (Enter / F2 on a focused editable cell). */
|
|
542
|
+
onCellKeydown(event, index, col) {
|
|
543
|
+
if (!col.editable || this.isEditing(index, col))
|
|
544
|
+
return;
|
|
545
|
+
if (event.key === 'Enter' || event.key === 'F2') {
|
|
546
|
+
event.preventDefault();
|
|
547
|
+
event.stopPropagation();
|
|
548
|
+
this.startEdit(index, col, event);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
commitEdit(row, col, value, restoreFocus = false) {
|
|
552
|
+
this.editing.set(null);
|
|
553
|
+
this.cellEdit.emit({ row, key: col.key, value });
|
|
554
|
+
this.announcer.announce(this.i18n.cellSaved(value));
|
|
555
|
+
if (restoreFocus)
|
|
556
|
+
this.editingCell?.focus();
|
|
557
|
+
this.editingCell = null;
|
|
558
|
+
}
|
|
559
|
+
cancelEdit(restoreFocus = false) {
|
|
560
|
+
this.editing.set(null);
|
|
561
|
+
if (restoreFocus)
|
|
562
|
+
this.editingCell?.focus();
|
|
563
|
+
this.editingCell = null;
|
|
564
|
+
}
|
|
565
|
+
onEditKeydown(event, row, col) {
|
|
566
|
+
if (event.key === 'Enter') {
|
|
567
|
+
event.preventDefault();
|
|
568
|
+
this.commitEdit(row, col, event.target.value, true);
|
|
569
|
+
}
|
|
570
|
+
else if (event.key === 'Escape') {
|
|
571
|
+
event.preventDefault();
|
|
572
|
+
event.stopPropagation();
|
|
573
|
+
this.cancelEdit(true);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/** The projected row-detail template (enable via `expandable`). */
|
|
577
|
+
rowDetail = contentChild(MkTableRowDetail, /* @ts-ignore */
|
|
578
|
+
...(ngDevMode ? [{ debugName: "rowDetail" }] : /* istanbul ignore next */ []));
|
|
579
|
+
/** Per-column cell templates, projected as `<ng-template mkTableCell="key">`. */
|
|
580
|
+
cellTemplates = contentChildren(MkTableCell, /* @ts-ignore */
|
|
581
|
+
...(ngDevMode ? [{ debugName: "cellTemplates" }] : /* istanbul ignore next */ []));
|
|
582
|
+
cellTemplateByKey = computed(() => {
|
|
583
|
+
const map = new Map();
|
|
584
|
+
for (const t of this.cellTemplates())
|
|
585
|
+
map.set(t.mkTableCell(), t);
|
|
586
|
+
return map;
|
|
587
|
+
}, /* @ts-ignore */
|
|
588
|
+
...(ngDevMode ? [{ debugName: "cellTemplateByKey" }] : /* istanbul ignore next */ []));
|
|
589
|
+
/** The template registered for a column, or null to fall back to text. */
|
|
590
|
+
cellTemplateFor(key) {
|
|
591
|
+
return this.cellTemplateByKey().get(key)?.template ?? null;
|
|
592
|
+
}
|
|
593
|
+
sortKey = signal(null, /* @ts-ignore */
|
|
594
|
+
...(ngDevMode ? [{ debugName: "sortKey" }] : /* istanbul ignore next */ []));
|
|
595
|
+
sortDir = signal(null, /* @ts-ignore */
|
|
596
|
+
...(ngDevMode ? [{ debugName: "sortDir" }] : /* istanbul ignore next */ []));
|
|
597
|
+
/** Stable id prefix so each detail row can be referenced by aria-controls. */
|
|
598
|
+
detailIdBase = mkUniqueId('mk-table-detail');
|
|
599
|
+
/** Total rendered columns, including the select and expander columns. */
|
|
600
|
+
totalColumns = computed(() => this.columns().length +
|
|
601
|
+
(this.selectable() ? 1 : 0) +
|
|
602
|
+
(this.expandable() ? 1 : 0), /* @ts-ignore */
|
|
603
|
+
...(ngDevMode ? [{ debugName: "totalColumns" }] : /* istanbul ignore next */ []));
|
|
604
|
+
/**
|
|
605
|
+
* Shared locale-sensitive collator for string sorting. `localeCompare`
|
|
606
|
+
* re-resolves locale data on every call; one cached `Intl.Collator` makes
|
|
607
|
+
* large-table sorts several-fold faster with the same default-locale order.
|
|
608
|
+
*/
|
|
609
|
+
static sortCollator = new Intl.Collator();
|
|
610
|
+
/** Data sorted by the active column, or the input order when unsorted. */
|
|
611
|
+
sortedData = computed(() => {
|
|
612
|
+
const key = this.sortKey();
|
|
613
|
+
const dir = this.sortDir();
|
|
614
|
+
const rows = this.data();
|
|
615
|
+
if (!key || !dir)
|
|
616
|
+
return rows;
|
|
617
|
+
const compare = (a, b) => {
|
|
618
|
+
const av = a[key];
|
|
619
|
+
const bv = b[key];
|
|
620
|
+
if (av == null && bv == null)
|
|
621
|
+
return 0;
|
|
622
|
+
if (av == null)
|
|
623
|
+
return -1;
|
|
624
|
+
if (bv == null)
|
|
625
|
+
return 1;
|
|
626
|
+
if (typeof av === 'number' && typeof bv === 'number')
|
|
627
|
+
return av - bv;
|
|
628
|
+
return MkTable.sortCollator.compare(String(av), String(bv));
|
|
629
|
+
};
|
|
630
|
+
// Negate the comparator for desc (instead of reversing) so the sort stays
|
|
631
|
+
// stable and null ordering is consistent in both directions.
|
|
632
|
+
return [...rows].sort(dir === 'desc' ? (a, b) => -compare(a, b) : compare);
|
|
633
|
+
}, /* @ts-ignore */
|
|
634
|
+
...(ngDevMode ? [{ debugName: "sortedData" }] : /* istanbul ignore next */ []));
|
|
635
|
+
/** `aria-sort` value for a header cell. */
|
|
636
|
+
ariaSort(col) {
|
|
637
|
+
if (!col.sortable)
|
|
638
|
+
return null;
|
|
639
|
+
if (this.sortKey() !== col.key)
|
|
640
|
+
return 'none';
|
|
641
|
+
return this.sortDir() === 'asc' ? 'ascending' : 'descending';
|
|
642
|
+
}
|
|
643
|
+
/** Glyph indicating a column's sort state. */
|
|
644
|
+
sortGlyph(col) {
|
|
645
|
+
if (this.sortKey() !== col.key)
|
|
646
|
+
return '↕';
|
|
647
|
+
return this.sortDir() === 'asc' ? '↑' : '↓';
|
|
648
|
+
}
|
|
649
|
+
/** Raw cell value, handed to an `[mkTableCell]` template unformatted. */
|
|
650
|
+
cellValue(row, col) {
|
|
651
|
+
return row[col.key];
|
|
652
|
+
}
|
|
653
|
+
/** Rendered text for a cell, applying the column formatter if present. */
|
|
654
|
+
cellText(row, col) {
|
|
655
|
+
const raw = row[col.key];
|
|
656
|
+
if (col.format)
|
|
657
|
+
return col.format(raw, row);
|
|
658
|
+
return raw == null ? '' : String(raw);
|
|
659
|
+
}
|
|
660
|
+
onSort(col) {
|
|
661
|
+
if (!col.sortable)
|
|
662
|
+
return;
|
|
663
|
+
let direction;
|
|
664
|
+
if (this.sortKey() !== col.key) {
|
|
665
|
+
this.sortKey.set(col.key);
|
|
666
|
+
this.sortDir.set('asc');
|
|
667
|
+
direction = 'asc';
|
|
668
|
+
}
|
|
669
|
+
else if (this.sortDir() === 'asc') {
|
|
670
|
+
this.sortDir.set('desc');
|
|
671
|
+
direction = 'desc';
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// desc -> cleared
|
|
675
|
+
this.sortKey.set(null);
|
|
676
|
+
this.sortDir.set(null);
|
|
677
|
+
direction = 'none';
|
|
678
|
+
}
|
|
679
|
+
this.sortChange.emit({ key: col.key, direction });
|
|
680
|
+
this.announcer.announce(direction === 'none'
|
|
681
|
+
? this.i18n.sortingCleared(col.header)
|
|
682
|
+
: this.i18n.sortedBy(col.header, direction));
|
|
683
|
+
}
|
|
684
|
+
onRowClick(row) {
|
|
685
|
+
if (this.clickableRows())
|
|
686
|
+
this.rowClick.emit(row);
|
|
687
|
+
}
|
|
688
|
+
/** Keyboard activation for clickable rows (Enter / Space). */
|
|
689
|
+
onRowKeydown(event, row) {
|
|
690
|
+
if (!this.clickableRows())
|
|
691
|
+
return;
|
|
692
|
+
if (event.target !== event.currentTarget)
|
|
693
|
+
return; // ignore inner controls
|
|
694
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
695
|
+
event.preventDefault();
|
|
696
|
+
this.rowClick.emit(row);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/** Stable row identity for `@for` tracking (trackKey when set, else the row). */
|
|
700
|
+
trackRow = (row) => this.rowKey(row);
|
|
701
|
+
/** Leading cell text used to label per-row controls for screen readers. */
|
|
702
|
+
rowLabel(row) {
|
|
703
|
+
const first = this.orderedColumns()[0];
|
|
704
|
+
return first ? this.cellText(row, first) : '';
|
|
705
|
+
}
|
|
706
|
+
// --- Selection ------------------------------------------------------------
|
|
707
|
+
rowKey(row) {
|
|
708
|
+
const key = this.trackKey();
|
|
709
|
+
return key ? row[key] : row;
|
|
710
|
+
}
|
|
711
|
+
/** Selected row keys as a Set — O(1) membership per cell per CD pass. */
|
|
712
|
+
selectedKeys = computed(() => new Set(this.selected().map((r) => this.rowKey(r))), /* @ts-ignore */
|
|
713
|
+
...(ngDevMode ? [{ debugName: "selectedKeys" }] : /* istanbul ignore next */ []));
|
|
714
|
+
/** Whether `row` is currently selected. */
|
|
715
|
+
isSelected(row) {
|
|
716
|
+
return this.selectedKeys().has(this.rowKey(row));
|
|
717
|
+
}
|
|
718
|
+
/** True when every visible row is selected. */
|
|
719
|
+
allSelected = computed(() => {
|
|
720
|
+
const rows = this.sortedData();
|
|
721
|
+
const keys = this.selectedKeys();
|
|
722
|
+
return rows.length > 0 && rows.every((row) => keys.has(this.rowKey(row)));
|
|
723
|
+
}, /* @ts-ignore */
|
|
724
|
+
...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
|
|
725
|
+
/** True when some — but not all — visible rows are selected. */
|
|
726
|
+
someSelected = computed(() => {
|
|
727
|
+
const keys = this.selectedKeys();
|
|
728
|
+
return (this.sortedData().some((row) => keys.has(this.rowKey(row))) &&
|
|
729
|
+
!this.allSelected());
|
|
730
|
+
}, /* @ts-ignore */
|
|
731
|
+
...(ngDevMode ? [{ debugName: "someSelected" }] : /* istanbul ignore next */ []));
|
|
732
|
+
commitSelection(next) {
|
|
733
|
+
this.selected.set(next);
|
|
734
|
+
this.selectionChange.emit(next);
|
|
735
|
+
}
|
|
736
|
+
/** Toggle a single row's selection without triggering `rowClick`. */
|
|
737
|
+
toggleRow(row) {
|
|
738
|
+
const rk = this.rowKey(row);
|
|
739
|
+
const current = this.selected();
|
|
740
|
+
const next = this.selectedKeys().has(rk)
|
|
741
|
+
? current.filter((r) => this.rowKey(r) !== rk)
|
|
742
|
+
: [...current, row];
|
|
743
|
+
this.commitSelection(next);
|
|
744
|
+
}
|
|
745
|
+
/** Select or deselect all visible rows. */
|
|
746
|
+
toggleAll() {
|
|
747
|
+
const rows = this.sortedData();
|
|
748
|
+
const current = this.selected();
|
|
749
|
+
if (this.allSelected()) {
|
|
750
|
+
const visible = new Set(rows.map((r) => this.rowKey(r)));
|
|
751
|
+
this.commitSelection(current.filter((r) => !visible.has(this.rowKey(r))));
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
const has = new Set(current.map((r) => this.rowKey(r)));
|
|
755
|
+
this.commitSelection([
|
|
756
|
+
...current,
|
|
757
|
+
...rows.filter((r) => !has.has(this.rowKey(r))),
|
|
758
|
+
]);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// --- Grouping ---------------------------------------------------------------
|
|
762
|
+
/** Group values currently collapsed. */
|
|
763
|
+
collapsedGroups = signal(new Set(), /* @ts-ignore */
|
|
764
|
+
...(ngDevMode ? [{ debugName: "collapsedGroups" }] : /* istanbul ignore next */ []));
|
|
765
|
+
/** Rows grouped by {@link groupBy}, or `null` when grouping is off. */
|
|
766
|
+
groups = computed(() => {
|
|
767
|
+
const by = this.groupBy();
|
|
768
|
+
if (by == null)
|
|
769
|
+
return null;
|
|
770
|
+
const accessor = typeof by === 'function'
|
|
771
|
+
? by
|
|
772
|
+
: (row) => row[by];
|
|
773
|
+
const map = new Map();
|
|
774
|
+
for (const row of this.sortedData()) {
|
|
775
|
+
const key = accessor(row);
|
|
776
|
+
const bucket = map.get(key);
|
|
777
|
+
if (bucket)
|
|
778
|
+
bucket.push(row);
|
|
779
|
+
else
|
|
780
|
+
map.set(key, [row]);
|
|
781
|
+
}
|
|
782
|
+
const label = this.groupLabel();
|
|
783
|
+
return [...map.entries()].map(([key, rows]) => ({
|
|
784
|
+
key,
|
|
785
|
+
label: label ? label(key, rows) : String(key),
|
|
786
|
+
rows,
|
|
787
|
+
}));
|
|
788
|
+
}, /* @ts-ignore */
|
|
789
|
+
...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
790
|
+
/**
|
|
791
|
+
* The tbody render list: group headers interleaved with their (expanded)
|
|
792
|
+
* rows when grouping is on, else just the sorted rows.
|
|
793
|
+
*/
|
|
794
|
+
displayItems = computed(() => {
|
|
795
|
+
const groups = this.groups();
|
|
796
|
+
if (!groups) {
|
|
797
|
+
return this.sortedData().map((row) => ({ kind: 'row', row }));
|
|
798
|
+
}
|
|
799
|
+
const collapsed = this.collapsedGroups();
|
|
800
|
+
const items = [];
|
|
801
|
+
for (const group of groups) {
|
|
802
|
+
items.push({ kind: 'group', group });
|
|
803
|
+
if (!collapsed.has(group.key)) {
|
|
804
|
+
for (const row of group.rows)
|
|
805
|
+
items.push({ kind: 'row', row });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return items;
|
|
809
|
+
}, /* @ts-ignore */
|
|
810
|
+
...(ngDevMode ? [{ debugName: "displayItems" }] : /* istanbul ignore next */ []));
|
|
811
|
+
/** `@for` identity: group headers by value, rows by {@link trackRow}. */
|
|
812
|
+
trackItem = (item) => item.kind === 'group' ? `mk-group:${String(item.group.key)}` : this.rowKey(item.row);
|
|
813
|
+
/** Whether a group is currently collapsed. */
|
|
814
|
+
isGroupCollapsed(key) {
|
|
815
|
+
return this.collapsedGroups().has(key);
|
|
816
|
+
}
|
|
817
|
+
/** Collapse or expand a group header. */
|
|
818
|
+
onGroupToggle(group) {
|
|
819
|
+
const next = new Set(this.collapsedGroups());
|
|
820
|
+
const collapsed = !next.has(group.key);
|
|
821
|
+
if (collapsed)
|
|
822
|
+
next.add(group.key);
|
|
823
|
+
else
|
|
824
|
+
next.delete(group.key);
|
|
825
|
+
this.collapsedGroups.set(next);
|
|
826
|
+
this.groupToggle.emit({ key: group.key, collapsed });
|
|
827
|
+
}
|
|
828
|
+
/** Collapse every group. */
|
|
829
|
+
collapseAllGroups() {
|
|
830
|
+
const groups = this.groups();
|
|
831
|
+
if (groups)
|
|
832
|
+
this.collapsedGroups.set(new Set(groups.map((g) => g.key)));
|
|
833
|
+
}
|
|
834
|
+
/** Expand every group. */
|
|
835
|
+
expandAllGroups() {
|
|
836
|
+
this.collapsedGroups.set(new Set());
|
|
837
|
+
}
|
|
838
|
+
// --- Expansion ------------------------------------------------------------
|
|
839
|
+
expandedKeys = signal(new Set(), /* @ts-ignore */
|
|
840
|
+
...(ngDevMode ? [{ debugName: "expandedKeys" }] : /* istanbul ignore next */ []));
|
|
841
|
+
/** Whether `row`'s detail panel is currently expanded. */
|
|
842
|
+
isExpanded(row) {
|
|
843
|
+
return this.expandedKeys().has(this.rowKey(row));
|
|
844
|
+
}
|
|
845
|
+
/** The DOM id of a row's detail panel (for `aria-controls`). */
|
|
846
|
+
detailId(index) {
|
|
847
|
+
return `${this.detailIdBase}-${index}`;
|
|
848
|
+
}
|
|
849
|
+
/** Toggle a row's detail panel, honouring `singleExpand`. */
|
|
850
|
+
toggleExpand(row, event) {
|
|
851
|
+
event?.stopPropagation();
|
|
852
|
+
const rk = this.rowKey(row);
|
|
853
|
+
const open = this.expandedKeys().has(rk);
|
|
854
|
+
const next = this.singleExpand() ? new Set() : new Set(this.expandedKeys());
|
|
855
|
+
if (open)
|
|
856
|
+
next.delete(rk);
|
|
857
|
+
else
|
|
858
|
+
next.add(rk);
|
|
859
|
+
this.expandedKeys.set(next);
|
|
860
|
+
this.expandedChange.emit(this.data().filter((r) => next.has(this.rowKey(r))));
|
|
861
|
+
}
|
|
862
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
863
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkTable, isStandalone: true, selector: "mk-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, zebra: { classPropertyName: "zebra", publicName: "zebra", isSignal: true, isRequired: false, transformFunction: null }, hover: { classPropertyName: "hover", publicName: "hover", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, stackAt: { classPropertyName: "stackAt", publicName: "stackAt", isSignal: true, isRequired: false, transformFunction: null }, clickableRows: { classPropertyName: "clickableRows", publicName: "clickableRows", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, trackKey: { classPropertyName: "trackKey", publicName: "trackKey", isSignal: true, isRequired: false, transformFunction: null }, rowClass: { classPropertyName: "rowClass", publicName: "rowClass", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, singleExpand: { classPropertyName: "singleExpand", publicName: "singleExpand", isSignal: true, isRequired: false, transformFunction: null }, resizableColumns: { classPropertyName: "resizableColumns", publicName: "resizableColumns", isSignal: true, isRequired: false, transformFunction: null }, reorderableColumns: { classPropertyName: "reorderableColumns", publicName: "reorderableColumns", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null }, groupLabel: { classPropertyName: "groupLabel", publicName: "groupLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange", sortChange: "sortChange", rowClick: "rowClick", selectionChange: "selectionChange", expandedChange: "expandedChange", columnResize: "columnResize", columnReorder: "columnReorder", cellEdit: "cellEdit", groupToggle: "groupToggle" }, host: { properties: { "class.mk-table--sticky": "stickyHeader()", "class.mk-table--zebra": "zebra()", "class.mk-table--hover": "hover()", "class.mk-table--compact": "density() === 'compact'", "class.mk-table--clickable": "clickableRows()", "class.mk-table--selectable": "selectable()", "class.mk-table--expandable": "expandable()", "class.mk-table--grouped": "groupBy() !== null", "class.mk-table--stacked": "stacked()" }, classAttribute: "mk-table" }, queries: [{ propertyName: "rowDetail", first: true, predicate: MkTableRowDetail, descendants: true, isSignal: true }, { propertyName: "cellTemplates", predicate: MkTableCell, isSignal: true }], viewQueries: [{ propertyName: "editInput", first: true, predicate: ["editInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"mk-table__scroll\">\n <!-- Explicit roles ONLY while stacked: `display: block` strips a table\n element of its implicit role, so without these a card layout stops\n being announced as tabular data at all. Redundant in the grid, so\n they are left off there rather than duplicating what the element\n already says. -->\n <table class=\"mk-table__table\" [attr.role]=\"stacked() ? 'table' : null\">\n <thead class=\"mk-table__head\" [attr.role]=\"stacked() ? 'rowgroup' : null\">\n <tr>\n @if (expandable()) {\n <th scope=\"col\" class=\"mk-table__th mk-table__th--expand\">\n <span class=\"mk-visually-hidden\">{{ i18n.expandHeader }}</span>\n </th>\n }\n @if (selectable()) {\n <th scope=\"col\" class=\"mk-table__th mk-table__th--select\">\n <mk-checkbox\n [aria-label]=\"i18n.selectAllRows\"\n [checked]=\"allSelected()\"\n [indeterminate]=\"someSelected()\"\n (checkedChange)=\"toggleAll()\"\n />\n </th>\n }\n @for (col of orderedColumns(); track col.key) {\n <th\n scope=\"col\"\n class=\"mk-table__th\"\n [class.mk-table__th--sortable]=\"col.sortable\"\n [class.mk-table__th--pinned]=\"col.pinned\"\n [class.mk-table__th--pinned-right]=\"col.pinned === 'right'\"\n [class.mk-table__th--dragging]=\"dragKey() === col.key\"\n [style.width]=\"colStyleWidth(col)\"\n [style.inset-inline-start.px]=\"col.pinned === 'left' ? pinnedOffset(col) : null\"\n [style.inset-inline-end.px]=\"col.pinned === 'right' ? pinnedOffset(col) : null\"\n [attr.data-align]=\"col.align ?? 'start'\"\n [attr.aria-sort]=\"ariaSort(col)\"\n [attr.draggable]=\"reorderableColumns() && !col.pinned ? true : null\"\n (dragstart)=\"onColDragStart($event, col)\"\n (dragover)=\"onColDragOver($event)\"\n (drop)=\"onColDrop($event, col)\"\n (dragend)=\"onColDragEnd()\"\n >\n @if (col.sortable || (reorderableColumns() && !col.pinned)) {\n <button\n type=\"button\"\n class=\"mk-table__th-button\"\n [class.mk-table__th-button--static]=\"!col.sortable\"\n (click)=\"onSort(col)\"\n (keydown)=\"onReorderKeydown($event, col)\"\n >\n <span class=\"mk-table__th-label\">{{ col.header }}</span>\n @if (col.sortable) {\n <span class=\"mk-table__sort\" aria-hidden=\"true\">{{ sortGlyph(col) }}</span>\n }\n </button>\n } @else {\n <span class=\"mk-table__th-inner\">\n <span class=\"mk-table__th-label\">{{ col.header }}</span>\n </span>\n }\n @if (resizableColumns() && col.resizable) {\n <span\n class=\"mk-table__resize\"\n role=\"separator\"\n tabindex=\"0\"\n aria-orientation=\"vertical\"\n [attr.aria-label]=\"i18n.resizeColumn\"\n [attr.aria-valuemin]=\"resizeValueMin(col)\"\n [attr.aria-valuenow]=\"resizeValueNow(col)\"\n [attr.aria-valuemax]=\"resizeValueMax\"\n (pointerdown)=\"startResize($event, col)\"\n (keydown)=\"onResizeKeydown($event, col)\"\n (click)=\"$event.stopPropagation()\"\n ></span>\n }\n </th>\n }\n </tr>\n </thead>\n <tbody class=\"mk-table__body\" [attr.role]=\"stacked() ? 'rowgroup' : null\">\n @for (item of displayItems(); track trackItem(item); let i = $index) {\n @if (item.kind === 'group') {\n <tr class=\"mk-table__group-row\" [attr.role]=\"stacked() ? 'row' : null\">\n <th\n class=\"mk-table__group\"\n scope=\"colgroup\"\n [attr.role]=\"stacked() ? 'rowheader' : null\"\n [attr.colspan]=\"totalColumns()\"\n >\n <button\n type=\"button\"\n class=\"mk-table__group-toggle\"\n [attr.aria-expanded]=\"!isGroupCollapsed(item.group.key)\"\n (click)=\"onGroupToggle(item.group)\"\n >\n <span\n class=\"mk-table__expander-icon\"\n [class.mk-table__expander-icon--open]=\"!isGroupCollapsed(item.group.key)\"\n aria-hidden=\"true\"\n >\u203A</span\n >\n <span class=\"mk-visually-hidden\">{{\n isGroupCollapsed(item.group.key) ? i18n.expandGroup : i18n.collapseGroup\n }}</span>\n <span class=\"mk-table__group-label\">{{ item.group.label }}</span>\n <span class=\"mk-table__group-count\">{{\n i18n.groupCount(item.group.rows.length)\n }}</span>\n </button>\n </th>\n </tr>\n } @else {\n <ng-container>\n <tr\n class=\"mk-table__row\"\n [attr.role]=\"stacked() ? 'row' : null\"\n [class]=\"rowClassFor(item.row)\"\n [class.mk-table__row--selected]=\"selectable() && isSelected(item.row)\"\n [class.mk-table__row--expanded]=\"expandable() && isExpanded(item.row)\"\n [attr.tabindex]=\"clickableRows() ? 0 : null\"\n (click)=\"onRowClick(item.row)\"\n (keydown)=\"onRowKeydown($event, item.row)\"\n >\n @if (expandable()) {\n <td\n class=\"mk-table__td mk-table__td--expand\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n (click)=\"$event.stopPropagation()\"\n >\n <button\n type=\"button\"\n class=\"mk-table__expander\"\n [class.mk-table__expander--open]=\"isExpanded(item.row)\"\n [attr.aria-expanded]=\"isExpanded(item.row)\"\n [attr.aria-controls]=\"detailId(i)\"\n [attr.aria-label]=\"isExpanded(item.row) ? i18n.collapseRow : i18n.expandRow\"\n (click)=\"toggleExpand(item.row, $event)\"\n >\n <span class=\"mk-table__expander-icon\" aria-hidden=\"true\">\u203A</span>\n </button>\n </td>\n }\n @if (selectable()) {\n <td\n class=\"mk-table__td mk-table__td--select\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n (click)=\"$event.stopPropagation()\"\n >\n <mk-checkbox\n [aria-label]=\"i18n.selectRow(rowLabel(item.row))\"\n [checked]=\"isSelected(item.row)\"\n (checkedChange)=\"toggleRow(item.row)\"\n />\n </td>\n }\n @if (!stacked()) {\n @for (col of orderedColumns(); track col.key) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ col, row: item.row, i, slot: null }\"\n />\n }\n } @else {\n <!-- Card layout. Same <td> elements, restyled \u2014 keeping the table\n DOM means selection, expansion, inline edit and every cell\n template keep working, since all of them reach for a `td`. -->\n @for (col of stackTitleColumns(); track col.key) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ col, row: item.row, i, slot: 'title' }\"\n />\n }\n @for (col of stackFieldColumns(); track col.key) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ col, row: item.row, i, slot: 'field' }\"\n />\n }\n @for (col of stackFooterColumns(); track col.key) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ col, row: item.row, i, slot: 'footer' }\"\n />\n }\n }\n </tr>\n @if (expandable() && isExpanded(item.row) && rowDetail()) {\n <tr class=\"mk-table__detail-row\" [attr.role]=\"stacked() ? 'row' : null\">\n <td\n class=\"mk-table__detail\"\n [id]=\"detailId(i)\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n [attr.colspan]=\"totalColumns()\"\n >\n <div class=\"mk-table__detail-inner\">\n <ng-container\n [ngTemplateOutlet]=\"rowDetail()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: item.row }\"\n />\n </div>\n </td>\n </tr>\n }\n </ng-container>\n }\n } @empty {\n <tr class=\"mk-table__row mk-table__row--empty\" [attr.role]=\"stacked() ? 'row' : null\">\n <td\n class=\"mk-table__empty\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n [attr.colspan]=\"totalColumns()\"\n >\n <ng-content select=\"[mkTableEmpty]\">{{ emptyMessage() }}</ng-content>\n </td>\n </tr>\n }\n </tbody>\n </table>\n</div>\n\n<!-- One cell, rendered by both layouts. `slot` is null in the grid and\n 'title' | 'field' | 'footer' in a card; everything else \u2014 the editor, the\n consumer's mkTableCell template, the formatted fallback \u2014 is identical, so\n a card can never drift from the grid it replaces. -->\n<ng-template #cellTpl let-col=\"col\" let-row=\"row\" let-i=\"i\" let-slot=\"slot\">\n <td\n class=\"mk-table__td\"\n [class.mk-table__td--pinned]=\"isPinned(col, 'left') || isPinned(col, 'right')\"\n [class.mk-table__td--pinned-right]=\"isPinned(col, 'right')\"\n [class.mk-table__td--editable]=\"col.editable\"\n [class.mk-table__td--stack-title]=\"slot === 'title'\"\n [class.mk-table__td--stack-field]=\"slot === 'field'\"\n [class.mk-table__td--stack-footer]=\"slot === 'footer'\"\n [style.width]=\"colStyleWidth(col)\"\n [style.inset-inline-start.px]=\"isPinned(col, 'left') ? pinnedOffset(col) : null\"\n [style.inset-inline-end.px]=\"isPinned(col, 'right') ? pinnedOffset(col) : null\"\n [attr.data-align]=\"col.align ?? 'start'\"\n [attr.tabindex]=\"col.editable ? 0 : null\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n (dblclick)=\"startEdit(i, col, $event)\"\n (keydown)=\"onCellKeydown($event, i, col)\"\n >\n @if (slot === 'field' && hasStackLabel(col)) {\n <!-- The column header, moved beside the value. Not aria-hidden: the\n <thead> is display:none while stacked, so this label is the only\n thing naming the value for a screen reader. -->\n <span class=\"mk-table__cell-label\">{{ col.header }}</span>\n }\n <span class=\"mk-table__cell-value\">\n @if (isEditing(i, col)) {\n <input\n #editInput\n class=\"mk-table__cell-input\"\n [value]=\"cellText(row, col)\"\n [attr.aria-label]=\"col.header\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onEditKeydown($event, row, col)\"\n (blur)=\"commitEdit(row, col, $any($event.target).value)\"\n />\n } @else if (cellTemplateFor(col.key); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{ $implicit: cellValue(row, col), row }\"\n />\n } @else {\n {{ cellText(row, col) }}\n @if (col.editable) {\n <span class=\"mk-visually-hidden\">{{ i18n.editCell }}</span>\n }\n }\n </span>\n </td>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{--_cell-pad-y: var(--mk-space-3);--_cell-pad-x: var(--mk-space-4);display:block;color:var(--mk-text)}:host(.mk-table--compact){--_cell-pad-y: var(--mk-space-2);--_cell-pad-x: var(--mk-space-3)}.mk-table__scroll{width:100%;overflow-x:auto;border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg)}.mk-table__table{width:100%;border-collapse:collapse;font-size:var(--mk-font-size-sm);background-color:var(--mk-surface)}.mk-table__th{padding:var(--_cell-pad-y) var(--_cell-pad-x);background-color:var(--mk-surface-2);color:var(--mk-text-muted);font-weight:var(--mk-font-weight-semibold);text-align:start;white-space:nowrap;border-bottom:var(--mk-border-width) solid var(--mk-border);vertical-align:middle}:host(.mk-table--sticky) .mk-table__head .mk-table__th{position:sticky;top:0;z-index:var(--mk-z-sticky)}.mk-table__th-inner{display:inline-flex;align-items:center;gap:var(--mk-space-1)}.mk-table__th--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.mk-table__th--sortable:hover{background-color:var(--mk-surface-3);color:var(--mk-text)}.mk-table__th-button{display:inline-flex;align-items:center;gap:var(--mk-space-1);width:100%;margin:calc(-1 * var(--_cell-pad-y)) calc(-1 * var(--_cell-pad-x));padding:var(--_cell-pad-y) var(--_cell-pad-x);font:inherit;font-weight:inherit;color:inherit;text-align:inherit;background:transparent;border:0;cursor:pointer}.mk-table__th-button--static{cursor:grab}.mk-table__th-button:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__th[data-align=center] .mk-table__th-button{justify-content:center}.mk-table__th[data-align=end] .mk-table__th-button{justify-content:flex-end}.mk-table__th[aria-sort=ascending],.mk-table__th[aria-sort=descending]{color:var(--mk-text)}.mk-table__sort{font-size:var(--mk-font-size-xs);opacity:.7;line-height:1}.mk-table__td{padding:var(--_cell-pad-y) var(--_cell-pad-x);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle);vertical-align:middle}.mk-table__row:last-child .mk-table__td{border-bottom:0}.mk-table__th[data-align=center],.mk-table__td[data-align=center]{text-align:center}.mk-table__th[data-align=end],.mk-table__td[data-align=end]{text-align:right}:host(.mk-table--zebra) .mk-table__body .mk-table__row:nth-child(2n) .mk-table__td{background-color:var(--mk-surface-2)}:host(.mk-table--hover) .mk-table__body .mk-table__row:not(.mk-table__row--empty):hover .mk-table__td{background-color:var(--mk-neutral-subtle)}:host(.mk-table--clickable) .mk-table__row:not(.mk-table__row--empty){cursor:pointer}:host(.mk-table--clickable) .mk-table__row:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__th--select,.mk-table__td--select{width:1%;white-space:nowrap;text-align:center;vertical-align:middle}:host .mk-table__body .mk-table__row--selected .mk-table__td,:host(.mk-table--zebra) .mk-table__body .mk-table__row--selected:nth-child(2n) .mk-table__td,:host(.mk-table--hover) .mk-table__body .mk-table__row--selected:hover .mk-table__td{background-color:var(--mk-selected-bg);color:var(--mk-selected-text)}.mk-table__th--expand,.mk-table__td--expand{width:1%;white-space:nowrap;text-align:center;vertical-align:middle}.mk-table__expander{display:inline-grid;place-items:center;width:1.5rem;height:1.5rem;padding:0;color:var(--mk-text-muted);background:transparent;border:none;border-radius:var(--mk-radius-sm);cursor:pointer}.mk-table__expander:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-table__expander:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__expander-icon{display:inline-block;font-size:var(--mk-font-size-lg);line-height:1;transition:transform var(--mk-transition-fast, .12s) ease}.mk-table__expander--open .mk-table__expander-icon,.mk-table__expander-icon--open{transform:rotate(90deg)}:host(:dir(rtl)) .mk-table__expander-icon{transform:scaleX(-1)}:host(:dir(rtl)) .mk-table__expander--open .mk-table__expander-icon,:host(:dir(rtl)) .mk-table__expander-icon--open{transform:scaleX(-1) rotate(-90deg)}.mk-table__detail{padding:0;background-color:var(--mk-surface-2);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle)}.mk-table__detail-inner{padding:var(--_cell-pad-y) var(--_cell-pad-x)}.mk-table__th{position:relative}.mk-table__resize{position:absolute;top:0;inset-inline-end:0;width:8px;height:100%;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}@media(pointer:coarse){.mk-table__resize:before{content:\"\";position:absolute;inset:min(0px,(100% - 24px) / 2)}}.mk-table__resize:after{content:\"\";position:absolute;top:25%;inset-inline-end:3px;width:2px;height:50%;background-color:var(--mk-border)}.mk-table__resize:hover:after,.mk-table__resize:focus-visible:after{background-color:var(--mk-primary)}.mk-table__resize:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__th[draggable=true]{cursor:grab}.mk-table__th--dragging{opacity:.5}.mk-table__th--pinned,.mk-table__td--pinned{position:sticky;z-index:1;background-color:var(--mk-surface)}.mk-table__th--pinned{z-index:2;background-color:var(--mk-surface-2)}.mk-table__th--pinned:not(.mk-table__th--pinned-right),.mk-table__td--pinned:not(.mk-table__td--pinned-right){box-shadow:2px 0 4px -2px var(--mk-shadow-color, rgba(0, 0, 0, .15))}.mk-table__th--pinned-right,.mk-table__td--pinned-right{box-shadow:-2px 0 4px -2px var(--mk-shadow-color, rgba(0, 0, 0, .15))}:host(:dir(rtl)) .mk-table__th--pinned:not(.mk-table__th--pinned-right),:host(:dir(rtl)) .mk-table__td--pinned:not(.mk-table__td--pinned-right){box-shadow:-2px 0 4px -2px var(--mk-shadow-color, rgba(0, 0, 0, .15))}:host(:dir(rtl)) .mk-table__th--pinned-right,:host(:dir(rtl)) .mk-table__td--pinned-right{box-shadow:2px 0 4px -2px var(--mk-shadow-color, rgba(0, 0, 0, .15))}:host(.mk-table--zebra) .mk-table__body .mk-table__row:nth-child(2n) .mk-table__td--pinned{background-color:var(--mk-surface-2)}.mk-table__td--editable{cursor:text}.mk-table__td--editable:hover{background-color:var(--mk-hover-overlay)}.mk-table__td--editable:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__cell-input{width:100%;margin:calc(-1 * var(--mk-space-1)) 0;padding:var(--mk-space-1) var(--mk-space-2);font:inherit;color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-primary);border-radius:var(--mk-radius-sm);outline:none}.mk-table__empty{padding:var(--mk-space-8) var(--_cell-pad-x);text-align:center;color:var(--mk-text-subtle)}.mk-table__group{padding:0;background-color:var(--mk-surface-2);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle);text-align:start}:host(.mk-table--grouped) .mk-table__group{position:sticky;top:var(--_group-top, 0px);z-index:calc(var(--mk-z-sticky) - 1)}.mk-table__group-toggle{display:flex;align-items:center;gap:var(--mk-space-2);width:100%;padding:var(--mk-space-2) var(--mk-space-3);border:none;background:none;color:var(--mk-text);font:inherit;font-weight:var(--mk-font-weight-semibold);cursor:pointer}.mk-table__group-toggle:hover{background-color:var(--mk-hover-overlay)}.mk-table__group-toggle:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__group-count{padding:0 var(--mk-space-2);border-radius:var(--mk-radius-full);background-color:var(--mk-surface-3);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);font-weight:var(--mk-font-weight-regular)}:host(.mk-table--stacked) .mk-table__scroll{overflow-x:visible;border:none;border-radius:0;background:none}:host(.mk-table--stacked) .mk-table__table,:host(.mk-table--stacked) .mk-table__body{display:block;background:none}:host(.mk-table--stacked) .mk-table__head{display:none}:host(.mk-table--stacked) .mk-table__row{display:grid;grid-template-columns:1fr;gap:var(--mk-space-1);padding:var(--mk-space-3);margin-bottom:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background-color:var(--mk-surface);box-shadow:var(--mk-shadow-xs)}:host(.mk-table--stacked) .mk-table__td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--mk-space-3);padding:var(--mk-space-1) 0;border:none;text-align:start}:host(.mk-table--stacked) .mk-table__td--stack-title{justify-content:flex-start;padding-bottom:var(--mk-space-2);font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold);color:var(--mk-text)}:host(.mk-table--stacked) .mk-table__td--stack-title~.mk-table__td--stack-title{justify-content:flex-end;margin-top:calc(-1 * var(--mk-space-2) - 1.5em);font-size:var(--mk-font-size-md)}:host(.mk-table--stacked) .mk-table__cell-label{flex:none;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}:host(.mk-table--stacked) .mk-table__cell-value{min-width:0;text-align:end;overflow-wrap:anywhere}:host(.mk-table--stacked) .mk-table__td--stack-title .mk-table__cell-value,:host(.mk-table--stacked) .mk-table__td--stack-footer .mk-table__cell-value{text-align:start}:host(.mk-table--stacked) .mk-table__td--stack-footer{justify-content:flex-start;margin-top:var(--mk-space-2);padding-top:var(--mk-space-3);border-top:var(--mk-border-width) solid var(--mk-border-subtle)}:host(.mk-table--stacked) .mk-table__td--stack-footer .mk-table__cell-value{display:flex;flex-wrap:wrap;gap:var(--mk-space-2);width:100%}:host(.mk-table--stacked) .mk-table__td--select,:host(.mk-table--stacked) .mk-table__td--expand{width:auto;justify-content:flex-start}:host(.mk-table--stacked).mk-table--zebra .mk-table__body .mk-table__row:nth-child(2n) .mk-table__td{background:none}:host(.mk-table--stacked) .mk-table__detail-row,:host(.mk-table--stacked) .mk-table__detail{display:block;padding:0}:host(.mk-table--stacked) .mk-table__detail-row{margin:calc(-1 * var(--mk-space-3)) 0 var(--mk-space-3)}:host(.mk-table--stacked) .mk-table__group-row,:host(.mk-table--stacked) .mk-table__group{display:block;top:0}:host(.mk-table--stacked) .mk-table__row--empty,:host(.mk-table--stacked) .mk-table__empty{display:block;border:none;box-shadow:none;background:none}@media(pointer:coarse){.mk-table__cell-input{font-size:max(var(--mk-font-size-md),16px)}}\n"], dependencies: [{ kind: "component", type: MkCheckbox, selector: "mk-checkbox", inputs: ["checked", "indeterminate", "disabled", "invalid", "required", "size", "tone", "aria-label"], outputs: ["checkedChange", "indeterminateChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
864
|
+
}
|
|
865
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTable, decorators: [{
|
|
866
|
+
type: Component,
|
|
867
|
+
args: [{ selector: 'mk-table', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkCheckbox, NgTemplateOutlet], host: {
|
|
868
|
+
class: 'mk-table',
|
|
869
|
+
'[class.mk-table--sticky]': 'stickyHeader()',
|
|
870
|
+
'[class.mk-table--zebra]': 'zebra()',
|
|
871
|
+
'[class.mk-table--hover]': 'hover()',
|
|
872
|
+
'[class.mk-table--compact]': "density() === 'compact'",
|
|
873
|
+
'[class.mk-table--clickable]': 'clickableRows()',
|
|
874
|
+
'[class.mk-table--selectable]': 'selectable()',
|
|
875
|
+
'[class.mk-table--expandable]': 'expandable()',
|
|
876
|
+
'[class.mk-table--grouped]': 'groupBy() !== null',
|
|
877
|
+
'[class.mk-table--stacked]': 'stacked()',
|
|
878
|
+
}, template: "<div class=\"mk-table__scroll\">\n <!-- Explicit roles ONLY while stacked: `display: block` strips a table\n element of its implicit role, so without these a card layout stops\n being announced as tabular data at all. Redundant in the grid, so\n they are left off there rather than duplicating what the element\n already says. -->\n <table class=\"mk-table__table\" [attr.role]=\"stacked() ? 'table' : null\">\n <thead class=\"mk-table__head\" [attr.role]=\"stacked() ? 'rowgroup' : null\">\n <tr>\n @if (expandable()) {\n <th scope=\"col\" class=\"mk-table__th mk-table__th--expand\">\n <span class=\"mk-visually-hidden\">{{ i18n.expandHeader }}</span>\n </th>\n }\n @if (selectable()) {\n <th scope=\"col\" class=\"mk-table__th mk-table__th--select\">\n <mk-checkbox\n [aria-label]=\"i18n.selectAllRows\"\n [checked]=\"allSelected()\"\n [indeterminate]=\"someSelected()\"\n (checkedChange)=\"toggleAll()\"\n />\n </th>\n }\n @for (col of orderedColumns(); track col.key) {\n <th\n scope=\"col\"\n class=\"mk-table__th\"\n [class.mk-table__th--sortable]=\"col.sortable\"\n [class.mk-table__th--pinned]=\"col.pinned\"\n [class.mk-table__th--pinned-right]=\"col.pinned === 'right'\"\n [class.mk-table__th--dragging]=\"dragKey() === col.key\"\n [style.width]=\"colStyleWidth(col)\"\n [style.inset-inline-start.px]=\"col.pinned === 'left' ? pinnedOffset(col) : null\"\n [style.inset-inline-end.px]=\"col.pinned === 'right' ? pinnedOffset(col) : null\"\n [attr.data-align]=\"col.align ?? 'start'\"\n [attr.aria-sort]=\"ariaSort(col)\"\n [attr.draggable]=\"reorderableColumns() && !col.pinned ? true : null\"\n (dragstart)=\"onColDragStart($event, col)\"\n (dragover)=\"onColDragOver($event)\"\n (drop)=\"onColDrop($event, col)\"\n (dragend)=\"onColDragEnd()\"\n >\n @if (col.sortable || (reorderableColumns() && !col.pinned)) {\n <button\n type=\"button\"\n class=\"mk-table__th-button\"\n [class.mk-table__th-button--static]=\"!col.sortable\"\n (click)=\"onSort(col)\"\n (keydown)=\"onReorderKeydown($event, col)\"\n >\n <span class=\"mk-table__th-label\">{{ col.header }}</span>\n @if (col.sortable) {\n <span class=\"mk-table__sort\" aria-hidden=\"true\">{{ sortGlyph(col) }}</span>\n }\n </button>\n } @else {\n <span class=\"mk-table__th-inner\">\n <span class=\"mk-table__th-label\">{{ col.header }}</span>\n </span>\n }\n @if (resizableColumns() && col.resizable) {\n <span\n class=\"mk-table__resize\"\n role=\"separator\"\n tabindex=\"0\"\n aria-orientation=\"vertical\"\n [attr.aria-label]=\"i18n.resizeColumn\"\n [attr.aria-valuemin]=\"resizeValueMin(col)\"\n [attr.aria-valuenow]=\"resizeValueNow(col)\"\n [attr.aria-valuemax]=\"resizeValueMax\"\n (pointerdown)=\"startResize($event, col)\"\n (keydown)=\"onResizeKeydown($event, col)\"\n (click)=\"$event.stopPropagation()\"\n ></span>\n }\n </th>\n }\n </tr>\n </thead>\n <tbody class=\"mk-table__body\" [attr.role]=\"stacked() ? 'rowgroup' : null\">\n @for (item of displayItems(); track trackItem(item); let i = $index) {\n @if (item.kind === 'group') {\n <tr class=\"mk-table__group-row\" [attr.role]=\"stacked() ? 'row' : null\">\n <th\n class=\"mk-table__group\"\n scope=\"colgroup\"\n [attr.role]=\"stacked() ? 'rowheader' : null\"\n [attr.colspan]=\"totalColumns()\"\n >\n <button\n type=\"button\"\n class=\"mk-table__group-toggle\"\n [attr.aria-expanded]=\"!isGroupCollapsed(item.group.key)\"\n (click)=\"onGroupToggle(item.group)\"\n >\n <span\n class=\"mk-table__expander-icon\"\n [class.mk-table__expander-icon--open]=\"!isGroupCollapsed(item.group.key)\"\n aria-hidden=\"true\"\n >\u203A</span\n >\n <span class=\"mk-visually-hidden\">{{\n isGroupCollapsed(item.group.key) ? i18n.expandGroup : i18n.collapseGroup\n }}</span>\n <span class=\"mk-table__group-label\">{{ item.group.label }}</span>\n <span class=\"mk-table__group-count\">{{\n i18n.groupCount(item.group.rows.length)\n }}</span>\n </button>\n </th>\n </tr>\n } @else {\n <ng-container>\n <tr\n class=\"mk-table__row\"\n [attr.role]=\"stacked() ? 'row' : null\"\n [class]=\"rowClassFor(item.row)\"\n [class.mk-table__row--selected]=\"selectable() && isSelected(item.row)\"\n [class.mk-table__row--expanded]=\"expandable() && isExpanded(item.row)\"\n [attr.tabindex]=\"clickableRows() ? 0 : null\"\n (click)=\"onRowClick(item.row)\"\n (keydown)=\"onRowKeydown($event, item.row)\"\n >\n @if (expandable()) {\n <td\n class=\"mk-table__td mk-table__td--expand\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n (click)=\"$event.stopPropagation()\"\n >\n <button\n type=\"button\"\n class=\"mk-table__expander\"\n [class.mk-table__expander--open]=\"isExpanded(item.row)\"\n [attr.aria-expanded]=\"isExpanded(item.row)\"\n [attr.aria-controls]=\"detailId(i)\"\n [attr.aria-label]=\"isExpanded(item.row) ? i18n.collapseRow : i18n.expandRow\"\n (click)=\"toggleExpand(item.row, $event)\"\n >\n <span class=\"mk-table__expander-icon\" aria-hidden=\"true\">\u203A</span>\n </button>\n </td>\n }\n @if (selectable()) {\n <td\n class=\"mk-table__td mk-table__td--select\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n (click)=\"$event.stopPropagation()\"\n >\n <mk-checkbox\n [aria-label]=\"i18n.selectRow(rowLabel(item.row))\"\n [checked]=\"isSelected(item.row)\"\n (checkedChange)=\"toggleRow(item.row)\"\n />\n </td>\n }\n @if (!stacked()) {\n @for (col of orderedColumns(); track col.key) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ col, row: item.row, i, slot: null }\"\n />\n }\n } @else {\n <!-- Card layout. Same <td> elements, restyled \u2014 keeping the table\n DOM means selection, expansion, inline edit and every cell\n template keep working, since all of them reach for a `td`. -->\n @for (col of stackTitleColumns(); track col.key) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ col, row: item.row, i, slot: 'title' }\"\n />\n }\n @for (col of stackFieldColumns(); track col.key) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ col, row: item.row, i, slot: 'field' }\"\n />\n }\n @for (col of stackFooterColumns(); track col.key) {\n <ng-container\n [ngTemplateOutlet]=\"cellTpl\"\n [ngTemplateOutletContext]=\"{ col, row: item.row, i, slot: 'footer' }\"\n />\n }\n }\n </tr>\n @if (expandable() && isExpanded(item.row) && rowDetail()) {\n <tr class=\"mk-table__detail-row\" [attr.role]=\"stacked() ? 'row' : null\">\n <td\n class=\"mk-table__detail\"\n [id]=\"detailId(i)\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n [attr.colspan]=\"totalColumns()\"\n >\n <div class=\"mk-table__detail-inner\">\n <ng-container\n [ngTemplateOutlet]=\"rowDetail()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: item.row }\"\n />\n </div>\n </td>\n </tr>\n }\n </ng-container>\n }\n } @empty {\n <tr class=\"mk-table__row mk-table__row--empty\" [attr.role]=\"stacked() ? 'row' : null\">\n <td\n class=\"mk-table__empty\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n [attr.colspan]=\"totalColumns()\"\n >\n <ng-content select=\"[mkTableEmpty]\">{{ emptyMessage() }}</ng-content>\n </td>\n </tr>\n }\n </tbody>\n </table>\n</div>\n\n<!-- One cell, rendered by both layouts. `slot` is null in the grid and\n 'title' | 'field' | 'footer' in a card; everything else \u2014 the editor, the\n consumer's mkTableCell template, the formatted fallback \u2014 is identical, so\n a card can never drift from the grid it replaces. -->\n<ng-template #cellTpl let-col=\"col\" let-row=\"row\" let-i=\"i\" let-slot=\"slot\">\n <td\n class=\"mk-table__td\"\n [class.mk-table__td--pinned]=\"isPinned(col, 'left') || isPinned(col, 'right')\"\n [class.mk-table__td--pinned-right]=\"isPinned(col, 'right')\"\n [class.mk-table__td--editable]=\"col.editable\"\n [class.mk-table__td--stack-title]=\"slot === 'title'\"\n [class.mk-table__td--stack-field]=\"slot === 'field'\"\n [class.mk-table__td--stack-footer]=\"slot === 'footer'\"\n [style.width]=\"colStyleWidth(col)\"\n [style.inset-inline-start.px]=\"isPinned(col, 'left') ? pinnedOffset(col) : null\"\n [style.inset-inline-end.px]=\"isPinned(col, 'right') ? pinnedOffset(col) : null\"\n [attr.data-align]=\"col.align ?? 'start'\"\n [attr.tabindex]=\"col.editable ? 0 : null\"\n [attr.role]=\"stacked() ? 'cell' : null\"\n (dblclick)=\"startEdit(i, col, $event)\"\n (keydown)=\"onCellKeydown($event, i, col)\"\n >\n @if (slot === 'field' && hasStackLabel(col)) {\n <!-- The column header, moved beside the value. Not aria-hidden: the\n <thead> is display:none while stacked, so this label is the only\n thing naming the value for a screen reader. -->\n <span class=\"mk-table__cell-label\">{{ col.header }}</span>\n }\n <span class=\"mk-table__cell-value\">\n @if (isEditing(i, col)) {\n <input\n #editInput\n class=\"mk-table__cell-input\"\n [value]=\"cellText(row, col)\"\n [attr.aria-label]=\"col.header\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onEditKeydown($event, row, col)\"\n (blur)=\"commitEdit(row, col, $any($event.target).value)\"\n />\n } @else if (cellTemplateFor(col.key); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{ $implicit: cellValue(row, col), row }\"\n />\n } @else {\n {{ cellText(row, col) }}\n @if (col.editable) {\n <span class=\"mk-visually-hidden\">{{ i18n.editCell }}</span>\n }\n }\n </span>\n </td>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{--_cell-pad-y: var(--mk-space-3);--_cell-pad-x: var(--mk-space-4);display:block;color:var(--mk-text)}:host(.mk-table--compact){--_cell-pad-y: var(--mk-space-2);--_cell-pad-x: var(--mk-space-3)}.mk-table__scroll{width:100%;overflow-x:auto;border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg)}.mk-table__table{width:100%;border-collapse:collapse;font-size:var(--mk-font-size-sm);background-color:var(--mk-surface)}.mk-table__th{padding:var(--_cell-pad-y) var(--_cell-pad-x);background-color:var(--mk-surface-2);color:var(--mk-text-muted);font-weight:var(--mk-font-weight-semibold);text-align:start;white-space:nowrap;border-bottom:var(--mk-border-width) solid var(--mk-border);vertical-align:middle}:host(.mk-table--sticky) .mk-table__head .mk-table__th{position:sticky;top:0;z-index:var(--mk-z-sticky)}.mk-table__th-inner{display:inline-flex;align-items:center;gap:var(--mk-space-1)}.mk-table__th--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.mk-table__th--sortable:hover{background-color:var(--mk-surface-3);color:var(--mk-text)}.mk-table__th-button{display:inline-flex;align-items:center;gap:var(--mk-space-1);width:100%;margin:calc(-1 * var(--_cell-pad-y)) calc(-1 * var(--_cell-pad-x));padding:var(--_cell-pad-y) var(--_cell-pad-x);font:inherit;font-weight:inherit;color:inherit;text-align:inherit;background:transparent;border:0;cursor:pointer}.mk-table__th-button--static{cursor:grab}.mk-table__th-button:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__th[data-align=center] .mk-table__th-button{justify-content:center}.mk-table__th[data-align=end] .mk-table__th-button{justify-content:flex-end}.mk-table__th[aria-sort=ascending],.mk-table__th[aria-sort=descending]{color:var(--mk-text)}.mk-table__sort{font-size:var(--mk-font-size-xs);opacity:.7;line-height:1}.mk-table__td{padding:var(--_cell-pad-y) var(--_cell-pad-x);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle);vertical-align:middle}.mk-table__row:last-child .mk-table__td{border-bottom:0}.mk-table__th[data-align=center],.mk-table__td[data-align=center]{text-align:center}.mk-table__th[data-align=end],.mk-table__td[data-align=end]{text-align:right}:host(.mk-table--zebra) .mk-table__body .mk-table__row:nth-child(2n) .mk-table__td{background-color:var(--mk-surface-2)}:host(.mk-table--hover) .mk-table__body .mk-table__row:not(.mk-table__row--empty):hover .mk-table__td{background-color:var(--mk-neutral-subtle)}:host(.mk-table--clickable) .mk-table__row:not(.mk-table__row--empty){cursor:pointer}:host(.mk-table--clickable) .mk-table__row:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__th--select,.mk-table__td--select{width:1%;white-space:nowrap;text-align:center;vertical-align:middle}:host .mk-table__body .mk-table__row--selected .mk-table__td,:host(.mk-table--zebra) .mk-table__body .mk-table__row--selected:nth-child(2n) .mk-table__td,:host(.mk-table--hover) .mk-table__body .mk-table__row--selected:hover .mk-table__td{background-color:var(--mk-selected-bg);color:var(--mk-selected-text)}.mk-table__th--expand,.mk-table__td--expand{width:1%;white-space:nowrap;text-align:center;vertical-align:middle}.mk-table__expander{display:inline-grid;place-items:center;width:1.5rem;height:1.5rem;padding:0;color:var(--mk-text-muted);background:transparent;border:none;border-radius:var(--mk-radius-sm);cursor:pointer}.mk-table__expander:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-table__expander:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__expander-icon{display:inline-block;font-size:var(--mk-font-size-lg);line-height:1;transition:transform var(--mk-transition-fast, .12s) ease}.mk-table__expander--open .mk-table__expander-icon,.mk-table__expander-icon--open{transform:rotate(90deg)}:host(:dir(rtl)) .mk-table__expander-icon{transform:scaleX(-1)}:host(:dir(rtl)) .mk-table__expander--open .mk-table__expander-icon,:host(:dir(rtl)) .mk-table__expander-icon--open{transform:scaleX(-1) rotate(-90deg)}.mk-table__detail{padding:0;background-color:var(--mk-surface-2);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle)}.mk-table__detail-inner{padding:var(--_cell-pad-y) var(--_cell-pad-x)}.mk-table__th{position:relative}.mk-table__resize{position:absolute;top:0;inset-inline-end:0;width:8px;height:100%;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}@media(pointer:coarse){.mk-table__resize:before{content:\"\";position:absolute;inset:min(0px,(100% - 24px) / 2)}}.mk-table__resize:after{content:\"\";position:absolute;top:25%;inset-inline-end:3px;width:2px;height:50%;background-color:var(--mk-border)}.mk-table__resize:hover:after,.mk-table__resize:focus-visible:after{background-color:var(--mk-primary)}.mk-table__resize:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__th[draggable=true]{cursor:grab}.mk-table__th--dragging{opacity:.5}.mk-table__th--pinned,.mk-table__td--pinned{position:sticky;z-index:1;background-color:var(--mk-surface)}.mk-table__th--pinned{z-index:2;background-color:var(--mk-surface-2)}.mk-table__th--pinned:not(.mk-table__th--pinned-right),.mk-table__td--pinned:not(.mk-table__td--pinned-right){box-shadow:2px 0 4px -2px var(--mk-shadow-color, rgba(0, 0, 0, .15))}.mk-table__th--pinned-right,.mk-table__td--pinned-right{box-shadow:-2px 0 4px -2px var(--mk-shadow-color, rgba(0, 0, 0, .15))}:host(:dir(rtl)) .mk-table__th--pinned:not(.mk-table__th--pinned-right),:host(:dir(rtl)) .mk-table__td--pinned:not(.mk-table__td--pinned-right){box-shadow:-2px 0 4px -2px var(--mk-shadow-color, rgba(0, 0, 0, .15))}:host(:dir(rtl)) .mk-table__th--pinned-right,:host(:dir(rtl)) .mk-table__td--pinned-right{box-shadow:2px 0 4px -2px var(--mk-shadow-color, rgba(0, 0, 0, .15))}:host(.mk-table--zebra) .mk-table__body .mk-table__row:nth-child(2n) .mk-table__td--pinned{background-color:var(--mk-surface-2)}.mk-table__td--editable{cursor:text}.mk-table__td--editable:hover{background-color:var(--mk-hover-overlay)}.mk-table__td--editable:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__cell-input{width:100%;margin:calc(-1 * var(--mk-space-1)) 0;padding:var(--mk-space-1) var(--mk-space-2);font:inherit;color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-primary);border-radius:var(--mk-radius-sm);outline:none}.mk-table__empty{padding:var(--mk-space-8) var(--_cell-pad-x);text-align:center;color:var(--mk-text-subtle)}.mk-table__group{padding:0;background-color:var(--mk-surface-2);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle);text-align:start}:host(.mk-table--grouped) .mk-table__group{position:sticky;top:var(--_group-top, 0px);z-index:calc(var(--mk-z-sticky) - 1)}.mk-table__group-toggle{display:flex;align-items:center;gap:var(--mk-space-2);width:100%;padding:var(--mk-space-2) var(--mk-space-3);border:none;background:none;color:var(--mk-text);font:inherit;font-weight:var(--mk-font-weight-semibold);cursor:pointer}.mk-table__group-toggle:hover{background-color:var(--mk-hover-overlay)}.mk-table__group-toggle:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-table__group-count{padding:0 var(--mk-space-2);border-radius:var(--mk-radius-full);background-color:var(--mk-surface-3);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);font-weight:var(--mk-font-weight-regular)}:host(.mk-table--stacked) .mk-table__scroll{overflow-x:visible;border:none;border-radius:0;background:none}:host(.mk-table--stacked) .mk-table__table,:host(.mk-table--stacked) .mk-table__body{display:block;background:none}:host(.mk-table--stacked) .mk-table__head{display:none}:host(.mk-table--stacked) .mk-table__row{display:grid;grid-template-columns:1fr;gap:var(--mk-space-1);padding:var(--mk-space-3);margin-bottom:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background-color:var(--mk-surface);box-shadow:var(--mk-shadow-xs)}:host(.mk-table--stacked) .mk-table__td{display:flex;align-items:baseline;justify-content:space-between;gap:var(--mk-space-3);padding:var(--mk-space-1) 0;border:none;text-align:start}:host(.mk-table--stacked) .mk-table__td--stack-title{justify-content:flex-start;padding-bottom:var(--mk-space-2);font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold);color:var(--mk-text)}:host(.mk-table--stacked) .mk-table__td--stack-title~.mk-table__td--stack-title{justify-content:flex-end;margin-top:calc(-1 * var(--mk-space-2) - 1.5em);font-size:var(--mk-font-size-md)}:host(.mk-table--stacked) .mk-table__cell-label{flex:none;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}:host(.mk-table--stacked) .mk-table__cell-value{min-width:0;text-align:end;overflow-wrap:anywhere}:host(.mk-table--stacked) .mk-table__td--stack-title .mk-table__cell-value,:host(.mk-table--stacked) .mk-table__td--stack-footer .mk-table__cell-value{text-align:start}:host(.mk-table--stacked) .mk-table__td--stack-footer{justify-content:flex-start;margin-top:var(--mk-space-2);padding-top:var(--mk-space-3);border-top:var(--mk-border-width) solid var(--mk-border-subtle)}:host(.mk-table--stacked) .mk-table__td--stack-footer .mk-table__cell-value{display:flex;flex-wrap:wrap;gap:var(--mk-space-2);width:100%}:host(.mk-table--stacked) .mk-table__td--select,:host(.mk-table--stacked) .mk-table__td--expand{width:auto;justify-content:flex-start}:host(.mk-table--stacked).mk-table--zebra .mk-table__body .mk-table__row:nth-child(2n) .mk-table__td{background:none}:host(.mk-table--stacked) .mk-table__detail-row,:host(.mk-table--stacked) .mk-table__detail{display:block;padding:0}:host(.mk-table--stacked) .mk-table__detail-row{margin:calc(-1 * var(--mk-space-3)) 0 var(--mk-space-3)}:host(.mk-table--stacked) .mk-table__group-row,:host(.mk-table--stacked) .mk-table__group{display:block;top:0}:host(.mk-table--stacked) .mk-table__row--empty,:host(.mk-table--stacked) .mk-table__empty{display:block;border:none;box-shadow:none;background:none}@media(pointer:coarse){.mk-table__cell-input{font-size:max(var(--mk-font-size-md),16px)}}\n"] }]
|
|
879
|
+
}], ctorParameters: () => [], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], stickyHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyHeader", required: false }] }], zebra: [{ type: i0.Input, args: [{ isSignal: true, alias: "zebra", required: false }] }], hover: [{ type: i0.Input, args: [{ isSignal: true, alias: "hover", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], stackAt: [{ type: i0.Input, args: [{ isSignal: true, alias: "stackAt", required: false }] }], clickableRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickableRows", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], trackKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackKey", required: false }] }], rowClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowClass", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], singleExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleExpand", required: false }] }], resizableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableColumns", required: false }] }], reorderableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderableColumns", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], groupLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupLabel", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], expandedChange: [{ type: i0.Output, args: ["expandedChange"] }], columnResize: [{ type: i0.Output, args: ["columnResize"] }], columnReorder: [{ type: i0.Output, args: ["columnReorder"] }], cellEdit: [{ type: i0.Output, args: ["cellEdit"] }], groupToggle: [{ type: i0.Output, args: ["groupToggle"] }], editInput: [{ type: i0.ViewChild, args: ['editInput', { isSignal: true }] }], rowDetail: [{ type: i0.ContentChild, args: [i0.forwardRef(() => MkTableRowDetail), { isSignal: true }] }], cellTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => MkTableCell), { isSignal: true }] }] } });
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Sort coordinator — apply `mkSort` to a table (or any container) to track
|
|
883
|
+
* which column is sorted and in which direction. It holds no data: register
|
|
884
|
+
* headers with `mkSortHeader`, then re-sort your rows in the
|
|
885
|
+
* `(mkSortChange)` handler. Mirrors the Angular Material `matSort` model but
|
|
886
|
+
* signal-based.
|
|
887
|
+
*
|
|
888
|
+
* Clicking a header cycles asc → desc → unsorted (set `mkSortDisableClear` to
|
|
889
|
+
* cycle asc ↔ desc only). `mkSortStart` flips the initial direction.
|
|
890
|
+
*
|
|
891
|
+
* ```html
|
|
892
|
+
* <table mkSort mkSortActive="name" mkSortDirection="asc"
|
|
893
|
+
* (mkSortChange)="sortData($event)">
|
|
894
|
+
* <thead><tr>
|
|
895
|
+
* <th mkSortHeader="name">Name</th>
|
|
896
|
+
* <th mkSortHeader="size" mkSortHeaderStart="desc">Size</th>
|
|
897
|
+
* </tr></thead>
|
|
898
|
+
* …
|
|
899
|
+
* </table>
|
|
900
|
+
* ```
|
|
901
|
+
*/
|
|
902
|
+
class MkSort {
|
|
903
|
+
announcer = inject(MkLiveAnnouncer);
|
|
904
|
+
i18n = inject(MK_I18N);
|
|
905
|
+
/** Id of the currently sorted column (two-way; empty when unsorted). */
|
|
906
|
+
active = model('', { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), alias: 'mkSortActive' });
|
|
907
|
+
/** Current sort direction (two-way). */
|
|
908
|
+
direction = model('none', { ...(ngDevMode ? { debugName: "direction" } : /* istanbul ignore next */ {}), alias: 'mkSortDirection' });
|
|
909
|
+
/** Direction the first click on a header applies. Default `asc`. */
|
|
910
|
+
start = input('asc', { ...(ngDevMode ? { debugName: "start" } : /* istanbul ignore next */ {}), alias: 'mkSortStart' });
|
|
911
|
+
/** Disable sorting for every header. */
|
|
912
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute,
|
|
913
|
+
alias: 'mkSortDisabled' });
|
|
914
|
+
/** Remove the "unsorted" step so headers cycle asc ↔ desc only. */
|
|
915
|
+
disableClear = input(false, { ...(ngDevMode ? { debugName: "disableClear" } : /* istanbul ignore next */ {}), transform: booleanAttribute,
|
|
916
|
+
alias: 'mkSortDisableClear' });
|
|
917
|
+
/** Emits the new sort state whenever a header is activated. */
|
|
918
|
+
sortChange = output({ alias: 'mkSortChange' });
|
|
919
|
+
/** Advance the sort state for the given header (called on click/keyboard). */
|
|
920
|
+
sort(header) {
|
|
921
|
+
if (this.disabled() || header.disabled())
|
|
922
|
+
return;
|
|
923
|
+
const id = header.id();
|
|
924
|
+
if (this.active() !== id) {
|
|
925
|
+
this.active.set(id);
|
|
926
|
+
this.direction.set(this.startFor(header));
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
const next = this.nextDirection(header, this.direction());
|
|
930
|
+
this.direction.set(next);
|
|
931
|
+
if (next === 'none')
|
|
932
|
+
this.active.set('');
|
|
933
|
+
}
|
|
934
|
+
const state = {
|
|
935
|
+
active: this.active(),
|
|
936
|
+
direction: this.direction(),
|
|
937
|
+
};
|
|
938
|
+
this.sortChange.emit(state);
|
|
939
|
+
this.announce(header, state.direction);
|
|
940
|
+
}
|
|
941
|
+
/** Whether the given column id is the active, non-cleared sort. */
|
|
942
|
+
isActive(id) {
|
|
943
|
+
return this.active() === id && this.direction() !== 'none';
|
|
944
|
+
}
|
|
945
|
+
startFor(header) {
|
|
946
|
+
return header.start() ?? this.start();
|
|
947
|
+
}
|
|
948
|
+
nextDirection(header, current) {
|
|
949
|
+
const order = this.startFor(header) === 'desc' ? ['desc', 'asc'] : ['asc', 'desc'];
|
|
950
|
+
if (!this.disableClear())
|
|
951
|
+
order.push('none');
|
|
952
|
+
const index = order.indexOf(current);
|
|
953
|
+
return order[(index + 1) % order.length];
|
|
954
|
+
}
|
|
955
|
+
announce(header, direction) {
|
|
956
|
+
const label = header.sortLabel() || header.id();
|
|
957
|
+
const message = direction === 'none'
|
|
958
|
+
? this.i18n.sortingCleared(label)
|
|
959
|
+
: this.i18n.sortedBy(label, direction === 'asc' ? 'asc' : 'desc');
|
|
960
|
+
this.announcer.announce(message, 'polite');
|
|
961
|
+
}
|
|
962
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSort, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
963
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkSort, isStandalone: true, selector: "[mkSort]", inputs: { active: { classPropertyName: "active", publicName: "mkSortActive", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "mkSortDirection", isSignal: true, isRequired: false, transformFunction: null }, start: { classPropertyName: "start", publicName: "mkSortStart", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "mkSortDisabled", isSignal: true, isRequired: false, transformFunction: null }, disableClear: { classPropertyName: "disableClear", publicName: "mkSortDisableClear", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { active: "mkSortActiveChange", direction: "mkSortDirectionChange", sortChange: "mkSortChange" }, exportAs: ["mkSort"], ngImport: i0 });
|
|
964
|
+
}
|
|
965
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSort, decorators: [{
|
|
966
|
+
type: Directive,
|
|
967
|
+
args: [{
|
|
968
|
+
selector: '[mkSort]',
|
|
969
|
+
exportAs: 'mkSort',
|
|
970
|
+
}]
|
|
971
|
+
}], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortActive", required: false }] }, { type: i0.Output, args: ["mkSortActiveChange"] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortDirection", required: false }] }, { type: i0.Output, args: ["mkSortDirectionChange"] }], start: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortStart", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortDisabled", required: false }] }], disableClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortDisableClear", required: false }] }], sortChange: [{ type: i0.Output, args: ["mkSortChange"] }] } });
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Sort header — attach `mkSortHeader` to a header cell (`<th>`) inside an
|
|
975
|
+
* element carrying `mkSort`. It wraps the projected header text in a real
|
|
976
|
+
* `<button>` (so assistive tech hears an operable control), adds a directional
|
|
977
|
+
* arrow (faint on hover, solid when active), reflects `aria-sort` on the cell,
|
|
978
|
+
* and toggles the sort on click or Enter/Space.
|
|
979
|
+
*
|
|
980
|
+
* ```html
|
|
981
|
+
* <th mkSortHeader="email" mkSortHeaderLabel="Email address">Email</th>
|
|
982
|
+
* ```
|
|
983
|
+
*/
|
|
984
|
+
class MkSortHeader {
|
|
985
|
+
sort = inject(MkSort);
|
|
986
|
+
/** Column id this header sorts by (the `mkSortHeader` value). */
|
|
987
|
+
id = input('', { ...(ngDevMode ? { debugName: "id" } : /* istanbul ignore next */ {}), alias: 'mkSortHeader' });
|
|
988
|
+
/** Per-header override for the initial sort direction. */
|
|
989
|
+
start = input(undefined, { ...(ngDevMode ? { debugName: "start" } : /* istanbul ignore next */ {}), alias: 'mkSortHeaderStart' });
|
|
990
|
+
/** Disable sorting for just this header. */
|
|
991
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute,
|
|
992
|
+
alias: 'mkSortHeaderDisabled' });
|
|
993
|
+
/** Accessible label used in sort announcements (defaults to the id). */
|
|
994
|
+
sortLabel = input('', { ...(ngDevMode ? { debugName: "sortLabel" } : /* istanbul ignore next */ {}), alias: 'mkSortHeaderLabel' });
|
|
995
|
+
/** Disabled if this header or the whole `mkSort` is disabled. */
|
|
996
|
+
isDisabled = computed(() => this.disabled() || this.sort.disabled(), /* @ts-ignore */
|
|
997
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
998
|
+
/** Whether this header is the active, non-cleared sort. */
|
|
999
|
+
isActive = computed(() => this.sort.isActive(this.id()), /* @ts-ignore */
|
|
1000
|
+
...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
|
|
1001
|
+
/** Active direction, or `none` when this header is not the active sort. */
|
|
1002
|
+
direction = computed(() => this.isActive() ? this.sort.direction() : 'none', /* @ts-ignore */
|
|
1003
|
+
...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
|
|
1004
|
+
/** `aria-sort` value for the header cell. */
|
|
1005
|
+
ariaSort = computed(() => {
|
|
1006
|
+
if (!this.isActive())
|
|
1007
|
+
return 'none';
|
|
1008
|
+
return this.sort.direction() === 'asc' ? 'ascending' : 'descending';
|
|
1009
|
+
}, /* @ts-ignore */
|
|
1010
|
+
...(ngDevMode ? [{ debugName: "ariaSort" }] : /* istanbul ignore next */ []));
|
|
1011
|
+
toggle() {
|
|
1012
|
+
if (!this.isDisabled())
|
|
1013
|
+
this.sort.sort(this);
|
|
1014
|
+
}
|
|
1015
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSortHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1016
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkSortHeader, isStandalone: true, selector: "[mkSortHeader]", inputs: { id: { classPropertyName: "id", publicName: "mkSortHeader", isSignal: true, isRequired: false, transformFunction: null }, start: { classPropertyName: "start", publicName: "mkSortHeaderStart", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "mkSortHeaderDisabled", isSignal: true, isRequired: false, transformFunction: null }, sortLabel: { classPropertyName: "sortLabel", publicName: "mkSortHeaderLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.mk-sort-header--active": "isActive()", "class.mk-sort-header--disabled": "isDisabled()", "attr.aria-sort": "ariaSort()" }, classAttribute: "mk-sort-header" }, ngImport: i0, template: "<!-- A real <button> rather than a clickable th: the cell keeps aria-sort,\n the button provides the operable-control semantics (and native\n Enter/Space activation) a screen-reader user expects. -->\n<button\n type=\"button\"\n class=\"mk-sort-header__button\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n>\n <span class=\"mk-sort-header__label\"><ng-content /></span>\n @if (!isDisabled()) {\n <span\n class=\"mk-sort-header__arrow\"\n [class.mk-sort-header__arrow--active]=\"isActive()\"\n [class.mk-sort-header__arrow--desc]=\"direction() === 'desc'\"\n aria-hidden=\"true\"\n ></span>\n }\n</button>\n", styles: ["@charset \"UTF-8\";:host{white-space:nowrap}:host(.mk-sort-header--active){color:var(--mk-primary)}.mk-sort-header__button{display:inline-flex;align-items:center;gap:var(--mk-space-1);width:100%;margin:calc(-1 * var(--mk-sort-header-pad-y, 0px)) calc(-1 * var(--mk-sort-header-pad-x, 0px));padding:var(--mk-sort-header-pad-y, 0px) var(--mk-sort-header-pad-x, 0px);font:inherit;font-weight:inherit;color:inherit;text-align:inherit;background:transparent;border:0;cursor:pointer;-webkit-user-select:none;user-select:none}.mk-sort-header__button:disabled{cursor:default}.mk-sort-header__button:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-sort-header__arrow{display:inline-block;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:5px solid currentColor;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard),transform var(--mk-duration-fast) var(--mk-ease-standard)}.mk-sort-header__button:hover .mk-sort-header__arrow,.mk-sort-header__button:focus-visible .mk-sort-header__arrow{opacity:.45}.mk-sort-header__arrow--active{opacity:1}.mk-sort-header__arrow--desc{transform:rotate(180deg)}@media(prefers-reduced-motion:reduce){.mk-sort-header__arrow{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1017
|
+
}
|
|
1018
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSortHeader, decorators: [{
|
|
1019
|
+
type: Component,
|
|
1020
|
+
args: [{ selector: '[mkSortHeader]', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1021
|
+
class: 'mk-sort-header',
|
|
1022
|
+
'[class.mk-sort-header--active]': 'isActive()',
|
|
1023
|
+
'[class.mk-sort-header--disabled]': 'isDisabled()',
|
|
1024
|
+
'[attr.aria-sort]': 'ariaSort()',
|
|
1025
|
+
}, template: "<!-- A real <button> rather than a clickable th: the cell keeps aria-sort,\n the button provides the operable-control semantics (and native\n Enter/Space activation) a screen-reader user expects. -->\n<button\n type=\"button\"\n class=\"mk-sort-header__button\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n>\n <span class=\"mk-sort-header__label\"><ng-content /></span>\n @if (!isDisabled()) {\n <span\n class=\"mk-sort-header__arrow\"\n [class.mk-sort-header__arrow--active]=\"isActive()\"\n [class.mk-sort-header__arrow--desc]=\"direction() === 'desc'\"\n aria-hidden=\"true\"\n ></span>\n }\n</button>\n", styles: ["@charset \"UTF-8\";:host{white-space:nowrap}:host(.mk-sort-header--active){color:var(--mk-primary)}.mk-sort-header__button{display:inline-flex;align-items:center;gap:var(--mk-space-1);width:100%;margin:calc(-1 * var(--mk-sort-header-pad-y, 0px)) calc(-1 * var(--mk-sort-header-pad-x, 0px));padding:var(--mk-sort-header-pad-y, 0px) var(--mk-sort-header-pad-x, 0px);font:inherit;font-weight:inherit;color:inherit;text-align:inherit;background:transparent;border:0;cursor:pointer;-webkit-user-select:none;user-select:none}.mk-sort-header__button:disabled{cursor:default}.mk-sort-header__button:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-sort-header__arrow{display:inline-block;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:5px solid currentColor;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard),transform var(--mk-duration-fast) var(--mk-ease-standard)}.mk-sort-header__button:hover .mk-sort-header__arrow,.mk-sort-header__button:focus-visible .mk-sort-header__arrow{opacity:.45}.mk-sort-header__arrow--active{opacity:1}.mk-sort-header__arrow--desc{transform:rotate(180deg)}@media(prefers-reduced-motion:reduce){.mk-sort-header__arrow{transition:none}}\n"] }]
|
|
1026
|
+
}], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortHeader", required: false }] }], start: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortHeaderStart", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortHeaderDisabled", required: false }] }], sortLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkSortHeaderLabel", required: false }] }] } });
|
|
1027
|
+
|
|
1028
|
+
/** Default debounce applied to {@link MkTableDataSource.setFilter}. */
|
|
1029
|
+
const DEFAULT_FILTER_DEBOUNCE = 300;
|
|
1030
|
+
/** Default page size, matching `mk-pagination`. */
|
|
1031
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
1032
|
+
/** Normalise any of the kit's sort payload shapes to `MkSortState | null`. */
|
|
1033
|
+
function normalizeSort(sort) {
|
|
1034
|
+
if (!sort)
|
|
1035
|
+
return null;
|
|
1036
|
+
const active = 'active' in sort ? sort.active : sort.key;
|
|
1037
|
+
if (!active || sort.direction === 'none')
|
|
1038
|
+
return null;
|
|
1039
|
+
return { active, direction: sort.direction };
|
|
1040
|
+
}
|
|
1041
|
+
/** Whether two normalised sort states describe the same ordering. */
|
|
1042
|
+
function sameSort(a, b) {
|
|
1043
|
+
if (a === b)
|
|
1044
|
+
return true;
|
|
1045
|
+
if (!a || !b)
|
|
1046
|
+
return false;
|
|
1047
|
+
return a.active === b.active && a.direction === b.direction;
|
|
1048
|
+
}
|
|
1049
|
+
/** Duck-typed Observable check, so rxjs is a type-only dependency here. */
|
|
1050
|
+
function isSubscribable(value) {
|
|
1051
|
+
return typeof value.subscribe === 'function';
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Server-side data adapter for `mk-table` — the page/sort/filter plumbing every
|
|
1055
|
+
* admin screen otherwise hand-rolls. A plain class (no component, no injection
|
|
1056
|
+
* required): give it a fetcher and bind its signals; every setter re-queries
|
|
1057
|
+
* the server and **stale responses never overwrite newer state** (latest-wins).
|
|
1058
|
+
*
|
|
1059
|
+
* - `setFilter` is debounced (default 300 ms); page, sort and page size load
|
|
1060
|
+
* immediately. Sort, filter and page-size changes reset to page 1.
|
|
1061
|
+
* - `rows` keeps its previous value while loading and on error, so the table
|
|
1062
|
+
* never blanks mid-transition; `error` is cleared by the next successful
|
|
1063
|
+
* load.
|
|
1064
|
+
* - Created in an injection context (a component field initialiser) it hooks
|
|
1065
|
+
* `DestroyRef` and cleans up automatically; anywhere else, call
|
|
1066
|
+
* {@link destroy} yourself.
|
|
1067
|
+
*
|
|
1068
|
+
* ```ts
|
|
1069
|
+
* interface User { id: number; name: string; email: string; }
|
|
1070
|
+
*
|
|
1071
|
+
* @Component({
|
|
1072
|
+
* imports: [MkTable, MkPagination, MkInput],
|
|
1073
|
+
* template: `
|
|
1074
|
+
* <input
|
|
1075
|
+
* mkInput
|
|
1076
|
+
* type="search"
|
|
1077
|
+
* placeholder="Search users…"
|
|
1078
|
+
* (input)="ds.setFilter($any($event.target).value)"
|
|
1079
|
+
* />
|
|
1080
|
+
*
|
|
1081
|
+
* <mk-table
|
|
1082
|
+
* [columns]="columns"
|
|
1083
|
+
* [data]="ds.rows()"
|
|
1084
|
+
* (sortChange)="ds.setSort($event)"
|
|
1085
|
+
* />
|
|
1086
|
+
* @if (ds.error()) { <p role="alert">Failed to load.</p> }
|
|
1087
|
+
* @if (ds.empty()) { <p>No users match.</p> }
|
|
1088
|
+
*
|
|
1089
|
+
* <mk-pagination
|
|
1090
|
+
* [total]="ds.total()"
|
|
1091
|
+
* [pageSize]="ds.pageSize()"
|
|
1092
|
+
* [page]="ds.page()"
|
|
1093
|
+
* (pageChange)="ds.setPage($event)"
|
|
1094
|
+
* />
|
|
1095
|
+
* `,
|
|
1096
|
+
* })
|
|
1097
|
+
* export class UsersPage {
|
|
1098
|
+
* private readonly http = inject(HttpClient);
|
|
1099
|
+
*
|
|
1100
|
+
* readonly columns: MkTableColumn<User>[] = [
|
|
1101
|
+
* { key: 'name', header: 'Name', sortable: true },
|
|
1102
|
+
* { key: 'email', header: 'Email', sortable: true },
|
|
1103
|
+
* ];
|
|
1104
|
+
*
|
|
1105
|
+
* // Field initialiser = injection context, so cleanup is automatic.
|
|
1106
|
+
* readonly ds = new MkTableDataSource<User>(
|
|
1107
|
+
* (req) =>
|
|
1108
|
+
* this.http.get<MkDataPage<User>>('/api/users', {
|
|
1109
|
+
* params: {
|
|
1110
|
+
* page: req.page,
|
|
1111
|
+
* size: req.pageSize,
|
|
1112
|
+
* q: req.filter,
|
|
1113
|
+
* ...(req.sort && {
|
|
1114
|
+
* sort: `${req.sort.active},${req.sort.direction}`,
|
|
1115
|
+
* }),
|
|
1116
|
+
* },
|
|
1117
|
+
* }),
|
|
1118
|
+
* { pageSize: 20 },
|
|
1119
|
+
* );
|
|
1120
|
+
* }
|
|
1121
|
+
* ```
|
|
1122
|
+
*
|
|
1123
|
+
* With a custom `mkSort` table, forward the directive instead of binding:
|
|
1124
|
+
*
|
|
1125
|
+
* ```ts
|
|
1126
|
+
* private readonly sort = viewChild.required(MkSort);
|
|
1127
|
+
* constructor() {
|
|
1128
|
+
* afterNextRender(() => this.ds.connectSort(this.sort()));
|
|
1129
|
+
* }
|
|
1130
|
+
* ```
|
|
1131
|
+
*/
|
|
1132
|
+
class MkTableDataSource {
|
|
1133
|
+
fetcher;
|
|
1134
|
+
debounceMs;
|
|
1135
|
+
_rows = signal([], /* @ts-ignore */
|
|
1136
|
+
...(ngDevMode ? [{ debugName: "_rows" }] : /* istanbul ignore next */ []));
|
|
1137
|
+
_total = signal(0, /* @ts-ignore */
|
|
1138
|
+
...(ngDevMode ? [{ debugName: "_total" }] : /* istanbul ignore next */ []));
|
|
1139
|
+
_loading = signal(false, /* @ts-ignore */
|
|
1140
|
+
...(ngDevMode ? [{ debugName: "_loading" }] : /* istanbul ignore next */ []));
|
|
1141
|
+
_error = signal(null, /* @ts-ignore */
|
|
1142
|
+
...(ngDevMode ? [{ debugName: "_error" }] : /* istanbul ignore next */ []));
|
|
1143
|
+
_page = signal(1, /* @ts-ignore */
|
|
1144
|
+
...(ngDevMode ? [{ debugName: "_page" }] : /* istanbul ignore next */ []));
|
|
1145
|
+
_pageSize = signal(DEFAULT_PAGE_SIZE, /* @ts-ignore */
|
|
1146
|
+
...(ngDevMode ? [{ debugName: "_pageSize" }] : /* istanbul ignore next */ []));
|
|
1147
|
+
_sort = signal(null, /* @ts-ignore */
|
|
1148
|
+
...(ngDevMode ? [{ debugName: "_sort" }] : /* istanbul ignore next */ []));
|
|
1149
|
+
_filter = signal('', /* @ts-ignore */
|
|
1150
|
+
...(ngDevMode ? [{ debugName: "_filter" }] : /* istanbul ignore next */ []));
|
|
1151
|
+
/** Rows of the current page (`[]` until the first load lands). */
|
|
1152
|
+
rows = this._rows.asReadonly();
|
|
1153
|
+
/** Total row count across all pages (feed to `mk-pagination`'s `total`). */
|
|
1154
|
+
total = this._total.asReadonly();
|
|
1155
|
+
/** True while the LATEST request is in flight. */
|
|
1156
|
+
loading = this._loading.asReadonly();
|
|
1157
|
+
/** The last load's error, or `null`; cleared by the next successful load. */
|
|
1158
|
+
error = this._error.asReadonly();
|
|
1159
|
+
/** Current 1-based page. */
|
|
1160
|
+
page = this._page.asReadonly();
|
|
1161
|
+
/** Current page size. */
|
|
1162
|
+
pageSize = this._pageSize.asReadonly();
|
|
1163
|
+
/** Current sort, or `null` when unsorted. */
|
|
1164
|
+
sort = this._sort.asReadonly();
|
|
1165
|
+
/** Current filter query (updates immediately, even while debouncing). */
|
|
1166
|
+
filter = this._filter.asReadonly();
|
|
1167
|
+
/** True when a settled load reported no rows at all. */
|
|
1168
|
+
empty = computed(() => !this._loading() && this._total() === 0, /* @ts-ignore */
|
|
1169
|
+
...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
|
|
1170
|
+
/** Monotonic request id — settles from older epochs are discarded. */
|
|
1171
|
+
epoch = 0;
|
|
1172
|
+
/** Subscription to an in-flight Observable fetch, if any. */
|
|
1173
|
+
activeSub = null;
|
|
1174
|
+
/** Pending filter-debounce timer. */
|
|
1175
|
+
filterTimer = null;
|
|
1176
|
+
/** Subscriptions created by {@link connectSort}, keyed for idempotence. */
|
|
1177
|
+
sortSubs = new Map();
|
|
1178
|
+
destroyed = false;
|
|
1179
|
+
constructor(fetcher, opts) {
|
|
1180
|
+
this.fetcher = fetcher;
|
|
1181
|
+
this.debounceMs = opts?.filterDebounce ?? DEFAULT_FILTER_DEBOUNCE;
|
|
1182
|
+
if (opts?.pageSize != null)
|
|
1183
|
+
this._pageSize.set(opts.pageSize);
|
|
1184
|
+
// Auto-cleanup when constructed in an injection context (a component
|
|
1185
|
+
// field initialiser). Outside one, inject() throws and the consumer owns
|
|
1186
|
+
// calling destroy().
|
|
1187
|
+
try {
|
|
1188
|
+
inject(DestroyRef).onDestroy(() => this.destroy());
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
// Not in an injection context — manual destroy().
|
|
1192
|
+
}
|
|
1193
|
+
this.load();
|
|
1194
|
+
}
|
|
1195
|
+
/** Jump to a 1-based page and load it immediately. */
|
|
1196
|
+
setPage(page) {
|
|
1197
|
+
const next = Math.max(1, Math.floor(page));
|
|
1198
|
+
if (next === this._page())
|
|
1199
|
+
return;
|
|
1200
|
+
this._page.set(next);
|
|
1201
|
+
this.load();
|
|
1202
|
+
}
|
|
1203
|
+
/** Change the page size; resets to page 1 and loads immediately. */
|
|
1204
|
+
setPageSize(size) {
|
|
1205
|
+
const next = Math.max(1, Math.floor(size));
|
|
1206
|
+
if (next === this._pageSize())
|
|
1207
|
+
return;
|
|
1208
|
+
this._pageSize.set(next);
|
|
1209
|
+
this._page.set(1);
|
|
1210
|
+
this.load();
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Change the sort; resets to page 1 and loads immediately. Accepts either
|
|
1214
|
+
* the `mkSort` directive's {@link MkSortState} or `mk-table`'s
|
|
1215
|
+
* {@link MkSortChange} payload; a cleared sort (`direction: 'none'` or an
|
|
1216
|
+
* empty column id) normalises to `null`. A no-op when the sort is unchanged.
|
|
1217
|
+
*/
|
|
1218
|
+
setSort(sort) {
|
|
1219
|
+
const next = normalizeSort(sort);
|
|
1220
|
+
if (sameSort(next, this._sort()))
|
|
1221
|
+
return;
|
|
1222
|
+
this._sort.set(next);
|
|
1223
|
+
this._page.set(1);
|
|
1224
|
+
this.load();
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Change the free-text filter. The `filter` signal updates (and the page
|
|
1228
|
+
* resets to 1) immediately, but the request is debounced — `refresh()`
|
|
1229
|
+
* flushes it early. A no-op when the query is unchanged.
|
|
1230
|
+
*/
|
|
1231
|
+
setFilter(query) {
|
|
1232
|
+
if (query === this._filter())
|
|
1233
|
+
return;
|
|
1234
|
+
this._filter.set(query);
|
|
1235
|
+
this._page.set(1);
|
|
1236
|
+
this.cancelDebounce();
|
|
1237
|
+
if (this.destroyed)
|
|
1238
|
+
return;
|
|
1239
|
+
this.filterTimer = setTimeout(() => {
|
|
1240
|
+
this.filterTimer = null;
|
|
1241
|
+
this.load();
|
|
1242
|
+
}, this.debounceMs);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Re-run the current request immediately (e.g. after a mutation). Flushes a
|
|
1246
|
+
* pending debounced filter, since the request reads the live filter value.
|
|
1247
|
+
*/
|
|
1248
|
+
refresh() {
|
|
1249
|
+
this.load();
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Pipe an `mkSort` directive's changes into {@link setSort}. Idempotent per
|
|
1253
|
+
* directive instance; all subscriptions are released by {@link destroy}.
|
|
1254
|
+
*/
|
|
1255
|
+
connectSort(sort) {
|
|
1256
|
+
if (this.destroyed || this.sortSubs.has(sort))
|
|
1257
|
+
return;
|
|
1258
|
+
this.sortSubs.set(sort, sort.sortChange.subscribe((state) => this.setSort(state)));
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Cancel pending work: the debounce timer, any in-flight Observable fetch,
|
|
1262
|
+
* and `connectSort` subscriptions. In-flight Promise settles are discarded.
|
|
1263
|
+
* Called automatically on host destroy when created in an injection context.
|
|
1264
|
+
*/
|
|
1265
|
+
destroy() {
|
|
1266
|
+
if (this.destroyed)
|
|
1267
|
+
return;
|
|
1268
|
+
this.destroyed = true;
|
|
1269
|
+
this.epoch++; // Anything still in flight settles stale.
|
|
1270
|
+
this.cancelDebounce();
|
|
1271
|
+
this.unsubscribeActive();
|
|
1272
|
+
for (const sub of this.sortSubs.values())
|
|
1273
|
+
sub.unsubscribe();
|
|
1274
|
+
this.sortSubs.clear();
|
|
1275
|
+
this._loading.set(false);
|
|
1276
|
+
}
|
|
1277
|
+
/** Start a load for the current request state; supersedes any in flight. */
|
|
1278
|
+
load() {
|
|
1279
|
+
if (this.destroyed)
|
|
1280
|
+
return;
|
|
1281
|
+
this.cancelDebounce();
|
|
1282
|
+
this.unsubscribeActive();
|
|
1283
|
+
const epoch = ++this.epoch;
|
|
1284
|
+
this._loading.set(true);
|
|
1285
|
+
const req = {
|
|
1286
|
+
page: this._page(),
|
|
1287
|
+
pageSize: this._pageSize(),
|
|
1288
|
+
sort: this._sort(),
|
|
1289
|
+
filter: this._filter(),
|
|
1290
|
+
};
|
|
1291
|
+
let result;
|
|
1292
|
+
try {
|
|
1293
|
+
result = this.fetcher(req);
|
|
1294
|
+
}
|
|
1295
|
+
catch (err) {
|
|
1296
|
+
this.settleError(epoch, err);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (isSubscribable(result)) {
|
|
1300
|
+
this.runObservable(result, epoch);
|
|
1301
|
+
}
|
|
1302
|
+
else {
|
|
1303
|
+
result.then((page) => this.settleSuccess(epoch, page), (err) => this.settleError(epoch, err));
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
/** Subscribe single-shot: first emission (or error) settles, then release. */
|
|
1307
|
+
runObservable(source, epoch) {
|
|
1308
|
+
let done = false;
|
|
1309
|
+
let sync = true;
|
|
1310
|
+
const sub = source.subscribe({
|
|
1311
|
+
next: (page) => {
|
|
1312
|
+
if (done)
|
|
1313
|
+
return;
|
|
1314
|
+
done = true;
|
|
1315
|
+
this.settleSuccess(epoch, page);
|
|
1316
|
+
if (!sync)
|
|
1317
|
+
this.clearSub(sub);
|
|
1318
|
+
},
|
|
1319
|
+
error: (err) => {
|
|
1320
|
+
if (done)
|
|
1321
|
+
return;
|
|
1322
|
+
done = true;
|
|
1323
|
+
this.settleError(epoch, err);
|
|
1324
|
+
if (!sync)
|
|
1325
|
+
this.clearSub(sub);
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
sync = false;
|
|
1329
|
+
if (done)
|
|
1330
|
+
sub.unsubscribe();
|
|
1331
|
+
else
|
|
1332
|
+
this.activeSub = sub;
|
|
1333
|
+
}
|
|
1334
|
+
/** Apply a successful settle, unless a newer request superseded it. */
|
|
1335
|
+
settleSuccess(epoch, page) {
|
|
1336
|
+
if (epoch !== this.epoch || this.destroyed)
|
|
1337
|
+
return;
|
|
1338
|
+
this._rows.set(page.rows);
|
|
1339
|
+
this._total.set(page.total);
|
|
1340
|
+
this._error.set(null);
|
|
1341
|
+
this._loading.set(false);
|
|
1342
|
+
}
|
|
1343
|
+
/** Record a failed settle (rows/total untouched), unless superseded. */
|
|
1344
|
+
settleError(epoch, err) {
|
|
1345
|
+
if (epoch !== this.epoch || this.destroyed)
|
|
1346
|
+
return;
|
|
1347
|
+
this._error.set(err);
|
|
1348
|
+
this._loading.set(false);
|
|
1349
|
+
}
|
|
1350
|
+
cancelDebounce() {
|
|
1351
|
+
if (this.filterTimer != null) {
|
|
1352
|
+
clearTimeout(this.filterTimer);
|
|
1353
|
+
this.filterTimer = null;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
unsubscribeActive() {
|
|
1357
|
+
if (this.activeSub) {
|
|
1358
|
+
this.activeSub.unsubscribe();
|
|
1359
|
+
this.activeSub = null;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
clearSub(sub) {
|
|
1363
|
+
if (this.activeSub === sub)
|
|
1364
|
+
this.activeSub = null;
|
|
1365
|
+
sub.unsubscribe();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* @mk-kit/ui/table — the data table, grid features and sort directives.
|
|
1371
|
+
*/
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Generated bundle index. Do not edit.
|
|
1375
|
+
*/
|
|
1376
|
+
|
|
1377
|
+
export { MkSort, MkSortHeader, MkTable, MkTableCell, MkTableDataSource, MkTableRowDetail };
|
|
1378
|
+
//# sourceMappingURL=mk-kit-ui-table.mjs.map
|