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':
|