@neovici/cosmoz-omnitable 7.2.1 → 8.0.0-beta.3

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 (50) hide show
  1. package/README.md +25 -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 +1 -5
  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 +5 -8
  19. package/cosmoz-omnitable-styles.js +4 -8
  20. package/cosmoz-omnitable.js +72 -764
  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 -123
  27. package/lib/cosmoz-omnitable-settings.js +8 -5
  28. package/lib/cosmoz-omnitable-time-range-input.js +130 -0
  29. package/lib/generic-sorter.js +2 -2
  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 +138 -0
  37. package/lib/use-hash-state.js +59 -0
  38. package/lib/use-layout.js +1 -1
  39. package/lib/use-omnitable.js +26 -14
  40. package/lib/use-processed-items.js +132 -0
  41. package/lib/use-sort-and-group-options.js +30 -0
  42. package/lib/utils-amount.js +147 -0
  43. package/lib/utils-data.js +36 -0
  44. package/lib/utils-date.js +204 -0
  45. package/lib/utils-datetime.js +71 -0
  46. package/lib/utils-number.js +112 -0
  47. package/lib/utils-time.js +115 -0
  48. package/package.json +1 -2
  49. package/lib/use-force-render.js +0 -8
  50. 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,12 +20,7 @@ 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
26
  import { html as litHtml, render } from 'lit-html';
@@ -37,9 +30,8 @@ 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,254 +177,104 @@ 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
278
  '_selectedItemsChanged(selectedItems.*)',
429
279
  'renderFastLayoutCss(layoutCss, $.layoutStyle)'
430
280
  ];
@@ -433,67 +283,27 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
433
283
  constructor() {
434
284
  super();
435
285
 
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
286
  this._onKey = this._onKey.bind(this);
442
- this._resizeObserver = new ResizeObserver(this._onResize.bind(this));
443
287
  }
444
288
 
445
289
  connectedCallback() {
446
290
  super.connectedCallback();
447
291
 
448
292
  this.$.groupedList.scrollTarget = this.$.scroller;
449
- this.addEventListener('cosmoz-column-hidden-changed', this._debounceUpdateColumns);
450
- this.addEventListener('cosmoz-column-disabled-changed', this._debounceUpdateColumns);
451
293
 
452
294
  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
295
  window.addEventListener('keydown', this._onKey);
458
296
  window.addEventListener('keyup', this._onKey);
459
- this._resizeObserver.observe(this);
460
- this._updateParamsFromHash();
461
297
  }
462
298
 
463
299
  disconnectedCallback() {
464
300
  super.disconnectedCallback();
465
301
 
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
302
  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
303
  window.removeEventListener('keydown', this._onKey);
478
304
  window.removeEventListener('keyup', this._onKey);
479
305
  }
480
306
 
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
307
  /** ELEMENT BEHAVIOR */
498
308
 
499
309
  _computeDataValidity({ base: data } = {}) {
@@ -509,12 +319,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
509
319
  return `(${ direction })`;
510
320
  }
511
321
 
512
- visibleChanged(turnedVisible) {
513
- if (turnedVisible) {
514
- this._debounceUpdateColumns();
515
- }
516
- }
517
-
518
322
  _onUpdateItemSize(event) {
519
323
  const { detail } = event;
520
324
  if (detail && detail.item) {
@@ -523,41 +327,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
523
327
  event.stopPropagation();
524
328
  }
525
329
 
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
330
  _onKey(e) {
562
331
  this._shiftKey = e.shiftKey;
563
332
  this._ctrlKey = e.ctrlKey;
@@ -579,387 +348,20 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
579
348
  event.stopPropagation();
580
349
  }
581
350
 
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
351
  /**
910
352
  * Triggers a download of selected rows as a CSV file.
911
353
  * @returns {undefined}
912
354
  */
913
355
  _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;
356
+ saveAsCsvAction(this.columns, this.selectedItems, this.csvFilename);
949
357
  }
950
358
 
951
359
  /**
952
360
  * 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
361
  * @returns {undefined}
955
362
  */
956
363
  _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
- }));
364
+ saveAsXlsxAction(this.columns, this.selectedItems, this.xlsxFilename, this.xlsxSheetname);
963
365
  }
964
366
 
965
367
  /** view functions */
@@ -970,6 +372,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
970
372
  _getFoldIcon(expanded) {
971
373
  return expanded ? 'expand-less' : 'expand-more';
972
374
  }
375
+
973
376
  /**
974
377
  * Toggle folding of a group
975
378
  * @param {Event} event event
@@ -1019,17 +422,19 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1019
422
  }
1020
423
  }
1021
424
 
425
+ // TODO: move to publicInterface mixin
1022
426
  /** PUBLIC */
1023
427
 
1024
428
  suppressNextScrollReset() {
1025
429
  const list = this.$.groupedList.$.list;
1026
430
  // HACK: Replace _resetScrollPosition for one call to maintain scroll position
1027
- if (list._scrollTop > 0) {
431
+ if (list._scrollTop > 0 && !list._resetScrollPosition.suppressed) {
1028
432
  const reset = list._resetScrollPosition;
1029
433
  list._resetScrollPosition = () => {
1030
434
  // restore hack
1031
435
  list._resetScrollPosition = reset;
1032
436
  };
437
+ list._resetScrollPosition.suppressed = true;
1033
438
  }
1034
439
  }
1035
440
 
@@ -1062,6 +467,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1062
467
  }
1063
468
 
1064
469
  const removed = this.splice('data', index, 1);
470
+ this.data = this.data.slice();
1065
471
  if (Array.isArray(removed) && removed.length > 0) {
1066
472
  return removed[0];
1067
473
  }
@@ -1075,6 +481,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1075
481
  replaceItemAtIndex(index, newItem) {
1076
482
  this.suppressNextScrollReset();
1077
483
  this.splice('data', index, 1, newItem);
484
+ this.data = this.data.slice();
1078
485
  }
1079
486
  /**
1080
487
  * Convenience method for setting a value to an item's path and notifying any
@@ -1102,117 +509,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1102
509
  return this.$.groupedList.isItemSelected(item);
1103
510
  }
1104
511
 
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
512
  _renderRowStats(numRows, totalAvailable) {
1217
513
  if (Number.isInteger(totalAvailable) && totalAvailable > numRows) {
1218
514
  return this.ngettext('{1} / {0} row', '{1} / {0} rows', totalAvailable, numRows);
@@ -1227,17 +523,28 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1227
523
  _onCompleteValues(columns, type, value) { /* eslint-disable-next-line no-bitwise */
1228
524
  return columns?.filter?.(c => c[type]).sort((a, b) => ((b === value) >> 0) - ((a === value) >> 0));
1229
525
  }
526
+
1230
527
  _onCompleteChange(type) {
1231
528
  return (val, close) => {
1232
529
  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;
530
+ setter = type === 'groupOn' ? this.setGroupOn : this.setSortOn,
531
+ directionSetter = type === 'groupOn' ? this.setGroupOnDescending : this.setDescending;
532
+
533
+ setter(oldValue => {
534
+ if (value) {
535
+ directionSetter(oldDirection => value === oldValue ? !oldDirection : false);
536
+ } else {
537
+ directionSetter(null);
538
+ }
539
+ return value;
540
+ });
541
+
1236
542
  value && close(); /* eslint-disable-line no-unused-expressions */
1237
543
  };
1238
544
  }
1239
545
 
1240
- _renderSettings(normalizedSettings, collapsed, settingsId, hasChangedSettings, hasHiddenFilter) {
546
+ // eslint-disable-next-line max-params
547
+ _renderSettings(normalizedSettings, collapsed, settingsId, hasChangedSettings, hasHiddenFilter, filters) {
1241
548
  return litHtml`<cosmoz-omnitable-settings
1242
549
  .settings=${ normalizedSettings }
1243
550
  .onSettings=${ this.setSettings }
@@ -1247,10 +554,11 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1247
554
  .onSave=${ this.onSettingsSave }
1248
555
  .onReset=${ this.onSettingsReset }
1249
556
  .badge=${ hasHiddenFilter }
557
+ .filters=${ filters }
1250
558
  >`;
1251
559
  }
1252
560
  }
1253
- customElements.define(Omnitable.is, Omnitable);
561
+ customElements.define('cosmoz-omnitable', Omnitable);
1254
562
 
1255
563
  const tmplt = `
1256
564
  <slot name="actions" slot="actions"></slot>