@neovici/cosmoz-omnitable 7.1.0 → 8.0.0-beta.1

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