@neovici/cosmoz-omnitable 7.2.1 → 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 (50) 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 +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 +1 -5
  20. package/cosmoz-omnitable.js +72 -763
  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 +7 -4
  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 +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 +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 -1
  49. package/lib/use-force-render.js +0 -8
  50. 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,9 +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
- import { genericSorter } from './lib/generic-sorter';
41
-
42
- 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';
43
36
 
44
37
  /**
45
38
  * @polymer
@@ -57,16 +50,17 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
57
50
  ${ html([styles]) }
58
51
  <div id="layoutStyle"></div>
59
52
 
60
- <cosmoz-page-location id="location" route-hash="{{ _routeHash }}"></cosmoz-page-location>
61
-
62
53
  <div class="mainContainer">
63
54
  <div class="header" id="header">
64
55
  <input class="checkbox all" type="checkbox" checked="[[ _allSelected ]]" on-input="_onAllCheckboxChange" disabled$="[[ !_dataIsValid ]]" />
65
56
  <cosmoz-omnitable-header-row
57
+ data="[[ data ]]"
66
58
  columns="[[ normalizedColumns ]]"
59
+ filters="[[ filters ]]"
67
60
  group-on-column="[[ groupOnColumn ]]"
68
- content="[[ _renderSettings(normalizedSettings, collapsedColumns, settingsId, hasChangedSettings, hasHiddenFilter) ]]"
69
- >
61
+ content="[[ _renderSettings(normalizedSettings, collapsedColumns, settingsId, hasChangedSettings, hasHiddenFilter, filters) ]]"
62
+ set-filter-state="[[ setFilterState ]]"
63
+ ></cosmoz-omnitable-header-row>
70
64
  </div>
71
65
  <div class="tableContent" id="tableContent">
72
66
  <template is="dom-if" if="[[ !_dataIsValid ]]">
@@ -101,18 +95,23 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
101
95
  <cosmoz-grouped-list id="groupedList"
102
96
  data="{{ sortedFilteredGroupedItems }}"
103
97
  selected-items="{{ selectedItems }}"
104
- highlighted-items="{{ highlightedItems }}"
105
98
  display-empty-groups="[[ displayEmptyGroups ]]"
106
99
  compare-items-fn="[[ compareItemsFn ]]"
107
100
  >
108
101
  <template slot="templates" data-type="item">
109
102
  <div class="item-row-wrapper">
110
- <div selected$="[[ selected ]]" class="itemRow" highlighted$="[[ highlighted ]]">
103
+ <div selected$="[[ selected ]]" class="itemRow">
111
104
  <input class="checkbox" type="checkbox" checked="[[ selected ]]" on-input="_onCheckboxChange" disabled$="[[ !_dataIsValid ]]" />
112
105
  <cosmoz-omnitable-item-row columns="[[ normalizedColumns ]]"
113
- 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 ]]">
114
108
  </cosmoz-omnitable-item-row>
115
- <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>
116
115
  </div>
117
116
  <cosmoz-omnitable-item-expand columns="[[ collapsedColumns ]]"
118
117
  item="[[item]]" selected="{{ selected }}" expanded$="{{ expanded }}" group-on-column="[[ groupOnColumn ]]"
@@ -139,18 +138,20 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
139
138
  <div class="footer-controls">
140
139
  <cosmoz-autocomplete
141
140
  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"
141
+ source="[[ _onCompleteValues(columns, 'groupOn', groupOnColumn) ]]" value="[[ groupOnColumn ]]" limit="1" text-property="title"
142
+ always-float-label item-height="48" item-limit="8"
143
143
  class="footer-control" on-change="[[ _onCompleteChange('groupOn') ]]" default-index="-1" show-single show-selection
144
144
  ></cosmoz-autocomplete>
145
145
  <cosmoz-autocomplete
146
146
  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"
147
+ source="[[ _onCompleteValues(columns, 'sortOn', sortOnColumn) ]]" value="[[ sortOnColumn ]]" limit="1" text-property="title"
148
+ always-float-label item-height="48" item-limit="8"
148
149
  class="footer-control" on-change="[[ _onCompleteChange('sortOn') ]]" default-index="-1" show-single show-selection
149
150
  ></cosmoz-autocomplete>
150
151
  </div>
151
152
  <div class="footer-tableStats">
152
- <span>[[ ngettext('{0} group', '{0} groups', _groupsCount, t) ]]</span>
153
- <span>[[ _renderRowStats(filteredItems.length, totalAvailable, t) ]]</span>
153
+ <span>[[ ngettext('{0} group', '{0} groups', groupsCount, t) ]]</span>
154
+ <span>[[ _renderRowStats(numProcessedItems, totalAvailable, t) ]]</span>
154
155
  </div>
155
156
  <cosmoz-bottom-bar id="bottomBar" class="footer-actionBar" match-parent
156
157
  on-action="_onAction" active$="[[ !isEmpty(selectedItems.length) ]]" computed-bar-height="{{ computedBarHeight }}">
@@ -177,254 +178,104 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
177
178
  </div>
178
179
 
179
180
  <div id="columns">
180
- <slot id="columnsSlot" on-slotchange="_debounceUpdateColumns"></slot>
181
+ <slot id="columnsSlot"></slot>
181
182
  </div>
182
183
  `;
183
184
  template.setAttribute('strip-whitespace', '');
184
185
  return template;
185
186
  }
186
187
 
187
- static get is() {
188
- return 'cosmoz-omnitable';
189
- }
190
-
191
188
  /* eslint-disable-next-line max-lines-per-function */
192
189
  static get properties() {
193
190
  return {
194
-
195
191
  /**
196
192
  * Filename when saving as CSV
197
193
  */
198
- csvFilename: {
199
- type: String,
200
- value: 'omnitable.csv'
201
- },
194
+ csvFilename: { type: String, value: 'omnitable.csv' },
202
195
 
203
196
  /**
204
197
  * Filename when saving as XLSX
205
198
  */
206
- xlsxFilename: {
207
- type: String,
208
- value: 'omnitable.xlsx'
209
- },
199
+ xlsxFilename: { type: String, value: 'omnitable.xlsx' },
210
200
 
211
201
  /**
212
202
  * Sheet name when saving as XLSX
213
203
  */
214
- xlsxSheetname: {
215
- type: String,
216
- value: 'Omnitable'
217
- },
204
+ xlsxSheetname: { type: String, value: 'Omnitable' },
218
205
 
219
206
  /**
220
207
  * Array used to list items.
221
208
  */
222
- data: {
223
- type: Array
224
- },
209
+ data: { type: Array },
225
210
 
226
211
  /**
227
212
  * This function is used to determine which items are kept selected across data updates
213
+ * TODO: probably broken
228
214
  */
229
215
  compareItemsFn: Function,
230
216
 
231
217
  /**
232
218
  * True if data is a valid and not empty array.
233
219
  */
234
- _dataIsValid: {
235
- type: Boolean,
236
- value: false,
237
- computed: '_computeDataValidity(data.*)'
238
- },
220
+ _dataIsValid: { type: Boolean, value: false, computed: '_computeDataValidity(data.*)' },
239
221
 
240
222
  /**
241
223
  * If set to true, then group a row will be displayed for groups that contain no items.
242
224
  */
243
- displayEmptyGroups: {
244
- type: Boolean,
245
- value: false
246
- },
225
+ displayEmptyGroups: { type: Boolean, value: false },
247
226
 
248
227
  /**
249
228
  * Specific columns to enable
250
229
  */
251
- enabledColumns: {
252
- type: Array,
253
- observer: '_debounceUpdateColumns'
254
- },
230
+ enabledColumns: { type: Array },
255
231
 
256
232
  /**
257
233
  * Whether bottom-bar has actions.
258
234
  */
259
- hasActions: {
260
- type: Boolean,
261
- value: false
262
- },
235
+ hasActions: { type: Boolean, value: false },
263
236
 
264
237
  /**
265
238
  * Shows a loading overlay to indicate data will be updated
266
239
  */
267
- loading: {
268
- type: Boolean,
269
- value: false
270
- },
240
+ loading: { type: Boolean, value: false },
271
241
 
272
242
  /**
273
243
  * List of selected rows/items in `data`.
274
244
  */
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
- },
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 },
315
249
 
316
250
  /**
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.
251
+ * The column name to group on.
336
252
  */
337
- filteredGroupedItems: {
338
- type: Array
339
- },
253
+ groupOn: { type: String, notify: true, value: '' },
340
254
 
341
255
  /**
342
256
  * Sorted items structure after filtering and grouping.
343
257
  */
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
- },
258
+ sortedFilteredGroupedItems: { type: Array, notify: true },
375
259
 
376
260
  /**
377
261
  * List of columns definition for this table.
378
262
  */
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
- },
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 },
407
267
 
408
268
  /**
409
269
  * True when all items are selected.
410
270
  */
411
- _allSelected: {
412
- type: Boolean
413
- },
414
- computedBarHeight: {
415
- type: Number
416
- },
417
- settingsId: {
418
- type: String,
419
- value: undefined
420
- }
271
+ _allSelected: { type: Boolean },
272
+ computedBarHeight: { type: Number },
273
+ settingsId: { type: String, value: undefined }
421
274
  };
422
275
  }
423
276
 
424
277
  static get observers() {
425
278
  return [
426
- '_dataChanged(data.splices)',
427
- '_debounceProcessItems(sortOn, descending)',
428
279
  '_selectedItemsChanged(selectedItems.*)',
429
280
  'renderFastLayoutCss(layoutCss, $.layoutStyle)'
430
281
  ];
@@ -433,67 +284,27 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
433
284
  constructor() {
434
285
  super();
435
286
 
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
287
  this._onKey = this._onKey.bind(this);
442
- this._resizeObserver = new ResizeObserver(this._onResize.bind(this));
443
288
  }
444
289
 
445
290
  connectedCallback() {
446
291
  super.connectedCallback();
447
292
 
448
293
  this.$.groupedList.scrollTarget = this.$.scroller;
449
- this.addEventListener('cosmoz-column-hidden-changed', this._debounceUpdateColumns);
450
- this.addEventListener('cosmoz-column-disabled-changed', this._debounceUpdateColumns);
451
294
 
452
295
  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
296
  window.addEventListener('keydown', this._onKey);
458
297
  window.addEventListener('keyup', this._onKey);
459
- this._resizeObserver.observe(this);
460
- this._updateParamsFromHash();
461
298
  }
462
299
 
463
300
  disconnectedCallback() {
464
301
  super.disconnectedCallback();
465
302
 
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
303
  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
304
  window.removeEventListener('keydown', this._onKey);
478
305
  window.removeEventListener('keyup', this._onKey);
479
306
  }
480
307
 
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
308
  /** ELEMENT BEHAVIOR */
498
309
 
499
310
  _computeDataValidity({ base: data } = {}) {
@@ -509,12 +320,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
509
320
  return `(${ direction })`;
510
321
  }
511
322
 
512
- visibleChanged(turnedVisible) {
513
- if (turnedVisible) {
514
- this._debounceUpdateColumns();
515
- }
516
- }
517
-
518
323
  _onUpdateItemSize(event) {
519
324
  const { detail } = event;
520
325
  if (detail && detail.item) {
@@ -523,41 +328,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
523
328
  event.stopPropagation();
524
329
  }
525
330
 
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
331
  _onKey(e) {
562
332
  this._shiftKey = e.shiftKey;
563
333
  this._ctrlKey = e.ctrlKey;
@@ -579,387 +349,20 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
579
349
  event.stopPropagation();
580
350
  }
581
351
 
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
352
  /**
910
353
  * Triggers a download of selected rows as a CSV file.
911
354
  * @returns {undefined}
912
355
  */
913
356
  _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;
357
+ saveAsCsvAction(this.columns, this.selectedItems, this.csvFilename);
949
358
  }
950
359
 
951
360
  /**
952
361
  * 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
362
  * @returns {undefined}
955
363
  */
956
364
  _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
- }));
365
+ saveAsXlsxAction(this.columns, this.selectedItems, this.xlsxFilename, this.xlsxSheetname);
963
366
  }
964
367
 
965
368
  /** view functions */
@@ -970,6 +373,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
970
373
  _getFoldIcon(expanded) {
971
374
  return expanded ? 'expand-less' : 'expand-more';
972
375
  }
376
+
973
377
  /**
974
378
  * Toggle folding of a group
975
379
  * @param {Event} event event
@@ -1019,17 +423,19 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1019
423
  }
1020
424
  }
1021
425
 
426
+ // TODO: move to publicInterface mixin
1022
427
  /** PUBLIC */
1023
428
 
1024
429
  suppressNextScrollReset() {
1025
430
  const list = this.$.groupedList.$.list;
1026
431
  // HACK: Replace _resetScrollPosition for one call to maintain scroll position
1027
- if (list._scrollTop > 0) {
432
+ if (list._scrollTop > 0 && !list._resetScrollPosition.suppressed) {
1028
433
  const reset = list._resetScrollPosition;
1029
434
  list._resetScrollPosition = () => {
1030
435
  // restore hack
1031
436
  list._resetScrollPosition = reset;
1032
437
  };
438
+ list._resetScrollPosition.suppressed = true;
1033
439
  }
1034
440
  }
1035
441
 
@@ -1062,6 +468,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1062
468
  }
1063
469
 
1064
470
  const removed = this.splice('data', index, 1);
471
+ this.data = this.data.slice();
1065
472
  if (Array.isArray(removed) && removed.length > 0) {
1066
473
  return removed[0];
1067
474
  }
@@ -1075,6 +482,7 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1075
482
  replaceItemAtIndex(index, newItem) {
1076
483
  this.suppressNextScrollReset();
1077
484
  this.splice('data', index, 1, newItem);
485
+ this.data = this.data.slice();
1078
486
  }
1079
487
  /**
1080
488
  * Convenience method for setting a value to an item's path and notifying any
@@ -1102,117 +510,6 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1102
510
  return this.$.groupedList.isItemSelected(item);
1103
511
  }
1104
512
 
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
513
  _renderRowStats(numRows, totalAvailable) {
1217
514
  if (Number.isInteger(totalAvailable) && totalAvailable > numRows) {
1218
515
  return this.ngettext('{1} / {0} row', '{1} / {0} rows', totalAvailable, numRows);
@@ -1227,17 +524,28 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1227
524
  _onCompleteValues(columns, type, value) { /* eslint-disable-next-line no-bitwise */
1228
525
  return columns?.filter?.(c => c[type]).sort((a, b) => ((b === value) >> 0) - ((a === value) >> 0));
1229
526
  }
527
+
1230
528
  _onCompleteChange(type) {
1231
529
  return (val, close) => {
1232
530
  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;
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
+
1236
543
  value && close(); /* eslint-disable-line no-unused-expressions */
1237
544
  };
1238
545
  }
1239
546
 
1240
- _renderSettings(normalizedSettings, collapsed, settingsId, hasChangedSettings, hasHiddenFilter) {
547
+ // eslint-disable-next-line max-params
548
+ _renderSettings(normalizedSettings, collapsed, settingsId, hasChangedSettings, hasHiddenFilter, filters) {
1241
549
  return litHtml`<cosmoz-omnitable-settings
1242
550
  .settings=${ normalizedSettings }
1243
551
  .onSettings=${ this.setSettings }
@@ -1247,10 +555,11 @@ class Omnitable extends hauntedPolymer(useOmnitable)(mixin({ isEmpty }, translat
1247
555
  .onSave=${ this.onSettingsSave }
1248
556
  .onReset=${ this.onSettingsReset }
1249
557
  .badge=${ hasHiddenFilter }
558
+ .filters=${ filters }
1250
559
  >`;
1251
560
  }
1252
561
  }
1253
- customElements.define(Omnitable.is, Omnitable);
562
+ customElements.define('cosmoz-omnitable', Omnitable);
1254
563
 
1255
564
  const tmplt = `
1256
565
  <slot name="actions" slot="actions"></slot>