From a99bee14b8348fc8cd0c85c18addf1cccbbefced Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:16:27 +0100 Subject: [PATCH 1/4] Grids: type m_search --- .../__tests__/search.integration.test.ts | 136 +++++++++++++ .../grid_core/search/__tests__/utils.test.ts | 20 ++ .../grids/grid_core/search/const.ts | 7 + .../grids/grid_core/search/m_search.ts | 188 ++++++++++-------- .../grids/grid_core/search/types.ts | 21 ++ .../grids/grid_core/search/utils.ts | 7 + 6 files changed, 296 insertions(+), 83 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/search/const.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/search/types.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/search.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/search.integration.test.ts index 2f480544f8a3..2e222ff1f7da 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/search.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/search.integration.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, jest, } from '@jest/globals'; import $ from '@js/core/renderer'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; import { afterTest, @@ -75,4 +76,139 @@ describe('SearchPanel', () => { expect($input.attr('placeholder')).toBe('updated'); }); }); + + describe('search text highlighting', () => { + it('should highlight a non-string cell whose text equals the formatted search text', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1, price: 15 }], + columns: [{ + dataField: 'price', + dataType: 'number', + format: { type: 'fixedPoint', precision: 2 }, + }], + searchPanel: { + visible: true, + text: '15', + }, + }); + jest.runAllTimers(); + + const $highlighted = $(instance.element()).find('.dx-datagrid-search-text'); + expect($highlighted.length).toBe(1); + expect($highlighted.text()).toBe('15.00'); + }); + + it('should highlight the typed text, not the formatted one, in a string column', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ name: 'Mark' }], + columns: [{ + dataField: 'name', + dataType: 'string', + customizeText: ({ valueText }): string => `Name: ${valueText}`, + }], + searchPanel: { + visible: true, + text: 'ar', + }, + }); + jest.runAllTimers(); + + const $highlighted = $(instance.element()).find('.dx-datagrid-search-text'); + expect($highlighted.length).toBe(1); + expect($highlighted.text()).toBe('ar'); + }); + + it('should highlight every match and keep the text around them', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ name: 'Mark Carter' }], + searchPanel: { + visible: true, + text: 'AR', + }, + }); + jest.runAllTimers(); + + const $cell = $(instance.element()).find('.dx-data-row td').eq(0); + expect($cell.html()).toBe( + 'Mark Carter', + ); + }); + + it('should highlight the search text in the rows of a data row template', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ name: 'Mark' }], + dataRowTemplate: (container, { data }): void => { + $(container).append($('').append($('').text(data.name))); + }, + searchPanel: { + visible: true, + text: 'ar', + }, + }); + jest.runAllTimers(); + + const $highlighted = $(instance.element()).find('.dx-datagrid-search-text'); + expect($highlighted.length).toBe(1); + expect($highlighted.text()).toBe('ar'); + }); + + describe('when templates render asynchronously', () => { + it('should highlight the search text in a cell without a template', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ name: 'Mark' }], + templatesRenderAsynchronously: true, + searchPanel: { + visible: true, + text: 'ar', + }, + } as DataGridProperties); + jest.runAllTimers(); + + const $highlighted = $(instance.element()).find('.dx-datagrid-search-text'); + expect($highlighted.length).toBe(1); + expect($highlighted.text()).toBe('ar'); + }); + + it('should highlight the search text once in each cell template', async () => { + const cellTemplate = (container, { text }): void => { + $(container).append($('').text(text)); + }; + const { instance } = await createDataGrid({ + dataSource: [{ name: 'Mark', city: 'Paris' }], + columns: [ + { dataField: 'name', cellTemplate }, + { dataField: 'city', cellTemplate }, + ], + templatesRenderAsynchronously: true, + searchPanel: { + visible: true, + text: 'ar', + }, + } as DataGridProperties); + jest.runAllTimers(); + + const $highlighted = $(instance.element()).find('.dx-datagrid-search-text'); + expect($highlighted.length).toBe(2); + expect($highlighted.eq(0).text()).toBe('ar'); + expect($highlighted.eq(1).text()).toBe('ar'); + }); + + it('should highlight the search text in a group row', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ name: 'Mark', city: 'Rome' }], + columns: [{ dataField: 'name', groupIndex: 0 }, 'city'], + templatesRenderAsynchronously: true, + searchPanel: { + visible: true, + text: 'ar', + }, + } as DataGridProperties); + jest.runAllTimers(); + + const $highlighted = $(instance.element()).find('.dx-group-cell .dx-datagrid-search-text'); + expect($highlighted.length).toBe(1); + expect($highlighted.text()).toBe('ar'); + }); + }); + }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/utils.test.ts b/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/utils.test.ts index ccbe843dcd1d..ff5d6b149470 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/utils.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/utils.test.ts @@ -7,6 +7,7 @@ import { allowSearch, createLookupFilterExpressions, createSearchExpression, + getFormattedSearchText, parseValue, } from '../utils'; @@ -67,6 +68,25 @@ describe('parseValue', () => { }); }); +describe('getFormattedSearchText', () => { + it('should format the parsed value with the column format', () => { + const column = createColumn({ + parseValue: (text: string) => Number(text), + format: { type: 'fixedPoint', precision: 2 }, + }); + + expect(getFormattedSearchText(column, '15')).toBe('15.00'); + }); + + it('should pass the search target to customizeText', () => { + const column = createColumn({ + customizeText: ({ target, valueText }) => `${target}:${valueText}`, + }); + + expect(getFormattedSearchText(column, '15')).toBe('search:15'); + }); +}); + describe('createLookupFilterExpressions', () => { const ITEMS: RawItemData[] = [ { id: 1, name: 'Berlin' }, diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/const.ts b/packages/devextreme/js/__internal/grids/grid_core/search/const.ts new file mode 100644 index 000000000000..6e4907c7f8ee --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/search/const.ts @@ -0,0 +1,7 @@ +export const CLASSES = { + searchPanel: 'search-panel', + searchText: 'search-text', +}; + +export const FILTERING_TIMEOUT = 700; +export const SEARCH_PANEL_ITEM_NAME = 'searchPanel'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts index 3ab82574a6d3..0e0e2dbb5bec 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts @@ -1,35 +1,39 @@ /* eslint-disable max-classes-per-file */ import messageLocalization from '@js/common/core/localization/message'; -import domAdapter from '@js/core/dom_adapter'; +import type { SearchPanel } from '@js/common/grids'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import { toComparable } from '@js/core/utils/data'; import type { DeferredObj } from '@js/core/utils/deferred'; import type TextBox from '@js/ui/text_box'; +import domAdapter from '@ts/core/dom_adapter'; +import { toComparable } from '@ts/core/utils/data'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; +import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { DataChange, UserState } from '@ts/grids/grid_core/data_controller/types'; +import type { DataFilter, FilterSourceContext } from '@ts/grids/grid_core/filter/types'; import { CLASSES as HEADER_PANEL_CLASSES } from '@ts/grids/grid_core/header_panel/const'; +import type { HeaderPanel } from '@ts/grids/grid_core/header_panel/header_panel'; +import modules from '@ts/grids/grid_core/modules/modules'; +import type { ModuleType, OptionChanged } from '@ts/grids/grid_core/types'; +import type { RowsView } from '@ts/grids/grid_core/views/rows_view'; import type { ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types'; -import type { DataController } from '../data_controller/data_controller'; -import type { DataChange, UserState } from '../data_controller/types'; -import type { DataFilter, FilterSourceContext } from '../filter/types'; -import type { HeaderPanel } from '../header_panel/header_panel'; -import modules from '../modules/modules'; -import type { ModuleType, OptionChanged } from '../types'; -import gridCoreUtils from '../utils'; -import type { RowsView } from '../views/rows_view'; -import { allowSearch, createSearchExpression, parseValue } from './utils'; - -const SEARCH_PANEL_CLASS = 'search-panel'; -const SEARCH_TEXT_CLASS = 'search-text'; -const FILTERING_TIMEOUT = 700; -const SEARCH_PANEL_ITEM_NAME = 'searchPanel'; +import { CLASSES, FILTERING_TIMEOUT, SEARCH_PANEL_ITEM_NAME } from './const'; +import type { + SearchCellOptions, + SearchDataController, + SearchDataControllerExtension, + SearchHighlightParams, +} from './types'; +import { allowSearch, createSearchExpression, getFormattedSearchText } from './utils'; const dataController = ( base: ModuleType, -) => class SearchDataControllerExtender extends base { - public optionChanged(args): void { +): ModuleType< + DataController & SearchDataControllerExtension +> => class SearchDataControllerExtender extends base { + public optionChanged(args: OptionChanged): void { switch (args.fullName) { case 'searchPanel.text': case 'searchPanel': @@ -57,16 +61,14 @@ const dataController = ( } }; -type SearchDataControllerExtender = InstanceType>; - export class SearchPanelViewController extends modules.ViewController { private headerPanel?: HeaderPanel; - private dataController?: SearchDataControllerExtender; + private dataController?: SearchDataController; public init(): void { this.headerPanel = this.getView('headerPanel'); - this.dataController = this.getController('data') as SearchDataControllerExtender; + this.dataController = this.getController('data') as SearchDataController; const isSearchPanelVisible = this.option('searchPanel.visible'); @@ -77,11 +79,14 @@ export class SearchPanelViewController extends modules.ViewController { } } + // Called by FilterController.getAdditionalFilter for each filter source in SOURCE_ORDER public isFilterSourceActive(): boolean { return true; } - public getFilterExpressions({ langParams, columnsController }: FilterSourceContext): DataFilter[] { + public getFilterExpressions( + { langParams, columnsController }: FilterSourceContext, + ): DataFilter[] { const columns = this.getColumnsToSearch(columnsController); const searchFilter = createSearchExpression(columns, this.option('searchPanel.text'), langParams); @@ -134,7 +139,7 @@ export class SearchPanelViewController extends modules.ViewController { template: (_data, _index, container: dxElementWrapper | Element): void => { if (this.headerPanel) { const $search = $('
') - .addClass(this.headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS)) + .addClass(this.headerPanel.addWidgetPrefix(CLASSES.searchPanel)) .appendTo(container); this.getController('editorFactory').createEditor($search, { @@ -172,13 +177,12 @@ export class SearchPanelViewController extends modules.ViewController { const headerPanelClass = this.headerPanel.addWidgetPrefix(HEADER_PANEL_CLASSES.headerPanel); const $searchPanel = $element - .find(`.${this.headerPanel.addWidgetPrefix(SEARCH_PANEL_CLASS)}`) + .find(`.${this.headerPanel.addWidgetPrefix(CLASSES.searchPanel)}`) .filter((_, el: Element) => $(el).closest(`.${headerPanelClass}`).is($element)); if ($searchPanel.length) { // @ts-expect-error dxTextBox is added to the renderer at runtime - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return $searchPanel.dxTextBox('instance'); + return $searchPanel.dxTextBox('instance') as TextBox; } return null; @@ -187,58 +191,67 @@ export class SearchPanelViewController extends modules.ViewController { const rowsView = ( Base: ModuleType, -) => class SearchRowsViewExtender extends Base { - private _searchParams: any; +): ModuleType => class SearchRowsViewExtender extends Base { + private _searchParams!: SearchHighlightParams[]; - private _highlightTimer: any; + private _highlightTimer?: ReturnType; - public init() { - super.init.apply(this, arguments as any); + public init(): void { + super.init(); this._searchParams = []; this._dataController = this.getController('data'); } - public dispose() { + public dispose(): void { clearTimeout(this._highlightTimer); super.dispose(); } - private _getFormattedSearchText(column, searchText) { - const value = parseValue(column, searchText); - const formatOptions = gridCoreUtils.getFormatOptionsByColumn(column, 'search'); - return gridCoreUtils.formatValue(value, formatOptions); - } - - private _getStringNormalizer() { + private _getStringNormalizer(): (str: string) => string { const isCaseSensitive = this.option('searchPanel.highlightCaseSensitive'); const dataSource = this.dataSourceController.getDataSource(); const langParams = dataSource?.loadOptions?.()?.langParams; + // @ts-expect-error toComparable() returns unknown even for a string return (str: string): string => toComparable(str, isCaseSensitive, langParams); } - private _findHighlightingTextNodes(column, cellElement, searchText) { - const that = this; - let $parent = cellElement.parent(); - let $items; - const stringNormalizer = this._getStringNormalizer(); - const normalizedSearchText = stringNormalizer(searchText); - const resultTextNodes: any[] = []; + private getHighlightingElements( + column: Column | undefined, + cellElement: dxElementWrapper, + ): dxElementWrapper { + const $parent = cellElement.parent(); if (!$parent.length) { - $parent = $('
').append(cellElement); - } else if (column) { - if (column.groupIndex >= 0 && !column.showWhenGrouped) { - $items = cellElement; - } else { - const columnIndex = that._columnsController.getVisibleIndex(column.index); - $items = $parent.children('td').eq(columnIndex).find('*'); - } + return $('
').append(cellElement).find('*'); + } + + if (!column) { + return $parent.find('*'); + } + + if (Number(column.groupIndex) >= 0 && !column.showWhenGrouped) { + return cellElement; } - $items = $items?.length ? $items : $parent.find('*'); - $items.each((_, element) => { + + const columnIndex = this._columnsController.getVisibleIndex(column.index); + const $columnElements = $parent.children('td').eq(columnIndex).find('*'); + + return $columnElements.length ? $columnElements : $parent.find('*'); + } + + private _findHighlightingTextNodes( + column: Column | undefined, + cellElement: dxElementWrapper, + searchText: string, + ): Node[] { + const stringNormalizer = this._getStringNormalizer(); + const normalizedSearchText = stringNormalizer(searchText); + const resultTextNodes: Node[] = []; + + this.getHighlightingElements(column, cellElement).each((_, element) => { const $contents = $(element).contents(); - for (let i = 0; i < $contents.length; i++) { + for (let i = 0; i < $contents.length; i += 1) { const node = $contents.get(i); if (node.nodeType === 3) { const normalizedText = stringNormalizer(node.textContent ?? node.nodeValue ?? ''); @@ -252,46 +265,52 @@ const rowsView = ( return resultTextNodes; } - private _highlightSearchTextCore($textNode, searchText) { - const that = this; - const $searchTextSpan = $('').addClass(that.addWidgetPrefix(SEARCH_TEXT_CLASS)); + private _highlightSearchTextCore($textNode: dxElementWrapper, searchText: string): void { + const $searchTextSpan = $('').addClass(this.addWidgetPrefix(CLASSES.searchText)); const text = $textNode.text(); - const firstContentElement = $textNode[0]; + const firstContentElement: Node = $textNode[0]; const stringNormalizer = this._getStringNormalizer(); const index = stringNormalizer(text).indexOf(stringNormalizer(searchText)); if (index >= 0) { + const endIndex = index + searchText.length; + if (firstContentElement.textContent) { - firstContentElement.textContent = text.substr(0, index); + firstContentElement.textContent = text.slice(0, index); } else { - firstContentElement.nodeValue = text.substr(0, index); + firstContentElement.nodeValue = text.slice(0, index); } - $textNode.after($searchTextSpan.text(text.substr(index, searchText.length))); + $textNode.after($searchTextSpan.text(text.slice(index, endIndex))); - $textNode = $(domAdapter.createTextNode(text.substr(index + searchText.length))).insertAfter($searchTextSpan); + const $nextTextNode = $(domAdapter.createTextNode(text.slice(endIndex))) + .insertAfter($searchTextSpan); - return that._highlightSearchTextCore($textNode, searchText); + return this._highlightSearchTextCore($nextTextNode, searchText); } + + return undefined; } - private _highlightSearchText(cellElement, isEquals?, column?) { - const that = this; + private _highlightSearchText( + cellElement: dxElementWrapper, + isEquals?: boolean, + column?: Column, + ): void { const stringNormalizer = this._getStringNormalizer(); - let searchText = that.option('searchPanel.text'); - - if (isEquals && column) { - searchText = searchText && that._getFormattedSearchText(column, searchText); - } + const searchPanelText = this.option('searchPanel.text'); + const searchText = isEquals && column && searchPanelText + ? getFormattedSearchText(column, searchPanelText) + : searchPanelText; - if (searchText && that.option('searchPanel.highlightSearchText')) { - const textNodes = that._findHighlightingTextNodes(column, cellElement, searchText); + if (searchText && this.option('searchPanel.highlightSearchText')) { + const textNodes = this._findHighlightingTextNodes(column, cellElement, searchText); textNodes.forEach((textNode) => { if (isEquals) { - if (stringNormalizer($(textNode).text()) === stringNormalizer(searchText ?? '')) { - $(textNode).replaceWith($('').addClass(that.addWidgetPrefix(SEARCH_TEXT_CLASS)).text($(textNode).text())); + if (stringNormalizer($(textNode).text()) === stringNormalizer(searchText)) { + $(textNode).replaceWith($('').addClass(this.addWidgetPrefix(CLASSES.searchText)).text($(textNode).text())); } } else { - that._highlightSearchTextCore($(textNode), searchText); + this._highlightSearchTextCore($(textNode), searchText); } }); } @@ -306,18 +325,21 @@ const rowsView = ( clearTimeout(this._highlightTimer); this._highlightTimer = setTimeout(() => { + // @ts-expect-error the view is rendered here this._highlightSearchText(this.getTableElement()); }); } else { + // @ts-expect-error the view is rendered here this._highlightSearchText(this.getTableElement()); } } return deferred; } - public _updateCell($cell, parameters) { + public _updateCell($cell: dxElementWrapper, parameters: SearchCellOptions): void { const { column } = parameters; - const dataType = column.lookup && column.lookup.dataType || column.dataType; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- '' falls back + const dataType = column.lookup?.dataType || column.dataType; const isEquals = dataType !== 'string'; if (allowSearch(column) && !parameters.isOnForm) { @@ -327,7 +349,7 @@ const rowsView = ( this._highlightTimer = setTimeout(() => { this._searchParams.forEach((params) => { - this._highlightSearchText.apply(this, params); + this._highlightSearchText(...params); }); this._searchParams = []; @@ -344,7 +366,7 @@ const rowsView = ( }; export const searchModule = { - defaultOptions() { + defaultOptions(): { searchPanel: SearchPanel } { return { searchPanel: { visible: false, diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/types.ts b/packages/devextreme/js/__internal/grids/grid_core/search/types.ts new file mode 100644 index 000000000000..65bdf18ce477 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/search/types.ts @@ -0,0 +1,21 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; +import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { ViewCellOptions } from '@ts/grids/grid_core/views/types'; + +export interface SearchDataControllerExtension { + searchByText: (text: string | undefined) => void; +} + +export type SearchDataController = DataController & SearchDataControllerExtension; + +export interface SearchCellOptions extends ViewCellOptions { + /** @architectureLeak editing: search skips the cells of the edit form */ + isOnForm?: boolean; +} + +export type SearchHighlightParams = [ + cellElement: dxElementWrapper, + isEquals: boolean, + column: Column, +]; diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/utils.ts b/packages/devextreme/js/__internal/grids/grid_core/search/utils.ts index a492a500a98d..4e471b226b3e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/utils.ts @@ -1,6 +1,7 @@ import type { LangParams } from '@js/common/data'; import dataQuery from '@js/common/data/query'; import { compileGetter } from '@js/core/utils/data'; +import gridCoreUtils from '@ts/grids/grid_core/utils'; import type { Column } from '../columns_controller/types'; import type { RawItemData } from '../data_source_adapter/types'; @@ -25,6 +26,12 @@ export const parseValue = (column: Column, text: string): unknown => { return column.parseValue(text); }; +export const getFormattedSearchText = (column: Column, searchText: string): string => { + const value = parseValue(column, searchText); + const formatOptions = gridCoreUtils.getFormatOptionsByColumn(column, 'search'); + return gridCoreUtils.formatValue(value, formatOptions); +}; + export const createLookupFilterExpressions = ( items: RawItemData[], column: Column, From f9a8fdeabffdbe12354a39fe27d41ff1ee936c60 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:24:38 +0100 Subject: [PATCH 2/4] Grids: type m_row_dragging --- .../column_fixing/m_column_fixing.ts | 9 +- .../grids/grid_core/column_fixing/types.ts | 3 + .../grids/grid_core/row_dragging/dom.ts | 11 +- .../grid_core/row_dragging/m_row_dragging.ts | 203 +++++++++++------- .../grids/grid_core/row_dragging/types.ts | 38 ++++ 5 files changed, 184 insertions(+), 80 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/column_fixing/types.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/row_dragging/types.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts b/packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts index a8d463540568..dff8e68971ea 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts @@ -36,6 +36,7 @@ import { normalizeWidth } from '../views/columns_view'; import type { RowsView } from '../views/rows_view'; import type { RowsViewScrollEvent } from '../views/types'; import { CLASSES } from './const'; +import type { ColumnFixingRowsViewExtension } from './types'; const FIRST_CELL_CLASS = 'dx-first-cell'; const LAST_CELL_CLASS = 'dx-last-cell'; @@ -91,7 +92,7 @@ const normalizeColumnWidths = function (fixedColumns, widths: number[], fixedWid // View const baseFixedColumns = >(Base: T) => class BaseFixedColumnsExtender extends Base { - protected _isFixedTableRendering!: boolean; + public _isFixedTableRendering!: boolean; protected _isFixedColumns!: boolean; @@ -681,7 +682,11 @@ const columnHeadersView = (Base: ModuleType) => class ColumnH } }; -const rowsView = (Base: ModuleType) => class RowsViewFixedColumnsExtender extends baseFixedColumns(Base) { +const rowsView = ( + Base: ModuleType, +): ModuleType< + RowsView & ColumnFixingRowsViewExtension +> => class RowsViewFixedColumnsExtender extends baseFixedColumns(Base) { protected _fixedScrollTimeout: any; public dispose() { diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_fixing/types.ts b/packages/devextreme/js/__internal/grids/grid_core/column_fixing/types.ts new file mode 100644 index 000000000000..5583647ea376 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/column_fixing/types.ts @@ -0,0 +1,3 @@ +export interface ColumnFixingRowsViewExtension { + _isFixedTableRendering: boolean; +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/row_dragging/dom.ts b/packages/devextreme/js/__internal/grids/grid_core/row_dragging/dom.ts index fd841224d043..f75a1e1c11ec 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/row_dragging/dom.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/row_dragging/dom.ts @@ -1,13 +1,12 @@ -/* eslint-disable consistent-return */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import $ from '@js/core/renderer'; +import gridCoreUtils from '@ts/grids/grid_core/utils'; -// eslint-disable-next-line @typescript-eslint/no-restricted-imports, -import gridCoreUtils from '../utils'; import { CLASSES } from './const'; +import type { HandleCellTemplate } from './types'; -const createHandleTemplateFunc = (addWidgetPrefix) => (container, options) => { +const createHandleTemplateFunc = ( + addWidgetPrefix: (className: string) => string, +): HandleCellTemplate => (container, options) => { const $container = $(container); if (options.rowType === 'data') { diff --git a/packages/devextreme/js/__internal/grids/grid_core/row_dragging/m_row_dragging.ts b/packages/devextreme/js/__internal/grids/grid_core/row_dragging/m_row_dragging.ts index 041fe77624e1..f17819b7e207 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/row_dragging/m_row_dragging.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/row_dragging/m_row_dragging.ts @@ -1,3 +1,4 @@ +import type { ChangedOptionInfo } from '@js/common/core/events'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { deferUpdate } from '@js/core/utils/common'; @@ -5,48 +6,79 @@ import type { DeferredObj } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { getWidth, setWidth } from '@js/core/utils/size'; import { isDefined } from '@js/core/utils/type'; -import Sortable from '@js/ui/sortable'; +import type { DragTemplateData } from '@js/ui/sortable'; +import { WIDGET_CLASS } from '@ts/core/widget/widget'; +import type { DragElementShownArgs, DragEventArgs, ElementOffsetOptions } from '@ts/draggable.types'; +/** @architectureLeak data_grid/summary: footer rows are not draggable */ +import { DATAGRID_FOOTER_ROW_CLASS } from '@ts/grids/data_grid/summary/const'; +import type { ColumnFixingRowsViewExtension } from '@ts/grids/grid_core/column_fixing/types'; import { CLASSES as COLUMN_HEADERS_CLASSES } from '@ts/grids/grid_core/column_headers/const'; -import type { DataChange } from '@ts/grids/grid_core/data_controller/types'; +import type { DataChange, ProcessedItem } from '@ts/grids/grid_core/data_controller/types'; import { CLASSES as GRID_VIEW_CLASSES } from '@ts/grids/grid_core/grid_view/const'; -import type { ModuleType } from '@ts/grids/grid_core/types'; +import type { + InternalGrid, + InternalGridOptions, + ModuleType, + OptionChanged, +} from '@ts/grids/grid_core/types'; +import gridCoreUtils from '@ts/grids/grid_core/utils'; import { CLASSES as VIEW_CLASSES } from '@ts/grids/grid_core/views/const'; import type { RowsView } from '@ts/grids/grid_core/views/rows_view'; +import { VIRTUAL_ROW_CLASS } from '@ts/grids/grid_core/virtual_scrolling/const'; +import Sortable from '@ts/sortable'; +import { SCROLLABLE_CONTAINER_CLASS } from '@ts/ui/scroll_view/consts'; -import gridCoreUtils from '../utils'; import { ATTRIBUTES, CLASSES } from './const'; import { GridCoreRowDraggingDom } from './dom'; - -const rowsView = (Base: ModuleType) => class RowsViewRowDraggingExtender extends Base { - public init() { - super.init.apply(this, arguments as any); +import type { + DraggableGridOptions, + HandleCellTemplate, + RowDraggingAddEvent, + RowDraggingEndEvent, + RowDraggingOptions, + RowDraggingStartEvent, +} from './types'; + +const rowsView = ( + Base: ModuleType, +): ModuleType< + RowsView & ColumnFixingRowsViewExtension +> => class RowsViewRowDraggingExtender extends Base { + private _sortable?: Sortable; + + private _sortableFixed?: Sortable; + + private _previousOffset?: number; + + public init(): void { + super.init(); this._updateHandleColumn(); } - public optionChanged(args) { + public optionChanged(args: OptionChanged): void { if (args.name === 'rowDragging') { this._updateHandleColumn(); this._invalidate(true, true); args.handled = true; } - super.optionChanged.apply(this, arguments as any); + super.optionChanged(args); } - private _allowReordering() { + private _allowReordering(): boolean { const rowDragging = this.option('rowDragging'); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return !!(rowDragging && (rowDragging.allowReordering || rowDragging.allowDropInsideItem || rowDragging.group)); + return !!rowDragging?.allowReordering + || !!rowDragging?.allowDropInsideItem + || !!rowDragging?.group; } - private _updateHandleColumn() { - const rowDragging: any = this.option('rowDragging'); + private _updateHandleColumn(): void { + const rowDragging = this.option('rowDragging'); const allowReordering = this._allowReordering(); - const columnsController = this._columnsController; - const isHandleColumnVisible = allowReordering && rowDragging.showDragIcons; + const isHandleColumnVisible = allowReordering && rowDragging?.showDragIcons; - columnsController?.addCommandColumn({ + this._columnsController?.addCommandColumn({ type: 'drag', command: 'drag', visibleIndex: -2, @@ -58,71 +90,91 @@ const rowsView = (Base: ModuleType) => class RowsViewRowDraggingExtend visible: isHandleColumnVisible, }); - columnsController?.columnOption('type:drag', 'visible', isHandleColumnVisible); + this._columnsController?.columnOption('type:drag', 'visible', isHandleColumnVisible); + } + + private getSortable(): Sortable | undefined { + return this._isFixedTableRendering ? this._sortableFixed : this._sortable; + } + + private setSortable(sortable: Sortable): void { + if (this._isFixedTableRendering) { + this._sortableFixed = sortable; + } else { + this._sortable = sortable; + } } - protected _renderContent() { - const rowDragging: any = this.option('rowDragging'); + protected _renderContent( + contentElement: dxElementWrapper, + tableElement: dxElementWrapper, + isFixedTable?: boolean, + ): dxElementWrapper | undefined { + // @ts-expect-error the rowDragging option has a default value + const rowDragging: RowDraggingOptions = this.option('rowDragging'); const allowReordering = this._allowReordering(); - // @ts-expect-error the view is rendered here - const $content: dxElementWrapper = super._renderContent.apply(this, arguments as any); - // @ts-expect-error - const isFixedTableRendering = this._isFixedTableRendering; - const sortableName = '_sortable'; - const sortableFixedName = '_sortableFixed'; - const currentSortableName = isFixedTableRendering ? sortableFixedName : sortableName; - const anotherSortableName = isFixedTableRendering ? sortableName : sortableFixedName; - const togglePointerEventsStyle = (toggle) => { + const $content = super._renderContent( + contentElement, + tableElement, + isFixedTable, + ); + + if (!$content) { + return undefined; + } + + const isFixedSortable = this._isFixedTableRendering; + const togglePointerEventsStyle = (toggle: boolean): void => { // T929503 - this[sortableFixedName]?.$element().css('pointerEvents', toggle ? 'auto' : ''); + this._sortableFixed?.$element().css('pointerEvents', toggle ? 'auto' : ''); }; - const rowSelector = `.${VIEW_CLASSES.row}:not(.dx-freespace-row):not(.dx-virtual-row):not(.${COLUMN_HEADERS_CLASSES.headerRow}):not(.dx-footer-row)`; + const rowSelector = `.${VIEW_CLASSES.row}:not(.${VIEW_CLASSES.freeSpaceRow}):not(.${VIRTUAL_ROW_CLASS}):not(.${COLUMN_HEADERS_CLASSES.headerRow}):not(.${DATAGRID_FOOTER_ROW_CLASS})`; const filter = this.option('dataRowTemplate') ? `> table > tbody${rowSelector}` : `> table > tbody > ${rowSelector}`; - if ((allowReordering || this[currentSortableName]) && $content.length) { - this[currentSortableName] = this._createComponent($content, Sortable, extend({ + if ((allowReordering || this.getSortable()) && $content.length) { + // @ts-expect-error rowDragging handlers are typed for the grid's event args + this.setSortable(this._createComponent($content, Sortable, extend({ component: this.component, contentTemplate: null, filter, - cursorOffset: (options) => { + cursorOffset: (options: ElementOffsetOptions) => { const { event } = options; const rowsViewOffset = $(this.element()).offset(); return { - // @ts-expect-error + // @ts-expect-error offset() is typed as possibly undefined x: event.pageX - rowsViewOffset.left, }; }, - onDraggableElementShown: (e) => { + onDraggableElementShown: (e: DragElementShownArgs) => { if (rowDragging.dragTemplate) { return; } - const $dragElement = $(e.dragElement); - const gridInstance = $dragElement.children('.dx-widget').data(this.component.NAME); + const gridInstance = $(e.dragElement).children(`.${WIDGET_CLASS}`).data(this.component.NAME); this._synchronizeScrollLeftPosition(gridInstance); }, dragTemplate: this._getDraggableRowTemplate(), handle: rowDragging.showDragIcons && `.${CLASSES.commandDrag}`, - dropFeedbackMode: 'indicate', + dropFeedbackMode: 'indicate' as const, }, rowDragging, { - onDragStart: (e) => { + onDragStart: (e: RowDraggingStartEvent) => { // TODO getController this.getController('keyboardNavigation')?._resetFocusedCell(); const row = e.component.getVisibleRows()[e.fromIndex]; e.itemData = row?.data; - const isDataRow = row && row.rowType === 'data'; + const isDataRow = row?.rowType === 'data'; e.cancel = !allowReordering || !isDataRow; rowDragging.onDragStart?.(e); }, - onDragEnter: (e) => { + onDragEnter: (e: DragEventArgs) => { if (e.fromComponent !== e.toComponent) { togglePointerEventsStyle(true); } @@ -130,27 +182,33 @@ const rowsView = (Base: ModuleType) => class RowsViewRowDraggingExtend onDragLeave: () => { togglePointerEventsStyle(false); }, - onDragEnd: (e) => { + onDragEnd: (e: RowDraggingEndEvent) => { togglePointerEventsStyle(false); rowDragging.onDragEnd?.(e); }, - onAdd: (e) => { + onAdd: (e: RowDraggingAddEvent) => { togglePointerEventsStyle(false); rowDragging.onAdd?.(e); }, dropFeedbackMode: rowDragging.dropFeedbackMode, - onOptionChanged: (e) => { - const hasFixedSortable = this[sortableFixedName]; - if (hasFixedSortable) { - if (e.name === 'fromIndex' || e.name === 'toIndex') { - this[anotherSortableName].option(e.name, e.value); + onOptionChanged: (e: ChangedOptionInfo) => { + const isIndexOption = e.name === 'fromIndex' || e.name === 'toIndex'; + if (this._sortableFixed && isIndexOption) { + if (isFixedSortable) { + // @ts-expect-error both sortables exist once the fixed one does + this._sortable.option(e.name, e.value); + } else { + this._sortableFixed.option(e.name, e.value); } } }, - })); + }))); - $content.toggleClass('dx-scrollable-container', isFixedTableRendering); - $content.toggleClass(CLASSES.sortableWithoutHandle, allowReordering && !rowDragging.showDragIcons); + $content.toggleClass(SCROLLABLE_CONTAINER_CLASS, this._isFixedTableRendering); + $content.toggleClass( + CLASSES.sortableWithoutHandle, + allowReordering && !rowDragging.showDragIcons, + ); } return $content; @@ -171,36 +229,34 @@ const rowsView = (Base: ModuleType) => class RowsViewRowDraggingExtend return deferred; } - private _updateSortable() { + private _updateSortable(): void { const offset = this._dataController.getRowIndexOffset(); - // @ts-expect-error - const offsetDiff = offset - this._previousOffset; - // @ts-expect-error [this._sortable, this._sortableFixed].forEach((sortable) => { const toIndex = sortable?.option('toIndex'); - // @ts-expect-error if (isDefined(toIndex) && isDefined(this._previousOffset)) { + const offsetDiff = offset - this._previousOffset; sortable?.option('toIndex', toIndex - offsetDiff); } sortable?.option('offset', offset); sortable?.update(); }); - // @ts-expect-error this._previousOffset = offset; } - protected _resizeCore() { - super._resizeCore.apply(this, arguments as any); + protected _resizeCore(): void { + super._resizeCore(); this._updateSortable(); } - private _getDraggableGridOptions(options) { + private _getDraggableGridOptions(options: ProcessedItem): DraggableGridOptions { const gridOptions = this.option(); const columns = this.getColumns(); - const $rowElement = $(this.getRowElement(options.rowIndex)); + const $rowElement = isDefined(options.rowIndex) + ? $(this.getRowElement(options.rowIndex)) + : $(); return { dataSource: [{ id: 1, parentId: 0 }], @@ -218,34 +274,37 @@ const rowsView = (Base: ModuleType) => class RowsViewRowDraggingExtend columnAutoWidth: gridOptions.columnAutoWidth, showColumnLines: gridOptions.showColumnLines, columns: columns.map((column) => ({ + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 0 falls back width: column.width || column.visibleWidth, fixed: column.fixed, fixedPosition: column.fixedPosition, })), isDragging: true, - onRowPrepared: (e) => { - const rowsView = e.component.getView('rowsView'); - $(e.rowElement).replaceWith($rowElement.eq(rowsView._isFixedTableRendering ? 1 : 0).clone()); + onRowPrepared: (e): void => { + const draggableRowsView = e.component.getView('rowsView') as RowsView & ColumnFixingRowsViewExtension; + const tableIndex = draggableRowsView._isFixedTableRendering ? 1 : 0; + + $(e.rowElement).replaceWith($rowElement.eq(tableIndex).clone()); }, }; } - private _synchronizeScrollLeftPosition(gridInstance) { + private _synchronizeScrollLeftPosition(gridInstance: InternalGrid | undefined): void { const scrollable = gridInstance?.getScrollable(); scrollable?.scrollTo({ left: this._scrollLeft }); } - private _getDraggableRowTemplate() { + private _getDraggableRowTemplate(): (options: DragTemplateData) => dxElementWrapper { return (options) => { const $rootElement = this.component.$element(); const $dataGridContainer = $('
'); setWidth($dataGridContainer, getWidth($rootElement.get(0))); const items = this._dataController.items(); - const row = items && items[options.fromIndex]; + const row = items?.[options.fromIndex]; const gridOptions = this._getDraggableGridOptions(row); - // @ts-expect-error + // @ts-expect-error _createComponent() is typed for a class, the grid passes its name this._createComponent($dataGridContainer, this.component.NAME, gridOptions); $dataGridContainer .find(`.${GRID_VIEW_CLASSES.gridBaseContainer}`) @@ -258,7 +317,7 @@ const rowsView = (Base: ModuleType) => class RowsViewRowDraggingExtend }; } - private _getHandleTemplate() { + private _getHandleTemplate(): HandleCellTemplate { return GridCoreRowDraggingDom.createHandleTemplateFunc( (string) => this.addWidgetPrefix(string), ); @@ -266,7 +325,7 @@ const rowsView = (Base: ModuleType) => class RowsViewRowDraggingExtend }; export const rowDraggingModule = { - defaultOptions() { + defaultOptions(): Pick { return { rowDragging: { showDragIcons: true, diff --git a/packages/devextreme/js/__internal/grids/grid_core/row_dragging/types.ts b/packages/devextreme/js/__internal/grids/grid_core/row_dragging/types.ts new file mode 100644 index 000000000000..56dfcfe5e3a6 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/row_dragging/types.ts @@ -0,0 +1,38 @@ +import type { DxElement } from '@js/core/element'; +import type { dxElementWrapper } from '@js/core/renderer'; +import type { Column, VisibleWidth } from '@ts/grids/grid_core/columns_controller/types'; +import type { InternalGrid, InternalGridOptions } from '@ts/grids/grid_core/types'; +import type { ViewCellOptions } from '@ts/grids/grid_core/views/types'; + +export type RowDraggingOptions = NonNullable; + +export type RowDraggingStartEvent = Parameters>[0]; + +export type RowDraggingEndEvent = Parameters>[0]; + +export type RowDraggingAddEvent = Parameters>[0]; + +export type HandleCellTemplate = ( + container: DxElement, + options: ViewCellOptions, +) => dxElementWrapper | undefined; + +export type DraggableGridOptions = Pick & { + columns: { + width: VisibleWidth; + fixed: Column['fixed']; + fixedPosition: Column['fixedPosition']; + }[]; + loadingTimeout: null; + isDragging: boolean; + onRowPrepared: (e: { component: InternalGrid; rowElement: DxElement }) => void; +}; From c30aeb258fa01bd1348153ecd46228f28a8af841 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:25:13 +0100 Subject: [PATCH 3/4] Grids: type m_pager --- .../__mock__/model/data_grid_base.ts | 5 + .../__tests__/pager_view.integration.test.ts | 26 ++- .../grids/grid_core/pager/m_pager.ts | 150 +++++++++--------- .../__internal/grids/grid_core/pager/types.ts | 3 + .../__tests__/__mock__/model/pager.ts | 13 +- .../columnsResizingReorderingModule.tests.js | 3 - .../gridView.tests.js | 4 +- .../pagerView.tests.js | 18 +-- 8 files changed, 116 insertions(+), 106 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/pager/types.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/data_grid_base.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/data_grid_base.ts index 0336d9be255d..5d8de8dd3f4a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/data_grid_base.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/data_grid_base.ts @@ -2,12 +2,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { GridBase } from '@js/common/grids'; import type { DxPromise } from '@js/core/utils/deferred'; +import { PagerModel } from '@ts/pagination/__tests__/__mock__/model/pager'; import { GridCoreModel } from './grid_core'; export abstract class DataGridBaseModel< TInstance extends GridBase = GridBase, > extends GridCoreModel { + public getPager(): PagerModel { + return new PagerModel(this.root, `.${this.addWidgetPrefix('pager')}`); + } + public apiColumnOption(id: string, name?: string, value?: any): any { const instance = this.getInstance(); diff --git a/packages/devextreme/js/__internal/grids/grid_core/pager/__tests__/pager_view.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/pager/__tests__/pager_view.integration.test.ts index 7f3f61f1dca0..c9b8a5650c62 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/pager/__tests__/pager_view.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/pager/__tests__/pager_view.integration.test.ts @@ -11,14 +11,6 @@ import { const ITEMS = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]; -interface Pagination { - option: (name: string) => unknown; -} - -const getPagination = (instance: DataGridInstance): Pagination => ( - instance.getView('pagerView') as unknown as { getPager: () => Pagination } -).getPager(); - const isPagerVisible = (instance: DataGridInstance): boolean => instance .getView('pagerView') .isVisible(); @@ -29,13 +21,14 @@ describe('PagerView', () => { describe('without a data source', () => { it('reports one page and no items', async () => { - const { instance } = await createDataGrid({ + const { component } = await createDataGrid({ pager: { visible: true }, }); + const pagination = component.getPager().getInstance(); - expect(getPagination(instance).option('pageCount')).toBe(1); - expect(getPagination(instance).option('itemCount')).toBe(0); - expect(getPagination(instance).option('hasKnownLastPage')).toBe(true); + expect(pagination.option('pageCount')).toBe(1); + expect(pagination.option('itemCount')).toBe(0); + expect(pagination.option('hasKnownLastPage')).toBe(true); }); it('is hidden in auto mode', async () => { @@ -49,15 +42,16 @@ describe('PagerView', () => { describe('with a data source', () => { it('reports the page count and the item count', async () => { - const { instance } = await createDataGrid({ + const { component } = await createDataGrid({ dataSource: ITEMS, paging: { pageSize: 2 }, pager: { visible: true }, }); + const pagination = component.getPager().getInstance(); - expect(getPagination(instance).option('pageCount')).toBe(3); - expect(getPagination(instance).option('itemCount')).toBe(5); - expect(getPagination(instance).option('hasKnownLastPage')).toBe(true); + expect(pagination.option('pageCount')).toBe(3); + expect(pagination.option('itemCount')).toBe(5); + expect(pagination.option('hasKnownLastPage')).toBe(true); }); it('is visible in auto mode when there is more than one page', async () => { diff --git a/packages/devextreme/js/__internal/grids/grid_core/pager/m_pager.ts b/packages/devextreme/js/__internal/grids/grid_core/pager/m_pager.ts index 6ea6ebddaade..3e8661e24413 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/pager/m_pager.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/pager/m_pager.ts @@ -1,39 +1,43 @@ import messageLocalization from '@js/common/core/localization/message'; import { isDefined } from '@js/core/utils/type'; import { hasWindow } from '@js/core/utils/window'; +import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; +import type { KeyboardNavigationController } from '@ts/grids/grid_core/keyboard_navigation/m_keyboard_navigation'; +import type { InternalGridOptions, OptionChanged } from '@ts/grids/grid_core/types'; +import type { PaginationProps } from '@ts/pagination/common/pagination_props'; import Pagination from '@ts/pagination/wrappers/pagination'; import modules from '../modules/modules'; +import type { PageSizes } from './types'; const PAGER_CLASS = 'pager'; export const MAX_PAGES_COUNT = 10; -const getPageIndex = function (dataController) { - // eslint-disable-next-line radix - return 1 + (parseInt(dataController.pageIndex()) || 0); -}; - // TODO getController export class PagerView extends modules.View { - private _pager: any; + private _pager?: Pagination | null; + + private _pageSizes?: PageSizes | null; - private _pageSizes: any; + private dataController!: DataController; private dataSourceController!: DataSourceController; - public init() { - const dataController = this.getController('data'); + private keyboardNavigationController!: KeyboardNavigationController; + public init(): void { + this.dataController = this.getController('data'); this.dataSourceController = this.getController('dataSource'); + this.keyboardNavigationController = this.getController('keyboardNavigation'); - dataController.changed.add((e) => { + this.dataController.changed.add((e) => { if (e && e.repaintChangesOnly) { const pager = this._pager; if (pager) { pager.option({ - pageIndex: getPageIndex(dataController), - pageSize: dataController.pageSize(), + pageIndex: this.getPageIndex(), + pageSize: this.dataController.pageSize(), pageCount: this.dataSourceController.pageCount(), itemCount: this.dataSourceController.totalCount(), hasKnownLastPage: this.dataSourceController.hasKnownLastPage(), @@ -48,22 +52,20 @@ export class PagerView extends modules.View { }); } - public dispose() { + public dispose(): void { this._pager = null; } - public optionChanged(args) { - const { name } = args; - const isPager = name === 'pager'; - const isPaging = name === 'paging'; - const isDataSource = name === 'dataSource'; - const isScrolling = name === 'scrolling'; - const dataController = this.getController('data'); + public optionChanged(args: OptionChanged): void { + const isPager = args.name === 'pager'; + const isPaging = args.name === 'paging'; + const isDataSource = args.name === 'dataSource'; + const isScrolling = args.name === 'scrolling'; if (isPager || isPaging || isScrolling || isDataSource) { args.handled = true; - if (dataController.skipProcessingPagingChange(args.fullName)) { + if (this.dataController.skipProcessingPagingChange(args.fullName)) { return; } @@ -75,47 +77,51 @@ export class PagerView extends modules.View { this._pager = null; this._invalidate(); if (hasWindow() && isPager && this.component) { - // @ts-expect-error + // @ts-expect-error the resizing controller's publicMethods() add resize() this.component.resize(); } } } } - protected _renderCore() { - const that = this; - // @ts-expect-error the view is rendered here - const $element = that.element().addClass(that.addWidgetPrefix(PAGER_CLASS)); - const pagerOptions = that.option('pager') ?? {}; - const dataController = that.getController('data'); - const keyboardController = that.getController('keyboardNavigation'); - const options: any = { + protected _renderCore(): void { + const $element = this.element(); + + if (!$element) { + return; + } + + $element.addClass(this.addWidgetPrefix(PAGER_CLASS)); + const pagerOptions = this.option('pager') ?? {}; + const options: Partial = { maxPagesCount: MAX_PAGES_COUNT, - pageIndex: getPageIndex(dataController), - pageCount: that.dataSourceController.pageCount(), - pageSize: dataController.pageSize(), + pageIndex: this.getPageIndex(), + pageCount: this.dataSourceController.pageCount(), + pageSize: this.dataController.pageSize(), showPageSizeSelector: pagerOptions.showPageSizeSelector, showInfo: pagerOptions.showInfo, displayMode: pagerOptions.displayMode, pagesNavigatorVisible: pagerOptions.visible, showNavigationButtons: pagerOptions.showNavigationButtons, label: pagerOptions.label, - allowedPageSizes: that.getPageSizes(), - itemCount: that.dataSourceController.totalCount(), - hasKnownLastPage: that.dataSourceController.hasKnownLastPage(), - rtlEnabled: that.option('rtlEnabled'), + // @ts-expect-error the grid's page sizes allow 'auto', Pagination's don't + allowedPageSizes: this.getPageSizes(), + itemCount: this.dataSourceController.totalCount(), + hasKnownLastPage: this.dataSourceController.hasKnownLastPage(), + rtlEnabled: this.option('rtlEnabled'), isGridCompatibilityMode: true, _getParentComponentRootNode: () => this.component.element(), _skipValidation: true, - pageIndexChanged(pageIndex) { - if (dataController.pageIndex() !== pageIndex - 1) { - dataController.pageIndex(pageIndex - 1); + pageIndexChanged: (pageIndex) => { + if (this.dataController.pageIndex() !== pageIndex - 1) { + this.dataController.pageIndex(pageIndex - 1); } }, - pageSizeChanged(pageSize) { - dataController.pageSize(pageSize); + pageSizeChanged: (pageSize) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget + this.dataController.pageSize(pageSize); }, - onKeyDown: (e) => keyboardController && keyboardController.executeAction('onKeyDown', e), + onKeyDown: (e) => this.keyboardNavigationController?.executeAction('onKeyDown', e), }; if (isDefined(pagerOptions.infoText)) { @@ -128,7 +134,7 @@ export class PagerView extends modules.View { } if (hasWindow()) { - this._pager = that._createComponent($element, Pagination, options); + this._pager = this._createComponent($element, Pagination, options); } else { $element .addClass('dx-pager') @@ -136,54 +142,50 @@ export class PagerView extends modules.View { } } - private getPager() { - return this._pager; + private getPageIndex(): number { + return 1 + (parseInt(String(this.dataController.pageIndex()), 10) || 0); } - private getPageSizes() { - const that = this; - const dataController = that.getController('data'); - const pagerOptions = that.option('pager'); - const allowedPageSizes = pagerOptions && pagerOptions.allowedPageSizes; + private getPageSizes(): PageSizes { + // state storing calls this before init() if the data loads synchronously + const dataController = this.getController('data'); + const pagerOptions = this.option('pager'); + const allowedPageSizes = pagerOptions?.allowedPageSizes; const pageSize = dataController.pageSize(); - if (!isDefined(that._pageSizes) || !that._pageSizes.includes(pageSize)) { - that._pageSizes = []; + if (!isDefined(this._pageSizes) || !this._pageSizes.includes(pageSize)) { + this._pageSizes = []; if (pagerOptions) { if (Array.isArray(allowedPageSizes)) { - that._pageSizes = allowedPageSizes; + this._pageSizes = allowedPageSizes; } else if (allowedPageSizes && pageSize > 1) { - that._pageSizes = [Math.floor(pageSize / 2), pageSize, pageSize * 2]; + this._pageSizes = [Math.floor(pageSize / 2), pageSize, pageSize * 2]; } } } - return that._pageSizes; + return this._pageSizes; } - public isVisible() { - const dataController = this.getController('data'); - const pagerOptions = this.option('pager'); - let pagerVisible = pagerOptions && pagerOptions.visible; - const scrolling = this.option('scrolling'); - - if (pagerVisible === 'auto') { - if (scrolling && (scrolling.mode === 'virtual' || scrolling.mode === 'infinite')) { - pagerVisible = false; - } else { - pagerVisible = this.dataSourceController.pageCount() > 1 - || (dataController.isLoaded() && !this.dataSourceController.hasKnownLastPage()); - } + public isVisible(): boolean { + const pagerVisible = this.option('pager')?.visible; + + if (pagerVisible !== 'auto') { + return !!pagerVisible; + } + + const scrollingMode = this.option('scrolling')?.mode; + + if (scrollingMode === 'virtual' || scrollingMode === 'infinite') { + return false; } - return !!pagerVisible; - } - private getHeight() { - return this.getElementHeight(); + return this.dataSourceController.pageCount() > 1 + || (this.dataController.isLoaded() && !this.dataSourceController.hasKnownLastPage()); } } export const pagerModule = { - defaultOptions() { + defaultOptions(): Pick { return { pager: { visible: 'auto', diff --git a/packages/devextreme/js/__internal/grids/grid_core/pager/types.ts b/packages/devextreme/js/__internal/grids/grid_core/pager/types.ts new file mode 100644 index 000000000000..a937673989ce --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/pager/types.ts @@ -0,0 +1,3 @@ +import type { PagerPageSize } from '@js/common/grids'; + +export type PageSizes = (number | PagerPageSize)[]; diff --git a/packages/devextreme/js/__internal/pagination/__tests__/__mock__/model/pager.ts b/packages/devextreme/js/__internal/pagination/__tests__/__mock__/model/pager.ts index 5e7d1b389f5b..d48ae63f65dd 100644 --- a/packages/devextreme/js/__internal/pagination/__tests__/__mock__/model/pager.ts +++ b/packages/devextreme/js/__internal/pagination/__tests__/__mock__/model/pager.ts @@ -1,11 +1,20 @@ +import Pagination from '@ts/pagination/wrappers/pagination'; + const CLASSES = { pagination: 'dx-pagination', }; export class PagerModel { - constructor(private readonly root: HTMLElement) {} + constructor( + private readonly root: HTMLElement, + private readonly selector = `.${CLASSES.pagination}`, + ) {} public getElement(): HTMLElement { - return this.root.querySelector(`.${CLASSES.pagination}`) as HTMLElement; + return this.root.querySelector(this.selector) as HTMLElement; + } + + public getInstance(): Pagination { + return Pagination.getInstance(this.getElement()) as Pagination; } } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js index 3816f272bbfa..cdc288ccb4fc 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js @@ -1654,9 +1654,6 @@ QUnit.module('Columns resizing', { QUnit.test('Update height of separator by headers and rows height with pager', function(assert) { // arrange this.component._controllers.tablePosition = new columnsResizingReordering.TablePositionViewController(this.component); - this.component._views.pagerView.getHeight = function() { - return 1; - }; const testElement = $('#container').css({ height: 500, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js index 69b2094ba85c..a73e108be1b9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js @@ -685,10 +685,10 @@ QUnit.module('Grid view', { const pagerView = gridView.getView('pagerView'); // B232626 - assert.roughEqual(columnsHeaderViewContainer[0].offsetHeight + rowsViewViewContainer[0].offsetHeight + pagerView.getHeight(), 200, 1.01); + assert.roughEqual(columnsHeaderViewContainer[0].offsetHeight + rowsViewViewContainer[0].offsetHeight + pagerView.getElementHeight(), 200, 1.01); assert.notStrictEqual(columnsHeaderViewContainer[0].offsetHeight, 0); assert.notStrictEqual(rowsViewViewContainer[0].offsetHeight, 0); - assert.notStrictEqual(pagerView.getHeight(), 0); + assert.notStrictEqual(pagerView.getElementHeight(), 0); }); QUnit.test('RowsView height calculation when no data', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/pagerView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/pagerView.tests.js index 30dd86c9ac15..281121703460 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/pagerView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/pagerView.tests.js @@ -70,7 +70,7 @@ QUnit.module('Pager', { // act pagerView.render(testElement); - const pager = pagerView.getPager(); + const pager = Pager.getInstance(pagerView.element()); // assert assert.ok(!pager); @@ -86,7 +86,7 @@ QUnit.module('Pager', { // act pagerView.render(testElement); - const pager = pagerView.getPager(); + const pager = Pager.getInstance(pagerView.element()); // assert assert.equal(testElement.find('.dx-pager').length, 1, 'pager element'); @@ -145,7 +145,7 @@ QUnit.module('Pager', { this.dataController.pageIndex(13); // assert - assert.equal(pagerView.getPager().option('pageIndex'), 14, 'page index'); + assert.equal(Pager.getInstance(pagerView.element()).option('pageIndex'), 14, 'page index'); }); // T211403 @@ -161,7 +161,7 @@ QUnit.module('Pager', { this.clock.tick(10); // assert - assert.equal(pagerView.getPager().option('pageIndex'), 15, 'page index'); + assert.equal(Pager.getInstance(pagerView.element()).option('pageIndex'), 15, 'page index'); }); // T220755 @@ -176,7 +176,7 @@ QUnit.module('Pager', { this.clock.tick(10); // assert - assert.equal(pagerView.getPager().option('pageIndex'), 15, 'page index changed'); + assert.equal(Pager.getInstance(pagerView.element()).option('pageIndex'), 15, 'page index changed'); }); QUnit.test('Page size is changed from dataController', function(assert) { @@ -189,7 +189,7 @@ QUnit.module('Pager', { this.dataController.pageSize(9); // assert - assert.equal(pagerView.getPager().option('pageSize'), 9, 'pageSize'); + assert.equal(Pager.getInstance(pagerView.element()).option('pageSize'), 9, 'pageSize'); }); QUnit.test('HasKnownLastPage is changed from dataController', function(assert) { @@ -266,7 +266,7 @@ QUnit.module('Pager', { pagerView.render(testElement); - sinon.spy(pagerView.getPager(), 'option'); + sinon.spy(Pager.getInstance(pagerView.element()), 'option'); assert.equal(pagerView._createComponent.callCount, 1, '_createComponent call count before partial update'); @@ -277,8 +277,8 @@ QUnit.module('Pager', { assert.equal(pagerView._createComponent.callCount, 1, '_createComponent call count after partial update'); // call count increased because the component should check if it need to perform the validation - assert.equal(pagerView.getPager().option.callCount, 2, 'pager option call count after partial update'); - assert.deepEqual(pagerView.getPager().option.getCall(0).args, [{ + assert.equal(Pager.getInstance(pagerView.element()).option.callCount, 2, 'pager option call count after partial update'); + assert.deepEqual(Pager.getInstance(pagerView.element()).option.getCall(0).args, [{ hasKnownLastPage: true, // T697587 itemCount: 143, // #7259 pageCount: 21, From 0ef3f4310f8b26ff67f32c07da83acbf443a2da9 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:59:42 +0100 Subject: [PATCH 4/4] Grids: use combined type aliases in search and row dragging extenders --- .../grids/grid_core/column_fixing/types.ts | 4 ++++ .../grids/grid_core/row_dragging/m_row_dragging.ts | 14 +++++++------- .../__internal/grids/grid_core/search/m_search.ts | 5 +---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_fixing/types.ts b/packages/devextreme/js/__internal/grids/grid_core/column_fixing/types.ts index 5583647ea376..e20ebba6ede9 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_fixing/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_fixing/types.ts @@ -1,3 +1,7 @@ +import type { RowsView } from '@ts/grids/grid_core/views/rows_view'; + export interface ColumnFixingRowsViewExtension { _isFixedTableRendering: boolean; } + +export type ColumnFixingRowsView = RowsView & ColumnFixingRowsViewExtension; diff --git a/packages/devextreme/js/__internal/grids/grid_core/row_dragging/m_row_dragging.ts b/packages/devextreme/js/__internal/grids/grid_core/row_dragging/m_row_dragging.ts index f17819b7e207..9cbeac9d2d92 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/row_dragging/m_row_dragging.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/row_dragging/m_row_dragging.ts @@ -1,3 +1,6 @@ +// ThemeBuilder reads the `// STYLE sortable` comment from the public module +import '@js/ui/sortable'; + import type { ChangedOptionInfo } from '@js/common/core/events'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; @@ -11,7 +14,7 @@ import { WIDGET_CLASS } from '@ts/core/widget/widget'; import type { DragElementShownArgs, DragEventArgs, ElementOffsetOptions } from '@ts/draggable.types'; /** @architectureLeak data_grid/summary: footer rows are not draggable */ import { DATAGRID_FOOTER_ROW_CLASS } from '@ts/grids/data_grid/summary/const'; -import type { ColumnFixingRowsViewExtension } from '@ts/grids/grid_core/column_fixing/types'; +import type { ColumnFixingRowsView } from '@ts/grids/grid_core/column_fixing/types'; import { CLASSES as COLUMN_HEADERS_CLASSES } from '@ts/grids/grid_core/column_headers/const'; import type { DataChange, ProcessedItem } from '@ts/grids/grid_core/data_controller/types'; import { CLASSES as GRID_VIEW_CLASSES } from '@ts/grids/grid_core/grid_view/const'; @@ -23,7 +26,6 @@ import type { } from '@ts/grids/grid_core/types'; import gridCoreUtils from '@ts/grids/grid_core/utils'; import { CLASSES as VIEW_CLASSES } from '@ts/grids/grid_core/views/const'; -import type { RowsView } from '@ts/grids/grid_core/views/rows_view'; import { VIRTUAL_ROW_CLASS } from '@ts/grids/grid_core/virtual_scrolling/const'; import Sortable from '@ts/sortable'; import { SCROLLABLE_CONTAINER_CLASS } from '@ts/ui/scroll_view/consts'; @@ -40,10 +42,8 @@ import type { } from './types'; const rowsView = ( - Base: ModuleType, -): ModuleType< - RowsView & ColumnFixingRowsViewExtension -> => class RowsViewRowDraggingExtender extends Base { + Base: ModuleType, +): ModuleType => class RowsViewRowDraggingExtender extends Base { private _sortable?: Sortable; private _sortableFixed?: Sortable; @@ -281,7 +281,7 @@ const rowsView = ( })), isDragging: true, onRowPrepared: (e): void => { - const draggableRowsView = e.component.getView('rowsView') as RowsView & ColumnFixingRowsViewExtension; + const draggableRowsView = e.component.getView('rowsView') as ColumnFixingRowsView; const tableIndex = draggableRowsView._isFixedTableRendering ? 1 : 0; $(e.rowElement).replaceWith($rowElement.eq(tableIndex).clone()); diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts index 0e0e2dbb5bec..000ea801ae86 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts @@ -23,16 +23,13 @@ import { CLASSES, FILTERING_TIMEOUT, SEARCH_PANEL_ITEM_NAME } from './const'; import type { SearchCellOptions, SearchDataController, - SearchDataControllerExtension, SearchHighlightParams, } from './types'; import { allowSearch, createSearchExpression, getFormattedSearchText } from './utils'; const dataController = ( base: ModuleType, -): ModuleType< - DataController & SearchDataControllerExtension -> => class SearchDataControllerExtender extends base { +): ModuleType => class SearchDataControllerExtender extends base { public optionChanged(args: OptionChanged): void { switch (args.fullName) { case 'searchPanel.text':