@neovici/cosmoz-omnitable 7.2.0 → 8.0.0-beta.10

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 (52) hide show
  1. package/cosmoz-omnitable-column-amount.js +89 -320
  2. package/cosmoz-omnitable-column-autocomplete.js +38 -47
  3. package/cosmoz-omnitable-column-boolean.js +107 -209
  4. package/cosmoz-omnitable-column-date.js +89 -102
  5. package/cosmoz-omnitable-column-datetime.js +86 -119
  6. package/cosmoz-omnitable-column-list-data.js +4 -1
  7. package/cosmoz-omnitable-column-list-horizontal.js +20 -38
  8. package/cosmoz-omnitable-column-list-mixin.js +135 -140
  9. package/cosmoz-omnitable-column-list.js +19 -28
  10. package/cosmoz-omnitable-column-mixin.js +70 -447
  11. package/cosmoz-omnitable-column-number.js +91 -183
  12. package/cosmoz-omnitable-column-time.js +77 -162
  13. package/cosmoz-omnitable-column.js +49 -93
  14. package/cosmoz-omnitable-group-row.js +1 -5
  15. package/cosmoz-omnitable-header-row.js +9 -6
  16. package/cosmoz-omnitable-item-expand.js +0 -3
  17. package/cosmoz-omnitable-item-row.js +5 -8
  18. package/cosmoz-omnitable-styles.js +4 -8
  19. package/cosmoz-omnitable.js +74 -771
  20. package/lib/cosmoz-omnitable-amount-range-input.js +295 -0
  21. package/{cosmoz-omnitable-column-date-mixin.js → lib/cosmoz-omnitable-date-input-mixin.js} +4 -26
  22. package/lib/cosmoz-omnitable-date-range-input.js +81 -0
  23. package/lib/cosmoz-omnitable-datetime-range-input.js +75 -0
  24. package/lib/cosmoz-omnitable-number-range-input.js +159 -0
  25. package/{cosmoz-omnitable-column-range-mixin.js → lib/cosmoz-omnitable-range-input-mixin.js} +45 -129
  26. package/lib/cosmoz-omnitable-settings.js +8 -5
  27. package/lib/cosmoz-omnitable-time-range-input.js +130 -0
  28. package/lib/generic-sorter.js +2 -2
  29. package/lib/invoke.js +1 -0
  30. package/lib/memoize.js +54 -0
  31. package/lib/normalize-settings.js +2 -5
  32. package/lib/polymer-haunted-render-mixin.js +19 -0
  33. package/lib/save-as-csv-action.js +32 -0
  34. package/lib/save-as-xlsx-action.js +25 -0
  35. package/lib/use-canvas-width.js +1 -1
  36. package/lib/use-dom-columns.js +143 -0
  37. package/lib/use-fast-layout.js +23 -18
  38. package/lib/use-hash-state.js +59 -0
  39. package/lib/use-layout.js +1 -1
  40. package/lib/use-omnitable.js +81 -22
  41. package/lib/use-processed-items.js +133 -0
  42. package/lib/use-resizable-columns.js +1 -2
  43. package/lib/use-sort-and-group-options.js +30 -0
  44. package/lib/utils-amount.js +147 -0
  45. package/lib/utils-data.js +41 -0
  46. package/lib/utils-date.js +204 -0
  47. package/lib/utils-datetime.js +71 -0
  48. package/lib/utils-number.js +112 -0
  49. package/lib/utils-time.js +115 -0
  50. package/package.json +1 -2
  51. package/lib/use-force-render.js +0 -8
  52. package/lib/use-render-on-column-updates.js +0 -18
@@ -3,7 +3,6 @@ import '@polymer/iron-icons/iron-icons';
3
3
  import '@polymer/iron-icon/iron-icon';
4
4
  import '@polymer/iron-label/iron-label';
5
5
  import '@polymer/paper-button/paper-button';
6
- import '@polymer/paper-checkbox/paper-checkbox';
7
6
  import '@polymer/paper-dropdown-menu/paper-dropdown-menu';
8
7
  import '@polymer/paper-icon-button/paper-icon-button';
9
8
  import '@polymer/paper-item/paper-item';
@@ -12,7 +11,6 @@ import '@polymer/paper-spinner/paper-spinner-lite';
12
11
 
13
12
  import '@neovici/cosmoz-grouped-list';
14
13
  import '@neovici/cosmoz-bottom-bar';
15
- import '@neovici/cosmoz-page-router/cosmoz-page-location';
16
14
 
17
15
  import './cosmoz-omnitable-column';
18
16
  import './cosmoz-omnitable-header-row';
@@ -22,24 +20,18 @@ import './cosmoz-omnitable-group-row';
22
20
  import './cosmoz-omnitable-columns';
23
21
  import styles from './cosmoz-omnitable-styles';
24
22
 
25
- import { NullXlsx } from '@neovici/nullxlsx';
26
23
 
27
- import { saveAs } from 'file-saver-es';
28
-
29
- import { timeOut } from '@polymer/polymer/lib/utils/async';
30
- import { Debouncer } from '@polymer/polymer/lib/utils/debounce';
31
24
  import { PolymerElement } from '@polymer/polymer/polymer-element';
32
25
  import { html } from '@polymer/polymer/lib/utils/html-tag';
33
- import { html as litHtml, render } from 'lit-html';
26
+ import { html as litHtml } from 'lit-html';
34
27
 
35
28
  import { translatable } from '@neovici/cosmoz-i18next';
36
29
  import { mixin, hauntedPolymer } from '@neovici/cosmoz-utils';
37
30
  import { isEmpty } from '@neovici/cosmoz-utils/lib/template.js';
38
31
  import { useOmnitable } from './lib/use-omnitable';
39
32
  import './lib/cosmoz-omnitable-settings';
40
- import { genericSorter } from './lib/generic-sorter';
41
-
42
- const PROPERTY_HASH_PARAMS = ['sortOn', 'groupOn', 'descending', 'groupOnDescending'];
33
+ import { saveAsCsvAction } from './lib/save-as-csv-action';
34
+ import { saveAsXlsxAction } from './lib/save-as-xlsx-action';
43
35
 
44
36
  /**
45
37
  * @polymer
@@ -57,16 +49,17 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
57
49
  ${ html([styles]) }
58
50
  <div id="layoutStyle"></div>
59
51
 
60
- <cosmoz-page-location id="location" route-hash="{{ _routeHash }}"></cosmoz-page-location>
61
-
62
52
  <div class="mainContainer">
63
53
  <div class="header" id="header">
64
54
  <input class="checkbox all" type="checkbox" checked="[[ _allSelected ]]" on-input="_onAllCheckboxChange" disabled$="[[ !_dataIsValid ]]" />
65
55
  <cosmoz-omnitable-header-row
56
+ data="[[ data ]]"
66
57
  columns="[[ normalizedColumns ]]"
58
+ filters="[[ filters ]]"
67
59
  group-on-column="[[ groupOnColumn ]]"
68
- content="[[ _renderSettings(normalizedSettings, collapsedColumns, settingsId, hasChangedSettings, hasHiddenFilter) ]]"
69
- >
60
+ content="[[ _renderSettings(normalizedSettings, collapsedColumns, settingsId, hasChangedSettings, hasHiddenFilter, filters) ]]"
61
+ set-filter-state="[[ setFilterState ]]"
62
+ ></cosmoz-omnitable-header-row>
70
63
  </div>
71
64
  <div class="tableContent" id="tableContent">
72
65
  <template is="dom-if" if="[[ !_dataIsValid ]]">
@@ -101,18 +94,23 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
101
94
  <cosmoz-grouped-list id="groupedList"
102
95
  data="{{ sortedFilteredGroupedItems }}"
103
96
  selected-items="{{ selectedItems }}"
104
- highlighted-items="{{ highlightedItems }}"
105
97
  display-empty-groups="[[ displayEmptyGroups ]]"
106
98
  compare-items-fn="[[ compareItemsFn ]]"
107
99
  >
108
100
  <template slot="templates" data-type="item">
109
101
  <div class="item-row-wrapper">
110
- <div selected$="[[ selected ]]" class="itemRow" highlighted$="[[ highlighted ]]">
102
+ <div selected$="[[ selected ]]" class="itemRow">
111
103
  <input class="checkbox" type="checkbox" checked="[[ selected ]]" on-input="_onCheckboxChange" disabled$="[[ !_dataIsValid ]]" />
112
104
  <cosmoz-omnitable-item-row columns="[[ normalizedColumns ]]"
113
- selected="[[ selected ]]" expanded="{{ expanded }}" item="[[ item ]]" group-on-column="[[ groupOnColumn ]]">
105
+ selected="[[ selected ]]" expanded="{{ expanded }}" item="[[ item ]]" group-on-column="[[ groupOnColumn ]]"
106
+ on-item-change="[[ onItemChange ]]">
114
107
  </cosmoz-omnitable-item-row>
115
- <paper-icon-button class="expand" hidden="[[ isEmpty(collapsedColumns.length) ]]" icon="[[ _getFoldIcon(expanded) ]]" on-tap="_toggleItem"></paper-icon-button>
108
+ <paper-icon-button
109
+ class="expand"
110
+ hidden="[[ isEmpty(collapsedColumns.length) ]]"
111
+ icon="[[ _getFoldIcon(expanded) ]]"
112
+ on-tap="_toggleItem"
113
+ ></paper-icon-button>
116
114
  </div>
117
115
  <cosmoz-omnitable-item-expand columns="[[ collapsedColumns ]]"
118
116
  item="[[item]]" selected="{{ selected }}" expanded$="{{ expanded }}" group-on-column="[[ groupOnColumn ]]"
@@ -139,18 +137,20 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
139
137
  <div class="footer-controls">
140
138
  <cosmoz-autocomplete
141
139
  label="[[ _('Group on', t) ]] [[ _computeSortDirection(groupOnDescending, t) ]]" placeholder="[[ _('No grouping', t) ]]"
142
- source="[[ _onCompleteValues(columns, 'groupOn', groupOnColumn) ]]" value="[[ groupOnColumn ]]" limit="1" text-property="title" always-float-label item-height="48" item-limit="8"
140
+ source="[[ _onCompleteValues(columns, 'groupOn', groupOnColumn) ]]" value="[[ groupOnColumn ]]" limit="1" text-property="title"
141
+ always-float-label item-height="48" item-limit="8"
143
142
  class="footer-control" on-change="[[ _onCompleteChange('groupOn') ]]" default-index="-1" show-single show-selection
144
143
  ></cosmoz-autocomplete>
145
144
  <cosmoz-autocomplete
146
145
  label="[[ _('Sort on', t) ]] [[ _computeSortDirection(descending, t) ]]" placeholder="[[ _('No sorting', t) ]]"
147
- source="[[ _onCompleteValues(columns, 'sortOn', sortOnColumn) ]]" value="[[ sortOnColumn ]]" limit="1" text-property="title" always-float-label item-height="48" item-limit="8"
146
+ source="[[ _onCompleteValues(columns, 'sortOn', sortOnColumn) ]]" value="[[ sortOnColumn ]]" limit="1" text-property="title"
147
+ always-float-label item-height="48" item-limit="8"
148
148
  class="footer-control" on-change="[[ _onCompleteChange('sortOn') ]]" default-index="-1" show-single show-selection
149
149
  ></cosmoz-autocomplete>
150
150
  </div>
151
151
  <div class="footer-tableStats">
152
- <span>[[ ngettext('{0} group', '{0} groups', _groupsCount, t) ]]</span>
153
- <span>[[ _renderRowStats(filteredItems.length, totalAvailable, t) ]]</span>
152
+ <span>[[ ngettext('{0} group', '{0} groups', groupsCount, t) ]]</span>
153
+ <span>[[ _renderRowStats(numProcessedItems, totalAvailable, t) ]]</span>
154
154
  </div>
155
155
  <cosmoz-bottom-bar id="bottomBar" class="footer-actionBar" match-parent
156
156
  on-action="_onAction" active$="[[ !isEmpty(selectedItems.length) ]]" computed-bar-height="{{ computedBarHeight }}">
@@ -177,323 +177,132 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
177
177
  </div>
178
178
 
179
179
  <div id="columns">
180
- <slot id="columnsSlot" on-slotchange="_debounceUpdateColumns"></slot>
180
+ <slot id="columnsSlot"></slot>
181
181
  </div>
182
182
  `;
183
183
  template.setAttribute('strip-whitespace', '');
184
184
  return template;
185
185
  }
186
186
 
187
- static get is() {
188
- return 'cosmoz-omnitable';
189
- }
190
-
191
187
  /* eslint-disable-next-line max-lines-per-function */
192
188
  static get properties() {
193
189
  return {
194
-
195
190
  /**
196
191
  * Filename when saving as CSV
197
192
  */
198
- csvFilename: {
199
- type: String,
200
- value: 'omnitable.csv'
201
- },
193
+ csvFilename: { type: String, value: 'omnitable.csv' },
202
194
 
203
195
  /**
204
196
  * Filename when saving as XLSX
205
197
  */
206
- xlsxFilename: {
207
- type: String,
208
- value: 'omnitable.xlsx'
209
- },
198
+ xlsxFilename: { type: String, value: 'omnitable.xlsx' },
210
199
 
211
200
  /**
212
201
  * Sheet name when saving as XLSX
213
202
  */
214
- xlsxSheetname: {
215
- type: String,
216
- value: 'Omnitable'
217
- },
203
+ xlsxSheetname: { type: String, value: 'Omnitable' },
218
204
 
219
205
  /**
220
206
  * Array used to list items.
221
207
  */
222
- data: {
223
- type: Array
224
- },
208
+ data: { type: Array },
225
209
 
226
210
  /**
227
211
  * This function is used to determine which items are kept selected across data updates
212
+ * TODO: probably broken
228
213
  */
229
214
  compareItemsFn: Function,
230
215
 
231
216
  /**
232
217
  * True if data is a valid and not empty array.
233
218
  */
234
- _dataIsValid: {
235
- type: Boolean,
236
- value: false,
237
- computed: '_computeDataValidity(data.*)'
238
- },
219
+ _dataIsValid: { type: Boolean, value: false, computed: '_computeDataValidity(data.*)' },
239
220
 
240
221
  /**
241
222
  * If set to true, then group a row will be displayed for groups that contain no items.
242
223
  */
243
- displayEmptyGroups: {
244
- type: Boolean,
245
- value: false
246
- },
224
+ displayEmptyGroups: { type: Boolean, value: false },
247
225
 
248
226
  /**
249
227
  * Specific columns to enable
250
228
  */
251
- enabledColumns: {
252
- type: Array,
253
- observer: '_debounceUpdateColumns'
254
- },
229
+ enabledColumns: { type: Array },
255
230
 
256
231
  /**
257
232
  * Whether bottom-bar has actions.
258
233
  */
259
- hasActions: {
260
- type: Boolean,
261
- value: false
262
- },
234
+ hasActions: { type: Boolean, value: false },
263
235
 
264
236
  /**
265
237
  * Shows a loading overlay to indicate data will be updated
266
238
  */
267
- loading: {
268
- type: Boolean,
269
- value: false
270
- },
239
+ loading: { type: Boolean, value: false },
271
240
 
272
241
  /**
273
242
  * List of selected rows/items in `data`.
274
243
  */
275
- selectedItems: {
276
- type: Array,
277
- notify: true
278
- },
279
-
280
- highlightedItems: {
281
- type: Array,
282
- notify: true
283
- },
284
-
285
- descending: {
286
- type: Boolean,
287
- value: false,
288
- notify: true
289
- },
290
-
291
- sortOn: {
292
- type: String,
293
- value: '',
294
- notify: true
295
- },
296
-
297
- sortOnColumn: {
298
- type: Object,
299
- computed: '_getColumn(sortOn, "name", columns)'
300
- },
301
-
302
- groupOnDescending: {
303
- type: Boolean,
304
- value: false,
305
- observer: '_debounceProcessItems'
306
- },
307
- /**
308
- * The column name to group on.
309
- */
310
- groupOn: {
311
- type: String,
312
- notify: true,
313
- value: ''
314
- },
244
+ selectedItems: { type: Array, notify: true },
245
+ descending: { type: Boolean, value: false, notify: true },
246
+ sortOn: { type: String, value: '', notify: true },
247
+ groupOnDescending: { type: Boolean, value: false },
315
248
 
316
249
  /**
317
- * The column that matches the current `groupOn` value.
318
- */
319
- groupOnColumn: {
320
- type: Object,
321
- notify: true,
322
- observer: '_groupOnColumnChanged',
323
- computed: '_getColumn(groupOn, "name", columns)'
324
- },
325
-
326
- /**
327
- * Items matching current set filter(s)
328
- */
329
- filteredItems: {
330
- type: Array,
331
- value: () => []
332
- },
333
-
334
- /**
335
- * Grouped items structure after filtering.
250
+ * The column name to group on.
336
251
  */
337
- filteredGroupedItems: {
338
- type: Array
339
- },
252
+ groupOn: { type: String, notify: true, value: '' },
340
253
 
341
254
  /**
342
255
  * Sorted items structure after filtering and grouping.
343
256
  */
344
- sortedFilteredGroupedItems: {
345
- type: Array,
346
- notify: true
347
- },
348
-
349
- _canvasWidth: {
350
- type: Number,
351
- value: 0,
352
- notify: true
353
- },
354
-
355
- /**
356
- * Keep track of width-changes to identify if we go bigger or smaller
357
- */
358
- _previousWidth: {
359
- type: Number,
360
- value: 0
361
- },
362
-
363
- _groupsCount: {
364
- type: Number,
365
- value: 0
366
- },
367
-
368
- visible: {
369
- type: Boolean,
370
- notify: true,
371
- readOnly: true,
372
- value: false,
373
- observer: 'visibleChanged'
374
- },
257
+ sortedFilteredGroupedItems: { type: Array, notify: true },
375
258
 
376
259
  /**
377
260
  * List of columns definition for this table.
378
261
  */
379
- columns: {
380
- type: Array,
381
- notify: true,
382
- value: () => []
383
- },
384
-
385
- settings: {
386
- type: Object,
387
- notify: true
388
- },
389
-
390
- _filterIsTooStrict: {
391
- type: Boolean,
392
- computed: '_computeFilterIsTooStrict(_dataIsValid, sortedFilteredGroupedItems.length)'
393
- },
394
-
395
- hashParam: {
396
- type: String
397
- },
398
-
399
- _routeHash: {
400
- type: Object
401
-
402
- },
403
- _routeHashKeyRule: {
404
- type: RegExp,
405
- computed: '_computeRouteHashKeyRule(hashParam)'
406
- },
262
+ columns: { type: Array, notify: true, value: () => []},
263
+ settings: { type: Object, notify: true },
264
+ _filterIsTooStrict: { type: Boolean, computed: '_computeFilterIsTooStrict(_dataIsValid, sortedFilteredGroupedItems.length)' },
265
+ hashParam: { type: String },
407
266
 
408
267
  /**
409
268
  * True when all items are selected.
410
269
  */
411
- _allSelected: {
412
- type: Boolean
413
- },
414
- computedBarHeight: {
415
- type: Number
416
- },
417
- settingsId: {
418
- type: String,
419
- value: undefined
420
- }
270
+ _allSelected: { type: Boolean },
271
+ computedBarHeight: { type: Number },
272
+ settingsId: { type: String, value: undefined }
421
273
  };
422
274
  }
423
275
 
424
276
  static get observers() {
425
277
  return [
426
- '_dataChanged(data.splices)',
427
- '_debounceProcessItems(sortOn, descending)',
428
- '_selectedItemsChanged(selectedItems.*)',
429
- 'renderFastLayoutCss(layoutCss, $.layoutStyle)'
278
+ '_selectedItemsChanged(selectedItems.*)'
430
279
  ];
431
280
  }
432
281
 
433
282
  constructor() {
434
283
  super();
435
284
 
436
- this.debouncers = {};
437
- this._updateColumns = this._updateColumns.bind(this);
438
- this._processItems = this._processItems.bind(this);
439
- this._groupItems = this._groupItems.bind(this);
440
- this._sortFilteredGroupedItems = this._sortFilteredGroupedItems.bind(this);
441
285
  this._onKey = this._onKey.bind(this);
442
- this._resizeObserver = new ResizeObserver(this._onResize.bind(this));
443
286
  }
444
287
 
445
288
  connectedCallback() {
446
289
  super.connectedCallback();
447
290
 
448
291
  this.$.groupedList.scrollTarget = this.$.scroller;
449
- this.addEventListener('cosmoz-column-hidden-changed', this._debounceUpdateColumns);
450
- this.addEventListener('cosmoz-column-disabled-changed', this._debounceUpdateColumns);
451
292
 
452
293
  this.addEventListener('update-item-size', this._onUpdateItemSize);
453
- this.addEventListener('cosmoz-column-title-changed', this._onColumnTitleChanged);
454
- this.addEventListener('cosmoz-column-filter-changed', this._filterChanged);
455
- this.addEventListener('cosmoz-column-editable-changed', this._onColumnEditableChanged);
456
- this.addEventListener('cosmoz-column-values-update', this._onColumnValuesUpdate);
457
294
  window.addEventListener('keydown', this._onKey);
458
295
  window.addEventListener('keyup', this._onKey);
459
- this._resizeObserver.observe(this);
460
- this._updateParamsFromHash();
461
296
  }
462
297
 
463
298
  disconnectedCallback() {
464
299
  super.disconnectedCallback();
465
300
 
466
- this.removeEventListener('cosmoz-column-hidden-changed', this._debounceUpdateColumns);
467
- this.removeEventListener('cosmoz-column-disabled-changed', this._debounceUpdateColumns);
468
- // Just in case we get detached before a planned debouncer has not run yet.
469
- this._cancelDebouncers();
470
-
471
301
  this.removeEventListener('update-item-size', this._onUpdateItemSize);
472
- this.removeEventListener('cosmoz-column-title-changed', this._onColumnTitleChanged);
473
- this.removeEventListener('cosmoz-column-filter-changed', this._filterChanged);
474
- this.removeEventListener('cosmoz-column-editable-changed', this._onColumnEditableChanged);
475
- this.removeEventListener('cosmoz-column-values-update', this._onColumnValuesUpdate);
476
- this._resizeObserver.unobserve(this);
477
302
  window.removeEventListener('keydown', this._onKey);
478
303
  window.removeEventListener('keyup', this._onKey);
479
304
  }
480
305
 
481
- flush() {
482
- // NOTE: in some instances flushing a debouncer causes another debouncer
483
- // to be set, so we must test each debouncer independently and in this order
484
- if (this.debouncers._updateColumnsDebouncer) {
485
- this.debouncers._updateColumnsDebouncer.flush();
486
- }
487
-
488
- if (this.debouncers._processItemsDebouncer) {
489
- this.debouncers._processItemsDebouncer.flush();
490
- }
491
- }
492
-
493
- _cancelDebouncers() {
494
- Object.values(this.debouncers).forEach(d => d.cancel());
495
- }
496
-
497
306
  /** ELEMENT BEHAVIOR */
498
307
 
499
308
  _computeDataValidity({ base: data } = {}) {
@@ -509,12 +318,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
509
318
  return `(${ direction })`;
510
319
  }
511
320
 
512
- visibleChanged(turnedVisible) {
513
- if (turnedVisible) {
514
- this._debounceUpdateColumns();
515
- }
516
- }
517
-
518
321
  _onUpdateItemSize(event) {
519
322
  const { detail } = event;
520
323
  if (detail && detail.item) {
@@ -523,41 +326,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
523
326
  event.stopPropagation();
524
327
  }
525
328
 
526
- _onColumnTitleChanged(event) {
527
- event.stopPropagation();
528
-
529
- if (!Array.isArray(this.columns)) {
530
- return;
531
- }
532
-
533
- const column = event.target,
534
- columnIndex = this.columns.indexOf(column);
535
-
536
- // re-notify column change to make dom-repeat re-render menu item title
537
- this.notifyPath(['columns', columnIndex, 'title']);
538
-
539
- if (column === this.groupOnColumn) {
540
- this.notifyPath(['groupOnColumn', 'title']);
541
- }
542
- }
543
-
544
- _onColumnEditableChanged(event) {
545
- event.stopPropagation();
546
- const { detail: { column }} = event,
547
- { columns } = this;
548
-
549
- if (!Array.isArray(columns) || columns.length === 0) {
550
- return;
551
- }
552
-
553
- const index = columns.indexOf(column);
554
- if (index < 0) {
555
- return;
556
- }
557
-
558
- this.columns = [...this.columns];
559
- }
560
-
561
329
  _onKey(e) {
562
330
  this._shiftKey = e.shiftKey;
563
331
  this._ctrlKey = e.ctrlKey;
@@ -579,387 +347,20 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
579
347
  event.stopPropagation();
580
348
  }
581
349
 
582
- _itemRowTapped(event) {
583
- const item = event.model.item;
584
- this.highlight(item, this.isItemHighlighted(item));
585
- }
586
-
587
- _onResize([entry]) {
588
- const hidden = entry.borderBoxSize?.[0]?.blockSize === 0 || entry.contentRect?.height === 0;
589
- this._setVisible(!hidden);
590
- if (hidden) {
591
- return;
592
- }
593
- requestAnimationFrame(() => requestAnimationFrame(() => this.$.groupedList.$.list._render()));
594
- }
595
-
596
- _dataChanged() {
597
- if (!Array.isArray(this.columns)) {
598
- return;
599
- }
600
- this._setColumnValues();
601
- this._debounceProcessItems();
602
- }
603
-
604
- _debounceUpdateColumns() {
605
- this._debounce('_updateColumnsDebouncer', this._updateColumns, timeOut.after(10));
606
- }
607
-
608
- /* eslint-disable-next-line max-lines-per-function, max-statements */
609
- _updateColumns() {
610
- if (!this.isConnected) {
611
- return;
612
- }
613
-
614
- this._setVisible(this.offsetParent != null);
615
-
616
- if (!this.visible) {
617
- return;
618
- }
619
-
620
- // NOTE: it's important to get all children, including those projected in slots
621
- let columns = this.$.columnsSlot.assignedElements({ flatten: true }).filter(child => child.isOmnitableColumn && !child.hidden),
622
- valuePathNames;
623
-
624
- const columnNames = columns.map(c => c.name);
625
-
626
- if (Array.isArray(this.enabledColumns)) {
627
- columns = columns.filter(column =>
628
- this.enabledColumns.indexOf(column.name) !== -1
629
- );
630
- } else {
631
- columns = columns.filter(column => !column.disabled);
632
- }
633
-
634
- const columnsChanged = !Array.isArray(this.columns) ||
635
- this.columns.length !== columns.length ||
636
- this.columns.some(col => columns.indexOf(col) === -1);
637
-
638
- if (!columns || columns.length === 0 || !columnsChanged) {
639
- return;
640
- }
641
-
642
- this._verifyColumnSetup(columns, columnNames);
643
-
644
- columns.forEach(column => {
645
- if (!column.name) {
646
- // No name set; Try to set name attribute via valuePath
647
- if (!valuePathNames) {
648
- valuePathNames = columns.map(c => c.valuePath);
649
- }
650
- const hasUniqueValuePath = valuePathNames.indexOf(column.valuePath) === valuePathNames.lastIndexOf(column.valuePath);
651
- if (hasUniqueValuePath && columnNames.indexOf(column.valuePath) === -1) {
652
- column.name = column.valuePath;
653
- }
654
- }
655
- });
656
-
657
- if (!Array.isArray(this.columns) || this.columns.length === 0) {
658
- this._setColumnValues(columns);
659
- }
660
-
661
- this.columns = columns;
662
- this._updateParamsFromHash();
663
-
664
- if (Array.isArray(this.data)) {
665
- this._debounceProcessItems();
666
- }
667
- }
668
-
669
- /**
670
- * Checks if the column setup is valid and logs errors.
671
- * As a separate functions to make testing easier.
672
- * @param {any} columns The columns.
673
- * @param {any} columnNames The column names.
674
- * @returns {Boolean} True if setup is valid.
675
- */
676
- _verifyColumnSetup(columns, columnNames = columns.map(c => c.name)) {
677
- // Check if column names are set and unique
678
- const columnsMissingNameAttribute = columns
679
- .filter(column => {
680
- const name = column.name;
681
- if (!name) {
682
- // eslint-disable-next-line no-console
683
- console.error('The name attribute needs to be set on all columns! Missing on column', column.title, column);
684
- return false;
685
- }
686
- return columnNames.indexOf(name) !== columnNames.lastIndexOf(name);
687
- });
688
-
689
- columnsMissingNameAttribute.forEach(column => {
690
- // eslint-disable-next-line no-console
691
- console.error('The name attribute needs to be unique among all columns! Not unique on column', column.title, column);
692
- });
693
-
694
- return columnsMissingNameAttribute.length === 0;
695
- }
696
-
697
- _onColumnValuesUpdate({ detail }) {
698
- if (detail == null || detail.column == null) {
699
- return;
700
- }
701
- this._setColumnValues([detail.column]);
702
- }
703
- // TODO: provides a mean to avoid setting the values for a column
704
- // TODO: should process (distinct, sort, min, max) the values at the column level depending on the column type
705
- _setColumnValues(columns = this.columns) {
706
- if (!Array.isArray(this.data) || this.data.length < 1 || !Array.isArray(columns) || columns.length < 1) {
707
- return;
708
- }
709
- columns.forEach(column => {
710
- if (!column.bindValues || column.externalValues) {
711
- return;
712
- }
713
-
714
- if (!column.valuePath) {
715
- // eslint-disable-next-line no-console
716
- console.error('value path is not defined for column', column, 'with bindValues');
717
- return;
718
- }
719
-
720
- column.set('values', this.data
721
- .map(item => this.get(column.valuePath, item))
722
- .filter((value, index, self) =>
723
- value != null && self.indexOf(value) === index
724
- )
725
- );
726
- });
727
- }
728
- /*
729
- * Returns a column based on an attribute.
730
- * @param {String} attributeValue The value of the column attribute.
731
- * @param {String} attribute The attribute name of the column.
732
- * @returns {Object} The found column.
733
- */
734
- _getColumn(attributeValue, attribute = 'name', columns) {
735
- if (!attributeValue || !columns) {
736
- return;
737
- }
738
- return columns.find(column => column[attribute] === attributeValue);
739
- }
740
-
741
- _filterChanged({ detail }) {
742
- if (!Array.isArray(this.columns) || this.columns.length < 1 || this.columns.indexOf(detail.column) < 0) {
743
- return;
744
- }
745
- this._debounceProcessItems();
746
- this._filterForRouteChanged(detail.column);
747
- }
748
-
749
- _groupOnColumnChanged() {
750
- this._debounceProcessItems();
751
- }
752
-
753
- _debounceProcessItems() {
754
- this._debounce('_processItemsDebouncer', this._processItems);
755
- }
756
-
757
- _processItems() {
758
- this._filterItems();
759
- this._groupItems();
760
- this._sortFilteredGroupedItems();
761
- }
762
-
763
- _filterItems() {
764
- if (Array.isArray(this.data) && this.data.length > 0 && Array.isArray(this.columns)) {
765
- // Call filtering code only on columns that has a filter
766
- const filterFunctions = this.columns
767
- .map(col => col.getFilterFn())
768
- .filter(fn => fn !== undefined);
769
-
770
- if (filterFunctions.length) {
771
- this.filteredItems = this.data.filter(item =>
772
- filterFunctions.every(filterFn => filterFn(item))
773
- );
774
- } else {
775
- this.filteredItems = this.data.slice();
776
- }
777
- } else {
778
- this.filteredItems = [];
779
- this.filteredGroupedItems = [];
780
- this.sortedFilteredGroupedItems = [];
781
- this._groupsCount = 0;
782
- }
783
- }
784
-
785
- /* eslint-disable-next-line max-statements */
786
- _groupItems() {
787
- // do not attempt to group items if no columns are defined
788
- if (!Array.isArray(this.columns) || this.columns.length === 0) {
789
- return;
790
- }
791
-
792
- this._updateRouteParam('groupOn');
793
-
794
- if (!Array.isArray(this.filteredItems) || this.filteredItems.length === 0) {
795
- this.filteredGroupedItems = [];
796
- this.sortedFilteredGroupedItems = [];
797
- this._groupsCount = 0;
798
- return;
799
- }
800
-
801
- const groupOnColumn = this.groupOnColumn;
802
-
803
- if (!groupOnColumn || !groupOnColumn.groupOn) {
804
- this.filteredGroupedItems = this.filteredItems;
805
- this._groupsCount = 0;
806
- return;
807
- }
808
-
809
- const groups = this.filteredItems.reduce((array, item) => {
810
- const gval = groupOnColumn.getComparableValue(item, groupOnColumn.groupOn);
811
-
812
- if (gval === undefined) {
813
- return array;
814
- }
815
-
816
- let group = array.find(g => g.id === gval);
817
- if (!group) {
818
- group = {
819
- id: gval,
820
- name: gval,
821
- items: []
822
- };
823
- array.push(group);
824
- }
825
- group.items.push(item);
826
- return array;
827
- }, []);
828
-
829
- groups.sort((a, b) => {
830
- const v1 = groupOnColumn.getComparableValue(a.items[0], groupOnColumn.groupOn),
831
- v2 = groupOnColumn.getComparableValue(b.items[0], groupOnColumn.groupOn);
832
-
833
- return genericSorter(v1, v2);
834
- });
835
-
836
- if (this.groupOnDescending) {
837
- groups.reverse();
838
- }
839
-
840
- this._groupsCount = groups.length;
841
- this.filteredGroupedItems = groups;
842
- }
843
-
844
- /**
845
- * compareFunction for sort(), can be overridden
846
- * @see Array.prototype.sort()
847
- * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort}
848
- * @param {*} a First compare value
849
- * @param {*} b Second compare value
850
- * @returns {number} -1 if a has lower index, 0 if a and b index are same, 1 if b is lower
851
- */
852
- sorter(a, b) {
853
- const v1 = this.sortOnColumn.getComparableValue(a, this.sortOnColumn.sortOn),
854
- v2 = this.sortOnColumn.getComparableValue(b, this.sortOnColumn.sortOn);
855
-
856
- return genericSorter(v1, v2);
857
- }
858
-
859
- /* eslint-disable-next-line max-statements */
860
- _sortFilteredGroupedItems() {
861
- if (!this.filteredGroupedItems) {
862
- return;
863
- }
864
-
865
- this._updateRouteParam('sortOn');
866
- this._updateRouteParam('descending');
867
- this._updateRouteParam('groupOnDescending');
868
-
869
- if (!this.sortOn || !this.sortOnColumn) {
870
- this.sortedFilteredGroupedItems = this.filteredGroupedItems;
871
- return;
872
- }
873
-
874
- const sorter = this.sorter.bind(this);
875
-
876
- if (this._groupsCount > 0) {
877
- this.set('sortedFilteredGroupedItems', this.filteredGroupedItems
878
- .filter(group => Array.isArray(group.items))
879
- .map(group => {
880
- group.items.sort(sorter);
881
- if (this.descending) {
882
- group.items.reverse();
883
- }
884
- return {
885
- name: group.name,
886
- id: group.id,
887
- items: group.items
888
- };
889
- }));
890
- return;
891
- }
892
-
893
- // No grouping
894
- this.filteredGroupedItems.sort(sorter);
895
- if (this.descending) {
896
- this.filteredGroupedItems.reverse();
897
- }
898
-
899
- this.set('sortedFilteredGroupedItems', this.filteredGroupedItems.slice());
900
- }
901
-
902
- _makeCsvField(str) {
903
- const result = str.replace(/"/gu, '""');
904
- if (result.search(/("|,|\n)/gu) >= 0) {
905
- return '"' + result + '"';
906
- }
907
- return str;
908
- }
909
350
  /**
910
351
  * Triggers a download of selected rows as a CSV file.
911
352
  * @returns {undefined}
912
353
  */
913
354
  _saveAsCsvAction() {
914
- const separator = ';',
915
- lf = '\n',
916
- header = this.columns.map(col => this._makeCsvField(col.title)).join(separator) + lf,
917
- rows = this.selectedItems.map(item => {
918
- return this.columns.map(column => {
919
- const cell = column.getString(item);
920
- if (cell === undefined || cell === null) {
921
- return '';
922
- }
923
- return this._makeCsvField(String(cell));
924
- }).join(separator) + lf;
925
- });
926
-
927
- rows.unshift(header);
928
-
929
- saveAs(new File(rows, this.csvFilename, {
930
- type: 'text/csv;charset=utf-8'
931
- }));
932
- }
933
-
934
- /**
935
- * Makes the data ready to be exported as XLSX.
936
- * @returns {Array} data Array of prepared rows.
937
- */
938
- _prepareXlsxData() {
939
- const headers = this.columns.map(col => col.title),
940
- data = this.selectedItems.map(item =>
941
- this.columns.map(column => {
942
- const value = column.toXlsxValue(item);
943
- return value == null ? '' : value;
944
- })
945
- );
946
-
947
- data.unshift(headers);
948
- return data;
355
+ saveAsCsvAction(this.columns, this.selectedItems, this.csvFilename);
949
356
  }
950
357
 
951
358
  /**
952
359
  * Triggers a download of selected rows as a XLSX file.
953
- * @param {Object} data The prepared rows to be saved as file with default value this._prepareXlsxData().
954
360
  * @returns {undefined}
955
361
  */
956
362
  _saveAsXlsxAction() {
957
- const data = this._prepareXlsxData(),
958
- xlsx = new NullXlsx(this.xlsxFilename).addSheetFromData(data, this.xlsxSheetname).generate();
959
-
960
- saveAs(new File([xlsx], this.xlsxFilename, {
961
- type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
962
- }));
363
+ saveAsXlsxAction(this.columns, this.selectedItems, this.xlsxFilename, this.xlsxSheetname);
963
364
  }
964
365
 
965
366
  /** view functions */
@@ -970,6 +371,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
970
371
  _getFoldIcon(expanded) {
971
372
  return expanded ? 'expand-less' : 'expand-more';
972
373
  }
374
+
973
375
  /**
974
376
  * Toggle folding of a group
975
377
  * @param {Event} event event
@@ -1019,17 +421,19 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1019
421
  }
1020
422
  }
1021
423
 
424
+ // TODO: move to publicInterface mixin
1022
425
  /** PUBLIC */
1023
426
 
1024
427
  suppressNextScrollReset() {
1025
428
  const list = this.$.groupedList.$.list;
1026
429
  // HACK: Replace _resetScrollPosition for one call to maintain scroll position
1027
- if (list._scrollTop > 0) {
430
+ if (list._scrollTop > 0 && !list._resetScrollPosition.suppressed) {
1028
431
  const reset = list._resetScrollPosition;
1029
432
  list._resetScrollPosition = () => {
1030
433
  // restore hack
1031
434
  list._resetScrollPosition = reset;
1032
435
  };
436
+ list._resetScrollPosition.suppressed = true;
1033
437
  }
1034
438
  }
1035
439
 
@@ -1062,6 +466,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1062
466
  }
1063
467
 
1064
468
  const removed = this.splice('data', index, 1);
469
+ this.data = this.data.slice();
1065
470
  if (Array.isArray(removed) && removed.length > 0) {
1066
471
  return removed[0];
1067
472
  }
@@ -1075,6 +480,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1075
480
  replaceItemAtIndex(index, newItem) {
1076
481
  this.suppressNextScrollReset();
1077
482
  this.splice('data', index, 1, newItem);
483
+ this.data = this.data.slice();
1078
484
  }
1079
485
  /**
1080
486
  * Convenience method for setting a value to an item's path and notifying any
@@ -1102,117 +508,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1102
508
  return this.$.groupedList.isItemSelected(item);
1103
509
  }
1104
510
 
1105
- isItemHighlighted(item) {
1106
- return this.$.groupedList.isItemHighlighted(item);
1107
- }
1108
-
1109
- highlight(items, reverse) {
1110
- if (!items) {
1111
- return;
1112
- }
1113
- const gl = this.$.groupedList;
1114
- if (Array.isArray(items)) {
1115
- items.forEach(item => gl.highlightItem(item, reverse));
1116
- return;
1117
- }
1118
- gl.highlightItem(items, reverse);
1119
- }
1120
-
1121
- _routeHashPropertyChanged(key, value) {
1122
- const deserialized = this._deserializeValue(value, Omnitable.properties[key].type);
1123
- if (deserialized === this.get(key)) {
1124
- return;
1125
- }
1126
- this.set(key, deserialized);
1127
- }
1128
-
1129
- _routeHashFilterChanged(key, value) {
1130
- const column = this.columns.find(c => c.name === key);
1131
-
1132
- if (!column) {
1133
- return;
1134
- }
1135
-
1136
- if (value === column._serializeFilter()) {
1137
- return;
1138
- }
1139
-
1140
- const deserialized = column._deserializeFilter(value);
1141
-
1142
- if (deserialized === null) {
1143
- column.resetFilter();
1144
- return;
1145
- }
1146
- column.set('filter', deserialized);
1147
- }
1148
- _computeRouteHashKeyRule(hashParam) {
1149
- if (!hashParam) {
1150
- return;
1151
- }
1152
- return new RegExp('^' + hashParam + '-(.+?)(?=(?:--|$))(?:-{2})?([A-Za-z0-9-_]+)?$', 'u');
1153
- }
1154
- _routeHashKeyChanged(key, value) {
1155
- const match = key.match(this._routeHashKeyRule);
1156
-
1157
- if (!Array.isArray(match)) {
1158
- return;
1159
- }
1160
-
1161
- if (match[2] == null && PROPERTY_HASH_PARAMS.indexOf(match[1]) > -1) {
1162
- this._routeHashPropertyChanged(match[1], value);
1163
- return;
1164
- }
1165
- if (match[2] !== null && match[1] === 'filter') {
1166
- this._routeHashFilterChanged(match[2], value);
1167
- }
1168
- }
1169
-
1170
- _updateParamsFromHash() {
1171
- if (!this.hashParam || !this._routeHash) {
1172
- return;
1173
- }
1174
- const hash = this._routeHash;
1175
- Object.keys(hash).forEach(key => {
1176
- this._routeHashKeyChanged(key, hash[key]);
1177
- });
1178
- }
1179
-
1180
- _updateRouteParam(key) {
1181
- if (!this.hashParam || !this._routeHash) {
1182
- return;
1183
- }
1184
-
1185
- const path = ['_routeHash', this.hashParam + '-' + key],
1186
- hashValue = this.get(path),
1187
- value = this.get(key),
1188
- serialized = this._serializeValue(value, Omnitable.properties[key].type);
1189
-
1190
- if (serialized === hashValue) {
1191
- return;
1192
- }
1193
- this.set(path, serialized === undefined ? null : serialized);
1194
- }
1195
-
1196
- _filterForRouteChanged(column) {
1197
- if (!this.hashParam || !this._routeHash || !Array.isArray(this.data)) {
1198
- return;
1199
- }
1200
-
1201
- const path = ['_routeHash', this.hashParam + '-filter--' + column.name],
1202
- hashValue = this.get(path),
1203
- serialized = column._serializeFilter();
1204
-
1205
- if (serialized === hashValue) {
1206
- return;
1207
- }
1208
-
1209
- this.set(path, serialized === undefined ? null : serialized);
1210
- }
1211
-
1212
- _debounce(name, fn, asyncModule = timeOut.after(0)) {
1213
- this.debouncers[name] = Debouncer.debounce(this.debouncers[name], asyncModule, fn);
1214
- }
1215
-
1216
511
  _renderRowStats(numRows, totalAvailable) {
1217
512
  if (Number.isInteger(totalAvailable) && totalAvailable > numRows) {
1218
513
  return this.ngettext('{1} / {0} row', '{1} / {0} rows', totalAvailable, numRows);
@@ -1220,24 +515,31 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1220
515
  return this.ngettext('{0} row', '{0} rows', numRows);
1221
516
  }
1222
517
 
1223
- renderFastLayoutCss(layoutCss, outlet) {
1224
- render(layoutCss, outlet);
1225
- }
1226
-
1227
518
  _onCompleteValues(columns, type, value) { /* eslint-disable-next-line no-bitwise */
1228
519
  return columns?.filter?.(c => c[type]).sort((a, b) => ((b === value) >> 0) - ((a === value) >> 0));
1229
520
  }
521
+
1230
522
  _onCompleteChange(type) {
1231
523
  return (val, close) => {
1232
524
  const value = (val[0] ?? val)?.name ?? '',
1233
- prop = type === 'groupOn' ? 'groupOnDescending' : 'descending';
1234
- this[prop] = value && value === this[type] ? !this[prop] : false;
1235
- this[type] = value;
525
+ setter = type === 'groupOn' ? this.setGroupOn : this.setSortOn,
526
+ directionSetter = type === 'groupOn' ? this.setGroupOnDescending : this.setDescending;
527
+
528
+ setter(oldValue => {
529
+ if (value) {
530
+ directionSetter(oldDirection => value === oldValue ? !oldDirection : false);
531
+ } else {
532
+ directionSetter(null);
533
+ }
534
+ return value;
535
+ });
536
+
1236
537
  value && close(); /* eslint-disable-line no-unused-expressions */
1237
538
  };
1238
539
  }
1239
540
 
1240
- _renderSettings(normalizedSettings, collapsed, settingsId, hasChangedSettings, hasHiddenFilter) {
541
+ // eslint-disable-next-line max-params
542
+ _renderSettings(normalizedSettings, collapsed, settingsId, hasChangedSettings, hasHiddenFilter, filters) {
1241
543
  return litHtml`<cosmoz-omnitable-settings
1242
544
  .settings=${ normalizedSettings }
1243
545
  .onSettings=${ this.setSettings }
@@ -1247,10 +549,11 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1247
549
  .onSave=${ this.onSettingsSave }
1248
550
  .onReset=${ this.onSettingsReset }
1249
551
  .badge=${ hasHiddenFilter }
552
+ .filters=${ filters }
1250
553
  >`;
1251
554
  }
1252
555
  }
1253
- customElements.define(Omnitable.is, Omnitable);
556
+ customElements.define('cosmoz-omnitable', Omnitable);
1254
557
 
1255
558
  const tmplt = `
1256
559
  <slot name="actions" slot="actions"></slot>