@vaadin/grid 24.2.0-dev.f254716fe → 24.3.0-alpha2

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 (40) hide show
  1. package/package.json +11 -11
  2. package/src/vaadin-grid-array-data-provider-mixin.js +1 -1
  3. package/src/vaadin-grid-column-group-mixin.d.ts +20 -0
  4. package/src/vaadin-grid-column-group-mixin.js +369 -0
  5. package/src/vaadin-grid-column-group.d.ts +4 -14
  6. package/src/vaadin-grid-column-group.js +8 -356
  7. package/src/vaadin-grid-column-mixin.d.ts +156 -0
  8. package/src/vaadin-grid-column-mixin.js +887 -0
  9. package/src/vaadin-grid-column.d.ts +11 -138
  10. package/src/vaadin-grid-column.js +6 -876
  11. package/src/vaadin-grid-data-provider-mixin.d.ts +6 -5
  12. package/src/vaadin-grid-data-provider-mixin.js +51 -20
  13. package/src/vaadin-grid-drag-and-drop-mixin.js +1 -1
  14. package/src/vaadin-grid-dynamic-columns-mixin.js +1 -1
  15. package/src/vaadin-grid-filter-column.js +5 -1
  16. package/src/vaadin-grid-filter-element-mixin.d.ts +34 -0
  17. package/src/vaadin-grid-filter-element-mixin.js +99 -0
  18. package/src/vaadin-grid-filter.d.ts +4 -21
  19. package/src/vaadin-grid-filter.js +8 -85
  20. package/src/vaadin-grid-keyboard-navigation-mixin.js +24 -4
  21. package/src/vaadin-grid-mixin.d.ts +218 -0
  22. package/src/vaadin-grid-mixin.js +1022 -0
  23. package/src/vaadin-grid-scroll-mixin.js +1 -1
  24. package/src/vaadin-grid-selection-column-base-mixin.d.ts +6 -0
  25. package/src/vaadin-grid-selection-column-base-mixin.js +151 -0
  26. package/src/vaadin-grid-selection-column.js +4 -1
  27. package/src/vaadin-grid-sort-column.js +5 -1
  28. package/src/vaadin-grid-sorter-mixin.d.ts +44 -0
  29. package/src/vaadin-grid-sorter-mixin.js +198 -0
  30. package/src/vaadin-grid-sorter.d.ts +3 -32
  31. package/src/vaadin-grid-sorter.js +8 -182
  32. package/src/vaadin-grid-tree-column-mixin.d.ts +19 -0
  33. package/src/vaadin-grid-tree-column-mixin.js +92 -0
  34. package/src/vaadin-grid-tree-column.d.ts +9 -7
  35. package/src/vaadin-grid-tree-column.js +7 -82
  36. package/src/vaadin-grid-tree-toggle.js +3 -1
  37. package/src/vaadin-grid.d.ts +5 -190
  38. package/src/vaadin-grid.js +7 -1018
  39. package/web-types.json +2311 -0
  40. package/web-types.lit.json +1007 -0
@@ -0,0 +1,1022 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2016 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js';
7
+ import { TabindexMixin } from '@vaadin/a11y-base/src/tabindex-mixin.js';
8
+ import { animationFrame, microTask } from '@vaadin/component-base/src/async.js';
9
+ import { isAndroid, isChrome, isFirefox, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.js';
10
+ import { Debouncer } from '@vaadin/component-base/src/debounce.js';
11
+ import { getClosestElement } from '@vaadin/component-base/src/dom-utils.js';
12
+ import { processTemplates } from '@vaadin/component-base/src/templates.js';
13
+ import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
14
+ import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js';
15
+ import { A11yMixin } from './vaadin-grid-a11y-mixin.js';
16
+ import { ActiveItemMixin } from './vaadin-grid-active-item-mixin.js';
17
+ import { ArrayDataProviderMixin } from './vaadin-grid-array-data-provider-mixin.js';
18
+ import { ColumnReorderingMixin } from './vaadin-grid-column-reordering-mixin.js';
19
+ import { ColumnResizingMixin } from './vaadin-grid-column-resizing-mixin.js';
20
+ import { DataProviderMixin } from './vaadin-grid-data-provider-mixin.js';
21
+ import { DragAndDropMixin } from './vaadin-grid-drag-and-drop-mixin.js';
22
+ import { DynamicColumnsMixin } from './vaadin-grid-dynamic-columns-mixin.js';
23
+ import { EventContextMixin } from './vaadin-grid-event-context-mixin.js';
24
+ import { FilterMixin } from './vaadin-grid-filter-mixin.js';
25
+ import {
26
+ getBodyRowCells,
27
+ iterateChildren,
28
+ iterateRowCells,
29
+ updateBooleanRowStates,
30
+ updateCellsPart,
31
+ } from './vaadin-grid-helpers.js';
32
+ import { KeyboardNavigationMixin } from './vaadin-grid-keyboard-navigation-mixin.js';
33
+ import { RowDetailsMixin } from './vaadin-grid-row-details-mixin.js';
34
+ import { ScrollMixin } from './vaadin-grid-scroll-mixin.js';
35
+ import { SelectionMixin } from './vaadin-grid-selection-mixin.js';
36
+ import { SortMixin } from './vaadin-grid-sort-mixin.js';
37
+ import { StylingMixin } from './vaadin-grid-styling-mixin.js';
38
+
39
+ /**
40
+ * A mixin providing common grid functionality.
41
+ *
42
+ * @polymerMixin
43
+ * @mixes A11yMixin
44
+ * @mixes ActiveItemMixin
45
+ * @mixes ArrayDataProviderMixin
46
+ * @mixes ColumnResizingMixin
47
+ * @mixes DataProviderMixin
48
+ * @mixes DynamicColumnsMixin
49
+ * @mixes FilterMixin
50
+ * @mixes RowDetailsMixin
51
+ * @mixes ScrollMixin
52
+ * @mixes SelectionMixin
53
+ * @mixes SortMixin
54
+ * @mixes KeyboardNavigationMixin
55
+ * @mixes ColumnReorderingMixin
56
+ * @mixes EventContextMixin
57
+ * @mixes StylingMixin
58
+ * @mixes DragAndDropMixin
59
+ */
60
+ export const GridMixin = (superClass) =>
61
+ class extends ArrayDataProviderMixin(
62
+ DataProviderMixin(
63
+ DynamicColumnsMixin(
64
+ ActiveItemMixin(
65
+ ScrollMixin(
66
+ SelectionMixin(
67
+ SortMixin(
68
+ RowDetailsMixin(
69
+ KeyboardNavigationMixin(
70
+ A11yMixin(
71
+ FilterMixin(
72
+ ColumnReorderingMixin(
73
+ ColumnResizingMixin(
74
+ EventContextMixin(DragAndDropMixin(StylingMixin(TabindexMixin(superClass)))),
75
+ ),
76
+ ),
77
+ ),
78
+ ),
79
+ ),
80
+ ),
81
+ ),
82
+ ),
83
+ ),
84
+ ),
85
+ ),
86
+ ),
87
+ ) {
88
+ static get observers() {
89
+ return [
90
+ '_columnTreeChanged(_columnTree, _columnTree.*)',
91
+ '_flatSizeChanged(_flatSize, __virtualizer, _hasData, _columnTree)',
92
+ ];
93
+ }
94
+
95
+ static get properties() {
96
+ return {
97
+ /** @private */
98
+ _safari: {
99
+ type: Boolean,
100
+ value: isSafari,
101
+ },
102
+
103
+ /** @private */
104
+ _ios: {
105
+ type: Boolean,
106
+ value: isIOS,
107
+ },
108
+
109
+ /** @private */
110
+ _firefox: {
111
+ type: Boolean,
112
+ value: isFirefox,
113
+ },
114
+
115
+ /** @private */
116
+ _android: {
117
+ type: Boolean,
118
+ value: isAndroid,
119
+ },
120
+
121
+ /** @private */
122
+ _touchDevice: {
123
+ type: Boolean,
124
+ value: isTouch,
125
+ },
126
+
127
+ /**
128
+ * If true, the grid's height is defined by its rows.
129
+ *
130
+ * Effectively, this disables the grid's virtual scrolling so that all the rows are rendered in the DOM at once.
131
+ * If the grid has a large number of items, using the feature is discouraged to avoid performance issues.
132
+ * @attr {boolean} all-rows-visible
133
+ * @type {boolean}
134
+ */
135
+ allRowsVisible: {
136
+ type: Boolean,
137
+ value: false,
138
+ reflectToAttribute: true,
139
+ },
140
+
141
+ /** @private */
142
+ __pendingRecalculateColumnWidths: {
143
+ type: Boolean,
144
+ value: true,
145
+ },
146
+
147
+ /** @private */
148
+ isAttached: {
149
+ value: false,
150
+ },
151
+
152
+ /**
153
+ * An internal property that is mainly used by `vaadin-template-renderer`
154
+ * to identify grid elements.
155
+ *
156
+ * @private
157
+ */
158
+ __gridElement: {
159
+ type: Boolean,
160
+ value: true,
161
+ },
162
+ };
163
+ }
164
+
165
+ constructor() {
166
+ super();
167
+ this.addEventListener('animationend', this._onAnimationEnd);
168
+ }
169
+
170
+ /** @private */
171
+ get _firstVisibleIndex() {
172
+ const firstVisibleItem = this.__getFirstVisibleItem();
173
+ return firstVisibleItem ? firstVisibleItem.index : undefined;
174
+ }
175
+
176
+ /** @private */
177
+ get _lastVisibleIndex() {
178
+ const lastVisibleItem = this.__getLastVisibleItem();
179
+ return lastVisibleItem ? lastVisibleItem.index : undefined;
180
+ }
181
+
182
+ /** @protected */
183
+ connectedCallback() {
184
+ super.connectedCallback();
185
+ this.isAttached = true;
186
+ this.recalculateColumnWidths();
187
+ }
188
+
189
+ /** @protected */
190
+ disconnectedCallback() {
191
+ super.disconnectedCallback();
192
+ this.isAttached = false;
193
+ this._hideTooltip(true);
194
+ }
195
+
196
+ /** @private */
197
+ __getFirstVisibleItem() {
198
+ return this._getRenderedRows().find((row) => this._isInViewport(row));
199
+ }
200
+
201
+ /** @private */
202
+ __getLastVisibleItem() {
203
+ return this._getRenderedRows()
204
+ .reverse()
205
+ .find((row) => this._isInViewport(row));
206
+ }
207
+
208
+ /** @private */
209
+ _isInViewport(item) {
210
+ const scrollTargetRect = this.$.table.getBoundingClientRect();
211
+ const itemRect = item.getBoundingClientRect();
212
+ const headerHeight = this.$.header.getBoundingClientRect().height;
213
+ const footerHeight = this.$.footer.getBoundingClientRect().height;
214
+ return (
215
+ itemRect.bottom > scrollTargetRect.top + headerHeight && itemRect.top < scrollTargetRect.bottom - footerHeight
216
+ );
217
+ }
218
+
219
+ /** @private */
220
+ _getRenderedRows() {
221
+ return Array.from(this.$.items.children)
222
+ .filter((item) => !item.hidden)
223
+ .sort((a, b) => a.index - b.index);
224
+ }
225
+
226
+ /** @protected */
227
+ _getRowContainingNode(node) {
228
+ const content = getClosestElement('vaadin-grid-cell-content', node);
229
+ if (!content) {
230
+ return;
231
+ }
232
+
233
+ const cell = content.assignedSlot.parentElement;
234
+ return cell.parentElement;
235
+ }
236
+
237
+ /** @protected */
238
+ _isItemAssignedToRow(item, row) {
239
+ const model = this.__getRowModel(row);
240
+ return this.getItemId(item) === this.getItemId(model.item);
241
+ }
242
+
243
+ /** @protected */
244
+ ready() {
245
+ super.ready();
246
+
247
+ this.__virtualizer = new Virtualizer({
248
+ createElements: this._createScrollerRows.bind(this),
249
+ updateElement: this._updateScrollerItem.bind(this),
250
+ scrollContainer: this.$.items,
251
+ scrollTarget: this.$.table,
252
+ reorderElements: true,
253
+ });
254
+
255
+ new ResizeObserver(() =>
256
+ setTimeout(() => {
257
+ this.__updateFooterPositioning();
258
+ this.__updateColumnsBodyContentHidden();
259
+ this.__tryToRecalculateColumnWidthsIfPending();
260
+ }),
261
+ ).observe(this.$.table);
262
+
263
+ processTemplates(this);
264
+
265
+ this._tooltipController = new TooltipController(this);
266
+ this.addController(this._tooltipController);
267
+ this._tooltipController.setManual(true);
268
+ }
269
+
270
+ /** @private */
271
+ __getBodyCellCoordinates(cell) {
272
+ if (this.$.items.contains(cell) && cell.localName === 'td') {
273
+ return {
274
+ item: cell.parentElement._item,
275
+ column: cell._column,
276
+ };
277
+ }
278
+ }
279
+
280
+ /** @private */
281
+ __focusBodyCell({ item, column }) {
282
+ const row = this._getRenderedRows().find((row) => row._item === item);
283
+ const cell = row && [...row.children].find((cell) => cell._column === column);
284
+ if (cell) {
285
+ cell.focus();
286
+ }
287
+ }
288
+
289
+ /** @protected */
290
+ _focusFirstVisibleRow() {
291
+ const row = this.__getFirstVisibleItem();
292
+ this.__rowFocusMode = true;
293
+ row.focus();
294
+ }
295
+
296
+ /** @private */
297
+ _flatSizeChanged(flatSize, virtualizer, hasData, columnTree) {
298
+ if (virtualizer && hasData && columnTree) {
299
+ // Changing the virtualizer size may result in the row with focus getting hidden
300
+ const cell = this.shadowRoot.activeElement;
301
+ const cellCoordinates = this.__getBodyCellCoordinates(cell);
302
+
303
+ const previousSize = virtualizer.size || 0;
304
+ virtualizer.size = flatSize;
305
+
306
+ // Request an update for the previous last row to have the "last" state removed
307
+ virtualizer.update(previousSize - 1, previousSize - 1);
308
+ if (flatSize < previousSize) {
309
+ // Size was decreased, so the new last row requires an explicit update
310
+ virtualizer.update(flatSize - 1, flatSize - 1);
311
+ }
312
+
313
+ // If the focused cell's parent row got hidden by the size change, focus the corresponding new cell
314
+ if (cellCoordinates && cell.parentElement.hidden) {
315
+ this.__focusBodyCell(cellCoordinates);
316
+ }
317
+
318
+ // Make sure the body has a tabbable element
319
+ this._resetKeyboardNavigation();
320
+ }
321
+ }
322
+
323
+ /** @private */
324
+ __hasRowsWithClientHeight() {
325
+ return !!Array.from(this.$.items.children).filter((row) => row.clientHeight).length;
326
+ }
327
+
328
+ /** @private */
329
+ __getIntrinsicWidth(col) {
330
+ if (!this.__intrinsicWidthCache.has(col)) {
331
+ this.__calculateAndCacheIntrinsicWidths([col]);
332
+ }
333
+ return this.__intrinsicWidthCache.get(col);
334
+ }
335
+
336
+ /** @private */
337
+ __getDistributedWidth(col, innerColumn) {
338
+ if (col == null || col === this) {
339
+ return 0;
340
+ }
341
+
342
+ const columnWidth = Math.max(this.__getIntrinsicWidth(col), this.__getDistributedWidth(col.parentElement, col));
343
+
344
+ // We're processing a regular grid-column and not a grid-column-group
345
+ if (!innerColumn) {
346
+ return columnWidth;
347
+ }
348
+
349
+ // At the end, the width of each vaadin-grid-column-group is determined by the sum of the width of its children.
350
+ // Here we determine how much space the vaadin-grid-column-group actually needs to render properly and then we distribute that space
351
+ // to its children, so when we actually do the summation it will be rendered properly.
352
+ // Check out vaadin-grid-column-group:_updateFlexAndWidth
353
+ const columnGroup = col;
354
+ const columnGroupWidth = columnWidth;
355
+ const sumOfWidthOfAllChildColumns = columnGroup._visibleChildColumns
356
+ .map((col) => this.__getIntrinsicWidth(col))
357
+ .reduce((sum, curr) => sum + curr, 0);
358
+
359
+ const extraNecessarySpaceForGridColumnGroup = Math.max(0, columnGroupWidth - sumOfWidthOfAllChildColumns);
360
+
361
+ // The distribution of the extra necessary space is done according to the intrinsic width of each child column.
362
+ // Lets say we need 100 pixels of extra space for the grid-column-group to render properly
363
+ // it has two grid-column children, |100px|300px| in total 400px
364
+ // the first column gets 25px of the additional space (100/400)*100 = 25
365
+ // the second column gets the 75px of the additional space (300/400)*100 = 75
366
+ const proportionOfExtraSpace = this.__getIntrinsicWidth(innerColumn) / sumOfWidthOfAllChildColumns;
367
+ const shareOfInnerColumnFromNecessaryExtraSpace = proportionOfExtraSpace * extraNecessarySpaceForGridColumnGroup;
368
+
369
+ return this.__getIntrinsicWidth(innerColumn) + shareOfInnerColumnFromNecessaryExtraSpace;
370
+ }
371
+
372
+ /**
373
+ * @param {!Array<!GridColumn>} cols the columns to auto size based on their content width
374
+ * @private
375
+ */
376
+ _recalculateColumnWidths(cols) {
377
+ // Flush to make sure DOM is up-to-date when measuring the column widths
378
+ this.__virtualizer.flush();
379
+ [...this.$.header.children, ...this.$.footer.children].forEach((row) => {
380
+ if (row.__debounceUpdateHeaderFooterRowVisibility) {
381
+ row.__debounceUpdateHeaderFooterRowVisibility.flush();
382
+ }
383
+ });
384
+
385
+ // Flush to account for any changes to the visibility of the columns
386
+ if (this._debouncerHiddenChanged) {
387
+ this._debouncerHiddenChanged.flush();
388
+ }
389
+
390
+ this.__intrinsicWidthCache = new Map();
391
+ // Cache the viewport rows to avoid unnecessary reflows while measuring the column widths
392
+ const fvi = this._firstVisibleIndex;
393
+ const lvi = this._lastVisibleIndex;
394
+ this.__viewportRowsCache = this._getRenderedRows().filter((row) => row.index >= fvi && row.index <= lvi);
395
+
396
+ // Pre-cache the intrinsic width of each column
397
+ this.__calculateAndCacheIntrinsicWidths(cols);
398
+
399
+ cols.forEach((col) => {
400
+ col.width = `${this.__getDistributedWidth(col)}px`;
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Toggles the cell content for the given column to use or not use auto width.
406
+ *
407
+ * While content for all the column cells uses auto width (instead of the default 100%),
408
+ * their offsetWidth can be used to calculate the collective intrinsic width of the column.
409
+ *
410
+ * @private
411
+ */
412
+ __setVisibleCellContentAutoWidth(col, autoWidth) {
413
+ col._allCells
414
+ .filter((cell) => {
415
+ if (this.$.items.contains(cell)) {
416
+ return this.__viewportRowsCache.includes(cell.parentElement);
417
+ }
418
+ return true;
419
+ })
420
+ .forEach((cell) => {
421
+ cell.__measuringAutoWidth = autoWidth;
422
+ cell._content.style.width = autoWidth ? 'auto' : '';
423
+ cell._content.style.position = autoWidth ? 'absolute' : '';
424
+ });
425
+ }
426
+
427
+ /**
428
+ * Returns the maximum intrinsic width of the cell content in the given column.
429
+ * Only cells which are marked for measuring auto width are considered.
430
+ *
431
+ * @private
432
+ */
433
+ __getAutoWidthCellsMaxWidth(col) {
434
+ // Note: _allCells only contains cells which are currently rendered in DOM
435
+ return col._allCells.reduce((width, cell) => {
436
+ // Add 1px buffer to the offset width to avoid too narrow columns (sub-pixel rendering)
437
+ return cell.__measuringAutoWidth ? Math.max(width, cell._content.offsetWidth + 1) : width;
438
+ }, 0);
439
+ }
440
+
441
+ /**
442
+ * Calculates and caches the intrinsic width of each given column.
443
+ *
444
+ * @private
445
+ */
446
+ __calculateAndCacheIntrinsicWidths(cols) {
447
+ // Make all the columns use auto width at once before measuring to
448
+ // avoid reflows in between the measurements
449
+ cols.forEach((col) => this.__setVisibleCellContentAutoWidth(col, true));
450
+ // Measure and cache
451
+ cols.forEach((col) => {
452
+ const width = this.__getAutoWidthCellsMaxWidth(col);
453
+ this.__intrinsicWidthCache.set(col, width);
454
+ });
455
+ // Reset the columns to use 100% width
456
+ cols.forEach((col) => this.__setVisibleCellContentAutoWidth(col, false));
457
+ }
458
+
459
+ /**
460
+ * Updates the `width` of all columns which have `autoWidth` set to `true`.
461
+ */
462
+ recalculateColumnWidths() {
463
+ if (!this._columnTree) {
464
+ return; // No columns
465
+ }
466
+ if (isElementHidden(this) || this._dataProviderController.isLoading()) {
467
+ this.__pendingRecalculateColumnWidths = true;
468
+ return;
469
+ }
470
+ const cols = this._getColumns().filter((col) => !col.hidden && col.autoWidth);
471
+ this._recalculateColumnWidths(cols);
472
+ }
473
+
474
+ /** @private */
475
+ __tryToRecalculateColumnWidthsIfPending() {
476
+ if (
477
+ this.__pendingRecalculateColumnWidths &&
478
+ !isElementHidden(this) &&
479
+ !this._dataProviderController.isLoading() &&
480
+ this.__hasRowsWithClientHeight()
481
+ ) {
482
+ this.__pendingRecalculateColumnWidths = false;
483
+ this.recalculateColumnWidths();
484
+ }
485
+ }
486
+
487
+ /**
488
+ * @protected
489
+ * @override
490
+ */
491
+ _onDataProviderPageLoaded() {
492
+ super._onDataProviderPageLoaded();
493
+ this.__tryToRecalculateColumnWidthsIfPending();
494
+ }
495
+
496
+ /** @private */
497
+ _createScrollerRows(count) {
498
+ const rows = [];
499
+ for (let i = 0; i < count; i++) {
500
+ const row = document.createElement('tr');
501
+ row.setAttribute('part', 'row');
502
+ row.setAttribute('role', 'row');
503
+ row.setAttribute('tabindex', '-1');
504
+ if (this._columnTree) {
505
+ this._updateRow(row, this._columnTree[this._columnTree.length - 1], 'body', false, true);
506
+ }
507
+ rows.push(row);
508
+ }
509
+
510
+ if (this._columnTree) {
511
+ this._columnTree[this._columnTree.length - 1].forEach(
512
+ (c) => c.isConnected && c.notifyPath && c.notifyPath('_cells.*', c._cells),
513
+ );
514
+ }
515
+
516
+ this.__afterCreateScrollerRowsDebouncer = Debouncer.debounce(
517
+ this.__afterCreateScrollerRowsDebouncer,
518
+ animationFrame,
519
+ () => {
520
+ this._afterScroll();
521
+ this.__tryToRecalculateColumnWidthsIfPending();
522
+ },
523
+ );
524
+ return rows;
525
+ }
526
+
527
+ /** @private */
528
+ _createCell(tagName, column) {
529
+ const contentId = (this._contentIndex = this._contentIndex + 1 || 0);
530
+ const slotName = `vaadin-grid-cell-content-${contentId}`;
531
+
532
+ const cellContent = document.createElement('vaadin-grid-cell-content');
533
+ cellContent.setAttribute('slot', slotName);
534
+
535
+ const cell = document.createElement(tagName);
536
+ cell.id = slotName.replace('-content-', '-');
537
+ cell.setAttribute('role', tagName === 'td' ? 'gridcell' : 'columnheader');
538
+
539
+ // For now we only support tooltip on desktop
540
+ if (!isAndroid && !isIOS) {
541
+ cell.addEventListener('mouseenter', (event) => {
542
+ if (!this.$.scroller.hasAttribute('scrolling')) {
543
+ this._showTooltip(event);
544
+ }
545
+ });
546
+
547
+ cell.addEventListener('mouseleave', () => {
548
+ this._hideTooltip();
549
+ });
550
+
551
+ cell.addEventListener('mousedown', () => {
552
+ this._hideTooltip(true);
553
+ });
554
+ }
555
+
556
+ const slot = document.createElement('slot');
557
+ slot.setAttribute('name', slotName);
558
+
559
+ if (column && column._focusButtonMode) {
560
+ const div = document.createElement('div');
561
+ div.setAttribute('role', 'button');
562
+ div.setAttribute('tabindex', '-1');
563
+ cell.appendChild(div);
564
+
565
+ // Patch `focus()` to use the button
566
+ cell._focusButton = div;
567
+ cell.focus = function () {
568
+ cell._focusButton.focus();
569
+ };
570
+
571
+ div.appendChild(slot);
572
+ } else {
573
+ cell.setAttribute('tabindex', '-1');
574
+ cell.appendChild(slot);
575
+ }
576
+
577
+ cell._content = cellContent;
578
+
579
+ // With native Shadow DOM, mousedown on slotted element does not focus
580
+ // focusable slot wrapper, that is why cells are not focused with
581
+ // mousedown. Workaround: listen for mousedown and focus manually.
582
+ cellContent.addEventListener('mousedown', () => {
583
+ if (isChrome) {
584
+ // Chrome bug: focusing before mouseup prevents text selection, see http://crbug.com/771903
585
+ const mouseUpListener = (event) => {
586
+ // If focus is on element within the cell content - respect it, do not change
587
+ const contentContainsFocusedElement = cellContent.contains(this.getRootNode().activeElement);
588
+ // Only focus if mouse is released on cell content itself
589
+ const mouseUpWithinCell = event.composedPath().includes(cellContent);
590
+ if (!contentContainsFocusedElement && mouseUpWithinCell) {
591
+ cell.focus();
592
+ }
593
+ document.removeEventListener('mouseup', mouseUpListener, true);
594
+ };
595
+ document.addEventListener('mouseup', mouseUpListener, true);
596
+ } else {
597
+ // Focus on mouseup, on the other hand, removes selection on Safari.
598
+ // Watch out sync focus removal issue, only async focus works here.
599
+ setTimeout(() => {
600
+ if (!cellContent.contains(this.getRootNode().activeElement)) {
601
+ cell.focus();
602
+ }
603
+ });
604
+ }
605
+ });
606
+
607
+ return cell;
608
+ }
609
+
610
+ /**
611
+ * @param {!HTMLTableRowElement} row
612
+ * @param {!Array<!GridColumn>} columns
613
+ * @param {?string} section
614
+ * @param {boolean} isColumnRow
615
+ * @param {boolean} noNotify
616
+ * @protected
617
+ */
618
+ _updateRow(row, columns, section = 'body', isColumnRow = false, noNotify = false) {
619
+ const contentsFragment = document.createDocumentFragment();
620
+
621
+ iterateRowCells(row, (cell) => {
622
+ cell._vacant = true;
623
+ });
624
+ row.innerHTML = '';
625
+ if (section === 'body') {
626
+ // Clear the cached cell references
627
+ row.__cells = [];
628
+ row.__detailsCell = null;
629
+ }
630
+
631
+ columns
632
+ .filter((column) => !column.hidden)
633
+ .forEach((column, index, cols) => {
634
+ let cell;
635
+
636
+ if (section === 'body') {
637
+ // Body
638
+ if (!column._cells) {
639
+ column._cells = [];
640
+ }
641
+ cell = column._cells.find((cell) => cell._vacant);
642
+ if (!cell) {
643
+ cell = this._createCell('td', column);
644
+ column._cells.push(cell);
645
+ }
646
+ cell.setAttribute('part', 'cell body-cell');
647
+ cell.__parentRow = row;
648
+ // Cache the cell reference
649
+ row.__cells.push(cell);
650
+ if (!column._bodyContentHidden) {
651
+ row.appendChild(cell);
652
+ }
653
+
654
+ if (row === this.$.sizer) {
655
+ column._sizerCell = cell;
656
+ }
657
+
658
+ if (index === cols.length - 1 && this.rowDetailsRenderer) {
659
+ // Add details cell as last cell to body rows
660
+ if (!this._detailsCells) {
661
+ this._detailsCells = [];
662
+ }
663
+ const detailsCell = this._detailsCells.find((cell) => cell._vacant) || this._createCell('td');
664
+ if (this._detailsCells.indexOf(detailsCell) === -1) {
665
+ this._detailsCells.push(detailsCell);
666
+ }
667
+ if (!detailsCell._content.parentElement) {
668
+ contentsFragment.appendChild(detailsCell._content);
669
+ }
670
+ this._configureDetailsCell(detailsCell);
671
+ row.appendChild(detailsCell);
672
+ // Cache the details cell reference
673
+ row.__detailsCell = detailsCell;
674
+ this._a11ySetRowDetailsCell(row, detailsCell);
675
+ detailsCell._vacant = false;
676
+ }
677
+
678
+ if (column.notifyPath && !noNotify) {
679
+ column.notifyPath('_cells.*', column._cells);
680
+ }
681
+ } else {
682
+ // Header & footer
683
+ const tagName = section === 'header' ? 'th' : 'td';
684
+ if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
685
+ cell = column[`_${section}Cell`] || this._createCell(tagName);
686
+ cell._column = column;
687
+ row.appendChild(cell);
688
+ column[`_${section}Cell`] = cell;
689
+ } else {
690
+ if (!column._emptyCells) {
691
+ column._emptyCells = [];
692
+ }
693
+ cell = column._emptyCells.find((cell) => cell._vacant) || this._createCell(tagName);
694
+ cell._column = column;
695
+ row.appendChild(cell);
696
+ if (column._emptyCells.indexOf(cell) === -1) {
697
+ column._emptyCells.push(cell);
698
+ }
699
+ }
700
+ cell.setAttribute('part', `cell ${section}-cell`);
701
+ }
702
+
703
+ if (!cell._content.parentElement) {
704
+ contentsFragment.appendChild(cell._content);
705
+ }
706
+ cell._vacant = false;
707
+ cell._column = column;
708
+ });
709
+
710
+ if (section !== 'body') {
711
+ this.__debounceUpdateHeaderFooterRowVisibility(row);
712
+ }
713
+
714
+ // Might be empty if only cache was used
715
+ this.appendChild(contentsFragment);
716
+
717
+ this._frozenCellsChanged();
718
+ this._updateFirstAndLastColumnForRow(row);
719
+ }
720
+
721
+ /**
722
+ * @param {HTMLTableRowElement} row
723
+ * @protected
724
+ */
725
+ __debounceUpdateHeaderFooterRowVisibility(row) {
726
+ row.__debounceUpdateHeaderFooterRowVisibility = Debouncer.debounce(
727
+ row.__debounceUpdateHeaderFooterRowVisibility,
728
+ microTask,
729
+ () => this.__updateHeaderFooterRowVisibility(row),
730
+ );
731
+ }
732
+
733
+ /**
734
+ * @param {HTMLTableRowElement} row
735
+ * @protected
736
+ */
737
+ __updateHeaderFooterRowVisibility(row) {
738
+ if (!row) {
739
+ return;
740
+ }
741
+
742
+ const visibleRowCells = Array.from(row.children).filter((cell) => {
743
+ const column = cell._column;
744
+ if (column._emptyCells && column._emptyCells.indexOf(cell) > -1) {
745
+ // The cell is an "empty cell" -> doesn't block hiding the row
746
+ return false;
747
+ }
748
+ if (row.parentElement === this.$.header) {
749
+ if (column.headerRenderer) {
750
+ // The cell is the header cell of a column that has a header renderer
751
+ // -> row should be visible
752
+ return true;
753
+ }
754
+ if (column.header === null) {
755
+ // The column header is explicilty set to null -> doesn't block hiding the row
756
+ return false;
757
+ }
758
+ if (column.path || column.header !== undefined) {
759
+ // The column has an explicit non-null header or a path that generates a header
760
+ // -> row should be visible
761
+ return true;
762
+ }
763
+ } else if (column.footerRenderer) {
764
+ // The cell is the footer cell of a column that has a footer renderer
765
+ // -> row should be visible
766
+ return true;
767
+ }
768
+ return false;
769
+ });
770
+
771
+ if (row.hidden !== !visibleRowCells.length) {
772
+ row.hidden = !visibleRowCells.length;
773
+ }
774
+
775
+ // Make sure the section has a tabbable element
776
+ this._resetKeyboardNavigation();
777
+ }
778
+
779
+ /** @private */
780
+ _updateScrollerItem(row, index) {
781
+ this._preventScrollerRotatingCellFocus(row, index);
782
+
783
+ if (!this._columnTree) {
784
+ return;
785
+ }
786
+
787
+ this._updateRowOrderParts(row, index);
788
+
789
+ this._a11yUpdateRowRowindex(row, index);
790
+ this._getItem(index, row);
791
+ }
792
+
793
+ /** @private */
794
+ _columnTreeChanged(columnTree) {
795
+ this._renderColumnTree(columnTree);
796
+ this.recalculateColumnWidths();
797
+ this.__updateColumnsBodyContentHidden();
798
+ }
799
+
800
+ /** @private */
801
+ _updateRowOrderParts(row, index = row.index) {
802
+ updateBooleanRowStates(row, {
803
+ first: index === 0,
804
+ last: index === this._flatSize - 1,
805
+ odd: index % 2 !== 0,
806
+ even: index % 2 === 0,
807
+ });
808
+ }
809
+
810
+ /** @private */
811
+ _updateRowStateParts(row, { expanded, selected, detailsOpened }) {
812
+ updateBooleanRowStates(row, {
813
+ expanded,
814
+ selected,
815
+ 'details-opened': detailsOpened,
816
+ });
817
+ }
818
+
819
+ /**
820
+ * @param {!Array<!GridColumn>} columnTree
821
+ * @protected
822
+ */
823
+ _renderColumnTree(columnTree) {
824
+ iterateChildren(this.$.items, (row) => {
825
+ this._updateRow(row, columnTree[columnTree.length - 1], 'body', false, true);
826
+
827
+ const model = this.__getRowModel(row);
828
+ this._updateRowOrderParts(row);
829
+ this._updateRowStateParts(row, model);
830
+ this._filterDragAndDrop(row, model);
831
+ });
832
+
833
+ while (this.$.header.children.length < columnTree.length) {
834
+ const headerRow = document.createElement('tr');
835
+ headerRow.setAttribute('part', 'row');
836
+ headerRow.setAttribute('role', 'row');
837
+ headerRow.setAttribute('tabindex', '-1');
838
+ this.$.header.appendChild(headerRow);
839
+
840
+ const footerRow = document.createElement('tr');
841
+ footerRow.setAttribute('part', 'row');
842
+ footerRow.setAttribute('role', 'row');
843
+ footerRow.setAttribute('tabindex', '-1');
844
+ this.$.footer.appendChild(footerRow);
845
+ }
846
+ while (this.$.header.children.length > columnTree.length) {
847
+ this.$.header.removeChild(this.$.header.firstElementChild);
848
+ this.$.footer.removeChild(this.$.footer.firstElementChild);
849
+ }
850
+
851
+ iterateChildren(this.$.header, (headerRow, index, rows) => {
852
+ this._updateRow(headerRow, columnTree[index], 'header', index === columnTree.length - 1);
853
+
854
+ const cells = getBodyRowCells(headerRow);
855
+ updateCellsPart(cells, 'first-header-row-cell', index === 0);
856
+ updateCellsPart(cells, 'last-header-row-cell', index === rows.length - 1);
857
+ });
858
+
859
+ iterateChildren(this.$.footer, (footerRow, index, rows) => {
860
+ this._updateRow(footerRow, columnTree[columnTree.length - 1 - index], 'footer', index === 0);
861
+
862
+ const cells = getBodyRowCells(footerRow);
863
+ updateCellsPart(cells, 'first-footer-row-cell', index === 0);
864
+ updateCellsPart(cells, 'last-footer-row-cell', index === rows.length - 1);
865
+ });
866
+
867
+ // Sizer rows
868
+ this._updateRow(this.$.sizer, columnTree[columnTree.length - 1]);
869
+
870
+ this._resizeHandler();
871
+ this._frozenCellsChanged();
872
+ this._updateFirstAndLastColumn();
873
+ this._resetKeyboardNavigation();
874
+ this._a11yUpdateHeaderRows();
875
+ this._a11yUpdateFooterRows();
876
+ this.__updateFooterPositioning();
877
+ this.generateCellClassNames();
878
+ this.generateCellPartNames();
879
+ }
880
+
881
+ /** @private */
882
+ __updateFooterPositioning() {
883
+ // TODO: fixed in Firefox 99, remove when we can drop Firefox ESR 91 support
884
+ if (this._firefox && parseFloat(navigator.userAgent.match(/Firefox\/(\d{2,3}.\d)/u)[1]) < 99) {
885
+ // Sticky (or translated) footer in a flexbox host doesn't get included in
886
+ // the scroll height calculation on FF. This is a workaround for the issue.
887
+ this.$.items.style.paddingBottom = 0;
888
+ if (!this.allRowsVisible) {
889
+ this.$.items.style.paddingBottom = `${this.$.footer.offsetHeight}px`;
890
+ }
891
+ }
892
+ }
893
+
894
+ /**
895
+ * @param {!HTMLElement} row
896
+ * @param {GridItem} item
897
+ * @protected
898
+ */
899
+ _updateItem(row, item) {
900
+ row._item = item;
901
+ const model = this.__getRowModel(row);
902
+
903
+ this._toggleDetailsCell(row, model.detailsOpened);
904
+
905
+ this._a11yUpdateRowLevel(row, model.level);
906
+ this._a11yUpdateRowSelected(row, model.selected);
907
+
908
+ this._updateRowStateParts(row, model);
909
+
910
+ this._generateCellClassNames(row, model);
911
+ this._generateCellPartNames(row, model);
912
+ this._filterDragAndDrop(row, model);
913
+
914
+ iterateChildren(row, (cell) => {
915
+ if (cell._renderer) {
916
+ const owner = cell._column || this;
917
+ cell._renderer.call(owner, cell._content, owner, model);
918
+ }
919
+ });
920
+
921
+ this._updateDetailsCellHeight(row);
922
+
923
+ this._a11yUpdateRowExpanded(row, model.expanded);
924
+ }
925
+
926
+ /** @private */
927
+ _resizeHandler() {
928
+ this._updateDetailsCellHeights();
929
+ this.__updateFooterPositioning();
930
+ this.__updateHorizontalScrollPosition();
931
+ }
932
+
933
+ /** @private */
934
+ _onAnimationEnd(e) {
935
+ // ShadyCSS applies scoping suffixes to animation names
936
+ if (e.animationName.indexOf('vaadin-grid-appear') === 0) {
937
+ e.stopPropagation();
938
+ this.__tryToRecalculateColumnWidthsIfPending();
939
+
940
+ requestAnimationFrame(() => {
941
+ this.__scrollToPendingIndexes();
942
+ });
943
+ }
944
+ }
945
+
946
+ /**
947
+ * @param {!HTMLTableRowElement} row
948
+ * @return {!GridItemModel}
949
+ * @protected
950
+ */
951
+ __getRowModel(row) {
952
+ return {
953
+ index: row.index,
954
+ item: row._item,
955
+ level: this._getIndexLevel(row.index),
956
+ expanded: this._isExpanded(row._item),
957
+ selected: this._isSelected(row._item),
958
+ detailsOpened: !!this.rowDetailsRenderer && this._isDetailsOpened(row._item),
959
+ };
960
+ }
961
+
962
+ /**
963
+ * @param {Event} event
964
+ * @protected
965
+ */
966
+ _showTooltip(event) {
967
+ // Check if there is a slotted vaadin-tooltip element.
968
+ const tooltip = this._tooltipController.node;
969
+ if (tooltip && tooltip.isConnected) {
970
+ this._tooltipController.setTarget(event.target);
971
+ this._tooltipController.setContext(this.getEventContext(event));
972
+
973
+ // Trigger opening using the corresponding delay.
974
+ tooltip._stateController.open({
975
+ focus: event.type === 'focusin',
976
+ hover: event.type === 'mouseenter',
977
+ });
978
+ }
979
+ }
980
+
981
+ /** @protected */
982
+ _hideTooltip(immediate) {
983
+ const tooltip = this._tooltipController.node;
984
+ if (tooltip) {
985
+ tooltip._stateController.close(immediate);
986
+ }
987
+ }
988
+
989
+ /**
990
+ * Requests an update for the content of cells.
991
+ *
992
+ * While performing the update, the following renderers are invoked:
993
+ * - `Grid.rowDetailsRenderer`
994
+ * - `GridColumn.renderer`
995
+ * - `GridColumn.headerRenderer`
996
+ * - `GridColumn.footerRenderer`
997
+ *
998
+ * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
999
+ */
1000
+ requestContentUpdate() {
1001
+ if (this._columnTree) {
1002
+ // Header and footer renderers
1003
+ this._columnTree.forEach((level) => {
1004
+ level.forEach((column) => {
1005
+ if (column._renderHeaderAndFooter) {
1006
+ column._renderHeaderAndFooter();
1007
+ }
1008
+ });
1009
+ });
1010
+
1011
+ // Body and row details renderers
1012
+ this.__updateVisibleRows();
1013
+ }
1014
+ }
1015
+
1016
+ /** @protected */
1017
+ __updateVisibleRows(start, end) {
1018
+ if (this.__virtualizer) {
1019
+ this.__virtualizer.update(start, end);
1020
+ }
1021
+ }
1022
+ };