@kubex/zinc 1.1.120 → 1.1.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/custom-elements.json +1952 -921
- package/dist/vscode.html-custom-data.json +80 -58
- package/dist/web-types.json +208 -142
- package/dist/zn.d.ts +403 -298
- package/dist/zn.min.js +1511 -1370
- package/docs/pages/components/button.md +9 -0
- package/docs/pages/components/data-table-filter.md +60 -4
- package/docs/pages/components/data-table-search.md +3 -3
- package/docs/pages/components/data-table.md +78 -9
- package/docs/pages/components/datepicker.md +10 -0
- package/docs/pages/components/menu-item.md +16 -2
- package/docs/pages/components/translation-group.md +2 -2
- package/package.json +1 -1
- package/src/components/button/button.component.ts +11 -4
- package/src/components/button/button.scss +12 -1
- package/src/components/data-table/data-table.component.ts +341 -139
- package/src/components/data-table/data-table.scss +105 -42
- package/src/components/data-table/data-table.test.ts +110 -0
- package/src/components/data-table-filter/data-table-filter.component.ts +487 -49
- package/src/components/data-table-filter/data-table-filter.scss +163 -9
- package/src/components/data-table-filter/data-table-filter.test.ts +328 -2
- package/src/components/data-table-search/data-table-search.component.ts +1 -1
- package/src/components/datepicker/datepicker.component.ts +23 -2
- package/src/components/datepicker/datepicker.scss +25 -0
- package/src/components/datepicker/datepicker.test.ts +28 -1
- package/src/components/input/input.component.ts +1 -0
- package/src/components/input/input.scss +1 -1
- package/src/components/menu/menu.component.ts +2 -0
- package/src/components/menu/menu.scss +23 -2
- package/src/components/menu-item/menu-item.component.ts +7 -1
- package/src/components/menu-item/menu-item.scss +4 -4
- package/src/components/menu-item/menu-item.test.ts +39 -1
- package/src/components/panel/panel.component.ts +12 -5
- package/src/components/panel/panel.scss +3 -3
- package/src/components/query-builder/query-builder.component.ts +1 -1
- package/src/components/tooltip/tooltip.component.ts +2 -2
- package/src/components/tooltip/tooltip.test.ts +70 -2
- package/src/components/translation-group/translation-group.component.ts +5 -7
- package/src/components/translation-group/translation-group.test.ts +8 -8
- package/src/components/translations/translations.component.ts +3 -4
- package/src/components/translations/translations.test.ts +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {classMap} from "lit/directives/class-map.js";
|
|
2
|
-
import {type CSSResultGroup, html, nothing, type TemplateResult, unsafeCSS} from 'lit';
|
|
2
|
+
import {type CSSResultGroup, html, nothing, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
|
|
3
3
|
import {HasSlotController} from "../../internal/slot";
|
|
4
4
|
import {ifDefined} from "lit/directives/if-defined.js";
|
|
5
|
-
import {property, query} from 'lit/decorators.js';
|
|
5
|
+
import {property, query, state} from 'lit/decorators.js';
|
|
6
6
|
import {ref} from "lit/directives/ref.js";
|
|
7
7
|
import {ResizeController} from '@lit-labs/observers/resize-controller.js';
|
|
8
8
|
import {Task} from "@lit/task";
|
|
@@ -22,6 +22,7 @@ import ZnEmptyState from "../empty-state";
|
|
|
22
22
|
import ZnHoverContainer from "../hover-container";
|
|
23
23
|
import ZnMenu from "../menu";
|
|
24
24
|
import ZnMenuItem from "../menu-item";
|
|
25
|
+
import ZnPanel from "../panel";
|
|
25
26
|
import ZnSkeleton from "../skeleton";
|
|
26
27
|
import ZnStyle from "../style";
|
|
27
28
|
import type ZnDataSelect from "../data-select";
|
|
@@ -35,6 +36,9 @@ import styles from './data-table.scss';
|
|
|
35
36
|
|
|
36
37
|
const DEFAULT_PAGE = 1;
|
|
37
38
|
const DEFAULT_PER_PAGE = 10;
|
|
39
|
+
// Numbered page buttons shown at once; the window slides so the control never grows
|
|
40
|
+
const PAGE_WINDOW = 3;
|
|
41
|
+
const SUGGESTION_LIMIT = 25;
|
|
38
42
|
|
|
39
43
|
interface Cell {
|
|
40
44
|
text: string;
|
|
@@ -57,6 +61,11 @@ interface Cell {
|
|
|
57
61
|
title?: string;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
interface RowGroup {
|
|
65
|
+
label: string;
|
|
66
|
+
rows: Row[];
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
interface Row {
|
|
61
70
|
id: string;
|
|
62
71
|
uri?: string;
|
|
@@ -167,19 +176,20 @@ type AllowedInputElement =
|
|
|
167
176
|
* @dependency zn-dropdown
|
|
168
177
|
* @dependency zn-menu
|
|
169
178
|
* @dependency zn-menu-item
|
|
179
|
+
* @dependency zn-panel
|
|
170
180
|
* @dependency zn-button-group
|
|
171
181
|
* @dependency zn-confirm
|
|
172
182
|
* @dependency zn-skeleton
|
|
173
183
|
* @dependency zn-data-table-search
|
|
174
184
|
*
|
|
175
185
|
* @slot - The default slot.
|
|
176
|
-
* @slot search - Slot for search component.
|
|
177
|
-
* @slot sort - Slot for sort component.
|
|
178
|
-
* @slot filter - Slot for filter component.
|
|
179
|
-
* @slot filter-top - Slot for top-level filter component.
|
|
180
|
-
* @slot delete-action - Slot for delete action button.
|
|
181
|
-
* @slot modify-action - Slot for modify action button.
|
|
182
|
-
* @slot create-action - Slot for create action button.
|
|
186
|
+
* @slot search - Slot for search component, in the header's right-hand group.
|
|
187
|
+
* @slot sort - Slot for sort component, in the header's right-hand group.
|
|
188
|
+
* @slot filter - Slot for filter component, in the header's right-hand group.
|
|
189
|
+
* @slot filter-top - Slot for a top-level filter component, rendered above the table's panel.
|
|
190
|
+
* @slot delete-action - Slot for delete action button, shown beside the caption once rows are selected.
|
|
191
|
+
* @slot modify-action - Slot for modify action button, shown beside the caption once rows are selected.
|
|
192
|
+
* @slot create-action - Slot for create action button, at the end of the header's right-hand group.
|
|
183
193
|
* @slot inputs - Slot for additional input controls.
|
|
184
194
|
* @slot empty-state - Slot for custom empty state.
|
|
185
195
|
* @slot no-results - Slot for a custom no-results state, shown when a search on a no-initial-load table returns no rows.
|
|
@@ -199,6 +209,7 @@ export default class ZnDataTable extends ZincElement {
|
|
|
199
209
|
'zn-dropdown': ZnDropdown,
|
|
200
210
|
'zn-menu': ZnMenu,
|
|
201
211
|
'zn-menu-item': ZnMenuItem,
|
|
212
|
+
'zn-panel': ZnPanel,
|
|
202
213
|
'zn-button-group': ZnButtonGroup,
|
|
203
214
|
'zn-confirm': ZnConfirm,
|
|
204
215
|
'zn-skeleton': ZnSkeleton,
|
|
@@ -234,6 +245,13 @@ export default class ZnDataTable extends ZincElement {
|
|
|
234
245
|
// Hide pagination
|
|
235
246
|
@property({attribute: 'hide-pagination', type: Boolean}) hidePagination: boolean;
|
|
236
247
|
|
|
248
|
+
// Hide the column select dropdown in the table header
|
|
249
|
+
@property({attribute: 'hide-column-select', type: Boolean}) hideColumnSelect: boolean = false;
|
|
250
|
+
|
|
251
|
+
// Hide the refresh button in the table header
|
|
252
|
+
@property({attribute: 'hide-refresh', type: Boolean}) hideRefresh: boolean = false;
|
|
253
|
+
|
|
254
|
+
// Wraps the caption, header controls, rows and pagination in a zn-panel
|
|
237
255
|
@property({type: Boolean}) standalone: boolean = false;
|
|
238
256
|
|
|
239
257
|
@property() caption: string;
|
|
@@ -276,8 +294,11 @@ export default class ZnDataTable extends ZincElement {
|
|
|
276
294
|
// private itemsPerPage: number = DEFAULT_PER_PAGE;
|
|
277
295
|
private page: number = DEFAULT_PAGE;
|
|
278
296
|
private totalPages: number;
|
|
297
|
+
private _totalRows = 0;
|
|
279
298
|
|
|
280
|
-
private _rows:
|
|
299
|
+
private _rows: Row[] = [];
|
|
300
|
+
|
|
301
|
+
private _suggestionsKey = '';
|
|
281
302
|
|
|
282
303
|
private numberOfRowsSelected: number = 0;
|
|
283
304
|
private selectedRows: any[] = [];
|
|
@@ -361,6 +382,9 @@ export default class ZnDataTable extends ZincElement {
|
|
|
361
382
|
private _expandedRows: Set<string> = new Set();
|
|
362
383
|
private _hiddenCells: Map<string, Cell[]> = new Map();
|
|
363
384
|
private _secondaryHeaders: HeaderConfig[];
|
|
385
|
+
@state() private _deselectedColumns: Set<string> = new Set();
|
|
386
|
+
@state() private _filtersOpen = false;
|
|
387
|
+
private _columnDefaultsApplied = false;
|
|
364
388
|
private _formatTemplates: Record<string, DisplayTemplate> = defaultTemplates;
|
|
365
389
|
|
|
366
390
|
requestParams: Record<string, any> = {};
|
|
@@ -371,13 +395,60 @@ export default class ZnDataTable extends ZincElement {
|
|
|
371
395
|
this._dataTask.run().then(r => r);
|
|
372
396
|
}
|
|
373
397
|
|
|
398
|
+
// Headers that make up the visible columns, in header order
|
|
399
|
+
private visibleHeaders(): HeaderConfig[] {
|
|
400
|
+
return this.selectableHeaders().filter((header: HeaderConfig) => this.isColumnVisible(header));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Headers the column select can toggle - everything but the permanently hidden and secondary ones
|
|
404
|
+
private selectableHeaders(): HeaderConfig[] {
|
|
405
|
+
return (Object.values(this.headers) as HeaderConfig[]).filter((header: HeaderConfig) => {
|
|
406
|
+
if (header.hideHeader || header.hideColumn || header.secondary) return false;
|
|
407
|
+
|
|
408
|
+
return !Object.values(this.hiddenColumns).includes(header.key);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private isColumnVisible(header: HeaderConfig): boolean {
|
|
413
|
+
return header.required === true || !this._deselectedColumns.has(header.key);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private applyColumnDefaults() {
|
|
417
|
+
if (this._columnDefaultsApplied) return;
|
|
418
|
+
|
|
419
|
+
const headers = Object.values(this.headers) as HeaderConfig[];
|
|
420
|
+
if (headers.length === 0) return;
|
|
421
|
+
|
|
422
|
+
this._columnDefaultsApplied = true;
|
|
423
|
+
const deselected = new Set(this._deselectedColumns);
|
|
424
|
+
headers.forEach((header: HeaderConfig) => {
|
|
425
|
+
if (header.default === false && header.required !== true) deselected.add(header.key);
|
|
426
|
+
});
|
|
427
|
+
this._deselectedColumns = deselected;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private toggleColumn(header: HeaderConfig) {
|
|
431
|
+
if (header.required === true) return;
|
|
432
|
+
|
|
433
|
+
const deselected = new Set(this._deselectedColumns);
|
|
434
|
+
if (!deselected.delete(header.key)) {
|
|
435
|
+
// Never let the last visible column be turned off
|
|
436
|
+
if (this.visibleHeaders().length <= 1) return;
|
|
437
|
+
deselected.add(header.key);
|
|
438
|
+
}
|
|
439
|
+
this._deselectedColumns = deselected;
|
|
440
|
+
}
|
|
441
|
+
|
|
374
442
|
render() {
|
|
443
|
+
this.applyColumnDefaults();
|
|
375
444
|
|
|
376
445
|
// If no-initial-load is set, do not invoke the Task on the first render
|
|
377
446
|
let tableBody: TemplateResult = html``;
|
|
447
|
+
let emptyBody = false;
|
|
378
448
|
if (this.noInitialLoad && this._initialLoad) {
|
|
379
449
|
tableBody = html`
|
|
380
450
|
<slot name="empty-state"></slot>`;
|
|
451
|
+
emptyBody = true;
|
|
381
452
|
} else if (this.dataUri) {
|
|
382
453
|
tableBody = this._dataTask.render({
|
|
383
454
|
pending: () => {
|
|
@@ -396,6 +467,7 @@ export default class ZnDataTable extends ZincElement {
|
|
|
396
467
|
this._initialLoad = false;
|
|
397
468
|
this._hasLoadedData = true;
|
|
398
469
|
this._lastLoadHadRows = ((data as Response)?.rows?.length ?? 0) > 0;
|
|
470
|
+
emptyBody = !this._lastLoadHadRows;
|
|
399
471
|
this._lastTableContent = html`
|
|
400
472
|
<div>${this.renderTable(data as Response)}</div>`;
|
|
401
473
|
return this._lastTableContent;
|
|
@@ -436,19 +508,46 @@ export default class ZnDataTable extends ZincElement {
|
|
|
436
508
|
|| this.hasSlotController.test(ActionSlots.modify.valueOf())
|
|
437
509
|
|| this.hasSlotController.test(ActionSlots.create.valueOf())
|
|
438
510
|
|| this.hasSlotController.test(ActionSlots.sort.valueOf())
|
|
439
|
-
|| this.hasSlotController.test(ActionSlots.filter.valueOf())
|
|
440
|
-
|| this.hasSlotController.test(ActionSlots.filter_top.valueOf())
|
|
441
511
|
|| this.hasSlotController.test(ActionSlots.search.valueOf());
|
|
442
512
|
|
|
443
513
|
const hasInputs = this.hasSlotController.test(ActionSlots.inputs.valueOf());
|
|
514
|
+
const hasControls = hasActions || this.hasColumnSelect() || this.hasRefresh();
|
|
515
|
+
|
|
516
|
+
// An empty state with no header has nothing to hang off the panel, and consumers
|
|
517
|
+
// often bring their own panel for it, so let it stand on its own
|
|
518
|
+
const bareEmptyState = emptyBody && !hasControls && !this.caption;
|
|
519
|
+
|
|
520
|
+
// The filter bar is inline, so it gets its own row rather than a slot in the header
|
|
521
|
+
const filters = this.hasSlotController.test(ActionSlots.filter.valueOf())
|
|
522
|
+
? html`
|
|
523
|
+
<div class="table__filters" ?hidden="${!this._filtersOpen}">
|
|
524
|
+
<slot name="${ActionSlots.filter.valueOf()}"></slot>
|
|
525
|
+
</div>`
|
|
526
|
+
: nothing;
|
|
444
527
|
|
|
445
528
|
// Headers do not need to be re-rendered with new data
|
|
446
529
|
return html`
|
|
447
530
|
<div class="table-container" ${ref((el) => (this.tableContainer = el))}>
|
|
448
531
|
${hasInputs ? html`
|
|
449
532
|
<slot name="${ActionSlots.inputs.valueOf()}" style="display: none"></slot>` : null}
|
|
450
|
-
${
|
|
451
|
-
${
|
|
533
|
+
<slot name="${ActionSlots.filter_top.valueOf()}"></slot>
|
|
534
|
+
${this.standalone && !bareEmptyState
|
|
535
|
+
? html`
|
|
536
|
+
<zn-panel caption="${ifDefined(this.caption || undefined)}" flush>
|
|
537
|
+
${hasControls ? html`
|
|
538
|
+
<div slot="actions" class="table__header__right">
|
|
539
|
+
${this.getActions()}
|
|
540
|
+
${this.getHeaderControls()}
|
|
541
|
+
</div>` : nothing}
|
|
542
|
+
${filters}
|
|
543
|
+
${tableBody}
|
|
544
|
+
${this.getTableFooter('footer')}
|
|
545
|
+
</zn-panel>`
|
|
546
|
+
: html`
|
|
547
|
+
${hasControls || this.caption ? this.getTableHeader() : nothing}
|
|
548
|
+
${filters}
|
|
549
|
+
${tableBody}
|
|
550
|
+
${this.getTableFooter()}`}
|
|
452
551
|
</div>
|
|
453
552
|
`;
|
|
454
553
|
}
|
|
@@ -456,9 +555,37 @@ export default class ZnDataTable extends ZincElement {
|
|
|
456
555
|
connectedCallback() {
|
|
457
556
|
super.connectedCallback();
|
|
458
557
|
this.addEventListener('zn-filter-change', this.filterChangeListener);
|
|
558
|
+
this.addEventListener('zn-clear', this.filterClearListener);
|
|
459
559
|
this.addEventListener('zn-search-change', this.searchChangeListener);
|
|
460
560
|
}
|
|
461
561
|
|
|
562
|
+
protected updated(changed: PropertyValues) {
|
|
563
|
+
super.updated(changed);
|
|
564
|
+
this.publishFilterSuggestions();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// The filter bar can't see the data, so hand it the values already on screen to type against
|
|
568
|
+
private publishFilterSuggestions() {
|
|
569
|
+
const bar = this.querySelector('zn-data-table-filter');
|
|
570
|
+
if (!bar || this._rows.length === 0) return;
|
|
571
|
+
|
|
572
|
+
const suggestions: Record<string, string[]> = {};
|
|
573
|
+
|
|
574
|
+
this._rows.forEach((row: Row) => row.cells.forEach((cell: Cell) => {
|
|
575
|
+
const text = cell.text?.trim();
|
|
576
|
+
if (!text) return;
|
|
577
|
+
|
|
578
|
+
const values = suggestions[cell.column] ?? (suggestions[cell.column] = []);
|
|
579
|
+
if (values.length < SUGGESTION_LIMIT && !values.includes(text)) values.push(text);
|
|
580
|
+
}));
|
|
581
|
+
|
|
582
|
+
const key = JSON.stringify(suggestions);
|
|
583
|
+
if (key === this._suggestionsKey) return;
|
|
584
|
+
|
|
585
|
+
this._suggestionsKey = key;
|
|
586
|
+
bar.suggestions = suggestions;
|
|
587
|
+
}
|
|
588
|
+
|
|
462
589
|
private getTemplate(name: string): DisplayTemplate | undefined {
|
|
463
590
|
return this.displayTemplates[name] ?? this._formatTemplates[name];
|
|
464
591
|
}
|
|
@@ -466,9 +593,17 @@ export default class ZnDataTable extends ZincElement {
|
|
|
466
593
|
disconnectedCallback() {
|
|
467
594
|
super.disconnectedCallback();
|
|
468
595
|
this.removeEventListener('zn-filter-change', this.filterChangeListener);
|
|
596
|
+
this.removeEventListener('zn-clear', this.filterClearListener);
|
|
469
597
|
this.removeEventListener('zn-search-change', this.searchChangeListener);
|
|
470
598
|
}
|
|
471
599
|
|
|
600
|
+
// Clearing the filters leaves an empty row behind, so fold it away
|
|
601
|
+
filterClearListener = (e: Event) => {
|
|
602
|
+
if (e.target instanceof ZnDataTableFilter) {
|
|
603
|
+
this._filtersOpen = false;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
472
607
|
filterChangeListener = (e: ZnFilterChangeEvent) => {
|
|
473
608
|
if (e.target instanceof ZnDataTableFilter) {
|
|
474
609
|
this.filter = (e.target as ZnDataTableFilter).value as string;
|
|
@@ -542,6 +677,7 @@ export default class ZnDataTable extends ZincElement {
|
|
|
542
677
|
renderTable(data: Response) {
|
|
543
678
|
this.itemsPerPage = Math.max(DEFAULT_PER_PAGE, data.perPage ?? DEFAULT_PER_PAGE);
|
|
544
679
|
this.page = Math.max(1, data.page ?? DEFAULT_PAGE);
|
|
680
|
+
this._totalRows = Math.max(0, data.total ?? 0);
|
|
545
681
|
this.totalPages = Math.ceil(Math.max(1, data.total) / this.itemsPerPage);
|
|
546
682
|
|
|
547
683
|
const error = data?.error?.text ? data.error : undefined;
|
|
@@ -610,21 +746,15 @@ export default class ZnDataTable extends ZincElement {
|
|
|
610
746
|
}
|
|
611
747
|
});
|
|
612
748
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
<div class="table-group">
|
|
618
|
-
<h3 class="table-group__title">${(groupKey === "Ungrouped" || groupKey === "*") ? "" : groupKey}</h3>
|
|
619
|
-
${this.renderTableData(groupedRows[groupKey], index === 0 ? error : undefined)}
|
|
620
|
-
</div>`
|
|
621
|
-
)}
|
|
622
|
-
</zn-sp>
|
|
623
|
-
`;
|
|
624
|
-
}
|
|
749
|
+
const groups: RowGroup[] = Object.keys(groupedRows).map((groupKey) => ({
|
|
750
|
+
label: (groupKey === 'Ungrouped' || groupKey === '*') ? '' : groupKey,
|
|
751
|
+
rows: groupedRows[groupKey],
|
|
752
|
+
}));
|
|
625
753
|
|
|
754
|
+
return this.renderTableData(groups, error);
|
|
755
|
+
}
|
|
626
756
|
|
|
627
|
-
return this.renderTableData(this._rows, error);
|
|
757
|
+
return this.renderTableData([{label: '', rows: this._rows}], error);
|
|
628
758
|
}
|
|
629
759
|
|
|
630
760
|
private renderErrorAlert(error: ResponseError) {
|
|
@@ -640,15 +770,9 @@ export default class ZnDataTable extends ZincElement {
|
|
|
640
770
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
641
771
|
}
|
|
642
772
|
|
|
643
|
-
public renderTableData(
|
|
644
|
-
// Primary (visible) headers exclude those explicitly hidden and those marked as secondary
|
|
645
|
-
const filteredHeaders =
|
|
646
|
-
if (header.hideHeader || header.hideColumn) return false;
|
|
647
|
-
|
|
648
|
-
if (Object.values(this.hiddenColumns).includes(header.key)) return false;
|
|
649
|
-
|
|
650
|
-
return !header.secondary;
|
|
651
|
-
});
|
|
773
|
+
public renderTableData(groups: RowGroup[], error?: ResponseError) {
|
|
774
|
+
// Primary (visible) headers exclude those explicitly hidden, deselected and those marked as secondary
|
|
775
|
+
const filteredHeaders = this.visibleHeaders();
|
|
652
776
|
|
|
653
777
|
// Secondary headers (shown in expandable details)
|
|
654
778
|
this._secondaryHeaders = Object.values(this.headers).filter((header: HeaderConfig) => {
|
|
@@ -689,10 +813,9 @@ export default class ZnDataTable extends ZincElement {
|
|
|
689
813
|
const colCount = filteredHeaders.length + (this.rowHasActions ? 1 : 0) + (anyHidden ? 1 : 0);
|
|
690
814
|
|
|
691
815
|
return html`
|
|
692
|
-
<div
|
|
816
|
+
<div class="table__scroll">
|
|
693
817
|
<table class="${classMap({
|
|
694
818
|
'table': true,
|
|
695
|
-
'table--standalone': this.standalone,
|
|
696
819
|
'with-hover': !this.unsortable && !this.hideCheckboxes,
|
|
697
820
|
})}">
|
|
698
821
|
<thead>
|
|
@@ -709,60 +832,167 @@ export default class ZnDataTable extends ZincElement {
|
|
|
709
832
|
<tr class="table__row--error">
|
|
710
833
|
<td colspan="${colCount}">${this.renderErrorAlert(error)}</td>
|
|
711
834
|
</tr>` : nothing}
|
|
712
|
-
${
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
${(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
835
|
+
${groups.map((group: RowGroup) => html`
|
|
836
|
+
${group.label ? html`
|
|
837
|
+
<tr class="table__row--group">
|
|
838
|
+
<th colspan="${colCount}" scope="colgroup">${group.label}</th>
|
|
839
|
+
</tr>` : nothing}
|
|
840
|
+
${group.rows.map((row: Row) => html`
|
|
841
|
+
<tr class="${classMap({
|
|
842
|
+
'table__row--selected': this.isRowSelected(row),
|
|
843
|
+
'table__row--data': true,
|
|
844
|
+
})}" data-row-id="${row.id}">
|
|
845
|
+
${anyHidden ? this.renderExpanderCell(row) : nothing}
|
|
846
|
+
${(visibleRowCells.get(row.id) || row.cells).map((value: Cell, index: number) => this.renderCellBody(index, value, row))}
|
|
847
|
+
${this.rowHasActions ? this.renderActions(row) : nothing}
|
|
848
|
+
</tr>
|
|
849
|
+
${this._expandedRows.has(row.id) ? this.renderDetailsRow(row, colCount) : nothing}
|
|
850
|
+
`)}
|
|
722
851
|
`)}
|
|
723
852
|
</tbody>
|
|
724
853
|
</table>
|
|
725
854
|
</div>
|
|
726
|
-
|
|
727
|
-
${this.getTableFooter()}
|
|
728
855
|
`;
|
|
729
856
|
}
|
|
730
857
|
|
|
731
858
|
getTableHeader() {
|
|
732
859
|
return html`
|
|
733
|
-
<slot name="${ActionSlots.filter_top.valueOf()}"></slot>
|
|
734
860
|
<div class="table__header">
|
|
735
861
|
<div class="table__header__actions">
|
|
862
|
+
${this.caption ? html`
|
|
863
|
+
<h3 class="table__header__caption">${this.caption}</h3>` : nothing}
|
|
736
864
|
${this.getActions()}
|
|
737
865
|
</div>
|
|
738
866
|
<div class="table__header__right">
|
|
739
|
-
|
|
740
|
-
<slot name="${ActionSlots.sort.valueOf()}"></slot>
|
|
741
|
-
<slot name="${ActionSlots.filter.valueOf()}"></slot>
|
|
867
|
+
${this.getHeaderControls()}
|
|
742
868
|
</div>
|
|
743
869
|
</div>
|
|
744
870
|
`;
|
|
745
871
|
}
|
|
746
872
|
|
|
747
|
-
|
|
873
|
+
private getHeaderControls() {
|
|
874
|
+
return html`
|
|
875
|
+
${this.getColumnSelect()}
|
|
876
|
+
${this.getFilterToggle()}
|
|
877
|
+
${this.getRefreshButton()}
|
|
878
|
+
<slot name="${ActionSlots.sort.valueOf()}"></slot>
|
|
879
|
+
<slot name="${ActionSlots.search.valueOf()}"></slot>
|
|
880
|
+
<slot name="${ActionSlots.create.valueOf()}"></slot>`;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Nothing to pick columns on or refresh into while the table has no rows
|
|
884
|
+
private hasRows(): boolean {
|
|
885
|
+
return this._hasLoadedData && this._lastLoadHadRows;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
private getFilterToggle() {
|
|
889
|
+
if (!this.hasSlotController.test(ActionSlots.filter.valueOf())) return nothing;
|
|
890
|
+
|
|
891
|
+
const applied = this.appliedFilterCount();
|
|
892
|
+
|
|
893
|
+
return html`
|
|
894
|
+
<zn-button icon-button="small"
|
|
895
|
+
icon="funnel@lu"
|
|
896
|
+
icon-size="18"
|
|
897
|
+
tooltip="Filter"
|
|
898
|
+
aria-label="Filter"
|
|
899
|
+
muted-notifications
|
|
900
|
+
notification="${applied || nothing}"
|
|
901
|
+
aria-pressed="${this._filtersOpen}"
|
|
902
|
+
class="${classMap({'table__header__toggle--active': applied > 0})}"
|
|
903
|
+
@click="${this.toggleFilters}">
|
|
904
|
+
</zn-button>`;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private toggleFilters = () => {
|
|
908
|
+
this._filtersOpen = !this._filtersOpen;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private appliedFilterCount(): number {
|
|
912
|
+
if (!this.filter) return 0;
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
const applied: unknown = JSON.parse(atob(this.filter));
|
|
916
|
+
return Array.isArray(applied) ? applied.length : 0;
|
|
917
|
+
} catch {
|
|
918
|
+
return 0;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private hasColumnSelect(): boolean {
|
|
923
|
+
return !this.hideColumnSelect && this.hasRows() && this.selectableHeaders().length > 1;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private hasRefresh(): boolean {
|
|
927
|
+
return !this.hideRefresh && this.hasRows() && !!this.dataUri;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
private getColumnSelect() {
|
|
931
|
+
if (!this.hasColumnSelect()) return nothing;
|
|
932
|
+
|
|
933
|
+
return html`
|
|
934
|
+
<zn-dropdown placement="bottom-end" stay-open-on-select>
|
|
935
|
+
<zn-button slot="trigger"
|
|
936
|
+
icon-button="small"
|
|
937
|
+
icon="columns-3@lu"
|
|
938
|
+
icon-size="18"
|
|
939
|
+
tooltip="Columns"
|
|
940
|
+
aria-label="Select columns">
|
|
941
|
+
</zn-button>
|
|
942
|
+
<zn-menu variant="shell" @zn-menu-select="${this.handleColumnSelect}">
|
|
943
|
+
${this.selectableHeaders().map((header: HeaderConfig) => html`
|
|
944
|
+
<zn-menu-item type="checkbox"
|
|
945
|
+
value="${header.key}"
|
|
946
|
+
?checked="${this.isColumnVisible(header)}"
|
|
947
|
+
?disabled="${header.required === true}">${header.label}
|
|
948
|
+
</zn-menu-item>`)}
|
|
949
|
+
</zn-menu>
|
|
950
|
+
</zn-dropdown>`;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private getRefreshButton() {
|
|
954
|
+
if (!this.hasRefresh()) return nothing;
|
|
955
|
+
|
|
956
|
+
return html`
|
|
957
|
+
<zn-button icon-button="small"
|
|
958
|
+
icon="refresh-cw@lu"
|
|
959
|
+
icon-size="18"
|
|
960
|
+
tooltip="Refresh"
|
|
961
|
+
aria-label="Refresh data"
|
|
962
|
+
@click="${this.refresh}">
|
|
963
|
+
</zn-button>`;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private handleColumnSelect = (event: CustomEvent<{ value: string }>) => {
|
|
967
|
+
const header = (Object.values(this.headers) as HeaderConfig[])
|
|
968
|
+
.find((h: HeaderConfig) => h.key === event.detail.value);
|
|
969
|
+
|
|
970
|
+
if (header) this.toggleColumn(header);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Returns null when there is nothing to show, so a panel footer is only slotted when it has content
|
|
974
|
+
getTableFooter(slot?: string) {
|
|
975
|
+
if (!this._hasLoadedData) return null;
|
|
976
|
+
|
|
748
977
|
const rowSelected = this.getRowsSelected();
|
|
749
978
|
const pagination = this.getPagination();
|
|
750
979
|
const rowsPerPage = this.getRowsPerPage();
|
|
751
980
|
|
|
752
|
-
if (rowSelected
|
|
753
|
-
return html`
|
|
754
|
-
<div class="table__footer">
|
|
755
|
-
<div class="table__footer__left">
|
|
756
|
-
${pagination}
|
|
757
|
-
${rowSelected}
|
|
758
|
-
</div>
|
|
759
|
-
<div class="table__footer__right">
|
|
760
|
-
${rowsPerPage}
|
|
761
|
-
</div>
|
|
762
|
-
</div>`;
|
|
763
|
-
}
|
|
981
|
+
if (rowSelected === null && pagination === null && rowsPerPage === null) return null;
|
|
764
982
|
|
|
765
|
-
return html
|
|
983
|
+
return html`
|
|
984
|
+
<div class="${classMap({
|
|
985
|
+
'table__footer': true,
|
|
986
|
+
'table__footer--panel': !!slot,
|
|
987
|
+
})}" slot="${ifDefined(slot)}">
|
|
988
|
+
<div class="table__footer__left">
|
|
989
|
+
${pagination}
|
|
990
|
+
${rowSelected}
|
|
991
|
+
</div>
|
|
992
|
+
<div class="table__footer__right">
|
|
993
|
+
${rowsPerPage}
|
|
994
|
+
</div>
|
|
995
|
+
</div>`;
|
|
766
996
|
}
|
|
767
997
|
|
|
768
998
|
getRowsSelected() {
|
|
@@ -773,14 +1003,17 @@ export default class ZnDataTable extends ZincElement {
|
|
|
773
1003
|
}
|
|
774
1004
|
|
|
775
1005
|
getRowsPerPage() {
|
|
776
|
-
if (this.hidePagination
|
|
1006
|
+
if (this.hidePagination) return null;
|
|
777
1007
|
|
|
778
1008
|
const optionsRowsPerPage = [10, 20, 30, 40, 50];
|
|
779
|
-
|
|
1009
|
+
|
|
1010
|
+
// Keyed to the dataset, not the current page size - raising the size past the total
|
|
1011
|
+
// must not hide the only control that can lower it again
|
|
1012
|
+
if (this._totalRows <= optionsRowsPerPage[0]) return null;
|
|
780
1013
|
|
|
781
1014
|
return html`
|
|
782
1015
|
<div class="table__footer__rows-per-page">
|
|
783
|
-
<p>Rows
|
|
1016
|
+
<p>Rows</p>
|
|
784
1017
|
<zn-select name="rowPerPage"
|
|
785
1018
|
size="small"
|
|
786
1019
|
value="${this.itemsPerPage}"
|
|
@@ -794,76 +1027,67 @@ export default class ZnDataTable extends ZincElement {
|
|
|
794
1027
|
</div>`;
|
|
795
1028
|
}
|
|
796
1029
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const showLeftEllipsis = current > 4;
|
|
806
|
-
const showRightEllipsis = current < total - 3;
|
|
1030
|
+
// A window of PAGE_WINDOW pages that slides with the current page and stays inside the page
|
|
1031
|
+
// count; the first/last buttons cover jumping to either end
|
|
1032
|
+
private getPageRange(): number[] {
|
|
1033
|
+
const size = Math.min(PAGE_WINDOW, this.totalPages);
|
|
1034
|
+
const start = Math.min(
|
|
1035
|
+
Math.max(1, this.page - Math.floor(size / 2)),
|
|
1036
|
+
this.totalPages - size + 1
|
|
1037
|
+
);
|
|
807
1038
|
|
|
808
|
-
|
|
809
|
-
return [1, 2, 3, 4, 5, 'ellipsis', total];
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if (showLeftEllipsis && !showRightEllipsis) {
|
|
813
|
-
return [1, 'ellipsis', total - 4, total - 3, total - 2, total - 1, total];
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
return [1, 'ellipsis', current - 1, current, current + 1, 'ellipsis', total];
|
|
1039
|
+
return Array.from({length: size}, (_, i) => start + i);
|
|
817
1040
|
}
|
|
818
1041
|
|
|
819
1042
|
getPagination() {
|
|
820
1043
|
if (this.hidePagination || this.totalPages <= 1) return null;
|
|
821
1044
|
|
|
822
1045
|
return html`
|
|
823
|
-
<
|
|
1046
|
+
<zn-button-group>
|
|
824
1047
|
<zn-button @click="${this.page !== 1 ? this.goToFirstPage : undefined}"
|
|
825
1048
|
?disabled="${this.page === 1}"
|
|
826
1049
|
icon="chevrons-left@lu"
|
|
827
1050
|
icon-button="small"
|
|
828
|
-
|
|
1051
|
+
label="First page">
|
|
829
1052
|
</zn-button>
|
|
830
1053
|
<zn-button @click="${this.page !== 1 ? this.goToPreviousPage : undefined}"
|
|
831
1054
|
?disabled="${this.page === 1}"
|
|
832
1055
|
icon="chevron-left@lu"
|
|
833
1056
|
icon-button="small"
|
|
834
|
-
|
|
1057
|
+
label="Previous page">
|
|
835
1058
|
</zn-button>
|
|
836
|
-
${this.getPageRange().map((p) =>
|
|
837
|
-
?
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1059
|
+
${this.getPageRange().map((p: number) => html`
|
|
1060
|
+
<zn-button @click="${p !== this.page ? () => this.goToPage(p) : undefined}"
|
|
1061
|
+
class="${classMap({
|
|
1062
|
+
'table__footer__pagination-page': true,
|
|
1063
|
+
'table__footer__pagination-page--active': p === this.page,
|
|
1064
|
+
})}"
|
|
1065
|
+
icon-button="small"
|
|
1066
|
+
aria-current="${ifDefined(p === this.page ? 'page' : undefined)}">${p}
|
|
1067
|
+
</zn-button>`)}
|
|
844
1068
|
<zn-button @click="${this.page !== this.totalPages ? this.goToNextPage : undefined}"
|
|
845
1069
|
?disabled="${this.page === this.totalPages}"
|
|
846
1070
|
icon="chevron-right@lu"
|
|
847
1071
|
icon-button="small"
|
|
848
|
-
|
|
1072
|
+
label="Next page">
|
|
849
1073
|
</zn-button>
|
|
850
1074
|
<zn-button @click="${this.page !== this.totalPages ? this.goToLastPage : undefined}"
|
|
851
1075
|
?disabled="${this.page === this.totalPages}"
|
|
852
1076
|
icon="chevrons-right@lu"
|
|
853
1077
|
icon-button="small"
|
|
854
|
-
|
|
1078
|
+
label="Last page">
|
|
855
1079
|
</zn-button>
|
|
856
|
-
</
|
|
1080
|
+
</zn-button-group>`;
|
|
857
1081
|
}
|
|
858
1082
|
|
|
859
1083
|
getActions() {
|
|
860
1084
|
const actions = [];
|
|
861
1085
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|| this.hasSlotController.test(ActionSlots.
|
|
1086
|
+
// Select-all only earns its place alongside the bulk actions it feeds
|
|
1087
|
+
const hasBulkActions = this.hasSlotController.test(ActionSlots.delete.valueOf())
|
|
1088
|
+
|| this.hasSlotController.test(ActionSlots.modify.valueOf());
|
|
865
1089
|
|
|
866
|
-
if (!
|
|
1090
|
+
if (!hasBulkActions) {
|
|
867
1091
|
return [];
|
|
868
1092
|
}
|
|
869
1093
|
|
|
@@ -901,11 +1125,6 @@ export default class ZnDataTable extends ZincElement {
|
|
|
901
1125
|
}
|
|
902
1126
|
}
|
|
903
1127
|
|
|
904
|
-
if (this.hasSlotController.test(ActionSlots.create.valueOf())) {
|
|
905
|
-
actions.push(html`
|
|
906
|
-
<slot name="${ActionSlots.create.valueOf()}"></slot>`);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
1128
|
return actions;
|
|
910
1129
|
}
|
|
911
1130
|
|
|
@@ -1192,16 +1411,7 @@ export default class ZnDataTable extends ZincElement {
|
|
|
1192
1411
|
private renderCellHeader(header: HeaderConfig) {
|
|
1193
1412
|
const sortable = !Object.values(this.unsortableHeaders).includes(header.key) && !Object.values(this.hiddenHeaders).includes(header.key) && !this.unsortable && header.sortable !== false;
|
|
1194
1413
|
|
|
1195
|
-
|
|
1196
|
-
const lastVisibleHeaderKey = Object.values(this.headers)
|
|
1197
|
-
.filter((h: HeaderConfig) => {
|
|
1198
|
-
if (h.hideHeader || h.hideColumn) return false;
|
|
1199
|
-
|
|
1200
|
-
if (Object.values(this.hiddenColumns).includes(h.key)) return false;
|
|
1201
|
-
|
|
1202
|
-
return !h.secondary;
|
|
1203
|
-
})
|
|
1204
|
-
.slice(-1)[0]?.key;
|
|
1414
|
+
const lastVisibleHeaderKey = this.visibleHeaders().slice(-1)[0]?.key;
|
|
1205
1415
|
|
|
1206
1416
|
return html`
|
|
1207
1417
|
<th
|
|
@@ -1221,13 +1431,7 @@ export default class ZnDataTable extends ZincElement {
|
|
|
1221
1431
|
}
|
|
1222
1432
|
|
|
1223
1433
|
private renderCellBody(index: number, value: Cell, row: Row) {
|
|
1224
|
-
const filteredHeaders =
|
|
1225
|
-
if (header.hideHeader || header.hideColumn) return false;
|
|
1226
|
-
|
|
1227
|
-
if (Object.values(this.hiddenColumns).includes(header.key)) return false;
|
|
1228
|
-
|
|
1229
|
-
return !header.secondary;
|
|
1230
|
-
});
|
|
1434
|
+
const filteredHeaders = this.visibleHeaders();
|
|
1231
1435
|
const header: HeaderConfig | undefined = filteredHeaders[index];
|
|
1232
1436
|
const headerKey: string | undefined = header?.key;
|
|
1233
1437
|
|
|
@@ -1253,14 +1457,15 @@ export default class ZnDataTable extends ZincElement {
|
|
|
1253
1457
|
<td class="table__cell table__cell--expander"></td>`;
|
|
1254
1458
|
|
|
1255
1459
|
const expanded = this._expandedRows.has(row.id);
|
|
1256
|
-
const icon = expanded ? 'chevron_down@lu' : 'chevron_right';
|
|
1460
|
+
const icon = expanded ? 'chevron_down@lu' : 'chevron_right@lu';
|
|
1257
1461
|
|
|
1258
1462
|
return html`
|
|
1259
1463
|
<td class="table__cell table__cell--expander">
|
|
1260
1464
|
<zn-button
|
|
1261
|
-
|
|
1465
|
+
icon-button="small"
|
|
1466
|
+
plain
|
|
1262
1467
|
icon="${icon}"
|
|
1263
|
-
icon-size="
|
|
1468
|
+
icon-size="20"
|
|
1264
1469
|
aria-label="${expanded ? 'Collapse row' : 'Expand row'}"
|
|
1265
1470
|
@click="${(e: Event) => this.toggleRowExpansion(e, row)}">
|
|
1266
1471
|
</zn-button>
|
|
@@ -1404,11 +1609,8 @@ export default class ZnDataTable extends ZincElement {
|
|
|
1404
1609
|
|
|
1405
1610
|
private loadingTable() {
|
|
1406
1611
|
return html`
|
|
1407
|
-
<div class="
|
|
1408
|
-
<table class="
|
|
1409
|
-
'table': true,
|
|
1410
|
-
'table--standalone': this.standalone,
|
|
1411
|
-
})}">
|
|
1612
|
+
<div class="table__scroll">
|
|
1613
|
+
<table class="table">
|
|
1412
1614
|
<thead>
|
|
1413
1615
|
<tr>
|
|
1414
1616
|
<th colspan="30%"></th>
|
|
@@ -1493,7 +1695,7 @@ export default class ZnDataTable extends ZincElement {
|
|
|
1493
1695
|
plain
|
|
1494
1696
|
aria-label="Row actions">
|
|
1495
1697
|
</zn-button>
|
|
1496
|
-
<zn-menu>
|
|
1698
|
+
<zn-menu variant="shell">
|
|
1497
1699
|
${row.actions?.map((action: ActionConfig) => {
|
|
1498
1700
|
if (action.confirmContent) {
|
|
1499
1701
|
const triggerId = 'confirm-action-' + Math.random().toString(36).substring(2, 15);
|