@digital-realty/ix-grid 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/demo/columns.js +65 -0
  4. package/demo/contacts.js +292 -0
  5. package/demo/index.html +92 -0
  6. package/dist/IxGrid.d.ts +46 -0
  7. package/dist/IxGrid.js +227 -0
  8. package/dist/IxGrid.js.map +1 -0
  9. package/dist/components/IxGridColumnFilter.d.ts +20 -0
  10. package/dist/components/IxGridColumnFilter.js +133 -0
  11. package/dist/components/IxGridColumnFilter.js.map +1 -0
  12. package/dist/components/IxGridRowFilter.d.ts +37 -0
  13. package/dist/components/IxGridRowFilter.js +317 -0
  14. package/dist/components/IxGridRowFilter.js.map +1 -0
  15. package/dist/components/IxPagination.d.ts +13 -0
  16. package/dist/components/IxPagination.js +93 -0
  17. package/dist/components/IxPagination.js.map +1 -0
  18. package/dist/components/grid-column-filter-styles.d.ts +1 -0
  19. package/dist/components/grid-column-filter-styles.js +71 -0
  20. package/dist/components/grid-column-filter-styles.js.map +1 -0
  21. package/dist/components/grid-row-filter-styles.d.ts +1 -0
  22. package/dist/components/grid-row-filter-styles.js +414 -0
  23. package/dist/components/grid-row-filter-styles.js.map +1 -0
  24. package/dist/components/pagination-styles.d.ts +1 -0
  25. package/dist/components/pagination-styles.js +33 -0
  26. package/dist/components/pagination-styles.js.map +1 -0
  27. package/dist/grid-view-styles.d.ts +1 -0
  28. package/dist/grid-view-styles.js +265 -0
  29. package/dist/grid-view-styles.js.map +1 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +2 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/ix-grid-copy.d.ts +10 -0
  34. package/dist/ix-grid-copy.js +11 -0
  35. package/dist/ix-grid-copy.js.map +1 -0
  36. package/dist/ix-grid.d.ts +1 -0
  37. package/dist/ix-grid.js +3 -0
  38. package/dist/ix-grid.js.map +1 -0
  39. package/dist/test/ix-grid-column-filter.test.d.ts +1 -0
  40. package/dist/test/ix-grid-column-filter.test.js +36 -0
  41. package/dist/test/ix-grid-column-filter.test.js.map +1 -0
  42. package/dist/test/ix-grid-row-filter.test.d.ts +1 -0
  43. package/dist/test/ix-grid-row-filter.test.js +37 -0
  44. package/dist/test/ix-grid-row-filter.test.js.map +1 -0
  45. package/dist/test/ix-grid.test.d.ts +1 -0
  46. package/dist/test/ix-grid.test.js +43 -0
  47. package/dist/test/ix-grid.test.js.map +1 -0
  48. package/dist/test/ix-pagination.test.d.ts +1 -0
  49. package/dist/test/ix-pagination.test.js +28 -0
  50. package/dist/test/ix-pagination.test.js.map +1 -0
  51. package/package.json +95 -0
  52. package/src/IxGrid.ts +229 -0
  53. package/src/components/IxGridColumnFilter.ts +144 -0
  54. package/src/components/IxGridRowFilter.ts +352 -0
  55. package/src/components/IxPagination.ts +85 -0
  56. package/src/components/grid-column-filter-styles.ts +71 -0
  57. package/src/components/grid-row-filter-styles.ts +414 -0
  58. package/src/components/pagination-styles.ts +33 -0
  59. package/src/grid-view-styles.ts +265 -0
  60. package/src/index.ts +1 -0
  61. package/src/ix-grid-copy.ts +10 -0
  62. package/src/ix-grid.ts +3 -0
  63. package/src/test/ix-grid-column-filter.test.ts +47 -0
  64. package/src/test/ix-grid-row-filter.test.ts +48 -0
  65. package/src/test/ix-grid.test.ts +50 -0
  66. package/src/test/ix-pagination.test.ts +33 -0
  67. package/tsconfig.json +22 -0
  68. package/web-dev-server.config.mjs +27 -0
  69. package/web-test-runner.config.mjs +43 -0
@@ -0,0 +1,352 @@
1
+ import { LitElement, html, nothing } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { repeat } from 'lit/directives/repeat.js';
4
+ import '@digital-realty/ix-icon-button/ix-icon-button.js';
5
+ import '@digital-realty/ix-select/ix-select.js';
6
+ import { IxGridViewStyles } from '../grid-view-styles.js';
7
+ import { IxGridRowFilterStyles } from './grid-row-filter-styles.js';
8
+ import type { Column } from '../IxGrid.js';
9
+ import { copy } from '../ix-grid-copy.js';
10
+
11
+ export interface Filter {
12
+ columnField: string;
13
+ operatorValue: string;
14
+ value: string;
15
+ }
16
+
17
+ @customElement('ix-grid-row-filter')
18
+ export class IxGridRowFilter extends LitElement {
19
+ static readonly styles = [IxGridViewStyles, IxGridRowFilterStyles];
20
+
21
+ @property({ type: Array }) columns: Column[] = [];
22
+
23
+ @state() private isDropdownVisible: boolean = false;
24
+
25
+ @state() private filters: Filter[] = [];
26
+
27
+ @state() private filterableColumns: Column[] = [];
28
+
29
+ @state() private filterColumns: string[] = [];
30
+
31
+ @state() private activeFilters: Filter[] = [];
32
+
33
+ @state() private mapSelect: boolean = false;
34
+
35
+ updateActiveFilters() {
36
+ this.activeFilters = this.filters.filter(filter => filter.value.length > 0);
37
+ }
38
+
39
+ connectedCallback() {
40
+ super.connectedCallback();
41
+ document.addEventListener('click', this.closeOnOuterClick);
42
+ }
43
+
44
+ disconnectedCallback() {
45
+ super.disconnectedCallback();
46
+ document.removeEventListener('click', this.closeOnOuterClick);
47
+ }
48
+
49
+ firstUpdated() {
50
+ this.filterableColumns = this.columns.filter(column => column.filterable);
51
+ this.filterColumns = this.filterableColumns.map(column => column.name);
52
+ this.filters = this.parseFilterQueryString();
53
+ if (!this.filters.length) {
54
+ this.addFilter();
55
+ }
56
+ this.updateActiveFilters();
57
+ }
58
+
59
+ get filterNames() {
60
+ return this.filters.map(filter => filter.columnField);
61
+ }
62
+
63
+ get unselectedFilters() {
64
+ return this.filterColumns.filter(f => !this.filterNames.includes(f));
65
+ }
66
+
67
+ closeOnOuterClick = (e: Event) => {
68
+ if (!e.composedPath().includes(this)) {
69
+ this.isDropdownVisible = false;
70
+ }
71
+ };
72
+
73
+ parseFilterQueryString(): Filter[] {
74
+ const params = new URLSearchParams(window.location.search);
75
+ const filters: Filter[] = [];
76
+
77
+ for (const [key, value] of params) {
78
+ if (this.filterColumns.includes(key)) {
79
+ filters.push({
80
+ columnField: key,
81
+ operatorValue: 'contains',
82
+ value,
83
+ });
84
+ }
85
+ }
86
+
87
+ return filters;
88
+ }
89
+
90
+ dispatchUpdate() {
91
+ this.dispatchEvent(
92
+ new CustomEvent('rowFilter', {
93
+ detail: {
94
+ filters: this.filters.filter(filter => filter.value.length >= 3),
95
+ },
96
+ bubbles: true,
97
+ composed: true,
98
+ })
99
+ );
100
+ }
101
+
102
+ addFilter() {
103
+ const nextFilter =
104
+ this.filterColumns.find(filter => !this.filterNames.includes(filter)) ||
105
+ '';
106
+ this.filters = [
107
+ ...this.filters,
108
+ {
109
+ columnField: nextFilter,
110
+ operatorValue: 'contains',
111
+ value: '',
112
+ },
113
+ ];
114
+ this.updateActiveFilters();
115
+ }
116
+
117
+ clearFilters() {
118
+ this.filters = [];
119
+ this.addFilter();
120
+ this.dispatchUpdate();
121
+ }
122
+
123
+ removeFilter(index: number) {
124
+ this.filters = this.filters.filter((_, i) => i !== index);
125
+ this.dispatchUpdate();
126
+ if (this.filters.length === 0) {
127
+ this.isDropdownVisible = false;
128
+ this.addFilter();
129
+ }
130
+ }
131
+
132
+ private onfilterColumnChange(index: number, e: Event) {
133
+ const selectedValue = (e.target as HTMLSelectElement).value;
134
+ this.filters[index].columnField = selectedValue;
135
+
136
+ this.filters = [...this.filters];
137
+
138
+ if (this.filters[index].value.length >= 3) {
139
+ this.dispatchUpdate();
140
+ }
141
+ }
142
+
143
+ private onfilterValueChange(index: number, e: Event) {
144
+ const oldValueLength = this.filters[index].value.length;
145
+ const { value } = e.target as HTMLInputElement;
146
+ this.filters[index].value = value;
147
+
148
+ const newValueLength = this.filters[index].value.length;
149
+ if (
150
+ this.filters[index].columnField.length > 0 &&
151
+ (newValueLength >= 3 || newValueLength < oldValueLength)
152
+ ) {
153
+ this.dispatchUpdate();
154
+ }
155
+ this.updateActiveFilters();
156
+ }
157
+
158
+ renderToolTip() {
159
+ if (this.isDropdownVisible) {
160
+ return copy.hideFilters;
161
+ }
162
+ if (!this.activeFilters.length) {
163
+ return copy.showFilters;
164
+ }
165
+ return html` <p>${this.activeFilters.length} ${copy.activeFilter}</p>
166
+ <ul>
167
+ ${this.activeFilters.map(
168
+ filter => html`<li>
169
+ ${filter.columnField} ${filter.operatorValue} ${filter.value}
170
+ </li>`
171
+ )}
172
+ </ul>`;
173
+ }
174
+
175
+ renderFilterInput(value: Filter, index: number) {
176
+ const options = [value.columnField, ...this.unselectedFilters];
177
+ const filterOptions = this.filterableColumns.filter(column =>
178
+ options.includes(column.name)
179
+ );
180
+ return html`
181
+ <div class="filter-form">
182
+ <div class="filter-remove filter-form-column">
183
+ <div class="form-group">
184
+ <span
185
+ class="close material-icons"
186
+ @click=${() => this.removeFilter(index)}
187
+ @keyDown=${(e: KeyboardEvent) => {
188
+ if (e.key === ' ' || e.key === 'Enter') {
189
+ this.removeFilter(index);
190
+ }
191
+ }}
192
+ >close</span
193
+ >
194
+ </div>
195
+ </div>
196
+ ${this.mapSelect
197
+ ? html`<div
198
+ class="filter-form-column filter-form-column-border filterColumnField"
199
+ >
200
+ <div class="form-group">
201
+ <label
202
+ class="form-group-column-label"
203
+ title="select: ${value.columnField}, options: ${filterOptions.map(
204
+ column => `value=${column.name}, selected=${
205
+ column.name === value.columnField
206
+ }, ${column.header}
207
+ `
208
+ )}"
209
+ >Columns</label
210
+ >
211
+
212
+ <select
213
+ @change=${(e: Event) => this.onfilterColumnChange(index, e)}
214
+ .value=${value.columnField}
215
+ data-v=${value.columnField}
216
+ >
217
+ ${filterOptions.map(
218
+ column => html`
219
+ <option
220
+ value=${column.name}
221
+ ?selected=${column.name === value.columnField}
222
+ >
223
+ ${column.header}
224
+ </option>
225
+ `
226
+ )}
227
+ </select>
228
+ </div>
229
+ </div>`
230
+ : nothing}
231
+
232
+ <div
233
+ class="filter-form-column filter-form-column-border filterColumnField"
234
+ >
235
+ <div class="filter-form-group">
236
+ <label
237
+ class="form-group-column-label"
238
+ title="select: ${value.columnField}, options: ${filterOptions.map(
239
+ column => `value=${column.name}, selected=${
240
+ column.name === value.columnField
241
+ }, ${column.header}
242
+ `
243
+ )}"
244
+ ><span>Columns</span>
245
+ <select
246
+ @change=${(e: Event) => this.onfilterColumnChange(index, e)}
247
+ .value=${value.columnField}
248
+ data-v=${value.columnField}
249
+ >
250
+ ${repeat(
251
+ filterOptions,
252
+ (column: Column) => column.name,
253
+ (column: Column) => html`
254
+ <option
255
+ value=${column.name}
256
+ ?selected=${column.name === value.columnField}
257
+ >
258
+ ${column.header}
259
+ </option>
260
+ `
261
+ )}
262
+ </select>
263
+ </label>
264
+ </div>
265
+ </div>
266
+
267
+ <div
268
+ class="filter-form-column filter-form-column-border filterOperatorField"
269
+ >
270
+ <div class="filter-form-group">
271
+ <label class="form-group-operator-label"
272
+ ><span>Operator</span>
273
+ <select>
274
+ <option select="select">contains</option>
275
+ </select>
276
+ </label>
277
+ </div>
278
+ </div>
279
+ <div
280
+ class="filter-form-column filter-form-column-border filterValueField"
281
+ >
282
+ <div class="filter-form-group">
283
+ <label class="form-group-value-label"
284
+ ><span>Value</span>
285
+ <input
286
+ placeholder="Filter value"
287
+ @input=${(e: Event) => this.onfilterValueChange(index, e)}
288
+ .value=${value.value}
289
+ />
290
+ </label>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ `;
295
+ }
296
+
297
+ renderDropdown() {
298
+ const disableAddButton =
299
+ this.filters.length >= this.filterColumns.length ||
300
+ this.activeFilters.length < this.filters.length;
301
+ return html` <div class="filter-dropdown-content">
302
+ <div class="filter-body">
303
+ ${this.filters.map((filter, index) =>
304
+ this.renderFilterInput(filter, index)
305
+ )}
306
+ </div>
307
+ <div class="filter-footer">
308
+ <button
309
+ class="add-filter-button"
310
+ @click="${() => this.addFilter()}"
311
+ ?disabled=${disableAddButton}
312
+ >
313
+ <span class="add material-icons">add </span>
314
+ <span class="add">ADD FILTER</span>
315
+ </button>
316
+ <button
317
+ class="clear"
318
+ @click="${() => this.clearFilters()}"
319
+ ?disabled=${this.activeFilters.length === 0}
320
+ >
321
+ <span class="add">CLEAR ALL</span>
322
+ </button>
323
+ </div>
324
+ </div>`;
325
+ }
326
+
327
+ render() {
328
+ return html` <div class="grid-menu">
329
+ <div class="filter-container tooltip-container">
330
+ <button
331
+ class="filter_list filter-button"
332
+ @click=${() => {
333
+ this.isDropdownVisible = !this.isDropdownVisible;
334
+ }}
335
+ @keyDown=${() => {
336
+ this.isDropdownVisible = !this.isDropdownVisible;
337
+ }}
338
+ >
339
+ ${this.activeFilters.length > 0
340
+ ? html`<span class="filter-superscript"
341
+ >${this.activeFilters.length}</span
342
+ >`
343
+ : nothing}
344
+ <ix-icon-button icon="filter_list"></ix-icon-button>
345
+ <span class="filter">${copy.filters}</span>
346
+ </button>
347
+ <div class="tool-tip">${this.renderToolTip()}</div>
348
+ ${this.isDropdownVisible ? this.renderDropdown() : nothing}
349
+ </div>
350
+ </div>`;
351
+ }
352
+ }
@@ -0,0 +1,85 @@
1
+ import { LitElement, html } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import '@digital-realty/ix-icon-button/ix-icon-button.js';
4
+ import '@digital-realty/ix-select/ix-select.js';
5
+ import { PaginationStyles } from './pagination-styles.js';
6
+ import { copy } from '../ix-grid-copy.js';
7
+
8
+ @customElement('ix-pagination')
9
+ export class IxPagination extends LitElement {
10
+ static readonly styles = [PaginationStyles];
11
+
12
+ @property({ type: Number }) recordCount = 0;
13
+
14
+ @property({ type: Number }) page = 1;
15
+
16
+ @property({ type: Number }) pageSize = 10;
17
+
18
+ @property({ type: Array }) pageSizes: number[] = [5, 10, 25, 100];
19
+
20
+ private changePage(offset: number) {
21
+ this.page += offset;
22
+ this.updatePagination();
23
+ }
24
+
25
+ updatePagination(page = this.page, pageSize = this.pageSize) {
26
+ this.dispatchEvent(
27
+ new CustomEvent('updatePagination', {
28
+ detail: {
29
+ page,
30
+ pageSize,
31
+ },
32
+ bubbles: true,
33
+ composed: true,
34
+ })
35
+ );
36
+ }
37
+
38
+ render() {
39
+ const back = this.page > 1;
40
+ const next = this.recordCount > this.page * this.pageSize;
41
+ return html` <div class="pagination">
42
+ <p>${copy.rowsPerPage}:</p>
43
+ <ix-select
44
+ wide-menu
45
+ class="pagination__select-input"
46
+ @request-selection=${(e: Event) => {
47
+ const el = e.target as HTMLInputElement;
48
+ this.pageSize = Number(el.value);
49
+ this.updatePagination();
50
+ }}
51
+ selected-index=${this.pageSizes.indexOf(this.pageSize)}
52
+ filled
53
+ >
54
+ ${this.pageSizes.map(
55
+ option => html`
56
+ <ix-select-option class="select-option" value=${option}>
57
+ <div slot="headline">${option}</div>
58
+ </ix-select-option>
59
+ `
60
+ )}
61
+ </ix-select>
62
+ <p>
63
+ ${(this.page - 1) * this.pageSize + 1} -
64
+ ${this.page * this.pageSize > this.recordCount
65
+ ? html`${this.recordCount}`
66
+ : html`${this.page * this.pageSize}`}
67
+ of ${this.recordCount}
68
+ </p>
69
+ <div class="pagination-nav">
70
+ <ix-icon-button
71
+ ?disabled=${!back}
72
+ @click=${() => back && this.changePage(-1)}
73
+ icon="chevron_left"
74
+ >
75
+ </ix-icon-button>
76
+ <ix-icon-button
77
+ ?disabled=${!next}
78
+ @click=${() => next && this.changePage(+1)}
79
+ icon="chevron_right"
80
+ >
81
+ </ix-icon-button>
82
+ </div>
83
+ </div>`;
84
+ }
85
+ }
@@ -0,0 +1,71 @@
1
+ import { css } from 'lit';
2
+
3
+ export const IxGridColumnFilterStyles = css`
4
+ .dropdown-content {
5
+ position: absolute;
6
+ background-color: #f9f9f9;
7
+ min-width: 160px;
8
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
9
+ z-index: 1;
10
+ -webkit-box-align: center;
11
+ align-items: center;
12
+ cursor: pointer;
13
+ vertical-align: middle;
14
+ -webkit-tap-highlight-color: transparent;
15
+ padding: 10px;
16
+ }
17
+ .dropdown-content > div {
18
+ margin: 0px;
19
+ font-family: 'Open Sans', sans-serif;
20
+ font-style: normal;
21
+ font-weight: 400;
22
+ font-size: 14px;
23
+ line-height: 24px;
24
+ letter-spacing: 0.25px;
25
+ color: #092241;
26
+ display: block;
27
+ cursor: pointer;
28
+ }
29
+ .dropdown-content span:hover {
30
+ background-color: #f1f1f1;
31
+ }
32
+ .dropdown-content label {
33
+ display: flex;
34
+ align-items: center;
35
+ }
36
+ .active {
37
+ position: absolute;
38
+ right: 0;
39
+ top: 0;
40
+ height: 8px;
41
+ width: 8px;
42
+ background-color: #db0028;
43
+ border-radius: 50%;
44
+ }
45
+ ix-switch {
46
+ padding: 2px 4px;
47
+ --md-switch-track-height: 12px;
48
+ --md-switch-track-width: 35px;
49
+ --md-sys-color-primary: rgba(20, 86, 224, 0.68);
50
+ --md-sys-color-on-primary: #1456e0;
51
+ --md-sys-color-primary-container: #1456e0;
52
+ --md-switch-handle-height: 19px;
53
+ --md-switch-handle-width: 18px;
54
+ --md-sys-color-surface-container-highest: #9e9e9e;
55
+ --md-sys-color-outline: #9e9e9e;
56
+ --md-switch-pressed-handle-height: 19px;
57
+ --md-switch-pressed-handle-width: 18px;
58
+ --md-switch-handle-color: #fff;
59
+ --md-sys-color-on-surface-variant: white;
60
+ --selected-handle-height: 19px;
61
+ --selected-handle-width: 18px;
62
+ --_pressed-handle-height: 19px;
63
+ --_pressed-handle-width: 18px;
64
+ --md-switch-selected-handle-height: 19px;
65
+ --md-switch-selected-handle-width: 18px;
66
+ margin: 11px 8px 1px 0px;
67
+ }
68
+ .list {
69
+ position: relative;
70
+ }
71
+ `;