// @ts-nocheck import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; import { timeOut, animationFrame } from '@polymer/polymer/lib/utils/async.js'; import { Grid } from '@vaadin/grid/src/vaadin-grid.js'; import { isFocusable } from '@vaadin/grid/src/vaadin-grid-active-item-mixin.js'; import { GridFlowSelectionColumn } from "./vaadin-grid-flow-selection-column.js"; (function () { const tryCatchWrapper = function (callback) { return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Grid'); }; window.Vaadin.Flow.gridConnector = { initLazy: (grid) => tryCatchWrapper(function (grid) { // Check whether the connector was already initialized for the grid if (grid.$connector) { return; } const dataProviderController = grid._dataProviderController; dataProviderController.ensureFlatIndexHierarchyOriginal = dataProviderController.ensureFlatIndexHierarchy; dataProviderController.ensureFlatIndexHierarchy = tryCatchWrapper(function (flatIndex) { const { item } = this.getFlatIndexContext(flatIndex); if (!item || !this.isExpanded(item)) { return; } const isCached = grid.$connector.hasCacheForParentKey(grid.getItemId(item)); if (isCached) { // The sub-cache items are already in the connector's cache. Skip the debouncing process. this.ensureFlatIndexHierarchyOriginal(flatIndex); } else { grid.$connector.beforeEnsureFlatIndexHierarchy(flatIndex, item); } }); dataProviderController.isLoadingOriginal = dataProviderController.isLoading; dataProviderController.isLoading = tryCatchWrapper(function () { return grid.$connector.hasEnsureSubCacheQueue() || this.isLoadingOriginal(); }); const rootPageCallbacks = {}; const treePageCallbacks = {}; const cache = {}; /* parentRequestDelay - optimizes parent requests by batching several requests * into one request. Delay in milliseconds. Disable by setting to 0. * parentRequestBatchMaxSize - maximum size of the batch. */ const parentRequestDelay = 50; const parentRequestBatchMaxSize = 20; let parentRequestQueue = []; let parentRequestDebouncer; let ensureSubCacheQueue = []; let ensureSubCacheDebouncer; const rootRequestDelay = 150; let rootRequestDebouncer; let lastRequestedRanges = {}; const root = 'null'; lastRequestedRanges[root] = [0, 0]; let currentUpdateClearRange = null; let currentUpdateSetRange = null; const validSelectionModes = ['SINGLE', 'NONE', 'MULTI']; let selectedKeys = {}; let selectionMode = 'SINGLE'; let sorterDirectionsSetFromServer = false; grid.size = 0; // To avoid NaN here and there before we get proper data grid.itemIdPath = 'key'; function createEmptyItemFromKey(key) { return { [grid.itemIdPath]: key }; } grid.$connector = {}; grid.$connector.hasCacheForParentKey = tryCatchWrapper((parentKey) => cache[parentKey]?.size !== undefined); grid.$connector.hasEnsureSubCacheQueue = tryCatchWrapper(() => ensureSubCacheQueue.length > 0); grid.$connector.hasParentRequestQueue = tryCatchWrapper(() => parentRequestQueue.length > 0); grid.$connector.hasRootRequestQueue = tryCatchWrapper(() => { return Object.keys(rootPageCallbacks).length > 0 || !!rootRequestDebouncer?.isActive(); }); grid.$connector.beforeEnsureFlatIndexHierarchy = tryCatchWrapper(function (flatIndex, item) { // add call to queue ensureSubCacheQueue.push({ flatIndex, itemkey: grid.getItemId(item) }); ensureSubCacheDebouncer = Debouncer.debounce(ensureSubCacheDebouncer, animationFrame, () => { while (ensureSubCacheQueue.length) { grid.$connector.flushEnsureSubCache(); } }); }); grid.$connector.doSelection = tryCatchWrapper(function (items, userOriginated) { if (selectionMode === 'NONE' || !items.length || (userOriginated && grid.hasAttribute('disabled'))) { return; } if (selectionMode === 'SINGLE') { selectedKeys = {}; } items.forEach((item) => { if (item) { selectedKeys[item.key] = item; item.selected = true; if (userOriginated) { grid.$server.select(item.key); } } // FYI: In single selection mode, the server can send items = [null] // which means a "Deselect All" command. const isSelectedItemDifferentOrNull = !grid.activeItem || !item || item.key != grid.activeItem.key; if (!userOriginated && selectionMode === 'SINGLE' && isSelectedItemDifferentOrNull) { grid.activeItem = item; } }); grid.selectedItems = Object.values(selectedKeys); }); grid.$connector.doDeselection = tryCatchWrapper(function (items, userOriginated) { if (selectionMode === 'NONE' || !items.length || (userOriginated && grid.hasAttribute('disabled'))) { return; } const updatedSelectedItems = grid.selectedItems.slice(); while (items.length) { const itemToDeselect = items.shift(); for (let i = 0; i < updatedSelectedItems.length; i++) { const selectedItem = updatedSelectedItems[i]; if (itemToDeselect?.key === selectedItem.key) { updatedSelectedItems.splice(i, 1); break; } } if (itemToDeselect) { delete selectedKeys[itemToDeselect.key]; delete itemToDeselect.selected; if (userOriginated) { grid.$server.deselect(itemToDeselect.key); } } } grid.selectedItems = updatedSelectedItems; }); grid.__activeItemChanged = tryCatchWrapper(function (newVal, oldVal) { if (selectionMode != 'SINGLE') { return; } if (!newVal) { if (oldVal && selectedKeys[oldVal.key]) { if (grid.__deselectDisallowed) { grid.activeItem = oldVal; } else { grid.$connector.doDeselection([oldVal], true); } } } else if (!selectedKeys[newVal.key]) { grid.$connector.doSelection([newVal], true); } }); grid._createPropertyObserver('activeItem', '__activeItemChanged', true); grid.__activeItemChangedDetails = tryCatchWrapper(function (newVal, oldVal) { if (grid.__disallowDetailsOnClick) { return; } // when grid is attached, newVal is not set and oldVal is undefined // do nothing if (newVal == null && oldVal === undefined) { return; } if (newVal && !newVal.detailsOpened) { grid.$server.setDetailsVisible(newVal.key); } else { grid.$server.setDetailsVisible(null); } }); grid._createPropertyObserver('activeItem', '__activeItemChangedDetails', true); grid.$connector._getSameLevelPage = tryCatchWrapper(function (parentKey, currentCache, currentCacheItemIndex) { const currentParentKey = currentCache.parentItem ? grid.getItemId(currentCache.parentItem) : root; if (currentParentKey === parentKey) { // Level match found, return the page number. return Math.floor(currentCacheItemIndex / grid.pageSize); } const { parentCache, parentCacheIndex } = currentCache; if (!parentCache) { // There is no parent cache to match level return null; } // Traverse the tree upwards until a match is found or the end is reached return this._getSameLevelPage(parentKey, parentCache, parentCacheIndex); }); grid.$connector.flushEnsureSubCache = tryCatchWrapper(function () { const pendingFetch = ensureSubCacheQueue.shift(); if (pendingFetch) { dataProviderController.ensureFlatIndexHierarchyOriginal(pendingFetch.flatIndex); return true; } return false; }); grid.$connector.debounceRootRequest = tryCatchWrapper(function (page) { const delay = grid._hasData ? rootRequestDelay : 0; rootRequestDebouncer = Debouncer.debounce(rootRequestDebouncer, timeOut.after(delay), () => { grid.$connector.fetchPage( (firstIndex, size) => grid.$server.setRequestedRange(firstIndex, size), page, root ); }); }); grid.$connector.flushParentRequests = tryCatchWrapper(function () { const pendingFetches = []; parentRequestQueue.splice(0, parentRequestBatchMaxSize).forEach(({ parentKey, page }) => { grid.$connector.fetchPage( (firstIndex, size) => pendingFetches.push({ parentKey, firstIndex, size }), page, parentKey ); }); if (pendingFetches.length) { grid.$server.setParentRequestedRanges(pendingFetches); } }); grid.$connector.debounceParentRequest = tryCatchWrapper(function (parentKey, page) { // Remove any pending requests for the same parentKey. parentRequestQueue = parentRequestQueue.filter((request) => request.parentKey !== parentKey); // Add the new request to the queue. parentRequestQueue.push({ parentKey, page }); // Debounce the request to avoid sending multiple requests for the same parentKey. parentRequestDebouncer = Debouncer.debounce(parentRequestDebouncer, timeOut.after(parentRequestDelay), () => { while (parentRequestQueue.length) { grid.$connector.flushParentRequests(); } }); }); grid.$connector.fetchPage = tryCatchWrapper(function (fetch, page, parentKey) { // Adjust the requested page to be within the valid range in case // the grid size has changed while fetchPage was debounced. if (parentKey === root) { page = Math.min(page, Math.floor((grid.size - 1) / grid.pageSize)); } // Determine what to fetch based on scroll position and not only // what grid asked for const visibleRows = grid._getRenderedRows(); let start = visibleRows.length > 0 ? visibleRows[0].index : 0; let end = visibleRows.length > 0 ? visibleRows[visibleRows.length - 1].index : 0; // The buffer size could be multiplied by some constant defined by the user, // if he needs to reduce the number of items sent to the Grid to improve performance // or to increase it to make Grid smoother when scrolling let buffer = end - start; let firstNeededIndex = Math.max(0, start - buffer); let lastNeededIndex = Math.min(end + buffer, grid._flatSize); let pageRange = [null, null]; for (let idx = firstNeededIndex; idx <= lastNeededIndex; idx++) { const { cache, index } = dataProviderController.getFlatIndexContext(idx); // Try to match level by going up in hierarchy. The page range should include // pages that contain either of the following: // - visible items of the current cache // - same level parents of visible descendant items // If the parent items are not considered, Flow would remove the hidden parent // items from the current level cache. This can lead to an infinite loop when using // scrollToIndex feature. const sameLevelPage = grid.$connector._getSameLevelPage(parentKey, cache, index); if (sameLevelPage === null) { continue; } pageRange[0] = Math.min(pageRange[0] ?? sameLevelPage, sameLevelPage); pageRange[1] = Math.max(pageRange[1] ?? sameLevelPage, sameLevelPage); } // When the viewport doesn't contain the requested page or it doesn't contain any items from // the requested level at all, it means that the scroll position has changed while fetchPage // was debounced. For example, it can happen if the user scrolls the grid to the bottom and // then immediately back to the top. In this case, the request for the last page will be left // hanging. To avoid this, as a workaround, we reset the range to only include the requested page // to make sure all hanging requests are resolved. After that, the grid requests the first page // or whatever in the viewport again. if (pageRange.some((p) => p === null) || page < pageRange[0] || page > pageRange[1]) { pageRange = [page, page]; } let lastRequestedRange = lastRequestedRanges[parentKey] || [-1, -1]; if (lastRequestedRange[0] != pageRange[0] || lastRequestedRange[1] != pageRange[1]) { lastRequestedRanges[parentKey] = pageRange; let pageCount = pageRange[1] - pageRange[0] + 1; fetch(pageRange[0] * grid.pageSize, pageCount * grid.pageSize); } }); grid.dataProvider = tryCatchWrapper(function (params, callback) { if (params.pageSize != grid.pageSize) { throw 'Invalid pageSize'; } let page = params.page; if (params.parentItem) { let parentUniqueKey = grid.getItemId(params.parentItem); if (!treePageCallbacks[parentUniqueKey]) { treePageCallbacks[parentUniqueKey] = {}; } const parentItemContext = dataProviderController.getItemContext(params.parentItem); if (cache[parentUniqueKey]?.[page] && parentItemContext.subCache) { // workaround: sometimes grid-element gives page index that overflows page = Math.min(page, Math.floor(cache[parentUniqueKey].size / grid.pageSize)); // Ensure grid isn't in loading state when the callback executes ensureSubCacheQueue = []; // Resolve the callback from cache callback(cache[parentUniqueKey][page], cache[parentUniqueKey].size); } else { treePageCallbacks[parentUniqueKey][page] = callback; grid.$connector.debounceParentRequest(parentUniqueKey, page); } } else { // workaround: sometimes grid-element gives page index that overflows page = Math.min(page, Math.floor(grid.size / grid.pageSize)); // size is controlled by the server (data communicator), so if the // size is zero, we know that there is no data to fetch. // This also prevents an empty grid getting stuck in a loading state. // The connector does not cache empty pages, so if the grid requests // data again, there would be no cache entry, causing a request to // the server. However, the data communicator will never respond, // as it assumes that the data is already cached. if (grid.size === 0) { callback([], 0); return; } if (cache[root]?.[page]) { callback(cache[root][page]); } else { rootPageCallbacks[page] = callback; grid.$connector.debounceRootRequest(page); } } }); grid.$connector.setSorterDirections = tryCatchWrapper(function (directions) { sorterDirectionsSetFromServer = true; setTimeout( tryCatchWrapper(() => { try { const sorters = Array.from(grid.querySelectorAll('vaadin-grid-sorter')); // Sorters for hidden columns are removed from DOM but stored in the web component. // We need to ensure that all the sorters are reset when using `grid.sort(null)`. grid._sorters.forEach((sorter) => { if (!sorters.includes(sorter)) { sorters.push(sorter); } }); sorters.forEach((sorter) => { sorter.direction = null; }); // Apply directions in correct order, depending on configured multi-sort priority. // For the default "prepend" mode, directions need to be applied in reverse, in // order for the sort indicators to match the order on the server. For "append" // just keep the order passed from the server. if (grid.multiSortPriority !== 'append') { directions = directions.reverse(); } directions.forEach(({ column, direction }) => { sorters.forEach((sorter) => { if (sorter.getAttribute('path') === column) { sorter.direction = direction; } }); }); } finally { sorterDirectionsSetFromServer = false; } }) ); }); grid._updateItem = tryCatchWrapper(function (row, item) { Grid.prototype._updateItem.call(grid, row, item); // There might be inactive component renderers on hidden rows that still refer to the // same component instance as one of the renderers on a visible row. Making the // inactive/hidden renderer attach the component might steal it from a visible/active one. if (!row.hidden) { // make sure that component renderers are updated Array.from(row.children).forEach((cell) => { Array.from(cell?._content?.__templateInstance?.children || []).forEach((content) => { if (content._attachRenderedComponentIfAble) { content._attachRenderedComponentIfAble(); } // In hierarchy column of tree grid, the component renderer is inside its content, // this updates it renderer from innerContent Array.from(content?.children || []).forEach((innerContent) => { if (innerContent._attachRenderedComponentIfAble) { innerContent._attachRenderedComponentIfAble(); } }); }); }); } // since no row can be selected when selection mode is NONE // if selectionMode is set to NONE, remove aria-selected attribute from the row if (selectionMode === validSelectionModes[1]) { // selectionMode === NONE row.removeAttribute('aria-selected'); Array.from(row.children).forEach((cell) => cell.removeAttribute('aria-selected')); } }); const itemExpandedChanged = tryCatchWrapper(function (item, expanded) { // method available only for the TreeGrid server-side component if (item == undefined || grid.$server.updateExpandedState == undefined) { return; } let parentKey = grid.getItemId(item); grid.$server.updateExpandedState(parentKey, expanded); }); // Patch grid.expandItem and grid.collapseItem to have // itemExpandedChanged run when either happens. grid.expandItem = tryCatchWrapper(function (item) { itemExpandedChanged(item, true); Grid.prototype.expandItem.call(grid, item); }); grid.collapseItem = tryCatchWrapper(function (item) { itemExpandedChanged(item, false); Grid.prototype.collapseItem.call(grid, item); }); const itemsUpdated = function (items) { if (!items || !Array.isArray(items)) { throw 'Attempted to call itemsUpdated with an invalid value: ' + JSON.stringify(items); } let detailsOpenedItems = Array.from(grid.detailsOpenedItems); let updatedSelectedItem = false; for (let i = 0; i < items.length; ++i) { const item = items[i]; if (!item) { continue; } if (item.detailsOpened) { if (grid._getItemIndexInArray(item, detailsOpenedItems) < 0) { detailsOpenedItems.push(item); } } else if (grid._getItemIndexInArray(item, detailsOpenedItems) >= 0) { detailsOpenedItems.splice(grid._getItemIndexInArray(item, detailsOpenedItems), 1); } if (selectedKeys[item.key]) { selectedKeys[item.key] = item; item.selected = true; updatedSelectedItem = true; } } grid.detailsOpenedItems = detailsOpenedItems; if (updatedSelectedItem) { // Replace the objects in the grid.selectedItems array without replacing the array // itself in order to avoid an unnecessary re-render of the grid. grid.selectedItems.splice(0, grid.selectedItems.length, ...Object.values(selectedKeys)); } }; /** * Updates the cache for the given page for grid or tree-grid. * * @param page index of the page to update * @param parentKey the key of the parent item for the page * @returns an array of the updated items for the page, or undefined if no items were cached for the page */ const updateGridCache = function (page, parentKey) { let items; if ((parentKey || root) !== root) { items = cache[parentKey][page]; const parentItem = createEmptyItemFromKey(parentKey); const parentItemContext = dataProviderController.getItemContext(parentItem); if (parentItemContext && parentItemContext.subCache) { const callbacksForParentKey = treePageCallbacks[parentKey]; const callback = callbacksForParentKey && callbacksForParentKey[page]; _updateGridCache(page, items, callback, parentItemContext.subCache); } } else { items = cache[root][page]; _updateGridCache(page, items, rootPageCallbacks[page], dataProviderController.rootCache); } return items; }; const _updateGridCache = function (page, items, callback, levelcache) { // Force update unless there's a callback waiting if (!callback) { let rangeStart = page * grid.pageSize; let rangeEnd = rangeStart + grid.pageSize; if (!items) { if (levelcache && levelcache.items) { for (let idx = rangeStart; idx < rangeEnd; idx++) { delete levelcache.items[idx]; } } } else { if (levelcache && levelcache.items) { for (let idx = rangeStart; idx < rangeEnd; idx++) { if (levelcache.items[idx]) { levelcache.items[idx] = items[idx - rangeStart]; } } } } } }; /** * Updates all visible grid rows in DOM. */ const updateAllGridRowsInDomBasedOnCache = function () { updateGridFlatSize(); grid.__updateVisibleRows(); }; /** * Updates the 's internal cache size and flat size. */ const updateGridFlatSize = function () { dataProviderController.recalculateFlatSize(); grid._flatSize = dataProviderController.flatSize; }; /** * Update the given items in DOM if currently visible. * * @param array items the items to update in DOM */ const updateGridItemsInDomBasedOnCache = function (items) { if (!items || !grid.$ || grid.$.items.childElementCount === 0) { return; } const itemKeys = items.map((item) => item.key); const indexes = grid ._getRenderedRows() .filter((row) => row._item && itemKeys.includes(row._item.key)) .map((row) => row.index); if (indexes.length > 0) { grid.__updateVisibleRows(indexes[0], indexes[indexes.length - 1]); } }; grid.$connector.set = tryCatchWrapper(function (index, items, parentKey) { if (index % grid.pageSize != 0) { throw 'Got new data to index ' + index + ' which is not aligned with the page size of ' + grid.pageSize; } let pkey = parentKey || root; const firstPage = index / grid.pageSize; const updatedPageCount = Math.ceil(items.length / grid.pageSize); // For root cache, remember the range of pages that were set during an update if (pkey === root) { currentUpdateSetRange = [firstPage, firstPage + updatedPageCount - 1]; } for (let i = 0; i < updatedPageCount; i++) { let page = firstPage + i; let slice = items.slice(i * grid.pageSize, (i + 1) * grid.pageSize); if (!cache[pkey]) { cache[pkey] = {}; } cache[pkey][page] = slice; grid.$connector.doSelection(slice.filter((item) => item.selected)); grid.$connector.doDeselection(slice.filter((item) => !item.selected && selectedKeys[item.key])); const updatedItems = updateGridCache(page, pkey); if (updatedItems) { itemsUpdated(updatedItems); updateGridItemsInDomBasedOnCache(updatedItems); } } }); const itemToCacheLocation = function (item) { let parent = item.parentUniqueKey || root; if (cache[parent]) { for (let page in cache[parent]) { for (let index in cache[parent][page]) { if (grid.getItemId(cache[parent][page][index]) === grid.getItemId(item)) { return { page: page, index: index, parentKey: parent }; } } } } return null; }; /** * Updates the given items for a hierarchical grid. * * @param updatedItems the updated items array */ grid.$connector.updateHierarchicalData = tryCatchWrapper(function (updatedItems) { let pagesToUpdate = []; // locate and update the items in cache // find pages that need updating for (let i = 0; i < updatedItems.length; i++) { let cacheLocation = itemToCacheLocation(updatedItems[i]); if (cacheLocation) { cache[cacheLocation.parentKey][cacheLocation.page][cacheLocation.index] = updatedItems[i]; let key = cacheLocation.parentKey + ':' + cacheLocation.page; if (!pagesToUpdate[key]) { pagesToUpdate[key] = { parentKey: cacheLocation.parentKey, page: cacheLocation.page }; } } } // IE11 doesn't work with the transpiled version of the forEach. let keys = Object.keys(pagesToUpdate); for (let i = 0; i < keys.length; i++) { let pageToUpdate = pagesToUpdate[keys[i]]; const affectedUpdatedItems = updateGridCache(pageToUpdate.page, pageToUpdate.parentKey); if (affectedUpdatedItems) { itemsUpdated(affectedUpdatedItems); updateGridItemsInDomBasedOnCache(affectedUpdatedItems); } } }); /** * Updates the given items for a non-hierarchical grid. * * @param updatedItems the updated items array */ grid.$connector.updateFlatData = tryCatchWrapper(function (updatedItems) { // update (flat) caches for (let i = 0; i < updatedItems.length; i++) { let cacheLocation = itemToCacheLocation(updatedItems[i]); if (cacheLocation) { // update connector cache cache[cacheLocation.parentKey][cacheLocation.page][cacheLocation.index] = updatedItems[i]; // update grid's cache const index = parseInt(cacheLocation.page) * grid.pageSize + parseInt(cacheLocation.index); const { rootCache } = dataProviderController; if (rootCache.items[index]) { rootCache.items[index] = updatedItems[i]; } } } itemsUpdated(updatedItems); updateGridItemsInDomBasedOnCache(updatedItems); }); grid.$connector.clearExpanded = tryCatchWrapper(function () { grid.expandedItems = []; ensureSubCacheQueue = []; parentRequestQueue = []; }); /** * Ensures that the last requested page range does not include pages for data that has been cleared. * The last requested range is used in `fetchPage` to skip requests to the server if the page range didn't * change. However, if some pages of that range have been cleared by data communicator, we need to clear the * range to ensure the pages get loaded again. This can happen for example when changing the requested range * on the server (e.g. preload of items on scroll to index), which can cause data communicator to clear pages * that the connector assumes are already loaded. */ const sanitizeLastRequestedRange = function () { // Only relevant for the root cache const range = lastRequestedRanges[root]; // Range may not be set yet, or nothing was cleared if (!range || !currentUpdateClearRange) { return; } // Determine all pages that were cleared const numClearedPages = currentUpdateClearRange[1] - currentUpdateClearRange[0] + 1; const clearedPages = Array.from({ length: numClearedPages }, (_, i) => currentUpdateClearRange[0] + i); // Remove pages that have been set in same update if (currentUpdateSetRange) { const [first, last] = currentUpdateSetRange; for (let page = first; page <= last; page++) { const index = clearedPages.indexOf(page); if (index >= 0) { clearedPages.splice(index, 1); } } } // Clear the last requested range if it includes any of the cleared pages if (clearedPages.some((page) => page >= range[0] && page <= range[1])) { range[0] = -1; range[1] = -1; } }; grid.$connector.clear = tryCatchWrapper(function (index, length, parentKey) { let pkey = parentKey || root; if (!cache[pkey] || Object.keys(cache[pkey]).length === 0) { return; } if (index % grid.pageSize != 0) { throw ( 'Got cleared data for index ' + index + ' which is not aligned with the page size of ' + grid.pageSize ); } let firstPage = Math.floor(index / grid.pageSize); let updatedPageCount = Math.ceil(length / grid.pageSize); // For root cache, remember the range of pages that were cleared during an update if (pkey === root) { currentUpdateClearRange = [firstPage, firstPage + updatedPageCount - 1]; } for (let i = 0; i < updatedPageCount; i++) { let page = firstPage + i; let items = cache[pkey][page]; grid.$connector.doDeselection(items.filter((item) => selectedKeys[item.key])); items.forEach((item) => grid.closeItemDetails(item)); delete cache[pkey][page]; const updatedItems = updateGridCache(page, parentKey); if (updatedItems) { itemsUpdated(updatedItems); } updateGridItemsInDomBasedOnCache(items); } let cacheToClear = dataProviderController.rootCache; if (parentKey) { const parentItem = createEmptyItemFromKey(pkey); const parentItemContext = dataProviderController.getItemContext(parentItem); cacheToClear = parentItemContext.subCache; } const endIndex = index + updatedPageCount * grid.pageSize; for (let itemIndex = index; itemIndex < endIndex; itemIndex++) { delete cacheToClear.items[itemIndex]; cacheToClear.removeSubCache(itemIndex); } updateGridFlatSize(); }); grid.$connector.reset = tryCatchWrapper(function () { grid.size = 0; deleteObjectContents(cache); deleteObjectContents(dataProviderController.rootCache.items); deleteObjectContents(lastRequestedRanges); if (ensureSubCacheDebouncer) { ensureSubCacheDebouncer.cancel(); } if (parentRequestDebouncer) { parentRequestDebouncer.cancel(); } if (rootRequestDebouncer) { rootRequestDebouncer.cancel(); } ensureSubCacheDebouncer = undefined; parentRequestDebouncer = undefined; ensureSubCacheQueue = []; parentRequestQueue = []; updateAllGridRowsInDomBasedOnCache(); }); const deleteObjectContents = (obj) => Object.keys(obj).forEach((key) => delete obj[key]); grid.$connector.updateSize = (newSize) => (grid.size = newSize); grid.$connector.updateUniqueItemIdPath = (path) => (grid.itemIdPath = path); grid.$connector.expandItems = tryCatchWrapper(function (items) { let newExpandedItems = Array.from(grid.expandedItems); items.filter((item) => !grid._isExpanded(item)).forEach((item) => newExpandedItems.push(item)); grid.expandedItems = newExpandedItems; }); grid.$connector.collapseItems = tryCatchWrapper(function (items) { let newExpandedItems = Array.from(grid.expandedItems); items.forEach((item) => { let index = grid._getItemIndexInArray(item, newExpandedItems); if (index >= 0) { newExpandedItems.splice(index, 1); } }); grid.expandedItems = newExpandedItems; items.forEach((item) => grid.$connector.removeFromQueue(item)); }); grid.$connector.removeFromQueue = tryCatchWrapper(function (item) { let itemId = grid.getItemId(item); // The treePageCallbacks for the itemId are about to be discarded -> // Resolve the callbacks with an empty array to not leave grid in loading state Object.values(treePageCallbacks[itemId] || {}).forEach((callback) => callback([])); delete treePageCallbacks[itemId]; ensureSubCacheQueue = ensureSubCacheQueue.filter((item) => item.itemkey !== itemId); parentRequestQueue = parentRequestQueue.filter((item) => item.parentKey !== itemId); }); grid.$connector.confirmParent = tryCatchWrapper(function (id, parentKey, levelSize) { // Create connector cache if it doesn't exist if (!cache[parentKey]) { cache[parentKey] = {}; } // Update connector cache size const hasSizeChanged = cache[parentKey].size !== levelSize; cache[parentKey].size = levelSize; if (levelSize === 0) { cache[parentKey][0] = []; } // If grid has outstanding requests for this parent, then resolve them // and let grid update the flat size and re-render. let outstandingRequests = Object.getOwnPropertyNames(treePageCallbacks[parentKey] || {}); for (let i = 0; i < outstandingRequests.length; i++) { let page = outstandingRequests[i]; let lastRequestedRange = lastRequestedRanges[parentKey] || [0, 0]; const callback = treePageCallbacks[parentKey][page]; if ( (cache[parentKey] && cache[parentKey][page]) || page < lastRequestedRange[0] || page > lastRequestedRange[1] ) { delete treePageCallbacks[parentKey][page]; let items = cache[parentKey][page] || new Array(levelSize); callback(items, levelSize); } else if (callback && levelSize === 0) { // The parent item has 0 child items => resolve the callback with an empty array delete treePageCallbacks[parentKey][page]; callback([], levelSize); } } // If size has changed, and there are no outstanding requests, then // manually update the size of the grid cache and update the effective // size, effectively re-rendering the grid. This is necessary when // individual items are refreshed on the server, in which case there // is no loading request from the grid itself. In that case, if // children were added or removed, the grid will not be aware of it // unless we manually update the size. if (hasSizeChanged && outstandingRequests.length === 0) { const parentItem = createEmptyItemFromKey(parentKey); const parentItemContext = dataProviderController.getItemContext(parentItem); if (parentItemContext && parentItemContext.subCache) { parentItemContext.subCache.size = levelSize; } updateGridFlatSize(); } // Let server know we're done grid.$server.confirmParentUpdate(id, parentKey); if (!grid.loading) { grid.__confirmParentUpdateDebouncer = Debouncer.debounce( grid.__confirmParentUpdateDebouncer, animationFrame, () => grid.__updateVisibleRows() ); } }); grid.$connector.confirm = tryCatchWrapper(function (id) { // We're done applying changes from this batch, resolve outstanding // callbacks let outstandingRequests = Object.getOwnPropertyNames(rootPageCallbacks); for (let i = 0; i < outstandingRequests.length; i++) { let page = outstandingRequests[i]; let lastRequestedRange = lastRequestedRanges[root] || [0, 0]; const lastAvailablePage = grid.size ? Math.ceil(grid.size / grid.pageSize) - 1 : 0; // It's possible that the lastRequestedRange includes a page that's beyond lastAvailablePage if the grid's size got reduced during an ongoing data request const lastRequestedRangeEnd = Math.min(lastRequestedRange[1], lastAvailablePage); // Resolve if we have data or if we don't expect to get data const callback = rootPageCallbacks[page]; if (cache[root]?.[page] || page < lastRequestedRange[0] || +page > lastRequestedRangeEnd) { delete rootPageCallbacks[page]; if (cache[root][page]) { // Cached data is available, resolve the callback callback(cache[root][page]); } else { // No cached data, resolve the callback with an empty array callback(new Array(grid.pageSize)); // Request grid for content update grid.requestContentUpdate(); } } else if (callback && grid.size === 0) { // The grid has 0 items => resolve the callback with an empty array delete rootPageCallbacks[page]; callback([]); } } // Sanitize last requested range for the root level sanitizeLastRequestedRange(); // Clear current update state currentUpdateSetRange = null; currentUpdateClearRange = null; // Let server know we're done grid.$server.confirmUpdate(id); }); grid.$connector.ensureHierarchy = tryCatchWrapper(function () { for (let parentKey in cache) { if (parentKey !== root) { delete cache[parentKey]; } } deleteObjectContents(lastRequestedRanges); dataProviderController.rootCache.removeSubCaches(); updateAllGridRowsInDomBasedOnCache(); }); grid.$connector.setSelectionMode = tryCatchWrapper(function (mode) { if ((typeof mode === 'string' || mode instanceof String) && validSelectionModes.indexOf(mode) >= 0) { selectionMode = mode; selectedKeys = {}; grid.$connector.updateMultiSelectable(); } else { throw 'Attempted to set an invalid selection mode'; } }); /* * Manage aria-multiselectable attribute depending on the selection mode. * see more: https://github.com/vaadin/web-components/issues/1536 * or: https://www.w3.org/TR/wai-aria-1.1/#aria-multiselectable * For selection mode SINGLE, set the aria-multiselectable attribute to false */ grid.$connector.updateMultiSelectable = tryCatchWrapper(function () { if (!grid.$) { return; } if (selectionMode === validSelectionModes[0]) { grid.$.table.setAttribute('aria-multiselectable', false); // For selection mode NONE, remove the aria-multiselectable attribute } else if (selectionMode === validSelectionModes[1]) { grid.$.table.removeAttribute('aria-multiselectable'); // For selection mode MULTI, set aria-multiselectable to true } else { grid.$.table.setAttribute('aria-multiselectable', true); } }); // Have the multi-selectable state updated on attach grid._createPropertyObserver('isAttached', () => grid.$connector.updateMultiSelectable()); const singleTimeRenderer = (renderer) => { return (root) => { if (renderer) { renderer(root); renderer = null; } }; }; grid.$connector.setHeaderRenderer = tryCatchWrapper(function (column, options) { const { content, showSorter, sorterPath } = options; if (content === null) { column.headerRenderer = null; return; } column.headerRenderer = singleTimeRenderer((root) => { // Clear previous contents root.innerHTML = ''; // Render sorter let contentRoot = root; if (showSorter) { const sorter = document.createElement('vaadin-grid-sorter'); sorter.setAttribute('path', sorterPath); const ariaLabel = content instanceof Node ? content.textContent : content; if (ariaLabel) { sorter.setAttribute('aria-label', `Sort by ${ariaLabel}`); } root.appendChild(sorter); // Use sorter as content root contentRoot = sorter; } // Add content if (content instanceof Node) { contentRoot.appendChild(content); } else { contentRoot.textContent = content; } }); }); grid.__applySorters = () => { const sorters = grid._mapSorters(); const sortersChanged = JSON.stringify(grid._previousSorters) !== JSON.stringify(sorters); // Update the _previousSorters in vaadin-grid-sort-mixin so that the __applySorters // method in the mixin will skip calling clearCache(). // // In Flow Grid's case, we never want to clear the cache eagerly when the sorter elements // change due to one of the following reasons: // // 1. Sorted by user: The items in the new sort order need to be fetched from the server, // and we want to avoid a heavy re-render before the updated items have actually been fetched. // // 2. Sorted programmatically on the server: The items in the new sort order have already // been fetched and applied to the grid. The sorter element states are updated programmatically // to reflect the new sort order, but there's no need to re-render the grid rows. grid._previousSorters = sorters; // Call the original __applySorters method in vaadin-grid-sort-mixin Grid.prototype.__applySorters.call(grid); if (sortersChanged && !sorterDirectionsSetFromServer) { grid.$server.sortersChanged(sorters); } }; grid.$connector.setFooterRenderer = tryCatchWrapper(function (column, options) { const { content } = options; if (content === null) { column.footerRenderer = null; return; } column.footerRenderer = singleTimeRenderer((root) => { // Clear previous contents root.innerHTML = ''; // Add content if (content instanceof Node) { root.appendChild(content); } else { root.textContent = content; } }); }); grid.addEventListener( 'vaadin-context-menu-before-open', tryCatchWrapper(function (e) { const { key, columnId } = e.detail; grid.$server.updateContextMenuTargetItem(key, columnId); }) ); grid.getContextMenuBeforeOpenDetail = tryCatchWrapper(function (event) { // For `contextmenu` events, we need to access the source event, // when using open on click we just use the click event itself const sourceEvent = event.detail.sourceEvent || event; const eventContext = grid.getEventContext(sourceEvent); const key = eventContext.item?.key || ''; const columnId = eventContext.column?.id || ''; return { key, columnId }; }); grid.preventContextMenu = tryCatchWrapper(function (event) { const isLeftClick = event.type === 'click'; const { column } = grid.getEventContext(event); return isLeftClick && column instanceof GridFlowSelectionColumn; }); grid.addEventListener( 'click', tryCatchWrapper((e) => _fireClickEvent(e, 'item-click')) ); grid.addEventListener( 'dblclick', tryCatchWrapper((e) => _fireClickEvent(e, 'item-double-click')) ); grid.addEventListener( 'column-resize', tryCatchWrapper((e) => { const cols = grid._getColumnsInOrder().filter((col) => !col.hidden); cols.forEach((col) => { col.dispatchEvent(new CustomEvent('column-drag-resize')); }); grid.dispatchEvent( new CustomEvent('column-drag-resize', { detail: { resizedColumnKey: e.detail.resizedColumn._flowId } }) ); }) ); grid.addEventListener( 'column-reorder', tryCatchWrapper((e) => { const columns = grid._columnTree .slice(0) .pop() .filter((c) => c._flowId) .sort((b, a) => b._order - a._order) .map((c) => c._flowId); grid.dispatchEvent( new CustomEvent('column-reorder-all-columns', { detail: { columns } }) ); }) ); grid.addEventListener( 'cell-focus', tryCatchWrapper((e) => { const eventContext = grid.getEventContext(e); const expectedSectionValues = ['header', 'body', 'footer']; if (expectedSectionValues.indexOf(eventContext.section) === -1) { return; } grid.dispatchEvent( new CustomEvent('grid-cell-focus', { detail: { itemKey: eventContext.item ? eventContext.item.key : null, internalColumnId: eventContext.column ? eventContext.column._flowId : null, section: eventContext.section } }) ); }) ); function _fireClickEvent(event, eventName) { // Click event was handled by the component inside grid, do nothing. if (event.defaultPrevented) { return; } const target = event.target; if (isFocusable(target) || target instanceof HTMLLabelElement) { return; } const eventContext = grid.getEventContext(event); const section = eventContext.section; if (eventContext.item && section !== 'details') { event.itemKey = eventContext.item.key; // if you have a details-renderer, getEventContext().column is undefined if (eventContext.column) { event.internalColumnId = eventContext.column._flowId; } grid.dispatchEvent(new CustomEvent(eventName, { detail: event })); } } grid.cellClassNameGenerator = tryCatchWrapper(function (column, rowData) { const style = rowData.item.style; if (!style) { return; } return (style.row || '') + ' ' + ((column && style[column._flowId]) || ''); }); grid.cellPartNameGenerator = tryCatchWrapper(function (column, rowData) { const part = rowData.item.part; if (!part) { return; } return (part.row || '') + ' ' + ((column && part[column._flowId]) || ''); }); grid.dropFilter = tryCatchWrapper((rowData) => rowData.item && !rowData.item.dropDisabled); grid.dragFilter = tryCatchWrapper((rowData) => rowData.item && !rowData.item.dragDisabled); grid.addEventListener( 'grid-dragstart', tryCatchWrapper((e) => { if (grid._isSelected(e.detail.draggedItems[0])) { // Dragging selected (possibly multiple) items if (grid.__selectionDragData) { Object.keys(grid.__selectionDragData).forEach((type) => { e.detail.setDragData(type, grid.__selectionDragData[type]); }); } else { (grid.__dragDataTypes || []).forEach((type) => { e.detail.setDragData(type, e.detail.draggedItems.map((item) => item.dragData[type]).join('\n')); }); } if (grid.__selectionDraggedItemsCount > 1) { e.detail.setDraggedItemsCount(grid.__selectionDraggedItemsCount); } } else { // Dragging just one (non-selected) item (grid.__dragDataTypes || []).forEach((type) => { e.detail.setDragData(type, e.detail.draggedItems[0].dragData[type]); }); } }) ); })(grid) }; })();