@neovici/cosmoz-omnitable 7.3.0 → 8.0.0-beta.4

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