diff --git a/packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts b/packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts index 1f0d2664b813..ba7aada7d9e1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts @@ -14,7 +14,7 @@ const ERROR_MESSAGE_CLASS = 'dx-error-message'; const ERROR_CLOSEBUTTON_CLASS = 'dx-closebutton'; const ACTION_CLASS = 'action'; -class ErrorHandlingController extends modules.ViewController { +export class ErrorHandlingController extends modules.ViewController { private _columnHeadersView: any; private _rowsView: any; @@ -126,7 +126,7 @@ class ErrorHandlingController extends modules.ViewController { return $firstErrorRow; } - removeErrorRow($row) { + removeErrorRow($row?) { if (!$row) { const $columnHeaders = this._columnHeadersView && this._columnHeadersView.element(); $row = $columnHeaders && $columnHeaders.find(`.${ERROR_ROW_CLASS}`); @@ -152,7 +152,7 @@ class ErrorHandlingController extends modules.ViewController { const data = (Base: ModuleType) => class ErrorHandlingDataControllerExtends extends Base { init() { const that = this; - // @ts-expect-error + const errorHandlingController = that.getController('errorHandling'); super.init(); @@ -166,7 +166,7 @@ const data = (Base: ModuleType) => class ErrorHandlingDataContro if (e && e.changeType === 'loadError') { return; } - // @ts-expect-error + const errorHandlingController = that.getController('errorHandling'); const editingController = that.getController('editing'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index 9b0a2b5ffe37..7435fe9aa0b1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -169,6 +169,7 @@ export interface Controllers { export: any; draggingHeader: any; selection: import('./selection/m_selection').SelectionController; + errorHandling: import('./error_handling/m_error_handling').ErrorHandlingController; } type ControllerTypes = { diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts index 18f520c7c0b7..203b4c1e7fee 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts @@ -996,7 +996,6 @@ export const validatingModule = { _showErrorRow(change) { let $popupContent; - // @ts-expect-error const errorHandling = this.getController('errorHandling'); const items = this.getController('data').items(); const rowIndex = this.getIndexByKey(change.key, items); diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index fe2371a8e937..8d0de3dd5d86 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -55,7 +55,7 @@ const restoreFocus = function (focusedElement, selectionRange) { export class ResizingController extends modules.ViewController { private _refreshSizesHandler: any; - private _dataController: any; + _dataController: any; _rowsView: any; diff --git a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling.ts b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling.ts index 57c75d6fb59f..3654c6f8f801 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ import $ from '@js/core/renderer'; import browser from '@js/core/utils/browser'; import { Deferred, when } from '@js/core/utils/deferred'; @@ -9,8 +10,11 @@ import { isDefined } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; import LoadIndicator from '@js/ui/load_indicator'; import errors from '@js/ui/widget/ui.errors'; +import type { DataController } from '@ts/grids/grid_core/data_controller/m_data_controller'; import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; import type { ModuleType } from '@ts/grids/grid_core/m_types'; +import type { ResizingController } from '@ts/grids/grid_core/views/m_grid_view'; +import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view'; import gridCoreUtils from '../m_utils'; import { subscribeToExternalScrollers, VirtualScrollController } from './m_virtual_scrolling_core'; @@ -95,6 +99,21 @@ const proxyDataSourceAdapterMethod = function (that, methodName, args) { return virtualScrollController[methodName].apply(virtualScrollController, args); }; +const removeEmptyRows = function ($emptyRows, className) { + const getRowParent = (row) => $(row).parent(`.${className}`).get(0); + const tBodies = $emptyRows.toArray().map(getRowParent).filter((row) => row); + + if (tBodies.length) { + $emptyRows = $(tBodies); + } + + const rowCount = className === FREESPACE_CLASS ? $emptyRows.length - 1 : $emptyRows.length; + + for (let i = 0; i < rowCount; i++) { + $emptyRows.eq(i).remove(); + } +}; + export const dataSourceAdapterExtender = (Base: ModuleType) => class VirtualScrollingCoreDataSourceAdapterExtender extends Base { _totalCount: any; @@ -427,1322 +446,1406 @@ export const dataSourceAdapterExtender = (Base: ModuleType) = return proxyDataSourceAdapterMethod(this, 'loadIfNeed', [...arguments]); } }; +export const data = (Base: ModuleType) => class VirtualScrollingDataControllerExtender extends Base { + _loadViewportParams: any; -const VirtualScrollingRowsViewExtender = (function () { - const removeEmptyRows = function ($emptyRows, className) { - const getRowParent = (row) => $(row).parent(`.${className}`).get(0); - const tBodies = $emptyRows.toArray().map(getRowParent).filter((row) => row); + _allItems: any; - if (tBodies.length) { - $emptyRows = $(tBodies); - } + _visibleItems: any; - const rowCount = className === FREESPACE_CLASS ? $emptyRows.length - 1 : $emptyRows.length; + _rowPageIndex: any; - for (let i = 0; i < rowCount; i++) { - $emptyRows.eq(i).remove(); - } - }; + _viewportChanging: any; - return { - init() { - const dataController = this.getController('data'); + _needUpdateViewportAfterLoading: any; - this.callBase(); + _itemCount: any; - dataController.pageChanged.add((pageIndex) => { - const scrollTop = this._scrollTop; + _refreshDataSource() { + // @ts-expect-error + const baseResult = super._refreshDataSource.apply(this, arguments as any) || new Deferred().resolve().promise(); - this.scrollToPage(pageIndex ?? dataController.pageIndex()); + baseResult.done(this.initVirtualRows.bind(this)); - if (this.option(LEGACY_SCROLLING_MODE) === false && this._scrollTop === scrollTop) { - dataController.updateViewport(); - } - }); + return baseResult; + } - dataController.dataSourceChanged.add(() => { - !this._scrollTop && this._scrollToCurrentPageOnResize(); - }); + _loadDataSource() { + if (this._rowsScrollController && isVirtualPaging(this)) { + const { loadPageCount } = isDefined(this._loadViewportParams) ? this.getLoadPageParams() : { loadPageCount: 0 }; - dataController.stateLoaded?.add(() => { - this._scrollToCurrentPageOnResize(); - }); + loadPageCount >= 1 && this._dataSource?.loadPageCount(loadPageCount); + } - this._scrollToCurrentPageOnResize(); - }, + return super._loadDataSource.apply(this, arguments as any); + } + + getRowPageSize() { + const rowPageSize = this.option('scrolling.rowPageSize'); + const pageSize = this.pageSize(); - _scrollToCurrentPageOnResize() { - const dataController = this.getController('data'); + return pageSize && pageSize < rowPageSize ? pageSize : rowPageSize; + } - if (dataController.pageIndex() > 0) { - const resizeHandler = () => { - this.resizeCompleted.remove(resizeHandler); - this.scrollToPage(dataController.pageIndex()); - }; - this.resizeCompleted.add(resizeHandler); + reload() { + const rowsScrollController = this._rowsScrollController || this._dataSource; + const itemIndex = rowsScrollController && rowsScrollController.getItemIndexByPosition(); + const result = super.reload.apply(this, arguments as any); + return result && result.done(() => { + if (isVirtualMode(this) || gridCoreUtils.isVirtualRowRendering(this)) { + const rowIndexOffset = this.getRowIndexOffset(); + const rowIndex = Math.floor(itemIndex) - rowIndexOffset; + const { component } = this; + const scrollable = component.getScrollable && component.getScrollable(); + const isSortingOperation = this.dataSource().operationTypes().sorting; + + if (scrollable && !isSortingOperation && rowIndex >= 0) { + const rowElement = component.getRowElement(rowIndex); + const $rowElement = rowElement && rowElement[0] && $(rowElement[0]); + let top = $rowElement && $rowElement.position()?.top; + const isChromeLatest = browser.chrome && Number(browser.version ?? 0) >= 91; + const allowedTopOffset = browser.mozilla || isChromeLatest ? 1 : 0; // T884308 + if (top && top > allowedTopOffset) { + top = Math.round(top + getOuterHeight($rowElement) * (itemIndex % 1)); + scrollable.scrollTo({ y: top }); + } + } } - }, + }); + } - scrollToPage(pageIndex) { - const that = this; - const dataController = that._dataController; - const pageSize = dataController ? dataController.pageSize() : 0; - let scrollPosition; + initVirtualRows() { + const virtualRowsRendering = gridCoreUtils.isVirtualRowRendering(this); - if (isVirtualMode(that) || isAppendMode(that)) { - const itemSize = dataController.getItemSize(); - const itemSizes = dataController.getItemSizes(); - const itemIndex = pageIndex * pageSize; + this._allItems = null; + this._loadViewportParams = null; - scrollPosition = itemIndex * itemSize; + if (this.option('scrolling.mode') !== 'virtual' && !virtualRowsRendering || !virtualRowsRendering || this.option(LEGACY_SCROLLING_MODE) !== false && !this.option('scrolling.rowPageSize')) { + this._visibleItems = null; + this._rowsScrollController = null; + return; + } - // eslint-disable-next-line no-restricted-syntax - for (const index in itemSizes) { - // eslint-disable-next-line radix - if (parseInt(index) < itemIndex) { - scrollPosition += itemSizes[index] - itemSize; - } - } - } else { - scrollPosition = 0; - } + const pageIndex = !isVirtualMode(this) && this.pageIndex() >= this.pageCount() ? this.pageCount() - 1 : this.pageIndex(); + this._rowPageIndex = Math.ceil(pageIndex * this.pageSize() / this.getRowPageSize()); + this._visibleItems = this.option(LEGACY_SCROLLING_MODE) === false ? null : []; + this._viewportChanging = false; + this._needUpdateViewportAfterLoading = false; - that.scrollTo({ y: scrollPosition, x: that._scrollLeft }); - }, + if (!this._rowsScrollController) { + this._rowsScrollController = new VirtualScrollController(this.component, this._getRowsScrollDataOptions(), true); - renderDelayedTemplates() { - this.waitAsyncTemplates().done(() => { - this._updateContentPosition(true); + this._rowsScrollController.positionChanged.add(() => { + if (this.option(LEGACY_SCROLLING_MODE) === false) { + this._viewportChanging = true; + this.loadViewport(); + this._viewportChanging = false; + return; + } + this._dataSource?.setViewportItemIndex(this._rowsScrollController.getViewportItemIndex()); }); - this.callBase.apply(this, arguments); - }, + } - _renderCore(e) { - const startRenderTime: any = new Date(); + if (this.option(LEGACY_SCROLLING_MODE) === false) { + this._updateLoadViewportParams(); + } - const deferred = this.callBase.apply(this, arguments); + if (this.isLoaded() && this.option(LEGACY_SCROLLING_MODE) !== false) { + this._rowsScrollController.load(); + } + } - const dataSource = this._dataController._dataSource; + isViewportChanging() { + return this._viewportChanging; + } - if (dataSource && e) { - const itemCount = e.items ? e.items.length : 20; - const viewportSize = this._dataController.viewportSize() || 20; + _getRowsScrollDataOptions() { + const that = this; + const isItemCountable = function (item) { + return isItemCountableByDataSource(item, that._dataSource); + }; - if (gridCoreUtils.isVirtualRowRendering(this) && itemCount > 0 && this.option(LEGACY_SCROLLING_MODE) !== false) { - dataSource._renderTime = ((new Date()) as any - startRenderTime) * viewportSize / itemCount; - } else { - dataSource._renderTime = ((new Date()) as any - startRenderTime); + return { + pageSize() { + return that.getRowPageSize(); + }, + loadedOffset() { + return isVirtualMode(that) && that._dataSource?.lastLoadOptions().skip || 0; + }, + loadedItemCount() { + return that._itemCount; + }, + totalItemsCount() { + if (isVirtualPaging(that)) { + return that.totalItemsCount(); } - } - return deferred; - }, - _getRowElements(tableElement) { - const $rows = this.callBase(tableElement); + return that.option(LEGACY_SCROLLING_MODE) === false ? that._itemCount : that._items.filter(isItemCountable).length; + }, + hasKnownLastPage() { + return that.option(LEGACY_SCROLLING_MODE) === false ? that.hasKnownLastPage() : true; + }, + pageIndex(index) { + if (index !== undefined) { + that._rowPageIndex = index; + } + return that._rowPageIndex; + }, + isLoading() { + return that.isLoading(); + }, + pageCount() { + const pageCount = Math.ceil(this.totalItemsCount() / this.pageSize()); + return pageCount || 1; + }, + load() { + if (that._rowsScrollController.pageIndex() >= this.pageCount()) { + that._rowPageIndex = this.pageCount() - 1; + that._rowsScrollController.pageIndex(that._rowPageIndex); + } - return $rows && $rows.not(`.${VIRTUAL_ROW_CLASS}`); - }, + if (!this.items().length && this.totalItemsCount()) return; - _removeRowsElements(contentTable, removeCount, changeType) { - let rowElements = this._getRowElements(contentTable).toArray(); - if (changeType === 'append') { - rowElements = rowElements.slice(0, removeCount); - } else { - rowElements = rowElements.slice(-removeCount); - } + that._rowsScrollController.handleDataChanged((change) => { + change = change || {}; + change.changeType = change.changeType || 'refresh'; + change.items = change.items || that._visibleItems; - const errorHandlingController = this.getController('errorHandling'); - rowElements.map((rowElement) => { - const $rowElement = $(rowElement); - // @ts-expect-error - errorHandlingController && errorHandlingController.removeErrorRow($rowElement.next()); - // @ts-expect-error - $rowElement.remove(); - }); - }, + that._visibleItems.forEach((item, index) => { + item.rowIndex = index; + }); + that._fireChanged(change); + }); + }, + updateLoading() { + }, + itemsCount() { + return this.items(true).length; + }, + correctCount(items, count, fromEnd) { + return correctCount(items, count, fromEnd, (item, isNextAfterLast, fromEnd) => { + if (item.isNewRow) { + return isNextAfterLast && !fromEnd; + } - _updateContent(tableElement, change) { - let $freeSpaceRowElements; - const contentElement = this._findContentElement(); - const changeType = change && change.changeType; - const d: any = Deferred(); - - const contentTable = contentElement.children().first(); - if (changeType === 'append' || changeType === 'prepend') { - this.waitAsyncTemplates().done(() => { - const $tBodies = this._getBodies(tableElement); - if ($tBodies.length === 1) { - this._getBodies(contentTable)[changeType === 'append' ? 'append' : 'prepend']($tBodies.children()); - } else { - $tBodies[changeType === 'append' ? 'appendTo' : 'prependTo'](contentTable); + if (isNextAfterLast && fromEnd) { + return !item.isNewRow; } - tableElement.remove(); - $freeSpaceRowElements = this._getFreeSpaceRowElements(contentTable); - removeEmptyRows($freeSpaceRowElements, FREESPACE_CLASS); + return isItemCountable(item); + }); + }, + items(countableOnly) { + let result = that._items; + + if (that.option(LEGACY_SCROLLING_MODE)) { + const dataSource = that.dataSource(); + const virtualItemsCount = dataSource?.virtualItemsCount(); + const begin = virtualItemsCount ? virtualItemsCount.begin : 0; + const rowPageSize = that.getRowPageSize(); + + let skip = that._rowPageIndex * rowPageSize - begin; + let take = rowPageSize; - if (change.removeCount) { - this._removeRowsElements(contentTable, change.removeCount, changeType); + if (skip < 0) { + return []; } - this._restoreErrorRow(contentTable); - d.resolve(); - }).fail(d.reject); - } else { - this.callBase.apply(this, arguments).done(() => { - if (changeType === 'update') { - this._restoreErrorRow(contentTable); + if (skip) { + skip = this.correctCount(result, skip); + result = result.slice(skip); } - d.resolve(); - }).fail(d.reject); - } + if (take) { + take = this.correctCount(result, take); + result = result.slice(0, take); + } + } - return d.promise().done(() => { - this._updateBottomLoading(); - }); - }, - _addVirtualRow($table, isFixed, location, position) { - if (!position) return; + return countableOnly ? result.filter(isItemCountable) : result; + }, + viewportItems(items) { + if (items && that.option(LEGACY_SCROLLING_MODE) !== false) { + that._visibleItems = items; + } + return that._visibleItems; + }, + onChanged() { + }, + changingDuration() { + const dataSource = that.dataSource(); - let $virtualRow = this._createEmptyRow(VIRTUAL_ROW_CLASS, isFixed, position); + if (dataSource?.isLoading() && that.option(LEGACY_SCROLLING_MODE) !== false) { + return LOAD_TIMEOUT; + } - $virtualRow = this._wrapRowIfNeed($table, $virtualRow); + return dataSource?._renderTime || 0; + }, + }; + } - this._appendEmptyRow($table, $virtualRow, location); - }, - _updateContentItemSizes() { - const rowHeights = this._getRowHeights(); - const correctedRowHeights = this._correctRowHeights(rowHeights); + _updateItemsCore(change) { + const delta = this.getRowIndexDelta(); - this._dataController.setContentItemSizes(correctedRowHeights); - }, - _updateViewportSize(viewportHeight, scrollTop) { - if (!isDefined(viewportHeight)) { - viewportHeight = this._hasHeight ? getOuterHeight(this.element()) : getOuterHeight(getWindow()); + super._updateItemsCore.apply(this, arguments as any); + if (this.option(LEGACY_SCROLLING_MODE) === false && gridCoreUtils.isVirtualRowRendering(this)) { + if (change.changeType === 'update' && change.rowIndices.length === 0 && change.cancelEmptyChanges) { + change.cancel = true; } + return; + } - this._dataController.viewportHeight(viewportHeight, scrollTop); - }, - _getRowHeights() { - const isPopupEditMode = this.getController('editing')?.isPopupEditMode?.(); - - let rowElements = this._getRowElements(this._tableElement).toArray(); + const rowsScrollController = this._rowsScrollController; - if (isPopupEditMode) { - rowElements = rowElements.filter((row) => !$(row).hasClass(ROW_INSERTED)); - } + if (rowsScrollController) { + const visibleItems = this._visibleItems; + const isRefresh = change.changeType === 'refresh' || change.isLiveUpdate; - return rowElements.map((row) => getBoundingRect(row).height); - }, - _correctRowHeights(rowHeights) { - const dataController = this._dataController; - const dataSource = dataController._dataSource; - const correctedRowHeights: any = []; - const visibleRows = dataController.getVisibleRows(); - let itemSize = 0; - let firstCountableItem = true; - let lastLoadIndex = -1; - - for (let i = 0; i < rowHeights.length; i++) { - const currentItem = visibleRows[i]; - if (!isDefined(currentItem)) { - continue; - } + if (change.changeType === 'append' && change.items && !change.items.length) return; - if (this.option(LEGACY_SCROLLING_MODE) === false) { - if (lastLoadIndex >= 0 && lastLoadIndex !== currentItem.loadIndex) { - correctedRowHeights.push(itemSize); - itemSize = 0; - } - lastLoadIndex = currentItem.loadIndex; - } else if (isItemCountableByDataSource(currentItem, dataSource)) { - if (firstCountableItem) { - firstCountableItem = false; - } else { - correctedRowHeights.push(itemSize); - itemSize = 0; - } + if (isRefresh || change.changeType === 'append' || change.changeType === 'prepend') { + change.cancel = true; + isRefresh && rowsScrollController.reset(true); + rowsScrollController.load(); + } else { + if (change.changeType === 'update') { + change.rowIndices.forEach((rowIndex, index) => { + const changeType = change.changeTypes[index]; + const newItem = change.items[index]; + if (changeType === 'update') { + visibleItems[rowIndex] = newItem; + } else if (changeType === 'insert') { + visibleItems.splice(rowIndex, 0, newItem); + } else if (changeType === 'remove') { + visibleItems.splice(rowIndex, 1); + } + }); + } else { + visibleItems.forEach((item, index) => { + visibleItems[index] = this._items[index + delta] || visibleItems[index]; + }); + change.items = visibleItems; } - itemSize += rowHeights[i]; + updateItemIndices(visibleItems); } - itemSize > 0 && correctedRowHeights.push(itemSize); - return correctedRowHeights; - }, - _updateContentPosition(isRender) { - const dataController = this._dataController; - const rowHeight = this._rowHeight || 20; - - dataController.viewportItemSize(rowHeight); - - if (isVirtualMode(this) || gridCoreUtils.isVirtualRowRendering(this)) { - if (!isRender) { - this._updateContentItemSizes(); - } + } + } - const top = dataController.getContentOffset('begin'); - const bottom = dataController.getContentOffset('end'); - const $tables = this.getTableElements(); - const $virtualRows = $tables.children('tbody').children(`.${VIRTUAL_ROW_CLASS}`); + _updateLoadViewportParams() { + const viewportParams = this._rowsScrollController.getViewportParams(); + const pageSize = this.pageSize(); - removeEmptyRows($virtualRows, VIRTUAL_ROW_CLASS); + if (viewportParams && !isVirtualPaging(this) && pageSize > 0) { + const pageOffset = this.pageIndex() * pageSize; + viewportParams.skip += pageOffset; + } + this._loadViewportParams = viewportParams; + } - $tables.each((index, element) => { - const isFixed = index > 0; - const prevFixed = this._isFixedTableRendering; - this._isFixedTableRendering = isFixed; - this._addVirtualRow($(element), isFixed, 'top', top); - this._addVirtualRow($(element), isFixed, 'bottom', bottom); - this._isFixedTableRendering = prevFixed; - }); - } - }, + _processItems() { + const resultItems = super._processItems.apply(this, arguments as any); - _isTableLinesDisplaysCorrect(table) { - const hasColumnLines = table.find(`.${COLUMN_LINES_CLASS}`).length > 0; - return hasColumnLines === this.option('showColumnLines'); - }, + if (this.option(LEGACY_SCROLLING_MODE) === false) { + const dataSource = this._dataSource; + let currentIndex = dataSource?.lastLoadOptions().skip ?? 0; + let prevCountable; + let prevRowType; + let isPrevRowNew; + let wasCountableItem = false; + let newRows: any = []; + resultItems.forEach((item) => { + const { rowType } = item; + const itemCountable = isItemCountableByDataSource(item, dataSource); + + const isNextGroupItem = rowType === 'group' && (prevCountable || itemCountable || (prevRowType !== 'group' && currentIndex > 0)); + const isNextDataItem = rowType === 'data' && itemCountable && (prevCountable || prevRowType !== 'group'); + + if (!item.isNewRow && isDefined(prevCountable)) { + const isPrevNewRowFirst = isPrevRowNew && !wasCountableItem; + if ((isNextGroupItem || isNextDataItem) && !isPrevNewRowFirst) { + currentIndex++; + } + } - _isColumnElementsEqual($columns, $virtualColumns) { - let result = $columns.length === $virtualColumns.length; + if (isNextGroupItem || isNextDataItem) { + wasCountableItem = true; + } + if (item.isNewRow) { + newRows.push(item); + } else { + newRows.forEach((it) => { it.loadIndex = currentIndex; }); + newRows = []; + } + item.loadIndex = currentIndex; + prevCountable = itemCountable; + prevRowType = rowType; + isPrevRowNew = item.isNewRow; + }); + newRows.forEach((it) => { it.loadIndex = currentIndex; }); + } - if (result) { - each($columns, (index, element) => { - if (element.style.width !== $virtualColumns[index].style.width) { - result = false; - return result; - } + return resultItems; + } - return undefined; + _afterProcessItems(items) { + this._itemCount = items.filter((item) => isItemCountableByDataSource(item, this._dataSource)).length; + if (isDefined(this._loadViewportParams)) { + this._updateLoadViewportParams(); + + let result = items; + this._allItems = items; + + if (items.length) { + const { skipForCurrentPage } = this.getLoadPageParams(true); + const skip = items[0].loadIndex + skipForCurrentPage; + const { take } = this._loadViewportParams; + result = items.filter((it) => { + const isNewRowInEmptyData = it.isNewRow && it.loadIndex === skip && take === 0; + const isLoadIndexGreaterStart = it.loadIndex >= skip; + const isLoadIndexLessEnd = it.loadIndex < skip + take || isNewRowInEmptyData; + return isLoadIndexGreaterStart && isLoadIndexLessEnd; }); } return result; - }, + } - _getCellClasses(column) { - const classes: any = []; - const { cssClass } = column; - const isExpandColumn = column.command === 'expand'; + return super._afterProcessItems.apply(this, arguments as any); + } - cssClass && classes.push(cssClass); - isExpandColumn && classes.push(this.addWidgetPrefix(GROUP_SPACE_CLASS)); + _applyChange(change) { + const that = this; + const { items } = change; + const { changeType } = change; + let { removeCount } = change; - return classes; - }, + if (removeCount) { + const fromEnd = changeType === 'prepend'; + removeCount = correctCount(that._items, removeCount, fromEnd, (item, isNextAfterLast) => item.rowType === 'data' && !item.isNewRow || (item.rowType === 'group' && (that._dataSource.isGroupItemCountable(item.data) || isNextAfterLast))); - _findBottomLoadPanel($contentElement) { - const $element = $contentElement || this.element(); - const $bottomLoadPanel = $element && $element.find(`.${this.addWidgetPrefix(BOTTOM_LOAD_PANEL_CLASS)}`); - if ($bottomLoadPanel && $bottomLoadPanel.length) { - return $bottomLoadPanel; - } - }, + change.removeCount = removeCount; + } - _updateBottomLoading() { - const that = this; - const virtualMode = isVirtualMode(this); - const appendMode = isAppendMode(this); - const showBottomLoading = !that._dataController.hasKnownLastPage() && that._dataController.isLoaded() && (virtualMode || appendMode); - const $contentElement = that._findContentElement(); - const bottomLoadPanelElement = that._findBottomLoadPanel($contentElement); - - if (showBottomLoading) { - if (!bottomLoadPanelElement) { - $('
') - .addClass(that.addWidgetPrefix(BOTTOM_LOAD_PANEL_CLASS)) - .append(that._createComponent($('
'), LoadIndicator).$element()) - .appendTo($contentElement); + switch (changeType) { + case 'prepend': + that._items.unshift.apply(that._items, items); + if (removeCount) { + that._items.splice(-removeCount); } - } else if (bottomLoadPanelElement) { - bottomLoadPanelElement.remove(); - } - }, + break; + case 'append': + that._items.push.apply(that._items, items); + if (removeCount) { + that._items.splice(0, removeCount); + } + break; + default: + super._applyChange(change); + break; + } + } - _handleScroll(e) { - const legacyScrollingMode = this.option(LEGACY_SCROLLING_MODE) === true; - const zeroTopPosition = e.scrollOffset.top === 0; - const isScrollTopChanged = this._scrollTop !== e.scrollOffset.top; - const hasScrolled = isScrollTopChanged || e.forceUpdateScrollPosition; - const isValidScrollTarget = this._hasHeight || !legacyScrollingMode && zeroTopPosition; + items(allItems?) { + return allItems ? this._allItems || this._items : this._visibleItems || this._items; + } - if (hasScrolled && isValidScrollTarget && this._rowHeight) { - this._scrollTop = e.scrollOffset.top; - const isVirtualRowRendering = isVirtualMode(this) || this.option('scrolling.rowRenderingMode') !== 'standard'; + getRowIndexDelta() { + let delta = 0; - if (isVirtualRowRendering && this.option(LEGACY_SCROLLING_MODE) === false) { - this._updateContentItemSizes(); - this._updateViewportSize(null, this._scrollTop); - } + if (this.option(LEGACY_SCROLLING_MODE)) { + const visibleItems = this._visibleItems; - this._dataController.setViewportPosition(e.scrollOffset.top); + if (visibleItems && visibleItems[0]) { + delta = this._items.indexOf(visibleItems[0]); } - this.callBase.apply(this, arguments); - }, + } - _needUpdateRowHeight(itemsCount) { - return this.callBase.apply(this, arguments) || (itemsCount > 0 - && (isAppendMode(this) && !gridCoreUtils.isVirtualRowRendering(this)) - ); - }, + return delta < 0 ? 0 : delta; + } - _updateRowHeight() { - this.callBase.apply(this, arguments); + getRowIndexOffset(byLoadedRows?, needGroupOffset?) { + let offset = 0; + const dataSource = this.dataSource(); + const rowsScrollController = this._rowsScrollController; + const newMode = this.option(LEGACY_SCROLLING_MODE) === false; + const virtualPaging = isVirtualPaging(this); + + if (rowsScrollController && !byLoadedRows) { + if (newMode && isDefined(this._loadViewportParams)) { + const { skipForCurrentPage, pageIndex } = this.getLoadPageParams(true); + const items = this.items(true); + offset = virtualPaging ? pageIndex * this.pageSize() : 0; + if (items.length) { + const firstLoadIndex = items[0].loadIndex; + offset += items.filter((item) => item.loadIndex < firstLoadIndex + skipForCurrentPage).length; + } + } else { + offset = rowsScrollController.beginPageIndex() * rowsScrollController.pageSize(); + } + } else if (virtualPaging && newMode && dataSource) { + const lastLoadOptions = dataSource.lastLoadOptions(); - if (this._rowHeight) { - this._updateContentPosition(); + if (needGroupOffset && lastLoadOptions.skips?.length) { + offset = lastLoadOptions.skips.reduce((res: number, skip: number) => res + skip, 0); + } else { + offset = lastLoadOptions.skip ?? 0; + } + } else if (isVirtualMode(this) && dataSource) { + offset = dataSource.beginPageIndex() * dataSource.pageSize(); + } - const viewportHeight = this._hasHeight ? getOuterHeight(this.element()) : getOuterHeight(getWindow()); - const dataController = this._dataController; + return offset; + } - if (this.option(LEGACY_SCROLLING_MODE) === false) { - this._updateViewportSize(viewportHeight); - dataController.updateViewport(); - } else { - dataController.viewportSize(Math.ceil(viewportHeight / this._rowHeight)); - } - } - }, + getDataIndex() { + if (this.option(LEGACY_SCROLLING_MODE) === false) { + return this.getRowIndexOffset(true, true); + } - updateFreeSpaceRowHeight() { - const result = this.callBase.apply(this, arguments); + return super.getDataIndex.apply(this, arguments as any); + } - if (result) { - this._updateContentPosition(); - } + viewportSize() { + const rowsScrollController = this._rowsScrollController; + const dataSource = this._dataSource; + const result = rowsScrollController?.viewportSize.apply(rowsScrollController, arguments); + if (this.option(LEGACY_SCROLLING_MODE) === false) { return result; - }, - - setLoading(isLoading, messageText) { - const dataController = this._dataController; - const hasBottomLoadPanel = dataController.pageIndex() > 0 && dataController.isLoaded() && !!this._findBottomLoadPanel(); + } - if (this.option(LEGACY_SCROLLING_MODE) === false && isLoading && dataController.isViewportChanging()) { - return; - } + return dataSource?.viewportSize.apply(dataSource, arguments); + } - if (hasBottomLoadPanel) { - isLoading = false; - } + viewportHeight(height, scrollTop) { + this._rowsScrollController?.viewportHeight(height, scrollTop); + } - this.callBase.call(this, isLoading, messageText); - }, + viewportItemSize() { + const rowsScrollController = this._rowsScrollController; + const dataSource = this._dataSource; + const result = rowsScrollController?.viewportItemSize.apply(rowsScrollController, arguments); - // NOTE: warning won't be thrown if height was specified and then removed, - // because for some reason `_hasHeight` is not updated properly in this case - throwHeightWarningIfNeed() { - if (this._hasHeight === undefined) { - return; - } + if (this.option(LEGACY_SCROLLING_MODE) === false) { + return result; + } - const needToThrow = !this._hasHeight && isVirtualPaging(this); - if (needToThrow && !this._heightWarningIsThrown) { - this._heightWarningIsThrown = true; - errors.log('W1025'); - } - }, + return dataSource?.viewportItemSize.apply(dataSource, arguments); + } - _resizeCore() { - const that = this; - const $element = that.element(); + setViewportPosition() { + const rowsScrollController = this._rowsScrollController; + const dataSource = this._dataSource; + this._isPaging = false; - that.callBase(); + if (rowsScrollController) { + rowsScrollController.setViewportPosition.apply(rowsScrollController, arguments); + } else { + dataSource?.setViewportPosition.apply(dataSource, arguments); + } + } - this.throwHeightWarningIfNeed(); + setContentItemSizes(sizes) { + const rowsScrollController = this._rowsScrollController; + const dataSource = this._dataSource; + const result = rowsScrollController?.setContentItemSizes(sizes); - if (that.component.$element() && !that._windowScroll && isElementInDom($element)) { - that._windowScroll = subscribeToExternalScrollers($element, (scrollPos) => { - if (!that._hasHeight && that._rowHeight) { - that._dataController.setViewportPosition(scrollPos); - } - }, that.component.$element()); + if (this.option(LEGACY_SCROLLING_MODE) === false) { + return result; + } - that.on('disposing', () => { - that._windowScroll.dispose(); - }); - } + return dataSource?.setContentItemSizes(sizes); + } - if (this.option(LEGACY_SCROLLING_MODE) !== false) { - that.loadIfNeed(); - } - }, + getPreloadedRowCount() { + const preloadCount = this.option('scrolling.preloadedRowCount'); + const preloadEnabled = this.option('scrolling.preloadEnabled'); - loadIfNeed() { - const dataController = this._dataController; - dataController?.loadIfNeed?.(); - }, + if (isDefined(preloadCount)) { + return preloadCount; + } - _restoreErrorRow() { - if (this.option(LEGACY_SCROLLING_MODE) === false) { - const errorHandling = this.getController('errorHandling'); - errorHandling?.removeErrorRow(); - } + const viewportSize = this.viewportSize(); - this.callBase.apply(this, arguments); - }, + return preloadEnabled ? 2 * viewportSize : viewportSize; + } - dispose() { - clearTimeout(this._scrollTimeoutID); - this.callBase(); - }, - }; -}()); + getLoadPageParams(byLoadedPage?) { + const pageSize = this.pageSize(); + const viewportParams = this._loadViewportParams; + const lastLoadOptions = this._dataSource?.lastLoadOptions(); + const loadedPageIndex = lastLoadOptions?.pageIndex || 0; + const loadedTake = lastLoadOptions?.take || 0; + + const isScrollingBack = this._rowsScrollController.isScrollingBack(); + const topPreloadCount = isScrollingBack ? this.getPreloadedRowCount() : 0; + const bottomPreloadCount = isScrollingBack ? 0 : this.getPreloadedRowCount(); + const totalCountCorrection = this._dataSource?.totalCountCorrection() || 0; + const skipWithPreload = Math.max(0, viewportParams.skip - topPreloadCount); + const pageIndex = byLoadedPage ? loadedPageIndex : Math.floor(pageSize ? skipWithPreload / pageSize : 0); + const pageOffset = pageIndex * pageSize; + const skipForCurrentPage = viewportParams.skip - pageOffset; + const loadingTake = viewportParams.take + skipForCurrentPage + bottomPreloadCount - totalCountCorrection; + const take = byLoadedPage ? loadedTake : loadingTake; + const loadPageCount = Math.ceil(pageSize ? take / pageSize : 0); -export const virtualScrollingModule = { - defaultOptions() { return { - scrolling: { - timeout: 300, - updateTimeout: 300, - minTimeout: 0, - renderingThreshold: 100, - removeInvisiblePages: true, - rowPageSize: 5, - prerenderedRowChunkSize: 1, - mode: 'standard', - preloadEnabled: false, - rowRenderingMode: 'standard', - loadTwoPagesOnStart: false, - legacyMode: false, - prerenderedRowCount: 1, - }, + pageIndex, + loadPageCount: Math.max(1, loadPageCount), + skipForCurrentPage: Math.max(0, skipForCurrentPage), }; - }, - extenders: { - controllers: { - data: (function () { - const members = { - _refreshDataSource() { - // @ts-expect-error - const baseResult = this.callBase.apply(this, arguments) || new Deferred().resolve().promise(); - baseResult.done(this.initVirtualRows.bind(this)); - return baseResult; - }, - _loadDataSource() { - if (this._rowsScrollController && isVirtualPaging(this)) { - const { loadPageCount } = isDefined(this._loadViewportParams) ? this.getLoadPageParams() : { loadPageCount: undefined }; - - loadPageCount >= 1 && this._dataSource?.loadPageCount(loadPageCount); - } + } - return this.callBase.apply(this, arguments); - }, - getRowPageSize() { - const rowPageSize = this.option('scrolling.rowPageSize'); - const pageSize = this.pageSize(); - - return pageSize && pageSize < rowPageSize ? pageSize : rowPageSize; - }, - reload() { - const rowsScrollController = this._rowsScrollController || this._dataSource; - const itemIndex = rowsScrollController && rowsScrollController.getItemIndexByPosition(); - const result = this.callBase.apply(this, arguments); - return result && result.done(() => { - if (isVirtualMode(this) || gridCoreUtils.isVirtualRowRendering(this)) { - const rowIndexOffset = this.getRowIndexOffset(); - const rowIndex = Math.floor(itemIndex) - rowIndexOffset; - const { component } = this; - const scrollable = component.getScrollable && component.getScrollable(); - const isSortingOperation = this.dataSource().operationTypes().sorting; - - if (scrollable && !isSortingOperation && rowIndex >= 0) { - const rowElement = component.getRowElement(rowIndex); - const $rowElement = rowElement && rowElement[0] && $(rowElement[0]); - let top = $rowElement && $rowElement.position().top; - const isChromeLatest = browser.chrome && Number(browser.version ?? 0) >= 91; - const allowedTopOffset = browser.mozilla || isChromeLatest ? 1 : 0; // T884308 - if (top > allowedTopOffset) { - top = Math.round(top + getOuterHeight($rowElement) * (itemIndex % 1)); - scrollable.scrollTo({ y: top }); - } - } - } - }); - }, - initVirtualRows() { - const virtualRowsRendering = gridCoreUtils.isVirtualRowRendering(this); - - this._allItems = null; - this._loadViewportParams = null; - - if (this.option('scrolling.mode') !== 'virtual' && !virtualRowsRendering || !virtualRowsRendering || this.option(LEGACY_SCROLLING_MODE) !== false && !this.option('scrolling.rowPageSize')) { - this._visibleItems = null; - this._rowsScrollController = null; - return; - } + _updateVisiblePageIndex(currentPageIndex?) { + if (!this._rowsScrollController) { + return; + } + if (isDefined(currentPageIndex)) { + this._silentOption(VISIBLE_PAGE_INDEX, currentPageIndex); + this.pageChanged.fire(); + return; + } - const pageIndex = !isVirtualMode(this) && this.pageIndex() >= this.pageCount() ? this.pageCount() - 1 : this.pageIndex(); - this._rowPageIndex = Math.ceil(pageIndex * this.pageSize() / this.getRowPageSize()); - this._visibleItems = this.option(LEGACY_SCROLLING_MODE) === false ? null : []; - this._viewportChanging = false; - this._needUpdateViewportAfterLoading = false; - - if (!this._rowsScrollController) { - this._rowsScrollController = new VirtualScrollController(this.component, this._getRowsScrollDataOptions(), true); - - this._rowsScrollController.positionChanged.add(() => { - if (this.option(LEGACY_SCROLLING_MODE) === false) { - this._viewportChanging = true; - this.loadViewport(); - this._viewportChanging = false; - return; - } - this._dataSource?.setViewportItemIndex(this._rowsScrollController.getViewportItemIndex()); - }); - } + const viewPortItemIndex = this._rowsScrollController.getViewportItemIndex(); + const newPageIndex = Math.floor(viewPortItemIndex / this.pageSize()); - if (this.option(LEGACY_SCROLLING_MODE) === false) { - this._updateLoadViewportParams(); - } + if (this.pageIndex() !== newPageIndex) { + this._silentOption(VISIBLE_PAGE_INDEX, newPageIndex); + this.updateItems({ + changeType: 'pageIndex', + }); + } + } - if (this.isLoaded() && this.option(LEGACY_SCROLLING_MODE) !== false) { - this._rowsScrollController.load(); - } - }, - isViewportChanging() { - return this._viewportChanging; - }, - _getRowsScrollDataOptions() { - const that = this; - const isItemCountable = function (item) { - return isItemCountableByDataSource(item, that._dataSource); - }; - - return { - pageSize() { - return that.getRowPageSize(); - }, - loadedOffset() { - return isVirtualMode(that) && that._dataSource?.lastLoadOptions().skip || 0; - }, - loadedItemCount() { - return that._itemCount; - }, - totalItemsCount() { - if (isVirtualPaging(that)) { - return that.totalItemsCount(); - } - - return that.option(LEGACY_SCROLLING_MODE) === false ? that._itemCount : that._items.filter(isItemCountable).length; - }, - hasKnownLastPage() { - return that.option(LEGACY_SCROLLING_MODE) === false ? that.hasKnownLastPage() : true; - }, - pageIndex(index) { - if (index !== undefined) { - that._rowPageIndex = index; - } - return that._rowPageIndex; - }, - isLoading() { - return that.isLoading(); - }, - pageCount() { - const pageCount = Math.ceil(this.totalItemsCount() / this.pageSize()); - return pageCount || 1; - }, - load() { - if (that._rowsScrollController.pageIndex() >= this.pageCount()) { - that._rowPageIndex = this.pageCount() - 1; - that._rowsScrollController.pageIndex(that._rowPageIndex); - } - - if (!this.items().length && this.totalItemsCount()) return; - - that._rowsScrollController.handleDataChanged((change) => { - change = change || {}; - change.changeType = change.changeType || 'refresh'; - change.items = change.items || that._visibleItems; - - that._visibleItems.forEach((item, index) => { - item.rowIndex = index; - }); - that._fireChanged(change); - }); - }, - updateLoading() { - }, - itemsCount() { - return this.items(true).length; - }, - correctCount(items, count, fromEnd) { - return correctCount(items, count, fromEnd, (item, isNextAfterLast, fromEnd) => { - if (item.isNewRow) { - return isNextAfterLast && !fromEnd; - } - - if (isNextAfterLast && fromEnd) { - return !item.isNewRow; - } - - return isItemCountable(item); - }); - }, - items(countableOnly) { - let result = that._items; - - if (that.option(LEGACY_SCROLLING_MODE)) { - const dataSource = that.dataSource(); - const virtualItemsCount = dataSource?.virtualItemsCount(); - const begin = virtualItemsCount ? virtualItemsCount.begin : 0; - const rowPageSize = that.getRowPageSize(); - - let skip = that._rowPageIndex * rowPageSize - begin; - let take = rowPageSize; - - if (skip < 0) { - return []; - } - - if (skip) { - skip = this.correctCount(result, skip); - result = result.slice(skip); - } - if (take) { - take = this.correctCount(result, take); - result = result.slice(0, take); - } - } - - return countableOnly ? result.filter(isItemCountable) : result; - }, - viewportItems(items) { - if (items && that.option(LEGACY_SCROLLING_MODE) !== false) { - that._visibleItems = items; - } - return that._visibleItems; - }, - onChanged() { - }, - changingDuration() { - const dataSource = that.dataSource(); - - if (dataSource?.isLoading() && that.option(LEGACY_SCROLLING_MODE) !== false) { - return LOAD_TIMEOUT; - } - - return dataSource?._renderTime || 0; - }, - }; - }, - _updateItemsCore(change) { - const delta = this.getRowIndexDelta(); - - this.callBase.apply(this, arguments); - if (this.option(LEGACY_SCROLLING_MODE) === false && gridCoreUtils.isVirtualRowRendering(this)) { - if (change.changeType === 'update' && change.rowIndices.length === 0 && change.cancelEmptyChanges) { - change.cancel = true; - } - return; - } + _getChangedLoadParams() { + const loadedPageParams = this.getLoadPageParams(true); + const { pageIndex, loadPageCount } = this.getLoadPageParams(); + const pageIndexIsValid = this._pageIndexIsValid(pageIndex); + let result: any = null; + + if (!this._isLoading && pageIndexIsValid && (pageIndex !== loadedPageParams.pageIndex || loadPageCount !== loadedPageParams.loadPageCount)) { + result = { + pageIndex, + loadPageCount, + }; + } + return result; + } - const rowsScrollController = this._rowsScrollController; - - if (rowsScrollController) { - const visibleItems = this._visibleItems; - const isRefresh = change.changeType === 'refresh' || change.isLiveUpdate; - - if (change.changeType === 'append' && change.items && !change.items.length) return; - - if (isRefresh || change.changeType === 'append' || change.changeType === 'prepend') { - change.cancel = true; - isRefresh && rowsScrollController.reset(true); - rowsScrollController.load(); - } else { - if (change.changeType === 'update') { - change.rowIndices.forEach((rowIndex, index) => { - const changeType = change.changeTypes[index]; - const newItem = change.items[index]; - if (changeType === 'update') { - visibleItems[rowIndex] = newItem; - } else if (changeType === 'insert') { - visibleItems.splice(rowIndex, 0, newItem); - } else if (changeType === 'remove') { - visibleItems.splice(rowIndex, 1); - } - }); - } else { - visibleItems.forEach((item, index) => { - visibleItems[index] = this._items[index + delta] || visibleItems[index]; - }); - change.items = visibleItems; - } - - updateItemIndices(visibleItems); - } - } - }, - _updateLoadViewportParams() { - const viewportParams = this._rowsScrollController.getViewportParams(); - const pageSize = this.pageSize(); - - if (viewportParams && !isVirtualPaging(this) && pageSize > 0) { - const pageOffset = this.pageIndex() * pageSize; - viewportParams.skip += pageOffset; - } - this._loadViewportParams = viewportParams; - }, - _processItems() { - const resultItems = this.callBase.apply(this, arguments); - - if (this.option(LEGACY_SCROLLING_MODE) === false) { - const dataSource = this._dataSource; - let currentIndex = dataSource?.lastLoadOptions().skip ?? 0; - let prevCountable; - let prevRowType; - let isPrevRowNew; - let wasCountableItem = false; - let newRows: any = []; - resultItems.forEach((item) => { - const { rowType } = item; - const itemCountable = isItemCountableByDataSource(item, dataSource); - - const isNextGroupItem = rowType === 'group' && (prevCountable || itemCountable || (prevRowType !== 'group' && currentIndex > 0)); - const isNextDataItem = rowType === 'data' && itemCountable && (prevCountable || prevRowType !== 'group'); - - if (!item.isNewRow && isDefined(prevCountable)) { - const isPrevNewRowFirst = isPrevRowNew && !wasCountableItem; - if ((isNextGroupItem || isNextDataItem) && !isPrevNewRowFirst) { - currentIndex++; - } - } - - if (isNextGroupItem || isNextDataItem) { - wasCountableItem = true; - } - if (item.isNewRow) { - newRows.push(item); - } else { - newRows.forEach((it) => { it.loadIndex = currentIndex; }); - newRows = []; - } - item.loadIndex = currentIndex; - prevCountable = itemCountable; - prevRowType = rowType; - isPrevRowNew = item.isNewRow; - }); - newRows.forEach((it) => { it.loadIndex = currentIndex; }); - } + _pageIndexIsValid(pageIndex) { + let result = true; - return resultItems; - }, - _afterProcessItems(items) { - this._itemCount = items.filter((item) => isItemCountableByDataSource(item, this._dataSource)).length; - if (isDefined(this._loadViewportParams)) { - this._updateLoadViewportParams(); - - let result = items; - this._allItems = items; - - if (items.length) { - const { skipForCurrentPage } = this.getLoadPageParams(true); - const skip = items[0].loadIndex + skipForCurrentPage; - const { take } = this._loadViewportParams; - result = items.filter((it) => { - const isNewRowInEmptyData = it.isNewRow && it.loadIndex === skip && take === 0; - const isLoadIndexGreaterStart = it.loadIndex >= skip; - const isLoadIndexLessEnd = it.loadIndex < skip + take || isNewRowInEmptyData; - return isLoadIndexGreaterStart && isLoadIndexLessEnd; - }); - } - - return result; - } + if (isAppendMode(this) && this.hasKnownLastPage() || isVirtualMode(this)) { + result = pageIndex * this.pageSize() < this.totalItemsCount(); + } - return this.callBase.apply(this, arguments); - }, - _applyChange(change) { - const that = this; - const { items } = change; - const { changeType } = change; - let { removeCount } = change; + return result; + } - if (removeCount) { - const fromEnd = changeType === 'prepend'; - removeCount = correctCount(that._items, removeCount, fromEnd, (item, isNextAfterLast) => item.rowType === 'data' && !item.isNewRow || (item.rowType === 'group' && (that._dataSource.isGroupItemCountable(item.data) || isNextAfterLast))); + _loadItems(checkLoading, viewportIsFilled) { + const virtualPaging = isVirtualPaging(this); + const dataSourceAdapter = this._dataSource; + const changedParams = this._getChangedLoadParams(); + const currentLoadPageCount = dataSourceAdapter?.loadPageCount() ?? 0; + const lastRequiredItemCount = this.pageSize() * currentLoadPageCount; + const currentPageIndex = dataSourceAdapter?.pageIndex() ?? 0; + const pageIndexNotChanged = changedParams?.pageIndex === currentPageIndex; + const allLoadedInAppendMode = isAppendMode(this) && this.totalItemsCount() < lastRequiredItemCount; + const isRepaintMode = this.option('editing.refreshMode') === 'repaint'; + const pageIndexIncreased = changedParams?.pageIndex > currentPageIndex; + let result = false; + + if (!dataSourceAdapter || (virtualPaging && checkLoading && ((isRepaintMode && viewportIsFilled) || (pageIndexIncreased || pageIndexNotChanged && allLoadedInAppendMode)))) { + return result; + } - change.removeCount = removeCount; - } + if (virtualPaging && this._isLoading) { + this._needUpdateViewportAfterLoading = true; + } - switch (changeType) { - case 'prepend': - that._items.unshift.apply(that._items, items); - if (removeCount) { - that._items.splice(-removeCount); - } - break; - case 'append': - that._items.push.apply(that._items, items); - if (removeCount) { - that._items.splice(0, removeCount); - } - break; - default: - that.callBase(change); - break; - } - }, - items(allItems) { - return allItems ? this._allItems || this._items : this._visibleItems || this._items; - }, - getRowIndexDelta() { - let delta = 0; - - if (this.option(LEGACY_SCROLLING_MODE)) { - const visibleItems = this._visibleItems; - - if (visibleItems && visibleItems[0]) { - delta = this._items.indexOf(visibleItems[0]); - } - } + if (virtualPaging && changedParams) { + result = true; + dataSourceAdapter.pageIndex(changedParams.pageIndex); + dataSourceAdapter.loadPageCount(changedParams.loadPageCount); + this._repaintChangesOnly = true; + this._needUpdateDimensions = true; + const viewportChanging = this._viewportChanging; + this.load().always(() => { + this._repaintChangesOnly = undefined; + this._needUpdateDimensions = undefined; + }).done(() => { + const isLastPage = this.pageCount() > 0 && this.pageIndex() === this.pageCount() - 1; + (viewportChanging || isLastPage) && this._updateVisiblePageIndex(); + if (this._needUpdateViewportAfterLoading) { + this._needUpdateViewportAfterLoading = false; + this.loadViewport({ checkLoadedParamsOnly: true }); + } + }); + } - return delta < 0 ? 0 : delta; - }, - getRowIndexOffset(byLoadedRows, needGroupOffset) { - let offset = 0; - const dataSource = this.dataSource(); - const rowsScrollController = this._rowsScrollController; - const newMode = this.option(LEGACY_SCROLLING_MODE) === false; - const virtualPaging = isVirtualPaging(this); - - if (rowsScrollController && !byLoadedRows) { - if (newMode && isDefined(this._loadViewportParams)) { - const { skipForCurrentPage, pageIndex } = this.getLoadPageParams(true); - const items = this.items(true); - offset = virtualPaging ? pageIndex * this.pageSize() : 0; - if (items.length) { - const firstLoadIndex = items[0].loadIndex; - offset += items.filter((item) => item.loadIndex < firstLoadIndex + skipForCurrentPage).length; - } - } else { - offset = rowsScrollController.beginPageIndex() * rowsScrollController.pageSize(); - } - } else if (virtualPaging && newMode && dataSource) { - const lastLoadOptions = dataSource.lastLoadOptions(); - - if (needGroupOffset && lastLoadOptions.skips?.length) { - offset = lastLoadOptions.skips.reduce((res: number, skip: number) => res + skip, 0); - } else { - offset = lastLoadOptions.skip ?? 0; - } - } else if (isVirtualMode(this) && dataSource) { - offset = dataSource.beginPageIndex() * dataSource.pageSize(); - } + return result; + } - return offset; - }, - getDataIndex() { - if (this.option(LEGACY_SCROLLING_MODE) === false) { - return this.getRowIndexOffset(true, true); - } + loadViewport(params?) { + const { checkLoadedParamsOnly, checkLoading, viewportIsNotFilled } = params ?? {}; + const virtualPaging = isVirtualPaging(this); + + if (virtualPaging || gridCoreUtils.isVirtualRowRendering(this)) { + this._updateLoadViewportParams(); + + const loadingItemsStarted = this._loadItems(checkLoading, !viewportIsNotFilled); + const isCustomLoading = this._dataSource?.isCustomLoading(); + const isLoading = checkLoading && !isCustomLoading && this._isLoading; + const needToUpdateItems = !(loadingItemsStarted + || isLoading + || checkLoadedParamsOnly); + + if (needToUpdateItems) { + const noPendingChangesInEditing = !this.getController('editing')?.getChanges()?.length; + this.updateItems({ + repaintChangesOnly: true, + needUpdateDimensions: true, + useProcessedItemsCache: noPendingChangesInEditing, + cancelEmptyChanges: true, + }); + } + } + } - return this.callBase.apply(this, arguments); - }, - viewportSize() { - const rowsScrollController = this._rowsScrollController; - const dataSource = this._dataSource; - const result = rowsScrollController?.viewportSize.apply(rowsScrollController, arguments); + updateViewport() { + const viewportSize = this.viewportSize(); + const itemCount = this.items().length; + const viewportIsNotFilled = viewportSize > itemCount; + const currentTake = this._loadViewportParams?.take ?? 0; + const rowsScrollController = this._rowsScrollController; + const newTake = rowsScrollController?.getViewportParams().take; + + (viewportIsNotFilled || currentTake < newTake) && !this._isPaging && itemCount && this.loadViewport({ + checkLoading: true, + viewportIsNotFilled, + }); + } - if (this.option(LEGACY_SCROLLING_MODE) === false) { - return result; - } + loadIfNeed() { + if (this.option(LEGACY_SCROLLING_MODE) === false) { + return; + } - return dataSource?.viewportSize.apply(dataSource, arguments); - }, - viewportHeight(height, scrollTop) { - this._rowsScrollController?.viewportHeight(height, scrollTop); - }, - viewportItemSize() { - const rowsScrollController = this._rowsScrollController; - const dataSource = this._dataSource; - const result = rowsScrollController?.viewportItemSize.apply(rowsScrollController, arguments); - - if (this.option(LEGACY_SCROLLING_MODE) === false) { - return result; - } + const rowsScrollController = this._rowsScrollController; + rowsScrollController && rowsScrollController.loadIfNeed(); - return dataSource?.viewportItemSize.apply(dataSource, arguments); - }, - setViewportPosition() { - const rowsScrollController = this._rowsScrollController; - const dataSource = this._dataSource; - this._isPaging = false; - - if (rowsScrollController) { - rowsScrollController.setViewportPosition.apply(rowsScrollController, arguments); - } else { - dataSource?.setViewportPosition.apply(dataSource, arguments); - } - }, - setContentItemSizes(sizes) { - const rowsScrollController = this._rowsScrollController; - const dataSource = this._dataSource; - const result = rowsScrollController?.setContentItemSizes(sizes); - - if (this.option(LEGACY_SCROLLING_MODE) === false) { - return result; - } + const dataSource = this._dataSource; + return dataSource && dataSource.loadIfNeed(); + } - return dataSource?.setContentItemSizes(sizes); - }, - getPreloadedRowCount() { - const preloadCount = this.option('scrolling.preloadedRowCount'); - const preloadEnabled = this.option('scrolling.preloadEnabled'); + getItemSize() { + const rowsScrollController = this._rowsScrollController; - if (isDefined(preloadCount)) { - return preloadCount; - } + if (rowsScrollController) { + return rowsScrollController.getItemSize.apply(rowsScrollController, arguments); + } - const viewportSize = this.viewportSize(); - - return preloadEnabled ? 2 * viewportSize : viewportSize; - }, - getLoadPageParams(byLoadedPage) { - const pageSize = this.pageSize(); - const viewportParams = this._loadViewportParams; - const lastLoadOptions = this._dataSource?.lastLoadOptions(); - const loadedPageIndex = lastLoadOptions?.pageIndex || 0; - const loadedTake = lastLoadOptions?.take || 0; - - const isScrollingBack = this._rowsScrollController.isScrollingBack(); - const topPreloadCount = isScrollingBack ? this.getPreloadedRowCount() : 0; - const bottomPreloadCount = isScrollingBack ? 0 : this.getPreloadedRowCount(); - const totalCountCorrection = this._dataSource?.totalCountCorrection() || 0; - const skipWithPreload = Math.max(0, viewportParams.skip - topPreloadCount); - const pageIndex = byLoadedPage ? loadedPageIndex : Math.floor(pageSize ? skipWithPreload / pageSize : 0); - const pageOffset = pageIndex * pageSize; - const skipForCurrentPage = viewportParams.skip - pageOffset; - const loadingTake = viewportParams.take + skipForCurrentPage + bottomPreloadCount - totalCountCorrection; - const take = byLoadedPage ? loadedTake : loadingTake; - const loadPageCount = Math.ceil(pageSize ? take / pageSize : 0); - - return { - pageIndex, - loadPageCount: Math.max(1, loadPageCount), - skipForCurrentPage: Math.max(0, skipForCurrentPage), - }; - }, - _updateVisiblePageIndex(currentPageIndex) { - if (!this._rowsScrollController) { - return; - } - if (isDefined(currentPageIndex)) { - this._silentOption(VISIBLE_PAGE_INDEX, currentPageIndex); - this.pageChanged.fire(); - return; - } + const dataSource = this._dataSource; + return dataSource && dataSource.getItemSize.apply(dataSource, arguments); + } - const viewPortItemIndex = this._rowsScrollController.getViewportItemIndex(); - const newPageIndex = Math.floor(viewPortItemIndex / this.pageSize()); + getItemSizes() { + const rowsScrollController = this._rowsScrollController; - if (this.pageIndex() !== newPageIndex) { - this._silentOption(VISIBLE_PAGE_INDEX, newPageIndex); - this.updateItems({ - changeType: 'pageIndex', - }); - } - }, - _getChangedLoadParams() { - const loadedPageParams = this.getLoadPageParams(true); - const { pageIndex, loadPageCount } = this.getLoadPageParams(); - const pageIndexIsValid = this._pageIndexIsValid(pageIndex); - let result: any = null; - - if (!this._isLoading && pageIndexIsValid && (pageIndex !== loadedPageParams.pageIndex || loadPageCount !== loadedPageParams.loadPageCount)) { - result = { - pageIndex, - loadPageCount, - }; - } - return result; - }, - _pageIndexIsValid(pageIndex) { - let result = true; + if (rowsScrollController) { + return rowsScrollController.getItemSizes.apply(rowsScrollController, arguments); + } - if (isAppendMode(this) && this.hasKnownLastPage() || isVirtualMode(this)) { - result = pageIndex * this.pageSize() < this.totalItemsCount(); - } + const dataSource = this._dataSource; + return dataSource && dataSource.getItemSizes.apply(dataSource, arguments); + } - return result; - }, - _loadItems(checkLoading, viewportIsFilled) { - const virtualPaging = isVirtualPaging(this); - const dataSourceAdapter = this._dataSource; - const changedParams = this._getChangedLoadParams(); - const currentLoadPageCount = dataSourceAdapter?.loadPageCount() ?? 0; - const lastRequiredItemCount = this.pageSize() * currentLoadPageCount; - const currentPageIndex = dataSourceAdapter?.pageIndex() ?? 0; - const pageIndexNotChanged = changedParams?.pageIndex === currentPageIndex; - const allLoadedInAppendMode = isAppendMode(this) && this.totalItemsCount() < lastRequiredItemCount; - const isRepaintMode = this.option('editing.refreshMode') === 'repaint'; - const pageIndexIncreased = changedParams?.pageIndex > currentPageIndex; - let result = false; - - if (!dataSourceAdapter || (virtualPaging && checkLoading && ((isRepaintMode && viewportIsFilled) || (pageIndexIncreased || pageIndexNotChanged && allLoadedInAppendMode)))) { - return result; - } + getContentOffset() { + const rowsScrollController = this._rowsScrollController; - if (virtualPaging && this._isLoading) { - this._needUpdateViewportAfterLoading = true; - } + if (rowsScrollController) { + return rowsScrollController.getContentOffset.apply(rowsScrollController, arguments); + } - if (virtualPaging && changedParams) { - result = true; - dataSourceAdapter.pageIndex(changedParams.pageIndex); - dataSourceAdapter.loadPageCount(changedParams.loadPageCount); - this._repaintChangesOnly = true; - this._needUpdateDimensions = true; - const viewportChanging = this._viewportChanging; - this.load().always(() => { - this._repaintChangesOnly = undefined; - this._needUpdateDimensions = undefined; - }).done(() => { - const isLastPage = this.pageCount() > 0 && this.pageIndex() === this.pageCount() - 1; - (viewportChanging || isLastPage) && this._updateVisiblePageIndex(); - if (this._needUpdateViewportAfterLoading) { - this._needUpdateViewportAfterLoading = false; - this.loadViewport({ checkLoadedParamsOnly: true }); - } - }); - } + const dataSource = this._dataSource; + return dataSource && dataSource.getContentOffset.apply(dataSource, arguments); + } - return result; - }, - loadViewport(params) { - const { checkLoadedParamsOnly, checkLoading, viewportIsNotFilled } = params ?? {}; - const virtualPaging = isVirtualPaging(this); - - if (virtualPaging || gridCoreUtils.isVirtualRowRendering(this)) { - this._updateLoadViewportParams(); - - const loadingItemsStarted = this._loadItems(checkLoading, !viewportIsNotFilled); - const isCustomLoading = this._dataSource?.isCustomLoading(); - const isLoading = checkLoading && !isCustomLoading && this._isLoading; - const needToUpdateItems = !(loadingItemsStarted - || isLoading - || checkLoadedParamsOnly); - - if (needToUpdateItems) { - const noPendingChangesInEditing = !this.getController('editing')?.getChanges()?.length; - this.updateItems({ - repaintChangesOnly: true, - needUpdateDimensions: true, - useProcessedItemsCache: noPendingChangesInEditing, - cancelEmptyChanges: true, - }); - } - } - }, - updateViewport() { - const viewportSize = this.viewportSize(); - const itemCount = this.items().length; - const viewportIsNotFilled = viewportSize > itemCount; - const currentTake = this._loadViewportParams?.take ?? 0; - const rowsScrollController = this._rowsScrollController; - const newTake = rowsScrollController?.getViewportParams().take; - - (viewportIsNotFilled || currentTake < newTake) && !this._isPaging && itemCount && this.loadViewport({ - checkLoading: true, - viewportIsNotFilled, - }); - }, - loadIfNeed() { - if (this.option(LEGACY_SCROLLING_MODE) === false) { - return; - } + refresh(options) { + const dataSource = this._dataSource; - const rowsScrollController = this._rowsScrollController; - rowsScrollController && rowsScrollController.loadIfNeed(); + if (dataSource && options && options.load && isAppendMode(this)) { + dataSource.resetCurrentTotalCount(); + } - const dataSource = this._dataSource; - return dataSource && dataSource.loadIfNeed(); - }, - getItemSize() { - const rowsScrollController = this._rowsScrollController; + return super.refresh.apply(this, arguments as any); + } - if (rowsScrollController) { - return rowsScrollController.getItemSize.apply(rowsScrollController, arguments); - } + dispose() { + const rowsScrollController = this._rowsScrollController; - const dataSource = this._dataSource; - return dataSource && dataSource.getItemSize.apply(dataSource, arguments); - }, - getItemSizes() { - const rowsScrollController = this._rowsScrollController; + rowsScrollController && rowsScrollController.dispose(); + super.dispose.apply(this, arguments as any); + } - if (rowsScrollController) { - return rowsScrollController.getItemSizes.apply(rowsScrollController, arguments); - } + topItemIndex() { + return this._loadViewportParams?.skip; + } - const dataSource = this._dataSource; - return dataSource && dataSource.getItemSizes.apply(dataSource, arguments); - }, - getContentOffset() { - const rowsScrollController = this._rowsScrollController; + bottomItemIndex() { + const viewportParams = this._loadViewportParams; + return viewportParams && viewportParams.skip + viewportParams.take; + } - if (rowsScrollController) { - return rowsScrollController.getContentOffset.apply(rowsScrollController, arguments); - } + virtualItemsCount() { + const rowsScrollController = this._rowsScrollController; - const dataSource = this._dataSource; - return dataSource && dataSource.getContentOffset.apply(dataSource, arguments); - }, - refresh(options) { - const dataSource = this._dataSource; + if (rowsScrollController) { + return rowsScrollController.virtualItemsCount.apply(rowsScrollController, arguments); + } - if (dataSource && options && options.load && isAppendMode(this)) { - dataSource.resetCurrentTotalCount(); - } + const dataSource = this._dataSource; + return dataSource?.virtualItemsCount.apply(dataSource, arguments); + } - return this.callBase.apply(this, arguments); - }, - dispose() { - const rowsScrollController = this._rowsScrollController; - - rowsScrollController && rowsScrollController.dispose(); - this.callBase.apply(this, arguments); - }, - topItemIndex() { - return this._loadViewportParams?.skip; - }, - bottomItemIndex() { - const viewportParams = this._loadViewportParams; - return viewportParams && viewportParams.skip + viewportParams.take; - }, - virtualItemsCount() { - const rowsScrollController = this._rowsScrollController; - - if (rowsScrollController) { - return rowsScrollController.virtualItemsCount.apply(rowsScrollController, arguments); - } + pageIndex(pageIndex?) { + const virtualPaging = isVirtualPaging(this); + const rowsScrollController = this._rowsScrollController; + if (this.option(LEGACY_SCROLLING_MODE) === false && virtualPaging && rowsScrollController) { + if (pageIndex === undefined) { + return this.option(VISIBLE_PAGE_INDEX) ?? 0; + } + } + return super.pageIndex.apply(this, arguments as any); + } - const dataSource = this._dataSource; - return dataSource?.virtualItemsCount.apply(dataSource, arguments); - }, - pageIndex(pageIndex) { - const virtualPaging = isVirtualPaging(this); - const rowsScrollController = this._rowsScrollController; - if (this.option(LEGACY_SCROLLING_MODE) === false && virtualPaging && rowsScrollController) { - if (pageIndex === undefined) { - return this.option(VISIBLE_PAGE_INDEX) ?? 0; - } - } - return this.callBase.apply(this, arguments); - }, - _fireChanged(e) { - this.callBase.apply(this, arguments); - - const { operationTypes } = e; - if (this.option(LEGACY_SCROLLING_MODE) === false && isVirtualPaging(this) && operationTypes) { - const { fullReload, pageIndex } = operationTypes; - - if (e.isDataChanged && !fullReload && pageIndex) { - this._updateVisiblePageIndex(this._dataSource.pageIndex()); - } - } - }, - _getPagingOptionValue(optionName) { - let result = this.callBase.apply(this, arguments); + _fireChanged(e) { + super._fireChanged.apply(this, arguments as any); - if (this.option(LEGACY_SCROLLING_MODE) === false && isVirtualPaging(this)) { - result = this[optionName](); - } + const { operationTypes } = e; + if (this.option(LEGACY_SCROLLING_MODE) === false && isVirtualPaging(this) && operationTypes) { + const { fullReload, pageIndex } = operationTypes; - return result; - }, - isEmpty() { - return this.option(LEGACY_SCROLLING_MODE) === false ? !this.items(true).length : this.callBase(this, arguments); - }, - isLastPageLoaded() { - let result = false; - - if (this.option(LEGACY_SCROLLING_MODE) === false && isVirtualPaging(this)) { - const { pageIndex, loadPageCount } = this.getLoadPageParams(true); - const pageCount = this.pageCount(); - - result = pageIndex + loadPageCount >= pageCount; - } else { - result = this.callBase.apply(this, arguments); - } + if (e.isDataChanged && !fullReload && pageIndex) { + this._updateVisiblePageIndex(this._dataSource.pageIndex()); + } + } + } - return result; - }, - reset() { - this._itemCount = 0; - this._allItems = null; - this.callBase.apply(this, arguments); - }, - _applyFilter(): Promise { - this._dataSource?.loadPageCount(1); - - return this.callBase.apply(this, arguments); - }, - }; - - gridCoreUtils.proxyMethod(members, 'getVirtualContentSize'); - gridCoreUtils.proxyMethod(members, 'setViewportItemIndex'); - - return members; - }()), - resizing: { - _updateMasterDataGridCore(masterDataGrid) { - return when(this.callBase.apply(this, arguments)).done((masterDataGridUpdated) => { - const isNewVirtualMode = isVirtualMode(masterDataGrid) && masterDataGrid.option(LEGACY_SCROLLING_MODE) === false; - - if (!masterDataGridUpdated && isNewVirtualMode) { - const scrollable = masterDataGrid.getScrollable(); - - if (scrollable) { - masterDataGrid.updateDimensions(); - } - } - }); - }, - - hasResizeTimeout() { - return !!this._resizeTimeout; - }, - - resize() { - const { callBase } = this; - let result; - - if (isVirtualMode(this) || gridCoreUtils.isVirtualRowRendering(this)) { - clearTimeout(this._resizeTimeout); - this._resizeTimeout = null; - - const diff = (new Date()) as any - this._lastTime; - const updateTimeout = this.option('scrolling.updateTimeout'); - - if (this._lastTime && diff < updateTimeout) { - // @ts-expect-error - result = new Deferred(); - this._resizeTimeout = setTimeout(() => { - this._resizeTimeout = null; - callBase.apply(this).done(result.resolve).fail(result.reject); - this._lastTime = new Date(); - }, updateTimeout); - this._lastTime = new Date(); - } else { - result = callBase.apply(this); - if (this._dataController.isLoaded()) { - this._lastTime = new Date(); - } - } - } else { - result = callBase.apply(this); - } + _getPagingOptionValue(optionName) { + let result = super._getPagingOptionValue.apply(this, arguments as any); + + if (this.option(LEGACY_SCROLLING_MODE) === false && isVirtualPaging(this)) { + result = this[optionName](); + } + + return result; + } + + isEmpty() { + return this.option(LEGACY_SCROLLING_MODE) === false ? !this.items(true).length : super.isEmpty.apply(this, arguments as any); + } + + isLastPageLoaded() { + let result = false; + + if (this.option(LEGACY_SCROLLING_MODE) === false && isVirtualPaging(this)) { + const { pageIndex, loadPageCount } = this.getLoadPageParams(true); + const pageCount = this.pageCount(); + + result = pageIndex + loadPageCount >= pageCount; + } else { + result = super.isLastPageLoaded.apply(this, arguments as any); + } + + return result; + } + + reset() { + this._itemCount = 0; + this._allItems = null; + super.reset.apply(this, arguments as any); + } + + _applyFilter(): Promise { + this._dataSource?.loadPageCount(1); + + return super._applyFilter.apply(this, arguments as any); + } + + getVirtualContentSize() { + return this._dataSource?.getVirtualContentSize.apply(this._dataSource, arguments as any); + } + + setViewportItemIndex() { + return this._dataSource?.setViewportItemIndex.apply(this._dataSource, arguments as any); + } +}; + +export const resizing = (Base: ModuleType) => class VirtualScrollingResizingControllerExtender extends Base { + _resizeTimeout: any; + + _lastTime: any; + + _updateMasterDataGridCore(masterDataGrid) { + // @ts-expect-error + return when(super._updateMasterDataGridCore.apply(this, arguments as any)).done((masterDataGridUpdated) => { + const isNewVirtualMode = isVirtualMode(masterDataGrid) && masterDataGrid.option(LEGACY_SCROLLING_MODE) === false; + + if (!masterDataGridUpdated && isNewVirtualMode) { + const scrollable = masterDataGrid.getScrollable(); + + if (scrollable) { + masterDataGrid.updateDimensions(); + } + } + }); + } + + hasResizeTimeout() { + return !!this._resizeTimeout; + } + + resize() { + let result; + + if (isVirtualMode(this) || gridCoreUtils.isVirtualRowRendering(this)) { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + + const diff = (new Date()) as any - this._lastTime; + const updateTimeout = this.option('scrolling.updateTimeout'); + + if (this._lastTime && diff < updateTimeout) { + // @ts-expect-error + result = new Deferred(); + this._resizeTimeout = setTimeout(() => { + this._resizeTimeout = null; + super.resize.apply(this).done(result.resolve).fail(result.reject); + this._lastTime = new Date(); + }, updateTimeout); + this._lastTime = new Date(); + } else { + result = super.resize.apply(this); + if (this._dataController.isLoaded()) { + this._lastTime = new Date(); + } + } + } else { + result = super.resize.apply(this); + } + return result; + } + + dispose() { + super.dispose.apply(this, arguments as any); + clearTimeout(this._resizeTimeout); + } +}; + +export const rowsView = (Base: ModuleType) => class VirtualScrollingRowsViewExtender extends Base { + _isFixedTableRendering: any; + + _heightWarningIsThrown: any; + + _windowScroll: any; + + _scrollTimeoutID: any; + + init() { + const dataController = this.getController('data'); + + super.init(); + + dataController.pageChanged.add((pageIndex) => { + const scrollTop = this._scrollTop; + + this.scrollToPage(pageIndex ?? dataController.pageIndex()); + + if (this.option(LEGACY_SCROLLING_MODE) === false && this._scrollTop === scrollTop) { + // @ts-expect-error + dataController.updateViewport(); + } + }); + + dataController.dataSourceChanged.add(() => { + !this._scrollTop && this._scrollToCurrentPageOnResize(); + }); + + // @ts-expect-error + dataController.stateLoaded?.add(() => { + this._scrollToCurrentPageOnResize(); + }); + + this._scrollToCurrentPageOnResize(); + } + + _scrollToCurrentPageOnResize() { + const dataController = this.getController('data'); + + if (dataController.pageIndex() > 0) { + const resizeHandler = () => { + this.resizeCompleted.remove(resizeHandler); + this.scrollToPage(dataController.pageIndex()); + }; + this.resizeCompleted.add(resizeHandler); + } + } + + scrollToPage(pageIndex) { + const that = this; + const dataController = that._dataController; + const pageSize = dataController ? dataController.pageSize() : 0; + let scrollPosition; + + if (isVirtualMode(that) || isAppendMode(that)) { + // @ts-expect-error + const itemSize = dataController.getItemSize(); + // @ts-expect-error + const itemSizes = dataController.getItemSizes(); + const itemIndex = pageIndex * pageSize; + + scrollPosition = itemIndex * itemSize; + + // eslint-disable-next-line no-restricted-syntax + for (const index in itemSizes) { + // eslint-disable-next-line radix + if (parseInt(index) < itemIndex) { + scrollPosition += itemSizes[index] - itemSize; + } + } + } else { + scrollPosition = 0; + } + + that.scrollTo({ y: scrollPosition, x: that._scrollLeft }); + } + + renderDelayedTemplates() { + this.waitAsyncTemplates().done(() => { + this._updateContentPosition(true); + }); + super.renderDelayedTemplates.apply(this, arguments as any); + } + + _renderCore(e) { + const startRenderTime: any = new Date(); + + const deferred = super._renderCore.apply(this, arguments as any); + + const dataSource = this._dataController._dataSource; + + if (dataSource && e) { + const itemCount = e.items ? e.items.length : 20; + // @ts-expect-error + const viewportSize = this._dataController.viewportSize() || 20; + + if (gridCoreUtils.isVirtualRowRendering(this) && itemCount > 0 && this.option(LEGACY_SCROLLING_MODE) !== false) { + dataSource._renderTime = ((new Date()) as any - startRenderTime) * viewportSize / itemCount; + } else { + dataSource._renderTime = ((new Date()) as any - startRenderTime); + } + } + return deferred; + } + + _getRowElements(tableElement) { + const $rows = super._getRowElements(tableElement); + + return $rows && $rows.not(`.${VIRTUAL_ROW_CLASS}`); + } + + _removeRowsElements(contentTable, removeCount, changeType) { + let rowElements = this._getRowElements(contentTable).toArray(); + if (changeType === 'append') { + rowElements = rowElements.slice(0, removeCount); + } else { + rowElements = rowElements.slice(-removeCount); + } + + const errorHandlingController = this.getController('errorHandling'); + rowElements.map((rowElement) => { + const $rowElement = $(rowElement); + // @ts-expect-error + errorHandlingController && errorHandlingController.removeErrorRow($rowElement.next()); + // @ts-expect-error + $rowElement.remove(); + }); + } + + _updateContent(tableElement, change) { + let $freeSpaceRowElements; + const contentElement = this._findContentElement(); + const changeType = change && change.changeType; + const d: any = Deferred(); + + const contentTable = contentElement.children().first(); + if (changeType === 'append' || changeType === 'prepend') { + this.waitAsyncTemplates().done(() => { + const $tBodies = this._getBodies(tableElement); + if ($tBodies.length === 1) { + this._getBodies(contentTable)[changeType === 'append' ? 'append' : 'prepend']($tBodies.children()); + } else { + $tBodies[changeType === 'append' ? 'appendTo' : 'prependTo'](contentTable); + } + + tableElement.remove(); + $freeSpaceRowElements = this._getFreeSpaceRowElements(contentTable); + removeEmptyRows($freeSpaceRowElements, FREESPACE_CLASS); + + if (change.removeCount) { + this._removeRowsElements(contentTable, change.removeCount, changeType); + } + + this._restoreErrorRow(contentTable); + d.resolve(); + }).fail(d.reject); + } else { + super._updateContent.apply(this, arguments as any).done(() => { + if (changeType === 'update') { + this._restoreErrorRow(contentTable); + } + d.resolve(); + }).fail(d.reject); + } + + return d.promise().done(() => { + this._updateBottomLoading(); + }); + } + + _addVirtualRow($table, isFixed, location, position) { + if (!position) return; + + let $virtualRow = this._createEmptyRow(VIRTUAL_ROW_CLASS, isFixed, position); + + $virtualRow = this._wrapRowIfNeed($table, $virtualRow); + + this._appendEmptyRow($table, $virtualRow, location); + } + + _updateContentItemSizes() { + const rowHeights = this._getRowHeights(); + const correctedRowHeights = this._correctRowHeights(rowHeights); + + // @ts-expect-error + this._dataController.setContentItemSizes(correctedRowHeights); + } + + _updateViewportSize(viewportHeight, scrollTop?) { + if (!isDefined(viewportHeight)) { + viewportHeight = this._hasHeight ? getOuterHeight(this.element()) : getOuterHeight(getWindow()); + } + + // @ts-expect-error + this._dataController.viewportHeight(viewportHeight, scrollTop); + } + + _getRowHeights() { + const isPopupEditMode = this.getController('editing')?.isPopupEditMode?.(); + + let rowElements = this._getRowElements(this._tableElement).toArray(); + + if (isPopupEditMode) { + rowElements = rowElements.filter((row) => !$(row).hasClass(ROW_INSERTED)); + } + + return rowElements.map((row) => getBoundingRect(row).height); + } + + _correctRowHeights(rowHeights) { + const dataController = this._dataController; + const dataSource = dataController._dataSource; + const correctedRowHeights: any = []; + const visibleRows = dataController.getVisibleRows(); + let itemSize = 0; + let firstCountableItem = true; + let lastLoadIndex: any = -1; + + for (let i = 0; i < rowHeights.length; i++) { + const currentItem = visibleRows[i]; + if (!isDefined(currentItem)) { + continue; + } + + if (this.option(LEGACY_SCROLLING_MODE) === false) { + if (lastLoadIndex >= 0 && lastLoadIndex !== currentItem.loadIndex) { + correctedRowHeights.push(itemSize); + itemSize = 0; + } + lastLoadIndex = currentItem.loadIndex; + } else if (isItemCountableByDataSource(currentItem, dataSource)) { + if (firstCountableItem) { + firstCountableItem = false; + } else { + correctedRowHeights.push(itemSize); + itemSize = 0; + } + } + + itemSize += rowHeights[i]; + } + itemSize > 0 && correctedRowHeights.push(itemSize); + return correctedRowHeights; + } + + _updateContentPosition(isRender?) { + const dataController = this._dataController; + const rowHeight = this._rowHeight || 20; + + // @ts-expect-error + dataController.viewportItemSize(rowHeight); + + if (isVirtualMode(this) || gridCoreUtils.isVirtualRowRendering(this)) { + if (!isRender) { + this._updateContentItemSizes(); + } + + // @ts-expect-error + const top = dataController.getContentOffset('begin'); + // @ts-expect-error + const bottom = dataController.getContentOffset('end'); + const $tables = this.getTableElements(); + const $virtualRows = $tables.children('tbody').children(`.${VIRTUAL_ROW_CLASS}`); + + removeEmptyRows($virtualRows, VIRTUAL_ROW_CLASS); + + $tables.each((index, element) => { + const isFixed = index > 0; + const prevFixed = this._isFixedTableRendering; + this._isFixedTableRendering = isFixed; + this._addVirtualRow($(element), isFixed, 'top', top); + this._addVirtualRow($(element), isFixed, 'bottom', bottom); + this._isFixedTableRendering = prevFixed; + }); + } + } + + _isTableLinesDisplaysCorrect(table) { + const hasColumnLines = table.find(`.${COLUMN_LINES_CLASS}`).length > 0; + return hasColumnLines === this.option('showColumnLines'); + } + + _isColumnElementsEqual($columns, $virtualColumns) { + let result = $columns.length === $virtualColumns.length; + + if (result) { + each($columns, (index, element) => { + if (element.style.width !== $virtualColumns[index].style.width) { + result = false; return result; - }, - dispose() { - this.callBase.apply(this, arguments); - clearTimeout(this._resizeTimeout); - }, + } + + return undefined; + }); + } + + return result; + } + + _getCellClasses(column) { + const classes: any = []; + const { cssClass } = column; + const isExpandColumn = column.command === 'expand'; + + cssClass && classes.push(cssClass); + isExpandColumn && classes.push(this.addWidgetPrefix(GROUP_SPACE_CLASS)); + + return classes; + } + + _findBottomLoadPanel($contentElement?) { + const $element = $contentElement || this.element(); + const $bottomLoadPanel = $element && $element.find(`.${this.addWidgetPrefix(BOTTOM_LOAD_PANEL_CLASS)}`); + if ($bottomLoadPanel && $bottomLoadPanel.length) { + return $bottomLoadPanel; + } + } + + _updateBottomLoading() { + const that = this; + const virtualMode = isVirtualMode(this); + const appendMode = isAppendMode(this); + const showBottomLoading = !that._dataController.hasKnownLastPage() && that._dataController.isLoaded() && (virtualMode || appendMode); + const $contentElement = that._findContentElement(); + const bottomLoadPanelElement = that._findBottomLoadPanel($contentElement); + + if (showBottomLoading) { + if (!bottomLoadPanelElement) { + $('
') + .addClass(that.addWidgetPrefix(BOTTOM_LOAD_PANEL_CLASS)) + .append(that._createComponent($('
'), LoadIndicator).$element()) + .appendTo($contentElement); + } + } else if (bottomLoadPanelElement) { + bottomLoadPanelElement.remove(); + } + } + + _handleScroll(e) { + const legacyScrollingMode = this.option(LEGACY_SCROLLING_MODE) === true; + const zeroTopPosition = e.scrollOffset.top === 0; + const isScrollTopChanged = this._scrollTop !== e.scrollOffset.top; + const hasScrolled = isScrollTopChanged || e.forceUpdateScrollPosition; + const isValidScrollTarget = this._hasHeight || !legacyScrollingMode && zeroTopPosition; + + if (hasScrolled && isValidScrollTarget && this._rowHeight) { + this._scrollTop = e.scrollOffset.top; + const isVirtualRowRendering = isVirtualMode(this) || this.option('scrolling.rowRenderingMode') !== 'standard'; + + if (isVirtualRowRendering && this.option(LEGACY_SCROLLING_MODE) === false) { + this._updateContentItemSizes(); + this._updateViewportSize(null, this._scrollTop); + } + + // @ts-expect-error + this._dataController.setViewportPosition(e.scrollOffset.top); + } + super._handleScroll.apply(this, arguments as any); + } + + _needUpdateRowHeight(itemsCount) { + return super._needUpdateRowHeight.apply(this, arguments as any) || (itemsCount > 0 + && (isAppendMode(this) && !gridCoreUtils.isVirtualRowRendering(this)) + ); + } + + _updateRowHeight() { + super._updateRowHeight.apply(this, arguments as any); + + if (this._rowHeight) { + this._updateContentPosition(); + + const viewportHeight = this._hasHeight ? getOuterHeight(this.element()) : getOuterHeight(getWindow()); + const dataController = this._dataController; + + if (this.option(LEGACY_SCROLLING_MODE) === false) { + this._updateViewportSize(viewportHeight); + // @ts-expect-error + dataController.updateViewport(); + } else { + // @ts-expect-error + dataController.viewportSize(Math.ceil(viewportHeight / this._rowHeight)); + } + } + } + + updateFreeSpaceRowHeight() { + const result = super.updateFreeSpaceRowHeight.apply(this, arguments as any); + + // @ts-expect-error + if (result) { + this._updateContentPosition(); + } + + return result; + } + + setLoading(isLoading, messageText) { + const dataController = this._dataController; + const hasBottomLoadPanel = dataController.pageIndex() > 0 && dataController.isLoaded() && !!this._findBottomLoadPanel(); + + // @ts-expect-error + if (this.option(LEGACY_SCROLLING_MODE) === false && isLoading && dataController.isViewportChanging()) { + return; + } + + if (hasBottomLoadPanel) { + isLoading = false; + } + + super.setLoading.call(this, isLoading, messageText); + } + + // NOTE: warning won't be thrown if height was specified and then removed, + // because for some reason `_hasHeight` is not updated properly in this case + throwHeightWarningIfNeed() { + if (this._hasHeight === undefined) { + return; + } + + const needToThrow = !this._hasHeight && isVirtualPaging(this); + if (needToThrow && !this._heightWarningIsThrown) { + this._heightWarningIsThrown = true; + errors.log('W1025'); + } + } + + _resizeCore() { + const that = this; + const $element = that.element(); + + super._resizeCore(); + + this.throwHeightWarningIfNeed(); + + if (that.component.$element() && !that._windowScroll && isElementInDom($element)) { + that._windowScroll = subscribeToExternalScrollers($element, (scrollPos) => { + if (!that._hasHeight && that._rowHeight) { + // @ts-expect-error + that._dataController.setViewportPosition(scrollPos); + } + }, that.component.$element()); + + that.on('disposing', () => { + that._windowScroll.dispose(); + }); + } + + if (this.option(LEGACY_SCROLLING_MODE) !== false) { + that.loadIfNeed(); + } + } + + loadIfNeed() { + const dataController = this._dataController; + // @ts-expect-error + dataController?.loadIfNeed?.(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _restoreErrorRow(contentTable?) { + if (this.option(LEGACY_SCROLLING_MODE) === false) { + const errorHandling = this.getController('errorHandling'); + errorHandling?.removeErrorRow(); + } + + super._restoreErrorRow.apply(this, arguments as any); + } + + dispose() { + clearTimeout(this._scrollTimeoutID); + super.dispose(); + } +}; + +export const virtualScrollingModule = { + defaultOptions() { + return { + scrolling: { + timeout: 300, + updateTimeout: 300, + minTimeout: 0, + renderingThreshold: 100, + removeInvisiblePages: true, + rowPageSize: 5, + prerenderedRowChunkSize: 1, + mode: 'standard', + preloadEnabled: false, + rowRenderingMode: 'standard', + loadTwoPagesOnStart: false, + legacyMode: false, + prerenderedRowCount: 1, }, + }; + }, + extenders: { + controllers: { + data, + resizing, }, views: { - rowsView: VirtualScrollingRowsViewExtender, + rowsView, }, }, }; diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts b/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts index f1d59913d37a..1d7ca9c54871 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts @@ -1,202 +1,201 @@ import { equalByValue } from '@js/core/utils/common'; import { Deferred } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; -import { dataControllerModule } from '@ts/grids/grid_core/data_controller/m_data_controller'; +import { DataController, dataControllerModule } from '@ts/grids/grid_core/data_controller/m_data_controller'; import dataSourceAdapterProvider from '../data_source_adapter/m_data_source_adapter'; import treeListCore from '../m_core'; -export const DataController = (dataControllerModule.controllers as any).data.inherit((function () { - return { - _getDataSourceAdapter() { - return dataSourceAdapterProvider; - }, - - _getNodeLevel(node) { - let level = -1; - while (node.parent) { - if (node.visible) { - level++; - } - node = node.parent; - } - return level; - }, - - _generateDataItem(node, options) { - return { - rowType: 'data', - node, - key: node.key, - data: node.data, - isExpanded: this.isRowExpanded(node.key, options), - level: this._getNodeLevel(node), - }; - }, - - _loadOnOptionChange() { - this._dataSource.load(); - }, - - _isItemEquals(item1, item2) { - if (item1.isSelected !== item2.isSelected) { - return false; - } +export class TreeListDataController extends DataController { + _getDataSourceAdapter() { + return dataSourceAdapterProvider; + } - if (item1.node && item2.node && item1.node.hasChildren !== item2.node.hasChildren) { - return false; + _getNodeLevel(node) { + let level = -1; + while (node.parent) { + if (node.visible) { + level++; } + node = node.parent; + } + return level; + } + + _generateDataItem(node, options) { + return { + rowType: 'data', + node, + key: node.key, + data: node.data, + isExpanded: this.isRowExpanded(node.key, options), + level: this._getNodeLevel(node), + }; + } + + _loadOnOptionChange() { + this._dataSource.load(); + } + + _isItemEquals(item1, item2) { + if (item1.isSelected !== item2.isSelected) { + return false; + } + + if (item1.node && item2.node && item1.node.hasChildren !== item2.node.hasChildren) { + return false; + } + + if (item1.level !== item2.level || item1.isExpanded !== item2.isExpanded) { + return false; + } + + return super._isItemEquals.apply(this, arguments as any); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _isCellChanged(oldRow, newRow, visibleRowIndex, columnIndex, isLiveUpdate) { + // @ts-expect-error + const firstDataColumnIndex = this._columnsController.getFirstDataColumnIndex(); + + if (columnIndex === firstDataColumnIndex && oldRow.isSelected !== newRow.isSelected) { + return true; + } + + return super._isCellChanged.apply(this, arguments as any); + } + + init() { + this.createAction('onRowExpanding'); + this.createAction('onRowExpanded'); + this.createAction('onRowCollapsing'); + this.createAction('onRowCollapsed'); + + super.init.apply(this, arguments as any); + } + + keyOf(data) { + const dataSource = this._dataSource; + + if (dataSource) { + return dataSource.keyOf(data); + } + } + + key() { + const dataSource = this._dataSource; + + if (dataSource) { + return dataSource.getKeyExpr(); + } + } + + publicMethods() { + return super.publicMethods().concat(['expandRow', 'collapseRow', 'isRowExpanded', 'getRootNode', 'getNodeByKey', 'loadDescendants', 'forEachNode']); + } + + changeRowExpand(key) { + if (this._dataSource) { + const args: any = { + key, + }; + const isExpanded = this.isRowExpanded(key); - if (item1.level !== item2.level || item1.isExpanded !== item2.isExpanded) { - return false; - } - - return this.callBase.apply(this, arguments); - }, - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _isCellChanged(oldRow, newRow, visibleRowIndex, columnIndex, isLiveUpdate) { - const firstDataColumnIndex = this._columnsController.getFirstDataColumnIndex(); - - if (columnIndex === firstDataColumnIndex && oldRow.isSelected !== newRow.isSelected) { - return true; - } - - return this.callBase.apply(this, arguments); - }, - - init() { - this.createAction('onRowExpanding'); - this.createAction('onRowExpanded'); - this.createAction('onRowCollapsing'); - this.createAction('onRowCollapsed'); - - this.callBase.apply(this, arguments); - }, - - keyOf(data) { - const dataSource = this._dataSource; - - if (dataSource) { - return dataSource.keyOf(data); - } - }, - - key() { - const dataSource = this._dataSource; + this.executeAction(isExpanded ? 'onRowCollapsing' : 'onRowExpanding', args); - if (dataSource) { - return dataSource.getKeyExpr(); + if (!args.cancel) { + return this._dataSource.changeRowExpand(key).done(() => { + this.executeAction(isExpanded ? 'onRowCollapsed' : 'onRowExpanded', args); + }); } - }, - - publicMethods() { - return this.callBase().concat(['expandRow', 'collapseRow', 'isRowExpanded', 'getRootNode', 'getNodeByKey', 'loadDescendants', 'forEachNode']); - }, - - changeRowExpand(key) { - if (this._dataSource) { - const args: any = { - key, - }; - const isExpanded = this.isRowExpanded(key); - - this.executeAction(isExpanded ? 'onRowCollapsing' : 'onRowExpanding', args); - - if (!args.cancel) { - return this._dataSource.changeRowExpand(key).done(() => { - this.executeAction(isExpanded ? 'onRowCollapsed' : 'onRowExpanded', args); - }); + } + + // @ts-expect-error + return new Deferred().resolve(); + } + + isRowExpanded(key, cache?) { + return this._dataSource && this._dataSource.isRowExpanded(key, cache); + } + + expandRow(key) { + if (!this.isRowExpanded(key)) { + return this.changeRowExpand(key); + } + // @ts-expect-error + return new Deferred().resolve(); + } + + collapseRow(key) { + if (this.isRowExpanded(key)) { + return this.changeRowExpand(key); + } + // @ts-expect-error + return new Deferred().resolve(); + } + + getRootNode() { + return this._dataSource && this._dataSource.getRootNode(); + } + + optionChanged(args) { + switch (args.name) { + case 'rootValue': + case 'parentIdExpr': + case 'itemsExpr': + case 'filterMode': + case 'expandNodesOnFiltering': + case 'autoExpandAll': + case 'hasItemsExpr': + case 'dataStructure': + this._columnsController.reset(); + this._items = []; + this._refreshDataSource(); + args.handled = true; + break; + case 'expandedRowKeys': + case 'onNodesInitialized': + if (this._dataSource && !this._dataSource._isNodesInitializing && !equalByValue(args.value, args.previousValue)) { + this._loadOnOptionChange(); } - } - - // @ts-expect-error - return new Deferred().resolve(); - }, - - isRowExpanded(key, cache) { - return this._dataSource && this._dataSource.isRowExpanded(key, cache); - }, - - expandRow(key) { - if (!this.isRowExpanded(key)) { - return this.changeRowExpand(key); - } - // @ts-expect-error - return new Deferred().resolve(); - }, - - collapseRow(key) { - if (this.isRowExpanded(key)) { - return this.changeRowExpand(key); - } - // @ts-expect-error - return new Deferred().resolve(); - }, - - getRootNode() { - return this._dataSource && this._dataSource.getRootNode(); - }, - - optionChanged(args) { - switch (args.name) { - case 'rootValue': - case 'parentIdExpr': - case 'itemsExpr': - case 'filterMode': - case 'expandNodesOnFiltering': - case 'autoExpandAll': - case 'hasItemsExpr': - case 'dataStructure': - this._columnsController.reset(); - this._items = []; - this._refreshDataSource(); - args.handled = true; - break; - case 'expandedRowKeys': - case 'onNodesInitialized': - if (this._dataSource && !this._dataSource._isNodesInitializing && !equalByValue(args.value, args.previousValue)) { - this._loadOnOptionChange(); - } - args.handled = true; - break; - case 'maxFilterLengthInRequest': - args.handled = true; - break; - default: - this.callBase(args); - } - }, - - getNodeByKey(key) { - if (!this._dataSource) { - return; - } - - return this._dataSource.getNodeByKey(key); - }, - - getChildNodeKeys(parentKey) { - if (!this._dataSource) { - return; - } - - return this._dataSource.getChildNodeKeys(parentKey); - }, - - loadDescendants(keys, childrenOnly) { - if (!this._dataSource) { - return; - } - - return this._dataSource.loadDescendants(keys, childrenOnly); - }, - - forEachNode() { - this._dataSource.forEachNode.apply(this, arguments); - }, - }; -})()); + args.handled = true; + break; + case 'maxFilterLengthInRequest': + args.handled = true; + break; + default: + super.optionChanged(args); + } + } + + getNodeByKey(key) { + if (!this._dataSource) { + return; + } + + return this._dataSource.getNodeByKey(key); + } + + getChildNodeKeys(parentKey) { + if (!this._dataSource) { + return; + } + + return this._dataSource.getChildNodeKeys(parentKey); + } + + loadDescendants(keys, childrenOnly) { + if (!this._dataSource) { + return; + } + + return this._dataSource.loadDescendants(keys, childrenOnly); + } + + forEachNode() { + this._dataSource.forEachNode.apply(this, arguments); + } +} treeListCore.registerModule('data', { defaultOptions() { @@ -218,6 +217,6 @@ treeListCore.registerModule('data', { }); }, controllers: { - data: DataController, + data: TreeListDataController, }, }); diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_columns_controller.ts b/packages/devextreme/js/__internal/grids/tree_list/m_columns_controller.ts index 57072f1ca1d4..66d196710a6f 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_columns_controller.ts @@ -1,33 +1,32 @@ import { isDefined } from '@js/core/utils/type'; -import { columnsControllerModule } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; +import { ColumnsController, columnsControllerModule } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; import treeListCore from './m_core'; -export const ColumnsController = (columnsControllerModule as any).controllers.columns.inherit((function () { - return { - _getFirstItems(dataSourceAdapter) { - return this.callBase(dataSourceAdapter).map((node) => node.data); - }, - getFirstDataColumnIndex() { - const visibleColumns = this.getVisibleColumns(); - const visibleColumnsLength = visibleColumns.length; - let firstDataColumnIndex = 0; +class TreeListColumnsController extends ColumnsController { + _getFirstItems(dataSourceAdapter) { + return super._getFirstItems(dataSourceAdapter).map((node) => node.data); + } - for (let i = 0; i <= visibleColumnsLength - 1; i++) { - if (!isDefined(visibleColumns[i].command)) { - firstDataColumnIndex = visibleColumns[i].index; - break; - } + getFirstDataColumnIndex() { + const visibleColumns = this.getVisibleColumns(); + const visibleColumnsLength = visibleColumns.length; + let firstDataColumnIndex = 0; + + for (let i = 0; i <= visibleColumnsLength - 1; i++) { + if (!isDefined(visibleColumns[i].command)) { + firstDataColumnIndex = visibleColumns[i].index; + break; } + } - return firstDataColumnIndex; - }, - }; -})()); + return firstDataColumnIndex; + } +} treeListCore.registerModule('columns', { defaultOptions: columnsControllerModule.defaultOptions, controllers: { - columns: ColumnsController, + columns: TreeListColumnsController, }, }); diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_grid_view.ts b/packages/devextreme/js/__internal/grids/tree_list/m_grid_view.ts index e94901d35e30..0ee379ae0a74 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_grid_view.ts @@ -1,25 +1,25 @@ -import { gridViewModule } from '@ts/grids/grid_core/views/m_grid_view'; +import { gridViewModule, ResizingController } from '@ts/grids/grid_core/views/m_grid_view'; import treeListCore from './m_core'; -const ResizingController = gridViewModule.controllers.resizing.inherit({ +class TreeListResizingController extends ResizingController { _getWidgetAriaLabel() { return 'dxTreeList-ariaTreeList'; - }, + } _toggleBestFitMode(isBestFit) { - this.callBase(isBestFit); + super._toggleBestFitMode(isBestFit); const $rowsTable = this._rowsView.getTableElement(); $rowsTable.find('.dx-treelist-cell-expandable').toggleClass(this.addWidgetPrefix('best-fit'), isBestFit); - }, -}); + } +} treeListCore.registerModule('gridView', { defaultOptions: gridViewModule.defaultOptions, controllers: { ...gridViewModule.controllers, - resizing: ResizingController, + resizing: TreeListResizingController, }, views: gridViewModule.views, }); diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_keyboard_navigation.ts b/packages/devextreme/js/__internal/grids/tree_list/m_keyboard_navigation.ts index 052e9992dfc2..2b16be24afd5 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_keyboard_navigation.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_keyboard_navigation.ts @@ -1,30 +1,37 @@ import { extend } from '@js/core/utils/extend'; +import type { KeyboardNavigationController } from '@ts/grids/grid_core/keyboard_navigation/m_keyboard_navigation'; import { keyboardNavigationModule } from '@ts/grids/grid_core/keyboard_navigation/m_keyboard_navigation'; +import { keyboardNavigationScrollableA11yExtender } from '@ts/grids/grid_core/keyboard_navigation/scrollable_a11y'; +import type { ModuleType } from '@ts/grids/grid_core/m_types'; import core from './m_core'; +const keyboardNavigation = (Base: ModuleType) => class TreeListKeyboardNavigationControllerExtender extends keyboardNavigationScrollableA11yExtender(Base) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _leftRightKeysHandler(eventArgs, _isEditing?) { + const rowIndex = this.getVisibleRowIndex(); + const dataController = this._dataController; + + if (eventArgs.ctrl) { + const directionCode = this._getDirectionCodeByKey(eventArgs.keyName); + const key = dataController.getKeyByRowIndex(rowIndex); + if (directionCode === 'nextInRow') { + // @ts-expect-error + dataController.expandRow(key); + } else { + // @ts-expect-error + dataController.collapseRow(key); + } + } else { + return super._leftRightKeysHandler.apply(this, arguments as any); + } + } +}; + core.registerModule('keyboardNavigation', extend(true, {}, keyboardNavigationModule, { extenders: { controllers: { - keyboardNavigation: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _leftRightKeysHandler(eventArgs, isEditing) { - const rowIndex = this.getVisibleRowIndex(); - const dataController = this._dataController; - - if (eventArgs.ctrl) { - const directionCode = this._getDirectionCodeByKey(eventArgs.keyName); - const key = dataController.getKeyByRowIndex(rowIndex); - if (directionCode === 'nextInRow') { - dataController.expandRow(key); - } else { - dataController.collapseRow(key); - } - } else { - return this.callBase.apply(this, arguments); - } - }, - }, + keyboardNavigation, }, }, })); diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_virtual_scrolling.ts b/packages/devextreme/js/__internal/grids/tree_list/m_virtual_scrolling.ts index 28f3fbc4c16e..9118c1492803 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_virtual_scrolling.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_virtual_scrolling.ts @@ -1,22 +1,29 @@ +/* eslint-disable max-classes-per-file */ import { extend } from '@js/core/utils/extend'; +import type { DataController } from '@ts/grids/grid_core/data_controller/m_data_controller'; import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; import type { ModuleType } from '@ts/grids/grid_core/m_types'; -import { dataSourceAdapterExtender as virtualScrollingDataSourceAdapterExtender, virtualScrollingModule } from '@ts/grids/grid_core/virtual_scrolling/m_virtual_scrolling'; +import { + data as virtualScrollingDataControllerExtender, + dataSourceAdapterExtender as virtualScrollingDataSourceAdapterExtender, + virtualScrollingModule, +} from '@ts/grids/grid_core/virtual_scrolling/m_virtual_scrolling'; import dataSourceAdapterProvider from './data_source_adapter/m_data_source_adapter'; import gridCore from './m_core'; const oldDefaultOptions = virtualScrollingModule.defaultOptions; -const originalDataControllerExtender = virtualScrollingModule.extenders.controllers.data; -virtualScrollingModule.extenders.controllers.data = extend({}, originalDataControllerExtender, { +virtualScrollingModule.extenders.controllers.data = (Base: ModuleType) => class TreeListVirtualScrollingDataControllerExtender extends virtualScrollingDataControllerExtender(Base) { _loadOnOptionChange() { - const virtualScrollController = this._dataSource && this._dataSource._virtualScrollController; + const virtualScrollController = this._dataSource?._virtualScrollController; + + virtualScrollController?.reset(); + // @ts-expect-error + super._loadOnOptionChange(); + } +}; - virtualScrollController && virtualScrollController.reset(); - this.callBase(); - }, -}); const dataSourceAdapterExtender = (Base: ModuleType) => class VirtualScrollingDataSourceAdapterExtender extends virtualScrollingDataSourceAdapterExtender(Base) { changeRowExpand() { return super.changeRowExpand.apply(this, arguments as any).done(() => { @@ -27,7 +34,8 @@ const dataSourceAdapterExtender = (Base: ModuleType) => class } }; -gridCore.registerModule('virtualScrolling', extend({}, virtualScrollingModule, { +gridCore.registerModule('virtualScrolling', { + ...virtualScrollingModule, defaultOptions() { return extend(true, oldDefaultOptions(), { scrolling: { @@ -35,6 +43,6 @@ gridCore.registerModule('virtualScrolling', extend({}, virtualScrollingModule, { }, }); }, -})); +}); dataSourceAdapterProvider.extend(dataSourceAdapterExtender); diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts index 4a89e82e2dcf..e20ef40bd9b5 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts @@ -9,11 +9,12 @@ import './m_grid_view'; import './module_not_extended/header_panel'; import registerComponent from '@js/core/component_registrator'; -import { deferRender, noop } from '@js/core/utils/common'; +import { deferRender } from '@js/core/utils/common'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; import { isDefined, isFunction } from '@js/core/utils/type'; import { isMaterialBased } from '@js/ui/themes'; +import type { Properties as dxTreeListOptions } from '@js/ui/tree_list'; import Widget from '@js/ui/widget/ui.widget'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; @@ -57,12 +58,14 @@ treeListCore.registerModulesOrder([ 'export', 'gridView']); -const TreeList = (Widget as any).inherit({ - _activeStateUnit: DATAGRID_ROW_SELECTOR, +class TreeList extends Widget { + _deprecatedOptions: any; + + _activeStateUnit = DATAGRID_ROW_SELECTOR; _getDefaultOptions() { - const that = this; - const result = that.callBase(); + // @ts-expect-error + const result = super._getDefaultOptions(); each(treeListCore.modules, function () { if (isFunction(this.defaultOptions)) { @@ -70,19 +73,21 @@ const TreeList = (Widget as any).inherit({ } }); return result; - }, + } _setDeprecatedOptions() { - this.callBase(); + // @ts-expect-error + super._setDeprecatedOptions(); extend(this._deprecatedOptions, { 'columnChooser.allowSearch': { since: '23.1', message: 'Use the "columnChooser.search.enabled" option instead' }, 'columnChooser.searchTimeout': { since: '23.1', message: 'Use the "columnChooser.search.timeout" option instead' }, }); - }, + } _defaultOptionsRules() { - return this.callBase().concat([ + // @ts-expect-error + return super._defaultOptionsRules().concat([ { device() { // @ts-expect-error @@ -100,12 +105,13 @@ const TreeList = (Widget as any).inherit({ }, }, ]); - }, + } _init() { const that = this; - that.callBase(); + // @ts-expect-error + super._init(); if (!this.option('_disableDeprecationWarnings')) { gridCoreUtils.logHeaderFilterDeprecatedWarningIfNeed(this); @@ -114,38 +120,42 @@ const TreeList = (Widget as any).inherit({ treeListCore.processModules(that, treeListCore); callModuleItemsMethod(that, 'init'); - }, + } - _clean: noop, + _clean() {} _optionChanged(args) { const that = this; callModuleItemsMethod(that, 'optionChanged', [args]); if (!args.handled) { - that.callBase(args); + // @ts-expect-error + super._optionChanged(args); } - }, + } _dimensionChanged() { + // @ts-expect-error this.updateDimensions(true); - }, + } _visibilityChanged(visible) { if (visible) { + // @ts-expect-error this.updateDimensions(); } - }, + } _initMarkup() { - this.callBase.apply(this, arguments); - this.$element().addClass(TREELIST_CLASS); + // @ts-expect-error + super._initMarkup.apply(this, arguments); + (this.$element() as any).addClass(TREELIST_CLASS); this.getView('gridView').render(this.$element()); - }, + } _renderContentImpl() { this.getView('gridView').update(); - }, + } _renderContent() { const that = this; @@ -153,52 +163,54 @@ const TreeList = (Widget as any).inherit({ deferRender(() => { that._renderContentImpl(); }); - }, + } _dispose() { const that = this; - that.callBase(); + // @ts-expect-error + super._dispose(); callModuleItemsMethod(that, 'dispose'); - }, + } isReady() { return this.getController('data').isReady(); - }, + } beginUpdate() { - const that = this; - - that.callBase(); - callModuleItemsMethod(that, 'beginUpdate'); - }, + super.beginUpdate(); + callModuleItemsMethod(this, 'beginUpdate'); + } endUpdate() { - const that = this; - - callModuleItemsMethod(that, 'endUpdate'); - that.callBase(); - }, + callModuleItemsMethod(this, 'endUpdate'); + super.endUpdate(); + } getController(name) { + // @ts-expect-error return this._controllers[name]; - }, + } getView(name) { + // @ts-expect-error return this._views[name]; - }, + } - focus(element) { - this.callBase(); + focus(element?) { + super.focus(); if (isDefined(element)) { this.getController('keyboardNavigation').focus(element); } - }, -}); + } -TreeList.registerModule = treeListCore.registerModule.bind(treeListCore); + static registerModule() { + treeListCore.registerModule.apply(treeListCore, arguments as any); + } +} +// @ts-expect-error registerComponent('dxTreeList', TreeList); export default TreeList; diff --git a/packages/devextreme/js/__internal/grids/tree_list/rows/m_rows.ts b/packages/devextreme/js/__internal/grids/tree_list/rows/m_rows.ts index 86563a4d5279..5c19388b0b98 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/rows/m_rows.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/rows/m_rows.ts @@ -2,7 +2,7 @@ import $ from '@js/core/renderer'; import { isDefined } from '@js/core/utils/type'; import eventsEngine from '@js/events/core/events_engine'; import { removeEvent } from '@js/events/remove'; -import { rowsModule } from '@ts/grids/grid_core/views/m_rows_view'; +import { rowsModule, RowsView } from '@ts/grids/grid_core/views/m_rows_view'; import treeListCore from '../m_core'; @@ -13,143 +13,143 @@ const TREELIST_EMPTY_SPACE = 'dx-treelist-empty-space'; const TREELIST_EXPANDED_CLASS = 'dx-treelist-expanded'; const TREELIST_COLLAPSED_CLASS = 'dx-treelist-collapsed'; -export const RowsView = rowsModule.views.rowsView.inherit((function () { - const createCellContent = function ($container) { - return $('
') - .addClass(TREELIST_TEXT_CONTENT) +const createCellContent = function ($container) { + return $('
') + .addClass(TREELIST_TEXT_CONTENT) + .appendTo($container); +}; + +const createIcon = function (hasIcon, isExpanded) { + const $iconElement = $('
').addClass(TREELIST_EMPTY_SPACE); + + if (hasIcon) { + $iconElement + .toggleClass(TREELIST_EXPANDED_CLASS, isExpanded) + .toggleClass(TREELIST_COLLAPSED_CLASS, !isExpanded) + .append($('')); + } + + return $iconElement; +}; + +class TreeListRowsView extends RowsView { + _renderIconContainer($container, options) { + const $iconContainer = $('
') + .addClass(TREELIST_EXPAND_ICON_CONTAINER_CLASS) .appendTo($container); - }; - const createIcon = function (hasIcon, isExpanded) { - const $iconElement = $('
').addClass(TREELIST_EMPTY_SPACE); - - if (hasIcon) { - $iconElement - .toggleClass(TREELIST_EXPANDED_CLASS, isExpanded) - .toggleClass(TREELIST_COLLAPSED_CLASS, !isExpanded) - .append($('')); + if (options.watch) { + const dispose = options.watch(() => [ + options.row.level, + options.row.isExpanded, + options.row.node.hasChildren, + ], () => { + $iconContainer.empty(); + this._renderIcons($iconContainer, options); + }); + + eventsEngine.on($iconContainer, removeEvent, dispose); } - return $iconElement; - }; - - return { - _renderIconContainer($container, options) { - const $iconContainer = $('
') - .addClass(TREELIST_EXPAND_ICON_CONTAINER_CLASS) - .appendTo($container); - - if (options.watch) { - const dispose = options.watch(() => [ - options.row.level, - options.row.isExpanded, - options.row.node.hasChildren, - ], () => { - $iconContainer.empty(); - this._renderIcons($iconContainer, options); - }); - - eventsEngine.on($iconContainer, removeEvent, dispose); - } - - $container.addClass(TREELIST_CELL_EXPANDABLE_CLASS); + $container.addClass(TREELIST_CELL_EXPANDABLE_CLASS); - return this._renderIcons($iconContainer, options); - }, + return this._renderIcons($iconContainer, options); + } - _renderIcons($iconContainer, options) { - const { row } = options; - const { level } = row; + _renderIcons($iconContainer, options) { + const { row } = options; + const { level } = row; - for (let i = 0; i <= level; i++) { - $iconContainer.append(createIcon(i === level && row.node.hasChildren, row.isExpanded)); - } + for (let i = 0; i <= level; i++) { + $iconContainer.append(createIcon(i === level && row.node.hasChildren, row.isExpanded)); + } - return $iconContainer; - }, + return $iconContainer; + } - _renderCellCommandContent(container, model) { - this._renderIconContainer(container, model); - return true; - }, + _renderCellCommandContent(container, model) { + this._renderIconContainer(container, model); + return true; + } - _processTemplate(template, options) { - const that = this; - let resultTemplate; - const renderingTemplate = this.callBase(template); + _processTemplate(template, options) { + const that = this; + let resultTemplate; + const renderingTemplate = super._processTemplate(template); - const firstDataColumnIndex = that._columnsController.getFirstDataColumnIndex(); + // @ts-expect-error + const firstDataColumnIndex = that._columnsController.getFirstDataColumnIndex(); - if (renderingTemplate && options.column?.index === firstDataColumnIndex) { - resultTemplate = { - render(options) { - const $container = options.container; + if (renderingTemplate && options.column?.index === firstDataColumnIndex) { + resultTemplate = { + render(options) { + const $container = options.container; - if (that._renderCellCommandContent($container, options.model)) { - options.container = createCellContent($container); - } + if (that._renderCellCommandContent($container, options.model)) { + options.container = createCellContent($container); + } - renderingTemplate.render(options); - }, - }; - } else { - resultTemplate = renderingTemplate; - } + renderingTemplate.render(options); + }, + }; + } else { + resultTemplate = renderingTemplate; + } - return resultTemplate; - }, + return resultTemplate; + } - _updateCell($cell, options) { - $cell = $cell.hasClass(TREELIST_TEXT_CONTENT) ? $cell.parent() : $cell; - this.callBase($cell, options); - }, + _updateCell($cell, options) { + $cell = $cell.hasClass(TREELIST_TEXT_CONTENT) ? $cell.parent() : $cell; + super._updateCell($cell, options); + } - _rowClick(e) { - const dataController = this._dataController; - const $targetElement = $(e.event.target); - const isExpandIcon = this.isExpandIcon($targetElement); - const item = dataController && dataController.items()[e.rowIndex]; + _rowClick(e) { + const dataController = this._dataController; + const $targetElement = $(e.event.target); + const isExpandIcon = this.isExpandIcon($targetElement); + const item = dataController?.items()[e.rowIndex]; - if (isExpandIcon && item) { - dataController.changeRowExpand(item.key); - } + if (isExpandIcon && item) { + // @ts-expect-error + dataController.changeRowExpand(item.key); + } - this.callBase(e); - }, + super._rowClick(e); + } - _createRow(row) { - const node = row && row.node; - const $rowElement = this.callBase.apply(this, arguments); + _createRow(row) { + const node = row && row.node; + const $rowElement = super._createRow.apply(this, arguments as any); - if (node) { - this.setAria('level', row.level + 1, $rowElement); + if (node) { + this.setAria('level', row.level + 1, $rowElement); - if (node.hasChildren) { - this.setAria('expanded', row.isExpanded, $rowElement); - } + if (node.hasChildren) { + this.setAria('expanded', row.isExpanded, $rowElement); } + } - return $rowElement; - }, + return $rowElement; + } - _getGridRoleName() { - return 'treegrid'; - }, + _getGridRoleName() { + return 'treegrid'; + } - isExpandIcon($targetElement) { - return !!$targetElement.closest(`.${TREELIST_EXPANDED_CLASS}, .${TREELIST_COLLAPSED_CLASS}`).length; - }, + isExpandIcon($targetElement) { + return !!$targetElement.closest(`.${TREELIST_EXPANDED_CLASS}, .${TREELIST_COLLAPSED_CLASS}`).length; + } - setAriaExpandedAttribute($row, row) { - const isRowExpanded = row.isExpanded; - this.setAria('expanded', isDefined(isRowExpanded) && isRowExpanded.toString(), $row); - }, - }; -})()); + setAriaExpandedAttribute($row, row) { + const isRowExpanded = row.isExpanded; + this.setAria('expanded', isDefined(isRowExpanded) && isRowExpanded.toString(), $row); + } +} treeListCore.registerModule('rows', { defaultOptions: rowsModule.defaultOptions, views: { - rowsView: RowsView, + rowsView: TreeListRowsView, }, });