@synergy-design-system/mcp 2.14.0 → 2.15.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/CHANGELOG.md +23 -0
- package/metadata/checksum.txt +1 -1
- package/metadata/packages/assets/CHANGELOG.md +10 -0
- package/metadata/packages/components/components/syn-dialog/component.styles.ts +43 -35
- package/metadata/packages/components/components/syn-dialog/component.ts +30 -22
- package/metadata/packages/components/components/syn-header/component.ts +1 -1
- package/metadata/packages/components/components/syn-pagination/component.angular.ts +201 -0
- package/metadata/packages/components/components/syn-pagination/component.react.ts +56 -0
- package/metadata/packages/components/components/syn-pagination/component.styles.ts +128 -0
- package/metadata/packages/components/components/syn-pagination/component.ts +452 -0
- package/metadata/packages/components/components/syn-pagination/component.vue +144 -0
- package/metadata/packages/components/static/CHANGELOG.md +26 -56
- package/metadata/packages/tokens/CHANGELOG.md +12 -0
- package/metadata/packages/tokens/dark.css +1 -1
- package/metadata/packages/tokens/index.js +1 -1
- package/metadata/packages/tokens/light.css +1 -1
- package/metadata/packages/tokens/sick2018_dark.css +1 -1
- package/metadata/packages/tokens/sick2018_light.css +1 -1
- package/metadata/packages/tokens/sick2025_dark.css +1 -1
- package/metadata/packages/tokens/sick2025_light.css +1 -1
- package/metadata/static/components/syn-pagination/docs.md +102 -0
- package/package.json +5 -5
- package/metadata/packages/components/components/syn-dialog/component.custom.styles.ts +0 -60
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
|
|
3
|
+
export default css`
|
|
4
|
+
:host {
|
|
5
|
+
display: block;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.pagination {
|
|
9
|
+
--base-font: var(--syn-body-medium-regular);
|
|
10
|
+
--base-gap: var(--syn-spacing-small);
|
|
11
|
+
--navigation-gap: var(--syn-spacing-small);
|
|
12
|
+
--pagination-page-size-option-char-count: 2;
|
|
13
|
+
--pagination-total-pages-char-count: 3;
|
|
14
|
+
|
|
15
|
+
align-items: center;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-wrap: wrap;
|
|
18
|
+
font: var(--base-font);
|
|
19
|
+
gap: var(--base-gap) var(--syn-spacing-large);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Sizes */
|
|
23
|
+
:host([size="small"]) .pagination {
|
|
24
|
+
--base-gap: var(--syn-spacing-x-small);
|
|
25
|
+
--base-font: var(--syn-body-small-regular);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:host([size="large"]) .pagination {
|
|
29
|
+
--base-gap: var(--syn-spacing-medium);
|
|
30
|
+
--base-font: var(--syn-body-large-regular);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Divider */
|
|
34
|
+
syn-divider {
|
|
35
|
+
--divider-spacing: var(--syn-spacing-small);
|
|
36
|
+
--spacing: 0 auto var(--divider-spacing);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:host([size="small"]) syn-divider {
|
|
40
|
+
--divider-spacing: var(--syn-spacing-x-small);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:host([size="large"]) syn-divider {
|
|
44
|
+
--divider-spacing: var(--syn-spacing-medium);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Select */
|
|
48
|
+
.pagination__page-size-select-wrapper {
|
|
49
|
+
align-items: center;
|
|
50
|
+
display: flex;
|
|
51
|
+
gap: var(--base-gap);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.pagination__page-size-select::part(form-control) {
|
|
55
|
+
align-items: center;
|
|
56
|
+
display: flex;
|
|
57
|
+
gap: var(--base-gap);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.pagination__page-size-select::part(form-control-label) {
|
|
61
|
+
font: var(--base-font);
|
|
62
|
+
margin-bottom: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.pagination__page-size-select::part(display-input) {
|
|
66
|
+
width: calc((var(--pagination-page-size-option-char-count) * 1ch) + 1ch);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Navigation */
|
|
70
|
+
.pagination__navigation {
|
|
71
|
+
align-items: center;
|
|
72
|
+
display: flex;
|
|
73
|
+
flex: 1;
|
|
74
|
+
flex-wrap: nowrap;
|
|
75
|
+
gap: var(--navigation-gap);
|
|
76
|
+
justify-content: end;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.pagination__navigation > section {
|
|
80
|
+
align-items: center;
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-wrap: nowrap;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.pagination__page-input {
|
|
86
|
+
margin-inline-end: var(--navigation-gap);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.pagination__page-input::part(input) {
|
|
90
|
+
text-align: center;
|
|
91
|
+
width: calc((var(--pagination-total-pages-char-count) * 1ch) + 3ch);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Make sure to hide the label of the page input, but keep it accessible for screen readers.
|
|
96
|
+
* We can't use the label slot of syn-input for this, because it would mess with the layout.
|
|
97
|
+
*/
|
|
98
|
+
.pagination__page-input::part(form-control-label) {
|
|
99
|
+
border: 0;
|
|
100
|
+
/* stylelint-disable-next-line property-no-deprecated */
|
|
101
|
+
clip: rect(0, 0, 0, 0);
|
|
102
|
+
height: 1px;
|
|
103
|
+
margin: -1px;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
padding: 0;
|
|
106
|
+
position: absolute;
|
|
107
|
+
white-space: nowrap;
|
|
108
|
+
width: 1px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Compact Version */
|
|
112
|
+
:host([variant="compact"]) .pagination__navigation {
|
|
113
|
+
justify-content: center;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Adjustments for really small container widths */
|
|
117
|
+
@supports (container-type: inline-size) {
|
|
118
|
+
:host {
|
|
119
|
+
container-type: inline-size;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@container (max-width: 400px) {
|
|
123
|
+
.pagination__navigation {
|
|
124
|
+
justify-content: center;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CSSResultGroup,
|
|
3
|
+
type PropertyValues,
|
|
4
|
+
html,
|
|
5
|
+
nothing,
|
|
6
|
+
} from 'lit';
|
|
7
|
+
import { property } from 'lit/decorators.js';
|
|
8
|
+
import { createRef, ref } from 'lit/directives/ref.js';
|
|
9
|
+
import { LocalizeController } from '../../utilities/localize.js';
|
|
10
|
+
import SynergyElement from '../../internal/synergy-element.js';
|
|
11
|
+
import componentStyles from '../../styles/component.styles.js';
|
|
12
|
+
import styles from './pagination.styles.js';
|
|
13
|
+
import SynDivider from '../divider/divider.component.js';
|
|
14
|
+
import SynIconButton from '../icon-button/icon-button.component.js';
|
|
15
|
+
import SynInput from '../input/input.component.js';
|
|
16
|
+
import SynOption from '../option/option.component.js';
|
|
17
|
+
import SynSelect from '../select/select.component.js';
|
|
18
|
+
import {
|
|
19
|
+
calculatePageItemIndices,
|
|
20
|
+
calculateTotalPages,
|
|
21
|
+
clampPage,
|
|
22
|
+
getMaxOptionCharCount,
|
|
23
|
+
getPreviousOrDefault,
|
|
24
|
+
getTotalPages,
|
|
25
|
+
getTotalPagesCharCount,
|
|
26
|
+
isValidNonNegativeInteger,
|
|
27
|
+
isValidPositiveInteger,
|
|
28
|
+
sanitizePageSizeOptions,
|
|
29
|
+
} from './utility.js';
|
|
30
|
+
import type { SynInputEvent } from '../../events/events.js';
|
|
31
|
+
import { enableDefaultSettings } from '../../utilities/defaultSettings/decorator.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @summary <syn-pagination /> provides page navigation, direct page input, and page-size selection for large data sets.
|
|
35
|
+
*
|
|
36
|
+
* @documentation https://synergy-design-system.github.io/?path=/docs/components-syn-pagination--docs
|
|
37
|
+
* @status stable
|
|
38
|
+
* @since 3.12.0
|
|
39
|
+
*
|
|
40
|
+
* @event syn-pagination-page-changed - Emitted when the current page changes
|
|
41
|
+
* @event syn-pagination-page-size-changed - Emitted when the page size changes
|
|
42
|
+
*
|
|
43
|
+
* @csspart base - The component's base wrapper.
|
|
44
|
+
* @csspart divider - The divider element displayed at the top of the pagination component.
|
|
45
|
+
* @csspart page-size-select-wrapper - The wrapper element containing the page size select and page item summary.
|
|
46
|
+
* @csspart page-size-select - The page size select element.
|
|
47
|
+
* @csspart page-item-summary - The text element displaying the current page item range and total items.
|
|
48
|
+
* @csspart page-input-section - The section containing the page number input and total pages display.
|
|
49
|
+
* @csspart page-input - The page number input element.
|
|
50
|
+
* @csspart navigation - The pagination navigation element.
|
|
51
|
+
* @csspart navigation-action - The individual navigation action buttons (first, previous, next, last).
|
|
52
|
+
*
|
|
53
|
+
* @accessibility
|
|
54
|
+
* The entire component is wrapped in a semantic `<nav>` landmark with an `aria-label` for screen reader accessibility.
|
|
55
|
+
* Use the `aria-label` attribute to provide a unique, descriptive label when multiple pagination controls exist on the page.
|
|
56
|
+
* Example: `<syn-pagination aria-label="Search results pagination"></syn-pagination>`
|
|
57
|
+
*/
|
|
58
|
+
@enableDefaultSettings('SynPagination')
|
|
59
|
+
export default class SynPagination extends SynergyElement {
|
|
60
|
+
private static readonly DEFAULT_PAGE_SIZE = 25;
|
|
61
|
+
|
|
62
|
+
private static readonly DEFAULT_CURRENT_PAGE = 1;
|
|
63
|
+
|
|
64
|
+
private static readonly DEFAULT_TOTAL_ITEMS = 0;
|
|
65
|
+
|
|
66
|
+
private readonly baseRef = createRef<HTMLElement>();
|
|
67
|
+
|
|
68
|
+
private readonly localize = new LocalizeController(this);
|
|
69
|
+
|
|
70
|
+
static styles: CSSResultGroup = [
|
|
71
|
+
componentStyles,
|
|
72
|
+
styles,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
static dependencies = {
|
|
76
|
+
'syn-divider': SynDivider,
|
|
77
|
+
'syn-icon-button': SynIconButton,
|
|
78
|
+
'syn-input': SynInput,
|
|
79
|
+
'syn-option': SynOption,
|
|
80
|
+
'syn-select': SynSelect,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* When true, a divider is displayed at the top of the pagination component.
|
|
85
|
+
*/
|
|
86
|
+
@property({ type: Boolean }) divider = false;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* When true, the pagination controls are disabled and non-interactive.
|
|
90
|
+
*/
|
|
91
|
+
@property({ reflect: true, type: Boolean }) disabled = false;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The size of the pagination controls.
|
|
95
|
+
*/
|
|
96
|
+
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The current page number. The default value is 1.
|
|
100
|
+
* The component will emit a `syn-pagination-page-changed` event whenever the page changes, allowing you to respond to page changes in your application.
|
|
101
|
+
*/
|
|
102
|
+
@property({ attribute: 'current-page', reflect: true, type: Number }) currentPage = 1;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The number of items to display per page. The default value is 25.
|
|
106
|
+
* The component will emit a `syn-pagination-page-size-changed` event whenever the page size changes, allowing you to respond to page size changes in your application.
|
|
107
|
+
*/
|
|
108
|
+
@property({ attribute: 'page-size', reflect: true, type: Number }) pageSize = 25;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* An array of numbers representing the available options for the number of items to display per page. The default value is [10, 25, 50, 100].
|
|
112
|
+
* The component will use this array to populate the rows-per-page selector, allowing users to choose from the specified options.
|
|
113
|
+
*/
|
|
114
|
+
@property({
|
|
115
|
+
attribute: 'page-size-options',
|
|
116
|
+
converter: {
|
|
117
|
+
fromAttribute: (value: string) => value
|
|
118
|
+
.split(',')
|
|
119
|
+
.map(val => {
|
|
120
|
+
const num = parseInt(val.trim(), 10);
|
|
121
|
+
return Number.isSafeInteger(num) ? num : null;
|
|
122
|
+
})
|
|
123
|
+
.filter(Boolean),
|
|
124
|
+
},
|
|
125
|
+
type: Array,
|
|
126
|
+
}) pageSizeOptions: number[] = [10, 25, 50, 100];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Total amount of items. The component will use this value to calculate the total number of pages based on the selected rows per page.
|
|
130
|
+
*/
|
|
131
|
+
@property({ attribute: 'total-items', reflect: true, type: Number }) totalItems = 0;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The pagination variant to use. The "full" variant includes comprehensive controls for navigating between pages and adjusting the number of displayed rows,
|
|
135
|
+
* while the "compact" variant offers a streamlined interface with essential navigation controls.
|
|
136
|
+
* The default value is "full".
|
|
137
|
+
*/
|
|
138
|
+
@property({ attribute: 'variant', reflect: true }) variant: 'full' | 'compact' = 'full';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* An accessible label for the navigation landmark. Customize for multiple paginations on a page.
|
|
142
|
+
*/
|
|
143
|
+
@property({ attribute: 'aria-label' }) ariaLabel = 'Pagination';
|
|
144
|
+
|
|
145
|
+
private pageChangedViaUserInput(e: SynInputEvent) {
|
|
146
|
+
const newPage = (e.target as SynInput).valueAsNumber;
|
|
147
|
+
// Ignore invalid page inputs
|
|
148
|
+
if (!isValidPositiveInteger(newPage)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.updateCurrentPage(newPage);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private navigationClicked(event: MouseEvent, newPage: number) {
|
|
155
|
+
// Blur first so icon-button focus state settles before this render cycle disables it.
|
|
156
|
+
(event.currentTarget as SynIconButton | null)?.blur();
|
|
157
|
+
this.updateCurrentPage(newPage);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private sanitizeInvalidPropertyValues(changed: PropertyValues<this>) {
|
|
161
|
+
if (changed.has('pageSize') && !isValidPositiveInteger(this.pageSize)) {
|
|
162
|
+
this.pageSize = getPreviousOrDefault(
|
|
163
|
+
changed,
|
|
164
|
+
'pageSize',
|
|
165
|
+
SynPagination.DEFAULT_PAGE_SIZE,
|
|
166
|
+
isValidPositiveInteger,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (changed.has('currentPage') && !isValidPositiveInteger(this.currentPage)) {
|
|
171
|
+
this.currentPage = getPreviousOrDefault(
|
|
172
|
+
changed,
|
|
173
|
+
'currentPage',
|
|
174
|
+
SynPagination.DEFAULT_CURRENT_PAGE,
|
|
175
|
+
isValidPositiveInteger,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (changed.has('totalItems') && !isValidNonNegativeInteger(this.totalItems)) {
|
|
180
|
+
this.totalItems = getPreviousOrDefault(
|
|
181
|
+
changed,
|
|
182
|
+
'totalItems',
|
|
183
|
+
SynPagination.DEFAULT_TOTAL_ITEMS,
|
|
184
|
+
isValidNonNegativeInteger,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (changed.has('pageSizeOptions')) {
|
|
189
|
+
const sanitizedPageSizeOptions = sanitizePageSizeOptions(this.pageSizeOptions);
|
|
190
|
+
const hasChanged = this.pageSizeOptions.length !== sanitizedPageSizeOptions.length
|
|
191
|
+
|| this.pageSizeOptions.some((option, index) => option !== sanitizedPageSizeOptions[index]);
|
|
192
|
+
|
|
193
|
+
if (hasChanged) {
|
|
194
|
+
this.pageSizeOptions = sanitizedPageSizeOptions;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Updates the current page number and emits a "syn-pagination-page-changed" event with the new and previous page numbers.
|
|
201
|
+
* This method is called when the page number is changed via the page input or the navigation buttons.
|
|
202
|
+
*/
|
|
203
|
+
private updateCurrentPage(newPage: number) {
|
|
204
|
+
const totalPages = getTotalPages(this.pageSize, this.totalItems);
|
|
205
|
+
const safeNewPage = clampPage(newPage, totalPages);
|
|
206
|
+
const { currentPage } = this;
|
|
207
|
+
|
|
208
|
+
if (safeNewPage === currentPage) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.emit('syn-pagination-page-changed', {
|
|
213
|
+
detail: {
|
|
214
|
+
currentPage: safeNewPage,
|
|
215
|
+
previousPage: currentPage,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this.currentPage = safeNewPage;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Called when the page size is changed via the page size select.
|
|
224
|
+
* Emits a `syn-pagination-page-size-changed` event with the new and previous page sizes.
|
|
225
|
+
*/
|
|
226
|
+
private pageSizeChanged(e: SynInputEvent) {
|
|
227
|
+
const { currentPage, pageSize } = this;
|
|
228
|
+
const currentTarget = e.target as SynSelect;
|
|
229
|
+
const { value } = currentTarget;
|
|
230
|
+
const nextPageSize = parseInt(value as string, 10);
|
|
231
|
+
|
|
232
|
+
// Validate the new page size before applying it
|
|
233
|
+
if (!Number.isSafeInteger(nextPageSize) || nextPageSize <= 0) return;
|
|
234
|
+
|
|
235
|
+
// When we switch from a smaller value to a larger one,
|
|
236
|
+
// we also have to adjust the current page marker.
|
|
237
|
+
const oldStartIndex = (currentPage - 1) * pageSize + 1;
|
|
238
|
+
const nextTotalPages = getTotalPages(nextPageSize, this.totalItems);
|
|
239
|
+
|
|
240
|
+
const nextPage = clampPage(
|
|
241
|
+
Math.floor((oldStartIndex - 1) / nextPageSize) + 1,
|
|
242
|
+
nextTotalPages,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
this.pageSize = nextPageSize;
|
|
246
|
+
this.currentPage = nextPage;
|
|
247
|
+
|
|
248
|
+
this.emit('syn-pagination-page-size-changed', {
|
|
249
|
+
detail: {
|
|
250
|
+
currentPageSize: nextPageSize,
|
|
251
|
+
previousPageSize: pageSize,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (nextPage !== currentPage) {
|
|
256
|
+
this.emit('syn-pagination-page-changed', {
|
|
257
|
+
detail: {
|
|
258
|
+
currentPage: nextPage,
|
|
259
|
+
previousPage: currentPage,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
protected willUpdate(changed: PropertyValues<this>) {
|
|
266
|
+
super.willUpdate(changed);
|
|
267
|
+
|
|
268
|
+
this.sanitizeInvalidPropertyValues(changed);
|
|
269
|
+
|
|
270
|
+
const previousPageSizeOptions = changed.get('pageSizeOptions');
|
|
271
|
+
const hasPreviousPageSizeOptions = Array.isArray(previousPageSizeOptions);
|
|
272
|
+
|
|
273
|
+
if (hasPreviousPageSizeOptions && !this.pageSizeOptions.includes(this.pageSize)) {
|
|
274
|
+
const nextPageSize = this.pageSizeOptions[0];
|
|
275
|
+
const oldStartIndex = (this.currentPage - 1) * this.pageSize + 1;
|
|
276
|
+
const nextTotalPages = getTotalPages(nextPageSize, this.totalItems);
|
|
277
|
+
|
|
278
|
+
const nextPage = clampPage(
|
|
279
|
+
Math.floor((oldStartIndex - 1) / nextPageSize) + 1,
|
|
280
|
+
nextTotalPages,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
this.pageSize = nextPageSize;
|
|
284
|
+
this.currentPage = nextPage;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (changed.has('currentPage') || changed.has('pageSize') || changed.has('totalItems')) {
|
|
288
|
+
// If the current page exceeds the total number of pages based on the new page size or total items,
|
|
289
|
+
// we need to adjust it to ensure it remains within valid bounds.
|
|
290
|
+
const totalPages = getTotalPages(this.pageSize, this.totalItems);
|
|
291
|
+
const clamped = clampPage(this.currentPage, totalPages);
|
|
292
|
+
if (clamped !== this.currentPage) {
|
|
293
|
+
this.currentPage = clamped;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
protected updated(changed: PropertyValues<this>) {
|
|
299
|
+
super.updated(changed);
|
|
300
|
+
|
|
301
|
+
// When the size of available options or the page size changes,
|
|
302
|
+
// we have to adjust the width of our page size select/input controls.
|
|
303
|
+
if (changed.has('pageSizeOptions') || changed.has('pageSize') || changed.has('totalItems')) {
|
|
304
|
+
const maxOptionCharCount = getMaxOptionCharCount(this.pageSizeOptions);
|
|
305
|
+
const totalPagesCharCount = getTotalPagesCharCount(this.pageSize, this.totalItems);
|
|
306
|
+
const baseElement = this.baseRef.value;
|
|
307
|
+
|
|
308
|
+
if (baseElement) {
|
|
309
|
+
baseElement.style.setProperty('--pagination-page-size-option-char-count', String(maxOptionCharCount));
|
|
310
|
+
baseElement.style.setProperty('--pagination-total-pages-char-count', String(totalPagesCharCount));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
render() {
|
|
316
|
+
// List of total pages.
|
|
317
|
+
// Used to determine the disabled state of navigation buttons and to calculate the page item indices for display.
|
|
318
|
+
const totalPages = calculateTotalPages(this.totalItems, this.pageSize);
|
|
319
|
+
|
|
320
|
+
// Determine if we're in compact mode to conditionally apply styles and layout adjustments.
|
|
321
|
+
const isCompact = this.variant === 'compact';
|
|
322
|
+
|
|
323
|
+
// Condition to determine if the pagination controls should be disabled.
|
|
324
|
+
const isDisabled = this.disabled || totalPages === 0;
|
|
325
|
+
|
|
326
|
+
// Calculate the start and end item indices for the current page to display in the pagination summary (e.g., "11-20 of 100 items").
|
|
327
|
+
const pageItemIndices = calculatePageItemIndices(this.totalItems, this.pageSize, this.currentPage);
|
|
328
|
+
|
|
329
|
+
// If the current page size is not included in the available options,
|
|
330
|
+
// we select the first option as the default value to ensure the select input displays a valid option.
|
|
331
|
+
const selectedPageSizeOption = this.pageSizeOptions.includes(this.pageSize)
|
|
332
|
+
? this.pageSize
|
|
333
|
+
: this.pageSizeOptions[0];
|
|
334
|
+
|
|
335
|
+
const isFirstPage = this.currentPage === 1;
|
|
336
|
+
const isLastPage = this.currentPage === totalPages;
|
|
337
|
+
|
|
338
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
339
|
+
return html`
|
|
340
|
+
${this.divider ? html`<syn-divider part="divider"></syn-divider>` : nothing}
|
|
341
|
+
<nav
|
|
342
|
+
aria-label=${this.ariaLabel}
|
|
343
|
+
class="pagination"
|
|
344
|
+
part="base"
|
|
345
|
+
${ref(this.baseRef)}
|
|
346
|
+
>
|
|
347
|
+
${!isCompact ? html`
|
|
348
|
+
<div class="pagination__page-size-select-wrapper" part="page-size-select-wrapper">
|
|
349
|
+
<syn-select
|
|
350
|
+
class="pagination__page-size-select"
|
|
351
|
+
?disabled=${isDisabled}
|
|
352
|
+
label=${this.localize.term('paginationItemsPerPage')}
|
|
353
|
+
part="page-size-select"
|
|
354
|
+
value=${selectedPageSizeOption}
|
|
355
|
+
size=${this.size}
|
|
356
|
+
@syn-change=${this.pageSizeChanged}
|
|
357
|
+
>
|
|
358
|
+
${this.pageSizeOptions.map(option => html`
|
|
359
|
+
<syn-option value="${option}">
|
|
360
|
+
${option}
|
|
361
|
+
</syn-option>
|
|
362
|
+
`)}
|
|
363
|
+
</syn-select>
|
|
364
|
+
<!-- /.pagination__page-size-select -->
|
|
365
|
+
|
|
366
|
+
<span part="page-item-summary">
|
|
367
|
+
${this.localize.term(
|
|
368
|
+
'paginationItemSummary',
|
|
369
|
+
pageItemIndices.startIndex,
|
|
370
|
+
pageItemIndices.endIndex,
|
|
371
|
+
this.totalItems,
|
|
372
|
+
)}
|
|
373
|
+
</span>
|
|
374
|
+
<!-- /.pagination__page-item-summary -->
|
|
375
|
+
</div>
|
|
376
|
+
<!-- /.pagination__page-size-select-wrapper -->
|
|
377
|
+
`: nothing}
|
|
378
|
+
|
|
379
|
+
<div class="pagination__navigation" part="navigation">
|
|
380
|
+
<section>
|
|
381
|
+
<syn-icon-button
|
|
382
|
+
@click=${(e: MouseEvent) => this.navigationClicked(e, 1)}
|
|
383
|
+
color="primary"
|
|
384
|
+
?disabled=${isFirstPage || isDisabled}
|
|
385
|
+
label=${this.localize.term('paginationFirstPage')}
|
|
386
|
+
library="system"
|
|
387
|
+
name="first-page"
|
|
388
|
+
part="navigation-action"
|
|
389
|
+
size=${this.size}
|
|
390
|
+
></syn-icon-button>
|
|
391
|
+
|
|
392
|
+
<syn-icon-button
|
|
393
|
+
@click=${(e: MouseEvent) => this.navigationClicked(e, this.currentPage - 1)}
|
|
394
|
+
color="primary"
|
|
395
|
+
?disabled=${isFirstPage || isDisabled}
|
|
396
|
+
label=${this.localize.term('paginationPreviousPage')}
|
|
397
|
+
library="system"
|
|
398
|
+
name="previous-page"
|
|
399
|
+
part="navigation-action"
|
|
400
|
+
size=${this.size}
|
|
401
|
+
></syn-icon-button>
|
|
402
|
+
</section>
|
|
403
|
+
|
|
404
|
+
<section part="page-input-section">
|
|
405
|
+
<syn-input
|
|
406
|
+
class="pagination__page-input"
|
|
407
|
+
?disabled=${isDisabled}
|
|
408
|
+
label=${this.localize.term('paginationInputLabel')}
|
|
409
|
+
max=${totalPages}
|
|
410
|
+
min="1"
|
|
411
|
+
no-spin-buttons
|
|
412
|
+
numeric-strategy="modern"
|
|
413
|
+
part="page-input"
|
|
414
|
+
size=${this.size}
|
|
415
|
+
@syn-change=${this.pageChangedViaUserInput}
|
|
416
|
+
type="number"
|
|
417
|
+
value=${this.currentPage}
|
|
418
|
+
></syn-input>
|
|
419
|
+
|
|
420
|
+
<span>${this.localize.term('paginationOfTotalPages', totalPages)}</span>
|
|
421
|
+
</section>
|
|
422
|
+
|
|
423
|
+
<section>
|
|
424
|
+
<syn-icon-button
|
|
425
|
+
@click=${(e: MouseEvent) => this.navigationClicked(e, this.currentPage + 1)}
|
|
426
|
+
color="primary"
|
|
427
|
+
?disabled=${isLastPage || isDisabled}
|
|
428
|
+
label=${this.localize.term('paginationNextPage')}
|
|
429
|
+
library="system"
|
|
430
|
+
name="next-page"
|
|
431
|
+
part="navigation-action"
|
|
432
|
+
size=${this.size}
|
|
433
|
+
></syn-icon-button>
|
|
434
|
+
|
|
435
|
+
<syn-icon-button
|
|
436
|
+
@click=${(e: MouseEvent) => this.navigationClicked(e, totalPages)}
|
|
437
|
+
color="primary"
|
|
438
|
+
?disabled=${isLastPage || isDisabled}
|
|
439
|
+
label=${this.localize.term('paginationLastPage')}
|
|
440
|
+
library="system"
|
|
441
|
+
name="last-page"
|
|
442
|
+
part="navigation-action"
|
|
443
|
+
size=${this.size}
|
|
444
|
+
></syn-icon-button>
|
|
445
|
+
</section>
|
|
446
|
+
</div>
|
|
447
|
+
<!-- /.pagination__navigation -->
|
|
448
|
+
</nav>
|
|
449
|
+
`;
|
|
450
|
+
/* eslint-enable @typescript-eslint/unbound-method */
|
|
451
|
+
}
|
|
452
|
+
}
|