From 7362f67a5f2231ad482a1bcc83420d9974547cdb Mon Sep 17 00:00:00 2001 From: Andrei Kharitonov Date: Mon, 26 Oct 2026 17:25:19 +0100 Subject: [PATCH 1/2] Core: type position and the collection data controller --- .../common/core/animation/m_position.ts | 264 +++++++++++------- .../__internal/common/core/animation/types.ts | 54 ++++ .../ui/collection/collection_widget.edit.ts | 7 +- .../ui/collection/m_data_controller.ts | 104 ++++--- .../ui/drop_down_editor/drop_down_editor.ts | 13 +- .../ui/drop_down_editor/drop_down_list.ts | 5 +- .../js/__internal/ui/list/list.edit.search.ts | 1 - .../ui/overlay/overlay_position_controller.ts | 3 +- .../js/__internal/ui/popover/popover.ts | 6 +- .../devextreme/js/__internal/ui/select_box.ts | 1 - .../devextreme/js/__internal/ui/tag_box.ts | 2 +- 11 files changed, 299 insertions(+), 161 deletions(-) create mode 100644 packages/devextreme/js/__internal/common/core/animation/types.ts diff --git a/packages/devextreme/js/__internal/common/core/animation/m_position.ts b/packages/devextreme/js/__internal/common/core/animation/m_position.ts index e26067fe4d54..d7c64787b549 100644 --- a/packages/devextreme/js/__internal/common/core/animation/m_position.ts +++ b/packages/devextreme/js/__internal/common/core/animation/m_position.ts @@ -1,6 +1,7 @@ import { move, resetPosition } from '@js/common/core/animation/translator'; import devices from '@js/core/devices'; import domAdapter from '@js/core/dom_adapter'; +import type { Coordinates, dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; @@ -14,6 +15,15 @@ import { isDefined, isWindow } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; import { pairToObject, splitPair } from '@ts/core/utils/common'; +import type { + AxisBounds, + AxisPositionData, + CollisionSide, + PositionOptions, + PositionResult, + PositionTarget, +} from './types'; + const window = getWindow(); const horzRe = /left|right/; @@ -21,7 +31,7 @@ const vertRe = /top|bottom/; const collisionRe = /fit|flip|none/; const scaleRe = /scale\(.+?\)/; -const normalizeAlign = function (raw) { +const normalizeAlign = function normalizeAlign(raw: unknown): { h: string; v: string } { const result = { h: 'center', v: 'center', @@ -30,7 +40,7 @@ const normalizeAlign = function (raw) { const pair = splitPair(raw); if (pair) { - each(pair, function () { + each(pair, function normalizeAlignPart(this: unknown) { const w = String(this).toLowerCase(); if (horzRe.test(w)) { result.h = w; @@ -43,11 +53,14 @@ const normalizeAlign = function (raw) { return result; }; -const normalizeOffset = function (raw, preventRound) { +const normalizeOffset = function normalizeOffset( + raw: unknown, + preventRound?: boolean, +): { h: number; v: number } { return pairToObject(raw, preventRound); }; -const normalizeCollision = function (raw) { +const normalizeCollision = function normalizeCollision(raw: unknown): { h: string; v: string } { const pair = splitPair(raw); let h = String(pair && pair[0]).toLowerCase(); let v = String(pair && pair[1]).toLowerCase(); @@ -59,10 +72,10 @@ const normalizeCollision = function (raw) { v = h; } - return { h: h, v: v }; + return { h, v }; }; -const getAlignFactor = function (align) { +const getAlignFactor = function getAlignFactor(align: string): number { switch (align) { case 'center': return 0.5; @@ -74,7 +87,7 @@ const getAlignFactor = function (align) { } }; -const inverseAlign = function (align) { +const inverseAlign = function inverseAlign(align: string): string { switch (align) { case 'left': return 'right'; @@ -89,7 +102,10 @@ const inverseAlign = function (align) { } }; -const calculateOversize = function (data, bounds) { +const calculateOversize = function calculateOversize( + data: AxisPositionData, + bounds: AxisBounds, +): number { let oversize = 0; if (data.myLocation < bounds.min) { @@ -103,7 +119,11 @@ const calculateOversize = function (data, bounds) { return oversize; }; -const collisionSide = function (direction, data, bounds) { +const collisionSide = function collisionSide( + direction: 'h' | 'v', + data: AxisPositionData, + bounds: AxisBounds, +): CollisionSide { if (data.myLocation < bounds.min) { return direction === 'h' ? 'left' : 'top'; } @@ -114,16 +134,24 @@ const collisionSide = function (direction, data, bounds) { }; // TODO: rename? -const initMyLocation = function (data) { +const initMyLocation = function initMyLocation(data: AxisPositionData): void { data.myLocation = data.atLocation + getAlignFactor(data.atAlign) * data.atSize - getAlignFactor(data.myAlign) * data.mySize + data.offset; }; -const collisionResolvers = { +interface CollisionResolvers { + fit: (data: AxisPositionData, bounds: AxisBounds) => void; + flip: (data: AxisPositionData, bounds: AxisBounds) => void; + // eslint-disable-next-line spellcheck/spell-checker + flipfit: (data: AxisPositionData, bounds: AxisBounds) => void; + none: (data: AxisPositionData) => void; +} - fit: function (data, bounds) { +const collisionResolvers: CollisionResolvers = { + + fit(data, bounds) { let result = false; if (data.myLocation > bounds.max) { data.myLocation = bounds.max; @@ -137,7 +165,7 @@ const collisionResolvers = { data.fit = result; }, - flip: function (data, bounds) { + flip(data, bounds) { data.flip = false; if (data.myAlign === 'center' && data.atAlign === 'center') { @@ -154,7 +182,10 @@ const collisionResolvers = { initMyLocation(inverseData); inverseData.oversize = calculateOversize(inverseData, bounds); - if ((inverseData.myLocation >= bounds.min && inverseData.myLocation <= bounds.max) || data.oversize > inverseData.oversize) { + if ( + (inverseData.myLocation >= bounds.min && inverseData.myLocation <= bounds.max) + || data.oversize > inverseData.oversize + ) { data.myLocation = inverseData.myLocation; data.oversize = inverseData.oversize; data.flip = true; @@ -163,19 +194,20 @@ const collisionResolvers = { }, // eslint-disable-next-line spellcheck/spell-checker - flipfit: function (data, bounds) { + flipfit(data, bounds) { this.flip(data, bounds); this.fit(data, bounds); }, - none: function (data) { + none(data) { data.oversize = 0; }, }; -let scrollbarWidth; +// eslint-disable-next-line @typescript-eslint/init-declarations -- set by calculateScrollbarWidth +let scrollbarWidth: number | undefined; -const calculateScrollbarWidth = function () { +const calculateScrollbarWidth = function calculateScrollbarWidth(): void { const $scrollDiv = $('
').css({ width: 100, height: 100, @@ -183,7 +215,7 @@ const calculateScrollbarWidth = function () { position: 'absolute', top: -9999, }).appendTo($('body')); - // @ts-expect-error + // @ts-expect-error get(0) is typed as Element const result = $scrollDiv.get(0).offsetWidth - $scrollDiv.get(0).clientWidth; $scrollDiv.remove(); @@ -206,27 +238,63 @@ const defaultPositionResult = { }, }; -interface AxisPositionResult { - location: number; - flip: boolean; - fit: boolean; - oversize: number; - collisionSide?: string; -} +// NOTE: Setting the 'element.style' requires creating attributeNode when both of the conditions +// met: +// - a form contains an input with the name property set to "style"; +// - a form contains a dx-validator (or other popup widget). +// T941581 +const setScaleProperty = function setScaleProperty( + element: Element & { style?: unknown }, + scale: string, + styleAttr: string, + isEmpty: boolean, +): void { + const stylePropIsValid = isDefined(element.style) && !domAdapter.isNode(element.style); + const newStyleValue = isEmpty ? styleAttr.replace(scale, '') : styleAttr; -interface PositionResult { - h: AxisPositionResult; - v: AxisPositionResult; - precise?: boolean; -} + if (stylePropIsValid) { + setStyle(element, newStyleValue, false); + } else { + const styleAttributeNode = domAdapter.createAttribute('style'); + styleAttributeNode.value = newStyleValue; + element.setAttributeNode(styleAttributeNode); + } +}; + +const getOffsetWithoutScale = function getOffsetWithoutScale( + $startElement: dxElementWrapper, + $currentElement: dxElementWrapper = $startElement, +): Coordinates | undefined { + const currentElement = $currentElement.get(0); + if (!currentElement) { + return $startElement.offset(); + } + + const style = currentElement.getAttribute?.('style') ?? ''; + const scale = scaleRe.exec(style)?.[0]; + // eslint-disable-next-line @typescript-eslint/init-declarations -- set in both branches + let offset: Coordinates | undefined; + + if (scale) { + setScaleProperty(currentElement, scale, style, true); + offset = getOffsetWithoutScale($startElement, $currentElement.parent()); + setScaleProperty(currentElement, scale, style, false); + } else { + offset = getOffsetWithoutScale($startElement, $currentElement.parent()); + } + + return offset; +}; -const calculatePosition = function (what, options) { +const calculatePosition = function calculatePosition( + what: PositionTarget, + options?: PositionOptions, +): PositionResult { const $what = $(what); - const currentOffset = $what.offset(); + // @ts-expect-error offset() is typed as possibly undefined + const currentOffset: Coordinates = $what.offset(); const result: PositionResult = extend(true, {}, defaultPositionResult, { - // @ts-expect-error h: { location: currentOffset.left }, - // @ts-expect-error v: { location: currentOffset.top }, }); @@ -236,13 +304,16 @@ const calculatePosition = function (what, options) { const my = normalizeAlign(options.my); const at = normalizeAlign(options.at); - let of = ($(options.of).length && options.of) || window; + // @ts-expect-error renderer() is typed without event objects + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- element, selector, window, event + let of: any = ($(options.of).length && options.of) || window; const offset = normalizeOffset(options.offset, options.precise); const collision = normalizeCollision(options.collision); const { boundary } = options; const boundaryOffset = normalizeOffset(options.boundaryOffset, options.precise); - const h: any = { + const h: AxisPositionData = { + // @ts-expect-error getOuterWidth() can be undefined; the location fields are set below mySize: getOuterWidth($what), myAlign: my.h, atAlign: at.h, @@ -251,7 +322,8 @@ const calculatePosition = function (what, options) { boundaryOffset: boundaryOffset.h, }; - const v: any = { + const v: AxisPositionData = { + // @ts-expect-error getOuterHeight() can be undefined; the location fields are set below mySize: getOuterHeight($what), myAlign: my.v, atAlign: at.v, @@ -290,7 +362,8 @@ const calculatePosition = function (what, options) { v.atSize = getHeight(of); } else { const ofRect = getBoundingRect(of.get(0)); - const o = getOffsetWithoutScale(of); + // @ts-expect-error offset() is typed as possibly undefined + const o: Coordinates = getOffsetWithoutScale(of); h.atLocation = o.left; v.atLocation = o.top; h.atSize = Math.max(ofRect.width, getOuterWidth(of)); @@ -301,12 +374,14 @@ const calculatePosition = function (what, options) { initMyLocation(h); initMyLocation(v); - const bounds = (function () { + const bounds = (function calculateBounds(): { h: AxisBounds; v: AxisBounds } { const win = $(window); const windowWidth = getWidth(win.get(0)); const windowHeight = getHeight(win.get(0)); - let left = win.scrollLeft(); - let top = win.scrollTop(); + // @ts-expect-error scrollLeft() is typed as a setter only + let left: number = win.scrollLeft(); + // @ts-expect-error scrollTop() is typed as a setter only + let top: number = win.scrollTop(); const documentElement = domAdapter.getDocumentElement(); const hZoomLevel = touch ? documentElement.clientWidth / windowWidth : 1; const vZoomLevel = touch ? documentElement.clientHeight / windowHeight : 1; @@ -320,10 +395,9 @@ const calculatePosition = function (what, options) { if (boundary && !isWindow(boundary)) { const $boundary = $(boundary); - const boundaryPosition = $boundary.offset(); - // @ts-expect-error + // @ts-expect-error offset() is typed as possibly undefined + const boundaryPosition: Coordinates = $boundary.offset(); left = boundaryPosition.left; - // @ts-expect-error top = boundaryPosition.top; boundaryWidth = Number(getWidth($boundary)); @@ -333,16 +407,14 @@ const calculatePosition = function (what, options) { return { h: { min: left + h.boundaryOffset, - // @ts-expect-error max: left + boundaryWidth / hZoomLevel - h.mySize - h.boundaryOffset, }, v: { min: top + v.boundaryOffset, - // @ts-expect-error max: top + boundaryHeight / vZoomLevel - v.mySize - v.boundaryOffset, }, }; - })(); + }()); h.oversize = calculateOversize(h, bounds.h); v.oversize = calculateOversize(v, bounds.v); @@ -358,61 +430,44 @@ const calculatePosition = function (what, options) { collisionResolvers[v.collision](v, bounds.v); } - const preciser = function (number) { + const preciser = function preciser(number: number): number { return options.precise ? number : Math.round(number); }; extend(true, result, { h: { - location: preciser(h.myLocation), oversize: preciser(h.oversize), fit: h.fit, flip: h.flip, collisionSide: h.collisionSide, + location: preciser(h.myLocation), + oversize: preciser(h.oversize), + fit: h.fit, + flip: h.flip, + collisionSide: h.collisionSide, }, v: { - location: preciser(v.myLocation), oversize: preciser(v.oversize), fit: v.fit, flip: v.flip, collisionSide: v.collisionSide, + location: preciser(v.myLocation), + oversize: preciser(v.oversize), + fit: v.fit, + flip: v.flip, + collisionSide: v.collisionSide, }, precise: options.precise, }); return result; }; -// NOTE: Setting the 'element.style' requires creating attributeNode when both of the conditions met: -// - a form contains an input with the name property set to "style"; -// - a form contains a dx-validator (or other popup widget). -// T941581 -const setScaleProperty = function (element, scale, styleAttr, isEmpty) { - const stylePropIsValid = isDefined(element.style) && !domAdapter.isNode(element.style); - const newStyleValue = isEmpty ? styleAttr.replace(scale, '') : styleAttr; - - if (stylePropIsValid) { - setStyle(element, newStyleValue, false); - } else { - const styleAttributeNode = domAdapter.createAttribute('style'); - styleAttributeNode.value = newStyleValue; - element.setAttributeNode(styleAttributeNode); - } -}; - -const getOffsetWithoutScale = function ($startElement, $currentElement = $startElement) { - const currentElement = $currentElement.get(0); - if (!currentElement) { - return $startElement.offset(); - } - - const style = currentElement.getAttribute?.('style') || ''; - const scale = style.match(scaleRe)?.[0]; - let offset; - - if (scale) { - setScaleProperty(currentElement, scale, style, true); - offset = getOffsetWithoutScale($startElement, $currentElement.parent()); - setScaleProperty(currentElement, scale, style, false); - } else { - offset = getOffsetWithoutScale($startElement, $currentElement.parent()); - } - - return offset; -}; -const position = function (what, options?) { +function position(what: PositionTarget): Coordinates | undefined; +function position( + what: PositionTarget, + options: PositionOptions | PositionResult, +): PositionResult | undefined; +function position( + what: PositionTarget, + options?: PositionOptions | PositionResult, +): Coordinates | PositionResult | undefined; +function position( + what: PositionTarget, + options?: PositionOptions | PositionResult, +): Coordinates | PositionResult | undefined { const $what = $(what); if (!$what.length) { @@ -425,10 +480,13 @@ const position = function (what, options?) { resetPosition($what, true); - const offset = getOffsetWithoutScale($what); - const targetPosition = options.h && options.v ? options : calculatePosition($what, options); + // @ts-expect-error offset() is typed as possibly undefined + const offset: Coordinates = getOffsetWithoutScale($what); + const targetPosition = options.h && options.v + ? options + : calculatePosition($what, options); - const preciser = function (number) { + const preciser = function preciser(number: number): number { return options.precise ? number : Math.round(number); }; @@ -438,30 +496,34 @@ const position = function (what, options?) { }); return targetPosition; -} as any; +} -const offset = function (element) { - element = $(element).get(0); - if (isWindow(element)) { +const offset = function offset(element: PositionOptions['of']): Coordinates | null | undefined { + // @ts-expect-error renderer() is typed without event objects + const domElement = $(element).get(0); + if (isWindow(domElement)) { return null; - } if (element && 'pageY' in element && 'pageX' in element) { - return { top: element.pageY, left: element.pageX }; + } if (domElement && 'pageY' in domElement && 'pageX' in domElement) { + // @ts-expect-error get(0) is typed as Element, but an event has pageX and pageY + return { top: domElement.pageY, left: domElement.pageX }; } - return $(element).offset(); + return $(domElement).offset(); }; +// @ts-expect-error inverseAlign is assigned right below if (!position.inverseAlign) { position.inverseAlign = inverseAlign; } +// @ts-expect-error normalizeAlign is assigned right below if (!position.normalizeAlign) { position.normalizeAlign = normalizeAlign; } export default { - calculateScrollbarWidth: calculateScrollbarWidth, + calculateScrollbarWidth, calculate: calculatePosition, setup: position, - offset: offset, + offset, }; diff --git a/packages/devextreme/js/__internal/common/core/animation/types.ts b/packages/devextreme/js/__internal/common/core/animation/types.ts new file mode 100644 index 000000000000..67ac6a8e6fa5 --- /dev/null +++ b/packages/devextreme/js/__internal/common/core/animation/types.ts @@ -0,0 +1,54 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import type { DxEvent } from '@js/events'; + +export type PositionTarget = string | EventTarget | dxElementWrapper | null | undefined; + +export type CollisionSide = 'left' | 'right' | 'top' | 'bottom' | 'none'; + +export interface AxisPositionResult { + location: number; + flip: boolean; + fit: boolean; + oversize: number; + collisionSide?: CollisionSide; +} + +export interface PositionResult { + h: AxisPositionResult; + v: AxisPositionResult; + precise?: boolean; +} + +export interface PositionOptions { + my?: unknown; + at?: unknown; + of?: PositionTarget | DxEvent; + offset?: unknown; + collision?: unknown; + boundary?: PositionTarget | 0; + boundaryOffset?: unknown; + precise?: boolean; + h?: undefined; + v?: undefined; +} + +export interface AxisPositionData { + mySize: number; + myAlign: string; + atAlign: string; + offset: number; + collision: string; + boundaryOffset: number; + atLocation: number; + atSize: number; + myLocation: number; + oversize: number; + collisionSide?: CollisionSide; + fit?: boolean; + flip?: boolean; +} + +export interface AxisBounds { + min: number; + max: number; +} diff --git a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts index 85bcffe6447f..42b2d2dcf67c 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts @@ -192,7 +192,6 @@ class CollectionWidget< } _getCombinedFilter(): unknown { - // @ts-expect-error arguments return this._dataController.filter(); } @@ -272,7 +271,7 @@ class CollectionWidget< options.userData = dataController.userData(); if (dataController.store()) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + // @ts-expect-error the store is typed for StoreLoadOptions and unknown items return dataController.loadFromStore(options).done((loadResult) => { if (that._disposed) { return; @@ -284,8 +283,7 @@ class CollectionWidget< } return Deferred().resolve(this.plainItems()); }, - // eslint-disable-next-line @stylistic/max-len - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/explicit-function-return-type + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type dataFields: () => this._dataController.select(), plainItems: itemsGetter.bind(this._editStrategy), }); @@ -816,7 +814,6 @@ class CollectionWidget< _refreshLastPage(): DeferredObj { this._expectLastItemLoading(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this._dataController.load(); } diff --git a/packages/devextreme/js/__internal/ui/collection/m_data_controller.ts b/packages/devextreme/js/__internal/ui/collection/m_data_controller.ts index 64d0247cb2d1..ae0cfcd0c201 100644 --- a/packages/devextreme/js/__internal/ui/collection/m_data_controller.ts +++ b/packages/devextreme/js/__internal/ui/collection/m_data_controller.ts @@ -1,11 +1,16 @@ import { noop } from '@js/core/utils/common'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; +import type Store from '@ts/data/abstract_store'; +import type { StoreKey } from '@ts/data/abstract_store'; +import type { DataSource } from '@ts/data/data_source/data_source'; +import type { DataSourceEventName, StoreLoadOptions } from '@ts/data/data_source/types'; const DataControllerMock = { - load: () => Deferred().reject(), - loadSingle: () => Deferred().reject(), - loadFromStore: () => Deferred().reject(), - loadNextPage: () => Deferred().reject(), + load: (): DeferredObj => Deferred().reject(), + loadSingle: (): DeferredObj => Deferred().reject(), + loadFromStore: (): DeferredObj => Deferred().reject(), + loadNextPage: (): DeferredObj => Deferred().reject(), loadOptions: noop, userData: noop, cancel: noop, @@ -37,104 +42,113 @@ const DataControllerMock = { }; class DataController { - _dataSource: any; + private readonly _dataSource!: DataSource; - constructor(dataSource) { + constructor(dataSource: DataSource | null | undefined) { if (!dataSource) { - // @ts-expect-error + // @ts-expect-error the mock has the same methods, all of them no-ops // eslint-disable-next-line no-constructor-return return DataControllerMock; } this._dataSource = dataSource; } - load() { + load(): DeferredObj { return this._dataSource.load(); } - loadSingle(propName, propValue) { + loadSingle(propName: unknown, propValue?: unknown): DeferredObj { if (arguments.length < 2) { - propValue = propName; - propName = this.key(); + return this._dataSource.loadSingle(this.key(), propName); } return this._dataSource.loadSingle(propName, propValue); } - loadFromStore(loadOptions) { + loadFromStore(loadOptions?: StoreLoadOptions): DeferredObj { return this.store().load(loadOptions); } - loadNextPage() { + loadNextPage(): DeferredObj { this.pageIndex(1 + this.pageIndex()); return this.load(); } - loadOptions() { + loadOptions(): StoreLoadOptions { return this._dataSource.loadOptions(); } - userData() { + userData(): Record { return this._dataSource._userData; } - cancel(operationId) { + cancel(operationId: number): void { this._dataSource.cancel(operationId); } - cancelAll() { + cancelAll(): void { this._dataSource.cancelAll(); } - filter(filter) { + filter(filter?: undefined): StoreLoadOptions['filter']; + filter(filter: NonNullable): void; + filter(filter?: StoreLoadOptions['filter']): StoreLoadOptions['filter'] { return this._dataSource.filter(filter); } - addSearchFilter(storeLoadOptions) { + addSearchFilter(storeLoadOptions: StoreLoadOptions): void { this._dataSource._addSearchFilter(storeLoadOptions); } - group(group?) { + group(group?: undefined): StoreLoadOptions['group']; + group(group: NonNullable): void; + group(group?: StoreLoadOptions['group']): StoreLoadOptions['group'] { + // @ts-expect-error DataSource accessors are typed without the undefined getter argument return this._dataSource.group(group); } - paginate() { + paginate(): boolean | undefined { return this._dataSource.paginate(); } - pageSize() { + pageSize(): number { return this._dataSource._pageSize; } - pageIndex(pageIndex?: number) { + pageIndex(): number; + pageIndex(pageIndex: number): void; + pageIndex(pageIndex?: number): number | undefined { + // @ts-expect-error DataSource accessors are typed without the undefined getter argument return this._dataSource.pageIndex(pageIndex); } - resetDataSourcePageIndex() { + resetDataSourcePageIndex(): void { if (this.pageIndex()) { this.pageIndex(0); this.load(); } } - totalCount() { + totalCount(): number { return this._dataSource.totalCount(); } - isLastPage() { + isLastPage(): boolean { return this._dataSource.isLastPage() || !this._dataSource._pageSize; } - isLoading() { + isLoading(): boolean { return this._dataSource.isLoading(); } - isLoaded() { + isLoaded(): boolean { return this._dataSource.isLoaded(); } - searchValue(value) { + searchValue(): StoreLoadOptions['searchValue']; + searchValue(value: StoreLoadOptions['searchValue']): void; + searchValue(value?: StoreLoadOptions['searchValue']): StoreLoadOptions['searchValue'] { if (!arguments.length) { return this._dataSource.searchValue(); } @@ -142,55 +156,61 @@ class DataController { return this._dataSource.searchValue(value); } - searchOperation(operation) { + searchOperation(operation?: unknown): StoreLoadOptions['searchOperation'] { + // @ts-expect-error DataSource.searchOperation() treats a non-string argument as the getter return this._dataSource.searchOperation(operation); } - searchExpr(expr) { + searchExpr(): StoreLoadOptions['searchExpr']; + searchExpr(expr: NonNullable): void; + searchExpr(expr?: StoreLoadOptions['searchExpr']): StoreLoadOptions['searchExpr'] { if (!arguments.length) { return this._dataSource.searchExpr(); } + // @ts-expect-error DataSource.searchExpr() is typed without an array or undefined argument return this._dataSource.searchExpr(expr); } - select() { - return this._dataSource.select(...arguments); + select(): StoreLoadOptions['select']; + select(...selectExpr: NonNullable[]): void; + select(...selectExpr: NonNullable[]): unknown { + return this._dataSource.select(...selectExpr); } - key() { + key(): StoreKey | undefined { return this._dataSource.key(); } - keyOf(item) { + keyOf(item: unknown): unknown { return this.store().keyOf(item); } - store() { + store(): Store { return this._dataSource.store(); } - items() { + items(): unknown[] { return this._dataSource.items(); } - applyMapFunction(data) { + applyMapFunction(data: unknown[]): unknown[] { return this._dataSource._applyMapFunction(data); } - getDataSource() { + getDataSource(): DataSource | null { return this._dataSource || null; } - reload() { + reload(): DeferredObj { return this._dataSource.reload(); } - on(event, handler): void { + on(event: DataSourceEventName, handler: Function): void { this._dataSource.on(event, handler); } - off(event, handler): void { + off(event: DataSourceEventName, handler?: Function): void { this._dataSource.off(event, handler); } } diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_editor.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_editor.ts index 3f9853d9d5f4..4a1b8114336b 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_editor.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_editor.ts @@ -1001,12 +1001,15 @@ class DropDownEditor< let positionSide = 'bottom'; if (this._popup?.option('visible')) { - const { top: myTop } = animationPosition.setup(this.$element()); + const myOffset = animationPosition.setup(this.$element()); - const { top: popupTop } = animationPosition.setup(this._popup.$content()); - const { popupPosition } = this.option(); - // @ts-expect-error Should be updated on PositionConfig level - positionSide = (myTop + popupPosition.offset.v) > popupTop ? 'bottom' : 'top'; + const popupOffset = animationPosition.setup(this._popup.$content()); + + if (myOffset && popupOffset) { + const { popupPosition } = this.option(); + // @ts-expect-error Should be updated on PositionConfig level + positionSide = (myOffset.top + popupPosition.offset.v) > popupOffset.top ? 'bottom' : 'top'; + } } return positionSide; diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_list.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_list.ts index 81e34094c987..c798b34e448a 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_list.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_list.ts @@ -740,7 +740,7 @@ class DropDownList< const isMinSearchLengthExceeded = this._isMinSearchLengthExceeded(); const { showDataBeforeSearch, minSearchLength } = this.option(); - return this._dataController.isLoaded() as boolean + return this._dataController.isLoaded() && Boolean(showDataBeforeSearch) && Boolean(minSearchLength) && !isMinSearchLengthExceeded @@ -877,7 +877,6 @@ class DropDownList< _clearFilter(): void { const dataController = this._dataController; - // @ts-expect-error fix argument type in m_data_controller.ts if (dataController.searchValue()) { dataController.searchValue(null); } @@ -963,7 +962,7 @@ class DropDownList< const dataController = this._dataController; const currentPageIndex = dataController.pageIndex(); const needRepaint = (isDefined(this._pageIndex) && currentPageIndex <= this._pageIndex) - || (dataController.isLastPage() as boolean && !this._list?._scrollViewIsFull()); + || (dataController.isLastPage() && !this._list?._scrollViewIsFull()); this._pageIndex = currentPageIndex; diff --git a/packages/devextreme/js/__internal/ui/list/list.edit.search.ts b/packages/devextreme/js/__internal/ui/list/list.edit.search.ts index f590d87d9e7f..7298b11e2668 100644 --- a/packages/devextreme/js/__internal/ui/list/list.edit.search.ts +++ b/packages/devextreme/js/__internal/ui/list/list.edit.search.ts @@ -24,7 +24,6 @@ class ListSearch extends ListEdit { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types _getCombinedFilter() { const dataController = this._dataController; - // @ts-expect-error ts-error const storeLoadOptions = { filter: dataController.filter() }; dataController.addSearchFilter(storeLoadOptions); const { filter } = storeLoadOptions; diff --git a/packages/devextreme/js/__internal/ui/overlay/overlay_position_controller.ts b/packages/devextreme/js/__internal/ui/overlay/overlay_position_controller.ts index 6c243eecaf13..8e67a464e5b3 100644 --- a/packages/devextreme/js/__internal/ui/overlay/overlay_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/overlay/overlay_position_controller.ts @@ -14,6 +14,7 @@ import type { DxEvent, PointerInteractionEvent, } from '@js/events'; +import type { PositionResult } from '@ts/common/core/animation/types'; import swatch from '@ts/core/utils/swatch_container'; import windowUtils from '@ts/core/utils/window'; import type { @@ -142,7 +143,7 @@ export class OverlayPositionController< _visualPosition?: Coordinates; - _initialPosition?: OverlayPosition; + _initialPosition?: Coordinates | PositionResult; _previousVisualPosition?: Coordinates; diff --git a/packages/devextreme/js/__internal/ui/popover/popover.ts b/packages/devextreme/js/__internal/ui/popover/popover.ts index e503e74fbb9e..4b78f98a3218 100644 --- a/packages/devextreme/js/__internal/ui/popover/popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/popover.ts @@ -511,6 +511,10 @@ class Popover< const contentPosition = this._getContainerPosition(); const resultLocation = positionUtils.setup(this.$overlayContent(), contentPosition); + if (!resultLocation) { + return; + } + const positionSide = this._getSideByLocation(resultLocation); this._togglePositionClass(`dx-position-${positionSide}`); @@ -626,7 +630,7 @@ class Popover< const contentOffset = positionUtils.offset(this.$overlayContent()); const arrowSize = arrowRect[sizeProperty]; - const contentLocation = contentOffset?.[axis]; + const contentLocation = contentOffset?.[axis] ?? NaN; const contentSize = getBoundingRect(this.$overlayContent().get(0))[sizeProperty]; const targetLocation = targetOffset[axis]; const targetElement = $target.get(0); diff --git a/packages/devextreme/js/__internal/ui/select_box.ts b/packages/devextreme/js/__internal/ui/select_box.ts index a9c947e5abab..19928fe24d86 100644 --- a/packages/devextreme/js/__internal/ui/select_box.ts +++ b/packages/devextreme/js/__internal/ui/select_box.ts @@ -488,7 +488,6 @@ class SelectBox< } _getActualSearchValue(): string | null { - // @ts-expect-error fix argument type in m_data_controller.ts return this._dataController.searchValue() as string | null; } diff --git a/packages/devextreme/js/__internal/ui/tag_box.ts b/packages/devextreme/js/__internal/ui/tag_box.ts index 3008a40180ee..6a985a145be7 100644 --- a/packages/devextreme/js/__internal/ui/tag_box.ts +++ b/packages/devextreme/js/__internal/ui/tag_box.ts @@ -897,7 +897,6 @@ class TagBox< } _getFilter(creator: FilterCreator): unknown[] | undefined { - // @ts-expect-error fix argument type in m_data_controller.ts const dataSourceFilter: unknown[] | null = this._dataController.filter(); const { valueExpr, maxFilterQueryLength } = this.option(); const filterExpr = creator.getCombinedFilter(valueExpr, dataSourceFilter); @@ -955,6 +954,7 @@ class TagBox< const mappedItems = dataController.applyMapFunction(items); d.resolve(mappedItems.filter(clientFilterFunction as (item: unknown) => boolean)); }) + // eslint-disable-next-line @typescript-eslint/no-misused-promises -- ignores the result .fail(d.reject); this._loadFilteredItemsPromise = d; From ed02c25f934c57da4baa93be89128760de54db32 Mon Sep 17 00:00:00 2001 From: Andrei Kharitonov Date: Mon, 26 Oct 2026 18:41:12 +0100 Subject: [PATCH 2/2] Core: remove the m_ prefix from position, the collection data controller and the themes callback --- .../core/animation/{m_position.test.ts => position.test.ts} | 2 +- .../common/core/animation/{m_position.ts => position.ts} | 0 packages/devextreme/js/__internal/data/m_data_helper.ts | 2 +- .../devextreme/js/__internal/integration/jquery/hold_ready.ts | 2 +- .../js/__internal/ui/collection/collection_widget.edit.ts | 2 +- .../ui/collection/{m_data_controller.ts => data_controller.ts} | 0 .../js/__internal/ui/drop_down_editor/drop_down_list.ts | 2 +- packages/devextreme/js/__internal/ui/themes.ts | 2 +- .../__internal/ui/{m_themes_callback.ts => themes_callback.ts} | 0 packages/devextreme/js/common/core/animation/position.js | 2 +- packages/devextreme/js/ui/themes_callback.js | 2 +- 11 files changed, 8 insertions(+), 8 deletions(-) rename packages/devextreme/js/__internal/common/core/animation/{m_position.test.ts => position.test.ts} (96%) rename packages/devextreme/js/__internal/common/core/animation/{m_position.ts => position.ts} (100%) rename packages/devextreme/js/__internal/ui/collection/{m_data_controller.ts => data_controller.ts} (100%) rename packages/devextreme/js/__internal/ui/{m_themes_callback.ts => themes_callback.ts} (100%) diff --git a/packages/devextreme/js/__internal/common/core/animation/m_position.test.ts b/packages/devextreme/js/__internal/common/core/animation/position.test.ts similarity index 96% rename from packages/devextreme/js/__internal/common/core/animation/m_position.test.ts rename to packages/devextreme/js/__internal/common/core/animation/position.test.ts index 20da271eba0b..059123bd259e 100644 --- a/packages/devextreme/js/__internal/common/core/animation/m_position.test.ts +++ b/packages/devextreme/js/__internal/common/core/animation/position.test.ts @@ -3,7 +3,7 @@ import { } from '@jest/globals'; import $ from '@js/core/renderer'; -import positionUtils from './m_position'; +import positionUtils from './position'; describe('position (setup)', () => { describe('when called with an element that does not exist in the DOM', () => { diff --git a/packages/devextreme/js/__internal/common/core/animation/m_position.ts b/packages/devextreme/js/__internal/common/core/animation/position.ts similarity index 100% rename from packages/devextreme/js/__internal/common/core/animation/m_position.ts rename to packages/devextreme/js/__internal/common/core/animation/position.ts diff --git a/packages/devextreme/js/__internal/data/m_data_helper.ts b/packages/devextreme/js/__internal/data/m_data_helper.ts index b9499ff9641a..156fc0842aaf 100644 --- a/packages/devextreme/js/__internal/data/m_data_helper.ts +++ b/packages/devextreme/js/__internal/data/m_data_helper.ts @@ -1,7 +1,7 @@ import { DataSource } from '@js/common/data/data_source/data_source'; import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; import { extend } from '@js/core/utils/extend'; -import DataController from '@ts/ui/collection/m_data_controller'; +import DataController from '@ts/ui/collection/data_controller'; const DATA_SOURCE_OPTIONS_METHOD = '_dataSourceOptions'; const DATA_SOURCE_CHANGED_METHOD = '_dataSourceChangedHandler'; diff --git a/packages/devextreme/js/__internal/integration/jquery/hold_ready.ts b/packages/devextreme/js/__internal/integration/jquery/hold_ready.ts index ec54aac3f71a..09951ffb8c14 100644 --- a/packages/devextreme/js/__internal/integration/jquery/hold_ready.ts +++ b/packages/devextreme/js/__internal/integration/jquery/hold_ready.ts @@ -1,5 +1,5 @@ import readyCallbacks from '@ts/core/utils/ready_callbacks'; -import { themeReadyCallback } from '@ts/ui/m_themes_callback'; +import { themeReadyCallback } from '@ts/ui/themes_callback'; // eslint-disable-next-line import/no-extraneous-dependencies import jQuery from 'jquery'; diff --git a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts index 42b2d2dcf67c..a65ab0907746 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts @@ -28,7 +28,7 @@ import type { } from '@ts/ui/collection/collection_widget.base'; import BaseCollectionWidget from '@ts/ui/collection/collection_widget.base'; import PlainEditStrategy from '@ts/ui/collection/collection_widget.edit.strategy.plain'; -import type DataController from '@ts/ui/collection/m_data_controller'; +import type DataController from '@ts/ui/collection/data_controller'; import Selection from '@ts/ui/selection/selection'; import type { CollectionItemIndex } from './collection_widget.edit.strategy'; diff --git a/packages/devextreme/js/__internal/ui/collection/m_data_controller.ts b/packages/devextreme/js/__internal/ui/collection/data_controller.ts similarity index 100% rename from packages/devextreme/js/__internal/ui/collection/m_data_controller.ts rename to packages/devextreme/js/__internal/ui/collection/data_controller.ts diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_list.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_list.ts index c798b34e448a..56a8d03e76c5 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_list.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_list.ts @@ -29,7 +29,7 @@ import type { Item, ItemClickEvent } from '@js/ui/list'; import errors from '@js/ui/widget/ui.errors'; import type { OptionChanged } from '@ts/core/widget/types'; import { getDataSourceOptions } from '@ts/data/data_converter/grouped'; -import type DataController from '@ts/ui/collection/m_data_controller'; +import type DataController from '@ts/ui/collection/data_controller'; import type { DropDownEditorInternalProperties } from '@ts/ui/drop_down_editor/drop_down_editor'; import DropDownEditor from '@ts/ui/drop_down_editor/drop_down_editor'; import type { ListBaseProperties } from '@ts/ui/list/list.base'; diff --git a/packages/devextreme/js/__internal/ui/themes.ts b/packages/devextreme/js/__internal/ui/themes.ts index 8fb38ea3e50f..561631d0435f 100644 --- a/packages/devextreme/js/__internal/ui/themes.ts +++ b/packages/devextreme/js/__internal/ui/themes.ts @@ -11,7 +11,7 @@ import { changeCallback, originalViewPort, value as viewPortValue } from '@js/co import { getWindow, hasWindow } from '@js/core/utils/window'; import errors from '@js/ui/widget/ui.errors'; import { uiLayerInitialized } from '@ts/core/utils/common'; -import { themeReadyCallback } from '@ts/ui/m_themes_callback'; +import { themeReadyCallback } from '@ts/ui/themes_callback'; const window = getWindow(); const ready = readyCallbacks.add; diff --git a/packages/devextreme/js/__internal/ui/m_themes_callback.ts b/packages/devextreme/js/__internal/ui/themes_callback.ts similarity index 100% rename from packages/devextreme/js/__internal/ui/m_themes_callback.ts rename to packages/devextreme/js/__internal/ui/themes_callback.ts diff --git a/packages/devextreme/js/common/core/animation/position.js b/packages/devextreme/js/common/core/animation/position.js index 6546eaafb8ce..0e48d6bfb7ab 100644 --- a/packages/devextreme/js/common/core/animation/position.js +++ b/packages/devextreme/js/common/core/animation/position.js @@ -1,3 +1,3 @@ -import Position from '../../../__internal/common/core/animation/m_position'; +import Position from '../../../__internal/common/core/animation/position'; export default Position; diff --git a/packages/devextreme/js/ui/themes_callback.js b/packages/devextreme/js/ui/themes_callback.js index c5c154f1d788..fb6979116e83 100644 --- a/packages/devextreme/js/ui/themes_callback.js +++ b/packages/devextreme/js/ui/themes_callback.js @@ -1,3 +1,3 @@ -import { themeReadyCallback } from '../__internal/ui/m_themes_callback'; +import { themeReadyCallback } from '../__internal/ui/themes_callback'; export { themeReadyCallback };