diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6f9ad4bfd6f6..d41b13a735b9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -786,7 +786,7 @@ /packages/devextreme/js/ui/load_panel.d.ts @DevExpress/devextreme-editors @DevExpress/devextreme-apireviewers /packages/devextreme/js/ui/load_panel_types.d.ts @DevExpress/devextreme-editors @DevExpress/devextreme-apireviewers -/packages/devextreme/js/__internal/ui/m_load_panel.ts @DevExpress/devextreme-editors +/packages/devextreme/js/__internal/ui/load_panel.ts @DevExpress/devextreme-editors /packages/devextreme-scss/scss/widgets/base/_loadPanel.scss @DevExpress/devextreme-editors /packages/devextreme-scss/scss/widgets/**/loadPanel/** @DevExpress/devextreme-editors diff --git a/.gitignore b/.gitignore index 80a08884b786..5764ac370ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # See http://help.github.com/ignore-files/ for more about ignoring files. +# tests +__test-artifacts__ + # compiled output npm dist @@ -26,7 +29,6 @@ node_modules !.vscode/launch.json !.vscode/extensions.json - # System Files .DS_Store Thumbs.db diff --git a/apps/demos/Demos/Charts/Crosshair/description.md b/apps/demos/Demos/Charts/Crosshair/description.md index cd4141231a7f..17b5160fb352 100644 --- a/apps/demos/Demos/Charts/Crosshair/description.md +++ b/apps/demos/Demos/Charts/Crosshair/description.md @@ -1,2 +1,15 @@ -In this example, the crosshair pointer is enabled and a custom style is applied to it. The crosshair pointer allows a user to determine the argument and the value of a specific point more precisely. - \ No newline at end of file +The DevExtreme Chart component ships with integrated crosshair support (vertical and horizontal lines centered on a data point). When enabled, the crosshair follows the cursor and snaps to the nearest series point. To configure crosshair settings, specify the [crosshair](/Documentation/ApiReference/UI_Components/dxChart/Configuration/crosshair/) object. + + +This demo configures the following **crosshair** properties: + +- [color](/Documentation/ApiReference/UI_Components/dxChart/Configuration/crosshair/#color) +Specifies line color. +- [width](/Documentation/ApiReference/UI_Components/dxChart/Configuration/crosshair/#width) +Configures line width. +- [dashStyle](/Documentation/ApiReference/UI_Components/dxChart/Configuration/crosshair/#dashStyle) +Specifies line style. +- [label](/Documentation/ApiReference/UI_Components/dxChart/Configuration/crosshair/label/) +Configures labels (text and appearance). + +To override settings for each line individually, configure **crosshair**.[horizontalLine](/Documentation/ApiReference/UI_Components/dxChart/Configuration/crosshair/horizontalLine/) and **crosshair**.[verticalLine](/Documentation/ApiReference/UI_Components/dxChart/Configuration/crosshair/verticalLine/) objects. \ No newline at end of file diff --git a/apps/demos/Demos/Charts/ErrorBars/description.md b/apps/demos/Demos/Charts/ErrorBars/description.md index 41799b5ec3a0..e8c0933954c5 100644 --- a/apps/demos/Demos/Charts/ErrorBars/description.md +++ b/apps/demos/Demos/Charts/ErrorBars/description.md @@ -1,13 +1,13 @@ -DevExtreme Chart supports value error bars. Implement these bars to indicate data measurement tolerances and [confidence intervals](https://en.wikipedia.org/wiki/Confidence_interval). Configure error bar settings and appearance in the **series**.[valueErrorBar](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/valueErrorBar/) object. +DevExtreme Chart supports value error bars. These bars indicate data measurement tolerances and [confidence intervals](https://en.wikipedia.org/wiki/Confidence_interval). Use the **series**.[valueErrorBar](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/valueErrorBar/) object to configure error bar settings and appearance. -To enable error bars, specify one of the following pairs of **valueErrorBar** properties: +To display error bars, specify one of the following pairs of **valueErrorBar** properties: - [lowValueField](Documentation/ApiReference/UI_Components/dxChart/Configuration/series/valueErrorBar/#lowValueField) and [highValueField](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/valueErrorBar/#highValueField) -Specify predefined error bars for each series point. +Bind fields that contain error values for each series point. - [value](Documentation/ApiReference/UI_Components/dxChart/Configuration/series/valueErrorBar/#value) and [type](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/valueErrorBar/#type) -Configure dynamically calculated error bars. +Calculate error bar values based on series data points. This demo implements predefined error bars specified in the Chart data source. \ No newline at end of file diff --git a/apps/demos/Demos/Charts/FunnelChart/description.md b/apps/demos/Demos/Charts/FunnelChart/description.md index 2d1d4747f273..8a68ce494cae 100644 --- a/apps/demos/Demos/Charts/FunnelChart/description.md +++ b/apps/demos/Demos/Charts/FunnelChart/description.md @@ -1,29 +1,29 @@ -DevExtreme Funnel displays data in a funnel chart. You can implement funnel charts to display the flow of data over different stages. This demo displays conversion rates from website visits to product renewals. +DevExtreme Funnel displays data/information flow across different stages/periods. This demo displays conversion rates from website visits to product renewals. -This demo specifies multiple Funnel configuration objects: +This demo configures the following objects: - [title](/Documentation/ApiReference/UI_Components/dxFunnel/Configuration/title/) -Specifies a title for the Funnel component. +Specifies component title. - [export](/Documentation/ApiReference/UI_Components/dxFunnel/Configuration/export/) -Configures export settings. Funnel supports multiple export [formats](/Documentation/ApiReference/UI_Components/dxFunnel/Configuration/export/#formats). +Configures export settings. Our Funnel Chart supports the following export [formats](/Documentation/ApiReference/UI_Components/dxFunnel/Configuration/export/#formats): PNG, PDF, JPEG, SVG, and GIF. - [tooltip](/Documentation/ApiReference/UI_Components/dxFunnel/Configuration/tooltip/) Specifies item tooltips. - [label](/Documentation/ApiReference/UI_Components/dxFunnel/Configuration/label/) Configures item labels. - [item](/Documentation/ApiReference/UI_Components/dxFunnel/Configuration/item/) -Customizes the visual appearance of items. +Customizes item appearance. [note] -Use our DevExpress BI Dashboard to embed interactive business intelligence into your next web app. +Use the DevExpress BI Dashboard to embed interactive business intelligence into your next web app. -The Web Dashboard is a data analysis UI component that you can embed into your ASP.NET Core or Angular, React, and Vue applications with .NET backend. Dashboards allow you to display multiple inter-connected data analysis elements such as grids, charts, maps, gauges, and others: all within an automatically-arranged layout. +Our Web Dashboard is a data analysis UI component you can embed into your ASP.NET Core or Angular, React, and Vue applications with a .NET backend. DevExpress Dashboards allow you to display multiple inter-connected data analysis elements such as grids, charts, maps, gauges, and others: all within an auto-arranged layout. -The set of components allows you to deploy an all-in-one solution and switch between Viewer and Designer modes directly on the web client (includes adaptive layouts for tablet & mobile). +DevExpress Dashboard allows you to deploy an all-in-one solution and switch between Viewer and Designer modes directly within the web client (includes built-in adaptive layouts for tablet & mobile devices). -The Web Dashboard is available as a part of a Universal subscription. +DevExpress is available as part of a Universal subscription. [Get Started with DevExpress BI Dashboard](https://docs.devexpress.com/Dashboard/115955/web-dashboard) | [Explore Demos](https://demos.devexpress.com/Dashboard/) diff --git a/apps/demos/Demos/Charts/HoverMode/description.md b/apps/demos/Demos/Charts/HoverMode/description.md index ae908b88756b..9b5abd8c5001 100644 --- a/apps/demos/Demos/Charts/HoverMode/description.md +++ b/apps/demos/Demos/Charts/HoverMode/description.md @@ -1,2 +1,23 @@ -The Chart and PieChart components support different modes of series hovering. This demo shows the _«includePoints»_ mode, when all the points of a hovered series change their display style. In addition, you can specify a custom hover mode for legend items. Here, the _«excludePoints»_ mode is used, when only the series line changes its display style leaving the points as they were. - \ No newline at end of file +DevExtreme Chart supports configurable hover modes for both series and points. This demo applies a common **hoverMode** for all series/points and a custom mode for the component legend. + + +You can specify **hoverMode** for the following Chart elements: + +- [series](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/#hoverMode) +A specific series. +- [commonSeriesSettings](/Documentation/ApiReference/UI_Components/dxChart/Configuration/commonSeriesSettings/#hoverMode) +All series. +- **commonSeriesSettings**.[spline](/Documentation/ApiReference/UI_Components/dxChart/Configuration/commonSeriesSettings/#spline) (or other [Series Type](/Documentation/ApiReference/UI_Components/dxChart/Series_Types/) objects) +All series (specific type). +- [legend](/Documentation/ApiReference/UI_Components/dxChart/Configuration/legend/#hoverMode) +Series hovered in the Chart legend. +- **series**.[point](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/point/#hoverMode) +Points in a specific series. +- **commonSeriesSettings**.[point](/Documentation/ApiReference/UI_Components/dxChart/Configuration/commonSeriesSettings/point/#hoverMode) (or other Series Type objects) +All points. +- **commonSeriesSettings**.**spline**.**point** +All points in series (specific type). +- [argumentAxis](/Documentation/ApiReference/UI_Components/dxChart/Configuration/argumentAxis/#hoverMode) +All points at common argument values. + +To further customize Chart behavior, define the [stickyHovering](/Documentation/ApiReference/UI_Components/dxChart/Configuration/#stickyHovering) property. When enabled (default), points remain in a hovered state until users hover the mouse pointer over other points or move it outside the bounds of the component. \ No newline at end of file diff --git a/apps/demos/Demos/Charts/MultiplePanes/description.md b/apps/demos/Demos/Charts/MultiplePanes/description.md index 79e9a4ac03f8..668dea8edf30 100644 --- a/apps/demos/Demos/Charts/MultiplePanes/description.md +++ b/apps/demos/Demos/Charts/MultiplePanes/description.md @@ -1,7 +1,7 @@ -The DevExtreme Chart component allows you to display data in multiple panes. To configure a multi-pane chart, specify the [panes[]](/Documentation/ApiReference/UI_Components/dxChart/Configuration/panes/) array. The component stacks panes vertically (or horizontally when [rotated](/Documentation/ApiReference/UI_Components/dxChart/Configuration/#rotated) is `true`). +The DevExtreme Chart component allows you to display data across multiple panes. DevExtreme Chart stacks panes vertically (or horizontally when Chart axes are [rotated](/Documentation/ApiReference/UI_Components/dxChart/Configuration/#rotated)). To create a multi-pane chart, add items to the [panes[]](/Documentation/ApiReference/UI_Components/dxChart/Configuration/panes/) array. -Chart assigns unique [value axes](/Documentation/ApiReference/UI_Components/dxChart/Configuration/valueAxis/) and a shared [argument axis](/Documentation/ApiReference/UI_Components/dxChart/Configuration/argumentAxis/) to all panes. To configure the value axis of a specific pane, define **valueAxis** properties and assign the **pane**.[name](/Documentation/ApiReference/UI_Components/dxChart/Configuration/panes/#name) value to **valueAxis**.[pane](/Documentation/ApiReference/UI_Components/dxChart/Configuration/valueAxis/#pane). +The Chart renders multiple [value axes](/Documentation/ApiReference/UI_Components/dxChart/Configuration/valueAxis/) and a shared [argument axis](/Documentation/ApiReference/UI_Components/dxChart/Configuration/argumentAxis/). To create multiple value axes, add a **valueAxis[]** object for each value axis. To assign a value axis to a pane, pass the pane [name](/Documentation/ApiReference/UI_Components/dxChart/Configuration/panes/#name) to the **valueAxis**.[pane](/Documentation/ApiReference/UI_Components/dxChart/Configuration/valueAxis/#pane) property. -DevExtreme Chart can display multiple [series](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/) in a single pane. Specify the **series**.[pane](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/#pane) property to display a series in a specific pane. \ No newline at end of file +DevExtreme Chart can display multiple [series](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/) within a single pane. Specify the **series**.[pane](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/#pane) property to display a series in a specific pane. \ No newline at end of file diff --git a/apps/demos/Demos/Charts/ParetoChart/description.md b/apps/demos/Demos/Charts/ParetoChart/description.md index d8d76941cec9..2e61c125c536 100644 --- a/apps/demos/Demos/Charts/ParetoChart/description.md +++ b/apps/demos/Demos/Charts/ParetoChart/description.md @@ -1,5 +1,9 @@ -You can utilize the DevExtreme Chart component to display data in a [Pareto chart](https://en.wikipedia.org/wiki/Pareto_chart). A Pareto chart displays individual values along with their cumulative totals. In this demo, individual values are numbers of complaints and cumulative totals are represented as percentages. +You can use DevExtreme Chart to visualize data as a [Pareto chart](https://en.wikipedia.org/wiki/Pareto_chart) and display individual values along with their cumulative totals. In this demo, individual values are numbers of complaints and cumulative totals are given in percentages. -To create a Pareto chart, configure two [series](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/) objects. Assign the [Bar](/Documentation/ApiReference/UI_Components/dxChart/Series_Types/BarSeries/) and [Line](/Documentation/ApiReference/UI_Components/dxChart/Series_Types/LineSeries/)/[Spline](/Documentation/ApiReference/UI_Components/dxChart/Series_Types/SplineSeries/) series types to **series**.[type](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/#type). To represent the numbers of the [Pareto principle](https://en.wikipedia.org/wiki/Pareto_principle) (80/20), you can specify constant lines. This demo configures a **valueAxis**.[constantLines](/Documentation/ApiReference/UI_Components/dxChart/Configuration/valueAxis/constantLines/) object to display a constant line at 80% of the cumulative total axis. \ No newline at end of file +To create a Pareto chart you must: + +1. Configure a [Bar](/Documentation/ApiReference/UI_Components/dxChart/Series_Types/BarSeries/) series and assign your primary data to this series (specify **series**.[valueField](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/#valueField) property). +2. Add a [Line](/Documentation/ApiReference/UI_Components/dxChart/Series_Types/LineSeries/) or [Spline](/Documentation/ApiReference/UI_Components/dxChart/Series_Types/SplineSeries/) series and assign cumulative total values to this series. +3. Specify **valueAxis**.[constantLines](/Documentation/ApiReference/UI_Components/dxChart/Configuration/valueAxis/constantLines/) to illustrate [Pareto principle](https://en.wikipedia.org/wiki/Pareto_principle) correlations (80/20). diff --git a/apps/demos/Demos/Charts/PieResolveLabelOverlap/description.md b/apps/demos/Demos/Charts/PieResolveLabelOverlap/description.md index 8779b76d6d46..f190bf5a20c3 100644 --- a/apps/demos/Demos/Charts/PieResolveLabelOverlap/description.md +++ b/apps/demos/Demos/Charts/PieResolveLabelOverlap/description.md @@ -1,2 +1,2 @@ -In the PieChart, series may include a large number of points, which may result in point label overlapping. This demo illustrates the **resolveLabelOverlapping** property that allows you to specify how the component must behave when point labels overlap. - \ No newline at end of file +When a DevExtreme PieChart contains a large number of data points or if data points are compressed visually, point labels may overlap. Use the [resolveLabelOverlapping](/Documentation/ApiReference/UI_Components/dxPieChart/Configuration/#resolveLabelOverlapping) property to minimize the impact of overlapping labels. In this demo, you can hide or shift overlapping labels. + diff --git a/apps/demos/Demos/Charts/PointImage/description.md b/apps/demos/Demos/Charts/PointImage/description.md index d9537732899e..43771c2a38b7 100644 --- a/apps/demos/Demos/Charts/PointImage/description.md +++ b/apps/demos/Demos/Charts/PointImage/description.md @@ -1,2 +1,4 @@ -This demo shows that the Chart can display custom images instead of standard point symbols. - \ No newline at end of file +DevExtreme Chart can display custom images for series points. Specify the **point**.**image** object within [common series](/Documentation/ApiReference/UI_Components/dxChart/Configuration/commonSeriesSettings/point/image/) or [series](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/point/image/) settings to display these images. + + +This demo [hides](/Documentation/ApiReference/UI_Components/dxChart/Configuration/commonSeriesSettings/point/#visible) default point [symbols](/Documentation/ApiReference/UI_Components/dxChart/Configuration/commonSeriesSettings/point/#symbol) and calls the [customizePoint](/Documentation/ApiReference/UI_Components/dxChart/Configuration/#customizePoint) function to display images (based on point values). diff --git a/apps/demos/Demos/Charts/PointSelectionAPI/Angular/app/app.component.html b/apps/demos/Demos/Charts/PointSelectionAPI/Angular/app/app.component.html index 4f102b792eed..f70d7fb27c4c 100644 --- a/apps/demos/Demos/Charts/PointSelectionAPI/Angular/app/app.component.html +++ b/apps/demos/Demos/Charts/PointSelectionAPI/Angular/app/app.component.html @@ -3,8 +3,8 @@ [dataSource]="catBreedsData" [rotated]="true" title="Most Popular US Cat Breeds" - (onPointClick)="pointClick($event)" (onDone)="done($event)" + (onPointClick)="pointClick($event)" > diff --git a/apps/demos/Demos/Charts/PointSelectionAPI/Angular/app/app.component.ts b/apps/demos/Demos/Charts/PointSelectionAPI/Angular/app/app.component.ts index 193ccc9b3bd4..4eb7574d6dd6 100644 --- a/apps/demos/Demos/Charts/PointSelectionAPI/Angular/app/app.component.ts +++ b/apps/demos/Demos/Charts/PointSelectionAPI/Angular/app/app.component.ts @@ -1,7 +1,7 @@ import { NgModule, Component, enableProdMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { DxChartModule, DxChartTypes } from 'devextreme-angular/ui/chart'; +import { DxChartModule, type DxChartTypes } from 'devextreme-angular/ui/chart'; import { Service, CatBreed } from './app.service'; if (!/localhost/.test(document.location.host)) { @@ -27,17 +27,18 @@ export class AppComponent { this.catBreedsData = service.getCatBreedsData(); } - pointClick({ target: point }: DxChartTypes.PointClickEvent) { + done(e: DxChartTypes.DoneEvent) { + e.component.getSeriesByPos(0).getPointsByArg('Siamese')[0].select(); + } + + pointClick(e: DxChartTypes.PointClickEvent) { + const point = e.target; if (point.isSelected()) { point.clearSelection(); } else { point.select(); } } - - done(e: DxChartTypes.DoneEvent) { - e.component.getSeriesByPos(0).getPointsByArg('Siamese')[0].select(); - } } @NgModule({ diff --git a/apps/demos/Demos/Charts/PointSelectionAPI/React/App.tsx b/apps/demos/Demos/Charts/PointSelectionAPI/React/App.tsx index 1008e4ce8db8..0fd82feab5a7 100644 --- a/apps/demos/Demos/Charts/PointSelectionAPI/React/App.tsx +++ b/apps/demos/Demos/Charts/PointSelectionAPI/React/App.tsx @@ -1,19 +1,22 @@ import React from 'react'; -import Chart, { +import { + Chart, CommonSeriesSettings, Series, SelectionStyle, Hatching, Legend, Export, + type ChartTypes, } from 'devextreme-react/chart'; import { catBreedsData } from './data.ts'; -function onDone({ component }) { - component.getSeriesByPos(0).getPointsByArg('Siamese')[0].select(); +function onDone(e: ChartTypes.DoneEvent) { + e.component.getSeriesByPos(0).getPointsByArg('Siamese')[0].select(); } -function onPointClick({ target: point }) { +function onPointClick(e: ChartTypes.PointClickEvent) { + const point = e.target; if (point.isSelected()) { point.clearSelection(); } else { @@ -27,9 +30,9 @@ function App() { id="chart" dataSource={catBreedsData} rotated={true} + title="Most Popular US Cat Breeds" onDone={onDone} onPointClick={onPointClick} - title="Most Popular US Cat Breeds" > diff --git a/apps/demos/Demos/Menu/Scrolling/Vue/data.ts b/apps/demos/Demos/Menu/Scrolling/Vue/data.ts index fe6d5bf69e31..def802c158c8 100644 --- a/apps/demos/Demos/Menu/Scrolling/Vue/data.ts +++ b/apps/demos/Demos/Menu/Scrolling/Vue/data.ts @@ -1,4 +1,6 @@ -const products = [ +import type { ProductType } from './types'; + +const products: ProductType[] = [ { text: 'Electronics', items: [ diff --git a/apps/demos/Demos/Menu/Scrolling/Vue/types.ts b/apps/demos/Demos/Menu/Scrolling/Vue/types.ts new file mode 100644 index 000000000000..e55751ab4ee6 --- /dev/null +++ b/apps/demos/Demos/Menu/Scrolling/Vue/types.ts @@ -0,0 +1,8 @@ +interface ProductItemType { + text: string; +} + +export interface ProductType { + text: string; + items: (ProductType | ProductItemType)[]; +} diff --git a/apps/demos/Demos/Scheduler/ContextMenu/Angular/app/app.component.ts b/apps/demos/Demos/Scheduler/ContextMenu/Angular/app/app.component.ts index d57a9464911c..34b5d5acb7a4 100644 --- a/apps/demos/Demos/Scheduler/ContextMenu/Angular/app/app.component.ts +++ b/apps/demos/Demos/Scheduler/ContextMenu/Angular/app/app.component.ts @@ -3,9 +3,15 @@ import { } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { DxSchedulerModule, DxSchedulerComponent, DxSchedulerTypes } from 'devextreme-angular/ui/scheduler'; -import { DxContextMenuModule, DxContextMenuTypes } from 'devextreme-angular/ui/context-menu'; -import { Appointment, Resource, Service } from './app.service'; + +import { + DxSchedulerModule, + DxSchedulerComponent, + type DxSchedulerTypes +} from 'devextreme-angular/ui/scheduler'; +import { DxContextMenuModule, type DxContextMenuTypes } from 'devextreme-angular/ui/context-menu'; + +import { Service, type Appointment, type Resource, type ContextMenuItem } from './app.service'; if (!/localhost/.test(document.location.host)) { enableProdMode(); @@ -56,7 +62,7 @@ export class AppComponent { const resourceItems = this.resourcesData .map((item) => ({ ...item, - onItemClick: ({ itemData }) => scheduler.updateAppointment(appointmentData, { + onItemClick: ({ itemData }: DxContextMenuTypes.ItemClickEvent) => scheduler.updateAppointment(appointmentData, { ...appointmentData, ...{ roomId: [itemData.id] }, }), @@ -129,8 +135,8 @@ export class AppComponent { ]; } - onContextMenuItemClick(e: DxContextMenuTypes.ItemClickEvent) { - (e.itemData as unknown & { onItemClick: Function }).onItemClick(e); + onContextMenuItemClick(e: DxContextMenuTypes.ItemClickEvent) { + e.itemData.onItemClick(e); } } diff --git a/apps/demos/Demos/Scheduler/ContextMenu/Angular/app/app.service.ts b/apps/demos/Demos/Scheduler/ContextMenu/Angular/app/app.service.ts index 353f0a2cb3c9..a9161e169628 100644 --- a/apps/demos/Demos/Scheduler/ContextMenu/Angular/app/app.service.ts +++ b/apps/demos/Demos/Scheduler/ContextMenu/Angular/app/app.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { type DxContextMenuTypes } from 'devextreme-angular/ui/context-menu'; export interface Appointment { text: string; @@ -15,6 +16,10 @@ export interface Resource { color: string; } +export type ContextMenuItem = DxContextMenuTypes.Item & Resource & { + onItemClick?: (e: DxContextMenuTypes.ItemClickEvent) => void +} + const appointments: Appointment[] = [ { text: 'Watercolor Landscape', diff --git a/apps/demos/Demos/Scheduler/ContextMenu/React/App.tsx b/apps/demos/Demos/Scheduler/ContextMenu/React/App.tsx index e626c8a8bc8d..6ad7ebb127f3 100644 --- a/apps/demos/Demos/Scheduler/ContextMenu/React/App.tsx +++ b/apps/demos/Demos/Scheduler/ContextMenu/React/App.tsx @@ -1,19 +1,19 @@ import React, { useCallback, useRef, useState } from 'react'; -import Scheduler, { Resource, type SchedulerTypes, SchedulerRef } from 'devextreme-react/scheduler'; -import ContextMenu, { type ContextMenuTypes } from 'devextreme-react/context-menu'; -import { data, resourcesData, Resource as ResourceItem } from './data.ts'; -import AppointmentMenuTemplate from './AppointmentTemplate.tsx'; -// eslint-disable-next-line no-unused-vars -type ContextMenuItem = ContextMenuTypes.Item & ResourceItem & { onItemClick?: (e: ContextMenuTypes.ItemClickEvent) => void }; +import { Scheduler, Resource, type SchedulerTypes, SchedulerRef } from 'devextreme-react/scheduler'; +import { ContextMenu, type ContextMenuTypes } from 'devextreme-react/context-menu'; + +import AppointmentMenuTemplate from './AppointmentTemplate.tsx'; +import { data, resourcesData } from './data.ts'; +import type { ContextMenuItem } from './types'; const views: SchedulerTypes.ViewType[] = ['day', 'month']; const appointmentClassName = '.dx-scheduler-appointment'; const cellClassName = '.dx-scheduler-date-table-cell'; -const onContextMenuItemClick = (e: ContextMenuTypes.ItemClickEvent) => { - (e.itemData as ContextMenuItem).onItemClick?.(e); +const onContextMenuItemClick = (e: ContextMenuTypes.ItemClickEvent) => { + e.itemData.onItemClick?.(e); }; const App = () => { @@ -31,9 +31,9 @@ const App = () => { const resourceItems: ContextMenuItem[] = resourcesData.map((item) => ({ ...item, - onItemClick: (e) => scheduler?.updateAppointment(appointmentData, { + onItemClick: (e: ContextMenuTypes.ItemClickEvent) => scheduler?.updateAppointment(appointmentData, { ...appointmentData, - ...{ roomId: [(e.itemData as ContextMenuItem).id] }, + ...{ roomId: [e.itemData.id] }, }), })); diff --git a/apps/demos/Demos/Scheduler/ContextMenu/React/data.ts b/apps/demos/Demos/Scheduler/ContextMenu/React/data.ts index 56ba44a33ad5..4f5e4c3d0cb4 100644 --- a/apps/demos/Demos/Scheduler/ContextMenu/React/data.ts +++ b/apps/demos/Demos/Scheduler/ContextMenu/React/data.ts @@ -1,12 +1,4 @@ -import { SchedulerTypes } from 'devextreme-react/scheduler'; - -type Appointment = SchedulerTypes.Appointment & { roomId: number[] }; - -export type Resource = { - id?: number; - text: string; - color?: string; -}; +import type { Appointment, Resource } from './types'; export const data: Appointment[] = [ { diff --git a/apps/demos/Demos/Scheduler/ContextMenu/React/types.ts b/apps/demos/Demos/Scheduler/ContextMenu/React/types.ts new file mode 100644 index 000000000000..f771684e69dc --- /dev/null +++ b/apps/demos/Demos/Scheduler/ContextMenu/React/types.ts @@ -0,0 +1,12 @@ +import { SchedulerTypes } from 'devextreme-react/scheduler'; +import { type ContextMenuTypes } from 'devextreme-react/context-menu'; + +export type Appointment = SchedulerTypes.Appointment & { roomId: number[] }; + +export type Resource = { + id?: number; + text: string; + color?: string; +}; + +export type ContextMenuItem = ContextMenuTypes.Item & Resource & { onItemClick?: (e: ContextMenuTypes.ItemClickEvent) => void }; diff --git a/apps/demos/Demos/Scheduler/ContextMenu/ReactJs/App.js b/apps/demos/Demos/Scheduler/ContextMenu/ReactJs/App.js index d15df29d3233..6a7283466ce7 100644 --- a/apps/demos/Demos/Scheduler/ContextMenu/ReactJs/App.js +++ b/apps/demos/Demos/Scheduler/ContextMenu/ReactJs/App.js @@ -1,8 +1,8 @@ import React, { useCallback, useRef, useState } from 'react'; -import Scheduler, { Resource } from 'devextreme-react/scheduler'; -import ContextMenu from 'devextreme-react/context-menu'; -import { data, resourcesData } from './data.js'; +import { Scheduler, Resource } from 'devextreme-react/scheduler'; +import { ContextMenu } from 'devextreme-react/context-menu'; import AppointmentMenuTemplate from './AppointmentTemplate.js'; +import { data, resourcesData } from './data.js'; const views = ['day', 'month']; const appointmentClassName = '.dx-scheduler-appointment'; diff --git a/apps/demos/Demos/Scheduler/ContextMenu/Vue/App.vue b/apps/demos/Demos/Scheduler/ContextMenu/Vue/App.vue index 5d7c50310a2f..682c0b9ddd6f 100644 --- a/apps/demos/Demos/Scheduler/ContextMenu/Vue/App.vue +++ b/apps/demos/Demos/Scheduler/ContextMenu/Vue/App.vue @@ -38,10 +38,11 @@ diff --git a/apps/demos/Demos/Scheduler/ContextMenu/Vue/types.ts b/apps/demos/Demos/Scheduler/ContextMenu/Vue/types.ts new file mode 100644 index 000000000000..fa6270ed6e35 --- /dev/null +++ b/apps/demos/Demos/Scheduler/ContextMenu/Vue/types.ts @@ -0,0 +1,11 @@ +import type { DxContextMenuTypes } from 'devextreme-vue/context-menu'; + +interface ResourceItem { + text: string; + id: number; + color: string; +} + +export type ContextMenuItem = DxContextMenuTypes.Item & Partial & { + onItemClick?: (e: DxContextMenuTypes.ItemClickEvent) => void +}; diff --git a/apps/demos/Demos/Scheduler/TimeZonesSupport/Angular/app/app.component.ts b/apps/demos/Demos/Scheduler/TimeZonesSupport/Angular/app/app.component.ts index 3e2aaeb9b39d..1264b12c27a2 100644 --- a/apps/demos/Demos/Scheduler/TimeZonesSupport/Angular/app/app.component.ts +++ b/apps/demos/Demos/Scheduler/TimeZonesSupport/Angular/app/app.component.ts @@ -44,7 +44,7 @@ export class AppComponent { this.currentTimeZone = this.timeZones[0].id; } - getDefaultTimeZones = (date: Date) => getTimeZones(date).filter((timeZone) => this.service.getLocations().indexOf(timeZone.id) !== -1); + getDefaultTimeZones = (date: Date) => getTimeZones(date, this.service.getLocations()) onAppointmentFormOpening({ form }: DxSchedulerTypes.AppointmentFormOpeningEvent) { const startDateDataSource = form.getEditor('startDateTimeZone').option('dataSource') as DataSource; diff --git a/apps/demos/Demos/Scheduler/TimeZonesSupport/React/App.tsx b/apps/demos/Demos/Scheduler/TimeZonesSupport/React/App.tsx index 34684dd61aa9..fade171de855 100644 --- a/apps/demos/Demos/Scheduler/TimeZonesSupport/React/App.tsx +++ b/apps/demos/Demos/Scheduler/TimeZonesSupport/React/App.tsx @@ -10,11 +10,7 @@ const timeZoneLabel = { 'aria-label': 'Time zone' }; const currentDate = new Date(2021, 3, 27); const views: SchedulerTypes.ViewType[] = ['workWeek']; -const getTimeZones = (date: Date) => { - const timeZones = timeZoneUtils.getTimeZones(date); - - return timeZones.filter((timeZone: { id: string; }) => locations.indexOf(timeZone.id) !== -1); -}; +const getTimeZones = (date) => timeZoneUtils.getTimeZones(date, locations) const defaultTimeZones = getTimeZones(currentDate); diff --git a/apps/demos/Demos/Scheduler/TimeZonesSupport/ReactJs/App.js b/apps/demos/Demos/Scheduler/TimeZonesSupport/ReactJs/App.js index f5a74fb7f31e..ebe5da241ebd 100644 --- a/apps/demos/Demos/Scheduler/TimeZonesSupport/ReactJs/App.js +++ b/apps/demos/Demos/Scheduler/TimeZonesSupport/ReactJs/App.js @@ -7,10 +7,7 @@ import { data, locations } from './data.js'; const timeZoneLabel = { 'aria-label': 'Time zone' }; const currentDate = new Date(2021, 3, 27); const views = ['workWeek']; -const getTimeZones = (date) => { - const timeZones = timeZoneUtils.getTimeZones(date); - return timeZones.filter((timeZone) => locations.indexOf(timeZone.id) !== -1); -}; +const getTimeZones = (date) => timeZoneUtils.getTimeZones(date, locations); const defaultTimeZones = getTimeZones(currentDate); const onAppointmentFormOpening = (e) => { const { form } = e; diff --git a/apps/demos/Demos/Scheduler/TimeZonesSupport/Vue/App.vue b/apps/demos/Demos/Scheduler/TimeZonesSupport/Vue/App.vue index 3c3cd6057039..b66974d5afb8 100644 --- a/apps/demos/Demos/Scheduler/TimeZonesSupport/Vue/App.vue +++ b/apps/demos/Demos/Scheduler/TimeZonesSupport/Vue/App.vue @@ -40,10 +40,7 @@ const views = ['workWeek']; const dataSource = data; const currentDate = new Date(2021, 3, 27); -const getTimeZones = function(date: Date) { - const timeZones = getTimeZonesUtility(date); - return timeZones.filter((timeZone) => locations.indexOf(timeZone.id) !== -1); -}; +const getTimeZones = (date: Date) => getTimeZonesUtility(date, locations) const timeZones = ref(getTimeZones(currentDate)); const currentTimeZone = ref(timeZones.value[0].id); diff --git a/apps/demos/Demos/Scheduler/TimeZonesSupport/description.md b/apps/demos/Demos/Scheduler/TimeZonesSupport/description.md index 55f2f56f007e..b7e6209a32ac 100644 --- a/apps/demos/Demos/Scheduler/TimeZonesSupport/description.md +++ b/apps/demos/Demos/Scheduler/TimeZonesSupport/description.md @@ -1,6 +1,6 @@ -The Scheduler allows its users to view appointments in different time zones. Set the [timeZone](/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/#timeZone) property to specify the current time zone. This property accepts values from the IANA time zone database. - -In this demo, you can use the drop-down menu above the Scheduler to choose between the London, Berlin, and Helsinki time zones. To populate the menu, the [getTimeZones](/Documentation/ApiReference/Common/Utils/utils/#getTimeZonesdate) utility method is used. It returns a list of all IANA time zones that is then filtered. +DevExtreme Scheduler allows you to specify time zones used for the component and associated events/appointments. In this demo, you can change the time zone using the [SelectBox](/Documentation/Guide/UI_Components/SelectBox/Overview/) positioned above the Scheduler. A [getTimeZones()](/Documentation/ApiReference/Common/Utils/utils/#getTimeZonesdate_timeZones) method call populates the SelectBox with appropriate values. -Users can edit the time zones of individual appointments in the appointment details form. To enable this functionality, set the **editing**.[allowTimeZoneEditing](/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/editing/#allowTimeZoneEditing) property to **true**. Information about individual time zones is saved in the [startDateTimeZone](/Documentation/ApiReference/UI_Components/dxScheduler/Interfaces/dxSchedulerAppointment/#startDateTimeZone) and [endDateTimeZone](/Documentation/ApiReference/UI_Components/dxScheduler/Interfaces/dxSchedulerAppointment/#endDateTimeZone) fields of the appointment data objects. +To define the time zone at the component level, assign an IANA time zone value to the [timeZone](/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/#timeZone) property. + +To modify time zones used for appointments, enable the **editing**.[allowTimeZoneEditing](/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/editing/#allowTimeZoneEditing) option. Our Scheduler supports different time zones for appointment start and end dates ([startDateTimeZone](/Documentation/ApiReference/UI_Components/dxScheduler/Interfaces/dxSchedulerAppointment/#startDateTimeZone) and [endDateTimeZone](/Documentation/ApiReference/UI_Components/dxScheduler/Interfaces/dxSchedulerAppointment/#endDateTimeZone) appointment properties). diff --git a/apps/demos/Demos/Scheduler/TimeZonesSupport/jQuery/index.js b/apps/demos/Demos/Scheduler/TimeZonesSupport/jQuery/index.js index ba55f13efe51..351caa904d43 100644 --- a/apps/demos/Demos/Scheduler/TimeZonesSupport/jQuery/index.js +++ b/apps/demos/Demos/Scheduler/TimeZonesSupport/jQuery/index.js @@ -1,10 +1,7 @@ $(() => { const currentDate = new Date(2021, 3, 27); - const getTimeZones = function (date) { - const timeZones = DevExpress.utils.getTimeZones(date); - return timeZones.filter((timeZone) => locations.indexOf(timeZone.id) !== -1); - }; + const getTimeZones = (date) => DevExpress.utils.getTimeZones(date, locations) const timeZones = getTimeZones(currentDate); diff --git a/apps/demos/Demos/TreeView/ContextMenuIntegration/Angular/app/app.component.ts b/apps/demos/Demos/TreeView/ContextMenuIntegration/Angular/app/app.component.ts index 1365d2d672be..4214c308b4bb 100644 --- a/apps/demos/Demos/TreeView/ContextMenuIntegration/Angular/app/app.component.ts +++ b/apps/demos/Demos/TreeView/ContextMenuIntegration/Angular/app/app.component.ts @@ -3,10 +3,12 @@ import { } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + import { DxListModule } from 'devextreme-angular'; -import { DxTreeViewComponent, DxTreeViewModule, DxTreeViewTypes } from 'devextreme-angular/ui/tree-view'; -import { DxContextMenuModule, DxContextMenuComponent, DxContextMenuTypes } from 'devextreme-angular/ui/context-menu'; -import { Product, Service, MenuItem } from './app.service'; +import { DxTreeViewComponent, DxTreeViewModule, type DxTreeViewTypes } from 'devextreme-angular/ui/tree-view'; +import { DxContextMenuModule, DxContextMenuComponent, type DxContextMenuTypes } from 'devextreme-angular/ui/context-menu'; + +import { Service, type Product, type MenuItem } from './app.service'; if (!/localhost/.test(document.location.host)) { enableProdMode(); @@ -25,9 +27,9 @@ if (window && window.config?.packageConfigPaths) { providers: [Service], }) export class AppComponent { - @ViewChild(DxTreeViewComponent, { static: false }) treeView: DxTreeViewComponent; + @ViewChild(DxTreeViewComponent, { static: false }) treeView: DxTreeViewComponent; - @ViewChild(DxContextMenuComponent, { static: false }) contextMenu: DxContextMenuComponent; + @ViewChild(DxContextMenuComponent, { static: false }) contextMenu: DxContextMenuComponent; products: Product[]; @@ -45,21 +47,21 @@ export class AppComponent { treeViewItemContextMenu(e: DxTreeViewTypes.ItemContextMenuEvent) { this.selectedTreeItem = e.itemData; - const isProduct = e.itemData.price !== undefined; + const isProductItem = !e.itemData.items; const contextMenu = this.contextMenu.instance; - contextMenu.option('items[0].visible', !isProduct); - contextMenu.option('items[1].visible', !isProduct); - contextMenu.option('items[2].visible', isProduct); - contextMenu.option('items[3].visible', isProduct); + contextMenu.option('items[0].visible', !isProductItem); + contextMenu.option('items[1].visible', !isProductItem); + contextMenu.option('items[2].visible', isProductItem); + contextMenu.option('items[3].visible', isProductItem); contextMenu.option('items[0].disabled', e.node.expanded); contextMenu.option('items[1].disabled', !e.node.expanded); } - contextMenuItemClick(e: DxContextMenuTypes.ItemClickEvent) { + contextMenuItemClick(e: DxContextMenuTypes.ItemClickEvent) { let logEntry = ''; const treeView = this.treeView.instance; - switch ((e.itemData as Product).id) { + switch (e.itemData.id) { case 'expand': { logEntry = `The '${this.selectedTreeItem.text}' group was expanded`; treeView.expandItem(this.selectedTreeItem.id); diff --git a/apps/demos/Demos/TreeView/ContextMenuIntegration/Angular/app/app.service.ts b/apps/demos/Demos/TreeView/ContextMenuIntegration/Angular/app/app.service.ts index d80a6742c85d..7b5dc1fe655f 100644 --- a/apps/demos/Demos/TreeView/ContextMenuIntegration/Angular/app/app.service.ts +++ b/apps/demos/Demos/TreeView/ContextMenuIntegration/Angular/app/app.service.ts @@ -1,17 +1,23 @@ import { Injectable } from '@angular/core'; -export class Product { +export class ProductItem { id: string; text: string; - expanded?: boolean; + price: number; - items?: Product[]; + image: string; +} - price?: number; +export class Product { + id: string; + + text: string; + + expanded?: boolean; - image?: string; + items: (Product | ProductItem)[]; } export class MenuItem { diff --git a/apps/demos/Demos/TreeView/ContextMenuIntegration/React/App.tsx b/apps/demos/Demos/TreeView/ContextMenuIntegration/React/App.tsx index 00b22bc4ecf2..0f8833f53bdd 100644 --- a/apps/demos/Demos/TreeView/ContextMenuIntegration/React/App.tsx +++ b/apps/demos/Demos/TreeView/ContextMenuIntegration/React/App.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useRef, useState } from 'react'; -import TreeView, { type TreeViewTypes } from 'devextreme-react/tree-view'; -import ContextMenu, { type ContextMenuTypes } from 'devextreme-react/context-menu'; + +import { TreeView, type TreeViewTypes } from 'devextreme-react/tree-view'; +import { ContextMenu, type ContextMenuTypes } from 'devextreme-react/context-menu'; import List from 'devextreme-react/list'; import service from './data.ts'; @@ -13,25 +14,25 @@ const App = () => { const contextMenuRef = useRef(null); const treeViewRef = useRef(null); const [logItems, setLogItems] = useState([]); - const [selectedTreeItem, setSelectedTreeItem] = useState(undefined); + const [selectedTreeItem, setSelectedTreeItem] = useState(undefined); const treeViewItemContextMenu = useCallback(( e: TreeViewTypes.ItemContextMenuEvent, ) => { setSelectedTreeItem(e.itemData); - const isProduct = e.itemData.price !== undefined; - contextMenuRef.current.instance().option('items[0].visible', !isProduct); - contextMenuRef.current.instance().option('items[1].visible', !isProduct); - contextMenuRef.current.instance().option('items[2].visible', isProduct); - contextMenuRef.current.instance().option('items[3].visible', isProduct); + const isProductItem = !e.itemData.items; + contextMenuRef.current.instance().option('items[0].visible', !isProductItem); + contextMenuRef.current.instance().option('items[1].visible', !isProductItem); + contextMenuRef.current.instance().option('items[2].visible', isProductItem); + contextMenuRef.current.instance().option('items[3].visible', isProductItem); contextMenuRef.current.instance().option('items[0].disabled', e.node.expanded); contextMenuRef.current.instance().option('items[1].disabled', !e.node.expanded); }, []); const contextMenuItemClick = useCallback(( - e: ContextMenuTypes.ItemClickEvent & { itemData: { id?: any; }; }, + e: ContextMenuTypes.ItemClickEvent, ) => { let logEntry = ''; switch (e.itemData.id) { @@ -58,7 +59,7 @@ const App = () => { } const updatedLogItems = [...logItems, logEntry]; setLogItems(updatedLogItems); - }, [logItems, selectedTreeItem, setLogItems]); + }, [logItems, selectedTreeItem]); return (
diff --git a/apps/demos/Demos/TreeView/ContextMenuIntegration/React/types.ts b/apps/demos/Demos/TreeView/ContextMenuIntegration/React/types.ts index 8a09bd046bc7..836121d6d8cd 100644 --- a/apps/demos/Demos/TreeView/ContextMenuIntegration/React/types.ts +++ b/apps/demos/Demos/TreeView/ContextMenuIntegration/React/types.ts @@ -1,8 +1,13 @@ +interface ProductItem { + id: string; + text: string; + price: number; + image: string; +} + export interface Product { id: string; text: string; expanded?: boolean; - items?: Product[]; - price?: number; - image?: string; + items: (Product | ProductItem)[]; } diff --git a/apps/demos/Demos/TreeView/ContextMenuIntegration/ReactJs/App.js b/apps/demos/Demos/TreeView/ContextMenuIntegration/ReactJs/App.js index 0bf3ed9f07da..0a32a6ada678 100644 --- a/apps/demos/Demos/TreeView/ContextMenuIntegration/ReactJs/App.js +++ b/apps/demos/Demos/TreeView/ContextMenuIntegration/ReactJs/App.js @@ -1,6 +1,6 @@ import React, { useCallback, useRef, useState } from 'react'; -import TreeView from 'devextreme-react/tree-view'; -import ContextMenu from 'devextreme-react/context-menu'; +import { TreeView } from 'devextreme-react/tree-view'; +import { ContextMenu } from 'devextreme-react/context-menu'; import List from 'devextreme-react/list'; import service from './data.js'; @@ -13,11 +13,11 @@ const App = () => { const [selectedTreeItem, setSelectedTreeItem] = useState(undefined); const treeViewItemContextMenu = useCallback((e) => { setSelectedTreeItem(e.itemData); - const isProduct = e.itemData.price !== undefined; - contextMenuRef.current.instance().option('items[0].visible', !isProduct); - contextMenuRef.current.instance().option('items[1].visible', !isProduct); - contextMenuRef.current.instance().option('items[2].visible', isProduct); - contextMenuRef.current.instance().option('items[3].visible', isProduct); + const isProductItem = !e.itemData.items; + contextMenuRef.current.instance().option('items[0].visible', !isProductItem); + contextMenuRef.current.instance().option('items[1].visible', !isProductItem); + contextMenuRef.current.instance().option('items[2].visible', isProductItem); + contextMenuRef.current.instance().option('items[3].visible', isProductItem); contextMenuRef.current.instance().option('items[0].disabled', e.node.expanded); contextMenuRef.current.instance().option('items[1].disabled', !e.node.expanded); }, []); @@ -49,7 +49,7 @@ const App = () => { const updatedLogItems = [...logItems, logEntry]; setLogItems(updatedLogItems); }, - [logItems, selectedTreeItem, setLogItems], + [logItems, selectedTreeItem], ); return (
diff --git a/apps/demos/Demos/TreeView/ContextMenuIntegration/Vue/App.vue b/apps/demos/Demos/TreeView/ContextMenuIntegration/Vue/App.vue index f019048f8520..6fff20cf0638 100644 --- a/apps/demos/Demos/TreeView/ContextMenuIntegration/Vue/App.vue +++ b/apps/demos/Demos/TreeView/ContextMenuIntegration/Vue/App.vue @@ -31,33 +31,33 @@ '; + +test('Script inside cell text should not be executed after opening header filter', async (t) => { + const cardView = new CardView('#container'); + + await t.click( + cardView.getHeaderPanel().getHeaderItem().getFilterIcon(), + ); + + await t.expect( + cardView.getHeaderFilterList().getItem(0).text, + ).eql(UNSAFE_TEXT); +}).before(async () => createWidget('dxCardView', { + columns: ['caption'], + headerFilter: { + visible: true, + }, + dataSource: [ + { id: 1, caption: UNSAFE_TEXT }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/common/etalons/Icon set (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/common/etalons/Icon set (fluent-blue-light).png index 82cb24db8086..882ef70e1816 100644 Binary files a/e2e/testcafe-devextreme/tests/common/etalons/Icon set (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/common/etalons/Icon set (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/common/etalons/Icon set (generic-light).png b/e2e/testcafe-devextreme/tests/common/etalons/Icon set (generic-light).png index 5aff276474bb..d73362cd6f63 100644 Binary files a/e2e/testcafe-devextreme/tests/common/etalons/Icon set (generic-light).png and b/e2e/testcafe-devextreme/tests/common/etalons/Icon set (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/common/etalons/Icon set (material-blue-light).png b/e2e/testcafe-devextreme/tests/common/etalons/Icon set (material-blue-light).png index 19c39d5617d4..98a8488b743d 100644 Binary files a/e2e/testcafe-devextreme/tests/common/etalons/Icon set (material-blue-light).png and b/e2e/testcafe-devextreme/tests/common/etalons/Icon set (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (fluent-blue-light).png index 4a6e3ba2d402..4a8c79720990 100644 Binary files a/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (generic-light).png b/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (generic-light).png index 13743a93f38c..8e7ca93a3061 100644 Binary files a/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (generic-light).png and b/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (material-blue-light).png b/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (material-blue-light).png index ba903c16c681..09c422a5f7c5 100644 Binary files a/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (material-blue-light).png and b/e2e/testcafe-devextreme/tests/common/etalons/SVG icon set (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/common/icons.ts b/e2e/testcafe-devextreme/tests/common/icons.ts index 7d08c8f92d83..643674ef7996 100644 --- a/e2e/testcafe-devextreme/tests/common/icons.ts +++ b/e2e/testcafe-devextreme/tests/common/icons.ts @@ -276,6 +276,8 @@ const iconSet = { fixcolumnleft: '\f17b', stickcolumn: '\f17c', fixcolumnright: '\f17d', + ratingoutline: '\f17f', + ratingfilled: '\f180', csv: '\f181', packagebox: '\f182', checkmarkcircle: '\f183', @@ -295,6 +297,9 @@ const iconSet = { groupbycolumn: '\f197', ungroupcolumn: '\f198', ungroupallcolumns: '\f199', + chatadd: '\f200', + colordismiss: '\f201', + clipboardpastesparkle: '\f202', }; fixture.disablePageReloads`Icons` diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/columnChooser.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/columnChooser.ts index dcb1fd92be04..93b1f8429ed6 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/columnChooser.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/columnChooser.ts @@ -192,3 +192,48 @@ test('Check the behavior of pressing the Esc button when dragging a column from mode: 'dragAndDrop', }, })); + +test( + 'Should take into account column options change during general option change (T1267471)', + async (t) => { + const dataGrid = new DataGrid('#container'); + const columnChooserBtn = dataGrid.getColumnChooserButton(); + + await t.click(columnChooserBtn); + + const columnChooser = dataGrid.getColumnChooser(); + const lastItemCheckbox = columnChooser.getCheckbox(1); + + await t.expect(columnChooser.isCheckboxDisabled(0)).notOk(); + await t.expect(columnChooser.isCheckboxDisabled(1)).notOk(); + + await t.click(lastItemCheckbox); + + await t.expect(columnChooser.isCheckboxDisabled(0)).ok(); + await t.expect(columnChooser.isCheckboxDisabled(1)).notOk(); + }, +).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { id: 0, A: 'A', B: 'B' }, + ], + keyExpr: 'id', + columns: ['A', 'B'], + columnChooser: { + enabled: true, + mode: 'select', + }, + onOptionChanged: ({ component, fullName }) => { + if (!/columns\[\d+\]\.visible/.test(fullName)) { + return; + } + + const visibleColumns = component.getVisibleColumns(); + const [{ dataField: lastColumnDataField }] = visibleColumns; + + if (!lastColumnDataField) { + return; + } + + component.columnOption(lastColumnDataField, 'allowHiding', false); + }, +})); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/total-summary-focused-with-sticky.png b/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/total-summary-focused-with-sticky.png new file mode 100644 index 000000000000..0fd084982c89 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/total-summary-focused-with-sticky.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns.ts index c3010ebe2965..0dfe69302d29 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns.ts @@ -436,3 +436,212 @@ test('DataGrid - Group summary is not updated when a column is fixed on the righ }, ], })); + +const mockCountries = [ + { + ID: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Rural: 0.15, + Population_Total: 205809000, + GDP_Agriculture: 0.054, + GDP_Industry: 0.274, + GDP_Services: 0.672, + GDP_Total: 2353025, + }, + { + ID: 2, + Country: 'China', + Area: 9388211, + Population_Urban: 0.54, + Population_Rural: 0.46, + Population_Total: 1375530000, + GDP_Agriculture: 0.091, + GDP_Industry: 0.426, + GDP_Services: 0.483, + GDP_Total: 10380380, + }, + { + ID: 3, + Country: 'France', + Area: 675417, + Population_Urban: 0.79, + Population_Rural: 0.21, + Population_Total: 64529000, + GDP_Agriculture: 0.019, + GDP_Industry: 0.183, + GDP_Services: 0.798, + GDP_Total: 2846889, + }, +]; + +test('Warning should be shown when trying to set fixed state for child columns', async (t) => { + const consoleMessages = await t.getBrowserConsoleMessages(); + const warnings = consoleMessages?.warn.filter((message) => message.startsWith('W1028')) || []; + await t.expect(warnings.length).eql(1, 'There is warning W1028'); +}).before(async () => { + await createWidget( + 'dxDataGrid', + { + dataSource: mockCountries, + keyExpr: 'ID', + columnAutoWidth: true, + allowColumnReordering: true, + width: 600, + showBorders: true, + columnChooser: { enabled: true }, + columns: [ + { + dataField: 'Country', + fixed: true, + fixedPosition: 'left', + }, + { + dataField: 'Area', + fixed: true, + fixedPosition: 'left', + }, + { + caption: 'Population', + columns: [ + { + caption: 'Total', + dataField: 'Population_Total', + format: 'fixedPoint', + fixed: true, + fixedPosition: 'left', + }, + { + caption: 'Urban', + dataField: 'Population_Urban', + format: 'percent', + fixed: true, + fixedPosition: 'left', + }, + ], + }, + ], + }, + '#container', + ); +}); + +test('Warning should work when columns changed in real time', async (t) => { + const grid = new DataGrid('#container'); + + let consoleMessages = await t.getBrowserConsoleMessages(); + let warnings = consoleMessages?.warn.filter((message) => message.startsWith('W1028')) || []; + + await t.expect(warnings.length).eql(0, 'There is not any warning W1028'); + + await grid.option('columns', [ + { + dataField: 'test3', + caption: 'test3', + }, + { + caption: 'test3 group', + columns: [ + { + dataField: 'test4', + caption: 'test4', + fixed: true, + }, + { + dataField: 'test5', + caption: 'test5', + fixed: true, + }, + ], + }, + ]); + consoleMessages = await t.getBrowserConsoleMessages(); + warnings = consoleMessages?.warn.filter((message) => message.startsWith('W1028')) || []; + await t.expect(warnings.length).eql(1, 'There is warning W1028'); +}).before(async () => { + await createWidget( + 'dxDataGrid', + { + dataSource: [], + }, + '#container', + ); +}); + +test('Warning should be shown one time for every dataGrid instance', async (t) => { + const otherGrid = new DataGrid('#otherContainer'); + + await otherGrid.apiAddColumn({ + dataField: 'test', + caption: 'test', + columns: [ + { + dataField: 'test1', + caption: 'test1', + fixed: true, + }, + { + dataField: 'test2', + caption: 'test2', + fixed: true, + }, + ], + }); + const consoleMessages = await t.getBrowserConsoleMessages(); + const warnings = consoleMessages?.warn.filter((message) => message.startsWith('W1028')) || []; + await t.expect(warnings.length).eql(2, 'There are two warnings W1028'); +}).before(async () => { + await createWidget( + 'dxDataGrid', + { + dataSource: mockCountries, + keyExpr: 'ID', + columnAutoWidth: true, + allowColumnReordering: true, + width: 600, + showBorders: true, + columnChooser: { enabled: true }, + columns: [ + { + dataField: 'Country', + fixed: true, + fixedPosition: 'left', + }, + { + dataField: 'Area', + fixed: true, + fixedPosition: 'left', + }, + { + caption: 'Population', + columns: [ + { + caption: 'Total', + dataField: 'Population_Total', + format: 'fixedPoint', + fixed: true, + fixedPosition: 'left', + }, + { + caption: 'Urban', + dataField: 'Population_Urban', + format: 'percent', + fixed: true, + fixedPosition: 'left', + }, + ], + }, + ], + }, + '#container', + ); + + await createWidget( + 'dxDataGrid', + { + dataSource: [], + }, + '#otherContainer', + ); +}); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focus.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focus.ts index e5e2ade0c3dd..7d3c1ff8321d 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focus.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/focus/focus.ts @@ -298,3 +298,77 @@ test('DataGrid - FocusedRowChanged event isnt raised when the push API is used t }, })); }); + +test('DataGrid - Focused cell appearance is applied to non-editable CheckBox cells on mouse clicks (T1282082)', async (t) => { + const grid = new DataGrid(GRID_SELECTOR); + + await t + .click(grid.getDataCell(0, 0).element) + .click(grid.getDataCell(0, 1).element) + .click(grid.getDataCell(0, 0).element) + .expect(grid.getDataCell(0, 0).isFocused) + .notOk(); +}).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { BoolOne: false, BoolTwo: false }, + ], + columns: [ + 'BoolOne', + 'BoolTwo', + ], +})); + +// T1293309 +test('Focus method should focus the first data cell', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await dataGrid.apiFocus(); + + await t + .expect(dataGrid.getDataCell(0, 0).element.focused) + .ok(); +}).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { id: 1, name: 'name 1' }, + { id: 2, name: 'name 2' }, + { id: 3, name: 'name 3' }, + ], + keyExpr: 'id', + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], +})); + +// T1293309 +test('Focus method should focus the first data row when focusedRowEnabled = true', async (t) => { + const dataGrid = new DataGrid(GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await dataGrid.apiFocus(); + + await t + .expect(dataGrid.getDataRow(0).element.focused) + .ok(); +}).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { id: 1, name: 'name 1' }, + { id: 2, name: 'name 2' }, + { id: 3, name: 'name 3' }, + ], + keyExpr: 'id', + focusedRowEnabled: true, + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts index c66431a78d2f..4914d9836a35 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts @@ -5514,7 +5514,7 @@ test('Focus events should be called when pressing the Ctrl + End key when virtua })(); }); -test('Focus events should be called when pressing the Ctrl + End key when virtual scrolling and columns are enabled', async (t) => { +test.meta({ unstable: true })('Focus events should be called when pressing the Ctrl + End key when virtual scrolling and columns are enabled', async (t) => { // arrange const dataGrid = new DataGrid('#container'); @@ -5584,7 +5584,7 @@ test('Focus events should be called when pressing the Ctrl + End key when virtua })(); }); -test('Focus events should be called when pressing the Ctrl + End key when rowRenderingMode is \'virtual\'', async (t) => { +test.meta({ unstable: true })('Focus events should be called when pressing the Ctrl + End key when rowRenderingMode is \'virtual\'', async (t) => { // arrange const dataGrid = new DataGrid('#container'); @@ -5653,7 +5653,7 @@ test('Focus events should be called when pressing the Ctrl + End key when rowRen })(); }); -test('Focus events should be called when pressing the Ctrl + End key when infinite scrolling is enabled', async (t) => { +test.meta({ unstable: true })('Focus events should be called when pressing the Ctrl + End key when infinite scrolling is enabled', async (t) => { // arrange const dataGrid = new DataGrid('#container'); @@ -5879,7 +5879,7 @@ test('Focus events should be called when pressing the Ctrl + End key when virtua })(); }); -test('Focus events should be called when pressing the Ctrl + End key when virtual columns, virtual scrolling and focusedRowEnabled are enabled', async (t) => { +test.meta({ unstable: true })('Focus events should be called when pressing the Ctrl + End key when virtual columns, virtual scrolling and focusedRowEnabled are enabled', async (t) => { // arrange const dataGrid = new DataGrid('#container'); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts index 6d370b6a1b9b..f8a879191153 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.visual.ts @@ -431,7 +431,7 @@ test('Navigate to first cell in the first row when pressing the Ctrl + Home key' showBorders: true, })); -test('Navigate to last cell in the last row when virtual scrolling is enabled', async (t) => { +test.meta({ unstable: true })('Navigate to last cell in the last row when virtual scrolling is enabled', async (t) => { // arrange const dataGrid = new DataGrid('#container'); const { takeScreenshot, compareResults } = createScreenshotsComparer(t); @@ -496,7 +496,7 @@ test('Navigate to first cell in the first row when virtual scrolling is enabled' }, })); -test('Navigate to last cell in the last row when virtual scrolling and columns are enabled', async (t) => { +test.meta({ unstable: true })('Navigate to last cell in the last row when virtual scrolling and columns are enabled', async (t) => { // arrange const dataGrid = new DataGrid('#container'); const { takeScreenshot, compareResults } = createScreenshotsComparer(t); @@ -568,7 +568,7 @@ test('Navigate to first cell in the first row when virtual scrolling and columns })); [true, false].forEach((useNative) => { - test(`${useNative ? 'Native' : 'Simulated'} scrolling: Focus should be on the first focusable cell when pressing the Ctrl + Home key when row dragging, virtual scrolling and columns are enabled`, async (t) => { + test.meta({ unstable: true })(`${useNative ? 'Native' : 'Simulated'} scrolling: Focus should be on the first focusable cell when pressing the Ctrl + Home key when row dragging, virtual scrolling and columns are enabled`, async (t) => { // arrange const dataGrid = new DataGrid('#container'); const { takeScreenshot, compareResults } = createScreenshotsComparer(t); @@ -612,7 +612,7 @@ test('Navigate to first cell in the first row when virtual scrolling and columns }, })); - test(`${useNative ? 'Native' : 'Simulated'} scrolling: Focus should be on the last focusable cell when pressing the Ctrl + Home key when row dragging, virtual scrolling and columns are enabled`, async (t) => { + test.meta({ unstable: true })(`${useNative ? 'Native' : 'Simulated'} scrolling: Focus should be on the last focusable cell when pressing the Ctrl + Home key when row dragging, virtual scrolling and columns are enabled`, async (t) => { // arrange const dataGrid = new DataGrid('#container'); const { takeScreenshot, compareResults } = createScreenshotsComparer(t); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts index 35996af8c303..8b5ee51c3427 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/scrolling.ts @@ -20,12 +20,12 @@ async function getRightScrollOffset(dataGrid: DataGrid): Promise { return maxHorizontalOffset - scrollLeft; } -function getData(rowCount, colCount): Record[] { +function getData(rowCount: number, colCount: number): Record[] { const items: Record[] = []; for (let i = 0; i < rowCount; i += 1) { const item: Record = {}; for (let j = 0; j < colCount; j += 1) { - item[`field_${i}_${j}`] = `val_${i}_${j}`; + item[`field_${j}`] = `val_${i}_${j}`; } items.push(item); } @@ -1824,3 +1824,88 @@ test('DataGrid - Scrolling position is reset to far right on an attempt to scrol enabled: false, }, })); + +// T1280020 +test('DataGrid - The "row" parameter in the FocusedRowChanged event refers to a non-focused row if the grid height is small', async (t) => { + const dataGrid = new DataGrid('#container'); + const otherContainer = Selector('#otherContainer'); + + await dataGrid.apiOption('focusedRowKey', '2'); + await t.expect(otherContainer.innerText).eql('2'); + + await dataGrid.apiOption('focusedRowKey', '0'); + await t.expect(otherContainer.innerText).eql('0'); +}).before(async () => createWidget('dxDataGrid', { + height: 70, + dataSource: [ + { id: '0' }, + { id: '1' }, + { id: '2' }, + ], + scrolling: { mode: 'virtual' }, + keyExpr: 'id', + focusedRowEnabled: true, + onFocusedRowChanged(e) { + const data = e.row?.data; + $('#otherContainer').text(data.id); + }, +})); + +[true, false].forEach((nativeScroll) => { + type TestCaseWindow = typeof window & { dataGridScrollableEventValues?: number[] }; + + test( + `Should not scroll back on top with virtual scrolling and adaptive master detail (nativeScroll: ${nativeScroll}) [T1278804]`, + async (t) => { + // NOTE: idx + 1 logic inside POM + const adaptiveCellIdx = 101; + const scrollValuesThreshold = 100; + + const dataGrid = new DataGrid('#container'); + const firstRow = dataGrid.getDataRow(0); + const firstDataCell = firstRow.getDataCell(0); + const adaptiveCell = firstRow.getCommandCell(adaptiveCellIdx); + const scrollContainer = dataGrid.getScrollContainer(); + + await t + .click(firstDataCell.element) + .click(adaptiveCell.element); + + await t + .scroll(scrollContainer, 0, 1000) + .scroll(scrollContainer, 0, 1000); + + const scrollOffsets = await t + .eval(() => (window as TestCaseWindow).dataGridScrollableEventValues) as number[]; + + const hasSmallScrollValues = scrollOffsets.some((offset) => offset < scrollValuesThreshold); + await t.expect(hasSmallScrollValues).notOk(); + }, + ).before(async () => { + await createWidget('dxDataGrid', { + dataSource: getData(3, 100).map((item, idx) => ({ ...item, id: idx })), + keyExpr: 'id', + columnHidingEnabled: true, + focusedRowEnabled: true, + scrolling: { + mode: 'virtual', + useNative: nativeScroll, + }, + onContentReady: ({ component }) => { + const testWindow = window as TestCaseWindow; + + component.getScrollable().on('scroll', ({ scrollOffset: { top } }) => { + if (!Array.isArray(testWindow.dataGridScrollableEventValues)) { + testWindow.dataGridScrollableEventValues = []; + } + + testWindow.dataGridScrollableEventValues.push(top); + }); + }, + width: 400, + height: 400, + }); + }).after(async (t) => t.eval(() => { + delete (window as TestCaseWindow).dataGridScrollableEventValues; + })); +}); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/stateStoring/stateStoring.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/stateStoring/stateStoring.ts index 0934476e8aab..10f81f11ea5a 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/stateStoring/stateStoring.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/stateStoring/stateStoring.ts @@ -142,3 +142,53 @@ test('DataGrid - Cannot read properties of undefined (reading \'done\') error oc }).before(async () => { await createWidget('dxDataGrid', { ...dataGridConfig }); }); + +test('DataGrid - The filterType property is reset if client state storing contains no filtering settings (T1296608)', async (t) => { + const dataGrid = new DataGrid('#container'); + + // assert + await t + .expect(dataGrid.isReady()) + .ok() + .expect(dataGrid.getDataCell(0, 0).element().innerText) + .eql('1'); +}).before(async () => { + await createWidget('dxDataGrid', { + dataSource: [ + { id: 0, textID: '0', text: 'item 0' }, + { id: 1, textID: '1', text: 'item 1' }, + ], + keyExpr: 'id', + filterSyncEnabled: true, + columns: [ + { + dataField: 'id', + caption: 'ID', + dataType: 'string', + }, + { + dataField: 'textID', + filterType: 'exclude', + name: 'textID', + dataType: 'string', + filterValues: ['0'], + }, + ], + stateStoring: { + enabled: true, + type: 'custom', + customLoad() { + return Promise.resolve({ + columns: [ + { + dataField: 'id', + }, + { + dataField: 'textID', + }, + ], + }); + }, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/summary.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/summary.ts index 28159032b3c4..21ac16789e69 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/summary.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/summary.ts @@ -70,6 +70,42 @@ test('Total summary should be focusable', async (t) => { }, })); +test('Focused total summary should have right appearance with sticky columns', async (t) => { + const dataGrid = new DataGrid('#container'); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.click(dataGrid.getDataRow(3).element); + await t + .pressKey('tab'); + + await t + .expect(await takeScreenshot('total-summary-focused-with-sticky.png', dataGrid.element)).ok() + .expect(compareResults.isValid()).ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { id: 1, value1: 1, value2: 2 }, + { id: 2, value1: 1, value2: 2 }, + { id: 3, value1: 1, value2: 2 }, + { id: 4, value1: 1, value2: 2 }, + ], + columns: [ + { dataField: 'value1', fixed: true }, + { dataField: 'value2' }, + ], + summary: { + totalItems: [ + { + column: 'value1', + summaryType: 'count', + }, + { + column: 'value2', + summaryType: 'count', + }, + ], + }, +})); + test('Group footer navigation should work without keyboard trap', async (t) => { const dataGrid = new DataGrid('#container'); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/etalons/T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=false).png b/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/etalons/T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=false).png new file mode 100644 index 000000000000..b632279ea2d3 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/etalons/T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=false).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/etalons/T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=true).png b/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/etalons/T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=true).png new file mode 100644 index 000000000000..e38e7b2e3b0f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/etalons/T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=true).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/withBandColumns.ts b/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/withBandColumns.ts new file mode 100644 index 000000000000..aecf662d300e --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/sticky/common/withBandColumns.ts @@ -0,0 +1,52 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { safeSizeTest } from '../../../../helpers/safeSizeTest'; +import { createWidget } from '../../../../helpers/createWidget'; +import url from '../../../../helpers/getPageUrl'; + +const DATA_GRID_SELECTOR = '#container'; + +fixture.disablePageReloads`Band sticky columns` + .page(url(__dirname, '../../../container.html')); + +[false, true].forEach((rtlEnabled) => { + // T1279722 + safeSizeTest(`Headers and filter row should display correctly after scrolling to the max right position when there is a grouped column (rtl=${rtlEnabled})`, async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const dataGrid = new DataGrid(DATA_GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await dataGrid.scrollTo(t, { x: rtlEnabled ? 0 : 10000 }); + await takeScreenshot(`T1279722_band_sticky_columns-headers_with_filter_row_and_grouped_column_(rtl=${rtlEnabled}).png`, dataGrid.element); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }, [900, 800]).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { + field0: 1, field1: 1, field2: 1, field3: 1, field4: 1, field5: 1, field6: 1, field7: 1, + }, + ], + keyExpr: 'field0', + width: 500, + columnWidth: 100, + columns: [{ + dataField: 'field0', + fixed: true, + fixedPosition: rtlEnabled ? 'right' : 'left', + }, { + caption: 'Band', + fixed: true, + fixedPosition: rtlEnabled ? 'right' : 'left', + columns: [{ + dataField: 'field1', + groupIndex: 0, + }, 'field2'], + }, 'field3', 'field4', 'field5', 'field6', 'field7'], + showBorders: true, + filterRow: { visible: true }, + rtlEnabled, + })); +}); diff --git a/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (fluent-blue-light).png index b228a2ca4fc4..2e2e40632596 100644 Binary files a/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (generic-light).png b/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (generic-light).png index 90137d62e2f2..41408c5ea3cd 100644 Binary files a/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (generic-light).png and b/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (material-blue-light).png b/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (material-blue-light).png index 1c263ca1f894..9e3e2e753dc5 100644 Binary files a/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (material-blue-light).png and b/e2e/testcafe-devextreme/tests/editors/colorbox/etalons/Colorbox with placeholder (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png index 511e39729291..92333bdf19d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png index 60cba24b6282..3e1c83698774 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png index 6d77f91c6aa7..c4772a04c242 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png index ecb1ee272955..7fe5756790d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png index 7a20d12b6209..4d43fe7eb4d4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png index 87145d1f0484..101c1af2269b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png index b252ab72ef77..a90efb02188d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png index c14d6ddea53e..fd34fdab391b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png index 50c15ac53e2c..59e00a066239 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 511e39729291..92333bdf19d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png index 60cba24b6282..3e1c83698774 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 6d77f91c6aa7..c4772a04c242 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index a82d682423ff..07cdb23faeb7 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 4af716a4a407..ed7671f634c8 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 4055eec4202a..0f65f30a228b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index fa0c462e62ab..9593af82f69c 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 6a1ff717f370..76388b2b8733 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index 8f769fa40c5b..3df6b4ff17f0 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png index e9d89aeb1e47..a28a893e8690 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png index 34c78ed7267c..73dd6bfe6740 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png index 382372a75559..4c97e01bf56e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png index 05e54f2dedce..cb0f6bde2335 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png index 9a8055ecf527..cc1da98b44e1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png index 7897a63f2415..6aa38dda93bd 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index e9d89aeb1e47..a28a893e8690 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index ef16ae21431c..8c785d14ccad 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index c30fcb10e485..ac3d53bfd2d4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 34c78ed7267c..73dd6bfe6740 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 98c5ad34c929..0db707458712 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index bd832d0f346b..b9aca5f6a409 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 0e81206296f0..4fcb466c977b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 9a072cf67732..58100f37fffe 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 497caaa049ed..2e26fece2868 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 8dbbcf4d3b83..68fb0e856cc1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 67b452132a05..18fe84061def 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 4b45f7324dfe..d16a53a4eb51 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 07d7b01d8487..19ad31f7a26d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 3305983af1b1..1e36d11372b7 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index c382cc6542da..c6415eaef6c2 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 015b77fa1e99..c26a0e0850f3 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index d92716e1f4ae..403d739cfa54 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index 593e7432051b..d5f398c5ec90 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 19e17af3f1e1..5e8dd2274166 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 4cacb32a3b9a..0f158d3c1ee5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 42d2764149ce..d42a81bc33df 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index d2c9a27ab3cf..7630202cdcca 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index a9c756750b65..4ceca55b9825 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 17f714974758..6cdbb4db6708 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 3acc9c1d23b4..78473d1749ef 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 3e45bc251350..ae7a23c572c4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 14b713d905f4..addeb144f32f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 71d997b6ae60..b56c571bd36d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index ff6d6b1a61d0..12ed3d05535d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index dbc67be7d455..ca48eeb07b04 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index c08a7643e561..a2a825329b3b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index deed1b0e0965..545695620e8a 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 93b09de29c6b..9a8ddeba279b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 002e2da04cab..6f73613c33d7 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 1e2a4663e424..d00b0ccaeeb8 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index f27903d811c9..f1711040a0e9 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 5aeb5c61cca1..4d3df75a1376 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 93c4c802bba9..0ccd8492990e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index e2fb48251a70..1b286755c008 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 3cf743c2058d..0af80bac3cb3 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index e1331102d3d8..6530f76f9ce9 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 01f3f0013287..9424d1682b02 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 3ffe80cdd9ec..9cfdec5b5699 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 15afaf70f2ae..31a729c7fdd5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 4193faf3dba1..4fec85f8dd21 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index fe53ee59a8e7..63334f380f43 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 7013c3e1010b..1345f4cce6e7 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 4a6c31bce1f5..7dabc67c4347 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=floating,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png index 511e39729291..92333bdf19d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png index 60cba24b6282..3e1c83698774 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png index 6d77f91c6aa7..c4772a04c242 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png index ecb1ee272955..7fe5756790d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png index 7a20d12b6209..4d43fe7eb4d4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png index 87145d1f0484..101c1af2269b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png index b252ab72ef77..a90efb02188d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png index c14d6ddea53e..fd34fdab391b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png index 50c15ac53e2c..59e00a066239 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index ecb1ee272955..7fe5756790d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png index 7a20d12b6209..4d43fe7eb4d4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 87145d1f0484..101c1af2269b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 955b46754c8c..cc56807ed76a 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 2685693d89b2..9974d0733801 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 33941f9c409a..4aaf416a43a7 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 4e1002a71c56..f5dcfd4f09dc 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 665bd2559a02..f207fea5484b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index 08940ef6ff57..3ec5ffc32325 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png index e9d89aeb1e47..a28a893e8690 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png index 34c78ed7267c..73dd6bfe6740 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png index 382372a75559..4c97e01bf56e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png index 05e54f2dedce..cb0f6bde2335 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png index 9a8055ecf527..cc1da98b44e1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png index 7897a63f2415..6aa38dda93bd 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 382372a75559..4c97e01bf56e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 42818d739318..4e6fe64f815c 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index f47af59a5d51..b85b433c173b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 05e54f2dedce..cb0f6bde2335 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index da2434a4acd1..06d6b4c68e1f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 1e6d8b181f22..895393afdd74 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 389b62f89211..a5489ea71484 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 7481faeaf189..6fdbdef1f16b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index f8f5a2283b4a..ce533a9c543f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 274dabf53a68..040463302d12 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 9f3efefe41f1..53fa3cf3603e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 16aa159f46fc..a2ceabc26f8b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index d8f6c5fe7521..819835b6adf5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 7c1bc14482e7..09d13fba4e61 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 10c588309469..ef26810668d6 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 9bf15d710c39..f83d31886336 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 738d09a0ce7a..24088578ac9f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index d39c49b2749f..7a2b97d5277b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 6680f27bfe13..da3d3653fa4f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 7c1516bbb530..40ee37032d59 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 33596d92fce4..da7e6ed03be4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 4c3394e7ee07..3b205418fe4e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index aa96e60a2b99..7a54a3190810 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 231d47be47f7..82c38077f923 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index e51f48376ea5..0f3e0ab9c8da 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 437bce142d81..0ef47f95f787 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index e82abdf83f5d..5090a84c8125 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index fc30ce358b58..44ccf405df4f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 53f9169ce575..6ffed92ba497 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 2e5b4967e738..b6ebc890b186 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index ce5d02a1a542..39b522b696b5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index d8caa7989a1b..93590d698d26 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 54d8e2e06cd6..3985396cc400 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 6e12abc80dac..c7fc4e403a8f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 28e4e2b3f938..bfa0747a1033 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index 2742d1445741..42d43af0c3e8 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 970664455ab0..a7c76cee214c 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 76702df89e8a..122f98638a74 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index f1336bb6c319..5f680f1e2b91 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 06a3cf725669..4dbaff920d7a 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 7fcff3ae162b..6a693cf3e276 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 8151c48ce3e3..0d4eb12d3fb8 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 8418079c202f..db969cfcfe1d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 3c25737f0056..baa2713dafa2 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index b38584f046d6..22310cf7f32d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 696bac7c9e7b..5d7064f026cb 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index b1ec4fa18940..7d17829e325b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 75b6b2cdd72c..366f858dc817 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=hidden,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png index 511e39729291..92333bdf19d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png index 60cba24b6282..3e1c83698774 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png index 6d77f91c6aa7..c4772a04c242 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png index ecb1ee272955..7fe5756790d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png index 7a20d12b6209..4d43fe7eb4d4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png index 87145d1f0484..101c1af2269b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png index b252ab72ef77..a90efb02188d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png index c14d6ddea53e..fd34fdab391b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png index 50c15ac53e2c..59e00a066239 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index ecb1ee272955..7fe5756790d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png index 7a20d12b6209..4d43fe7eb4d4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 87145d1f0484..101c1af2269b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 955b46754c8c..cc56807ed76a 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 2685693d89b2..9974d0733801 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 33941f9c409a..4aaf416a43a7 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 4e1002a71c56..f5dcfd4f09dc 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 665bd2559a02..f207fea5484b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index 08940ef6ff57..3ec5ffc32325 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png index e9d89aeb1e47..a28a893e8690 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png index 34c78ed7267c..73dd6bfe6740 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png index 382372a75559..4c97e01bf56e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png index 05e54f2dedce..cb0f6bde2335 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png index 9a8055ecf527..cc1da98b44e1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png index 7897a63f2415..6aa38dda93bd 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 382372a75559..4c97e01bf56e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 42818d739318..4e6fe64f815c 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index f47af59a5d51..b85b433c173b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 05e54f2dedce..cb0f6bde2335 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index da2434a4acd1..06d6b4c68e1f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 1e6d8b181f22..895393afdd74 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 389b62f89211..a5489ea71484 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 7481faeaf189..6fdbdef1f16b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index f8f5a2283b4a..ce533a9c543f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 274dabf53a68..040463302d12 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 9f3efefe41f1..53fa3cf3603e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 16aa159f46fc..a2ceabc26f8b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index d8f6c5fe7521..819835b6adf5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 7c1bc14482e7..09d13fba4e61 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 10c588309469..ef26810668d6 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 9bf15d710c39..f83d31886336 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 738d09a0ce7a..24088578ac9f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index d39c49b2749f..7a2b97d5277b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 6680f27bfe13..da3d3653fa4f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 7c1516bbb530..40ee37032d59 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 33596d92fce4..da7e6ed03be4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 4c3394e7ee07..3b205418fe4e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index aa96e60a2b99..7a54a3190810 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 231d47be47f7..82c38077f923 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index e51f48376ea5..0f3e0ab9c8da 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 437bce142d81..0ef47f95f787 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index e82abdf83f5d..5090a84c8125 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index fc30ce358b58..44ccf405df4f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 53f9169ce575..6ffed92ba497 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 2e5b4967e738..b6ebc890b186 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index ce5d02a1a542..39b522b696b5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index d8caa7989a1b..93590d698d26 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 54d8e2e06cd6..3985396cc400 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 6e12abc80dac..c7fc4e403a8f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 28e4e2b3f938..bfa0747a1033 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index 2742d1445741..42d43af0c3e8 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 970664455ab0..a7c76cee214c 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 76702df89e8a..122f98638a74 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index f1336bb6c319..5f680f1e2b91 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 06a3cf725669..4dbaff920d7a 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 7fcff3ae162b..6a693cf3e276 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 8151c48ce3e3..0d4eb12d3fb8 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 8418079c202f..db969cfcfe1d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 3c25737f0056..baa2713dafa2 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index b38584f046d6..22310cf7f32d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 696bac7c9e7b..5d7064f026cb 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index b1ec4fa18940..7d17829e325b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 75b6b2cdd72c..366f858dc817 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=outside,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png index 511e39729291..92333bdf19d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png index 60cba24b6282..3e1c83698774 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png index 6d77f91c6aa7..c4772a04c242 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png index ecb1ee272955..7fe5756790d5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png index 7a20d12b6209..4d43fe7eb4d4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png index 87145d1f0484..101c1af2269b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png index b252ab72ef77..a90efb02188d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png index c14d6ddea53e..fd34fdab391b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png index 50c15ac53e2c..59e00a066239 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index b252ab72ef77..a90efb02188d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png index c14d6ddea53e..fd34fdab391b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=filled (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 50c15ac53e2c..59e00a066239 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=false,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 2e704e9bdd13..283783e807d3 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 7eb1d559cda9..b515d5585354 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 2c7b261279f8..3908947c1d07 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 36365b9adbeb..d66ffc28e1c0 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 16ed7ea8e0a7..58e170e9d0fc 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index caabd8ce0b9a..bcf618104c8a 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png index e9d89aeb1e47..a28a893e8690 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png index 34c78ed7267c..73dd6bfe6740 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=floating,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png index 382372a75559..4c97e01bf56e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png index 05e54f2dedce..cb0f6bde2335 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=hidden,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png index 9a8055ecf527..cc1da98b44e1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png index 7897a63f2415..6aa38dda93bd 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=static,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 9a8055ecf527..cc1da98b44e1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index f9efb46fb110..9c80ac9c4828 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index c8205de5ba8f..71e6751c9322 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 7897a63f2415..6aa38dda93bd 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 81287c7fd3cf..53fce2e9d0d9 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 4cbffa2ccfcb..907b6c7ca3c8 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index b2797f7a0026..b7047b0eab55 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index d3cf47c37704..bf875115ce39 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index bb2ddb86e2df..54254d651543 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 7211d6e1edef..f1e7f3bfef6f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index fb973741fd28..4dc81119c968 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index 81f7a81f5fbf..5de6fc220e65 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=left,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index db9090caf7fe..0800917dc35d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 5c3616aa6179..02000ea0b896 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index b77b97f6c1fd..c714cf5b856f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index 4e0604e6eeba..29b0c2011524 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 7eb1896ab917..5e13e8f6b8da 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index effc751133ce..703bd40d8c34 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 403d073dc454..d940c0637c5e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 66475a325256..4847bc2edd18 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 0c4c76a8a74e..0a58bde356df 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 8c112835623d..b3264bf16ee2 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 3ff2b09dd992..dca87ac2845e 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index d2047d244e08..e9f8ccd52f96 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index bcd13b602048..fea7cc96744a 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index 237232211c85..4fea1d91c448 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index c5e18245ae60..e6bcecd3ff02 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 5f10264f7953..369f8895e6eb 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 10c1f8396391..bea89cf8e4bb 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index c221f18be5f2..87c95c28108d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=right,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 134e58398ea2..8b466e0a5af2 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png index 631e77c8e638..b0d3a28f5062 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index b951b1ee2352..b8bf4b99a005 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png index ea8b0bfd2214..bfb8f70afadb 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index ab12101d3c27..58c70f9945bf 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png index 00fb87a63047..cb8c0ac0b9a1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=center,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 8f44f4ab7ea9..509fcd47e274 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png index 3263d8033c47..12314d765181 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index aa15ee43854c..017faeda8443 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png index 68884f5e336f..5b33ebe14d56 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 7e20921efff1..49347c0a18d3 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png index 04607af48224..a15f9b4f8e2b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=left,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png index 26391436f637..4a4c57f96f8f 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png index aab72b23e197..7b36e739791d 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=filled (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png index 280f9b6e3855..ef45dfbfaeee 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png index 833f7af67c2e..25b790245103 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=outlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png index 02b6795758c8..651522a95019 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png index ce31bc6806af..11f5e6fd8d4b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/Form,lMode=static,lLoc=top,lVis=true,lAl=right,e.lMode=undef,e.sMode=underlined (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (fluent-blue-light).png index 6144ccb4f224..e0fd37613088 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (generic-light).png index 89ee5ee8a542..3326a37936ea 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (material-blue-light).png index 7faad2322425..9e19863e660b 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=false (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (fluent-blue-light).png index fca943e60670..90fd01eb7b07 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (generic-light).png index cf06edd27d13..c027e7d16424 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (material-blue-light).png index 7518251716ac..b8ac52d56fbd 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=floating, shwSmclnAfterlbl=true (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (fluent-blue-light).png index 9e289800be08..715d4c66d14c 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (generic-light).png index a68a80c4a402..31bb62ae5ed4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (material-blue-light).png index 6c5889962282..3fa085630cd1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=false (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (fluent-blue-light).png index 9e289800be08..715d4c66d14c 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (generic-light).png index a68a80c4a402..31bb62ae5ed4 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (material-blue-light).png index 6c5889962282..3fa085630cd1 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=hidden, shwSmclnAfterlbl=true (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (fluent-blue-light).png index d97ae130bf21..e1f79f51b00a 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (generic-light).png index d8d3a3f8dbee..f2c8185fe2ed 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (material-blue-light).png index 436ec6895498..ad0d1e251896 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=false (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (fluent-blue-light).png index 1fc7f126e2fc..4dfcd9ed02e5 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (generic-light).png index b8616bafbdde..75c5e5391131 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (material-blue-light).png index 9cdf0a7b7915..d4f8457bd059 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=outside, shwSmclnAfterlbl=true (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (fluent-blue-light).png index 5310d226c536..3337e3a1709c 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (generic-light).png index d2119f07bdc7..9ea7b4c688d2 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (material-blue-light).png index 6b83c6d86966..0105657d0250 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=false (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (fluent-blue-light).png index 576f7fe1f7fd..aec959e71058 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (generic-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (generic-light).png index e91163b0671e..c06eed3b32e0 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (generic-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (material-blue-light).png b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (material-blue-light).png index ae193dbd2818..6263bace5b04 100644 Binary files a/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (material-blue-light).png and b/e2e/testcafe-devextreme/tests/form/etalons/show semicolon, lblMode=static, shwSmclnAfterlbl=true (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/navigation/contextMenu/contextMenu.ts b/e2e/testcafe-devextreme/tests/navigation/contextMenu/contextMenu.ts index 4da56c7fb9ce..80bedd3c66b2 100644 --- a/e2e/testcafe-devextreme/tests/navigation/contextMenu/contextMenu.ts +++ b/e2e/testcafe-devextreme/tests/navigation/contextMenu/contextMenu.ts @@ -13,7 +13,9 @@ test('Context menu should be shown in the same position when item was added in r await t .click(target) - .expect(contextMenu.overlay.getContent().getStyleProperty('visibility')).eql('visible'); + .expect(Selector('.dx-context-menu').exists).ok('Context menu element should exist') + .expect(contextMenu.overlay.getContent().getStyleProperty('visibility')) + .eql('visible'); const initialOverlayOffset = await contextMenu.overlay.getOverlayOffset(); diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/outlookDragging/base.ts b/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/outlookDragging/base.ts index 7d8cd42ca804..01c859e4709c 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/outlookDragging/base.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/outlookDragging/base.ts @@ -113,7 +113,7 @@ test('Basic drag-n-drop movements from tooltip in month view', async (t) => { .ok(); await t - .click(scheduler.collectors.find('1').element) + .click(scheduler.collectors.find('1', 1).element) .expect(scheduler.appointmentTooltip.isVisible()).ok() .drag(scheduler.appointmentTooltip.getListItem('Appointment 4').element, 320, 150) .expect(await takeScreenshot('drag-n-drop-\'Appointment 4\'-from-tooltip-in-month.png', scheduler.workSpace)) diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-agenda-view-long-names.png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-agenda-view-long-names.png new file mode 100644 index 000000000000..9633e3bf0296 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-agenda-view-long-names.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-group-by-date-long-names.png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-group-by-date-long-names.png new file mode 100644 index 000000000000..1adbbc74049d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-group-by-date-long-names.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-horizontal-grouping-long-names.png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-horizontal-grouping-long-names.png new file mode 100644 index 000000000000..a8049b17a961 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-horizontal-grouping-long-names.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-vertical-grouping-long-names.png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-vertical-grouping-long-names.png new file mode 100644 index 000000000000..c54f2ccfb802 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-vertical-grouping-long-names.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/groupHeaderLongNamesCss.ts b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/groupHeaderLongNamesCss.ts new file mode 100644 index 000000000000..a0bea0d4119c --- /dev/null +++ b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/groupHeaderLongNamesCss.ts @@ -0,0 +1,172 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import { extend } from 'devextreme/core/utils/extend'; +import Scheduler from 'devextreme-testcafe-models/scheduler'; +import { insertStylesheetRulesToPage, removeStylesheetRulesFromPage } from '../../../../helpers/domUtils'; +import { createWidget } from '../../../../helpers/createWidget'; +import url from '../../../../helpers/getPageUrl'; + +const SCHEDULER_SELECTOR = '#container'; + +const resources = [ + { + text: 'Very Long Priority Name for High Priority Tasks and Urgent Matters', + id: 1, + color: '#ff9747', + }, + { + text: 'Extremely Long Priority Name for Medium Priority Tasks and Regular Work', + id: 2, + color: '#24ff50', + }, + { + text: 'Super Long Priority Name for Low Priority Tasks and Background Activities', + id: 3, + color: '#3366ff', + }, +]; + +const dataSource = [ + { + text: 'Team Meeting', + startDate: new Date(2021, 3, 21, 10, 0), + endDate: new Date(2021, 3, 21, 11, 30), + priorityId: 1, + }, + { + text: 'Code Review', + startDate: new Date(2021, 3, 21, 14, 0), + endDate: new Date(2021, 3, 21, 15, 0), + priorityId: 2, + }, + { + text: 'Planning Session', + startDate: new Date(2021, 3, 22, 9, 0), + endDate: new Date(2021, 3, 22, 12, 0), + priorityId: 3, + }, +]; + +const DEFAULT_OPTIONS = { + currentDate: new Date(2021, 3, 21), + height: 600, + width: 1000, + startDayHour: 9, + endDayHour: 16, + crossScrollingEnabled: true, + showCurrentTimeIndicator: false, + showAllDayPanel: false, + groups: ['priorityId'], + views: [{ + type: 'workWeek', + name: 'Vertical Grouping', + groupOrientation: 'vertical', + cellDuration: 60, + intervalCount: 2, + }, + { + type: 'workWeek', + name: 'Horizontal Grouping', + groupOrientation: 'horizontal', + cellDuration: 30, + intervalCount: 2, + }, { + type: 'month', + name: 'Group By Date', + groupOrientation: 'horizontal', + }, 'agenda'], + resources: [{ + fieldExpr: 'priorityId', + allowMultiple: false, + dataSource: resources, + label: 'Priority', + }], + dataSource, +}; + +const CELL_SIZE_CSS = ` + #container .dx-scheduler-group-header { + width: auto; + } + #container .dx-scheduler-group-flex-container, + #container .dx-scheduler-work-space-vertical-group-table, + #container .dx-scheduler-sidebar-scrollable { + flex: 0 0 auto; + } +`; + +const createScheduler = async (options = {}): Promise => createWidget('dxScheduler', extend(DEFAULT_OPTIONS, options)); + +fixture.disablePageReloads`Scheduler: Group Header CSS for Long Resource Names` + .page(url(__dirname, '../../../container.html')); + +test('Group header CSS should work with vertical grouping and long resource names', async (t) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.expect(scheduler.element.find('.dx-scheduler-group-header').exists) + .ok('Group headers should exist'); + + await takeScreenshot('group-header-css-vertical-grouping-long-names.png', scheduler.element); + + await t.expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await insertStylesheetRulesToPage(CELL_SIZE_CSS); + await createScheduler({ currentView: 'Vertical Grouping' }); +}).after(async () => { + await removeStylesheetRulesFromPage(); +}); + +test('Group header CSS should work with horizontal grouping and long resource names', async (t) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.expect(scheduler.element.find('.dx-scheduler-group-header').exists) + .ok('Group headers should exist'); + + await takeScreenshot('group-header-css-horizontal-grouping-long-names.png', scheduler.element); + + await t.expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await insertStylesheetRulesToPage(CELL_SIZE_CSS); + await createScheduler({ currentView: 'Horizontal Grouping' }); +}).after(async () => { + await removeStylesheetRulesFromPage(); +}); + +test('Group header CSS should work with group by date and long resource names', async (t) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.expect(scheduler.element.find('.dx-scheduler-group-header').exists) + .ok('Group headers should exist'); + + await takeScreenshot('group-header-css-group-by-date-long-names.png', scheduler.element); + + await t.expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await insertStylesheetRulesToPage(CELL_SIZE_CSS); + await createScheduler({ currentView: 'Group By Date', groupByDate: true }); +}).after(async () => { + await removeStylesheetRulesFromPage(); +}); + +test('Group header CSS should work with agenda view and long resource names', async (t) => { + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.expect(scheduler.element.find('.dx-scheduler-group-header').exists) + .ok('Group headers should exist'); + + await takeScreenshot('group-header-css-agenda-view-long-names.png', scheduler.element); + + await t.expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await insertStylesheetRulesToPage(CELL_SIZE_CSS); + await createScheduler({ currentView: 'agenda' }); +}).after(async () => { + await removeStylesheetRulesFromPage(); +}); diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/collector.ts b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/collector.ts index ca91f75b4ce2..a64f3b864a90 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/collector.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/collector.ts @@ -2,11 +2,13 @@ import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; import Scheduler from 'devextreme-testcafe-models/scheduler'; import { createWidget } from '../../../../../helpers/createWidget'; import url from '../../../../../helpers/getPageUrl'; +import { generateOptionMatrix } from '../../../../../helpers/generateOptionMatrix'; +import { changeTheme } from '../../../../../helpers/changeTheme'; fixture.disablePageReloads`Appointments collector` .page(url(__dirname, '../../../../container.html')); -test('Appointment collector has correct offset when adaptivityEnabled=true', async (t) => { +test('Appointment collector has correct offset when adaptivityEnabled=true (T1024299)', async (t) => { const scheduler = new Scheduler('#container'); const { takeScreenshot, compareResults } = createScreenshotsComparer(t); @@ -29,3 +31,111 @@ test('Appointment collector has correct offset when adaptivityEnabled=true', asy }], height: 300, })); + +const getSchedulerBaseOptions = (view: string, count = 20) => { + const day = ['workWeek', 'timelineWorkWeek'].includes(view) ? 2 : 1; + const allDayAppointments = Array(Math.round(count / 4)).fill({ + allDay: true, + text: 'text', + startDate: new Date(2021, 7, day, 0), + endDate: new Date(2021, 7, day, 2), + }); + const regularAppointments = Array(Math.round((count * 3) / 4)).fill({ + text: 'text', + startDate: new Date(2021, 7, day, 0), + endDate: new Date(2021, 7, day, 2), + }); + const width = ['month', 'week', 'workWeek'].includes(view) ? 800 : 500; + const height = ['month'].includes(view) ? 500 : 300; + + return { + currentDate: new Date(2021, 7, day), + views: [view], + currentView: view, + dataSource: [...allDayAppointments, ...regularAppointments], + height, + width, + }; +}; + +generateOptionMatrix({ + view: ['day', 'week', 'workWeek', 'month', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth'], + theme: ['generic.light', 'material.blue.light', 'fluent.blue.light', 'generic.light.compact', 'material.blue.light.compact', 'fluent.blue.light.compact'], + adaptivityEnabled: [false, true], +}).forEach(({ view, theme, adaptivityEnabled }) => { + test(`Appointment collector has correct offset when view=${view} adaptivityEnabled=${adaptivityEnabled} theme=${theme}`, async (t) => { + const scheduler = new Scheduler('#container'); + + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t + .expect(await takeScreenshot(`appointment-collector-${view}-adapt(${adaptivityEnabled})-${theme}.png`, scheduler.workSpace)) + .ok() + + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => { + await changeTheme(theme); + + return createWidget('dxScheduler', { + adaptivityEnabled, + ...getSchedulerBaseOptions(view), + }); + }).after(async () => { + await changeTheme('generic.light'); + }); +}); + +['generic.light', 'material.blue.light', 'fluent.blue.light'].forEach((theme) => { + test(`Appointment collector has correct offset when month view with double interval theme=${theme}`, async (t) => { + const scheduler = new Scheduler('#container'); + + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t + .expect(await takeScreenshot(`appointment-collector-month-double-interval-${theme}.png`, scheduler.workSpace)) + .ok() + + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => { + await changeTheme(theme); + + return createWidget('dxScheduler', { + ...getSchedulerBaseOptions('month'), + views: [{ type: 'month', intervalCount: 2 }], + }); + }).after(async () => { + await changeTheme('generic.light'); + }); +}); + +generateOptionMatrix({ + view: ['day', 'week', 'workWeek', 'month', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth'], + variants: [ + { maxAppointmentsPerCell: 'auto', rtlEnabled: false }, + { maxAppointmentsPerCell: 'auto', rtlEnabled: true }, + { maxAppointmentsPerCell: 0, rtlEnabled: false }, + { maxAppointmentsPerCell: 2, rtlEnabled: false }, + { maxAppointmentsPerCell: 'unlimited', rtlEnabled: false }, + ], +}).forEach(({ + view, variants: { maxAppointmentsPerCell, rtlEnabled }, +}) => { + test(`Appointment collector has correct offset when view=${view} maxAppointmentsPerCell=${maxAppointmentsPerCell} rtlEnabled=${rtlEnabled}`, async (t) => { + const scheduler = new Scheduler('#container'); + + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t + .expect(await takeScreenshot(`appointment-collector-${view}-${maxAppointmentsPerCell}-rtl(${rtlEnabled}).png`, scheduler.workSpace)) + .ok() + + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => createWidget('dxScheduler', { + maxAppointmentsPerCell, + rtlEnabled, + ...getSchedulerBaseOptions(view, maxAppointmentsPerCell === 'unlimited' ? 8 : 20), + })); +}); diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-0-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-0-rtl(false).png new file mode 100644 index 000000000000..b7ca6d553b7e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-0-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-2-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-2-rtl(false).png new file mode 100644 index 000000000000..b070cb8258c5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-2-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..4522d8a55d2c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-fluent.blue.light.png new file mode 100644 index 000000000000..e7a4e2204535 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-generic.light.compact.png new file mode 100644 index 000000000000..e6f549dd6ae5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-generic.light.png new file mode 100644 index 000000000000..1bcf813e498b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-material.blue.light.compact.png new file mode 100644 index 000000000000..80c475db7dde Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-material.blue.light.png new file mode 100644 index 000000000000..91e23570c820 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(false)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..2a8e055e6bbc Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-fluent.blue.light.png new file mode 100644 index 000000000000..b63d6d2a6797 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-generic.light.compact.png new file mode 100644 index 000000000000..8fccb4d66758 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-generic.light.png new file mode 100644 index 000000000000..f5b79a3cbb29 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-material.blue.light.compact.png new file mode 100644 index 000000000000..a879dd72aff7 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-material.blue.light.png new file mode 100644 index 000000000000..46c56952293f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-adapt(true)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-auto-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-auto-rtl(false).png new file mode 100644 index 000000000000..1bcf813e498b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-auto-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-auto-rtl(true).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-auto-rtl(true).png new file mode 100644 index 000000000000..4f26cd78ee9d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-auto-rtl(true).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-unlimited-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-unlimited-rtl(false).png new file mode 100644 index 000000000000..0d04a6fcfe8a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-day-unlimited-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-0-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-0-rtl(false).png new file mode 100644 index 000000000000..11f1d7a70713 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-0-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-2-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-2-rtl(false).png new file mode 100644 index 000000000000..e0213ead2395 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-2-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..4b52c1771887 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-fluent.blue.light.png new file mode 100644 index 000000000000..ee55c0a73418 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-generic.light.compact.png new file mode 100644 index 000000000000..8cfdc4f8e7b3 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-generic.light.png new file mode 100644 index 000000000000..a43368a41c59 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-material.blue.light.compact.png new file mode 100644 index 000000000000..56a136550db1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-material.blue.light.png new file mode 100644 index 000000000000..354fa878f239 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(false)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..b6772362bd42 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-fluent.blue.light.png new file mode 100644 index 000000000000..25f619091653 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-generic.light.compact.png new file mode 100644 index 000000000000..28ef2ef17083 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-generic.light.png new file mode 100644 index 000000000000..bfca316fe3f7 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-material.blue.light.compact.png new file mode 100644 index 000000000000..5c8906b31a50 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-material.blue.light.png new file mode 100644 index 000000000000..ba15be701969 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-adapt(true)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-auto-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-auto-rtl(false).png new file mode 100644 index 000000000000..a43368a41c59 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-auto-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-auto-rtl(true).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-auto-rtl(true).png new file mode 100644 index 000000000000..11240eef2908 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-auto-rtl(true).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-fluent.blue.light.png new file mode 100644 index 000000000000..f95940c04a9b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-generic.light.png new file mode 100644 index 000000000000..8c39358ab271 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-material.blue.light.png new file mode 100644 index 000000000000..2b6fbf771bd1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-double-interval-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-unlimited-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-unlimited-rtl(false).png new file mode 100644 index 000000000000..2d5a90376286 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-month-unlimited-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-0-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-0-rtl(false).png new file mode 100644 index 000000000000..001236acc180 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-0-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-2-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-2-rtl(false).png new file mode 100644 index 000000000000..29b02e261b5b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-2-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..7f2e9cb1a0ad Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-fluent.blue.light.png new file mode 100644 index 000000000000..27d5854696e2 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-generic.light.compact.png new file mode 100644 index 000000000000..0567707d9c6d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-generic.light.png new file mode 100644 index 000000000000..29b02e261b5b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-material.blue.light.compact.png new file mode 100644 index 000000000000..e8dd3bd26af6 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-material.blue.light.png new file mode 100644 index 000000000000..1f97f7df0bc7 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(false)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..af1f1c9d8e7c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-fluent.blue.light.png new file mode 100644 index 000000000000..ae17b9c62bf7 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-generic.light.compact.png new file mode 100644 index 000000000000..8db26288c2fa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-generic.light.png new file mode 100644 index 000000000000..33134f23226c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-material.blue.light.compact.png new file mode 100644 index 000000000000..9f5b91f1949e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-material.blue.light.png new file mode 100644 index 000000000000..748c2ae95f04 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-adapt(true)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-auto-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-auto-rtl(false).png new file mode 100644 index 000000000000..29b02e261b5b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-auto-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-auto-rtl(true).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-auto-rtl(true).png new file mode 100644 index 000000000000..ecc220fff40e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-auto-rtl(true).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-unlimited-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-unlimited-rtl(false).png new file mode 100644 index 000000000000..717fc1bb3372 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineDay-unlimited-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-0-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-0-rtl(false).png new file mode 100644 index 000000000000..fdbbf5255ca3 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-0-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-2-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-2-rtl(false).png new file mode 100644 index 000000000000..e9a3f6a2e387 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-2-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..62311a571a00 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-fluent.blue.light.png new file mode 100644 index 000000000000..92106a14f130 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-generic.light.compact.png new file mode 100644 index 000000000000..2c30ac2b6714 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-generic.light.png new file mode 100644 index 000000000000..e9a3f6a2e387 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-material.blue.light.compact.png new file mode 100644 index 000000000000..320d1dc6b8e6 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-material.blue.light.png new file mode 100644 index 000000000000..bbd87af5e1b8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(false)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..4c82657a4748 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-fluent.blue.light.png new file mode 100644 index 000000000000..cfa9ab73e038 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-generic.light.compact.png new file mode 100644 index 000000000000..335b6be934a6 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-generic.light.png new file mode 100644 index 000000000000..fe84b70b8cba Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-material.blue.light.compact.png new file mode 100644 index 000000000000..4cef15567d53 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-material.blue.light.png new file mode 100644 index 000000000000..1d3f3e1435ef Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-adapt(true)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-auto-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-auto-rtl(false).png new file mode 100644 index 000000000000..e9a3f6a2e387 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-auto-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-auto-rtl(true).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-auto-rtl(true).png new file mode 100644 index 000000000000..9c7d86226f86 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-auto-rtl(true).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-unlimited-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-unlimited-rtl(false).png new file mode 100644 index 000000000000..7092a17164b8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineMonth-unlimited-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-0-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-0-rtl(false).png new file mode 100644 index 000000000000..7e224e09b5fd Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-0-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-2-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-2-rtl(false).png new file mode 100644 index 000000000000..d0d5e3fd4f4b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-2-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..3e39bbf7cd3f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-fluent.blue.light.png new file mode 100644 index 000000000000..6c48d0c353a9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-generic.light.compact.png new file mode 100644 index 000000000000..4e79e025455a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-generic.light.png new file mode 100644 index 000000000000..ae9f7aeaf62d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-material.blue.light.compact.png new file mode 100644 index 000000000000..f47b329da242 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-material.blue.light.png new file mode 100644 index 000000000000..ab78452ca3c9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(false)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..8168deb61619 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-fluent.blue.light.png new file mode 100644 index 000000000000..7f772565b45b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-generic.light.compact.png new file mode 100644 index 000000000000..fbcabdb78e1c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-generic.light.png new file mode 100644 index 000000000000..979e22405f44 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-material.blue.light.compact.png new file mode 100644 index 000000000000..2cb1d07c7ab4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-material.blue.light.png new file mode 100644 index 000000000000..6d712f4159ef Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-adapt(true)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-auto-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-auto-rtl(false).png new file mode 100644 index 000000000000..ae9f7aeaf62d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-auto-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-auto-rtl(true).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-auto-rtl(true).png new file mode 100644 index 000000000000..ea69b94e7c6e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-auto-rtl(true).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-unlimited-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-unlimited-rtl(false).png new file mode 100644 index 000000000000..243426c40b68 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWeek-unlimited-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-0-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-0-rtl(false).png new file mode 100644 index 000000000000..7e224e09b5fd Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-0-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-2-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-2-rtl(false).png new file mode 100644 index 000000000000..d0d5e3fd4f4b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-2-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..7b87162ee4fa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-fluent.blue.light.png new file mode 100644 index 000000000000..e32244923958 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-generic.light.compact.png new file mode 100644 index 000000000000..4e79e025455a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-generic.light.png new file mode 100644 index 000000000000..ae9f7aeaf62d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-material.blue.light.compact.png new file mode 100644 index 000000000000..49f63d024472 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-material.blue.light.png new file mode 100644 index 000000000000..e82fc1efc738 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(false)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..57a451dc07c2 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-fluent.blue.light.png new file mode 100644 index 000000000000..b3d327b7f883 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-generic.light.compact.png new file mode 100644 index 000000000000..fbcabdb78e1c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-generic.light.png new file mode 100644 index 000000000000..979e22405f44 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-material.blue.light.compact.png new file mode 100644 index 000000000000..e4b2b3cc9eb9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-material.blue.light.png new file mode 100644 index 000000000000..fa850d40f0c3 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-adapt(true)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-auto-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-auto-rtl(false).png new file mode 100644 index 000000000000..ae9f7aeaf62d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-auto-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-auto-rtl(true).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-auto-rtl(true).png new file mode 100644 index 000000000000..ea69b94e7c6e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-auto-rtl(true).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-unlimited-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-unlimited-rtl(false).png new file mode 100644 index 000000000000..243426c40b68 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-timelineWorkWeek-unlimited-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-0-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-0-rtl(false).png new file mode 100644 index 000000000000..acd5c13d621d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-0-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-2-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-2-rtl(false).png new file mode 100644 index 000000000000..e1680c74256b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-2-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..99b436b9a4ac Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-fluent.blue.light.png new file mode 100644 index 000000000000..2705048b164a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-generic.light.compact.png new file mode 100644 index 000000000000..fda40fc8be89 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-generic.light.png new file mode 100644 index 000000000000..c011c3b54648 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-material.blue.light.compact.png new file mode 100644 index 000000000000..2d0e69fa510c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-material.blue.light.png new file mode 100644 index 000000000000..b1b2feac837c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(false)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..a08a82ab5bab Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-fluent.blue.light.png new file mode 100644 index 000000000000..f97da20874c8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-generic.light.compact.png new file mode 100644 index 000000000000..9bd08fc4ec1f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-generic.light.png new file mode 100644 index 000000000000..67dc1dd82333 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-material.blue.light.compact.png new file mode 100644 index 000000000000..d06d36096539 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-material.blue.light.png new file mode 100644 index 000000000000..1c6df7175efa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-adapt(true)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-auto-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-auto-rtl(false).png new file mode 100644 index 000000000000..c011c3b54648 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-auto-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-auto-rtl(true).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-auto-rtl(true).png new file mode 100644 index 000000000000..927722793135 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-auto-rtl(true).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-unlimited-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-unlimited-rtl(false).png new file mode 100644 index 000000000000..b609eb293499 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-week-unlimited-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-0-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-0-rtl(false).png new file mode 100644 index 000000000000..4acbfcfe982f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-0-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-2-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-2-rtl(false).png new file mode 100644 index 000000000000..aa192948d455 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-2-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..225815894a2e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-fluent.blue.light.png new file mode 100644 index 000000000000..7a50523b92dc Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-generic.light.compact.png new file mode 100644 index 000000000000..5ed68934e662 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-generic.light.png new file mode 100644 index 000000000000..aa192948d455 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-material.blue.light.compact.png new file mode 100644 index 000000000000..d3f477d551df Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-material.blue.light.png new file mode 100644 index 000000000000..3a7032e8195f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(false)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-fluent.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-fluent.blue.light.compact.png new file mode 100644 index 000000000000..4d08fd202d78 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-fluent.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-fluent.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-fluent.blue.light.png new file mode 100644 index 000000000000..ef54bd4a67f5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-fluent.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-generic.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-generic.light.compact.png new file mode 100644 index 000000000000..538bf7eea208 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-generic.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-generic.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-generic.light.png new file mode 100644 index 000000000000..1bdf02ed7523 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-generic.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-material.blue.light.compact.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-material.blue.light.compact.png new file mode 100644 index 000000000000..3b1dd6c7a7ac Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-material.blue.light.compact.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-material.blue.light.png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-material.blue.light.png new file mode 100644 index 000000000000..9f49ffe8eb78 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-adapt(true)-material.blue.light.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-auto-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-auto-rtl(false).png new file mode 100644 index 000000000000..aa192948d455 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-auto-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-auto-rtl(true).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-auto-rtl(true).png new file mode 100644 index 000000000000..5d6ae5aea5be Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-auto-rtl(true).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-unlimited-rtl(false).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-unlimited-rtl(false).png new file mode 100644 index 000000000000..ab5349138bcf Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/appointments/etalons/appointment-collector-workWeek-unlimited-rtl(false).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/rerenderOnResize.ts b/e2e/testcafe-devextreme/tests/scheduler/common/rerenderOnResize.ts index adfdd703f9d2..8600efa565a3 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/rerenderOnResize.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/rerenderOnResize.ts @@ -8,7 +8,6 @@ fixture.disablePageReloads`Re-render on resize` .page(url(__dirname, '../../container.html')); const createScheduler = async (container, options?): Promise => createWidget('dxScheduler', { - ...options, currentDate: new Date(2020, 8, 7), startDayHour: 8, endDayHour: 20, @@ -27,24 +26,34 @@ const createScheduler = async (container, options?): Promise => createWidg endDate: new Date(2020, 8, 7, 9), text: 'test', }], + ...options, }, container); -safeSizeTest('Appointment should re-rendered on window resize when width and height not set (T1139566)', async (t) => { +safeSizeTest('Appointment should re-rendered on window resize-up (T1139566)', async (t) => { + const scheduler = new Scheduler('#container'); + const { element } = scheduler.getAppointment('test'); + + await setStyleAttribute(element, 'background-color: red;'); + await t.resizeWindow(800, 400); + await t.expect(await getStyleAttribute(element)).match(/transform: translate\(0px, 0px\); width: 10\d\.\d\d\dpx; height: 50px;/); +}, [400, 400]).before(async () => createScheduler('#container', { currentView: 'workWeek' })); + +safeSizeTest('Appointment should not re-rendered on window resize when width and height not set (T1139566)', async (t) => { const scheduler = new Scheduler('#container'); const { element } = scheduler.getAppointment('test'); await setStyleAttribute(element, 'background-color: red;'); await t.resizeWindow(300, 300); - await t.expect(await getStyleAttribute(element)).eql('transform: translate(0px, 26px); width: 200px; height: 74px;'); + await t.expect(await getStyleAttribute(element)).eql('transform: translate(0px, 26px); width: 200px; height: 74px; background-color: red;'); }).before(async () => createScheduler('#container')); -safeSizeTest('Appointment should re-rendered on window resize when width and height have percent value (T1139566)', async (t) => { +safeSizeTest('Appointment should not re-rendered on window resize when width and height have percent value (T1139566)', async (t) => { const scheduler = new Scheduler('#container'); const { element } = scheduler.getAppointment('test'); await setStyleAttribute(element, 'background-color: red;'); await t.resizeWindow(300, 400); - await t.expect(await getStyleAttribute(element)).eql('transform: translate(0px, 26px); width: 200px; height: 74px;'); + await t.expect(await getStyleAttribute(element)).eql('transform: translate(0px, 26px); width: 200px; height: 74px; background-color: red;'); }).before(async () => createScheduler('#container', { width: '100%', height: '100%' })); safeSizeTest('Appointment should not re-rendered on window resize when width and height have static value (T1139566)', async (t) => { diff --git a/e2e/testcafe-devextreme/tests/scheduler/timezones/etalons/appointments_support_old_timezone_db.png b/e2e/testcafe-devextreme/tests/scheduler/timezones/etalons/appointments_support_old_timezone_db.png deleted file mode 100644 index d8e326d2e758..000000000000 Binary files a/e2e/testcafe-devextreme/tests/scheduler/timezones/etalons/appointments_support_old_timezone_db.png and /dev/null differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/timezones/globalConfigTimezoneDB.ts b/e2e/testcafe-devextreme/tests/scheduler/timezones/globalConfigTimezoneDB.ts deleted file mode 100644 index a82d5ede8d03..000000000000 --- a/e2e/testcafe-devextreme/tests/scheduler/timezones/globalConfigTimezoneDB.ts +++ /dev/null @@ -1,57 +0,0 @@ -import Scheduler from 'devextreme-testcafe-models/scheduler'; -import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; -import { ClientFunction } from 'testcafe'; -import { createWidget } from '../../../helpers/createWidget'; -import url from '../../../helpers/getPageUrl'; - -fixture.disablePageReloads`Timezone DB backward compatibility` - .page(url(__dirname, '../../container.html')); - -const setTimezoneOffset = ClientFunction(() => { - (window as any).DevExpress.config({ - timezones: [ - { - id: 'Europe/London', untils: 'Infinity', offsets: '5', offsetIndices: '0', - }, - ], - }); -}); -const clearTimezoneOffset = ClientFunction(() => { - (window as any).DevExpress.config({ - timezones: null, - }); -}); - -const data = [ - { - text: 'Prepare 2021 Marketing Plan', - startDate: '2021-04-26T07:00:00.000Z', - endDate: '2021-04-26T09:30:00.000Z', - }]; - -test('Scheduler should support old timezone DB format', async (t) => { - const scheduler = new Scheduler('#container'); - const { takeScreenshot, compareResults } = createScreenshotsComparer(t); - - await takeScreenshot( - 'appointments_support_old_timezone_db.png', - scheduler.workSpace, - ); - - await t.expect(compareResults.isValid()) - .ok(compareResults.errorMessages()); -}).before(async () => { - await setTimezoneOffset(); - - await createWidget('dxScheduler', { - dataSource: data, - views: ['workWeek'], - timeZone: 'Europe/London', - currentView: 'workWeek', - currentDate: new Date(2021, 3, 27), - startDayHour: 8, - height: 600, - }); -}).after(async () => { - await clearTimezoneOffset(); -}); diff --git a/e2e/testcafe-devextreme/tests/treeList/focus.ts b/e2e/testcafe-devextreme/tests/treeList/focus.ts new file mode 100644 index 000000000000..85ed91426684 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/treeList/focus.ts @@ -0,0 +1,65 @@ +import TreeList from 'devextreme-testcafe-models/treeList'; +import { createWidget } from '../../helpers/createWidget'; +import url from '../../helpers/getPageUrl'; + +fixture.disablePageReloads`Focus` + .page(url(__dirname, '../container.html')); + +const TREE_LIST_SELECTOR = '#container'; + +// T1294363 +test('Focus method should focus the first data cell', async (t) => { + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await treeList.apiFocus(); + + await t + .expect(treeList.getDataCell(0, 0).element.focused) + .ok(); +}).before(async () => createWidget('dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, name: 'name 1' }, + { id: 2, parentId: 1, name: 'name 2' }, + { id: 3, parentId: 0, name: 'name 3' }, + ], + keyExpr: 'id', + parentId: 'parentId', + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], +})); + +// T1294363 +test('Focus method should focus the first data row when focusedRowEnabled = true', async (t) => { + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await treeList.apiFocus(); + + await t + .expect(treeList.getDataRow(0).element.focused) + .ok(); +}).before(async () => createWidget('dxTreeList', { + dataSource: [ + { id: 1, parentId: 0, name: 'name 1' }, + { id: 2, parentId: 1, name: 'name 2' }, + { id: 3, parentId: 0, name: 'name 3' }, + ], + keyExpr: 'id', + parentId: 'parentId', + focusedRowEnabled: true, + columns: [ + 'id', + { + dataField: 'name', + cellTemplate: (_, options) => $('
').attr('tabindex', 0).text(options.text), + }, + ], +})); diff --git a/packages/devextreme-angular/src/ui/card-view/index.ts b/packages/devextreme-angular/src/ui/card-view/index.ts index c6b559a79d30..ccb3fbf99cd0 100644 --- a/packages/devextreme-angular/src/ui/card-view/index.ts +++ b/packages/devextreme-angular/src/ui/card-view/index.ts @@ -47,6 +47,7 @@ import { } from 'devextreme-angular/core'; +import { DxoCardViewAiOptionsModule } from 'devextreme-angular/ui/card-view/nested'; import { DxoCardViewAnimationModule } from 'devextreme-angular/ui/card-view/nested'; import { DxiCardViewAsyncRuleModule } from 'devextreme-angular/ui/card-view/nested'; import { DxoCardViewAtModule } from 'devextreme-angular/ui/card-view/nested'; @@ -1339,6 +1340,7 @@ export class DxCardViewComponent extends DxComponen @NgModule({ imports: [ DxCardViewComponent, + DxoCardViewAiOptionsModule, DxoCardViewAnimationModule, DxiCardViewAsyncRuleModule, DxoCardViewAtModule, @@ -1419,6 +1421,7 @@ export class DxCardViewComponent extends DxComponen ], exports: [ DxCardViewComponent, + DxoCardViewAiOptionsModule, DxoCardViewAnimationModule, DxiCardViewAsyncRuleModule, DxoCardViewAtModule, diff --git a/packages/devextreme-angular/src/ui/card-view/nested/ai-options.ts b/packages/devextreme-angular/src/ui/card-view/nested/ai-options.ts new file mode 100644 index 000000000000..ffd284aa957a --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/ai-options.ts @@ -0,0 +1,83 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-card-view-ai-options', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoCardViewAiOptionsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get instruction(): string | undefined { + return this._getOption('instruction'); + } + set instruction(value: string | undefined) { + this._setOption('instruction', value); + } + + + protected get _optionPath() { + return 'aiOptions'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoCardViewAiOptionsComponent + ], + exports: [ + DxoCardViewAiOptionsComponent + ], +}) +export class DxoCardViewAiOptionsModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/button-item-dxi.ts b/packages/devextreme-angular/src/ui/card-view/nested/button-item-dxi.ts index 350a59412229..cacff2683882 100644 --- a/packages/devextreme-angular/src/ui/card-view/nested/button-item-dxi.ts +++ b/packages/devextreme-angular/src/ui/card-view/nested/button-item-dxi.ts @@ -14,7 +14,7 @@ import { import { dxButtonOptions } from 'devextreme/ui/button'; import { HorizontalAlignment, VerticalAlignment } from 'devextreme/common'; -import { FormItemType } from 'devextreme/ui/form'; +import { FormItemType, FormPredefinedButtonItem } from 'devextreme/ui/form'; import { DxIntegrationModule, @@ -73,10 +73,10 @@ export class DxiCardViewButtonItemComponent extends CollectionNestedOption { } @Input() - get name(): string | undefined { + get name(): FormPredefinedButtonItem | string | undefined { return this._getOption('name'); } - set name(value: string | undefined) { + set name(value: FormPredefinedButtonItem | string | undefined) { this._setOption('name', value); } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/form-item.ts b/packages/devextreme-angular/src/ui/card-view/nested/form-item.ts index bbefe52f2bb9..07b54fe5b3eb 100644 --- a/packages/devextreme-angular/src/ui/card-view/nested/form-item.ts +++ b/packages/devextreme-angular/src/ui/card-view/nested/form-item.ts @@ -57,6 +57,14 @@ import { DxiCardViewValidationRuleComponent } from './validation-rule-dxi'; }) export class DxoCardViewFormItemComponent extends NestedOption implements AfterViewInit, OnDestroy, OnInit, IDxTemplateHost, AfterContentInit { + @Input() + get aiOptions(): { disabled?: boolean, instruction?: string | undefined } { + return this._getOption('aiOptions'); + } + set aiOptions(value: { disabled?: boolean, instruction?: string | undefined }) { + this._setOption('aiOptions', value); + } + @Input() get colSpan(): number | undefined { return this._getOption('colSpan'); diff --git a/packages/devextreme-angular/src/ui/card-view/nested/form.ts b/packages/devextreme-angular/src/ui/card-view/nested/form.ts index 7fe4475c0154..3706c67b6f70 100644 --- a/packages/devextreme-angular/src/ui/card-view/nested/form.ts +++ b/packages/devextreme-angular/src/ui/card-view/nested/form.ts @@ -20,8 +20,9 @@ import { +import { AIIntegration } from 'devextreme/common/ai-integration'; import { Mode } from 'devextreme/common'; -import { dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent } from 'devextreme/ui/form'; +import { dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent, SmartPastedEvent, SmartPastingEvent } from 'devextreme/ui/form'; import { DxIntegrationModule, @@ -61,6 +62,14 @@ export class DxoCardViewFormComponent extends NestedOption implements OnDestroy, this._setOption('activeStateEnabled', value); } + @Input() + get aiIntegration(): AIIntegration | undefined { + return this._getOption('aiIntegration'); + } + set aiIntegration(value: AIIntegration | undefined) { + this._setOption('aiIntegration', value); + } + @Input() get alignItemLabels(): boolean { return this._getOption('alignItemLabels'); @@ -245,6 +254,22 @@ export class DxoCardViewFormComponent extends NestedOption implements OnDestroy, this._setOption('onOptionChanged', value); } + @Input() + get onSmartPasted(): ((e: SmartPastedEvent) => void) { + return this._getOption('onSmartPasted'); + } + set onSmartPasted(value: ((e: SmartPastedEvent) => void)) { + this._setOption('onSmartPasted', value); + } + + @Input() + get onSmartPasting(): ((e: SmartPastingEvent) => void) { + return this._getOption('onSmartPasting'); + } + set onSmartPasting(value: ((e: SmartPastingEvent) => void)) { + this._setOption('onSmartPasting', value); + } + @Input() get optionalMark(): string { return this._getOption('optionalMark'); diff --git a/packages/devextreme-angular/src/ui/card-view/nested/index.ts b/packages/devextreme-angular/src/ui/card-view/nested/index.ts index 0cd66ec42be6..7f418c889305 100644 --- a/packages/devextreme-angular/src/ui/card-view/nested/index.ts +++ b/packages/devextreme-angular/src/ui/card-view/nested/index.ts @@ -1,3 +1,4 @@ +export * from './ai-options'; export * from './animation'; export * from './async-rule-dxi'; export * from './at'; diff --git a/packages/devextreme-angular/src/ui/card-view/nested/item-dxi.ts b/packages/devextreme-angular/src/ui/card-view/nested/item-dxi.ts index 43f7d95cdce9..0b94884a3a51 100644 --- a/packages/devextreme-angular/src/ui/card-view/nested/item-dxi.ts +++ b/packages/devextreme-angular/src/ui/card-view/nested/item-dxi.ts @@ -24,7 +24,7 @@ import * as CommonTypes from 'devextreme/common'; import { LocateInMenuMode, ShowTextMode } from 'devextreme/ui/toolbar'; import { ToolbarItemLocation, ToolbarItemComponent, HorizontalAlignment, VerticalAlignment } from 'devextreme/common'; import { CardHeaderPredefinedItem, PredefinedToolbarItem } from 'devextreme/ui/card_view'; -import { FormItemComponent, FormItemType, LabelLocation, dxFormButtonItem, dxFormEmptyItem, dxFormGroupItem, dxFormSimpleItem, dxFormTabbedItem } from 'devextreme/ui/form'; +import { FormPredefinedButtonItem, FormItemComponent, FormItemType, LabelLocation, dxFormButtonItem, dxFormEmptyItem, dxFormGroupItem, dxFormSimpleItem, dxFormTabbedItem } from 'devextreme/ui/form'; import { dxTabPanelOptions } from 'devextreme/ui/tab_panel'; import { dxButtonOptions } from 'devextreme/ui/button'; @@ -109,10 +109,10 @@ export class DxiCardViewItemComponent extends CollectionNestedOption implements } @Input() - get name(): CardHeaderPredefinedItem | string | undefined | PredefinedToolbarItem { + get name(): CardHeaderPredefinedItem | string | undefined | FormPredefinedButtonItem | PredefinedToolbarItem { return this._getOption('name'); } - set name(value: CardHeaderPredefinedItem | string | undefined | PredefinedToolbarItem) { + set name(value: CardHeaderPredefinedItem | string | undefined | FormPredefinedButtonItem | PredefinedToolbarItem) { this._setOption('name', value); } @@ -196,6 +196,14 @@ export class DxiCardViewItemComponent extends CollectionNestedOption implements this._setOption('title', value); } + @Input() + get aiOptions(): { disabled?: boolean, instruction?: string | undefined } { + return this._getOption('aiOptions'); + } + set aiOptions(value: { disabled?: boolean, instruction?: string | undefined }) { + this._setOption('aiOptions', value); + } + @Input() get colSpan(): number | undefined { return this._getOption('colSpan'); diff --git a/packages/devextreme-angular/src/ui/card-view/nested/simple-item-dxi.ts b/packages/devextreme-angular/src/ui/card-view/nested/simple-item-dxi.ts index 215b4bcd5689..486d7c20e91d 100644 --- a/packages/devextreme-angular/src/ui/card-view/nested/simple-item-dxi.ts +++ b/packages/devextreme-angular/src/ui/card-view/nested/simple-item-dxi.ts @@ -55,6 +55,14 @@ import { DxiCardViewValidationRuleComponent } from './validation-rule-dxi'; }) export class DxiCardViewSimpleItemComponent extends CollectionNestedOption implements AfterViewInit, IDxTemplateHost, AfterContentInit { + @Input() + get aiOptions(): { disabled?: boolean, instruction?: string | undefined } { + return this._getOption('aiOptions'); + } + set aiOptions(value: { disabled?: boolean, instruction?: string | undefined }) { + this._setOption('aiOptions', value); + } + @Input() get colSpan(): number | undefined { return this._getOption('colSpan'); diff --git a/packages/devextreme-angular/src/ui/chat/index.ts b/packages/devextreme-angular/src/ui/chat/index.ts index 0fc0985c7af7..c254bbb187f7 100644 --- a/packages/devextreme-angular/src/ui/chat/index.ts +++ b/packages/devextreme-angular/src/ui/chat/index.ts @@ -193,6 +193,19 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges } + /** + * [descr:dxChatOptions.emptyViewTemplate] + + */ + @Input() + get emptyViewTemplate(): any { + return this._getOption('emptyViewTemplate'); + } + set emptyViewTemplate(value: any) { + this._setOption('emptyViewTemplate', value); + } + + /** * [descr:dxChatOptions.focusStateEnabled] @@ -565,6 +578,13 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges */ @Output() elementAttrChange: EventEmitter>; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() emptyViewTemplateChange: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -769,6 +789,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges { emit: 'disabledChange' }, { emit: 'editingChange' }, { emit: 'elementAttrChange' }, + { emit: 'emptyViewTemplateChange' }, { emit: 'focusStateEnabledChange' }, { emit: 'heightChange' }, { emit: 'hintChange' }, diff --git a/packages/devextreme-angular/src/ui/color-box/index.ts b/packages/devextreme-angular/src/ui/color-box/index.ts index e101e12ffd25..a62eaf27d5bd 100644 --- a/packages/devextreme-angular/src/ui/color-box/index.ts +++ b/packages/devextreme-angular/src/ui/color-box/index.ts @@ -25,7 +25,7 @@ import { import { ApplyValueMode, TextEditorButton, LabelMode, EditorStyle, ValidationMessageMode, Mode, Position, ValidationStatus } from 'devextreme/common'; -import { DropDownPredefinedButton } from 'devextreme/ui/drop_down_editor/ui.drop_down_editor'; +import { DropDownPredefinedButton, FieldAddons } from 'devextreme/ui/drop_down_editor/ui.drop_down_editor'; import { dxPopupOptions } from 'devextreme/ui/popup'; import { ChangeEvent, ClosedEvent, CopyEvent, CutEvent, DisposingEvent, EnterKeyEvent, FocusInEvent, FocusOutEvent, InitializedEvent, InputEvent, KeyDownEvent, KeyUpEvent, OpenedEvent, OptionChangedEvent, PasteEvent, ValueChangedEvent } from 'devextreme/ui/color_box'; @@ -67,6 +67,7 @@ import { DxoColorBoxBoundaryOffsetModule } from 'devextreme-angular/ui/color-box import { DxiColorBoxButtonModule } from 'devextreme-angular/ui/color-box/nested'; import { DxoColorBoxCollisionModule } from 'devextreme-angular/ui/color-box/nested'; import { DxoColorBoxDropDownOptionsModule } from 'devextreme-angular/ui/color-box/nested'; +import { DxoColorBoxFieldAddonsModule } from 'devextreme-angular/ui/color-box/nested'; import { DxoColorBoxFromModule } from 'devextreme-angular/ui/color-box/nested'; import { DxoColorBoxHideModule } from 'devextreme-angular/ui/color-box/nested'; import { DxoColorBoxMyModule } from 'devextreme-angular/ui/color-box/nested'; @@ -278,9 +279,24 @@ export class DxColorBoxComponent extends DxComponent implements OnDestroy, Contr } + /** + * [descr:dxDropDownEditorOptions.fieldAddons] + + */ + @Input() + get fieldAddons(): FieldAddons { + return this._getOption('fieldAddons'); + } + set fieldAddons(value: FieldAddons) { + this._setOption('fieldAddons', value); + } + + /** * [descr:dxColorBoxOptions.fieldTemplate] + * @deprecated [depNote:dxColorBoxOptions.fieldTemplate] + */ @Input() get fieldTemplate(): any { @@ -886,6 +902,13 @@ export class DxColorBoxComponent extends DxComponent implements OnDestroy, Contr */ @Output() elementAttrChange: EventEmitter>; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() fieldAddonsChange: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -1168,6 +1191,7 @@ export class DxColorBoxComponent extends DxComponent implements OnDestroy, Contr { emit: 'dropDownOptionsChange' }, { emit: 'editAlphaChannelChange' }, { emit: 'elementAttrChange' }, + { emit: 'fieldAddonsChange' }, { emit: 'fieldTemplateChange' }, { emit: 'focusStateEnabledChange' }, { emit: 'heightChange' }, @@ -1288,6 +1312,7 @@ export class DxColorBoxComponent extends DxComponent implements OnDestroy, Contr DxiColorBoxButtonModule, DxoColorBoxCollisionModule, DxoColorBoxDropDownOptionsModule, + DxoColorBoxFieldAddonsModule, DxoColorBoxFromModule, DxoColorBoxHideModule, DxoColorBoxMyModule, @@ -1322,6 +1347,7 @@ export class DxColorBoxComponent extends DxComponent implements OnDestroy, Contr DxiColorBoxButtonModule, DxoColorBoxCollisionModule, DxoColorBoxDropDownOptionsModule, + DxoColorBoxFieldAddonsModule, DxoColorBoxFromModule, DxoColorBoxHideModule, DxoColorBoxMyModule, diff --git a/packages/devextreme-angular/src/ui/color-box/nested/field-addons.ts b/packages/devextreme-angular/src/ui/color-box/nested/field-addons.ts new file mode 100644 index 000000000000..071c3d1b076f --- /dev/null +++ b/packages/devextreme-angular/src/ui/color-box/nested/field-addons.ts @@ -0,0 +1,83 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-color-box-field-addons', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoColorBoxFieldAddonsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get afterTemplate(): any { + return this._getOption('afterTemplate'); + } + set afterTemplate(value: any) { + this._setOption('afterTemplate', value); + } + + @Input() + get beforeTemplate(): any { + return this._getOption('beforeTemplate'); + } + set beforeTemplate(value: any) { + this._setOption('beforeTemplate', value); + } + + + protected get _optionPath() { + return 'fieldAddons'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoColorBoxFieldAddonsComponent + ], + exports: [ + DxoColorBoxFieldAddonsComponent + ], +}) +export class DxoColorBoxFieldAddonsModule { } diff --git a/packages/devextreme-angular/src/ui/color-box/nested/index.ts b/packages/devextreme-angular/src/ui/color-box/nested/index.ts index a5926253bf21..d0d98e2b17e0 100644 --- a/packages/devextreme-angular/src/ui/color-box/nested/index.ts +++ b/packages/devextreme-angular/src/ui/color-box/nested/index.ts @@ -4,6 +4,7 @@ export * from './boundary-offset'; export * from './button-dxi'; export * from './collision'; export * from './drop-down-options'; +export * from './field-addons'; export * from './from'; export * from './hide'; export * from './my'; diff --git a/packages/devextreme-angular/src/ui/data-grid/index.ts b/packages/devextreme-angular/src/ui/data-grid/index.ts index 580102cd0a12..5ff4f7d73ba9 100644 --- a/packages/devextreme-angular/src/ui/data-grid/index.ts +++ b/packages/devextreme-angular/src/ui/data-grid/index.ts @@ -116,6 +116,7 @@ import { DxoValueFormatModule } from 'devextreme-angular/ui/nested'; import { DxiTotalItemModule } from 'devextreme-angular/ui/nested'; import { DxoToolbarModule } from 'devextreme-angular/ui/nested'; +import { DxoDataGridAiOptionsModule } from 'devextreme-angular/ui/data-grid/nested'; import { DxoDataGridAnimationModule } from 'devextreme-angular/ui/data-grid/nested'; import { DxiDataGridAsyncRuleModule } from 'devextreme-angular/ui/data-grid/nested'; import { DxoDataGridAtModule } from 'devextreme-angular/ui/data-grid/nested'; @@ -2348,6 +2349,7 @@ export class DxDataGridComponent extends DxComponent DxoValueFormatModule, DxiTotalItemModule, DxoToolbarModule, + DxoDataGridAiOptionsModule, DxoDataGridAnimationModule, DxiDataGridAsyncRuleModule, DxoDataGridAtModule, @@ -2504,6 +2506,7 @@ export class DxDataGridComponent extends DxComponent DxoValueFormatModule, DxiTotalItemModule, DxoToolbarModule, + DxoDataGridAiOptionsModule, DxoDataGridAnimationModule, DxiDataGridAsyncRuleModule, DxoDataGridAtModule, diff --git a/packages/devextreme-angular/src/ui/data-grid/nested/ai-options.ts b/packages/devextreme-angular/src/ui/data-grid/nested/ai-options.ts new file mode 100644 index 000000000000..9ab630b092c3 --- /dev/null +++ b/packages/devextreme-angular/src/ui/data-grid/nested/ai-options.ts @@ -0,0 +1,83 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-data-grid-ai-options', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoDataGridAiOptionsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get instruction(): string | undefined { + return this._getOption('instruction'); + } + set instruction(value: string | undefined) { + this._setOption('instruction', value); + } + + + protected get _optionPath() { + return 'aiOptions'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoDataGridAiOptionsComponent + ], + exports: [ + DxoDataGridAiOptionsComponent + ], +}) +export class DxoDataGridAiOptionsModule { } diff --git a/packages/devextreme-angular/src/ui/data-grid/nested/form-item.ts b/packages/devextreme-angular/src/ui/data-grid/nested/form-item.ts index 54a579c85da3..3bcc45ef313c 100644 --- a/packages/devextreme-angular/src/ui/data-grid/nested/form-item.ts +++ b/packages/devextreme-angular/src/ui/data-grid/nested/form-item.ts @@ -57,6 +57,14 @@ import { DxiDataGridValidationRuleComponent } from './validation-rule-dxi'; }) export class DxoDataGridFormItemComponent extends NestedOption implements AfterViewInit, OnDestroy, OnInit, IDxTemplateHost, AfterContentInit { + @Input() + get aiOptions(): { disabled?: boolean, instruction?: string | undefined } { + return this._getOption('aiOptions'); + } + set aiOptions(value: { disabled?: boolean, instruction?: string | undefined }) { + this._setOption('aiOptions', value); + } + @Input() get colSpan(): number | undefined { return this._getOption('colSpan'); diff --git a/packages/devextreme-angular/src/ui/data-grid/nested/form.ts b/packages/devextreme-angular/src/ui/data-grid/nested/form.ts index 5ad917f2ac0e..fd41e09cffbc 100644 --- a/packages/devextreme-angular/src/ui/data-grid/nested/form.ts +++ b/packages/devextreme-angular/src/ui/data-grid/nested/form.ts @@ -16,8 +16,9 @@ import { +import { AIIntegration } from 'devextreme/common/ai-integration'; import { Mode } from 'devextreme/common'; -import { dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent } from 'devextreme/ui/form'; +import { dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent, SmartPastedEvent, SmartPastingEvent } from 'devextreme/ui/form'; import { DxIntegrationModule, @@ -51,6 +52,14 @@ export class DxoDataGridFormComponent extends NestedOption implements OnDestroy, this._setOption('activeStateEnabled', value); } + @Input() + get aiIntegration(): AIIntegration | undefined { + return this._getOption('aiIntegration'); + } + set aiIntegration(value: AIIntegration | undefined) { + this._setOption('aiIntegration', value); + } + @Input() get alignItemLabels(): boolean { return this._getOption('alignItemLabels'); @@ -235,6 +244,22 @@ export class DxoDataGridFormComponent extends NestedOption implements OnDestroy, this._setOption('onOptionChanged', value); } + @Input() + get onSmartPasted(): ((e: SmartPastedEvent) => void) { + return this._getOption('onSmartPasted'); + } + set onSmartPasted(value: ((e: SmartPastedEvent) => void)) { + this._setOption('onSmartPasted', value); + } + + @Input() + get onSmartPasting(): ((e: SmartPastingEvent) => void) { + return this._getOption('onSmartPasting'); + } + set onSmartPasting(value: ((e: SmartPastingEvent) => void)) { + this._setOption('onSmartPasting', value); + } + @Input() get optionalMark(): string { return this._getOption('optionalMark'); diff --git a/packages/devextreme-angular/src/ui/data-grid/nested/index.ts b/packages/devextreme-angular/src/ui/data-grid/nested/index.ts index 44a8bc68951c..fdc95eba603f 100644 --- a/packages/devextreme-angular/src/ui/data-grid/nested/index.ts +++ b/packages/devextreme-angular/src/ui/data-grid/nested/index.ts @@ -1,3 +1,4 @@ +export * from './ai-options'; export * from './animation'; export * from './async-rule-dxi'; export * from './at'; diff --git a/packages/devextreme-angular/src/ui/drop-down-box/index.ts b/packages/devextreme-angular/src/ui/drop-down-box/index.ts index 670f36ad5c0b..c5631b26bffa 100644 --- a/packages/devextreme-angular/src/ui/drop-down-box/index.ts +++ b/packages/devextreme-angular/src/ui/drop-down-box/index.ts @@ -25,7 +25,7 @@ import { import DataSource from 'devextreme/data/data_source'; -import { DropDownPredefinedButton } from 'devextreme/ui/drop_down_editor/ui.drop_down_editor'; +import { DropDownPredefinedButton, FieldAddons } from 'devextreme/ui/drop_down_editor/ui.drop_down_editor'; import { TextEditorButton, LabelMode, EditorStyle, ValidationMessageMode, Mode, Position, ValidationStatus } from 'devextreme/common'; import { DataSourceOptions } from 'devextreme/data/data_source'; import { Store } from 'devextreme/data/store'; @@ -71,6 +71,7 @@ import { DxoDropDownBoxBoundaryOffsetModule } from 'devextreme-angular/ui/drop-d import { DxiDropDownBoxButtonModule } from 'devextreme-angular/ui/drop-down-box/nested'; import { DxoDropDownBoxCollisionModule } from 'devextreme-angular/ui/drop-down-box/nested'; import { DxoDropDownBoxDropDownOptionsModule } from 'devextreme-angular/ui/drop-down-box/nested'; +import { DxoDropDownBoxFieldAddonsModule } from 'devextreme-angular/ui/drop-down-box/nested'; import { DxoDropDownBoxFromModule } from 'devextreme-angular/ui/drop-down-box/nested'; import { DxoDropDownBoxHideModule } from 'devextreme-angular/ui/drop-down-box/nested'; import { DxoDropDownBoxMyModule } from 'devextreme-angular/ui/drop-down-box/nested'; @@ -283,9 +284,24 @@ export class DxDropDownBoxComponent extends DxComponent implements OnDestroy, Co } + /** + * [descr:dxDropDownEditorOptions.fieldAddons] + + */ + @Input() + get fieldAddons(): FieldAddons { + return this._getOption('fieldAddons'); + } + set fieldAddons(value: FieldAddons) { + this._setOption('fieldAddons', value); + } + + /** * [descr:dxDropDownBoxOptions.fieldTemplate] + * @deprecated [depNote:dxDropDownBoxOptions.fieldTemplate] + */ @Input() get fieldTemplate(): any { @@ -930,6 +946,13 @@ export class DxDropDownBoxComponent extends DxComponent implements OnDestroy, Co */ @Output() elementAttrChange: EventEmitter>; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() fieldAddonsChange: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -1241,6 +1264,7 @@ export class DxDropDownBoxComponent extends DxComponent implements OnDestroy, Co { emit: 'dropDownButtonTemplateChange' }, { emit: 'dropDownOptionsChange' }, { emit: 'elementAttrChange' }, + { emit: 'fieldAddonsChange' }, { emit: 'fieldTemplateChange' }, { emit: 'focusStateEnabledChange' }, { emit: 'heightChange' }, @@ -1369,6 +1393,7 @@ export class DxDropDownBoxComponent extends DxComponent implements OnDestroy, Co DxiDropDownBoxButtonModule, DxoDropDownBoxCollisionModule, DxoDropDownBoxDropDownOptionsModule, + DxoDropDownBoxFieldAddonsModule, DxoDropDownBoxFromModule, DxoDropDownBoxHideModule, DxoDropDownBoxMyModule, @@ -1404,6 +1429,7 @@ export class DxDropDownBoxComponent extends DxComponent implements OnDestroy, Co DxiDropDownBoxButtonModule, DxoDropDownBoxCollisionModule, DxoDropDownBoxDropDownOptionsModule, + DxoDropDownBoxFieldAddonsModule, DxoDropDownBoxFromModule, DxoDropDownBoxHideModule, DxoDropDownBoxMyModule, diff --git a/packages/devextreme-angular/src/ui/drop-down-box/nested/field-addons.ts b/packages/devextreme-angular/src/ui/drop-down-box/nested/field-addons.ts new file mode 100644 index 000000000000..99de2830e90f --- /dev/null +++ b/packages/devextreme-angular/src/ui/drop-down-box/nested/field-addons.ts @@ -0,0 +1,83 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-drop-down-box-field-addons', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoDropDownBoxFieldAddonsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get afterTemplate(): any { + return this._getOption('afterTemplate'); + } + set afterTemplate(value: any) { + this._setOption('afterTemplate', value); + } + + @Input() + get beforeTemplate(): any { + return this._getOption('beforeTemplate'); + } + set beforeTemplate(value: any) { + this._setOption('beforeTemplate', value); + } + + + protected get _optionPath() { + return 'fieldAddons'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoDropDownBoxFieldAddonsComponent + ], + exports: [ + DxoDropDownBoxFieldAddonsComponent + ], +}) +export class DxoDropDownBoxFieldAddonsModule { } diff --git a/packages/devextreme-angular/src/ui/drop-down-box/nested/index.ts b/packages/devextreme-angular/src/ui/drop-down-box/nested/index.ts index a5926253bf21..d0d98e2b17e0 100644 --- a/packages/devextreme-angular/src/ui/drop-down-box/nested/index.ts +++ b/packages/devextreme-angular/src/ui/drop-down-box/nested/index.ts @@ -4,6 +4,7 @@ export * from './boundary-offset'; export * from './button-dxi'; export * from './collision'; export * from './drop-down-options'; +export * from './field-addons'; export * from './from'; export * from './hide'; export * from './my'; diff --git a/packages/devextreme-angular/src/ui/form/index.ts b/packages/devextreme-angular/src/ui/form/index.ts index f1a54b578044..76d9533e086d 100644 --- a/packages/devextreme-angular/src/ui/form/index.ts +++ b/packages/devextreme-angular/src/ui/form/index.ts @@ -22,8 +22,9 @@ import { } from '@angular/core'; +import { AIIntegration } from 'devextreme/common/ai-integration'; import { Mode } from 'devextreme/common'; -import { dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent } from 'devextreme/ui/form'; +import { dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent, SmartPastedEvent, SmartPastingEvent } from 'devextreme/ui/form'; import DxForm from 'devextreme/ui/form'; @@ -46,6 +47,7 @@ import { DxoTabPanelOptionsModule } from 'devextreme-angular/ui/nested'; import { DxiTabModule } from 'devextreme-angular/ui/nested'; import { DxoButtonOptionsModule } from 'devextreme-angular/ui/nested'; +import { DxoFormAiOptionsModule } from 'devextreme-angular/ui/form/nested'; import { DxiFormAsyncRuleModule } from 'devextreme-angular/ui/form/nested'; import { DxiFormButtonItemModule } from 'devextreme-angular/ui/form/nested'; import { DxoFormButtonOptionsModule } from 'devextreme-angular/ui/form/nested'; @@ -125,6 +127,16 @@ export class DxFormComponent extends DxComponent implements OnDestroy, OnChanges } + + @Input() + get aiIntegration(): AIIntegration | undefined { + return this._getOption('aiIntegration'); + } + set aiIntegration(value: AIIntegration | undefined) { + this._setOption('aiIntegration', value); + } + + /** * [descr:dxFormOptions.alignItemLabels] @@ -588,6 +600,22 @@ export class DxFormComponent extends DxComponent implements OnDestroy, OnChanges */ @Output() onOptionChanged: EventEmitter; + /** + + * [descr:undefined] + + + */ + @Output() onSmartPasted: EventEmitter; + + /** + + * [descr:undefined] + + + */ + @Output() onSmartPasting: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -602,6 +630,13 @@ export class DxFormComponent extends DxComponent implements OnDestroy, OnChanges */ @Output() activeStateEnabledChange: EventEmitter; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() aiIntegrationChange: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -905,8 +940,11 @@ export class DxFormComponent extends DxComponent implements OnDestroy, OnChanges { subscribe: 'fieldDataChanged', emit: 'onFieldDataChanged' }, { subscribe: 'initialized', emit: 'onInitialized' }, { subscribe: 'optionChanged', emit: 'onOptionChanged' }, + { subscribe: 'smartPasted', emit: 'onSmartPasted' }, + { subscribe: 'smartPasting', emit: 'onSmartPasting' }, { emit: 'accessKeyChange' }, { emit: 'activeStateEnabledChange' }, + { emit: 'aiIntegrationChange' }, { emit: 'alignItemLabelsChange' }, { emit: 'alignItemLabelsInAllGroupsChange' }, { emit: 'colCountChange' }, @@ -993,6 +1031,7 @@ export class DxFormComponent extends DxComponent implements OnDestroy, OnChanges DxoTabPanelOptionsModule, DxiTabModule, DxoButtonOptionsModule, + DxoFormAiOptionsModule, DxiFormAsyncRuleModule, DxiFormButtonItemModule, DxoFormButtonOptionsModule, @@ -1027,6 +1066,7 @@ export class DxFormComponent extends DxComponent implements OnDestroy, OnChanges DxoTabPanelOptionsModule, DxiTabModule, DxoButtonOptionsModule, + DxoFormAiOptionsModule, DxiFormAsyncRuleModule, DxiFormButtonItemModule, DxoFormButtonOptionsModule, diff --git a/packages/devextreme-angular/src/ui/form/nested/ai-options.ts b/packages/devextreme-angular/src/ui/form/nested/ai-options.ts new file mode 100644 index 000000000000..aa01899c79f2 --- /dev/null +++ b/packages/devextreme-angular/src/ui/form/nested/ai-options.ts @@ -0,0 +1,83 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-form-ai-options', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoFormAiOptionsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get instruction(): string | undefined { + return this._getOption('instruction'); + } + set instruction(value: string | undefined) { + this._setOption('instruction', value); + } + + + protected get _optionPath() { + return 'aiOptions'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoFormAiOptionsComponent + ], + exports: [ + DxoFormAiOptionsComponent + ], +}) +export class DxoFormAiOptionsModule { } diff --git a/packages/devextreme-angular/src/ui/form/nested/button-item-dxi.ts b/packages/devextreme-angular/src/ui/form/nested/button-item-dxi.ts index 72c2f7373f8e..6c13567f399a 100644 --- a/packages/devextreme-angular/src/ui/form/nested/button-item-dxi.ts +++ b/packages/devextreme-angular/src/ui/form/nested/button-item-dxi.ts @@ -14,7 +14,7 @@ import { import { dxButtonOptions } from 'devextreme/ui/button'; import { HorizontalAlignment, VerticalAlignment } from 'devextreme/common'; -import { FormItemType } from 'devextreme/ui/form'; +import { FormItemType, FormPredefinedButtonItem } from 'devextreme/ui/form'; import { DxIntegrationModule, @@ -73,10 +73,10 @@ export class DxiFormButtonItemComponent extends CollectionNestedOption { } @Input() - get name(): string | undefined { + get name(): FormPredefinedButtonItem | string | undefined { return this._getOption('name'); } - set name(value: string | undefined) { + set name(value: FormPredefinedButtonItem | string | undefined) { this._setOption('name', value); } diff --git a/packages/devextreme-angular/src/ui/form/nested/index.ts b/packages/devextreme-angular/src/ui/form/nested/index.ts index 91c7a47348b4..922b0d41f16c 100644 --- a/packages/devextreme-angular/src/ui/form/nested/index.ts +++ b/packages/devextreme-angular/src/ui/form/nested/index.ts @@ -1,3 +1,4 @@ +export * from './ai-options'; export * from './async-rule-dxi'; export * from './button-item-dxi'; export * from './button-options'; diff --git a/packages/devextreme-angular/src/ui/form/nested/item-dxi.ts b/packages/devextreme-angular/src/ui/form/nested/item-dxi.ts index c027f283a6fa..56bd53c4d802 100644 --- a/packages/devextreme-angular/src/ui/form/nested/item-dxi.ts +++ b/packages/devextreme-angular/src/ui/form/nested/item-dxi.ts @@ -21,7 +21,7 @@ import { DOCUMENT } from '@angular/common'; import * as CommonTypes from 'devextreme/common'; -import { FormItemComponent, FormItemType, LabelLocation, dxFormButtonItem, dxFormEmptyItem, dxFormGroupItem, dxFormSimpleItem, dxFormTabbedItem } from 'devextreme/ui/form'; +import { FormItemComponent, FormItemType, LabelLocation, FormPredefinedButtonItem, dxFormButtonItem, dxFormEmptyItem, dxFormGroupItem, dxFormSimpleItem, dxFormTabbedItem } from 'devextreme/ui/form'; import { HorizontalAlignment, VerticalAlignment } from 'devextreme/common'; import { dxTabPanelOptions } from 'devextreme/ui/tab_panel'; import { dxButtonOptions } from 'devextreme/ui/button'; @@ -130,6 +130,14 @@ export class DxiFormItemComponent extends CollectionNestedOption implements Afte this._setOption('visible', value); } + @Input() + get aiOptions(): { disabled?: boolean, instruction?: string | undefined } { + return this._getOption('aiOptions'); + } + set aiOptions(value: { disabled?: boolean, instruction?: string | undefined }) { + this._setOption('aiOptions', value); + } + @Input() get colSpan(): number | undefined { return this._getOption('colSpan'); @@ -203,10 +211,10 @@ export class DxiFormItemComponent extends CollectionNestedOption implements Afte } @Input() - get name(): string | undefined { + get name(): string | undefined | FormPredefinedButtonItem { return this._getOption('name'); } - set name(value: string | undefined) { + set name(value: string | undefined | FormPredefinedButtonItem) { this._setOption('name', value); } diff --git a/packages/devextreme-angular/src/ui/form/nested/simple-item-dxi.ts b/packages/devextreme-angular/src/ui/form/nested/simple-item-dxi.ts index 59d5f6708d8e..042107447a75 100644 --- a/packages/devextreme-angular/src/ui/form/nested/simple-item-dxi.ts +++ b/packages/devextreme-angular/src/ui/form/nested/simple-item-dxi.ts @@ -55,6 +55,14 @@ import { DxiFormValidationRuleComponent } from './validation-rule-dxi'; }) export class DxiFormSimpleItemComponent extends CollectionNestedOption implements AfterViewInit, IDxTemplateHost, AfterContentInit { + @Input() + get aiOptions(): { disabled?: boolean, instruction?: string | undefined } { + return this._getOption('aiOptions'); + } + set aiOptions(value: { disabled?: boolean, instruction?: string | undefined }) { + this._setOption('aiOptions', value); + } + @Input() get colSpan(): number | undefined { return this._getOption('colSpan'); diff --git a/packages/devextreme-angular/src/ui/select-box/index.ts b/packages/devextreme-angular/src/ui/select-box/index.ts index 0597a971b979..e32f1f3e82a0 100644 --- a/packages/devextreme-angular/src/ui/select-box/index.ts +++ b/packages/devextreme-angular/src/ui/select-box/index.ts @@ -25,7 +25,7 @@ import { import DataSource from 'devextreme/data/data_source'; -import { DropDownPredefinedButton } from 'devextreme/ui/drop_down_editor/ui.drop_down_editor'; +import { DropDownPredefinedButton, FieldAddons } from 'devextreme/ui/drop_down_editor/ui.drop_down_editor'; import { TextEditorButton, LabelMode, SimplifiedSearchMode, EditorStyle, ValidationMessageMode, Mode, Position, ValidationStatus } from 'devextreme/common'; import { CollectionWidgetItem } from 'devextreme/ui/collection/ui.collection_widget.base'; import { DataSourceOptions } from 'devextreme/data/data_source'; @@ -72,6 +72,7 @@ import { DxoSelectBoxBoundaryOffsetModule } from 'devextreme-angular/ui/select-b import { DxiSelectBoxButtonModule } from 'devextreme-angular/ui/select-box/nested'; import { DxoSelectBoxCollisionModule } from 'devextreme-angular/ui/select-box/nested'; import { DxoSelectBoxDropDownOptionsModule } from 'devextreme-angular/ui/select-box/nested'; +import { DxoSelectBoxFieldAddonsModule } from 'devextreme-angular/ui/select-box/nested'; import { DxoSelectBoxFromModule } from 'devextreme-angular/ui/select-box/nested'; import { DxoSelectBoxHideModule } from 'devextreme-angular/ui/select-box/nested'; import { DxiSelectBoxItemModule } from 'devextreme-angular/ui/select-box/nested'; @@ -286,9 +287,24 @@ export class DxSelectBoxComponent extends DxComponent implements OnDestroy, Cont } + /** + * [descr:dxSelectBoxOptions.fieldAddons] + + */ + @Input() + get fieldAddons(): FieldAddons { + return this._getOption('fieldAddons'); + } + set fieldAddons(value: FieldAddons) { + this._setOption('fieldAddons', value); + } + + /** * [descr:dxSelectBoxOptions.fieldTemplate] + * @deprecated [depNote:dxSelectBoxOptions.fieldTemplate] + */ @Input() get fieldTemplate(): any { @@ -1160,6 +1176,13 @@ export class DxSelectBoxComponent extends DxComponent implements OnDestroy, Cont */ @Output() elementAttrChange: EventEmitter>; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() fieldAddonsChange: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -1588,6 +1611,7 @@ export class DxSelectBoxComponent extends DxComponent implements OnDestroy, Cont { emit: 'dropDownButtonTemplateChange' }, { emit: 'dropDownOptionsChange' }, { emit: 'elementAttrChange' }, + { emit: 'fieldAddonsChange' }, { emit: 'fieldTemplateChange' }, { emit: 'focusStateEnabledChange' }, { emit: 'groupedChange' }, @@ -1733,6 +1757,7 @@ export class DxSelectBoxComponent extends DxComponent implements OnDestroy, Cont DxiSelectBoxButtonModule, DxoSelectBoxCollisionModule, DxoSelectBoxDropDownOptionsModule, + DxoSelectBoxFieldAddonsModule, DxoSelectBoxFromModule, DxoSelectBoxHideModule, DxiSelectBoxItemModule, @@ -1769,6 +1794,7 @@ export class DxSelectBoxComponent extends DxComponent implements OnDestroy, Cont DxiSelectBoxButtonModule, DxoSelectBoxCollisionModule, DxoSelectBoxDropDownOptionsModule, + DxoSelectBoxFieldAddonsModule, DxoSelectBoxFromModule, DxoSelectBoxHideModule, DxiSelectBoxItemModule, diff --git a/packages/devextreme-angular/src/ui/select-box/nested/field-addons.ts b/packages/devextreme-angular/src/ui/select-box/nested/field-addons.ts new file mode 100644 index 000000000000..ad9677aa6ea4 --- /dev/null +++ b/packages/devextreme-angular/src/ui/select-box/nested/field-addons.ts @@ -0,0 +1,83 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-select-box-field-addons', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoSelectBoxFieldAddonsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get afterTemplate(): any { + return this._getOption('afterTemplate'); + } + set afterTemplate(value: any) { + this._setOption('afterTemplate', value); + } + + @Input() + get beforeTemplate(): any { + return this._getOption('beforeTemplate'); + } + set beforeTemplate(value: any) { + this._setOption('beforeTemplate', value); + } + + + protected get _optionPath() { + return 'fieldAddons'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoSelectBoxFieldAddonsComponent + ], + exports: [ + DxoSelectBoxFieldAddonsComponent + ], +}) +export class DxoSelectBoxFieldAddonsModule { } diff --git a/packages/devextreme-angular/src/ui/select-box/nested/index.ts b/packages/devextreme-angular/src/ui/select-box/nested/index.ts index 5d8a4c3bbef5..3871a35503c4 100644 --- a/packages/devextreme-angular/src/ui/select-box/nested/index.ts +++ b/packages/devextreme-angular/src/ui/select-box/nested/index.ts @@ -4,6 +4,7 @@ export * from './boundary-offset'; export * from './button-dxi'; export * from './collision'; export * from './drop-down-options'; +export * from './field-addons'; export * from './from'; export * from './hide'; export * from './item-dxi'; diff --git a/packages/devextreme-angular/src/ui/tag-box/index.ts b/packages/devextreme-angular/src/ui/tag-box/index.ts index a12f0c5d29e0..f69548ce6348 100644 --- a/packages/devextreme-angular/src/ui/tag-box/index.ts +++ b/packages/devextreme-angular/src/ui/tag-box/index.ts @@ -26,7 +26,7 @@ import { import DataSource from 'devextreme/data/data_source'; import { ApplyValueMode, TextEditorButton, LabelMode, SimplifiedSearchMode, SelectAllMode, EditorStyle, ValidationMessageMode, Mode, Position, ValidationStatus } from 'devextreme/common'; -import { DropDownPredefinedButton } from 'devextreme/ui/drop_down_editor/ui.drop_down_editor'; +import { DropDownPredefinedButton, FieldAddons } from 'devextreme/ui/drop_down_editor/ui.drop_down_editor'; import { CollectionWidgetItem } from 'devextreme/ui/collection/ui.collection_widget.base'; import { DataSourceOptions } from 'devextreme/data/data_source'; import { Store } from 'devextreme/data/store'; @@ -72,6 +72,7 @@ import { DxoTagBoxBoundaryOffsetModule } from 'devextreme-angular/ui/tag-box/nes import { DxiTagBoxButtonModule } from 'devextreme-angular/ui/tag-box/nested'; import { DxoTagBoxCollisionModule } from 'devextreme-angular/ui/tag-box/nested'; import { DxoTagBoxDropDownOptionsModule } from 'devextreme-angular/ui/tag-box/nested'; +import { DxoTagBoxFieldAddonsModule } from 'devextreme-angular/ui/tag-box/nested'; import { DxoTagBoxFromModule } from 'devextreme-angular/ui/tag-box/nested'; import { DxoTagBoxHideModule } from 'devextreme-angular/ui/tag-box/nested'; import { DxiTagBoxItemModule } from 'devextreme-angular/ui/tag-box/nested'; @@ -286,9 +287,24 @@ export class DxTagBoxComponent extends DxComponent implements OnDestroy, Control } + /** + * [descr:dxSelectBoxOptions.fieldAddons] + + */ + @Input() + get fieldAddons(): FieldAddons { + return this._getOption('fieldAddons'); + } + set fieldAddons(value: FieldAddons) { + this._setOption('fieldAddons', value); + } + + /** * [descr:dxSelectBoxOptions.fieldTemplate] + * @deprecated [depNote:dxSelectBoxOptions.fieldTemplate] + */ @Input() get fieldTemplate(): any { @@ -1243,6 +1259,13 @@ export class DxTagBoxComponent extends DxComponent implements OnDestroy, Control */ @Output() elementAttrChange: EventEmitter>; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() fieldAddonsChange: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -1719,6 +1742,7 @@ export class DxTagBoxComponent extends DxComponent implements OnDestroy, Control { emit: 'dropDownButtonTemplateChange' }, { emit: 'dropDownOptionsChange' }, { emit: 'elementAttrChange' }, + { emit: 'fieldAddonsChange' }, { emit: 'fieldTemplateChange' }, { emit: 'focusStateEnabledChange' }, { emit: 'groupedChange' }, @@ -1875,6 +1899,7 @@ export class DxTagBoxComponent extends DxComponent implements OnDestroy, Control DxiTagBoxButtonModule, DxoTagBoxCollisionModule, DxoTagBoxDropDownOptionsModule, + DxoTagBoxFieldAddonsModule, DxoTagBoxFromModule, DxoTagBoxHideModule, DxiTagBoxItemModule, @@ -1911,6 +1936,7 @@ export class DxTagBoxComponent extends DxComponent implements OnDestroy, Control DxiTagBoxButtonModule, DxoTagBoxCollisionModule, DxoTagBoxDropDownOptionsModule, + DxoTagBoxFieldAddonsModule, DxoTagBoxFromModule, DxoTagBoxHideModule, DxiTagBoxItemModule, diff --git a/packages/devextreme-angular/src/ui/tag-box/nested/field-addons.ts b/packages/devextreme-angular/src/ui/tag-box/nested/field-addons.ts new file mode 100644 index 000000000000..79f86d2c70bc --- /dev/null +++ b/packages/devextreme-angular/src/ui/tag-box/nested/field-addons.ts @@ -0,0 +1,83 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-tag-box-field-addons', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoTagBoxFieldAddonsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get afterTemplate(): any { + return this._getOption('afterTemplate'); + } + set afterTemplate(value: any) { + this._setOption('afterTemplate', value); + } + + @Input() + get beforeTemplate(): any { + return this._getOption('beforeTemplate'); + } + set beforeTemplate(value: any) { + this._setOption('beforeTemplate', value); + } + + + protected get _optionPath() { + return 'fieldAddons'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoTagBoxFieldAddonsComponent + ], + exports: [ + DxoTagBoxFieldAddonsComponent + ], +}) +export class DxoTagBoxFieldAddonsModule { } diff --git a/packages/devextreme-angular/src/ui/tag-box/nested/index.ts b/packages/devextreme-angular/src/ui/tag-box/nested/index.ts index 5d8a4c3bbef5..3871a35503c4 100644 --- a/packages/devextreme-angular/src/ui/tag-box/nested/index.ts +++ b/packages/devextreme-angular/src/ui/tag-box/nested/index.ts @@ -4,6 +4,7 @@ export * from './boundary-offset'; export * from './button-dxi'; export * from './collision'; export * from './drop-down-options'; +export * from './field-addons'; export * from './from'; export * from './hide'; export * from './item-dxi'; diff --git a/packages/devextreme-angular/src/ui/tree-list/index.ts b/packages/devextreme-angular/src/ui/tree-list/index.ts index b435f24ed98d..c197f18e9d43 100644 --- a/packages/devextreme-angular/src/ui/tree-list/index.ts +++ b/packages/devextreme-angular/src/ui/tree-list/index.ts @@ -106,6 +106,7 @@ import { DxoSortingModule } from 'devextreme-angular/ui/nested'; import { DxoStateStoringModule } from 'devextreme-angular/ui/nested'; import { DxoToolbarModule } from 'devextreme-angular/ui/nested'; +import { DxoTreeListAiOptionsModule } from 'devextreme-angular/ui/tree-list/nested'; import { DxoTreeListAnimationModule } from 'devextreme-angular/ui/tree-list/nested'; import { DxiTreeListAsyncRuleModule } from 'devextreme-angular/ui/tree-list/nested'; import { DxoTreeListAtModule } from 'devextreme-angular/ui/tree-list/nested'; @@ -2293,6 +2294,7 @@ export class DxTreeListComponent extends DxComponent DxoSortingModule, DxoStateStoringModule, DxoToolbarModule, + DxoTreeListAiOptionsModule, DxoTreeListAnimationModule, DxiTreeListAsyncRuleModule, DxoTreeListAtModule, @@ -2428,6 +2430,7 @@ export class DxTreeListComponent extends DxComponent DxoSortingModule, DxoStateStoringModule, DxoToolbarModule, + DxoTreeListAiOptionsModule, DxoTreeListAnimationModule, DxiTreeListAsyncRuleModule, DxoTreeListAtModule, diff --git a/packages/devextreme-angular/src/ui/tree-list/nested/ai-options.ts b/packages/devextreme-angular/src/ui/tree-list/nested/ai-options.ts new file mode 100644 index 000000000000..1be839d28f37 --- /dev/null +++ b/packages/devextreme-angular/src/ui/tree-list/nested/ai-options.ts @@ -0,0 +1,83 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-tree-list-ai-options', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoTreeListAiOptionsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get instruction(): string | undefined { + return this._getOption('instruction'); + } + set instruction(value: string | undefined) { + this._setOption('instruction', value); + } + + + protected get _optionPath() { + return 'aiOptions'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoTreeListAiOptionsComponent + ], + exports: [ + DxoTreeListAiOptionsComponent + ], +}) +export class DxoTreeListAiOptionsModule { } diff --git a/packages/devextreme-angular/src/ui/tree-list/nested/form-item.ts b/packages/devextreme-angular/src/ui/tree-list/nested/form-item.ts index cfce15af9272..37942237257e 100644 --- a/packages/devextreme-angular/src/ui/tree-list/nested/form-item.ts +++ b/packages/devextreme-angular/src/ui/tree-list/nested/form-item.ts @@ -57,6 +57,14 @@ import { DxiTreeListValidationRuleComponent } from './validation-rule-dxi'; }) export class DxoTreeListFormItemComponent extends NestedOption implements AfterViewInit, OnDestroy, OnInit, IDxTemplateHost, AfterContentInit { + @Input() + get aiOptions(): { disabled?: boolean, instruction?: string | undefined } { + return this._getOption('aiOptions'); + } + set aiOptions(value: { disabled?: boolean, instruction?: string | undefined }) { + this._setOption('aiOptions', value); + } + @Input() get colSpan(): number | undefined { return this._getOption('colSpan'); diff --git a/packages/devextreme-angular/src/ui/tree-list/nested/form.ts b/packages/devextreme-angular/src/ui/tree-list/nested/form.ts index 5579e6792d3c..b139d3fe7f2f 100644 --- a/packages/devextreme-angular/src/ui/tree-list/nested/form.ts +++ b/packages/devextreme-angular/src/ui/tree-list/nested/form.ts @@ -16,8 +16,9 @@ import { +import { AIIntegration } from 'devextreme/common/ai-integration'; import { Mode } from 'devextreme/common'; -import { dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent } from 'devextreme/ui/form'; +import { dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent, SmartPastedEvent, SmartPastingEvent } from 'devextreme/ui/form'; import { DxIntegrationModule, @@ -51,6 +52,14 @@ export class DxoTreeListFormComponent extends NestedOption implements OnDestroy, this._setOption('activeStateEnabled', value); } + @Input() + get aiIntegration(): AIIntegration | undefined { + return this._getOption('aiIntegration'); + } + set aiIntegration(value: AIIntegration | undefined) { + this._setOption('aiIntegration', value); + } + @Input() get alignItemLabels(): boolean { return this._getOption('alignItemLabels'); @@ -235,6 +244,22 @@ export class DxoTreeListFormComponent extends NestedOption implements OnDestroy, this._setOption('onOptionChanged', value); } + @Input() + get onSmartPasted(): ((e: SmartPastedEvent) => void) { + return this._getOption('onSmartPasted'); + } + set onSmartPasted(value: ((e: SmartPastedEvent) => void)) { + this._setOption('onSmartPasted', value); + } + + @Input() + get onSmartPasting(): ((e: SmartPastingEvent) => void) { + return this._getOption('onSmartPasting'); + } + set onSmartPasting(value: ((e: SmartPastingEvent) => void)) { + this._setOption('onSmartPasting', value); + } + @Input() get optionalMark(): string { return this._getOption('optionalMark'); diff --git a/packages/devextreme-angular/src/ui/tree-list/nested/index.ts b/packages/devextreme-angular/src/ui/tree-list/nested/index.ts index c6116a21500d..41c8b18ab90b 100644 --- a/packages/devextreme-angular/src/ui/tree-list/nested/index.ts +++ b/packages/devextreme-angular/src/ui/tree-list/nested/index.ts @@ -1,3 +1,4 @@ +export * from './ai-options'; export * from './animation'; export * from './async-rule-dxi'; export * from './at'; diff --git a/packages/devextreme-metadata/common/index.ts b/packages/devextreme-metadata/common/index.ts index 5f9530df137f..e41c76260a75 100644 --- a/packages/devextreme-metadata/common/index.ts +++ b/packages/devextreme-metadata/common/index.ts @@ -1,8 +1,7 @@ import { AddMutation, - ArrayDataType, DataType, - MemberRefDataType, + DataTypes, Mutation, RemoveMutation, } from 'devextreme-internal-tools/metadata'; @@ -37,13 +36,13 @@ export function replaceTypes({ uid, types }: { uid: string; types: DataType[] }) } export const types = { - array(...itemTypes: ArrayDataType['itemTypes']): ArrayDataType { + array(...itemTypes: DataTypes.Array['itemTypes']): DataTypes.Array { return { kind: 'array', itemTypes }; }, - memberRef(uid: string): MemberRefDataType { - return { kind: 'memberRef', uid }; + uidRef(uid: string): DataTypes.UidRef { + return { kind: 'uidRef', uid }; }, - object: { kind: 'object' } as any, + object: { kind: 'object' } as DataTypes.Object, }; diff --git a/packages/devextreme-metadata/make-angular-metadata.ts b/packages/devextreme-metadata/make-angular-metadata.ts index 623b02401227..8cbc2ea6cb2e 100644 --- a/packages/devextreme-metadata/make-angular-metadata.ts +++ b/packages/devextreme-metadata/make-angular-metadata.ts @@ -12,6 +12,10 @@ Ng.makeMetadata({ mutations: [ removeMembers(/\/calendar:dxCalendarOptions.todayButtonText/), removeMembers(/\/card_view:/), + removeMembers(/\/form:dxFormOptions\.(aiIntegration|onSmartPasting|onSmartPasted|smartPaste)/), + removeMembers(/\/form:dxFormSimpleItem\.aiOptions/), + removeMembers(/\/form:FormPredefinedButtonItem/), + removeMembers(/\/drop_down_editor\/ui.drop_down_editor:FieldAddons/), removeMembers(/\/scheduler:Toolbar/), removeMembers(/\/stepper:/), ], diff --git a/packages/devextreme-metadata/make-aspnet-metadata.ts b/packages/devextreme-metadata/make-aspnet-metadata.ts index f6383d75b095..0c7c9ed00436 100644 --- a/packages/devextreme-metadata/make-aspnet-metadata.ts +++ b/packages/devextreme-metadata/make-aspnet-metadata.ts @@ -1,7 +1,7 @@ import { AspNet, Mutation } from 'devextreme-internal-tools/metadata'; import { addMember, cleanArtifacts, removeMembers, types } from './common'; import { commonSmdCollectionItems } from './common/smd'; -import { enums, enumAliases, enumItemRenamings } from './aspnet/enums' +import { enums, enumAliases, enumItemRenamings } from './aspnet/enums'; import { PATHS } from './common/paths'; cleanArtifacts('StrongMetaData.json', 'StrongMetaDataGenerator.cfg.json'); @@ -43,7 +43,7 @@ AspNet.makeMetadata({ }), addMember({ uid: 'ui/popover:dxPopoverOptions.toolbarItems', - types: [types.array(types.memberRef('ui/popover:ToolbarItem'))], + types: [types.array(types.uidRef('ui/popover:ToolbarItem'))], }), removeMembers(/ui\/scheduler:ToolbarItem\.options/), ], @@ -61,7 +61,7 @@ AspNet.makeMetadata({ collectionItems: [...commonSmdCollectionItems], enums, enumAliases, - enumItemRenamings + enumItemRenamings, }, }); diff --git a/packages/devextreme-react/package.json b/packages/devextreme-react/package.json index 6f0e57ca97b2..614fc9744498 100644 --- a/packages/devextreme-react/package.json +++ b/packages/devextreme-react/package.json @@ -7,6 +7,7 @@ "type": "git", "url": "https://github.com/DevExpress/devextreme-react.git" }, + "sideEffects": false, "main": "./cjs/index.js", "module": "./esm/index.js", "types": "./cjs/index.d.ts", diff --git a/packages/devextreme-react/src/autocomplete.ts b/packages/devextreme-react/src/autocomplete.ts index faa06f6e00b2..8111a067985a 100644 --- a/packages/devextreme-react/src/autocomplete.ts +++ b/packages/devextreme-react/src/autocomplete.ts @@ -238,7 +238,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -461,7 +460,6 @@ const Offset = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; diff --git a/packages/devextreme-react/src/card-view.ts b/packages/devextreme-react/src/card-view.ts index 4550b420cb7a..7257ff12f0e3 100644 --- a/packages/devextreme-react/src/card-view.ts +++ b/packages/devextreme-react/src/card-view.ts @@ -13,7 +13,7 @@ import type { CardClickEvent, CardDblClickEvent, CardInsertedEvent, CardInsertin import type { AnimationConfig, CollisionResolution, PositionConfig, AnimationState, AnimationType, CollisionResolutionCombination } from "devextreme/common/core/animation"; import type { ValidationRuleType, HorizontalAlignment, VerticalAlignment, ButtonStyle, template, ButtonType, ToolbarItemLocation, ToolbarItemComponent, SearchMode, SingleMultipleOrNone, SelectAllMode, DataType, Format as CommonFormat, SortOrder, ComparisonOperator, DragHighlight, Mode, Direction, PositionAlignment, DisplayMode, ScrollbarMode, TabsIconPosition, TabsStyle, Position as CommonPosition } from "devextreme/common"; import type { dxButtonOptions, ClickEvent, ContentReadyEvent, DisposingEvent, InitializedEvent, OptionChangedEvent } from "devextreme/ui/button"; -import type { FormItemType, ContentReadyEvent as FormContentReadyEvent, DisposingEvent as FormDisposingEvent, InitializedEvent as FormInitializedEvent, OptionChangedEvent as FormOptionChangedEvent, dxFormSimpleItem, dxFormOptions, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, EditorEnterKeyEvent, FieldDataChangedEvent, FormItemComponent } from "devextreme/ui/form"; +import type { FormItemType, FormPredefinedButtonItem, ContentReadyEvent as FormContentReadyEvent, DisposingEvent as FormDisposingEvent, InitializedEvent as FormInitializedEvent, OptionChangedEvent as FormOptionChangedEvent, dxFormSimpleItem, dxFormOptions, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, EditorEnterKeyEvent, FieldDataChangedEvent, SmartPastedEvent, SmartPastingEvent, FormItemComponent } from "devextreme/ui/form"; import type { ContentReadyEvent as FilterBuilderContentReadyEvent, DisposingEvent as FilterBuilderDisposingEvent, InitializedEvent as FilterBuilderInitializedEvent, OptionChangedEvent as FilterBuilderOptionChangedEvent, dxFilterBuilderField, FieldInfo, FilterBuilderOperation, dxFilterBuilderCustomOperation, GroupOperation, EditorPreparedEvent, EditorPreparingEvent, ValueChangedEvent } from "devextreme/ui/filter_builder"; import type { ContentReadyEvent as LoadPanelContentReadyEvent, DisposingEvent as LoadPanelDisposingEvent, InitializedEvent as LoadPanelInitializedEvent, OptionChangedEvent as LoadPanelOptionChangedEvent, HiddenEvent, HidingEvent, ShowingEvent, ShownEvent } from "devextreme/ui/load_panel"; import type { ContentReadyEvent as TabPanelContentReadyEvent, DisposingEvent as TabPanelDisposingEvent, InitializedEvent as TabPanelInitializedEvent, OptionChangedEvent as TabPanelOptionChangedEvent, dxTabPanelOptions, dxTabPanelItem, ItemClickEvent, ItemContextMenuEvent, ItemHoldEvent, ItemRenderedEvent, SelectionChangedEvent, SelectionChangingEvent, TitleClickEvent, TitleHoldEvent, TitleRenderedEvent } from "devextreme/ui/tab_panel"; @@ -23,6 +23,7 @@ import type { HeaderFilterSearchConfig, HeaderFilterTexts, SelectionColumnDispla import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; import type { DataSourceOptions } from "devextreme/data/data_source"; import type { Store } from "devextreme/data/store"; +import type { AIIntegration } from "devextreme/common/ai-integration"; import type { event } from "devextreme/events/events.types"; import type dxForm from "devextreme/ui/form"; @@ -163,6 +164,26 @@ const CardView = memo( ) as (props: React.PropsWithChildren> & { ref?: Ref> }) => ReactElement | null; +// owners: +// FormItem +// SimpleItem +type IAiOptionsProps = React.PropsWithChildren<{ + disabled?: boolean; + instruction?: string | undefined; +}> +const _componentAiOptions = (props: IAiOptionsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "aiOptions", + }, + }); +}; + +const AiOptions = Object.assign(_componentAiOptions, { + componentType: "option", +}); + // owners: // LoadPanel type IAnimationProps = React.PropsWithChildren<{ @@ -260,7 +281,7 @@ type IButtonItemProps = React.PropsWithChildren<{ cssClass?: string | undefined; horizontalAlignment?: HorizontalAlignment; itemType?: FormItemType; - name?: string | undefined; + name?: FormPredefinedButtonItem | string | undefined; verticalAlignment?: VerticalAlignment; visible?: boolean; visibleIndex?: number | undefined; @@ -290,7 +311,6 @@ const ButtonItem = Object.assign; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; @@ -1118,7 +1138,6 @@ type IFilterBuilderProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; allowHierarchicalFields?: boolean; - bindingOptions?: Record; customOperations?: Array; disabled?: boolean; elementAttr?: Record; @@ -1272,9 +1291,9 @@ const FilterPanelTexts = Object.assign; colCount?: Mode | number; colCountByScreen?: Record | { lg?: number | undefined; @@ -1301,6 +1320,8 @@ type IFormProps = React.PropsWithChildren<{ onFieldDataChanged?: ((e: FieldDataChangedEvent) => void); onInitialized?: ((e: FormInitializedEvent) => void); onOptionChanged?: ((e: FormOptionChangedEvent) => void); + onSmartPasted?: ((e: SmartPastedEvent) => void); + onSmartPasting?: ((e: SmartPastingEvent) => void); optionalMark?: string; readOnly?: boolean; requiredMark?: string; @@ -1371,6 +1392,10 @@ const Format = Object.assign(_comp // owners: // Column type IFormItemProps = React.PropsWithChildren<{ + aiOptions?: Record | { + disabled?: boolean; + instruction?: string | undefined; + }; colSpan?: number | undefined; cssClass?: string | undefined; dataField?: string | undefined; @@ -1401,6 +1426,7 @@ const _componentFormItem = (props: IFormItemProps) => { elementDescriptor: { OptionName: "formItem", ExpectedChildren: { + aiOptions: { optionName: "aiOptions", isCollectionItem: false }, AsyncRule: { optionName: "validationRules", isCollectionItem: true }, CompareRule: { optionName: "validationRules", isCollectionItem: true }, CustomRule: { optionName: "validationRules", isCollectionItem: true }, @@ -1647,7 +1673,7 @@ type IItemProps = React.PropsWithChildren<{ locateInMenu?: LocateInMenuMode; location?: ToolbarItemLocation; menuItemTemplate?: (() => string | any) | template; - name?: CardHeaderPredefinedItem | string | undefined | PredefinedToolbarItem; + name?: CardHeaderPredefinedItem | string | undefined | FormPredefinedButtonItem | PredefinedToolbarItem; options?: any; showText?: ShowTextMode; template?: ((itemData: CollectionWidgetItem, itemIndex: number, itemElement: any) => string | any) | template; @@ -1658,6 +1684,10 @@ type IItemProps = React.PropsWithChildren<{ icon?: string; tabTemplate?: (() => string | any) | template; title?: string; + aiOptions?: Record | { + disabled?: boolean; + instruction?: string | undefined; + }; colSpan?: number | undefined; dataField?: string | undefined; editorOptions?: any | undefined; @@ -1723,6 +1753,7 @@ const _componentItem = (props: IItemProps) => { OptionName: "items", IsCollectionItem: true, ExpectedChildren: { + aiOptions: { optionName: "aiOptions", isCollectionItem: false }, AsyncRule: { optionName: "validationRules", isCollectionItem: true }, buttonOptions: { optionName: "buttonOptions", isCollectionItem: false }, colCountByScreen: { optionName: "colCountByScreen", isCollectionItem: false }, @@ -1802,7 +1833,6 @@ type ILoadPanelProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; deferRendering?: boolean; delay?: number; @@ -2283,6 +2313,10 @@ const Show = Object.assign(_componen // owners: // Form type ISimpleItemProps = React.PropsWithChildren<{ + aiOptions?: Record | { + disabled?: boolean; + instruction?: string | undefined; + }; colSpan?: number | undefined; cssClass?: string | undefined; dataField?: string | undefined; @@ -2314,6 +2348,7 @@ const _componentSimpleItem = (props: ISimpleItemProps) => { OptionName: "items", IsCollectionItem: true, ExpectedChildren: { + aiOptions: { optionName: "aiOptions", isCollectionItem: false }, AsyncRule: { optionName: "validationRules", isCollectionItem: true }, CompareRule: { optionName: "validationRules", isCollectionItem: true }, CustomRule: { optionName: "validationRules", isCollectionItem: true }, @@ -2497,7 +2532,6 @@ type ITabPanelOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; animationEnabled?: boolean; - bindingOptions?: Record; dataSource?: Array | DataSource | DataSourceOptions | null | Store | string; deferRendering?: boolean; disabled?: boolean; @@ -2787,6 +2821,8 @@ export { CardView, ICardViewOptions, CardViewRef, + AiOptions, + IAiOptionsProps, Animation, IAnimationProps, AsyncRule, diff --git a/packages/devextreme-react/src/chat.ts b/packages/devextreme-react/src/chat.ts index be01f2a3e96d..13aae683df8e 100644 --- a/packages/devextreme-react/src/chat.ts +++ b/packages/devextreme-react/src/chat.ts @@ -31,6 +31,8 @@ type IChatOptionsNarrowedEvents = { } type IChatOptions = React.PropsWithChildren & IHtmlOptions & { + emptyViewRender?: (...params: any) => React.ReactNode; + emptyViewComponent?: React.ComponentType; messageRender?: (...params: any) => React.ReactNode; messageComponent?: React.ComponentType; defaultItems?: Array; @@ -72,6 +74,11 @@ const Chat = memo( }), []); const templateProps = useMemo(() => ([ + { + tmplOption: "emptyViewTemplate", + render: "emptyViewRender", + component: "emptyViewComponent" + }, { tmplOption: "messageTemplate", render: "messageRender", diff --git a/packages/devextreme-react/src/color-box.ts b/packages/devextreme-react/src/color-box.ts index 912cfd710627..010bf01430c9 100644 --- a/packages/devextreme-react/src/color-box.ts +++ b/packages/devextreme-react/src/color-box.ts @@ -83,7 +83,8 @@ const ColorBox = memo( const expectedChildren = useMemo(() => ({ button: { optionName: "buttons", isCollectionItem: true }, - dropDownOptions: { optionName: "dropDownOptions", isCollectionItem: false } + dropDownOptions: { optionName: "dropDownOptions", isCollectionItem: false }, + fieldAddons: { optionName: "fieldAddons", isCollectionItem: false } }), []); const templateProps = useMemo(() => ([ @@ -228,7 +229,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -321,6 +321,38 @@ const DropDownOptions = Object.assign string | any) | template; + beforeTemplate?: ((data: any, element: any) => string | any) | template; + afterRender?: (...params: any) => React.ReactNode; + afterComponent?: React.ComponentType; + beforeRender?: (...params: any) => React.ReactNode; + beforeComponent?: React.ComponentType; +}> +const _componentFieldAddons = (props: IFieldAddonsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "fieldAddons", + TemplateProps: [{ + tmplOption: "afterTemplate", + render: "afterRender", + component: "afterComponent" + }, { + tmplOption: "beforeTemplate", + render: "beforeRender", + component: "beforeComponent" + }], + }, + }); +}; + +const FieldAddons = Object.assign(_componentFieldAddons, { + componentType: "option", +}); + // owners: // Hide // Show @@ -421,7 +453,6 @@ const Offset = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; @@ -631,6 +662,8 @@ export { ICollisionProps, DropDownOptions, IDropDownOptionsProps, + FieldAddons, + IFieldAddonsProps, From, IFromProps, Hide, diff --git a/packages/devextreme-react/src/core/__tests__/integration/integration.test.tsx b/packages/devextreme-react/src/core/__tests__/integration/integration.test.tsx index f9957aa0369a..5ab45d396785 100644 --- a/packages/devextreme-react/src/core/__tests__/integration/integration.test.tsx +++ b/packages/devextreme-react/src/core/__tests__/integration/integration.test.tsx @@ -13,14 +13,13 @@ import { import Validator from '../../../validator'; import ValidationSummary from '../../../validation-summary'; import DataGrid from '../../../data-grid'; +import Scheduler, { View, Resource } from '../../../scheduler'; import { ContextMenu, Item as ContextMenuItem } from '../../../context-menu'; import Button from '../../../button'; jest.useFakeTimers(); describe('integration tests', () => { - let consoleWarnSpy; - afterEach(() => { jest.clearAllMocks(); testingLib.cleanup(); @@ -155,7 +154,7 @@ describe('integration tests', () => { it('ref callback is not triggered when not needed', async () => { const user = userEvent.setup({ delay: null }); - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); const DataGridWithSelectButton = () => { const [selectedRowKeys, setSelectedRowKeys] = React.useState([] as any); @@ -226,4 +225,134 @@ describe('integration tests', () => { expect(consoleWarnSpy).toHaveBeenCalledTimes(1); consoleWarnSpy.mockRestore(); }); + + it('must not fail if a template with two root elements is unmounted (T1300588)', async () => { + try { + jest.useRealTimers(); + expect.assertions(1); + + const user = userEvent.setup({ delay: null }); + + const currentDate = new Date(2021, 3, 27); + const dayOfWeekNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const typeGroups = ['typeId']; + const priorityGroups = ['priorityId']; + + const data = [ + { + text: 'Walking a dog', + priorityId: 1, + typeId: 1, + startDate: new Date('2021-04-26T15:00:00.000Z'), + endDate: new Date('2021-04-26T15:30:00.000Z'), + recurrenceRule: 'FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20210502', + }, + { + text: 'Website Re-Design Plan', + priorityId: 2, + typeId: 2, + startDate: new Date('2021-04-26T16:00:00.000Z'), + endDate: new Date('2021-04-26T18:30:00.000Z'), + }, + { + text: 'Book Flights to San Fran for Sales Trip', + priorityId: 2, + typeId: 2, + startDate: new Date('2021-04-26T19:00:00.000Z'), + endDate: new Date('2021-04-26T20:00:00.000Z'), + } + ]; + + const priorityData = [{ + text: 'Low Priority', + id: 1, + color: '#fcb65e', + }, { + text: 'High Priority', + id: 2, + color: '#e18e92', + }]; + + const typeData = [{ + text: 'Home', + id: 1, + color: '#b6d623', + }, { + text: 'Work', + id: 2, + color: '#679ec5', + }]; + + const DateCell = ({ data: cellData }) => ( + +
{dayOfWeekNames[cellData.date.getDay()]}
+
{cellData.date.getDate()}
+
+ ); + + let clicked = false; + let resolve = () => {}; + + const onContentReady = async () => { + if (clicked) + return; + + clicked = true; + await expect(user.click(document.querySelector('.dx-icon-chevronnext')!)).resolves.toBe(void 0); + resolve(); + } + + const SchedulerWithTemplates = () => ( + + + + + + + + + ); + + testingLib.render( + + + + ); + + await testingLib.act(async () => { + return new Promise((r) => { + resolve = r; + }); + }); + } + finally { + jest.useFakeTimers(); + } + }); }); \ No newline at end of file diff --git a/packages/devextreme-react/src/core/__tests__/template-wrapper.test.tsx b/packages/devextreme-react/src/core/__tests__/template-wrapper.test.tsx index 202f130b605d..ddddd3f3b53b 100644 --- a/packages/devextreme-react/src/core/__tests__/template-wrapper.test.tsx +++ b/packages/devextreme-react/src/core/__tests__/template-wrapper.test.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { default as dxRender } from 'devextreme/core/renderer'; import { useEffect, useContext } from 'react'; import { TemplateWrapper } from '../template-wrapper'; import { cleanup, render } from '@testing-library/react'; @@ -6,8 +7,8 @@ import * as events from 'devextreme/events'; import { RemovalLockerContext, UpdateLocker } from '../contexts'; import { TemplateFunc } from '../types'; -function TemplateComponent(args: { data, index, onRendered?, effect? }) { - const { data, index, onRendered, effect } = args; +function TemplateComponent(args: { data, index, onRendered?, effect?, multipleRoots }) { + const { data, index, onRendered, effect, multipleRoots } = args; effect?.(); @@ -15,12 +16,19 @@ function TemplateComponent(args: { data, index, onRendered?, effect? }) { onRendered?.(); }, [onRendered]); - return ( -
{`${data.text} - ${index}`}
- ); + return multipleRoots + ? ( + <> +
{`${data.text} - ${index}`}
+
{`${data.text} - ${index}`}
+ + ) + : ( +
{`${data.text} - ${index}`}
+ ); } -function getComponentTemplateFunction(effect?) { +function getComponentTemplateFunction(effect?, multipleRoots = false) { return ({ data, index, onRendered }) => { return ( ); }; @@ -43,7 +52,7 @@ describe('Template Wrapper', () => { cleanup(); }); - it('works with locker in the context correctly', () => { + it('works with locker in the context correctly', async () => { let onRemovedFired = false; let removalLocker: UpdateLocker | undefined = undefined; @@ -69,7 +78,8 @@ describe('Template Wrapper', () => { <>
{ events.triggerHandler(document.querySelector('.template-element')!, 'dxremove'); + await Promise.resolve(); expect(onRemovedFired).toBeTruthy(); onRemovedFired = false; @@ -93,7 +104,8 @@ describe('Template Wrapper', () => { <>
{ removalLocker.lock(); events.triggerHandler(document.querySelector('.template-element')!, 'dxremove'); + await Promise.resolve(); expect(onRemovedFired).toBeFalsy(); rerender( <>
{ removalLocker.lock(); events.triggerHandler(document.querySelector('.template-element')!, 'dxremove'); + await Promise.resolve(); expect(onRemovedFired).toBeFalsy(); rerender( <>
{ removalLocker.unlock(); events.triggerHandler(document.querySelector('.template-element')!, 'dxremove'); + await Promise.resolve(); expect(onRemovedFired).toBeTruthy(); }); - it('does not fire onRemove when the event comes from wrappers', () => { + it('does not fire onRemove when the event comes from wrappers', async () => { let onRemovedFired = false; const onRemoved = () => { @@ -169,7 +186,8 @@ describe('Template Wrapper', () => { <>
{ events.triggerHandler(document.querySelector('.template-element')!, 'dxremove', { isUnmounting: true }); + await Promise.resolve(); expect(onRemovedFired).toBeFalsy(); rerender( <>
{ events.triggerHandler(document.querySelector('.template-element')!, 'dxremove'); + await Promise.resolve(); expect(onRemovedFired).toBeTruthy(); }); @@ -217,6 +238,8 @@ describe('Template Wrapper', () => { <>
{ { onRemoved={() => undefined} /> { onRemoved={() => undefined} /> { <>
{ .toBe('
My template - 1
'); }); - it('triggers onRemove when the element is removed', () => { + it('triggers onRemove when the element is removed', async () => { let onRemovedFired = false; const onRemoved = () => { @@ -374,6 +405,8 @@ describe('Template Wrapper', () => { <>
{ /> ); - + + await Promise.resolve(); expect(onRemovedFired).toBeFalsy(); events.triggerHandler(document.querySelector('.template-element')!, 'dxremove'); + await Promise.resolve(); expect(onRemovedFired).toBeTruthy(); }); - it('removes text templates when the removal listener is removed', () => { + it('removes text templates when the removal listener is removed', async () => { let onRemovedFired = false; const onRemoved = () => { @@ -410,6 +445,8 @@ describe('Template Wrapper', () => { <>
{ ); + await Promise.resolve(); expect(onRemovedFired).toBeFalsy(); const containerChildren = document.querySelector('.template-container')?.children!; @@ -428,10 +466,11 @@ describe('Template Wrapper', () => { events.triggerHandler(containerChildren[i], 'dxremove'); } + await Promise.resolve(); expect(onRemovedFired).toBeTruthy(); }); - it('returns all the elements to DOM on unmount to avoid upsetting React', () => { + it('returns the element and hidden nodes to DOM on unmount to avoid upsetting React', () => { const templateFunction: TemplateFunc = getComponentTemplateFunction(); const { rerender, unmount } = render( @@ -444,6 +483,44 @@ describe('Template Wrapper', () => { <>
undefined} + container={document.querySelector('.template-container')!} + onRemoved={() => undefined} + /> + + ); + + const container = document.querySelector('.template-container')!; + const children = container.children; + + for(var i = 0; i < children.length; i++) { + dxRender(children[i]).remove(); + } + + expect(() => unmount()).not.toThrow(); + expect(container.children.length).toBe(0); + }); + + it('returns multiple template root elements to DOM on unmount to avoid upsetting React', () => { + const templateFunction: TemplateFunc = getComponentTemplateFunction(void 0, true); + + const { rerender, unmount } = render( + <> +
+ + ); + + rerender( + <> +
+ { const children = container.children; for(var i = 0; i < children.length; i++) { - container.removeChild(children[i]); + dxRender(children[i]).remove(); } expect(() => unmount()).not.toThrow(); + expect(container.children.length).toBe(0); }); it('portals its component to the specified container', () => { @@ -477,6 +555,8 @@ describe('Template Wrapper', () => { <>
({ key1, key2 }); -const unsubscribeOnRemoval = (container: HTMLElement, onRemoved: () => void) => { +const unsubscribeOnRemoval = (container: HTMLElement, onContainerRemoved: () => void) => { if (container.nodeType === Node.ELEMENT_NODE) { - events.off(container, DX_REMOVE_EVENT, onRemoved); + events.off(container, DX_REMOVE_EVENT, onContainerRemoved); } }; -const subscribeOnRemoval = (container: HTMLElement, onRemoved: () => void) => { +const subscribeOnRemoval = (container: HTMLElement, onContainerRemoved: () => void) => { if (container.nodeType === Node.ELEMENT_NODE) { - events.on(container, DX_REMOVE_EVENT, onRemoved); + events.on(container, DX_REMOVE_EVENT, onContainerRemoved); } }; @@ -74,13 +74,19 @@ export const TemplateManager: FC = ({ init, onTemplatesRen const containerElement = unwrapElement(container); const key = createMapKey(data, containerElement); - const onRemoved = (): void => { - if (collection.get(key)) { + const onRemoved = (componentKey?: string): void => { + const model = collection.get(key); + + if (model && (!componentKey || model.componentKey === componentKey)) { collection.delete(key); setInstantiationModels({ collection }); } }; + const onContainerRemoved = (): void => { + onRemoved(); + }; + const hostWidgetId = widgetId.current; collection.set(key, { @@ -88,13 +94,14 @@ export const TemplateManager: FC = ({ init, onTemplatesRen index, componentKey: getRandomId(), onRendered: () => { - unsubscribeOnRemoval(containerElement, onRemoved); + unsubscribeOnRemoval(containerElement, onContainerRemoved); if (hostWidgetId === widgetId.current) { onRendered?.(); } }, onRemoved, + onContainerRemoved, }); setInstantiationModels({ collection }); @@ -185,14 +192,16 @@ export const TemplateManager: FC = ({ init, onTemplatesRen componentKey, onRendered, onRemoved, + onContainerRemoved, }]) => { - subscribeOnRemoval(container, onRemoved); + subscribeOnRemoval(container, onContainerRemoved); const factory = templateFactories.current[templateKey]; if (factory) { return = ({ container, onRemoved, onRendered, + componentKey, }) => { const [removalListenerRequired, setRemovalListenerRequired] = useState(false); const isRemovalLocked = useRef(false); @@ -49,7 +49,7 @@ const TemplateWrapperComponent: FC = ({ unlock(): void { isRemovalLocked.current = false; }, }), []); - const element = useRef(); + const elements = useRef([]); const hiddenNodeElement = useRef(); const removalListenerElement = useRef(); @@ -58,23 +58,25 @@ const TemplateWrapperComponent: FC = ({ return; } - if (element.current) { - events.off(element.current, DX_REMOVE_EVENT, onTemplateRemoved); - } - - if (removalListenerElement.current) { - events.off(removalListenerElement.current, DX_REMOVE_EVENT, onTemplateRemoved); - } + [ + ...elements.current, + removalListenerElement.current, + ].forEach((el) => el && events.off(el, DX_REMOVE_EVENT, onTemplateRemoved)); - onRemoved(); + // In case of multiple root elements, letting the widget remove them all sync + Promise.resolve().then(() => { + onRemoved(componentKey); + }); }, [onRemoved]); useLayoutEffect(() => { - const el = element.current; + const elementNodes = elements.current.filter((el) => el.nodeType === Node.ELEMENT_NODE); - if (el && el.nodeType === Node.ELEMENT_NODE) { - events.off(el, DX_REMOVE_EVENT, onTemplateRemoved); - events.on(el, DX_REMOVE_EVENT, onTemplateRemoved); + if (elementNodes.length) { + elementNodes.forEach((el) => { + events.off(el, DX_REMOVE_EVENT, onTemplateRemoved); + events.on(el, DX_REMOVE_EVENT, onTemplateRemoved); + }); } else if (!removalListenerRequired) { setRemovalListenerRequired(true); } else if (removalListenerElement.current) { @@ -83,18 +85,20 @@ const TemplateWrapperComponent: FC = ({ } return () => { - const safeAppend = (child?: MutableRefObject) => { - if (child?.current && container && !container.contains(child.current)) { - container.appendChild(child.current); + const safeAppend = (child: HTMLElement | undefined) => { + if (child && container && !container.contains(child)) { + container.appendChild(child); } }; - safeAppend(element); - safeAppend(hiddenNodeElement); - safeAppend(removalListenerElement); + [ + ...elements.current, + hiddenNodeElement.current, + removalListenerElement.current, + ].forEach((el) => safeAppend(el)); - if (el) { - events.off(el, DX_REMOVE_EVENT, onTemplateRemoved); + if (elementNodes.length) { + elementNodes.forEach((el) => events.off(el, DX_REMOVE_EVENT, onTemplateRemoved)); } }; }, [onTemplateRemoved, removalListenerRequired, container]); @@ -103,9 +107,21 @@ const TemplateWrapperComponent: FC = ({ onRendered(); }, [onRendered]); + const containerContent = Array.from(container.childNodes); + const hiddenNode = createHiddenNode(container?.nodeName, (node: HTMLElement) => { hiddenNodeElement.current = node; - element.current = node?.previousSibling as HTMLElement; + elements.current = []; + + let currentNode = node?.previousSibling as HTMLElement; + + while (currentNode) { + if (!containerContent.includes(currentNode)) { + elements.current.push(currentNode); + } + + currentNode = currentNode?.previousSibling as HTMLElement; + } }, 'div'); const removalListener = removalListenerRequired diff --git a/packages/devextreme-react/src/core/types.ts b/packages/devextreme-react/src/core/types.ts index 9ac572ee5829..cbc66e4768a7 100644 --- a/packages/devextreme-react/src/core/types.ts +++ b/packages/devextreme-react/src/core/types.ts @@ -36,8 +36,9 @@ export interface TemplateWrapperProps { data: any; index: number; container: HTMLElement; + componentKey: string; onRendered: () => void; - onRemoved: () => void; + onRemoved: (componentKey: string) => void; } export type TemplateFunc = (arg: TemplateArgs) => JSX.Element | ReactNode; @@ -66,7 +67,8 @@ export interface TemplateInstantiationModel { componentKey: string; index: any; onRendered: () => void; - onRemoved: () => void; + onRemoved: (componentKey: string) => void; + onContainerRemoved: () => void; } export type GetRenderFuncFn = (templateKey: string) => RenderFunc; diff --git a/packages/devextreme-react/src/data-grid.ts b/packages/devextreme-react/src/data-grid.ts index fb0b610fdff0..27a7e1ae5b96 100644 --- a/packages/devextreme-react/src/data-grid.ts +++ b/packages/devextreme-react/src/data-grid.ts @@ -13,7 +13,7 @@ import type { dxDataGridColumn, AdaptiveDetailRowPreparingEvent, CellClickEvent, import type { DataChange, DataChangeType, FilterOperation, FilterType, FixedPosition, ColumnHeaderFilter as GridsColumnHeaderFilter, SelectedFilterOperation, ColumnChooserMode, ColumnChooserSearchConfig, ColumnChooserSelectionConfig, HeaderFilterGroupInterval, ColumnHeaderFilterSearchConfig, HeaderFilterSearchConfig, HeaderFilterTexts, SelectionColumnDisplayMode, GridsEditMode, NewRowPosition, GridsEditRefreshMode, StartEditAction, FilterPanel as GridsFilterPanel, FilterPanelTexts as GridsFilterPanelTexts, ApplyFilterMode, GroupExpandMode, SummaryType, EnterKeyAction, EnterKeyDirection, PagerPageSize, GridBase, DataRenderMode, StateStoreType } from "devextreme/common/grids"; import type { Mode, ValidationRuleType, HorizontalAlignment, VerticalAlignment, template, DataType, Format as CommonFormat, SortOrder, SearchMode, ComparisonOperator, SingleMultipleOrNone, SelectAllMode, PositionAlignment, Direction, ToolbarItemLocation, ToolbarItemComponent, DisplayMode, DragDirection, DragHighlight, ScrollbarMode } from "devextreme/common"; import type { ContentReadyEvent as FilterBuilderContentReadyEvent, DisposingEvent as FilterBuilderDisposingEvent, EditorPreparedEvent as FilterBuilderEditorPreparedEvent, EditorPreparingEvent as FilterBuilderEditorPreparingEvent, InitializedEvent as FilterBuilderInitializedEvent, dxFilterBuilderField, FieldInfo, FilterBuilderOperation, dxFilterBuilderCustomOperation, GroupOperation, OptionChangedEvent, ValueChangedEvent } from "devextreme/ui/filter_builder"; -import type { ContentReadyEvent as FormContentReadyEvent, DisposingEvent as FormDisposingEvent, InitializedEvent as FormInitializedEvent, dxFormSimpleItem, dxFormOptions, OptionChangedEvent as FormOptionChangedEvent, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, EditorEnterKeyEvent, FieldDataChangedEvent, FormItemComponent, FormItemType } from "devextreme/ui/form"; +import type { ContentReadyEvent as FormContentReadyEvent, DisposingEvent as FormDisposingEvent, InitializedEvent as FormInitializedEvent, dxFormSimpleItem, dxFormOptions, OptionChangedEvent as FormOptionChangedEvent, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, EditorEnterKeyEvent, FieldDataChangedEvent, SmartPastedEvent, SmartPastingEvent, FormItemComponent, FormItemType } from "devextreme/ui/form"; import type { AnimationConfig, CollisionResolution, PositionConfig, AnimationState, AnimationType, CollisionResolutionCombination } from "devextreme/common/core/animation"; import type { Format as LocalizationFormat } from "devextreme/common/core/localization"; import type { DataSourceOptions } from "devextreme/data/data_source"; @@ -22,6 +22,7 @@ import type { dxPopupOptions, dxPopupToolbarItem, ToolbarLocation } from "devext import type { event } from "devextreme/events/events.types"; import type { EventInfo } from "devextreme/common/core/events"; import type { Component } from "devextreme/core/component"; +import type { AIIntegration } from "devextreme/common/ai-integration"; import type { LocateInMenuMode, ShowTextMode } from "devextreme/ui/toolbar"; import type { CollectionWidgetItem } from "devextreme/ui/collection/ui.collection_widget.base"; @@ -201,6 +202,25 @@ const DataGrid = memo( ) as (props: React.PropsWithChildren> & { ref?: Ref> }) => ReactElement | null; +// owners: +// FormItem +type IAiOptionsProps = React.PropsWithChildren<{ + disabled?: boolean; + instruction?: string | undefined; +}> +const _componentAiOptions = (props: IAiOptionsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "aiOptions", + }, + }); +}; + +const AiOptions = Object.assign(_componentAiOptions, { + componentType: "option", +}); + // owners: // Popup // FilterBuilderPopup @@ -1194,7 +1214,6 @@ type IFilterBuilderProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; allowHierarchicalFields?: boolean; - bindingOptions?: Record; customOperations?: Array; disabled?: boolean; elementAttr?: Record; @@ -1271,7 +1290,6 @@ type IFilterBuilderPopupProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -1490,9 +1508,9 @@ const FilterRow = Object.assign type IFormProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; + aiIntegration?: AIIntegration | undefined; alignItemLabels?: boolean; alignItemLabelsInAllGroups?: boolean; - bindingOptions?: Record; colCount?: Mode | number; colCountByScreen?: Record | { lg?: number | undefined; @@ -1519,6 +1537,8 @@ type IFormProps = React.PropsWithChildren<{ onFieldDataChanged?: ((e: FieldDataChangedEvent) => void); onInitialized?: ((e: FormInitializedEvent) => void); onOptionChanged?: ((e: FormOptionChangedEvent) => void); + onSmartPasted?: ((e: SmartPastedEvent) => void); + onSmartPasting?: ((e: SmartPastingEvent) => void); optionalMark?: string; readOnly?: boolean; requiredMark?: string; @@ -1583,6 +1603,10 @@ const Format = Object.assign(_comp // owners: // Column type IFormItemProps = React.PropsWithChildren<{ + aiOptions?: Record | { + disabled?: boolean; + instruction?: string | undefined; + }; colSpan?: number | undefined; cssClass?: string | undefined; dataField?: string | undefined; @@ -1613,6 +1637,7 @@ const _componentFormItem = (props: IFormItemProps) => { elementDescriptor: { OptionName: "formItem", ExpectedChildren: { + aiOptions: { optionName: "aiOptions", isCollectionItem: false }, AsyncRule: { optionName: "validationRules", isCollectionItem: true }, CompareRule: { optionName: "validationRules", isCollectionItem: true }, CustomRule: { optionName: "validationRules", isCollectionItem: true }, @@ -2231,7 +2256,6 @@ type IPopupProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -3069,6 +3093,8 @@ export { DataGrid, IDataGridOptions, DataGridRef, + AiOptions, + IAiOptionsProps, Animation, IAnimationProps, AsyncRule, diff --git a/packages/devextreme-react/src/date-box.ts b/packages/devextreme-react/src/date-box.ts index 04623f10a14c..3ba0d33fd256 100644 --- a/packages/devextreme-react/src/date-box.ts +++ b/packages/devextreme-react/src/date-box.ts @@ -204,7 +204,6 @@ const Button = Object.assign(_comp type ICalendarOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; cellTemplate?: ((itemData: { date: Date, text: string, view: string }, itemIndex: number, itemElement: any) => string | any) | template; dateSerializationFormat?: string | undefined; disabled?: boolean; @@ -323,7 +322,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -516,7 +514,6 @@ const Offset = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; diff --git a/packages/devextreme-react/src/date-range-box.ts b/packages/devextreme-react/src/date-range-box.ts index 2a9d0f02735c..aed0ca125d1d 100644 --- a/packages/devextreme-react/src/date-range-box.ts +++ b/packages/devextreme-react/src/date-range-box.ts @@ -209,7 +209,6 @@ const Button = Object.assign(_comp type ICalendarOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; cellTemplate?: ((itemData: { date: Date, text: string, view: string }, itemIndex: number, itemElement: any) => string | any) | template; dateSerializationFormat?: string | undefined; disabled?: boolean; @@ -328,7 +327,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -521,7 +519,6 @@ const Offset = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; diff --git a/packages/devextreme-react/src/drop-down-box.ts b/packages/devextreme-react/src/drop-down-box.ts index 1ce6ad427e48..ce01022a167d 100644 --- a/packages/devextreme-react/src/drop-down-box.ts +++ b/packages/devextreme-react/src/drop-down-box.ts @@ -82,7 +82,8 @@ const DropDownBox = memo( const expectedChildren = useMemo(() => ({ button: { optionName: "buttons", isCollectionItem: true }, - dropDownOptions: { optionName: "dropDownOptions", isCollectionItem: false } + dropDownOptions: { optionName: "dropDownOptions", isCollectionItem: false }, + fieldAddons: { optionName: "fieldAddons", isCollectionItem: false } }), []); const templateProps = useMemo(() => ([ @@ -232,7 +233,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -325,6 +325,38 @@ const DropDownOptions = Object.assign string | any) | template; + beforeTemplate?: ((data: any, element: any) => string | any) | template; + afterRender?: (...params: any) => React.ReactNode; + afterComponent?: React.ComponentType; + beforeRender?: (...params: any) => React.ReactNode; + beforeComponent?: React.ComponentType; +}> +const _componentFieldAddons = (props: IFieldAddonsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "fieldAddons", + TemplateProps: [{ + tmplOption: "afterTemplate", + render: "afterRender", + component: "afterComponent" + }, { + tmplOption: "beforeTemplate", + render: "beforeRender", + component: "beforeComponent" + }], + }, + }); +}; + +const FieldAddons = Object.assign(_componentFieldAddons, { + componentType: "option", +}); + // owners: // Hide // Show @@ -425,7 +457,6 @@ const Offset = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; @@ -635,6 +666,8 @@ export { ICollisionProps, DropDownOptions, IDropDownOptionsProps, + FieldAddons, + IFieldAddonsProps, From, IFromProps, Hide, diff --git a/packages/devextreme-react/src/drop-down-button.ts b/packages/devextreme-react/src/drop-down-button.ts index 4ca42dd49f41..2c06e7766b52 100644 --- a/packages/devextreme-react/src/drop-down-button.ts +++ b/packages/devextreme-react/src/drop-down-button.ts @@ -188,7 +188,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; diff --git a/packages/devextreme-react/src/form.ts b/packages/devextreme-react/src/form.ts index 4556b4cf2005..314de07a13f0 100644 --- a/packages/devextreme-react/src/form.ts +++ b/packages/devextreme-react/src/form.ts @@ -8,7 +8,7 @@ import dxForm, { import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponentMeta } from "./core/component"; import NestedOption from "./core/nested-option"; -import type { ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, InitializedEvent, FormItemType, dxFormButtonItem, dxFormEmptyItem, dxFormGroupItem, dxFormSimpleItem, dxFormTabbedItem, FormItemComponent, LabelLocation } from "devextreme/ui/form"; +import type { ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, InitializedEvent, SmartPastedEvent, SmartPastingEvent, FormItemType, FormPredefinedButtonItem, dxFormButtonItem, dxFormEmptyItem, dxFormGroupItem, dxFormSimpleItem, dxFormTabbedItem, FormItemComponent, LabelLocation } from "devextreme/ui/form"; import type { ContentReadyEvent as ButtonContentReadyEvent, DisposingEvent as ButtonDisposingEvent, InitializedEvent as ButtonInitializedEvent, dxButtonOptions, ClickEvent, OptionChangedEvent } from "devextreme/ui/button"; import type { ContentReadyEvent as TabPanelContentReadyEvent, DisposingEvent as TabPanelDisposingEvent, InitializedEvent as TabPanelInitializedEvent, OptionChangedEvent as TabPanelOptionChangedEvent, dxTabPanelOptions, dxTabPanelItem, ItemClickEvent, ItemContextMenuEvent, ItemHoldEvent, ItemRenderedEvent, SelectionChangedEvent, SelectionChangingEvent, TitleClickEvent, TitleHoldEvent, TitleRenderedEvent } from "devextreme/ui/tab_panel"; import type { ValidationRuleType, HorizontalAlignment, VerticalAlignment, ButtonStyle, template, ButtonType, ComparisonOperator, TabsIconPosition, TabsStyle, Position } from "devextreme/common"; @@ -29,6 +29,8 @@ type IFormOptionsNarrowedEvents = { onDisposing?: ((e: DisposingEvent) => void); onEditorEnterKey?: ((e: EditorEnterKeyEvent) => void); onInitialized?: ((e: InitializedEvent) => void); + onSmartPasted?: ((e: SmartPastedEvent) => void); + onSmartPasting?: ((e: SmartPastingEvent) => void); } type IFormOptions = React.PropsWithChildren & IHtmlOptions & { @@ -54,7 +56,7 @@ const Form = memo( ), []); const subscribableOptions = useMemo(() => (["formData"]), []); - const independentEvents = useMemo(() => (["onContentReady","onDisposing","onEditorEnterKey","onInitialized"]), []); + const independentEvents = useMemo(() => (["onContentReady","onDisposing","onEditorEnterKey","onInitialized","onSmartPasted","onSmartPasting"]), []); const defaults = useMemo(() => ({ defaultFormData: "formData", @@ -86,6 +88,25 @@ const Form = memo( ) as (props: React.PropsWithChildren & { ref?: Ref }) => ReactElement | null; +// owners: +// SimpleItem +type IAiOptionsProps = React.PropsWithChildren<{ + disabled?: boolean; + instruction?: string | undefined; +}> +const _componentAiOptions = (props: IAiOptionsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "aiOptions", + }, + }); +}; + +const AiOptions = Object.assign(_componentAiOptions, { + componentType: "option", +}); + // owners: // SimpleItem type IAsyncRuleProps = React.PropsWithChildren<{ @@ -120,7 +141,7 @@ type IButtonItemProps = React.PropsWithChildren<{ cssClass?: string | undefined; horizontalAlignment?: HorizontalAlignment; itemType?: FormItemType; - name?: string | undefined; + name?: FormPredefinedButtonItem | string | undefined; verticalAlignment?: VerticalAlignment; visible?: boolean; visibleIndex?: number | undefined; @@ -150,7 +171,6 @@ const ButtonItem = Object.assign; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; @@ -388,6 +408,10 @@ type IItemProps = React.PropsWithChildren<{ text?: string; title?: string; visible?: boolean; + aiOptions?: Record | { + disabled?: boolean; + instruction?: string | undefined; + }; colSpan?: number | undefined; cssClass?: string | undefined; dataField?: string | undefined; @@ -404,7 +428,7 @@ type IItemProps = React.PropsWithChildren<{ text?: string | undefined; visible?: boolean; }; - name?: string | undefined; + name?: string | undefined | FormPredefinedButtonItem; validationRules?: Array; visibleIndex?: number | undefined; alignItemLabels?: boolean; @@ -453,6 +477,7 @@ const _componentItem = (props: IItemProps) => { OptionName: "items", IsCollectionItem: true, ExpectedChildren: { + aiOptions: { optionName: "aiOptions", isCollectionItem: false }, AsyncRule: { optionName: "validationRules", isCollectionItem: true }, buttonOptions: { optionName: "buttonOptions", isCollectionItem: false }, colCountByScreen: { optionName: "colCountByScreen", isCollectionItem: false }, @@ -623,6 +648,10 @@ const RequiredRule = Object.assign | { + disabled?: boolean; + instruction?: string | undefined; + }; colSpan?: number | undefined; cssClass?: string | undefined; dataField?: string | undefined; @@ -654,6 +683,7 @@ const _componentSimpleItem = (props: ISimpleItemProps) => { OptionName: "items", IsCollectionItem: true, ExpectedChildren: { + aiOptions: { optionName: "aiOptions", isCollectionItem: false }, AsyncRule: { optionName: "validationRules", isCollectionItem: true }, CompareRule: { optionName: "validationRules", isCollectionItem: true }, CustomRule: { optionName: "validationRules", isCollectionItem: true }, @@ -813,7 +843,6 @@ type ITabPanelOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; animationEnabled?: boolean; - bindingOptions?: Record; dataSource?: Array | DataSource | DataSourceOptions | null | Store | string; deferRendering?: boolean; disabled?: boolean; @@ -975,6 +1004,8 @@ export { Form, IFormOptions, FormRef, + AiOptions, + IAiOptionsProps, AsyncRule, IAsyncRuleProps, ButtonItem, diff --git a/packages/devextreme-react/src/html-editor.ts b/packages/devextreme-react/src/html-editor.ts index 7d1749a5da91..dbe6f550fcf0 100644 --- a/packages/devextreme-react/src/html-editor.ts +++ b/packages/devextreme-react/src/html-editor.ts @@ -138,7 +138,6 @@ type IFileUploaderOptionsProps = React.PropsWithChildren<{ activeStateEnabled?: boolean; allowCanceling?: boolean; allowedFileExtensions?: Array; - bindingOptions?: Record; chunkSize?: number; dialogTrigger?: any | string | undefined; disabled?: boolean; diff --git a/packages/devextreme-react/src/list.ts b/packages/devextreme-react/src/list.ts index 1472716c8125..9514436e019e 100644 --- a/packages/devextreme-react/src/list.ts +++ b/packages/devextreme-react/src/list.ts @@ -200,7 +200,6 @@ type IItemDraggingProps = React.PropsWithChildren<{ allowDropInsideItem?: boolean; allowReordering?: boolean; autoScroll?: boolean; - bindingOptions?: Record; boundary?: any | string | undefined; container?: any | string | undefined; cursorOffset?: Record | string | { @@ -281,7 +280,6 @@ const MenuItem = Object.assign(_ type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; @@ -330,7 +328,6 @@ const Options = Object.assign(_co type ISearchEditorOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; buttons?: Array; disabled?: boolean; elementAttr?: Record; diff --git a/packages/devextreme-react/src/lookup.ts b/packages/devextreme-react/src/lookup.ts index e63130e88f8c..25bb3d4c4066 100644 --- a/packages/devextreme-react/src/lookup.ts +++ b/packages/devextreme-react/src/lookup.ts @@ -199,7 +199,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; diff --git a/packages/devextreme-react/src/number-box.ts b/packages/devextreme-react/src/number-box.ts index 7695a4805f9a..35e3966a2be6 100644 --- a/packages/devextreme-react/src/number-box.ts +++ b/packages/devextreme-react/src/number-box.ts @@ -135,7 +135,6 @@ const Format = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; diff --git a/packages/devextreme-react/src/scheduler.ts b/packages/devextreme-react/src/scheduler.ts index 88433a476629..5c15c77e859a 100644 --- a/packages/devextreme-react/src/scheduler.ts +++ b/packages/devextreme-react/src/scheduler.ts @@ -269,7 +269,6 @@ const Item = Object.assign(_componen type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; buttonTemplate?: ((buttonData: any, buttonContent: any) => string | any) | template; disabled?: boolean; elementAttr?: Record; diff --git a/packages/devextreme-react/src/select-box.ts b/packages/devextreme-react/src/select-box.ts index 9c33ab0f5787..bff5f49f379e 100644 --- a/packages/devextreme-react/src/select-box.ts +++ b/packages/devextreme-react/src/select-box.ts @@ -91,6 +91,7 @@ const SelectBox = memo( const expectedChildren = useMemo(() => ({ button: { optionName: "buttons", isCollectionItem: true }, dropDownOptions: { optionName: "dropDownOptions", isCollectionItem: false }, + fieldAddons: { optionName: "fieldAddons", isCollectionItem: false }, item: { optionName: "items", isCollectionItem: true } }), []); @@ -246,7 +247,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -339,6 +339,38 @@ const DropDownOptions = Object.assign string | any) | template; + beforeTemplate?: ((data: any, element: any) => string | any) | template; + afterRender?: (...params: any) => React.ReactNode; + afterComponent?: React.ComponentType; + beforeRender?: (...params: any) => React.ReactNode; + beforeComponent?: React.ComponentType; +}> +const _componentFieldAddons = (props: IFieldAddonsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "fieldAddons", + TemplateProps: [{ + tmplOption: "afterTemplate", + render: "afterRender", + component: "afterComponent" + }, { + tmplOption: "beforeTemplate", + render: "beforeRender", + component: "beforeComponent" + }], + }, + }); +}; + +const FieldAddons = Object.assign(_componentFieldAddons, { + componentType: "option", +}); + // owners: // Hide // Show @@ -469,7 +501,6 @@ const Offset = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; @@ -679,6 +710,8 @@ export { ICollisionProps, DropDownOptions, IDropDownOptionsProps, + FieldAddons, + IFieldAddonsProps, From, IFromProps, Hide, diff --git a/packages/devextreme-react/src/tag-box.ts b/packages/devextreme-react/src/tag-box.ts index 4f5255398f38..b58a368edac7 100644 --- a/packages/devextreme-react/src/tag-box.ts +++ b/packages/devextreme-react/src/tag-box.ts @@ -92,6 +92,7 @@ const TagBox = memo( const expectedChildren = useMemo(() => ({ button: { optionName: "buttons", isCollectionItem: true }, dropDownOptions: { optionName: "dropDownOptions", isCollectionItem: false }, + fieldAddons: { optionName: "fieldAddons", isCollectionItem: false }, item: { optionName: "items", isCollectionItem: true } }), []); @@ -252,7 +253,6 @@ type IDropDownOptionsProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -345,6 +345,38 @@ const DropDownOptions = Object.assign string | any) | template; + beforeTemplate?: ((data: any, element: any) => string | any) | template; + afterRender?: (...params: any) => React.ReactNode; + afterComponent?: React.ComponentType; + beforeRender?: (...params: any) => React.ReactNode; + beforeComponent?: React.ComponentType; +}> +const _componentFieldAddons = (props: IFieldAddonsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "fieldAddons", + TemplateProps: [{ + tmplOption: "afterTemplate", + render: "afterRender", + component: "afterComponent" + }, { + tmplOption: "beforeTemplate", + render: "beforeRender", + component: "beforeComponent" + }], + }, + }); +}; + +const FieldAddons = Object.assign(_componentFieldAddons, { + componentType: "option", +}); + // owners: // Hide // Show @@ -475,7 +507,6 @@ const Offset = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; @@ -685,6 +716,8 @@ export { ICollisionProps, DropDownOptions, IDropDownOptionsProps, + FieldAddons, + IFieldAddonsProps, From, IFromProps, Hide, diff --git a/packages/devextreme-react/src/text-box.ts b/packages/devextreme-react/src/text-box.ts index a922707bd7c1..4a5756cdf743 100644 --- a/packages/devextreme-react/src/text-box.ts +++ b/packages/devextreme-react/src/text-box.ts @@ -111,7 +111,6 @@ const Button = Object.assign(_comp type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; diff --git a/packages/devextreme-react/src/tree-list.ts b/packages/devextreme-react/src/tree-list.ts index 3b4926778343..c5512499b9f6 100644 --- a/packages/devextreme-react/src/tree-list.ts +++ b/packages/devextreme-react/src/tree-list.ts @@ -12,7 +12,7 @@ import NestedOption from "./core/nested-option"; import type { dxTreeListColumn, AdaptiveDetailRowPreparingEvent, CellClickEvent, CellDblClickEvent, CellPreparedEvent, ContentReadyEvent, ContextMenuPreparingEvent, DataErrorOccurredEvent, DisposingEvent, EditCanceledEvent, EditCancelingEvent, EditingStartEvent, EditorPreparedEvent, EditorPreparingEvent, FocusedCellChangingEvent, FocusedRowChangingEvent, InitializedEvent, InitNewRowEvent, KeyDownEvent, NodesInitializedEvent, RowClickEvent, RowCollapsedEvent, RowCollapsingEvent, RowDblClickEvent, RowExpandedEvent, RowExpandingEvent, RowInsertedEvent, RowInsertingEvent, RowPreparedEvent, RowRemovedEvent, RowRemovingEvent, RowUpdatedEvent, RowUpdatingEvent, RowValidatingEvent, SavedEvent, SavingEvent, ToolbarPreparingEvent, dxTreeListRowObject, TreeListPredefinedColumnButton, dxTreeListColumnButton, TreeListCommandColumnType, TreeListPredefinedToolbarItem, dxTreeListToolbarItem } from "devextreme/ui/tree_list"; import type { DataChange, DataChangeType, FilterOperation, FilterType, FixedPosition, ColumnHeaderFilter as GridsColumnHeaderFilter, SelectedFilterOperation, ColumnChooserMode, ColumnChooserSearchConfig, ColumnChooserSelectionConfig, HeaderFilterGroupInterval, ColumnHeaderFilterSearchConfig, GridsEditMode, GridsEditRefreshMode, StartEditAction, FilterPanel as GridsFilterPanel, FilterPanelTexts as GridsFilterPanelTexts, ApplyFilterMode, HeaderFilterSearchConfig, HeaderFilterTexts, EnterKeyAction, EnterKeyDirection, PagerPageSize, GridBase, DataRenderMode, StateStoreType } from "devextreme/common/grids"; import type { ContentReadyEvent as FilterBuilderContentReadyEvent, DisposingEvent as FilterBuilderDisposingEvent, EditorPreparedEvent as FilterBuilderEditorPreparedEvent, EditorPreparingEvent as FilterBuilderEditorPreparingEvent, InitializedEvent as FilterBuilderInitializedEvent, dxFilterBuilderField, FieldInfo, FilterBuilderOperation, dxFilterBuilderCustomOperation, GroupOperation, OptionChangedEvent, ValueChangedEvent } from "devextreme/ui/filter_builder"; -import type { ContentReadyEvent as FormContentReadyEvent, DisposingEvent as FormDisposingEvent, InitializedEvent as FormInitializedEvent, dxFormSimpleItem, dxFormOptions, OptionChangedEvent as FormOptionChangedEvent, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, EditorEnterKeyEvent, FieldDataChangedEvent, FormItemComponent, FormItemType } from "devextreme/ui/form"; +import type { ContentReadyEvent as FormContentReadyEvent, DisposingEvent as FormDisposingEvent, InitializedEvent as FormInitializedEvent, dxFormSimpleItem, dxFormOptions, OptionChangedEvent as FormOptionChangedEvent, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, EditorEnterKeyEvent, FieldDataChangedEvent, SmartPastedEvent, SmartPastingEvent, FormItemComponent, FormItemType } from "devextreme/ui/form"; import type { AnimationConfig, CollisionResolution, PositionConfig, AnimationState, AnimationType, CollisionResolutionCombination } from "devextreme/common/core/animation"; import type { ValidationRuleType, HorizontalAlignment, VerticalAlignment, template, DataType, Format as CommonFormat, SortOrder, SearchMode, ComparisonOperator, PositionAlignment, Mode, Direction, ToolbarItemLocation, ToolbarItemComponent, DisplayMode, DragDirection, DragHighlight, ScrollMode, ScrollbarMode, SingleMultipleOrNone } from "devextreme/common"; import type { event } from "devextreme/events/events.types"; @@ -22,6 +22,7 @@ import type { Store } from "devextreme/data/store"; import type { dxPopupOptions, dxPopupToolbarItem, ToolbarLocation } from "devextreme/ui/popup"; import type { EventInfo } from "devextreme/common/core/events"; import type { Component } from "devextreme/core/component"; +import type { AIIntegration } from "devextreme/common/ai-integration"; import type { LocateInMenuMode, ShowTextMode } from "devextreme/ui/toolbar"; import type { CollectionWidgetItem } from "devextreme/ui/collection/ui.collection_widget.base"; @@ -173,6 +174,25 @@ const TreeList = memo( ) as (props: React.PropsWithChildren> & { ref?: Ref> }) => ReactElement | null; +// owners: +// FormItem +type IAiOptionsProps = React.PropsWithChildren<{ + disabled?: boolean; + instruction?: string | undefined; +}> +const _componentAiOptions = (props: IAiOptionsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "aiOptions", + }, + }); +}; + +const AiOptions = Object.assign(_componentAiOptions, { + componentType: "option", +}); + // owners: // Popup // FilterBuilderPopup @@ -1007,7 +1027,6 @@ type IFilterBuilderProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; allowHierarchicalFields?: boolean; - bindingOptions?: Record; customOperations?: Array; disabled?: boolean; elementAttr?: Record; @@ -1084,7 +1103,6 @@ type IFilterBuilderPopupProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -1303,9 +1321,9 @@ const FilterRow = Object.assign type IFormProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; + aiIntegration?: AIIntegration | undefined; alignItemLabels?: boolean; alignItemLabelsInAllGroups?: boolean; - bindingOptions?: Record; colCount?: Mode | number; colCountByScreen?: Record | { lg?: number | undefined; @@ -1332,6 +1350,8 @@ type IFormProps = React.PropsWithChildren<{ onFieldDataChanged?: ((e: FieldDataChangedEvent) => void); onInitialized?: ((e: FormInitializedEvent) => void); onOptionChanged?: ((e: FormOptionChangedEvent) => void); + onSmartPasted?: ((e: SmartPastedEvent) => void); + onSmartPasting?: ((e: SmartPastingEvent) => void); optionalMark?: string; readOnly?: boolean; requiredMark?: string; @@ -1396,6 +1416,10 @@ const Format = Object.assign(_comp // owners: // Column type IFormItemProps = React.PropsWithChildren<{ + aiOptions?: Record | { + disabled?: boolean; + instruction?: string | undefined; + }; colSpan?: number | undefined; cssClass?: string | undefined; dataField?: string | undefined; @@ -1426,6 +1450,7 @@ const _componentFormItem = (props: IFormItemProps) => { elementDescriptor: { OptionName: "formItem", ExpectedChildren: { + aiOptions: { optionName: "aiOptions", isCollectionItem: false }, AsyncRule: { optionName: "validationRules", isCollectionItem: true }, CompareRule: { optionName: "validationRules", isCollectionItem: true }, CustomRule: { optionName: "validationRules", isCollectionItem: true }, @@ -1907,7 +1932,6 @@ type IPopupProps = React.PropsWithChildren<{ hide?: AnimationConfig; show?: AnimationConfig; }; - bindingOptions?: Record; container?: any | string | undefined; contentTemplate?: ((contentElement: any) => string | any) | template; deferRendering?: boolean; @@ -2648,6 +2672,8 @@ export { TreeList, ITreeListOptions, TreeListRef, + AiOptions, + IAiOptionsProps, Animation, IAnimationProps, AsyncRule, diff --git a/packages/devextreme-react/src/tree-view.ts b/packages/devextreme-react/src/tree-view.ts index 63a8cc0010d2..980124ba2734 100644 --- a/packages/devextreme-react/src/tree-view.ts +++ b/packages/devextreme-react/src/tree-view.ts @@ -164,7 +164,6 @@ const Item = Object.assign(_componen type IOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; disabled?: boolean; elementAttr?: Record; focusStateEnabled?: boolean; @@ -213,7 +212,6 @@ const Options = Object.assign(_co type ISearchEditorOptionsProps = React.PropsWithChildren<{ accessKey?: string | undefined; activeStateEnabled?: boolean; - bindingOptions?: Record; buttons?: Array; disabled?: boolean; elementAttr?: Record; diff --git a/packages/devextreme-scss/icons/dxicons.ttf b/packages/devextreme-scss/icons/dxicons.ttf index 53f7b3672a18..806dc4490512 100644 Binary files a/packages/devextreme-scss/icons/dxicons.ttf and b/packages/devextreme-scss/icons/dxicons.ttf differ diff --git a/packages/devextreme-scss/icons/dxicons.woff b/packages/devextreme-scss/icons/dxicons.woff index cbfc54ac0624..ed603f25b59f 100644 Binary files a/packages/devextreme-scss/icons/dxicons.woff and b/packages/devextreme-scss/icons/dxicons.woff differ diff --git a/packages/devextreme-scss/icons/dxicons.woff2 b/packages/devextreme-scss/icons/dxicons.woff2 index 2b2b50fcf840..415a24593fac 100644 Binary files a/packages/devextreme-scss/icons/dxicons.woff2 and b/packages/devextreme-scss/icons/dxicons.woff2 differ diff --git a/packages/devextreme-scss/icons/dxiconsfluent.ttf b/packages/devextreme-scss/icons/dxiconsfluent.ttf index 28c6e64d6848..5f9df6993c86 100644 Binary files a/packages/devextreme-scss/icons/dxiconsfluent.ttf and b/packages/devextreme-scss/icons/dxiconsfluent.ttf differ diff --git a/packages/devextreme-scss/icons/dxiconsfluent.woff b/packages/devextreme-scss/icons/dxiconsfluent.woff index 032b92ad8bdc..2679ca251c07 100644 Binary files a/packages/devextreme-scss/icons/dxiconsfluent.woff and b/packages/devextreme-scss/icons/dxiconsfluent.woff differ diff --git a/packages/devextreme-scss/icons/dxiconsfluent.woff2 b/packages/devextreme-scss/icons/dxiconsfluent.woff2 index e5718b0eaf38..0415dad256b6 100644 Binary files a/packages/devextreme-scss/icons/dxiconsfluent.woff2 and b/packages/devextreme-scss/icons/dxiconsfluent.woff2 differ diff --git a/packages/devextreme-scss/icons/dxiconsmaterial.ttf b/packages/devextreme-scss/icons/dxiconsmaterial.ttf index 43e0dc06bf97..73bd833aa8f2 100644 Binary files a/packages/devextreme-scss/icons/dxiconsmaterial.ttf and b/packages/devextreme-scss/icons/dxiconsmaterial.ttf differ diff --git a/packages/devextreme-scss/icons/dxiconsmaterial.woff b/packages/devextreme-scss/icons/dxiconsmaterial.woff index a28711d3c5f2..8eea260bf157 100644 Binary files a/packages/devextreme-scss/icons/dxiconsmaterial.woff and b/packages/devextreme-scss/icons/dxiconsmaterial.woff differ diff --git a/packages/devextreme-scss/icons/dxiconsmaterial.woff2 b/packages/devextreme-scss/icons/dxiconsmaterial.woff2 index ab0487c71570..865ec4b8e42d 100644 Binary files a/packages/devextreme-scss/icons/dxiconsmaterial.woff2 and b/packages/devextreme-scss/icons/dxiconsmaterial.woff2 differ diff --git a/packages/devextreme-scss/images/icons/fluent/chatadd.svg b/packages/devextreme-scss/images/icons/fluent/chatadd.svg new file mode 100644 index 000000000000..574d811dd9b9 --- /dev/null +++ b/packages/devextreme-scss/images/icons/fluent/chatadd.svg @@ -0,0 +1,4 @@ + +chatadd + + diff --git a/packages/devextreme-scss/images/icons/fluent/clipboardpastesparkle.svg b/packages/devextreme-scss/images/icons/fluent/clipboardpastesparkle.svg new file mode 100644 index 000000000000..ddef134e6713 --- /dev/null +++ b/packages/devextreme-scss/images/icons/fluent/clipboardpastesparkle.svg @@ -0,0 +1,7 @@ + +clipboardpastesparkle + + + + + diff --git a/packages/devextreme-scss/images/icons/fluent/colordismiss.svg b/packages/devextreme-scss/images/icons/fluent/colordismiss.svg new file mode 100644 index 000000000000..065b76d2b153 --- /dev/null +++ b/packages/devextreme-scss/images/icons/fluent/colordismiss.svg @@ -0,0 +1,4 @@ + +colordismiss + + diff --git a/packages/devextreme-scss/images/icons/fluent/ratingfilled.svg b/packages/devextreme-scss/images/icons/fluent/ratingfilled.svg new file mode 100644 index 000000000000..1b1e74c76955 --- /dev/null +++ b/packages/devextreme-scss/images/icons/fluent/ratingfilled.svg @@ -0,0 +1,4 @@ + +ratingfilled + + diff --git a/packages/devextreme-scss/images/icons/fluent/ratingoutline.svg b/packages/devextreme-scss/images/icons/fluent/ratingoutline.svg new file mode 100644 index 000000000000..dc65c40ac03f --- /dev/null +++ b/packages/devextreme-scss/images/icons/fluent/ratingoutline.svg @@ -0,0 +1,4 @@ + +ratingoutline + + diff --git a/packages/devextreme-scss/images/icons/generic/chatadd.svg b/packages/devextreme-scss/images/icons/generic/chatadd.svg new file mode 100644 index 000000000000..3f5e15c7852d --- /dev/null +++ b/packages/devextreme-scss/images/icons/generic/chatadd.svg @@ -0,0 +1,4 @@ + +chatadd + + diff --git a/packages/devextreme-scss/images/icons/generic/clipboardpastesparkle.svg b/packages/devextreme-scss/images/icons/generic/clipboardpastesparkle.svg new file mode 100644 index 000000000000..cf096a718532 --- /dev/null +++ b/packages/devextreme-scss/images/icons/generic/clipboardpastesparkle.svg @@ -0,0 +1,5 @@ + +clipboardpastesparkle + + + diff --git a/packages/devextreme-scss/images/icons/generic/colordismiss.svg b/packages/devextreme-scss/images/icons/generic/colordismiss.svg new file mode 100644 index 000000000000..4de4527bf74c --- /dev/null +++ b/packages/devextreme-scss/images/icons/generic/colordismiss.svg @@ -0,0 +1,4 @@ + +colordismiss + + diff --git a/packages/devextreme-scss/images/icons/generic/ratingfilled.svg b/packages/devextreme-scss/images/icons/generic/ratingfilled.svg new file mode 100644 index 000000000000..b54fbdd51b3c --- /dev/null +++ b/packages/devextreme-scss/images/icons/generic/ratingfilled.svg @@ -0,0 +1,4 @@ + +ratingfilled + + diff --git a/packages/devextreme-scss/images/icons/generic/ratingoutline.svg b/packages/devextreme-scss/images/icons/generic/ratingoutline.svg new file mode 100644 index 000000000000..636c0332c968 --- /dev/null +++ b/packages/devextreme-scss/images/icons/generic/ratingoutline.svg @@ -0,0 +1,4 @@ + +ratingoutline + + diff --git a/packages/devextreme-scss/images/icons/material/chatadd.svg b/packages/devextreme-scss/images/icons/material/chatadd.svg new file mode 100644 index 000000000000..e3fdd69f971a --- /dev/null +++ b/packages/devextreme-scss/images/icons/material/chatadd.svg @@ -0,0 +1,4 @@ + +chatadd + + diff --git a/packages/devextreme-scss/images/icons/material/clipboardpastesparkle.svg b/packages/devextreme-scss/images/icons/material/clipboardpastesparkle.svg new file mode 100644 index 000000000000..944c603f2a21 --- /dev/null +++ b/packages/devextreme-scss/images/icons/material/clipboardpastesparkle.svg @@ -0,0 +1,5 @@ + +clipboardpastesparkle + + + diff --git a/packages/devextreme-scss/images/icons/material/colordismiss.svg b/packages/devextreme-scss/images/icons/material/colordismiss.svg new file mode 100644 index 000000000000..2e9bac3d0b26 --- /dev/null +++ b/packages/devextreme-scss/images/icons/material/colordismiss.svg @@ -0,0 +1,4 @@ + +colordismiss + + diff --git a/packages/devextreme-scss/images/icons/material/ratingfilled.svg b/packages/devextreme-scss/images/icons/material/ratingfilled.svg new file mode 100644 index 000000000000..2329ef433a90 --- /dev/null +++ b/packages/devextreme-scss/images/icons/material/ratingfilled.svg @@ -0,0 +1,4 @@ + +ratingfilled + + diff --git a/packages/devextreme-scss/images/icons/material/ratingoutline.svg b/packages/devextreme-scss/images/icons/material/ratingoutline.svg new file mode 100644 index 000000000000..29d571faae07 --- /dev/null +++ b/packages/devextreme-scss/images/icons/material/ratingoutline.svg @@ -0,0 +1,4 @@ + +ratingoutline + + diff --git a/packages/devextreme-scss/images/widgets/fluent/color-schemes/dark/colorbox/nocolor.png b/packages/devextreme-scss/images/widgets/fluent/color-schemes/dark/colorbox/nocolor.png deleted file mode 100644 index 086f50057cb8..000000000000 Binary files a/packages/devextreme-scss/images/widgets/fluent/color-schemes/dark/colorbox/nocolor.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/fluent/color-schemes/light/colorbox/nocolor.png b/packages/devextreme-scss/images/widgets/fluent/color-schemes/light/colorbox/nocolor.png deleted file mode 100644 index 086f50057cb8..000000000000 Binary files a/packages/devextreme-scss/images/widgets/fluent/color-schemes/light/colorbox/nocolor.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/contrast/colorbox/nocolor.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/contrast/colorbox/nocolor.png deleted file mode 100644 index 323150d4e46f..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/contrast/colorbox/nocolor.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/dark/colorbox/nocolor.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/dark/colorbox/nocolor.png deleted file mode 100644 index 937807aaea11..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/dark/colorbox/nocolor.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/colorbox/nocolor.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/light/colorbox/nocolor.png deleted file mode 100644 index 086f50057cb8..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/colorbox/nocolor.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/material/color-schemes/dark/colorbox/nocolor.png b/packages/devextreme-scss/images/widgets/material/color-schemes/dark/colorbox/nocolor.png deleted file mode 100644 index 086f50057cb8..000000000000 Binary files a/packages/devextreme-scss/images/widgets/material/color-schemes/dark/colorbox/nocolor.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/material/color-schemes/light/colorbox/nocolor.png b/packages/devextreme-scss/images/widgets/material/color-schemes/light/colorbox/nocolor.png deleted file mode 100644 index 086f50057cb8..000000000000 Binary files a/packages/devextreme-scss/images/widgets/material/color-schemes/light/colorbox/nocolor.png and /dev/null differ diff --git a/packages/devextreme-scss/scss/widgets/base/_form.scss b/packages/devextreme-scss/scss/widgets/base/_form.scss index 3888c6f7dc07..b5d0184139bf 100644 --- a/packages/devextreme-scss/scss/widgets/base/_form.scss +++ b/packages/devextreme-scss/scss/widgets/base/_form.scss @@ -166,3 +166,13 @@ unicode-bidi: embed; } } + +.dx-form { + position: relative; +} + +.dx-form-loadpanel-wrapper { + .dx-loadpanel-content { + padding: 0; + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/_icons.scss b/packages/devextreme-scss/scss/widgets/base/_icons.scss index 18d557b78bdc..0ea2f0daa8f5 100644 --- a/packages/devextreme-scss/scss/widgets/base/_icons.scss +++ b/packages/devextreme-scss/scss/widgets/base/_icons.scss @@ -153,7 +153,12 @@ .dx-icon-restore .dx-icon-groupbycolumn, .dx-icon-ungroupcolumn, -.dx-icon-ungroupallcolumns { +.dx-icon-ungroupallcolumns, +.dx-icon-ratingoutline, +.dx-icon-ratingfilled, +.dx-icon-chatadd, +.dx-icon-colordismiss, +.dx-icon-clipboardpastesparkle { background-position: 0 0; background-repeat: no-repeat; } @@ -499,6 +504,8 @@ $icons: ( "datausage": "\f177", "datapie": "\f178", "pinmap": "\f179", + "ratingoutline": "\f17f", + "ratingfilled": "\f180", "csv": "\f181", "packagebox": "\f182", "checkmarkcircle": "\f183", @@ -522,6 +529,9 @@ $icons: ( "groupbycolumn":"\f197", "ungroupcolumn":"\f198", "ungroupallcolumns":"\f199", + "chatadd":"\f200", + "colordismiss":"\f201", + "clipboardpastesparkle":"\f202", ); // stylelint-enable diff --git a/packages/devextreme-scss/scss/widgets/base/colorView/_index.scss b/packages/devextreme-scss/scss/widgets/base/colorView/_index.scss index d75c4d9d3279..a319803d51a4 100644 --- a/packages/devextreme-scss/scss/widgets/base/colorView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/colorView/_index.scss @@ -61,7 +61,7 @@ $colorview-handle-second-side-size: 17px; .dx-colorview-container-row { overflow: hidden; - padding-top: 1px; + padding: 1px 0; &:first-child { margin-top: 0; diff --git a/packages/devextreme-scss/scss/widgets/base/dataGrid/_index.scss b/packages/devextreme-scss/scss/widgets/base/dataGrid/_index.scss index a56ea5567769..d2bd7e0c3d01 100644 --- a/packages/devextreme-scss/scss/widgets/base/dataGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/dataGrid/_index.scss @@ -220,7 +220,6 @@ $datagrid-text-stub-background-image-path: null !default; padding-inline: 0; .dx-datagrid-summary-item { - background-color: $datagrid-base-background-color; padding-inline: $datagrid-cell-padding; } } diff --git a/packages/devextreme-scss/scss/widgets/fluent/colorBox/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/colorBox/_colors.scss index 89c53a85429e..4b437aa96502 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/colorBox/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/colorBox/_colors.scss @@ -4,3 +4,4 @@ // adduse +$colorbox-preview-icon-color: $base-icon-color !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/colorBox/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/colorBox/_index.scss index 9c62a07d90c3..befd71dc3a18 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/colorBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/colorBox/_index.scss @@ -21,13 +21,6 @@ padding-inline-start: $fluent-colorbox-preview-width; } - &.dx-colorbox-color-is-not-defined { - .dx-colorbox-color-result-preview { - background: $colorbox-preview-empty-bg no-repeat 0 0; - background-size: contain; - } - } - &::after { left: $fluent-colorbox-preview-padding + 1; } @@ -52,6 +45,16 @@ inset-inline-start: $fluent-colorbox-preview-padding; border: 1px solid; border-color: $colorview-border-color; + + .dx-icon-colordismiss { + color: $colorbox-preview-icon-color; + } + + .dx-state-disabled & { + .dx-icon-colordismiss { + color: $base-foreground-disabled; + } + } } .dx-colorbox-overlay { diff --git a/packages/devextreme-scss/scss/widgets/fluent/colorView/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/colorView/_colors.scss index 0f6210af73b8..0ac69e1bfb5d 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/colorView/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/colorView/_colors.scss @@ -15,13 +15,3 @@ $colorview-border-color: $base-border-color !default; $colorview-label-color: $base-label-color !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: $base-border-color !default; -$colorbox-preview-empty-bg: null !default; - - -@if $mode == "light" { - $colorbox-preview-empty-bg: data-uri('images/widgets/fluent/color-schemes/light/colorbox/nocolor.png') !default; -} - -@if $mode == "dark" { - $colorbox-preview-empty-bg: data-uri('images/widgets/fluent/color-schemes/dark/colorbox/nocolor.png') !default; -} diff --git a/packages/devextreme-scss/scss/widgets/generic/colorBox/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/colorBox/_colors.scss index 89c53a85429e..4b437aa96502 100644 --- a/packages/devextreme-scss/scss/widgets/generic/colorBox/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/colorBox/_colors.scss @@ -4,3 +4,4 @@ // adduse +$colorbox-preview-icon-color: $base-icon-color !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/colorBox/_index.scss b/packages/devextreme-scss/scss/widgets/generic/colorBox/_index.scss index fcc5af3e7d2f..5d8761b36d76 100644 --- a/packages/devextreme-scss/scss/widgets/generic/colorBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/colorBox/_index.scss @@ -22,14 +22,6 @@ } } -.dx-colorbox-input-container { - &.dx-colorbox-color-is-not-defined { - .dx-colorbox-color-result-preview { - background: $colorbox-preview-empty-bg no-repeat 0 0; - } - } -} - .dx-colorbox-color-result-preview { position: absolute; top: 50%; @@ -40,6 +32,11 @@ inset-inline-start: 13px; border: 1px solid; border-color: $colorview-border-color; + + .dx-icon-colordismiss { + font-size: 17px; + color: $colorbox-preview-icon-color; + } } .dx-colorbox-overlay { diff --git a/packages/devextreme-scss/scss/widgets/generic/colorView/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/colorView/_colors.scss index dbf15fe07ce6..92aa30f175d3 100644 --- a/packages/devextreme-scss/scss/widgets/generic/colorView/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/colorView/_colors.scss @@ -11,7 +11,6 @@ */ $colorbox-overlay-bg: null !default; $colorbox-palette-cell-bg: null !default; -$colorbox-preview-empty-bg: null !default; $colorview-handle-color: null !default; $colorview-handle-border-color: null !default; $colorview-border-color: $base-border-color !default; @@ -21,7 +20,6 @@ $colorview-label-color: $base-label-color !default; @if $color == "carmine" { $colorbox-overlay-bg: $overlay-content-bg !default; $colorbox-palette-cell-bg: $colorbox-overlay-bg !default; - $colorbox-preview-empty-bg: data-uri('images/widgets/generic/color-schemes/light/colorbox/nocolor.png') !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: color.change($base-inverted-bg, $alpha: 0.2) !default; } @@ -29,7 +27,6 @@ $colorview-label-color: $base-label-color !default; @if $color == "contrast" { $colorbox-overlay-bg: $base-bg !default; $colorbox-palette-cell-bg: $base-bg !default; - $colorbox-preview-empty-bg: data-uri('images/widgets/generic/color-schemes/contrast/colorbox/nocolor.png') !default; $colorview-handle-color: $base-inverted-bg !default; $colorview-handle-border-color: color.change($base-border-color, $alpha: 0.2) !default; } @@ -37,7 +34,6 @@ $colorview-label-color: $base-label-color !default; @if $color == "dark" { $colorbox-overlay-bg: $overlay-content-bg !default; $colorbox-palette-cell-bg: $colorbox-overlay-bg !default; - $colorbox-preview-empty-bg: data-uri('images/widgets/generic/color-schemes/dark/colorbox/nocolor.png') !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: color.change($base-inverted-bg, $alpha: 0.2) !default; } @@ -45,7 +41,6 @@ $colorview-label-color: $base-label-color !default; @if $color == "darkmoon" { $colorbox-overlay-bg: $overlay-content-bg !default; $colorbox-palette-cell-bg: $colorbox-overlay-bg !default; - $colorbox-preview-empty-bg: data-uri('images/widgets/generic/color-schemes/dark/colorbox/nocolor.png') !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: color.change($base-inverted-bg, $alpha: 0.2) !default; } @@ -53,7 +48,6 @@ $colorview-label-color: $base-label-color !default; @if $color == "darkviolet" { $colorbox-overlay-bg: $overlay-content-bg !default; $colorbox-palette-cell-bg: $colorbox-overlay-bg !default; - $colorbox-preview-empty-bg: data-uri('images/widgets/generic/color-schemes/dark/colorbox/nocolor.png') !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: color.change($base-inverted-bg, $alpha: 0.2) !default; } @@ -61,7 +55,6 @@ $colorview-label-color: $base-label-color !default; @if $color == "greenmist" { $colorbox-overlay-bg: $overlay-content-bg !default; $colorbox-palette-cell-bg: $colorbox-overlay-bg !default; - $colorbox-preview-empty-bg: data-uri('images/widgets/generic/color-schemes/light/colorbox/nocolor.png') !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: color.change($base-inverted-bg, $alpha: 0.2) !default; } @@ -69,7 +62,6 @@ $colorview-label-color: $base-label-color !default; @if $color == "light" { $colorbox-overlay-bg: $overlay-content-bg !default; $colorbox-palette-cell-bg: $colorbox-overlay-bg !default; - $colorbox-preview-empty-bg: data-uri('images/widgets/generic/color-schemes/light/colorbox/nocolor.png') !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: color.change($base-inverted-bg, $alpha: 0.2) !default; } @@ -77,7 +69,6 @@ $colorview-label-color: $base-label-color !default; @if $color == "softblue" { $colorbox-overlay-bg: $overlay-content-bg !default; $colorbox-palette-cell-bg: $colorbox-overlay-bg !default; - $colorbox-preview-empty-bg: data-uri('images/widgets/generic/color-schemes/light/colorbox/nocolor.png') !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: color.change($base-inverted-bg, $alpha: 0.2) !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/colorBox/_colors.scss b/packages/devextreme-scss/scss/widgets/material/colorBox/_colors.scss index 89c53a85429e..4b437aa96502 100644 --- a/packages/devextreme-scss/scss/widgets/material/colorBox/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/colorBox/_colors.scss @@ -4,3 +4,4 @@ // adduse +$colorbox-preview-icon-color: $base-icon-color !default; diff --git a/packages/devextreme-scss/scss/widgets/material/colorBox/_index.scss b/packages/devextreme-scss/scss/widgets/material/colorBox/_index.scss index 5c5af11eacd8..23093f1483d2 100644 --- a/packages/devextreme-scss/scss/widgets/material/colorBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/colorBox/_index.scss @@ -20,12 +20,6 @@ padding-inline-start: $material-colorbox-preview-width; } - &.dx-colorbox-color-is-not-defined { - .dx-colorbox-color-result-preview { - background: $colorbox-preview-empty-bg no-repeat 0 0; - } - } - &::after { left: $material-colorbox-preview-left + 1; } @@ -51,6 +45,11 @@ border: 1px solid; border-color: $colorview-border-color; left: $material-colorbox-preview-left; + + .dx-icon-colordismiss { + font-size: 17px; + color: $colorbox-preview-icon-color; + } } .dx-colorbox-overlay { diff --git a/packages/devextreme-scss/scss/widgets/material/colorView/_colors.scss b/packages/devextreme-scss/scss/widgets/material/colorView/_colors.scss index 57118b3ca3f1..22d637a25254 100644 --- a/packages/devextreme-scss/scss/widgets/material/colorView/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/colorView/_colors.scss @@ -15,13 +15,3 @@ $colorview-border-color: $base-border-color !default; $colorview-label-color: $base-label-color !default; $colorview-handle-color: $base-bg !default; $colorview-handle-border-color: color.change($base-inverted-bg, $alpha: 0.2) !default; -$colorbox-preview-empty-bg: null !default; - - -@if $mode == "light" { - $colorbox-preview-empty-bg: data-uri('images/widgets/material/color-schemes/light/colorbox/nocolor.png') !default; -} - -@if $mode == "dark" { - $colorbox-preview-empty-bg: data-uri('images/widgets/material/color-schemes/dark/colorbox/nocolor.png') !default; -} diff --git a/packages/devextreme-scss/tests/.eslintrc.js b/packages/devextreme-scss/tests/.eslintrc.js deleted file mode 100644 index 0bef3bab12f0..000000000000 --- a/packages/devextreme-scss/tests/.eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-env node */ -/* eslint-disable spellcheck/spell-checker */ -module.exports = { - overrides: [ - { - files: ['*'], - parserOptions: { - project: true, - }, - }, - ], -}; diff --git a/packages/devextreme-vue/package.json b/packages/devextreme-vue/package.json index c297d468af6d..9a5115ed1096 100644 --- a/packages/devextreme-vue/package.json +++ b/packages/devextreme-vue/package.json @@ -6,6 +6,7 @@ "type": "git", "url": "https://github.com/DevExpress/devextreme-vue.git" }, + "sideEffects": false, "main": "./cjs/index.js", "module": "./esm/index.js", "types": "./cjs/index.d.ts", diff --git a/packages/devextreme-vue/src/autocomplete.ts b/packages/devextreme-vue/src/autocomplete.ts index 15337792b36c..2e4c7a1a9c9c 100644 --- a/packages/devextreme-vue/src/autocomplete.ts +++ b/packages/devextreme-vue/src/autocomplete.ts @@ -460,7 +460,6 @@ const DxDropDownOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -511,7 +510,6 @@ const DxDropDownOptionsConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -709,7 +707,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -736,7 +733,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, diff --git a/packages/devextreme-vue/src/card-view.ts b/packages/devextreme-vue/src/card-view.ts index a0d05557cd52..ed3223e740ab 100644 --- a/packages/devextreme-vue/src/card-view.ts +++ b/packages/devextreme-vue/src/card-view.ts @@ -152,6 +152,7 @@ import { } from "devextreme/ui/button"; import { FormItemType, + FormPredefinedButtonItem, dxFormSimpleItem, dxFormOptions, dxFormGroupItem, @@ -166,6 +167,8 @@ import { FieldDataChangedEvent, InitializedEvent as FormInitializedEvent, OptionChangedEvent as FormOptionChangedEvent, + SmartPastedEvent, + SmartPastingEvent, FormItemComponent, } from "devextreme/ui/form"; import { @@ -175,6 +178,9 @@ import { import { Format, } from "devextreme/common/core/localization"; +import { + AIIntegration, +} from "devextreme/common/ai-integration"; import { dxTabPanelOptions, dxTabPanelItem, @@ -481,6 +487,25 @@ prepareComponentConfig(componentConfig); const DxCardView = defineComponent(componentConfig); +const DxAiOptionsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:disabled": null, + "update:instruction": null, + }, + props: { + disabled: Boolean, + instruction: String + } +}; + +prepareConfigurationComponentConfig(DxAiOptionsConfig); + +const DxAiOptions = defineComponent(DxAiOptionsConfig); + +(DxAiOptions as any).$_optionName = "aiOptions"; + const DxAnimationConfig = { emits: { "update:isActive": null, @@ -591,7 +616,7 @@ const DxButtonItemConfig = { cssClass: String, horizontalAlignment: String as PropType, itemType: String as PropType, - name: String, + name: String as PropType, verticalAlignment: String as PropType, visible: Boolean, visibleIndex: Number @@ -617,7 +642,6 @@ const DxButtonOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -644,7 +668,6 @@ const DxButtonOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, @@ -1496,7 +1519,6 @@ const DxFilterBuilderConfig = { "update:accessKey": null, "update:activeStateEnabled": null, "update:allowHierarchicalFields": null, - "update:bindingOptions": null, "update:customOperations": null, "update:disabled": null, "update:elementAttr": null, @@ -1526,7 +1548,6 @@ const DxFilterBuilderConfig = { accessKey: String, activeStateEnabled: Boolean, allowHierarchicalFields: Boolean, - bindingOptions: Object as PropType>, customOperations: Array as PropType>, disabled: Boolean, elementAttr: Object as PropType>, @@ -1661,9 +1682,9 @@ const DxFormConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, + "update:aiIntegration": null, "update:alignItemLabels": null, "update:alignItemLabelsInAllGroups": null, - "update:bindingOptions": null, "update:colCount": null, "update:colCountByScreen": null, "update:customizeItem": null, @@ -1685,6 +1706,8 @@ const DxFormConfig = { "update:onFieldDataChanged": null, "update:onInitialized": null, "update:onOptionChanged": null, + "update:onSmartPasted": null, + "update:onSmartPasting": null, "update:optionalMark": null, "update:readOnly": null, "update:requiredMark": null, @@ -1704,9 +1727,9 @@ const DxFormConfig = { props: { accessKey: String, activeStateEnabled: Boolean, + aiIntegration: Object as PropType, alignItemLabels: Boolean, alignItemLabelsInAllGroups: Boolean, - bindingOptions: Object as PropType>, colCount: [String, Number] as PropType, colCountByScreen: Object as PropType>, customizeItem: Function as PropType<((item: dxFormSimpleItem | dxFormGroupItem | dxFormTabbedItem | dxFormEmptyItem | dxFormButtonItem) => void)>, @@ -1728,6 +1751,8 @@ const DxFormConfig = { onFieldDataChanged: Function as PropType<((e: FieldDataChangedEvent) => void)>, onInitialized: Function as PropType<((e: FormInitializedEvent) => void)>, onOptionChanged: Function as PropType<((e: FormOptionChangedEvent) => void)>, + onSmartPasted: Function as PropType<((e: SmartPastedEvent) => void)>, + onSmartPasting: Function as PropType<((e: SmartPastingEvent) => void)>, optionalMark: String, readOnly: Boolean, requiredMark: String, @@ -1792,6 +1817,7 @@ const DxFormItemConfig = { emits: { "update:isActive": null, "update:hoveredElement": null, + "update:aiOptions": null, "update:colSpan": null, "update:cssClass": null, "update:dataField": null, @@ -1808,6 +1834,7 @@ const DxFormItemConfig = { "update:visibleIndex": null, }, props: { + aiOptions: Object as PropType>, colSpan: Number, cssClass: String, dataField: String, @@ -1831,6 +1858,7 @@ const DxFormItem = defineComponent(DxFormItemConfig); (DxFormItem as any).$_optionName = "formItem"; (DxFormItem as any).$_expectedChildren = { + aiOptions: { isCollectionItem: false, optionName: "aiOptions" }, AsyncRule: { isCollectionItem: true, optionName: "validationRules" }, CompareRule: { isCollectionItem: true, optionName: "validationRules" }, CustomRule: { isCollectionItem: true, optionName: "validationRules" }, @@ -2055,6 +2083,7 @@ const DxItemConfig = { emits: { "update:isActive": null, "update:hoveredElement": null, + "update:aiOptions": null, "update:alignItemLabels": null, "update:badge": null, "update:buttonOptions": null, @@ -2095,6 +2124,7 @@ const DxItemConfig = { "update:widget": null, }, props: { + aiOptions: Object as PropType>, alignItemLabels: Boolean, badge: String, buttonOptions: Object as PropType>, @@ -2119,7 +2149,7 @@ const DxItemConfig = { locateInMenu: String as PropType, location: String as PropType, menuItemTemplate: {}, - name: String as PropType, + name: String as PropType, options: {}, showText: String as PropType, tabPanelOptions: Object as PropType>, @@ -2143,6 +2173,7 @@ const DxItem = defineComponent(DxItemConfig); (DxItem as any).$_optionName = "items"; (DxItem as any).$_isCollectionItem = true; (DxItem as any).$_expectedChildren = { + aiOptions: { isCollectionItem: false, optionName: "aiOptions" }, AsyncRule: { isCollectionItem: true, optionName: "validationRules" }, buttonOptions: { isCollectionItem: false, optionName: "buttonOptions" }, colCountByScreen: { isCollectionItem: false, optionName: "colCountByScreen" }, @@ -2192,7 +2223,6 @@ const DxLoadPanelConfig = { "update:isActive": null, "update:hoveredElement": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:deferRendering": null, "update:delay": null, @@ -2228,7 +2258,6 @@ const DxLoadPanelConfig = { }, props: { animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, deferRendering: Boolean, delay: Number, @@ -2701,6 +2730,7 @@ const DxSimpleItemConfig = { emits: { "update:isActive": null, "update:hoveredElement": null, + "update:aiOptions": null, "update:colSpan": null, "update:cssClass": null, "update:dataField": null, @@ -2717,6 +2747,7 @@ const DxSimpleItemConfig = { "update:visibleIndex": null, }, props: { + aiOptions: Object as PropType>, colSpan: Number, cssClass: String, dataField: String, @@ -2744,6 +2775,7 @@ const DxSimpleItem = defineComponent(DxSimpleItemConfig); itemType: "simple" }; (DxSimpleItem as any).$_expectedChildren = { + aiOptions: { isCollectionItem: false, optionName: "aiOptions" }, AsyncRule: { isCollectionItem: true, optionName: "validationRules" }, CompareRule: { isCollectionItem: true, optionName: "validationRules" }, CustomRule: { isCollectionItem: true, optionName: "validationRules" }, @@ -2898,7 +2930,6 @@ const DxTabPanelOptionsConfig = { "update:accessKey": null, "update:activeStateEnabled": null, "update:animationEnabled": null, - "update:bindingOptions": null, "update:dataSource": null, "update:deferRendering": null, "update:disabled": null, @@ -2946,7 +2977,6 @@ const DxTabPanelOptionsConfig = { accessKey: String, activeStateEnabled: Boolean, animationEnabled: Boolean, - bindingOptions: Object as PropType>, dataSource: [Array, Object, String] as PropType<(Array) | DataSource | DataSourceOptions | null | Store | string | Record>, deferRendering: Boolean, disabled: Boolean, @@ -3216,6 +3246,7 @@ const DxValidationRule = defineComponent(DxValidationRuleConfig); export default DxCardView; export { DxCardView, + DxAiOptions, DxAnimation, DxAsyncRule, DxAt, diff --git a/packages/devextreme-vue/src/chat.ts b/packages/devextreme-vue/src/chat.ts index f71ee209f1eb..04d3b76b1062 100644 --- a/packages/devextreme-vue/src/chat.ts +++ b/packages/devextreme-vue/src/chat.ts @@ -44,6 +44,7 @@ type AccessibleOptions = Pick>, elementAttr: Object as PropType>, + emptyViewTemplate: {}, focusStateEnabled: Boolean, height: [Number, String], hint: String, @@ -130,6 +132,7 @@ const componentConfig = { "update:disabled": null, "update:editing": null, "update:elementAttr": null, + "update:emptyViewTemplate": null, "update:focusStateEnabled": null, "update:height": null, "update:hint": null, diff --git a/packages/devextreme-vue/src/color-box.ts b/packages/devextreme-vue/src/color-box.ts index ec7d02f0ac78..9a44e881453e 100644 --- a/packages/devextreme-vue/src/color-box.ts +++ b/packages/devextreme-vue/src/color-box.ts @@ -26,6 +26,7 @@ import { } from "devextreme/common"; import { DropDownPredefinedButton, + FieldAddons, } from "devextreme/ui/drop_down_editor/ui.drop_down_editor"; import { dxPopupOptions, @@ -95,6 +96,7 @@ type AccessibleOptions = Pick | Record>, editAlphaChannel: Boolean, elementAttr: Object as PropType>, + fieldAddons: Object as PropType>, fieldTemplate: {}, focusStateEnabled: Boolean, height: [Number, String], @@ -225,6 +228,7 @@ const componentConfig = { "update:dropDownOptions": null, "update:editAlphaChannel": null, "update:elementAttr": null, + "update:fieldAddons": null, "update:fieldTemplate": null, "update:focusStateEnabled": null, "update:height": null, @@ -283,7 +287,8 @@ const componentConfig = { (this as any).$_hasAsyncTemplate = true; (this as any).$_expectedChildren = { button: { isCollectionItem: true, optionName: "buttons" }, - dropDownOptions: { isCollectionItem: false, optionName: "dropDownOptions" } + dropDownOptions: { isCollectionItem: false, optionName: "dropDownOptions" }, + fieldAddons: { isCollectionItem: false, optionName: "fieldAddons" } }; } }; @@ -404,7 +409,6 @@ const DxDropDownOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -455,7 +459,6 @@ const DxDropDownOptionsConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -516,6 +519,25 @@ const DxDropDownOptions = defineComponent(DxDropDownOptionsConfig); toolbarItem: { isCollectionItem: true, optionName: "toolbarItems" } }; +const DxFieldAddonsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:afterTemplate": null, + "update:beforeTemplate": null, + }, + props: { + afterTemplate: {}, + beforeTemplate: {} + } +}; + +prepareConfigurationComponentConfig(DxFieldAddonsConfig); + +const DxFieldAddons = defineComponent(DxFieldAddonsConfig); + +(DxFieldAddons as any).$_optionName = "fieldAddons"; + const DxFromConfig = { emits: { "update:isActive": null, @@ -627,7 +649,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -654,7 +675,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, @@ -840,6 +860,7 @@ export { DxButton, DxCollision, DxDropDownOptions, + DxFieldAddons, DxFrom, DxHide, DxMy, diff --git a/packages/devextreme-vue/src/data-grid.ts b/packages/devextreme-vue/src/data-grid.ts index 63e4aa970b42..d1db9b189b74 100644 --- a/packages/devextreme-vue/src/data-grid.ts +++ b/packages/devextreme-vue/src/data-grid.ts @@ -179,6 +179,8 @@ import { FieldDataChangedEvent, InitializedEvent as FormInitializedEvent, OptionChangedEvent as FormOptionChangedEvent, + SmartPastedEvent, + SmartPastingEvent, FormItemComponent, FormItemType, } from "devextreme/ui/form"; @@ -191,6 +193,9 @@ import { import { Component, } from "devextreme/core/component"; +import { + AIIntegration, +} from "devextreme/common/ai-integration"; import { LocateInMenuMode, ShowTextMode, @@ -607,6 +612,25 @@ prepareComponentConfig(componentConfig); const DxDataGrid = defineComponent(componentConfig); +const DxAiOptionsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:disabled": null, + "update:instruction": null, + }, + props: { + disabled: Boolean, + instruction: String + } +}; + +prepareConfigurationComponentConfig(DxAiOptionsConfig); + +const DxAiOptions = defineComponent(DxAiOptionsConfig); + +(DxAiOptions as any).$_optionName = "aiOptions"; + const DxAnimationConfig = { emits: { "update:isActive": null, @@ -1642,7 +1666,6 @@ const DxFilterBuilderConfig = { "update:accessKey": null, "update:activeStateEnabled": null, "update:allowHierarchicalFields": null, - "update:bindingOptions": null, "update:customOperations": null, "update:disabled": null, "update:elementAttr": null, @@ -1672,7 +1695,6 @@ const DxFilterBuilderConfig = { accessKey: String, activeStateEnabled: Boolean, allowHierarchicalFields: Boolean, - bindingOptions: Object as PropType>, customOperations: Array as PropType>, disabled: Boolean, elementAttr: Object as PropType>, @@ -1718,7 +1740,6 @@ const DxFilterBuilderPopupConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -1769,7 +1790,6 @@ const DxFilterBuilderPopupConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -1961,9 +1981,9 @@ const DxFormConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, + "update:aiIntegration": null, "update:alignItemLabels": null, "update:alignItemLabelsInAllGroups": null, - "update:bindingOptions": null, "update:colCount": null, "update:colCountByScreen": null, "update:customizeItem": null, @@ -1985,6 +2005,8 @@ const DxFormConfig = { "update:onFieldDataChanged": null, "update:onInitialized": null, "update:onOptionChanged": null, + "update:onSmartPasted": null, + "update:onSmartPasting": null, "update:optionalMark": null, "update:readOnly": null, "update:requiredMark": null, @@ -2004,9 +2026,9 @@ const DxFormConfig = { props: { accessKey: String, activeStateEnabled: Boolean, + aiIntegration: Object as PropType, alignItemLabels: Boolean, alignItemLabelsInAllGroups: Boolean, - bindingOptions: Object as PropType>, colCount: [String, Number] as PropType, colCountByScreen: Object as PropType>, customizeItem: Function as PropType<((item: dxFormSimpleItem | dxFormGroupItem | dxFormTabbedItem | dxFormEmptyItem | dxFormButtonItem) => void)>, @@ -2028,6 +2050,8 @@ const DxFormConfig = { onFieldDataChanged: Function as PropType<((e: FieldDataChangedEvent) => void)>, onInitialized: Function as PropType<((e: FormInitializedEvent) => void)>, onOptionChanged: Function as PropType<((e: FormOptionChangedEvent) => void)>, + onSmartPasted: Function as PropType<((e: SmartPastedEvent) => void)>, + onSmartPasting: Function as PropType<((e: SmartPastingEvent) => void)>, optionalMark: String, readOnly: Boolean, requiredMark: String, @@ -2086,6 +2110,7 @@ const DxFormItemConfig = { emits: { "update:isActive": null, "update:hoveredElement": null, + "update:aiOptions": null, "update:colSpan": null, "update:cssClass": null, "update:dataField": null, @@ -2102,6 +2127,7 @@ const DxFormItemConfig = { "update:visibleIndex": null, }, props: { + aiOptions: Object as PropType>, colSpan: Number, cssClass: String, dataField: String, @@ -2125,6 +2151,7 @@ const DxFormItem = defineComponent(DxFormItemConfig); (DxFormItem as any).$_optionName = "formItem"; (DxFormItem as any).$_expectedChildren = { + aiOptions: { isCollectionItem: false, optionName: "aiOptions" }, AsyncRule: { isCollectionItem: true, optionName: "validationRules" }, CompareRule: { isCollectionItem: true, optionName: "validationRules" }, CustomRule: { isCollectionItem: true, optionName: "validationRules" }, @@ -2765,7 +2792,6 @@ const DxPopupConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -2816,7 +2842,6 @@ const DxPopupConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -3685,6 +3710,7 @@ const DxValueFormat = defineComponent(DxValueFormatConfig); export default DxDataGrid; export { DxDataGrid, + DxAiOptions, DxAnimation, DxAsyncRule, DxAt, diff --git a/packages/devextreme-vue/src/date-box.ts b/packages/devextreme-vue/src/date-box.ts index 7c7a1cc0b627..a5957c5a99eb 100644 --- a/packages/devextreme-vue/src/date-box.ts +++ b/packages/devextreme-vue/src/date-box.ts @@ -455,7 +455,6 @@ const DxCalendarOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:cellTemplate": null, "update:dateSerializationFormat": null, "update:disabled": null, @@ -499,7 +498,6 @@ const DxCalendarOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, cellTemplate: {}, dateSerializationFormat: String, disabled: Boolean, @@ -600,7 +598,6 @@ const DxDropDownOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -651,7 +648,6 @@ const DxDropDownOptionsConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -823,7 +819,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -850,7 +845,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, diff --git a/packages/devextreme-vue/src/date-range-box.ts b/packages/devextreme-vue/src/date-range-box.ts index 64d8ffe709ab..ff5f88b085c1 100644 --- a/packages/devextreme-vue/src/date-range-box.ts +++ b/packages/devextreme-vue/src/date-range-box.ts @@ -464,7 +464,6 @@ const DxCalendarOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:cellTemplate": null, "update:dateSerializationFormat": null, "update:disabled": null, @@ -508,7 +507,6 @@ const DxCalendarOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, cellTemplate: {}, dateSerializationFormat: String, disabled: Boolean, @@ -609,7 +607,6 @@ const DxDropDownOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -660,7 +657,6 @@ const DxDropDownOptionsConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -832,7 +828,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -859,7 +854,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, diff --git a/packages/devextreme-vue/src/drop-down-box.ts b/packages/devextreme-vue/src/drop-down-box.ts index dd1a6a395c1b..9fcc93346eb6 100644 --- a/packages/devextreme-vue/src/drop-down-box.ts +++ b/packages/devextreme-vue/src/drop-down-box.ts @@ -8,6 +8,7 @@ import DOMComponent from "devextreme/core/dom_component"; import dxPopup from "devextreme/ui/popup"; import { DropDownPredefinedButton, + FieldAddons, } from "devextreme/ui/drop_down_editor/ui.drop_down_editor"; import { TextEditorButton, @@ -101,6 +102,7 @@ type AccessibleOptions = Pick | Record>, elementAttr: Object as PropType>, + fieldAddons: Object as PropType>, fieldTemplate: {}, focusStateEnabled: Boolean, height: [Number, String], @@ -237,6 +240,7 @@ const componentConfig = { "update:dropDownButtonTemplate": null, "update:dropDownOptions": null, "update:elementAttr": null, + "update:fieldAddons": null, "update:fieldTemplate": null, "update:focusStateEnabled": null, "update:height": null, @@ -298,7 +302,8 @@ const componentConfig = { (this as any).$_hasAsyncTemplate = true; (this as any).$_expectedChildren = { button: { isCollectionItem: true, optionName: "buttons" }, - dropDownOptions: { isCollectionItem: false, optionName: "dropDownOptions" } + dropDownOptions: { isCollectionItem: false, optionName: "dropDownOptions" }, + fieldAddons: { isCollectionItem: false, optionName: "fieldAddons" } }; } }; @@ -419,7 +424,6 @@ const DxDropDownOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -470,7 +474,6 @@ const DxDropDownOptionsConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -531,6 +534,25 @@ const DxDropDownOptions = defineComponent(DxDropDownOptionsConfig); toolbarItem: { isCollectionItem: true, optionName: "toolbarItems" } }; +const DxFieldAddonsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:afterTemplate": null, + "update:beforeTemplate": null, + }, + props: { + afterTemplate: {}, + beforeTemplate: {} + } +}; + +prepareConfigurationComponentConfig(DxFieldAddonsConfig); + +const DxFieldAddons = defineComponent(DxFieldAddonsConfig); + +(DxFieldAddons as any).$_optionName = "fieldAddons"; + const DxFromConfig = { emits: { "update:isActive": null, @@ -642,7 +664,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -669,7 +690,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, @@ -855,6 +875,7 @@ export { DxButton, DxCollision, DxDropDownOptions, + DxFieldAddons, DxFrom, DxHide, DxMy, diff --git a/packages/devextreme-vue/src/drop-down-button.ts b/packages/devextreme-vue/src/drop-down-button.ts index 84976224aca1..86f556f124c6 100644 --- a/packages/devextreme-vue/src/drop-down-button.ts +++ b/packages/devextreme-vue/src/drop-down-button.ts @@ -303,7 +303,6 @@ const DxDropDownOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -354,7 +353,6 @@ const DxDropDownOptionsConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, diff --git a/packages/devextreme-vue/src/form.ts b/packages/devextreme-vue/src/form.ts index 8c7686123377..84a79262c7c2 100644 --- a/packages/devextreme-vue/src/form.ts +++ b/packages/devextreme-vue/src/form.ts @@ -3,6 +3,9 @@ import { defineComponent } from "vue"; import { prepareComponentConfig } from "./core/index"; import Form, { Properties } from "devextreme/ui/form"; import DataSource from "devextreme/data/data_source"; +import { + AIIntegration, +} from "devextreme/common/ai-integration"; import { Mode, ValidationRuleType, @@ -29,7 +32,10 @@ import { FieldDataChangedEvent, InitializedEvent, OptionChangedEvent, + SmartPastedEvent, + SmartPastingEvent, FormItemType, + FormPredefinedButtonItem, FormItemComponent, } from "devextreme/ui/form"; import { @@ -69,6 +75,7 @@ import { prepareConfigurationComponentConfig } from "./core/index"; type AccessibleOptions = Pick, alignItemLabels: Boolean, alignItemLabelsInAllGroups: Boolean, colCount: [String, Number] as PropType, @@ -140,6 +150,8 @@ const componentConfig = { onFieldDataChanged: Function as PropType<((e: FieldDataChangedEvent) => void)>, onInitialized: Function as PropType<((e: InitializedEvent) => void)>, onOptionChanged: Function as PropType<((e: OptionChangedEvent) => void)>, + onSmartPasted: Function as PropType<((e: SmartPastedEvent) => void)>, + onSmartPasting: Function as PropType<((e: SmartPastingEvent) => void)>, optionalMark: String, readOnly: Boolean, requiredMark: String, @@ -161,6 +173,7 @@ const componentConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, + "update:aiIntegration": null, "update:alignItemLabels": null, "update:alignItemLabelsInAllGroups": null, "update:colCount": null, @@ -184,6 +197,8 @@ const componentConfig = { "update:onFieldDataChanged": null, "update:onInitialized": null, "update:onOptionChanged": null, + "update:onSmartPasted": null, + "update:onSmartPasting": null, "update:optionalMark": null, "update:readOnly": null, "update:requiredMark": null, @@ -225,6 +240,25 @@ prepareComponentConfig(componentConfig); const DxForm = defineComponent(componentConfig); +const DxAiOptionsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:disabled": null, + "update:instruction": null, + }, + props: { + disabled: Boolean, + instruction: String + } +}; + +prepareConfigurationComponentConfig(DxAiOptionsConfig); + +const DxAiOptions = defineComponent(DxAiOptionsConfig); + +(DxAiOptions as any).$_optionName = "aiOptions"; + const DxAsyncRuleConfig = { emits: { "update:isActive": null, @@ -274,7 +308,7 @@ const DxButtonItemConfig = { cssClass: String, horizontalAlignment: String as PropType, itemType: String as PropType, - name: String, + name: String as PropType, verticalAlignment: String as PropType, visible: Boolean, visibleIndex: Number @@ -300,7 +334,6 @@ const DxButtonOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -327,7 +360,6 @@ const DxButtonOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, @@ -548,6 +580,7 @@ const DxItemConfig = { emits: { "update:isActive": null, "update:hoveredElement": null, + "update:aiOptions": null, "update:alignItemLabels": null, "update:badge": null, "update:buttonOptions": null, @@ -582,6 +615,7 @@ const DxItemConfig = { "update:visibleIndex": null, }, props: { + aiOptions: Object as PropType>, alignItemLabels: Boolean, badge: String, buttonOptions: Object as PropType>, @@ -603,7 +637,7 @@ const DxItemConfig = { items: Array as PropType>, itemType: String as PropType, label: Object as PropType>, - name: String, + name: String as PropType, tabPanelOptions: Object as PropType>, tabs: Array as PropType>>, tabTemplate: {}, @@ -624,6 +658,7 @@ const DxItem = defineComponent(DxItemConfig); (DxItem as any).$_optionName = "items"; (DxItem as any).$_isCollectionItem = true; (DxItem as any).$_expectedChildren = { + aiOptions: { isCollectionItem: false, optionName: "aiOptions" }, AsyncRule: { isCollectionItem: true, optionName: "validationRules" }, buttonOptions: { isCollectionItem: false, optionName: "buttonOptions" }, colCountByScreen: { isCollectionItem: false, optionName: "colCountByScreen" }, @@ -780,6 +815,7 @@ const DxSimpleItemConfig = { emits: { "update:isActive": null, "update:hoveredElement": null, + "update:aiOptions": null, "update:colSpan": null, "update:cssClass": null, "update:dataField": null, @@ -796,6 +832,7 @@ const DxSimpleItemConfig = { "update:visibleIndex": null, }, props: { + aiOptions: Object as PropType>, colSpan: Number, cssClass: String, dataField: String, @@ -823,6 +860,7 @@ const DxSimpleItem = defineComponent(DxSimpleItemConfig); itemType: "simple" }; (DxSimpleItem as any).$_expectedChildren = { + aiOptions: { isCollectionItem: false, optionName: "aiOptions" }, AsyncRule: { isCollectionItem: true, optionName: "validationRules" }, CompareRule: { isCollectionItem: true, optionName: "validationRules" }, CustomRule: { isCollectionItem: true, optionName: "validationRules" }, @@ -952,7 +990,6 @@ const DxTabPanelOptionsConfig = { "update:accessKey": null, "update:activeStateEnabled": null, "update:animationEnabled": null, - "update:bindingOptions": null, "update:dataSource": null, "update:deferRendering": null, "update:disabled": null, @@ -1000,7 +1037,6 @@ const DxTabPanelOptionsConfig = { accessKey: String, activeStateEnabled: Boolean, animationEnabled: Boolean, - bindingOptions: Object as PropType>, dataSource: [Array, Object, String] as PropType<(Array) | DataSource | DataSourceOptions | null | Store | string | Record>, deferRendering: Boolean, disabled: Boolean, @@ -1134,6 +1170,7 @@ const DxValidationRule = defineComponent(DxValidationRuleConfig); export default DxForm; export { DxForm, + DxAiOptions, DxAsyncRule, DxButtonItem, DxButtonOptions, diff --git a/packages/devextreme-vue/src/html-editor.ts b/packages/devextreme-vue/src/html-editor.ts index 235839d5b88e..32d125827096 100644 --- a/packages/devextreme-vue/src/html-editor.ts +++ b/packages/devextreme-vue/src/html-editor.ts @@ -294,7 +294,6 @@ const DxFileUploaderOptionsConfig = { "update:activeStateEnabled": null, "update:allowCanceling": null, "update:allowedFileExtensions": null, - "update:bindingOptions": null, "update:chunkSize": null, "update:dialogTrigger": null, "update:disabled": null, @@ -361,7 +360,6 @@ const DxFileUploaderOptionsConfig = { activeStateEnabled: Boolean, allowCanceling: Boolean, allowedFileExtensions: Array as PropType>, - bindingOptions: Object as PropType>, chunkSize: Number, dialogTrigger: {}, disabled: Boolean, diff --git a/packages/devextreme-vue/src/list.ts b/packages/devextreme-vue/src/list.ts index d75fb8db400d..47113d5ea735 100644 --- a/packages/devextreme-vue/src/list.ts +++ b/packages/devextreme-vue/src/list.ts @@ -438,7 +438,6 @@ const DxItemDraggingConfig = { "update:allowDropInsideItem": null, "update:allowReordering": null, "update:autoScroll": null, - "update:bindingOptions": null, "update:boundary": null, "update:container": null, "update:cursorOffset": null, @@ -472,7 +471,6 @@ const DxItemDraggingConfig = { allowDropInsideItem: Boolean, allowReordering: Boolean, autoScroll: Boolean, - bindingOptions: Object as PropType>, boundary: {}, container: {}, cursorOffset: [Object, String] as PropType | string>, @@ -539,7 +537,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -566,7 +563,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, @@ -604,7 +600,6 @@ const DxSearchEditorOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:buttons": null, "update:disabled": null, "update:elementAttr": null, @@ -662,7 +657,6 @@ const DxSearchEditorOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, buttons: Array as PropType>, disabled: Boolean, elementAttr: Object as PropType>, diff --git a/packages/devextreme-vue/src/lookup.ts b/packages/devextreme-vue/src/lookup.ts index 5d1172b95ba5..2c98c0120be3 100644 --- a/packages/devextreme-vue/src/lookup.ts +++ b/packages/devextreme-vue/src/lookup.ts @@ -439,7 +439,6 @@ const DxDropDownOptionsConfig = { "update:isActive": null, "update:hoveredElement": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -481,7 +480,6 @@ const DxDropDownOptionsConfig = { }, props: { animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, diff --git a/packages/devextreme-vue/src/number-box.ts b/packages/devextreme-vue/src/number-box.ts index cc3f6acfffc0..a6a3340932f4 100644 --- a/packages/devextreme-vue/src/number-box.ts +++ b/packages/devextreme-vue/src/number-box.ts @@ -301,7 +301,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -328,7 +327,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, diff --git a/packages/devextreme-vue/src/scheduler.ts b/packages/devextreme-vue/src/scheduler.ts index 146bffafe585..3f5be69d9875 100644 --- a/packages/devextreme-vue/src/scheduler.ts +++ b/packages/devextreme-vue/src/scheduler.ts @@ -468,7 +468,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:buttonTemplate": null, "update:disabled": null, "update:elementAttr": null, @@ -496,7 +495,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, buttonTemplate: {}, disabled: Boolean, elementAttr: Object as PropType>, diff --git a/packages/devextreme-vue/src/select-box.ts b/packages/devextreme-vue/src/select-box.ts index 2cd88fe5eb57..66a6820b291d 100644 --- a/packages/devextreme-vue/src/select-box.ts +++ b/packages/devextreme-vue/src/select-box.ts @@ -8,6 +8,7 @@ import DOMComponent from "devextreme/core/dom_component"; import dxPopup from "devextreme/ui/popup"; import { DropDownPredefinedButton, + FieldAddons, } from "devextreme/ui/drop_down_editor/ui.drop_down_editor"; import { TextEditorButton, @@ -109,6 +110,7 @@ type AccessibleOptions = Pick | Record>, elementAttr: Object as PropType>, + fieldAddons: Object as PropType>, fieldTemplate: {}, focusStateEnabled: Boolean, grouped: Boolean, @@ -283,6 +286,7 @@ const componentConfig = { "update:dropDownButtonTemplate": null, "update:dropDownOptions": null, "update:elementAttr": null, + "update:fieldAddons": null, "update:fieldTemplate": null, "update:focusStateEnabled": null, "update:grouped": null, @@ -364,6 +368,7 @@ const componentConfig = { (this as any).$_expectedChildren = { button: { isCollectionItem: true, optionName: "buttons" }, dropDownOptions: { isCollectionItem: false, optionName: "dropDownOptions" }, + fieldAddons: { isCollectionItem: false, optionName: "fieldAddons" }, item: { isCollectionItem: true, optionName: "items" } }; } @@ -485,7 +490,6 @@ const DxDropDownOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -536,7 +540,6 @@ const DxDropDownOptionsConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -597,6 +600,25 @@ const DxDropDownOptions = defineComponent(DxDropDownOptionsConfig); toolbarItem: { isCollectionItem: true, optionName: "toolbarItems" } }; +const DxFieldAddonsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:afterTemplate": null, + "update:beforeTemplate": null, + }, + props: { + afterTemplate: {}, + beforeTemplate: {} + } +}; + +prepareConfigurationComponentConfig(DxFieldAddonsConfig); + +const DxFieldAddons = defineComponent(DxFieldAddonsConfig); + +(DxFieldAddons as any).$_optionName = "fieldAddons"; + const DxFromConfig = { emits: { "update:isActive": null, @@ -734,7 +756,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -761,7 +782,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, @@ -947,6 +967,7 @@ export { DxButton, DxCollision, DxDropDownOptions, + DxFieldAddons, DxFrom, DxHide, DxItem, diff --git a/packages/devextreme-vue/src/tag-box.ts b/packages/devextreme-vue/src/tag-box.ts index 586ae49ee698..81684434bd34 100644 --- a/packages/devextreme-vue/src/tag-box.ts +++ b/packages/devextreme-vue/src/tag-box.ts @@ -29,6 +29,7 @@ import { } from "devextreme/common"; import { DropDownPredefinedButton, + FieldAddons, } from "devextreme/ui/drop_down_editor/ui.drop_down_editor"; import { CollectionWidgetItem, @@ -110,6 +111,7 @@ type AccessibleOptions = Pick | Record>, elementAttr: Object as PropType>, + fieldAddons: Object as PropType>, fieldTemplate: {}, focusStateEnabled: Boolean, grouped: Boolean, @@ -296,6 +299,7 @@ const componentConfig = { "update:dropDownButtonTemplate": null, "update:dropDownOptions": null, "update:elementAttr": null, + "update:fieldAddons": null, "update:fieldTemplate": null, "update:focusStateEnabled": null, "update:grouped": null, @@ -383,6 +387,7 @@ const componentConfig = { (this as any).$_expectedChildren = { button: { isCollectionItem: true, optionName: "buttons" }, dropDownOptions: { isCollectionItem: false, optionName: "dropDownOptions" }, + fieldAddons: { isCollectionItem: false, optionName: "fieldAddons" }, item: { isCollectionItem: true, optionName: "items" } }; } @@ -504,7 +509,6 @@ const DxDropDownOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -555,7 +559,6 @@ const DxDropDownOptionsConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -616,6 +619,25 @@ const DxDropDownOptions = defineComponent(DxDropDownOptionsConfig); toolbarItem: { isCollectionItem: true, optionName: "toolbarItems" } }; +const DxFieldAddonsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:afterTemplate": null, + "update:beforeTemplate": null, + }, + props: { + afterTemplate: {}, + beforeTemplate: {} + } +}; + +prepareConfigurationComponentConfig(DxFieldAddonsConfig); + +const DxFieldAddons = defineComponent(DxFieldAddonsConfig); + +(DxFieldAddons as any).$_optionName = "fieldAddons"; + const DxFromConfig = { emits: { "update:isActive": null, @@ -753,7 +775,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -780,7 +801,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, @@ -966,6 +986,7 @@ export { DxButton, DxCollision, DxDropDownOptions, + DxFieldAddons, DxFrom, DxHide, DxItem, diff --git a/packages/devextreme-vue/src/text-box.ts b/packages/devextreme-vue/src/text-box.ts index d6b787cf5319..de9e1d52f1a0 100644 --- a/packages/devextreme-vue/src/text-box.ts +++ b/packages/devextreme-vue/src/text-box.ts @@ -273,7 +273,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -300,7 +299,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, diff --git a/packages/devextreme-vue/src/tree-list.ts b/packages/devextreme-vue/src/tree-list.ts index 4df3822f547a..56cd1344ded6 100644 --- a/packages/devextreme-vue/src/tree-list.ts +++ b/packages/devextreme-vue/src/tree-list.ts @@ -176,6 +176,8 @@ import { FieldDataChangedEvent, InitializedEvent as FormInitializedEvent, OptionChangedEvent as FormOptionChangedEvent, + SmartPastedEvent, + SmartPastingEvent, FormItemComponent, FormItemType, } from "devextreme/ui/form"; @@ -185,6 +187,9 @@ import { import { Component, } from "devextreme/core/component"; +import { + AIIntegration, +} from "devextreme/common/ai-integration"; import { LocateInMenuMode, ShowTextMode, @@ -595,6 +600,25 @@ prepareComponentConfig(componentConfig); const DxTreeList = defineComponent(componentConfig); +const DxAiOptionsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:disabled": null, + "update:instruction": null, + }, + props: { + disabled: Boolean, + instruction: String + } +}; + +prepareConfigurationComponentConfig(DxAiOptionsConfig); + +const DxAiOptions = defineComponent(DxAiOptionsConfig); + +(DxAiOptions as any).$_optionName = "aiOptions"; + const DxAnimationConfig = { emits: { "update:isActive": null, @@ -1460,7 +1484,6 @@ const DxFilterBuilderConfig = { "update:accessKey": null, "update:activeStateEnabled": null, "update:allowHierarchicalFields": null, - "update:bindingOptions": null, "update:customOperations": null, "update:disabled": null, "update:elementAttr": null, @@ -1490,7 +1513,6 @@ const DxFilterBuilderConfig = { accessKey: String, activeStateEnabled: Boolean, allowHierarchicalFields: Boolean, - bindingOptions: Object as PropType>, customOperations: Array as PropType>, disabled: Boolean, elementAttr: Object as PropType>, @@ -1536,7 +1558,6 @@ const DxFilterBuilderPopupConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -1587,7 +1608,6 @@ const DxFilterBuilderPopupConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -1779,9 +1799,9 @@ const DxFormConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, + "update:aiIntegration": null, "update:alignItemLabels": null, "update:alignItemLabelsInAllGroups": null, - "update:bindingOptions": null, "update:colCount": null, "update:colCountByScreen": null, "update:customizeItem": null, @@ -1803,6 +1823,8 @@ const DxFormConfig = { "update:onFieldDataChanged": null, "update:onInitialized": null, "update:onOptionChanged": null, + "update:onSmartPasted": null, + "update:onSmartPasting": null, "update:optionalMark": null, "update:readOnly": null, "update:requiredMark": null, @@ -1822,9 +1844,9 @@ const DxFormConfig = { props: { accessKey: String, activeStateEnabled: Boolean, + aiIntegration: Object as PropType, alignItemLabels: Boolean, alignItemLabelsInAllGroups: Boolean, - bindingOptions: Object as PropType>, colCount: [String, Number] as PropType, colCountByScreen: Object as PropType>, customizeItem: Function as PropType<((item: dxFormSimpleItem | dxFormGroupItem | dxFormTabbedItem | dxFormEmptyItem | dxFormButtonItem) => void)>, @@ -1846,6 +1868,8 @@ const DxFormConfig = { onFieldDataChanged: Function as PropType<((e: FieldDataChangedEvent) => void)>, onInitialized: Function as PropType<((e: FormInitializedEvent) => void)>, onOptionChanged: Function as PropType<((e: FormOptionChangedEvent) => void)>, + onSmartPasted: Function as PropType<((e: SmartPastedEvent) => void)>, + onSmartPasting: Function as PropType<((e: SmartPastingEvent) => void)>, optionalMark: String, readOnly: Boolean, requiredMark: String, @@ -1904,6 +1928,7 @@ const DxFormItemConfig = { emits: { "update:isActive": null, "update:hoveredElement": null, + "update:aiOptions": null, "update:colSpan": null, "update:cssClass": null, "update:dataField": null, @@ -1920,6 +1945,7 @@ const DxFormItemConfig = { "update:visibleIndex": null, }, props: { + aiOptions: Object as PropType>, colSpan: Number, cssClass: String, dataField: String, @@ -1943,6 +1969,7 @@ const DxFormItem = defineComponent(DxFormItemConfig); (DxFormItem as any).$_optionName = "formItem"; (DxFormItem as any).$_expectedChildren = { + aiOptions: { isCollectionItem: false, optionName: "aiOptions" }, AsyncRule: { isCollectionItem: true, optionName: "validationRules" }, CompareRule: { isCollectionItem: true, optionName: "validationRules" }, CustomRule: { isCollectionItem: true, optionName: "validationRules" }, @@ -2448,7 +2475,6 @@ const DxPopupConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:animation": null, - "update:bindingOptions": null, "update:container": null, "update:contentTemplate": null, "update:deferRendering": null, @@ -2499,7 +2525,6 @@ const DxPopupConfig = { props: { accessKey: String, animation: Object as PropType>, - bindingOptions: Object as PropType>, container: {}, contentTemplate: {}, deferRendering: Boolean, @@ -3270,6 +3295,7 @@ const DxValidationRule = defineComponent(DxValidationRuleConfig); export default DxTreeList; export { DxTreeList, + DxAiOptions, DxAnimation, DxAsyncRule, DxAt, diff --git a/packages/devextreme-vue/src/tree-view.ts b/packages/devextreme-vue/src/tree-view.ts index e19bdcda3e5a..89b9c4b8a3a7 100644 --- a/packages/devextreme-vue/src/tree-view.ts +++ b/packages/devextreme-vue/src/tree-view.ts @@ -366,7 +366,6 @@ const DxOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:disabled": null, "update:elementAttr": null, "update:focusStateEnabled": null, @@ -393,7 +392,6 @@ const DxOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, disabled: Boolean, elementAttr: Object as PropType>, focusStateEnabled: Boolean, @@ -431,7 +429,6 @@ const DxSearchEditorOptionsConfig = { "update:hoveredElement": null, "update:accessKey": null, "update:activeStateEnabled": null, - "update:bindingOptions": null, "update:buttons": null, "update:disabled": null, "update:elementAttr": null, @@ -489,7 +486,6 @@ const DxSearchEditorOptionsConfig = { props: { accessKey: String, activeStateEnabled: Boolean, - bindingOptions: Object as PropType>, buttons: Array as PropType>, disabled: Boolean, elementAttr: Object as PropType>, diff --git a/packages/devextreme/build/gulp/context.js b/packages/devextreme/build/gulp/context.js index 26f4662b67fb..101751f6fe7b 100644 --- a/packages/devextreme/build/gulp/context.js +++ b/packages/devextreme/build/gulp/context.js @@ -19,5 +19,6 @@ module.exports = { TRANSPILED_RENOVATION_PATH: 'artifacts/transpiled-renovation', TRANSPILED_PROD_ESM_PATH: 'artifacts/transpiled-esm-npm', SCSS_PACKAGE_PATH: '../devextreme-scss', - EULA_URL: 'https://js.devexpress.com/Licensing/' + EULA_URL: 'https://js.devexpress.com/Licensing/', + REMOVE_NON_PRODUCTION_MODULE: argv.uglify, }; diff --git a/packages/devextreme/build/gulp/js-bundles.js b/packages/devextreme/build/gulp/js-bundles.js index b8a7792a1497..875f779191c1 100644 --- a/packages/devextreme/build/gulp/js-bundles.js +++ b/packages/devextreme/build/gulp/js-bundles.js @@ -15,7 +15,6 @@ const ctx = require('./context.js'); const headerPipes = require('./header-pipes.js'); const webpackConfig = require('../../webpack.config.js'); const env = require('./env-variables.js'); - const namedDebug = lazyPipe() .pipe(named, (file) => path.basename(file.path, path.extname(file.path)) + '.debug'); @@ -30,15 +29,18 @@ const DEBUG_BUNDLES = BUNDLES.concat([ '/bundles/dx.custom.js' ]); const processBundles = (bundles, pathPrefix) => bundles.map((bundle) => pathPrefix + bundle); const muteWebPack = () => undefined; -const getWebpackConfig = () => env.BUILD_INTERNAL_PACKAGE || env.BUILD_TEST_INTERNAL_PACKAGE ? - Object.assign({ - plugins: [ - new webpack.NormalModuleReplacementPlugin(/(.*)\/license_validation/, resource => { - resource.request = resource.request.replace('license_validation', 'license_validation_internal'); - }) - ] - }, webpackConfig) : - webpackConfig; +const getWebpackConfig = () => { + const plugins = []; + const isInternalBuild = env.BUILD_INTERNAL_PACKAGE || env.BUILD_TEST_INTERNAL_PACKAGE; + + if (isInternalBuild) { + plugins.push(new webpack.NormalModuleReplacementPlugin(/(.*)\/license_validation/, resource => { + resource.request = resource.request.replace('license_validation', 'license_validation_internal'); + })); + } + + return Object.assign(webpackConfig, { plugins }); +}; const bundleProdPipe = lazyPipe() .pipe(named) diff --git a/packages/devextreme/build/gulp/state_manager/__tests__/build_state_manager.test.js b/packages/devextreme/build/gulp/state_manager/__tests__/build_state_manager.test.js new file mode 100644 index 000000000000..bc92bbb1d7d4 --- /dev/null +++ b/packages/devextreme/build/gulp/state_manager/__tests__/build_state_manager.test.js @@ -0,0 +1,172 @@ +const fs = require('fs'); +const path = require('path'); +const through2 = require('through2'); +const Vinyl = require('vinyl'); +const replaceStateManagerModulesForProduction = require('../replace_state_manager_modules_for_production'); +const { removeDevelopmentStateManagerModules } = require('../remove_development_state_manager_modules'); + +const createEnvContent = (env) => ({ + index: [ + `export { setupStateManager } from './setup_state_manager';`, + `export { signal } from './reactive_primitives/index';` + ].join('\n'), + setupStateManager: `export const setupStateManager = () => { + // this setupStateManager function body is for ${env} build + }`, + reactivePrimitivesIndex: `export const signal = () => { + // this signal function body is for ${env} build + }`, +}); + +const PROD_DIR_CONTENT = createEnvContent('prod'); +const DEV_DIR_CONTENT = createEnvContent('dev'); + +const INDEX_DEV_CONTENT = `export { setupStateManager, signal } from './dev/index';`; +const INDEX_PROD_CONTENT = `export * from './prod/index';`; + +const FILE_OUTSIDE_OF_ENV_SPECIFIC_FOLDER_CONTENT = 'test content'; +const FILE_OUTSIDE_STATE_MANGER_CONTENT = 'console.log("file outside of state manager");'; + +describe('Build the state manager', () => { + let testsContext; + let originalConsoleError; + let consoleErrorSpy; + + const createEnvPaths = (baseDir, env) => ({ + reactivePrimitivesDir: path.join(baseDir, env, 'reactive_primitives'), + reactivePrimitivesIndex: path.join(baseDir, env, 'reactive_primitives', 'index.js'), + setupStateManager: path.join(baseDir, env, 'setup_state_manager.js'), + index: path.join(baseDir, env, 'index.js'), + }); + + const createEnvFiles = (paths, content) => { + Object.entries(paths).forEach(([key, filePath]) => { + if (filePath.endsWith('.js') && content[key]) { + fs.writeFileSync(filePath, content[key]); + } + }); + }; + + const createEnvSpecificStreamFileObjects = (paths, content) => { + return Object.entries(paths) + .filter(([key, filePath]) => filePath.endsWith('.js') && content[key]) + .map(([key, filePath]) => new Vinyl({ + path: filePath, + contents: Buffer.from(content[key]) + })); + }; + + beforeEach(() => { + const stream = replaceStateManagerModulesForProduction(); + const tempDir = path.join(__dirname, '__test-artifacts__'); + + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + + const devextremeDir = path.join(tempDir, 'devextreme'); + const stateManagerDir = path.join(devextremeDir, 'esm', '__internal', 'core', 'state_manager'); + const devDir = path.join(stateManagerDir, 'dev'); + const prodDir = path.join(stateManagerDir, 'prod'); + + const devPaths = createEnvPaths(stateManagerDir, 'dev'); + const prodPaths = createEnvPaths(stateManagerDir, 'prod'); + + const indexFilePath = path.join(stateManagerDir, 'index.js'); + const fileOutsideOfEnvSpecificFolderFilePath = path.join(stateManagerDir, 'state_manager.test.js'); + const fileOutsideStateMangerPath = path.join(tempDir, 'other_file.js'); + + fs.mkdirSync(stateManagerDir, { recursive: true }); + fs.mkdirSync(devDir, { recursive: true }); + fs.mkdirSync(prodDir, { recursive: true }); + fs.mkdirSync(devPaths.reactivePrimitivesDir, { recursive: true }); + fs.mkdirSync(prodPaths.reactivePrimitivesDir, { recursive: true }); + + fs.writeFileSync(indexFilePath, INDEX_DEV_CONTENT); + fs.writeFileSync(fileOutsideOfEnvSpecificFolderFilePath, FILE_OUTSIDE_OF_ENV_SPECIFIC_FOLDER_CONTENT); + fs.writeFileSync(fileOutsideStateMangerPath, FILE_OUTSIDE_STATE_MANGER_CONTENT); + + createEnvFiles(devPaths, DEV_DIR_CONTENT); + createEnvFiles(prodPaths, PROD_DIR_CONTENT); + + originalConsoleError = console.error; + consoleErrorSpy = jest.fn(); + console.error = consoleErrorSpy; + + const files = [ + ...createEnvSpecificStreamFileObjects(prodPaths, PROD_DIR_CONTENT), + ...createEnvSpecificStreamFileObjects(devPaths, DEV_DIR_CONTENT), + new Vinyl({ + path: fileOutsideStateMangerPath, + contents: Buffer.from(FILE_OUTSIDE_STATE_MANGER_CONTENT) + }), + new Vinyl({ + path: fileOutsideOfEnvSpecificFolderFilePath, + contents: Buffer.from(FILE_OUTSIDE_OF_ENV_SPECIFIC_FOLDER_CONTENT) + }), + new Vinyl({ + path: indexFilePath, + contents: Buffer.from(INDEX_DEV_CONTENT) + }), + ]; + + stream.on('data', (file) => { + fs.writeFileSync(file.path, file.contents.toString()); + }); + + files.forEach(file => stream.write(file)); + stream.end(); + + testsContext = { + stream, + devextremeDir, + devDir, + prodPaths, + indexFilePath, + fileOutsideOfEnvSpecificFolderFilePath, + fileOutsideStateMangerPath + }; + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + const runTestWithStream = (testFn) => { + return (done) => { + testsContext.stream.on('end', () => { + try { + testFn(); + done(); + } catch (error) { + done(error); + } + }); + testsContext.stream.on('error', done); + }; + }; + + it('should remove development modules', runTestWithStream(() => { + removeDevelopmentStateManagerModules(testsContext.devextremeDir); + + expect(fs.existsSync(testsContext.devDir)).toBe(false); + expect(fs.existsSync(testsContext.fileOutsideOfEnvSpecificFolderFilePath)).toBe(false); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + })); + + it('should not remove modules that are unrelated to the state manager', runTestWithStream(() => { + removeDevelopmentStateManagerModules(testsContext.devextremeDir); + + const fileOutsideStateMangerPathContent = fs.readFileSync(testsContext.fileOutsideStateMangerPath, 'utf8'); + expect(fileOutsideStateMangerPathContent).toBe(FILE_OUTSIDE_STATE_MANGER_CONTENT); + })); + + it('should replace `index.js` content by `prod/index.js` content', runTestWithStream(() => { + removeDevelopmentStateManagerModules(testsContext.devextremeDir); + + const indexFileContent = fs.readFileSync(testsContext.indexFilePath, 'utf8'); + expect(indexFileContent).toBe(INDEX_PROD_CONTENT); + expect(fs.existsSync(testsContext.prodPaths.index)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + })); +}); diff --git a/packages/devextreme/build/gulp/state_manager/constants.js b/packages/devextreme/build/gulp/state_manager/constants.js new file mode 100644 index 000000000000..b962f97ebd6f --- /dev/null +++ b/packages/devextreme/build/gulp/state_manager/constants.js @@ -0,0 +1,11 @@ +const path = require('path'); + +const STATE_MANAGER_FOLDER_PATH = path.join('__internal', 'core', 'state_manager'); +const STATE_MANAGER_INDEX_MODULE_PATH = path.join(STATE_MANAGER_FOLDER_PATH, 'index.js'); +const STATE_MANAGER_PROD_FOLDER_PATH = path.join(STATE_MANAGER_FOLDER_PATH, 'prod'); + +module.exports = { + STATE_MANAGER_FOLDER_PATH, + STATE_MANAGER_INDEX_MODULE_PATH, + STATE_MANAGER_PROD_FOLDER_PATH +}; diff --git a/packages/devextreme/build/gulp/state_manager/index.js b/packages/devextreme/build/gulp/state_manager/index.js new file mode 100644 index 000000000000..40a8f4119714 --- /dev/null +++ b/packages/devextreme/build/gulp/state_manager/index.js @@ -0,0 +1,2 @@ +require('./remove_development_state_manager_modules'); +require('./replace_state_manager_modules_for_production') diff --git a/packages/devextreme/build/gulp/state_manager/remove_development_state_manager_modules.js b/packages/devextreme/build/gulp/state_manager/remove_development_state_manager_modules.js new file mode 100644 index 000000000000..0e59fab38ecc --- /dev/null +++ b/packages/devextreme/build/gulp/state_manager/remove_development_state_manager_modules.js @@ -0,0 +1,44 @@ +'use strict'; + +const path = require('path'); +const gulp = require('gulp'); +const del = require('del'); +const { + STATE_MANAGER_FOLDER_PATH, + STATE_MANAGER_INDEX_MODULE_PATH, + STATE_MANAGER_PROD_FOLDER_PATH +} = require('./constants'); +const ctx = require('../context'); + +const MODULE_TYPES = ['esm', 'cjs']; + +const removeDevelopmentStateManagerModules = (targetPath) => { + const patterns = []; + + MODULE_TYPES.forEach(type => { + patterns.push(`${path.join(targetPath, type, STATE_MANAGER_FOLDER_PATH)}/**`); + }); + + MODULE_TYPES.forEach(type => { + patterns.push(`!${path.join(targetPath, type, STATE_MANAGER_FOLDER_PATH)}`); + patterns.push(`!${path.join(targetPath, type, STATE_MANAGER_INDEX_MODULE_PATH)}`); + patterns.push(`!${path.join(targetPath, type, STATE_MANAGER_PROD_FOLDER_PATH)}`); + patterns.push(`!${path.join(targetPath, type, STATE_MANAGER_PROD_FOLDER_PATH)}/**`); + }); + + del.sync(patterns); +} + +const createRemoveDevelopmentStateManagerModulesTask = (targetPath) => (done) => { + removeDevelopmentStateManagerModules(targetPath); + done(); +}; + +gulp.task('state-manager-remove-development-only-modules-transpiled-prod-esm', createRemoveDevelopmentStateManagerModulesTask(ctx.TRANSPILED_PROD_ESM_PATH)); + +gulp.task('state-manager-remove-development-only-modules-transpiled-prod-renovation', createRemoveDevelopmentStateManagerModulesTask(ctx.TRANSPILED_PROD_RENOVATION_PATH)); + +module.exports = { + removeDevelopmentStateManagerModules, + createRemoveDevelopmentStateManagerModulesTask +}; diff --git a/packages/devextreme/build/gulp/state_manager/replace_state_manager_modules_for_production.js b/packages/devextreme/build/gulp/state_manager/replace_state_manager_modules_for_production.js new file mode 100644 index 000000000000..c4d2904f8f86 --- /dev/null +++ b/packages/devextreme/build/gulp/state_manager/replace_state_manager_modules_for_production.js @@ -0,0 +1,42 @@ +'use strict'; + +const gulp = require('gulp'); +const through2 = require('through2'); +const path = require('path'); +const fs = require('fs'); +const babel = require('@babel/core'); +const transpileConfig = require('../transpile-config'); +const { + STATE_MANAGER_FOLDER_PATH, + STATE_MANAGER_INDEX_MODULE_PATH, +} = require('./constants'); +const ctx = require('../context'); + +const ERROR_PREFIX = 'Error during replacing the state manager modules:'; + +function replaceStateManagerModulesForProduction() { + return through2.obj(function(file, enc, callback) { + if (file.path.includes(STATE_MANAGER_INDEX_MODULE_PATH)) { + try { + file.contents = Buffer.from(`export * from './prod/index';`); + } catch (error) { + console.error(ERROR_PREFIX, error); + } + } + + callback(null, file); + }); +} + +const prepareStateManager = (dist) => gulp.series.apply(gulp, [ + () => gulp + .src(`${dist}/**/${STATE_MANAGER_FOLDER_PATH}/**`) + .pipe(replaceStateManagerModulesForProduction()) + .pipe(gulp.dest(dist)), +]); + +gulp.task('state-manager-replace-production-modules-transpiled-prod-esm', prepareStateManager(ctx.TRANSPILED_PROD_ESM_PATH)); + +gulp.task('state-manager-replace-production-modules-transpiled-prod-renovation', prepareStateManager(ctx.TRANSPILED_PROD_RENOVATION_PATH)); + +module.exports = replaceStateManagerModulesForProduction; diff --git a/packages/devextreme/build/gulp/transpile.js b/packages/devextreme/build/gulp/transpile.js index f394c310bcea..dc80cf0ef746 100644 --- a/packages/devextreme/build/gulp/transpile.js +++ b/packages/devextreme/build/gulp/transpile.js @@ -196,7 +196,7 @@ const transpileProd = (dist, isEsm) => transpile( ], tsPipes: [ removeDebug(), - isEsm ? babel(transpileConfig.esm) : babel(transpileConfig.tsCjs) + isEsm ? babel(transpileConfig.esm) : babel(transpileConfig.tsCjs), ] }, ); diff --git a/packages/devextreme/eslint.config.mjs b/packages/devextreme/eslint.config.mjs index 6a7bfc4d2bb2..4fff57794af8 100644 --- a/packages/devextreme/eslint.config.mjs +++ b/packages/devextreme/eslint.config.mjs @@ -12,6 +12,8 @@ import importPlugin from 'eslint-plugin-import'; import globals from 'globals'; import simpleImportSort from 'eslint-plugin-simple-import-sort'; import { changeRulesToStylistic } from 'eslint-migration-utils'; +import unicorn from 'eslint-plugin-unicorn'; +import customRules from './eslint_plugins/index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -47,6 +49,8 @@ export default [ plugins: { 'no-only-tests': noOnlyTests, i18n: i18N, + unicorn, + 'devextreme-custom': customRules, }, settings: { 'import/resolver': { @@ -164,6 +168,7 @@ export default [ 'import/named': 2, 'import/default': 2, 'import/no-duplicates': 2, + 'devextreme-custom/no-direct-preact-signals-core-import': 'error', }, plugins: { '@stylistic': stylistic, @@ -214,6 +219,7 @@ export default [ '@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true, }], + 'devextreme-custom/no-direct-preact-signals-core-import': 'error', }, }, ...compat.extends('devextreme/typescript').map(config => { @@ -520,6 +526,51 @@ export default [ '@typescript-eslint/prefer-for-of': 'warn', }, }, + // Rules for scheduler + { + files: ['js/__internal/scheduler/**/*.ts?(x)'], + languageOptions: { + parser: tsParser, + ecmaVersion: 5, + sourceType: 'script', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: `${__dirname}/js/__internal`, + }, + }, + rules: { + 'no-implicit-coercion': ['error'], + 'no-extra-boolean-cast': 'error', + 'unicorn/filename-case': ['error', { case: 'snakeCase' }], + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: ['variable', 'function', 'parameter'], + format: null, + leadingUnderscore: 'forbid', + // allow only a single underscore identifier `_` to bypass this rule + filter: { + regex: '^_$', + match: false, + }, + }, + { + selector: 'memberLike', + format: null, + leadingUnderscore: 'allow', + }, + ], + 'devextreme-custom/no-deferred': 'error', + 'devextreme-custom/prefer-switch-true': ['error', { minBranches: 3 }], + }, + }, + // Allow Deferred in m_* scheduler files only + { + files: ['js/__internal/scheduler/**/m_*.ts?(x)'], + rules: { + 'devextreme-custom/no-deferred': 'off', + }, + }, // Rules for grid controls { files: [ diff --git a/packages/devextreme/eslint_plugins/index.js b/packages/devextreme/eslint_plugins/index.js new file mode 100644 index 000000000000..bb2639cd0edb --- /dev/null +++ b/packages/devextreme/eslint_plugins/index.js @@ -0,0 +1,11 @@ +const noDirectPreactSignalsCoreImport = require('./no_direct_preact_signals_core_import'); +const preferSwitchTrue = require('./prefer_switch_true'); +const noDeferred = require('./no_deferred'); + +module.exports = { + rules: { + 'no-direct-preact-signals-core-import': noDirectPreactSignalsCoreImport, + 'prefer-switch-true': preferSwitchTrue, + 'no-deferred': noDeferred, + }, +}; diff --git a/packages/devextreme/eslint_plugins/no_deferred.js b/packages/devextreme/eslint_plugins/no_deferred.js new file mode 100644 index 000000000000..e3b1c158b99a --- /dev/null +++ b/packages/devextreme/eslint_plugins/no_deferred.js @@ -0,0 +1,47 @@ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Disallow Deferred in favor of native Promise', + recommended: false, + }, + messages: { + noDeferred: 'Use native Promise instead of Deferred.', + }, + schema: [], + }, + create(context) { + return { + ImportDeclaration(node) { + const source = node.source && node.source.value; + if(typeof source !== 'string') return; + const specs = node.specifiers || []; + for(let i = 0; i < specs.length; i += 1) { + const spec = specs[i]; + if(spec.imported && spec.imported.name === 'Deferred') { + context.report({ node: spec, messageId: 'noDeferred' }); + } + } + }, + Identifier(node) { + if(node.name === 'Deferred') { + context.report({ node, messageId: 'noDeferred' }); + } + }, + NewExpression(node) { + // eslint-disable-next-line spellcheck/spell-checker + if(node.callee && node.callee.name === 'Deferred') { + context.report({ node, messageId: 'noDeferred' }); + } + }, + MemberExpression(node) { + // e.g., $.Deferred() + if(node.property && node.property.type === 'Identifier' && node.property.name === 'Deferred') { + context.report({ node, messageId: 'noDeferred' }); + } + }, + }; + }, +}; + + diff --git a/packages/devextreme/eslint_plugins/no_direct_preact_signals_core_import.js b/packages/devextreme/eslint_plugins/no_direct_preact_signals_core_import.js new file mode 100644 index 000000000000..9f6c848e400f --- /dev/null +++ b/packages/devextreme/eslint_plugins/no_direct_preact_signals_core_import.js @@ -0,0 +1,65 @@ +/* eslint-disable spellcheck/spell-checker */ + +const INVALID_IMPORT = '@preact/signals-core'; +const STATE_MANAGER_PATH = '/core/state_manager/'; +const VALID_IMPORT = '"@ts/core/state_manager/index"'; + +function validate(context, node, sourceNode) { + const filename = context.getFilename(); + const isStateManagerModule = filename.includes(STATE_MANAGER_PATH); + + if(!isStateManagerModule) { + context.report({ + node: sourceNode, + messageId: 'noDirectImport', + fix(fixer) { + return fixer.replaceText(sourceNode, VALID_IMPORT); + }, + }); + } +} + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Prevent direct imports from @preact/signals-core and enforce usage of the state manager.', + }, + fixable: 'code', + schema: [], + messages: { + noDirectImport: 'Direct imports from "@preact/signals-core" are not allowed. Use "@ts/core/state_manager/index" instead.', + }, + }, + + create(context) { + return { + ImportDeclaration(node) { + if(node.source.value === INVALID_IMPORT) { + validate(context, node, node.source); + } + }, + + ImportExpression(node) { + if( + node.source.type === 'Literal' && + node.source.value === INVALID_IMPORT + ) { + validate(context, node, node.source); + } + }, + + CallExpression(node) { + if( + node.callee.type === 'Identifier' && + node.callee.name === 'require' && + node.arguments.length === 1 && + node.arguments[0].type === 'Literal' && + node.arguments[0].value === INVALID_IMPORT + ) { + validate(context, node, node.arguments[0]); + } + }, + }; + }, +}; diff --git a/packages/devextreme/eslint_plugins/no_direct_preact_signals_core_import.test.js b/packages/devextreme/eslint_plugins/no_direct_preact_signals_core_import.test.js new file mode 100644 index 000000000000..e6ef640c379e --- /dev/null +++ b/packages/devextreme/eslint_plugins/no_direct_preact_signals_core_import.test.js @@ -0,0 +1,82 @@ +/* eslint-disable spellcheck/spell-checker */ +const { RuleTester } = require('eslint'); +const rule = require('./no_direct_preact_signals_core_import'); + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, +}); + +ruleTester.run('no-direct-preact-signals-core-import', rule, { + valid: [ + { + code: 'import { signal } from \'@preact/signals-core\';', + filename: '/path/to/core/state_manager/file.ts', + }, + { + code: 'import * as SignalsCore from \'@preact/signals-core\';', + filename: '/path/to/core/state_manager/reactive_primitives/index.ts', + }, + { + code: 'import { signal } from \'@ts/core/state_manager/index\';', + filename: '/path/to/some/component.ts', + }, + { + code: 'const signals = import(\'@preact/signals-core\');', + filename: '/path/to/core/state_manager/file.ts', + }, + { + code: 'const signals = require(\'@preact/signals-core\');', + filename: '/path/to/core/state_manager/file.ts', + }, + ], + + invalid: [ + { + code: 'import { signal } from \'@preact/signals-core\';', + filename: '/path/to/some/component.ts', + errors: [ + { + messageId: 'noDirectImport', + type: 'Literal', + }, + ], + output: 'import { signal } from "@ts/core/state_manager/index";', + }, + { + code: 'import * as SignalsCore from \'@preact/signals-core\';', + filename: '/path/to/some/component.ts', + errors: [ + { + messageId: 'noDirectImport', + type: 'Literal', + }, + ], + output: 'import * as SignalsCore from "@ts/core/state_manager/index";', + }, + { + code: 'const signals = import(\'@preact/signals-core\');', + filename: '/path/to/some/component.ts', + errors: [ + { + messageId: 'noDirectImport', + type: 'Literal', + }, + ], + output: 'const signals = import("@ts/core/state_manager/index");', + }, + { + code: 'const signals = require(\'@preact/signals-core\');', + filename: '/path/to/some/component.ts', + errors: [ + { + messageId: 'noDirectImport', + type: 'Literal', + }, + ], + output: 'const signals = require("@ts/core/state_manager/index");', + }, + ], +}); diff --git a/packages/devextreme/eslint_plugins/prefer_switch_true.js b/packages/devextreme/eslint_plugins/prefer_switch_true.js new file mode 100644 index 000000000000..83570c4d43e0 --- /dev/null +++ b/packages/devextreme/eslint_plugins/prefer_switch_true.js @@ -0,0 +1,51 @@ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Prefer switch(true) over long if/else chains', + recommended: false, + }, + schema: [ + { + type: 'object', + properties: { + minBranches: { type: 'integer', minimum: 2 }, + }, + additionalProperties: false, + }, + ], + messages: { + preferSwitchTrue: 'Prefer switch(true) over this if/else chain.', + }, + }, + create(context) { + const options = context.options && context.options[0] || {}; + const minBranches = Math.max(2, options.minBranches || 3); // total conditional branches (if + else-if's) + + function countChainBranches(ifNode) { + let branches = 1; // start with initial if + let current = ifNode.alternate; + while(current && current.type === 'IfStatement') { + branches += 1; + current = current.alternate; + } + return branches; + } + + return { + IfStatement(node) { + // Only evaluate at the head of the chain (parent isn't an else-if) + const parent = node.parent; + const isElseIf = parent && parent.type === 'IfStatement' && parent.alternate === node; + if(isElseIf) return; + + const branches = countChainBranches(node); + if(branches >= minBranches) { + context.report({ node, messageId: 'preferSwitchTrue' }); + } + }, + }; + }, +}; + + diff --git a/packages/devextreme/gulpfile.js b/packages/devextreme/gulpfile.js index a24b2c4333b7..e6fb12991b30 100644 --- a/packages/devextreme/gulpfile.js +++ b/packages/devextreme/gulpfile.js @@ -6,6 +6,7 @@ const multiProcess = require('gulp-multi-process'); const env = require('./build/gulp/env-variables'); const cache = require('gulp-cache'); const shell = require('gulp-shell'); +const { REMOVE_NON_PRODUCTION_MODULE } = require('./build/gulp/context'); gulp.task('clean', function(callback) { require('del').sync([ @@ -40,6 +41,7 @@ require('./build/gulp/generator/gulpfile'); require('./build/gulp/check_licenses'); require('./build/gulp/qunit-in-docker'); require('./build/gulp/systemjs'); +require('./build/gulp/state_manager'); if(env.TEST_CI) { console.warn('Using test CI mode!'); @@ -70,12 +72,20 @@ function createDefaultBatch(dev) { tasks.push('localization'); tasks.push(dev ? 'generate-components-dev' : 'generate-components'); tasks.push('transpile'); + + if(REMOVE_NON_PRODUCTION_MODULE) { + tasks.push('state-manager-replace-production-modules-transpiled-prod-renovation'); + tasks.push('state-manager-replace-production-modules-transpiled-prod-esm'); + + tasks.push('state-manager-remove-development-only-modules-transpiled-prod-renovation'); + tasks.push('state-manager-remove-development-only-modules-transpiled-prod-esm'); + } + tasks.push(dev && !env.BUILD_TESTCAFE ? 'main-batch-dev' : 'main-batch'); if(!env.TEST_CI && !dev && !env.BUILD_TESTCAFE) { tasks.push('npm'); tasks.push('check-license-notices'); } - return gulp.series(tasks); } @@ -102,5 +112,3 @@ gulp.task('dev', gulp.series( 'default-dev', 'dev-watch' )); - - diff --git a/packages/devextreme/jest.config.js b/packages/devextreme/jest.config.js index c29fb303d561..8d6bed5febf2 100644 --- a/packages/devextreme/jest.config.js +++ b/packages/devextreme/jest.config.js @@ -1,33 +1,53 @@ /** @type {import('ts-jest').JestConfigWithTsJest} **/ module.exports = { - testEnvironment: '/jsdom-with-timezone.js', - roots: ['/js'], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - moduleNameMapper: { - '@js/(.*)': '/js/$1', - '@ts/(.*)': '/js/__internal/$1', - '@preact/signals-core': '/node_modules/@preact/signals-core/dist/signals-core.js' - }, - modulePathIgnorePatterns: [ - 'node_modules' - ], - preset: 'ts-jest', - testMatch: [ - // TODO: change to '/**/*.test.(ts|tsx)' after removing renovation - '/js/__internal/**/*.test.(ts|tsx)', - ], - transform: { - '\\.[jt]sx?$': ['ts-jest', { - // eslint-disable-next-line spellcheck/spell-checker - tsconfig: '/js/__internal/tsconfig.json', - diagnostics: false, // set to true to enable type checking - isolatedModules: true, // performance optimization https://kulshekhar.github.io/ts-jest/user/config/isolatedModules - babelConfig: { - presets: ['@babel/preset-env'], - plugins: [ - ['babel-plugin-inferno', { 'imports': true }] - ] - } - }], - } + projects: [ + { + displayName: 'jsdom-tests', + testEnvironment: '/jsdom-with-timezone.js', + roots: ['/js'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + moduleNameMapper: { + '@js/(.*)': '/js/$1', + '@ts/(.*)': '/js/__internal/$1', + '@preact/signals-core': '/node_modules/@preact/signals-core/dist/signals-core.js' + }, + modulePathIgnorePatterns: [ + 'node_modules', + '__test-artifacts__' + ], + preset: 'ts-jest', + testMatch: [ + // TODO: change to '/**/*.test.(ts|tsx)' after removing renovation + '/js/__internal/**/*.test.(ts|tsx)', + ], + transform: { + '\\.[jt]sx?$': ['ts-jest', { + // eslint-disable-next-line spellcheck/spell-checker + tsconfig: '/js/__internal/tsconfig.json', + diagnostics: false, // set to true to enable type checking + isolatedModules: true, // performance optimization https://kulshekhar.github.io/ts-jest/user/config/isolatedModules + babelConfig: { + presets: ['@babel/preset-env'], + plugins: [ + ['babel-plugin-inferno', { 'imports': true }] + ] + } + }], + }, + }, + { + displayName: 'node-tests', + testEnvironment: 'node', + modulePathIgnorePatterns: [ + '__test-artifacts__' + ], + roots: ['/build', '/eslint_plugins'], + moduleFileExtensions: ['js'], + testMatch: [ + '/build/**/*.test.js', + '/eslint_plugins/**/*.test.js', + ], + } + ] }; + diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/index.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/index.ts index fa3957884050..804f7ce5311f 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/index.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/index.ts @@ -5,6 +5,7 @@ import { ExecuteCommand } from '@ts/core/ai_integration/commands/execute'; import { ExpandCommand } from '@ts/core/ai_integration/commands/expand'; import { ProofreadCommand } from '@ts/core/ai_integration/commands/proofread'; import { ShortenCommand } from '@ts/core/ai_integration/commands/shorten'; +import { SmartPasteCommand } from '@ts/core/ai_integration/commands/smartPaste'; import { SummarizeCommand } from '@ts/core/ai_integration/commands/summarize'; import { TranslateCommand } from '@ts/core/ai_integration/commands/translate'; @@ -16,6 +17,7 @@ export { ExpandCommand, ProofreadCommand, ShortenCommand, + SmartPasteCommand, SummarizeCommand, TranslateCommand, }; diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/smartPaste.test.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/smartPaste.test.ts new file mode 100644 index 000000000000..8153232b35c8 --- /dev/null +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/smartPaste.test.ts @@ -0,0 +1,176 @@ +import { + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import type { + AIProvider, + RequestCallbacks, + SmartPasteCommandParams, + SmartPasteCommandResult, +} from '@js/common/ai-integration'; +import { SmartPasteCommand } from '@ts/core/ai_integration/commands/smartPaste'; +import type { PromptData } from '@ts/core/ai_integration/core/prompt_manager'; +import { PromptManager } from '@ts/core/ai_integration/core/prompt_manager'; +import { RequestManager } from '@ts/core/ai_integration/core/request_manager'; +import { templates } from '@ts/core/ai_integration/templates'; +import { Provider } from '@ts/core/ai_integration/test_utils/provider_mock'; + +const COMMAND_NAME = 'smartPaste'; +const USER_TEXT = 'text to paste'; +const USER_FIELDS = [{ name: 'description', format: 'text' }]; +const USER_FIELDS_WITH_INSTRUCTION = [{ name: 'description', format: 'text', instruction: 'instruction' }]; +const PROCESSED_USER_FIELDS = 'fieldName: description, format: text'; +const PROCESSED_USER_FIELDS_WITH_INSTRUCTION = 'fieldName: description, format: text, instruction: instruction'; + +describe('SmartPasteCommand', () => { + const params: SmartPasteCommandParams = { text: USER_TEXT, fields: USER_FIELDS }; + const paramsWithInstruction: SmartPasteCommandParams = { + text: USER_TEXT, + fields: USER_FIELDS_WITH_INSTRUCTION, + }; + let promptManager = null as unknown as PromptManager; + let requestManager = null as unknown as RequestManager; + let command = null as unknown as SmartPasteCommand; + + beforeEach(() => { + const provider: AIProvider = new Provider(); + + requestManager = new RequestManager(provider); + promptManager = new PromptManager(); + + command = new SmartPasteCommand(promptManager, requestManager); + }); + + describe('getTemplateName', () => { + it('should return the name of the corresponding template', () => { + // @ts-expect-error Access to protected property for a test + const templateName = command.getTemplateName(); + + expect(templateName).toStrictEqual(COMMAND_NAME); + }); + }); + + describe('buildPromptData', () => { + it('should form PromptData with text and fields info', () => { + // @ts-expect-error Access to protected property for a test + const promptData: PromptData = command.buildPromptData(params); + + expect(promptData).toStrictEqual({ + user: { text: USER_TEXT, fields: PROCESSED_USER_FIELDS }, + }); + }); + + it('should form PromptData with text and fields info including instruction', () => { + // @ts-expect-error Access to protected property for a test + const promptData: PromptData = command.buildPromptData(paramsWithInstruction); + + expect(promptData).toStrictEqual({ + user: { text: USER_TEXT, fields: PROCESSED_USER_FIELDS_WITH_INSTRUCTION }, + }); + }); + }); + + describe('parseResult', () => { + it('should return the parsed result', () => { + const response = 'Field1:::value1;;;Field2:::value2'; + // @ts-expect-error Access to protected property for a test + const result = command.parseResult(response); + + const expectedResult = [{ + name: 'Field1', + value: 'value1', + }, { + name: 'Field2', + value: 'value2', + }]; + + expect(result).toStrictEqual(expectedResult); + }); + + it('should parse array values correctly', () => { + const response = 'Field1:::value1:::value2;;;Field2:::value3:::value4:::value5'; + // @ts-expect-error Access to protected property for a test + const result = command.parseResult(response); + + const expectedResult = [ + { + name: 'Field1', + value: ['value1', 'value2'], + }, + { + name: 'Field2', + value: ['value3', 'value4', 'value5'], + }, + ]; + + expect(result).toStrictEqual(expectedResult); + }); + + it('should not include an empty fields into parsed result', () => { + const response = 'Field1:::value1;;;Field2:::'; + // @ts-expect-error Access to protected property for a test + const result = command.parseResult(response); + + const expectedResult = [{ + name: 'Field1', + value: 'value1', + }]; + + expect(result).toStrictEqual(expectedResult); + }); + + it('should process multiple delimiters and malformed field data correctly', () => { + const response = 'Field1:::value1;;;;;;Field2'; + // @ts-expect-error Access to protected property for a test + const result = command.parseResult(response); + + const expectedResult = [{ + name: 'Field1', + value: 'value1', + }]; + + expect(result).toStrictEqual(expectedResult); + }); + }); + + describe('execute', () => { + const callbacks: RequestCallbacks = { onComplete: () => {} }; + + it('promptManager.buildPrompt should be called with parameters containing the passed values', () => { + const buildPromptSpy = jest.spyOn(promptManager, 'buildPrompt'); + + command.execute(params, callbacks); + + expect(buildPromptSpy).toHaveBeenCalledTimes(1); + expect(promptManager.buildPrompt).toHaveBeenCalledWith(COMMAND_NAME, { + user: { text: USER_TEXT, fields: PROCESSED_USER_FIELDS }, + }); + }); + + it('promptManager.buildPrompt should should return prompt with passed values', () => { + jest.spyOn(promptManager, 'buildPrompt'); + + command.execute(params, callbacks); + + const expectedUserPrompt = templates.smartPaste.user?.replace('{{text}}', USER_TEXT) + .replace('{{fields}}', PROCESSED_USER_FIELDS); + + expect(promptManager.buildPrompt).toHaveReturnedWith({ + system: templates.smartPaste.system, + user: expectedUserPrompt, + }); + }); + + it('should call provider.sendRequest once and return the abort function', () => { + const sendRequestSpy = jest.spyOn(requestManager, 'sendRequest'); + + const abort = command.execute(params, callbacks); + + expect(typeof abort).toBe('function'); + expect(sendRequestSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/smartPaste.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/smartPaste.ts new file mode 100644 index 000000000000..f4dce9707d3e --- /dev/null +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/smartPaste.ts @@ -0,0 +1,54 @@ +import type { FieldInfo, SmartPasteCommandParams, SmartPasteCommandResult } from '@js/common/ai-integration'; +import { BaseCommand } from '@ts/core/ai_integration/commands/base'; +import type { PromptData, PromptTemplateName } from '@ts/core/ai_integration/core/prompt_manager'; + +export class SmartPasteCommand extends BaseCommand< + SmartPasteCommandParams, + SmartPasteCommandResult +> { + protected getTemplateName(): PromptTemplateName { + return 'smartPaste'; + } + + protected buildPromptData(params: SmartPasteCommandParams): PromptData { + const fieldsInstructions = this.generateFieldsInstructions(params.fields); + return { + user: { + text: params.text, + fields: fieldsInstructions, + }, + }; + } + + protected parseResult(response: string): SmartPasteCommandResult { + const result: SmartPasteCommandResult = []; + + response.split(';;;').forEach((data: string) => { + if (!data) { + return; + } + + const [name, ...values] = data.split(':::'); + const value = values.length <= 1 ? values[0] : values; + + if (value) { + result.push({ + name, + value, + }); + } + }); + + return result; + } + + private generateFieldsInstructions(fields: FieldInfo[]): string { + const fieldData = fields.map((field) => { + const instruction = field.instruction ?? ''; + + return `fieldName: ${field.name}, format: ${field.format}${instruction ? `, instruction: ${instruction}` : ''}`; + }); + + return fieldData.join(';;;'); + } +} diff --git a/packages/devextreme/js/__internal/core/ai_integration/core/ai_integration.ts b/packages/devextreme/js/__internal/core/ai_integration/core/ai_integration.ts index ae7f078565fd..b273064f211b 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/core/ai_integration.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/core/ai_integration.ts @@ -14,6 +14,8 @@ import type { RequestCallbacks, ShortenCommandParams, ShortenCommandResult, + SmartPasteCommandParams, + SmartPasteCommandResult, SummarizeCommandParams, SummarizeCommandResult, TranslateCommandParams, @@ -27,6 +29,7 @@ import { ExpandCommand, ProofreadCommand, ShortenCommand, + SmartPasteCommand, SummarizeCommand, TranslateCommand, } from '@ts/core/ai_integration/commands/index'; @@ -42,6 +45,7 @@ export const enum CommandNames { Shorten = 'shorten', Summarize = 'summarize', Translate = 'translate', + SmartPaste = 'smartPaste', } export const COMMANDS = { @@ -53,6 +57,7 @@ export const COMMANDS = { [CommandNames.Shorten]: ShortenCommand, [CommandNames.Summarize]: SummarizeCommand, [CommandNames.Translate]: TranslateCommand, + [CommandNames.SmartPaste]: SmartPasteCommand, } as const; export interface CommandDefinition { @@ -70,6 +75,7 @@ export interface Commands { [CommandNames.Shorten]: CommandDefinition; [CommandNames.Summarize]: CommandDefinition; [CommandNames.Translate]: CommandDefinition; + [CommandNames.SmartPaste]: CommandDefinition; } export class AIIntegration implements IAIIntegration { @@ -91,8 +97,9 @@ export class AIIntegration implements IAIIntegration { callbacks: RequestCallbacks, ): () => void { type Command = BaseCommand; + type CommandInstance = Commands[K]['command']; - let command = this.commands.get(commandName) as Command | undefined; + let command = this.commands.get(commandName) as CommandInstance | undefined; if (!command) { const Command = COMMANDS[commandName]; @@ -102,7 +109,7 @@ export class AIIntegration implements IAIIntegration { this.commands.set(commandName, command); } - return command.execute(params, callbacks); + return (command as Command).execute(params, callbacks); } public changeStyle( @@ -192,4 +199,15 @@ export class AIIntegration implements IAIIntegration { callbacks, ); } + + public smartPaste( + params: SmartPasteCommandParams, + callbacks: RequestCallbacks, + ): () => void { + return this.executeCommand( + CommandNames.SmartPaste, + params, + callbacks, + ); + } } diff --git a/packages/devextreme/js/__internal/core/ai_integration/core/prompt_manager.ts b/packages/devextreme/js/__internal/core/ai_integration/core/prompt_manager.ts index 6f72a3100095..c4d05f2a3021 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/core/prompt_manager.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/core/prompt_manager.ts @@ -19,7 +19,8 @@ export type PromptTemplateName = | 'proofread' | 'shorten' | 'summarize' - | 'translate'; + | 'translate' + | 'smartPaste'; export type PromptTemplates = Map; diff --git a/packages/devextreme/js/__internal/core/ai_integration/templates/index.ts b/packages/devextreme/js/__internal/core/ai_integration/templates/index.ts index 1c1fa83e6ee6..c6354029b45c 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/templates/index.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/templates/index.ts @@ -27,4 +27,8 @@ export const templates: PromptTemplates = { translate: { system: 'Translate the text provided into {{lang}}. Ensure the translation retains the original meaning and tone. Provide only the translated text in your response, without any additional formatting or commentary.', }, + smartPaste: { + system: 'You are a helpful assistant that helps to fill fields based on the text provided. You will get a text and a list of fields that should be filled using info from the text. It can include the name of field, suitable format, optionally some additional instruction about what it should include. You need to return data for all the fields in the following format without any preamble, introduction, or explanatory text: {fieldName}:::{fieldValue};;;{fieldName}:::{fieldValue} and so on, where {fieldName} - is a variable for a field name and {fieldValue} - is a variable for a string to fill. If there is no info to fill, field value should be empty (like Name:::;;;)- do not use placeholders like (empty), N/A, null, or similar.', + user: 'Text: {{text}}. Fields: {{fields}}.', + }, }; diff --git a/packages/devextreme/js/__internal/core/di/index.test.ts b/packages/devextreme/js/__internal/core/di/index.test.ts index 4e5ece50a074..2adb5fe1f6af 100644 --- a/packages/devextreme/js/__internal/core/di/index.test.ts +++ b/packages/devextreme/js/__internal/core/di/index.test.ts @@ -1,8 +1,4 @@ -/* eslint-disable @typescript-eslint/no-extraneous-class */ -/* eslint-disable prefer-const */ -/* eslint-disable @typescript-eslint/init-declarations */ /* eslint-disable max-classes-per-file */ -/* eslint-disable class-methods-use-this */ import { describe, expect, it } from '@jest/globals'; import { DIContext } from './index'; @@ -184,3 +180,123 @@ describe('dependency cycle', () => { expect(() => ctx.get(MyClass2)).toThrow(); }); }); + +describe('decorators', () => { + class MyClass { + static dependencies = [] as const; + + value = 1; + + tag = ''; + } + + class AnotherClass { + static dependencies = [] as const; + + counter = 0; + } + + it('should apply global decorators to created instances', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + ctx.registerDecorator((instance) => { + if (instance instanceof MyClass) { + instance.value = 2; + } + return instance; + }); + + expect(ctx.get(MyClass).value).toBe(2); + }); + + it('should apply global decorators to registered instances', () => { + const ctx = new DIContext(); + const instance = new MyClass(); + + ctx.registerDecorator((obj) => { + if (obj instanceof MyClass) { + obj.value = 3; + } + return obj; + }); + + ctx.registerInstance(MyClass, instance); + + expect(ctx.get(MyClass).value).toBe(3); + expect(instance.value).toBe(3); + }); + + it('should apply multiple global decorators in the correct order', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + ctx.registerDecorator((instance) => { + if (instance instanceof MyClass) { + instance.value += 1; + instance.tag += 'A'; + } + return instance; + }); + + ctx.registerDecorator((instance) => { + if (instance instanceof MyClass) { + instance.value += 2; + instance.tag += 'B'; + } + return instance; + }); + + const result = ctx.get(MyClass); + expect(result.value).toBe(4); + expect(result.tag).toBe('AB'); + }); + + it('should apply global decorators to instances created from fabrics', () => { + const ctx = new DIContext(); + + class BaseClass { + static dependencies = [] as const; + + value = 1; + } + + class ExtendedClass extends BaseClass { + static dependencies = [] as const; + + extraValue = 10; + } + + ctx.register(BaseClass, ExtendedClass); + + ctx.registerDecorator((instance) => { + if (instance instanceof ExtendedClass) { + instance.extraValue = 20; + } + return instance; + }); + + const result = ctx.get(BaseClass); + + expect(result).toBeInstanceOf(ExtendedClass); + expect((result as ExtendedClass).extraValue).toBe(20); + }); + + it('should prevent adding decorators after instance creation', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + ctx.register(AnotherClass); + + const myClassInstance = ctx.get(MyClass); + + expect(() => ctx.registerDecorator((obj) => { + if (obj instanceof MyClass) { + obj.value = 42; + obj.tag = 'decorated'; + } + })).toThrowError(); + + expect(myClassInstance.value).toBe(1); + expect(myClassInstance.tag).toBe(''); + }); +}); diff --git a/packages/devextreme/js/__internal/core/di/index.ts b/packages/devextreme/js/__internal/core/di/index.ts index 11a7f79d43db..ebbb00556f2c 100644 --- a/packages/devextreme/js/__internal/core/di/index.ts +++ b/packages/devextreme/js/__internal/core/di/index.ts @@ -11,6 +11,8 @@ interface DIItem extends Constructor dependencies: readonly [...{ [P in keyof TDeps]: AbstractType }]; } +export type DecoratorFunction = (instance: T) => T; + export class DIContext { private readonly instances: Map = new Map(); @@ -20,6 +22,8 @@ export class DIContext { private readonly antiRecursionSet = new Set(); + private readonly globalDecorators: DecoratorFunction[] = []; + public register( id: AbstractType, fabric: DIItem, @@ -40,7 +44,9 @@ export class DIContext { id: AbstractType, instance: T, ): void { - this.instances.set(id, instance); + const decoratedInstance = this.applyGlobalDecorators(instance); + + this.instances.set(id, decoratedInstance); } public get( @@ -67,15 +73,37 @@ export class DIContext { const fabric = this.fabrics.get(id); if (fabric) { - const res: T = this.create(fabric as any); - this.instances.set(id, res); - this.instances.set(fabric, res); - return res; + const instance: T = this.create(fabric as any); + + const decoratedInstance = this.applyGlobalDecorators(instance); + + this.instances.set(id, decoratedInstance); + this.instances.set(fabric, decoratedInstance); + return decoratedInstance; } return null; } + public registerDecorator(decoratorFn: DecoratorFunction): void { + if (this.hasInitiatedInstances) { + throw new Error('Cannot register decorator: decorators must be registered before any instances are created or retrieved from the DI container.'); + } + + this.globalDecorators.push(decoratorFn); + } + + private get hasInitiatedInstances(): boolean { + return this.instances.size > 0; + } + + private applyGlobalDecorators(instance: T): T { + return this.globalDecorators.reduce( + (currentInstance, currentDecorator) => currentDecorator(currentInstance), + instance, + ); + } + private create(fabric: DIItem): T { if (this.antiRecursionSet.has(fabric)) { throw new Error('dependency cycle in DI'); diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index e6d93aa4a9e9..d70b39e923be 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -33,6 +33,9 @@ const KEY_SPLITTER = '.'; const BUY_NOW_LINK = 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeJQuery.aspx'; const LICENSING_DOC_LINK = 'https://go.devexpress.com/Licensing_Documentation_DevExtremeJQuery.aspx'; +const NBSP = '\u00A0'; +const SUBSCRIPTION_NAMES = `Universal, DXperience, ASP.NET${NBSP}and${NBSP}Blazor, DevExtreme${NBSP}Complete`; + const GENERAL_ERROR: Token = { kind: TokenKind.corrupted, error: 'general' }; const VERIFICATION_ERROR: Token = { kind: TokenKind.corrupted, error: 'verification' }; const DECODING_ERROR: Token = { kind: TokenKind.corrupted, error: 'decoding' }; @@ -188,7 +191,7 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer if (error && !internal) { const buyNowLink = config().buyNowLink ?? BUY_NOW_LINK; const licensingDocLink = config().licensingDocLink ?? LICENSING_DOC_LINK; - showTrialPanel(buyNowLink, licensingDocLink, fullVersion); + showTrialPanel(buyNowLink, licensingDocLink, fullVersion, SUBSCRIPTION_NAMES); } const preview = isPreview(version.patch); diff --git a/packages/devextreme/js/__internal/core/license/trial_panel.client.ts b/packages/devextreme/js/__internal/core/license/trial_panel.client.ts index 9dec813302a1..4449e3906aa1 100644 --- a/packages/devextreme/js/__internal/core/license/trial_panel.client.ts +++ b/packages/devextreme/js/__internal/core/license/trial_panel.client.ts @@ -31,6 +31,7 @@ const attributeNames = { buyNow: 'buy-now', licensingDoc: 'licensing-doc', version: 'version', + subscriptions: 'subscriptions', }; const commonStyles = { opacity: '1', @@ -185,6 +186,7 @@ class DxLicense extends SafeHTMLElement { private _createContentContainer(): HTMLElement { const contentContainer = document.createElement('div'); contentContainer.style.cssText = this._contentStyles; + const subscriptions = this.getAttribute(attributeNames.subscriptions); contentContainer.append( this._createSpan('For evaluation purposes only. Redistribution prohibited. Please '), this._createLink('register', this.getAttribute(attributeNames.licensingDoc) as string), @@ -192,6 +194,13 @@ class DxLicense extends SafeHTMLElement { this._createLink('purchase a new license', this.getAttribute(attributeNames.buyNow) as string), this._createSpan(` to continue use of DevExpress product libraries (v${this.getAttribute(attributeNames.version)}).`), ); + + if (subscriptions) { + contentContainer.append( + this._createSpan(` Included in Subscriptions: ${subscriptions}.`), + ); + } + return contentContainer; } @@ -255,20 +264,12 @@ class DxLicenseTrigger extends SafeHTMLElement { if (!licensePanel.length && !DxLicense.closed) { const license = document.createElement(componentNames.panel); - license.setAttribute( - attributeNames.version, - this.getAttribute(attributeNames.version) as string, - ); - - license.setAttribute( - attributeNames.buyNow, - this.getAttribute(attributeNames.buyNow) as string, - ); - - license.setAttribute( - attributeNames.licensingDoc, - this.getAttribute(attributeNames.licensingDoc) as string, - ); + Object.values(attributeNames).forEach((attrName) => { + license.setAttribute( + attrName, + this.getAttribute(attrName) as string, + ); + }); license.setAttribute(DATA_PERMANENT_ATTRIBUTE, ''); @@ -295,6 +296,7 @@ export function renderTrialPanel( buyNowUrl: string, licensingDocUrl: string, version: string, + subscriptions = '', customStyles?: CustomTrialPanelStyles, ): void { registerCustomComponents(customStyles); @@ -304,6 +306,7 @@ export function renderTrialPanel( trialPanelTrigger.setAttribute(attributeNames.buyNow, buyNowUrl); trialPanelTrigger.setAttribute(attributeNames.licensingDoc, licensingDocUrl); trialPanelTrigger.setAttribute(attributeNames.version, version); + trialPanelTrigger.setAttribute(attributeNames.subscriptions, subscriptions); document.body.appendChild(trialPanelTrigger); } diff --git a/packages/devextreme/js/__internal/core/license/trial_panel.ts b/packages/devextreme/js/__internal/core/license/trial_panel.ts index 87c9767f9730..3bc2bb438039 100644 --- a/packages/devextreme/js/__internal/core/license/trial_panel.ts +++ b/packages/devextreme/js/__internal/core/license/trial_panel.ts @@ -9,10 +9,11 @@ export function showTrialPanel( buyNowUrl: string, licensingDocUrl: string, version: string, + subscriptions?: string, customStyles?: CustomTrialPanelStyles, ): void { if (isClient()) { - renderTrialPanel(buyNowUrl, licensingDocUrl, version, customStyles); + renderTrialPanel(buyNowUrl, licensingDocUrl, version, subscriptions, customStyles); } } diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/event_emitter.ts b/packages/devextreme/js/__internal/core/state_manager/dev/event_emitter.ts new file mode 100644 index 000000000000..4c6bc5977fa4 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/event_emitter.ts @@ -0,0 +1,32 @@ +import type * as StateManagementTypes from './types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class EventEmitter void> { + private readonly listeners: T[] = []; + + constructor( + private readonly eventName: string, + private readonly logger: StateManagementTypes.Logger, + ) { + + } + + addListener(callback: T): void { + if (!callback || typeof callback !== 'function') { + this.logger.error(`Callback for ${this.eventName} must be a function`); + return; + } + + this.listeners.push(callback); + } + + emit(...args: Parameters): void { + this.listeners.forEach((listener) => { + try { + listener(...args); + } catch (error) { + this.logger.error(`Error in ${this.eventName} listener`, error); + } + }); + } +} diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/index.ts b/packages/devextreme/js/__internal/core/state_manager/dev/index.ts new file mode 100644 index 000000000000..b4d5f7de788b --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/index.ts @@ -0,0 +1,13 @@ +export type { + ReadonlySignal, + Signal, +} from './reactive_primitives/index'; +export { + batch, + computed, + effect, + signal, + // eslint-disable-next-line spellcheck/spell-checker + untracked, +} from './reactive_primitives/index'; +export { setupStateManager } from './setup_state_manager'; diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/logger.ts b/packages/devextreme/js/__internal/core/state_manager/dev/logger.ts new file mode 100644 index 000000000000..3f3558df9cfe --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/logger.ts @@ -0,0 +1,62 @@ +/* eslint-disable no-console */ +import type * as StateManagementTypes from './types'; + +const LOG_TYPE_TO_LEVEL: Record = { + debug: 0, info: 1, warn: 2, error: 3, +}; + +export interface ConsoleLoggerOptions { + logLevel?: StateManagementTypes.LogLevel; + prefix: string; +} + +export class Logger implements StateManagementTypes.Logger { + private logLevel: StateManagementTypes.LogLevel; + + private prefix: string; + + constructor(options?: ConsoleLoggerOptions) { + this.logLevel = options?.logLevel ?? 'info'; + this.prefix = options?.prefix ?? ''; + } + + setLevel(level: StateManagementTypes.LogLevel): void { + this.logLevel = level; + } + + setPrefix(prefix: string): void { + this.prefix = prefix; + } + + debug(message: string, ...args: unknown[]): void { + if (this.shouldLog('debug')) { + console.debug(this.formatMessage(message), ...args); + } + } + + info(message: string, ...args: unknown[]): void { + if (this.shouldLog('info')) { + console.info(this.formatMessage(message), ...args); + } + } + + warn(message: string, ...args: unknown[]): void { + if (this.shouldLog('warn')) { + console.warn(this.formatMessage(message), ...args); + } + } + + error(message: string, ...args: unknown[]): void { + if (this.shouldLog('error')) { + console.error(this.formatMessage(message), ...args); + } + } + + private formatMessage(message: string): string { + return this.prefix ? `${this.prefix} ${message}` : message; + } + + private shouldLog(level: StateManagementTypes.LogLevel): boolean { + return LOG_TYPE_TO_LEVEL[level] >= LOG_TYPE_TO_LEVEL[this.logLevel]; + } +} diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/preact_signal_value_container_manager.ts b/packages/devextreme/js/__internal/core/state_manager/dev/preact_signal_value_container_manager.ts new file mode 100644 index 000000000000..f5f6d1f47bba --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/preact_signal_value_container_manager.ts @@ -0,0 +1,96 @@ +import type * as StateManagementTypes from './types'; +import { isObject } from './utils'; + +function isSignal(value: { brand?: symbol } | unknown): + value is StateManagementTypes.ObservableValueContainer { + if (isObject(value) && 'brand' in value) { + return value.brand === Symbol.for('preact-signals'); + } + + return false; +} + +export class PreactSignalValueContainerManager +implements StateManagementTypes.ValueContainerManager { + constructor( + private readonly logger: StateManagementTypes.Logger, + private readonly stateSourceSign: RegExp, + private readonly valueContainer: StateManagementTypes.ObservableValueContainer, + ) { + + } + + trackChanges( + onChange: StateManagementTypes.ValueContainerChangeCallback, + ): void { + if (!onChange || typeof onChange !== 'function') { + this.logger.error('onChange callback is required'); + return; + } + + const previousValue = this.getValue(); + + this.valueContainer.subscribe((newValue) => { + try { + const payload: StateManagementTypes.ValueContainerChange['payload'] = { + previousValue, + newValue, + timestamp: Date.now(), + source: this.captureSource(this.valueContainer), + }; + + const change: StateManagementTypes.ValueContainerChange = { + payload, + }; + + onChange(change); + } catch (error) { + this.logger.error('Error in Preact Signal subscription', error); + } + }); + } + + getValue(): StateManagementTypes.ObservableValueContainer['value'] { + return this.valueContainer.peek(); + } + + private captureSource(valueContainer: StateManagementTypes.ObservableValueContainer): string { + if (valueContainer.stack) { + const { stack } = valueContainer; + + return this.findStateSourceLine(stack).trim(); + } + + return 'The source is not tracked'; + } + + private findStateSourceLine(stack: string): string { + const lines = stack.split('\n'); + + const stateSourceLine = lines + .find((line) => line && this.stateSourceSign.test(line)); + + return stateSourceLine ?? (lines.length > 1 ? lines[1] : ''); + } +} + +export const PreactSignalValueContainerManagerFactory: +StateManagementTypes.ValueContainerManagerConstructor = { + canHandle( + valueContainer: StateManagementTypes.MaybeValueContainer, + ): valueContainer is StateManagementTypes.ObservableValueContainer { + return isSignal(valueContainer); + }, + + create( + logger: StateManagementTypes.Logger, + stateSourceSign: RegExp, + valueContainer: StateManagementTypes.MaybeValueContainer, + ): StateManagementTypes.ValueContainerManager { + if (!isSignal(valueContainer)) { + throw new Error('Invalid value container for PreactSignalValueContainerManager'); + } + + return new PreactSignalValueContainerManager(logger, stateSourceSign, valueContainer); + }, +}; diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/reactive_primitives/index.ts b/packages/devextreme/js/__internal/core/state_manager/dev/reactive_primitives/index.ts new file mode 100644 index 000000000000..54c83a6de24c --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/reactive_primitives/index.ts @@ -0,0 +1,63 @@ +import * as Reactive from '../../prod/reactive_primitives/index'; +import type { + BatchFunction, + ComputedFunction, + EffectCleanup, + EffectFn, + ReadonlySignal, + Signal, + // eslint-disable-next-line spellcheck/spell-checker + UntrackedFunction, +} from '../../prod/reactive_primitives/types'; + +export type { + ReadonlySignal, + Signal, +} from '../../prod/reactive_primitives/types'; + +export function signal(initialValue: T): Signal { + const signalInstance = Reactive.signal(initialValue); + + const trace = new Error().stack; + if (trace) { + Object.defineProperty(signalInstance, 'stack', { + value: trace, + writable: false, + enumerable: false, + configurable: false, + }); + } + + return signalInstance; +} + +export function computed(fn: ComputedFunction): ReadonlySignal { + const computedInstance = Reactive.computed(fn); + + const trace = new Error().stack; + + if (trace) { + Object.defineProperty(computedInstance, 'stack', { + value: trace, + writable: false, + enumerable: false, + configurable: false, + }); + } + + return computedInstance; +} + +export function effect(fn: EffectFn): EffectCleanup { + return Reactive.effect(fn); +} + +export function batch(fn: BatchFunction): void { + Reactive.batch(fn); +} + +// eslint-disable-next-line spellcheck/spell-checker +export function untracked(fn: UntrackedFunction): T { + // eslint-disable-next-line spellcheck/spell-checker + return Reactive.untracked(fn); +} diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/redux_dev_tools_connector.ts b/packages/devextreme/js/__internal/core/state_manager/dev/redux_dev_tools_connector.ts new file mode 100644 index 000000000000..a949ad99a00d --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/redux_dev_tools_connector.ts @@ -0,0 +1,152 @@ +/* eslint-disable spellcheck/spell-checker */ +import { EventEmitter } from './event_emitter'; +import type * as StateManagementTypes from './types'; +import { isObject } from './utils'; + +export class ReduxDevToolsConnector implements StateManagementTypes.DevToolsConnector { + private devTools: StateManagementTypes.ReduxDevToolsInstance | null = null; + + private _isConnected = false; + + private readonly externalActionEmitter: + EventEmitter; + + constructor( + private readonly componentName: string, + private readonly logger: StateManagementTypes.Logger, + ) { + this.externalActionEmitter = new EventEmitter( + 'externalAction', + logger, + ); + } + + connect(): void { + if (!this.hasReduxDevTools(window)) { + this.logger.warn('Redux DevTools extension not found. Install the extension and serve your app via web server (not file://)'); + return; + } + + try { + this.devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ + name: `${this.componentName} ${new Date().valueOf()}`, + trace: true, + traceLimit: 25, + features: { + jump: true, + skip: false, + dispatch: true, + }, + shouldCatchErrors: true, + serialize: { + options: { + circular: '[CIRCULAR]', + date: true, + }, + replacer: (key, value) => { + // replaced because this property contains a reference to the component instance + // which causes "heap out of memory" + if (key === 'changes' && isObject(value) && 'component' in value && 'element' in value) { + return '[REPLACED]'; + } + + return value; + }, + }, + }); + + this.devTools.subscribe( + (message) => { + if (message.type !== 'DISPATCH') { + return; + } + + if (message.payload.type === 'JUMP_TO_STATE' || message.payload.type === 'JUMP_TO_ACTION') { + if (message.state) { + this.handleJumpToAction(message.state); + } + } else if (message.payload.type === 'COMMIT') { + this.externalActionEmitter.emit('COMMIT', null); + } else if (message.payload.type === 'RESET') { + this.externalActionEmitter.emit('RESET', null); + } else { + this.logger.error(`Unknown ${message.payload.type} message payload type`); + } + }, + ); + + this._isConnected = true; + this.logger.info('Connected to Redux DevTools'); + } catch (error) { + this.logger.error('Failed to connect to Redux DevTools', error); + } + } + + disconnect(): void { + if (!this.isConnected || this.devTools === null) { + return; + } + + try { + this.devTools.unsubscribe(); + this._isConnected = false; + this.logger.info('Disconnected from Redux DevTools'); + } catch (error) { + this.logger.error('Failed to disconnect from Redux DevTools', error); + } + } + + sendAction(...args: Parameters): ReturnType { + const [action, payload, state] = args; + + if (!action) { + this.logger.error('Action name is required'); + return; + } + + if (!this.isConnected || this.devTools === null) { + this.logger.warn('Cannot send action: Not connected to Redux DevTools'); + return; + } + + try { + const preparedAction = `${action}: ${payload.path}`; + + const currentState = state ?? {}; + + const actionObject = { + type: preparedAction, + payload: payload || {}, + }; + + this.devTools.send(actionObject, currentState); + } catch (error) { + this.logger.error(`Failed to send action to DevTools: ${action}`, error); + this.logger.debug(`Action details - Type: ${action}, Payload:`, payload); + } + } + + public get isConnected(): boolean { + return this._isConnected; + } + + onExternalAction(callback: StateManagementTypes.DevToolsExternalActionCallback): void { + this.externalActionEmitter.addListener(callback); + } + + private handleJumpToAction(state: string): void { + try { + const parsedState = JSON.parse(state); + this.externalActionEmitter.emit('JUMP_TO_STATE', parsedState); + } catch (error) { + this.logger.error('Failed to handle jump to action', error); + } + } + + private hasReduxDevTools(globalEnv: Window): globalEnv is + Window & { __REDUX_DEVTOOLS_EXTENSION__: StateManagementTypes.ReduxDevToolsExtension } { + return typeof globalEnv !== 'undefined' + && '__REDUX_DEVTOOLS_EXTENSION__' in globalEnv + && globalEnv.__REDUX_DEVTOOLS_EXTENSION__ !== undefined; + } +} diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/setup_state_manager.ts b/packages/devextreme/js/__internal/core/state_manager/dev/setup_state_manager.ts new file mode 100644 index 000000000000..fcffb9f06a67 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/setup_state_manager.ts @@ -0,0 +1,73 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { DIContext } from '../../di'; +import { Logger } from './logger'; +import { StateManagerFactory } from './state_manager'; +import type * as StateManagementTypes from './types'; +import { isObject } from './utils'; + +export type DecoratorFunction = (instance: T) => T; + +export interface StateManagerInitializerOptions { + logLevel?: StateManagementTypes.LogLevel; + componentName: string; + diContext: DIContext; + stateSourceSign?: RegExp; +} + +const DEFAULT_STATE_SOURCE_SIGN = /Controller/; + +function isStateSource( + instance: unknown, + stateSourceSign: RegExp, +): instance is StateManagementTypes.StateSource { + return isObject(instance) + && 'constructor' in instance + && 'name' in instance.constructor + && stateSourceSign.test(instance.constructor.name); +} + +export const setupStateManager = ( + options: StateManagerInitializerOptions, +): StateManagementTypes.StateManager | undefined => { + const { + diContext, + componentName, + logLevel = 'warn', + stateSourceSign = DEFAULT_STATE_SOURCE_SIGN, + } = options; + if (!diContext) { + throw new Error('DI context is not provided'); + } + + if (!componentName) { + throw new Error('Component name is not provided'); + } + + const logger = new Logger({ logLevel, prefix: '[StateManager]' }); + + const isDevelopmentMode = process.env.NODE_ENV === 'development'; + + if (!isDevelopmentMode) { + return undefined; + } + + const stateManager = StateManagerFactory.create({ + componentName, + stateSourceSign, + logger, + }); + + const trackStateSource: DecoratorFunction = (instance) => { + if (isStateSource(instance, stateSourceSign)) { + stateManager.trackStateOf(instance); + } else { + logger.debug(`The '${instance?.constructor?.name}' state source isn't tracked by the state manager because it doesn't match the "${stateSourceSign}" sign pattern.`); + } + + return instance; + }; + + diContext.registerDecorator(trackStateSource); + + return stateManager; +}; diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/state_manager.ts b/packages/devextreme/js/__internal/core/state_manager/dev/state_manager.ts new file mode 100644 index 000000000000..d7f7ac31949d --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/state_manager.ts @@ -0,0 +1,201 @@ +import { Logger } from './logger'; +import { PreactSignalValueContainerManagerFactory } from './preact_signal_value_container_manager'; +// eslint-disable-next-line spellcheck/spell-checker +import { ReduxDevToolsConnector } from './redux_dev_tools_connector'; +import type * as StateManagementTypes from './types'; +import { deepCopy, isObject, joinStatePath } from './utils'; + +class StateManager implements StateManagementTypes.StateManager { + private readonly devToolsConnector: StateManagementTypes.DevToolsConnector; + + private readonly logger: StateManagementTypes.Logger; + + readonly componentState: StateManagementTypes.ComponentState; + + private readonly stateSourceSign: RegExp; + + private readonly valueContainerManagers: StateManagementTypes.ValueContainerManagerConstructor[]; + + constructor( + config: StateManagementTypes.StateManagerConfig, + ) { + this.componentState = {}; + + this.valueContainerManagers = config.valueContainerManagers; + this.devToolsConnector = config.devToolsConnector; + this.logger = config.logger; + this.stateSourceSign = config.stateSourceSign; + + this.init(); + } + + private init(): void { + this.devToolsConnector.onExternalAction((action) => { + this.logger.warn(`Handler for the '${action}' action is not implemented`); + }); + + this.devToolsConnector.connect(); + + this.logger.info('StateManager initialized'); + } + + trackStateOf(sourceData: StateManagementTypes.StateSource, sourceDataId?: string): void { + const preparedSourceDataId = sourceDataId ?? sourceData?.constructor?.name; + + if (!sourceData) { + this.logger.error('State source cannot be null or undefined'); + return; + } + + if (this.componentState[preparedSourceDataId]) { + this.logger.debug(`State source with ID '${preparedSourceDataId}' is already tracked. Overwriting.`); + } + + Object.entries(sourceData).forEach(([propertyName, propertyValue]) => { + if (!this.hasValueContainerManagerFor(propertyValue)) { + this.logger.debug(`No value container manager found for the '${propertyName}' property of the '${preparedSourceDataId}' state source`); + return; + } + + if (!this.componentState[preparedSourceDataId]) { + this.componentState[preparedSourceDataId] = {}; + } + + this.componentState[preparedSourceDataId][propertyName] = isObject(propertyValue) + ? new WeakRef(propertyValue) : propertyValue; + + this.trackStateSourceChanges( + preparedSourceDataId, + propertyName, + propertyValue, + ); + }); + } + + private trackStateSourceChanges( + stateId: string, + propertyName: string, + propertyValue: StateManagementTypes.MaybeValueContainer, + ): void { + const valueContainerManager = this.createValueContainerManagerFor(propertyValue); + + if (!valueContainerManager) { + this.logger.debug(`No value container manager found for the '${propertyName}' property of the '${stateId}' state`); + return; + } + + const fullPathToProperty = joinStatePath(stateId, propertyName); + + try { + valueContainerManager.trackChanges( + (valueContainerChange: StateManagementTypes.ValueContainerChange) => { + const valueContainerChangeCopy = { + ...valueContainerChange, + payload: { ...valueContainerChange.payload, path: fullPathToProperty }, + }; + + const { previousValue, newValue } = valueContainerChange.payload; + + if (typeof previousValue === 'object' && previousValue !== null) { + valueContainerChangeCopy.payload.previousValue = deepCopy(previousValue); + } + + if (typeof newValue === 'object' && newValue !== null) { + valueContainerChangeCopy.payload.newValue = deepCopy(newValue); + } + + const updatedComponentState = this.getComponentState(); + + if (this.devToolsConnector.isConnected) { + this.devToolsConnector + .sendAction( + 'UPDATE', + valueContainerChangeCopy.payload, + updatedComponentState, + ); + } + }, + ); + } catch (error) { + this.logger.error(`Failed to track state for ${fullPathToProperty}`, error); + } + } + + private hasValueContainerManagerFor( + valueContainer: StateManagementTypes.MaybeValueContainer, + ): boolean { + return this.valueContainerManagers + .some(( + currentStateContainerManager, + ) => currentStateContainerManager.canHandle(valueContainer)); + } + + private createValueContainerManagerFor( + valueContainer: StateManagementTypes.MaybeValueContainer, + ): StateManagementTypes.ValueContainerManager | undefined { + const valueContainerManagerFactory = this.valueContainerManagers + .find(( + currentStateContainerManager, + ) => currentStateContainerManager.canHandle(valueContainer)); + + if (!valueContainerManagerFactory) { + return undefined; + } + + return valueContainerManagerFactory.create(this.logger, this.stateSourceSign, valueContainer); + } + + getComponentState(): StateManagementTypes.ComponentState { + const result = Object.entries(this.componentState) + .reduce((acc, [stateId, stateValue]) => { + Object.entries(stateValue).forEach(([propertyName, propertyValue]) => { + const preparedPropertyValue = propertyValue instanceof WeakRef + // eslint-disable-next-line spellcheck/spell-checker + ? propertyValue.deref() : propertyValue; + + if (!preparedPropertyValue) { + return acc; + } + + const valueContainerManager = this.createValueContainerManagerFor(preparedPropertyValue); + + if (!valueContainerManager) { + return acc; + } + + const value = valueContainerManager.getValue(); + + if (!acc[stateId]) { + acc[stateId] = {}; + } + + acc[stateId][propertyName] = isObject(value) ? deepCopy(value) : value; + + return acc; + }); + + return acc; + }, {}); + + return result; + } +} + +export const StateManagerFactory = { + create: (options: StateManagementTypes.StateManagerFactoryOptions): StateManager => { + const logger = options.logger ?? new Logger({ logLevel: options.logLevel, prefix: '[StateManager]' }); + + const stateContainerManagers: StateManagementTypes.StateManagerConfig['valueContainerManagers'] = options.valueContainerManagers ?? [PreactSignalValueContainerManagerFactory]; + + const preparedConfig: StateManagementTypes.StateManagerConfig = { + valueContainerManagers: stateContainerManagers, + devToolsConnector: options.devToolsConnector + // eslint-disable-next-line spellcheck/spell-checker + ?? new ReduxDevToolsConnector(options.componentName, logger), + logger, + stateSourceSign: options.stateSourceSign, + }; + + return new StateManager(preparedConfig); + }, +}; diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/types.ts b/packages/devextreme/js/__internal/core/state_manager/dev/types.ts new file mode 100644 index 000000000000..b852ff4d782c --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/types.ts @@ -0,0 +1,148 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import type { Signal } from '@preact/signals-core'; + +export type StateManagerCommands = { + trackStateOf: (sourceData: StateSource, id?: string) => void; +}; + +export type StateManagerQueries = { + getComponentState: () => ComponentState; +}; + +export interface StateManager extends StateManagerCommands, StateManagerQueries { } + +export type StateSource = Record; + +export interface StateManagerConfig { + devToolsConnector: DevToolsConnector; + logger: Logger; + valueContainerManagers: ValueContainerManagerConstructor[]; + stateSourceSign: RegExp; +} + +export interface ObservableValueContainer extends ValueContainer, Signal { + stack?: string; +} + +export type MaybeObservableValueContainer = ObservableValueContainer | T; + +export interface ValueContainerManagerConstructor { + canHandle: (valueContainer: MaybeValueContainer) => valueContainer is ValueContainer; + create: ( + logger: Logger, + stateSourceSign: RegExp, + valueContainer: MaybeValueContainer + ) => ValueContainerManager; +} + +export type ValueContainerChangeCallback = (change: ValueContainerChange) => void; + +export interface ValueContainerManager { + trackChanges: ( + onChange: ValueContainerChangeCallback + ) => void; + getValue: () => unknown; +} + +export interface StateManagerFactoryOptions extends Partial { + componentName: string; + valueContainerManagers?: ValueContainerManagerConstructor[]; + logLevel?: LogLevel; + stateSourceSign: RegExp; +} + +export type ValueContainerPayload = { + previousValue: unknown; + newValue: unknown; + timestamp: number; + source: string; +}; + +export type ValueContainerChange = { + payload: ValueContainerPayload; +}; + +export type StateChangeActionType = 'UPDATE'; + +export type StateChangePayload = { + path: string; + previousValue: unknown; + newValue: unknown; + timestamp: number; + source: string; +}; + +export type ComponentState = Record>; + +export interface EventEmitter void> { + addListener: (callback: T) => void; + emit: (...args: Parameters) => void; +} + +export type ValueContainer = { [key: string]: unknown }; +export type MaybeValueContainer = ValueContainer | T; + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; +export type LogMethod = (message: string, ...args: unknown[]) => void; + +export interface Logger { + debug: LogMethod; + info: LogMethod; + warn: LogMethod; + error: LogMethod; +} + +export type DevToolsActions = 'DISPATCH' | 'JUMP_TO_STATE' | 'JUMP_TO_ACTION' | 'COMMIT' | 'RESET'; + +export type DevToolsExternalActionCallback = +(action: DevToolsActions, payload: ComponentState | null) => void; + +export interface DevToolsConnector { + connect: (options?: Record) => void; + disconnect: () => void; + sendAction: ( + action: StateChangeActionType, payload: StateChangePayload, state?: ComponentState + ) => void; + onExternalAction: (callback: DevToolsExternalActionCallback) => void; + isConnected: boolean; +} + +// eslint-disable-next-line spellcheck/spell-checker +type ReduxDevToolsActions = DevToolsActions; + +// eslint-disable-next-line spellcheck/spell-checker +export type ReduxDevToolsInstance = { + subscribe: + (callback: ( + // eslint-disable-next-line spellcheck/spell-checker + message: { type: ReduxDevToolsActions; payload: { type: string }; state?: string } + ) => void) => void; + send: (action: { type: string; payload: unknown }, state: unknown) => void; + unsubscribe: () => void; +}; + +// eslint-disable-next-line spellcheck/spell-checker +export type ReduxDevToolsExtension = { + connect: (options?: { + name?: string; + trace?: boolean; + traceLimit?: number; + features?: { + jump?: boolean; + skip?: boolean; + dispatch?: boolean; + }; + shouldCatchErrors?: boolean; + serialize?: boolean | { + options?: boolean | { + undefined?: boolean; + date?: boolean; + circular?: string; + }; + // eslint-disable-next-line spellcheck/spell-checker + replacer?: (key: string, value: unknown) => unknown; + }; + // eslint-disable-next-line spellcheck/spell-checker + }) => ReduxDevToolsInstance; +}; diff --git a/packages/devextreme/js/__internal/core/state_manager/dev/utils.ts b/packages/devextreme/js/__internal/core/state_manager/dev/utils.ts new file mode 100644 index 000000000000..0bb1b7799895 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/dev/utils.ts @@ -0,0 +1,65 @@ +export function joinStatePath(stateId: string, propertyName: string, separator = '.'): string { + return [stateId, propertyName].join(separator); +} + +export function splitStatePath(statePath: string, separator = '.'): string[] { + return statePath.split(separator).filter(Boolean); +} + +export function isValidStatePath(statePath: string): boolean { + const parts = splitStatePath(statePath); + return parts.length >= 2; +} + +export function isObject(input: unknown): input is object { + return input !== undefined && input !== null && typeof input === 'object'; +} + +export function deepCopy(inputObject: T): T { + function iter(value: unknown, visited: Map): unknown { + if (value === null || typeof value !== 'object') { + return value; + } + + if (visited.has(value)) { + return visited.get(value); + } + + if (value instanceof Date) { + const dateCopy = new Date(value.getTime()); + visited.set(value, dateCopy); + return dateCopy; + } + + if (value instanceof RegExp) { + const regExpCopy = new RegExp(value.source, value.flags); + visited.set(value, regExpCopy); + return regExpCopy; + } + + if (Array.isArray(value)) { + const arrayCopy: unknown[] = []; + + visited.set(value, arrayCopy); + + value.forEach((item, index) => { + arrayCopy[index] = iter(item, visited); + }); + return arrayCopy; + } + + const objectCopy: Record = {}; + visited.set(value, objectCopy); + + Object.keys(value).forEach((key) => { + const propertyValue = value[key]; + objectCopy[key] = iter(propertyValue, visited); + }); + + return objectCopy; + } + + const result = iter(inputObject, new Map()); + + return result as T; +} diff --git a/packages/devextreme/js/__internal/core/state_manager/index.ts b/packages/devextreme/js/__internal/core/state_manager/index.ts new file mode 100644 index 000000000000..9c87c6c76b84 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/index.ts @@ -0,0 +1 @@ +export * from './dev/index'; diff --git a/packages/devextreme/js/__internal/core/state_manager/prod/index.ts b/packages/devextreme/js/__internal/core/state_manager/prod/index.ts new file mode 100644 index 000000000000..d9a8adf6df33 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/prod/index.ts @@ -0,0 +1,14 @@ +export const setupStateManager = (): void => {}; + +export type { + ReadonlySignal, + Signal, +} from './reactive_primitives/index'; +export { + batch, + computed, + effect, + signal, + // eslint-disable-next-line spellcheck/spell-checker + untracked, +} from './reactive_primitives/index'; diff --git a/packages/devextreme/js/__internal/core/state_manager/prod/reactive_primitives/index.ts b/packages/devextreme/js/__internal/core/state_manager/prod/reactive_primitives/index.ts new file mode 100644 index 000000000000..718195a9c3e9 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/prod/reactive_primitives/index.ts @@ -0,0 +1,39 @@ +import * as SignalsCore from '@preact/signals-core'; + +import type { + BatchFunction, + ComputedFunction, + EffectCleanup, + EffectFn, + ReadonlySignal, + Signal, + // eslint-disable-next-line spellcheck/spell-checker + UntrackedFunction, +} from './types'; + +export type { + ReadonlySignal, + Signal, +} from './types'; + +export function signal(initialValue: T): Signal { + return SignalsCore.signal(initialValue); +} + +export function computed(fn: ComputedFunction): ReadonlySignal { + return SignalsCore.computed(fn); +} + +export function effect(fn: EffectFn): EffectCleanup { + return SignalsCore.effect(fn); +} + +export function batch(fn: BatchFunction): void { + SignalsCore.batch(fn); +} + +// eslint-disable-next-line spellcheck/spell-checker +export function untracked(fn: UntrackedFunction): T { + // eslint-disable-next-line spellcheck/spell-checker + return SignalsCore.untracked(fn); +} diff --git a/packages/devextreme/js/__internal/core/state_manager/prod/reactive_primitives/types.ts b/packages/devextreme/js/__internal/core/state_manager/prod/reactive_primitives/types.ts new file mode 100644 index 000000000000..6252595b9185 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/prod/reactive_primitives/types.ts @@ -0,0 +1,16 @@ +import type * as SignalsCore from '@preact/signals-core'; + +export type Signal = SignalsCore.Signal; +export type ReadonlySignal = SignalsCore.ReadonlySignal; + +export type EffectCleanup = () => void; + +// eslint-disable-next-line @typescript-eslint/no-invalid-void-type +export type EffectFn = () => void | EffectCleanup; + +export type ComputedFunction = () => T; + +export type BatchFunction = () => void; + +// eslint-disable-next-line spellcheck/spell-checker +export type UntrackedFunction = () => T; diff --git a/packages/devextreme/js/__internal/core/state_manager/reactive_primitives.test.ts b/packages/devextreme/js/__internal/core/state_manager/reactive_primitives.test.ts new file mode 100644 index 000000000000..214542800ca3 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/reactive_primitives.test.ts @@ -0,0 +1,81 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + describe, + expect, + it, +} from '@jest/globals'; + +import * as ReactiveDev from './dev/reactive_primitives/index'; +import type { + ReadonlySignal, + Signal, +} from './prod/reactive_primitives/index'; +import * as Reactive from './prod/reactive_primitives/index'; + +describe('Reactive wrapper', () => { + describe.each([ + ['Prod', Reactive], + ['Dev', ReactiveDev], + ])('%s version', (name, ReactiveModule) => { + it('signal correctly wrapped', () => { + const testSignal: Signal = ReactiveModule.signal(42); + expect(testSignal.value).toBe(42); + + if (name === 'Dev') { + expect('stack' in testSignal).toBeTruthy(); + } + }); + + it('computed correctly wrapped', () => { + const testSignal: Signal = ReactiveModule.signal(42); + expect(testSignal.value).toBe(42); + // eslint-disable-next-line @stylistic/max-len + const testComputed: ReadonlySignal = ReactiveModule.computed(() => testSignal.value * 2); + expect(testComputed.value).toBe(84); + + if (name === 'Dev') { + expect('stack' in testComputed).toBeTruthy(); + } + }); + + it('batch correctly wrapped', () => { + const testSignal: Signal = ReactiveModule.signal(42); + expect(testSignal.value).toBe(42); + + let batchRan = false; + ReactiveModule.batch(() => { + batchRan = true; + testSignal.value = 50; + }); + expect(batchRan).toBe(true); + }); + + it('effect and untracked correctly wrapped', () => { + const untrackedSignal = ReactiveModule.signal('Jane'); + const trackedSignal = ReactiveModule.signal('tracked'); + + let untrackedEffectRunCount = 0; + + const untrackedDispose = ReactiveModule.effect(() => { + untrackedEffectRunCount += 1; + ReactiveModule.untracked(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + untrackedSignal.value; + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + trackedSignal.value; + }); + + expect(untrackedEffectRunCount).toBe(1); + + untrackedSignal.value = 'Doe'; + expect(untrackedEffectRunCount).toBe(1); + + trackedSignal.value = 'updated'; + expect(untrackedEffectRunCount).toBe(2); + + untrackedDispose(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/core/state_manager/state_manager.test.ts b/packages/devextreme/js/__internal/core/state_manager/state_manager.test.ts new file mode 100644 index 000000000000..68bf946e332e --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/state_manager.test.ts @@ -0,0 +1,256 @@ +/* eslint-disable @typescript-eslint/init-declarations */ +/* eslint-disable max-classes-per-file */ +/* eslint-disable spellcheck/spell-checker */ +import { + afterAll, + beforeAll, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import type { Signal } from '@ts/core/state_manager/index'; +import { signal } from '@ts/core/state_manager/index'; + +import { DIContext } from '../di'; +import { setupStateManager } from './dev/setup_state_manager'; +import type { StateManager } from './dev/types'; + +const waitGarbageCollection = async (): Promise => { + if (!global.gc) { + throw new Error('Global gc is not defined. Did you use the `--expose-gc` flag?'); + } + + global.gc(); + await new Promise((resolve) => { + // eslint-disable-next-line no-restricted-globals + setTimeout(resolve, 0); + }); + global.gc(); +}; + +type TrackedSignal = Signal; + +describe('StateManager', () => { + const originalEnv = { ...process.env }; + + beforeAll(() => { + jest.resetModules(); + process.env = { ...originalEnv, NODE_ENV: 'development' }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + describe('Component state sources tracking', () => { + let diContext: DIContext; + let testComponentStateTracker: StateManager; + let initialPagerConfig: { infinityScrollingEnabled: boolean }; + let nonPreactSignalProperty: { text: string }; + let NoSignForIncludingItByStateManager: new () => { + someField: TrackedSignal; + }; + let DataController: new () => { + pagesCount: TrackedSignal; + pagerConfig: TrackedSignal<{ infinityScrollingEnabled: boolean }>; + nonPreactSignalProperty: { text: string }; + }; + let ColumnsController: new () => { + columnsCount: TrackedSignal; + text: TrackedSignal; + nonPreactSignalProperty: { text: string }; + }; + let ignoredInstance: InstanceType; + let columnsControllerInstance: InstanceType; + let dataControllerInstance: InstanceType; + + const updatedPagerConfig = { infinityScrollingEnabled: false }; + + beforeAll(() => { + diContext = new DIContext(); + + const stateManager = setupStateManager({ + diContext, + componentName: 'TestComponent', + logLevel: 'error', + }); + + if (!stateManager) { + throw Error('StateManager not initialized'); + } + + testComponentStateTracker = stateManager; + + initialPagerConfig = { + infinityScrollingEnabled: true, + }; + + nonPreactSignalProperty = { + text: 'non-preact-signal-property', + }; + + NoSignForIncludingItByStateManager = class { + someField = signal(true); + }; + + DataController = class { + pagesCount = signal(10); + + pagerConfig = signal(initialPagerConfig); + + nonPreactSignalProperty = nonPreactSignalProperty; + }; + + ColumnsController = class { + columnsCount = signal(5); + + text = signal('initial'); + + nonPreactSignalProperty = nonPreactSignalProperty; + }; + + ignoredInstance = new NoSignForIncludingItByStateManager(); + columnsControllerInstance = new ColumnsController(); + dataControllerInstance = new DataController(); + + diContext.registerInstance(ColumnsController, columnsControllerInstance); + diContext.registerInstance(DataController, dataControllerInstance); + diContext.registerInstance(NoSignForIncludingItByStateManager, ignoredInstance); + }); + + it('should ignore non-controllers', () => { + expect(testComponentStateTracker.getComponentState()).not.toMatchObject({ + NoSignForIncludingItByStateManagerController: expect.anything(), + }); + }); + + it('should get component signal after controllers registration', () => { + expect(testComponentStateTracker.getComponentState().ColumnsController).toEqual({ + columnsCount: 5, + text: 'initial', + }); + + expect(testComponentStateTracker.getComponentState().DataController).toEqual({ + pagesCount: 10, + pagerConfig: initialPagerConfig, + }); + }); + + it('should get deep copies of signal values', () => { + expect(testComponentStateTracker.getComponentState().DataController.pagerConfig) + .not.toBe(initialPagerConfig); + }); + + it('should preserve original controller signal values after signal tracker initialization', () => { + expect(columnsControllerInstance.columnsCount.peek()).toBe(5); + expect(columnsControllerInstance.text.peek()).toBe('initial'); + expect(dataControllerInstance.pagesCount.peek()).toBe(10); + expect(dataControllerInstance.pagerConfig.peek()) + .toBe(initialPagerConfig); + expect(dataControllerInstance.nonPreactSignalProperty).toEqual(nonPreactSignalProperty); + }); + + it('should track controllers signal updates', () => { + columnsControllerInstance.columnsCount.value = 10; + dataControllerInstance.pagesCount.value = 15; + dataControllerInstance.pagerConfig.value = updatedPagerConfig; + + expect(testComponentStateTracker.getComponentState().ColumnsController).toEqual({ + columnsCount: 10, + text: 'initial', + }); + + expect(testComponentStateTracker.getComponentState().DataController).toEqual({ + pagesCount: 15, + pagerConfig: updatedPagerConfig, + }); + }); + + it('should preserve original controller signal values after tracking controllers signal updates', () => { + expect(testComponentStateTracker.getComponentState().DataController.pagerConfig) + .not.toBe(updatedPagerConfig); + expect(columnsControllerInstance.text.peek()).toBe('initial'); + expect(columnsControllerInstance.columnsCount.peek()).toBe(10); + expect(dataControllerInstance.pagesCount.peek()).toBe(15); + expect(dataControllerInstance.pagerConfig.peek()) + .toBe(updatedPagerConfig); + expect(dataControllerInstance.nonPreactSignalProperty).toEqual(nonPreactSignalProperty); + }); + }); + + it('should allow garbage collection of a state source when a component is destroyed', async () => { + let diContext: DIContext | null = new DIContext(); + + const testComponentStateTracker = setupStateManager({ + diContext, + componentName: 'TestComponent', + logLevel: 'error', + stateSourceSign: /Test/, + }); + + if (!testComponentStateTracker) { + throw Error('StateManager not initialized'); + } + + class TestController { + testValue = signal(42); + } + + let controllerInstance: TestController | null = new TestController(); + const controllerInstanceWeakRef = new WeakRef(controllerInstance); + + diContext.registerInstance(TestController, controllerInstance); + + expect(testComponentStateTracker.getComponentState().TestController).toEqual({ + testValue: 42, + }); + + controllerInstance = null; + diContext = null; + + await waitGarbageCollection(); + + const isGarbageCollected = controllerInstanceWeakRef.deref() === undefined; + + expect(isGarbageCollected).toBe(true); + }); + + it('should allow garbage collection of a tracked state source properties when they are destroyed', async () => { + const diContext: DIContext | null = new DIContext(); + + const testComponentStateTracker = setupStateManager({ + diContext, + componentName: 'TestComponent', + logLevel: 'error', + stateSourceSign: /Test/, + }); + + if (!testComponentStateTracker) { + throw Error('StateManager not initialized'); + } + + class TestController { + testValue: Signal | null = signal(42); + } + + const controllerInstance: TestController | null = new TestController(); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const controllerInstanceTestValuePropertyWeakRef = new WeakRef(controllerInstance.testValue!); + + diContext.registerInstance(TestController, controllerInstance); + + expect(testComponentStateTracker.getComponentState().TestController).toEqual({ + testValue: 42, + }); + + controllerInstance.testValue = null; + + await waitGarbageCollection(); + + const isGarbageCollected = controllerInstanceTestValuePropertyWeakRef.deref() === undefined; + + expect(isGarbageCollected).toBe(true); + }); +}); diff --git a/packages/devextreme/js/__internal/core/state_manager/utils.test.ts b/packages/devextreme/js/__internal/core/state_manager/utils.test.ts new file mode 100644 index 000000000000..26cb8d455f20 --- /dev/null +++ b/packages/devextreme/js/__internal/core/state_manager/utils.test.ts @@ -0,0 +1,155 @@ +import { + describe, + expect, + it, +} from '@jest/globals'; + +import { + deepCopy, +} from './dev/utils'; + +describe('deepCopy', () => { + it('should create a deep copy of an object', () => { + const original = { a: 1, b: { c: 2 } }; + const copy = deepCopy(original); + + expect(copy).toEqual(original); + expect(copy).not.toBe(original); + expect(copy.b).not.toBe(original.b); + }); + + it('should create a deep copy of an array', () => { + const original = [1, 2, [3, 4]]; + const copy = deepCopy(original); + + expect(copy).toEqual(original); + expect(copy).not.toBe(original); + expect(copy[2]).not.toBe(original[2]); + }); + + it('should handle Date objects', () => { + const original = new Date('2023-01-01'); + const copy = deepCopy(original); + + expect(copy).toEqual(original); + expect(copy).not.toBe(original); + expect(copy.getTime()).toBe(original.getTime()); + }); + + it('should handle RegExp objects', () => { + const original = /test/gi; + const copy = deepCopy(original); + + expect(copy).toEqual(original); + expect(copy).not.toBe(original); + expect(copy.source).toBe(original.source); + expect(copy.flags).toBe(original.flags); + }); + + it('should handle nested objects and arrays', () => { + const original = { + a: [1, 2, { b: 3 }], + c: { d: [4, 5], e: { f: 6 } }, + }; + const copy = deepCopy(original); + + expect(copy).toEqual(original); + expect(copy).not.toBe(original); + expect(copy.a).not.toBe(original.a); + expect(copy.a[2]).not.toBe(original.a[2]); + expect(copy.c).not.toBe(original.c); + expect(copy.c.d).not.toBe(original.c.d); + expect(copy.c.e).not.toBe(original.c.e); + }); + + it('should handle circular references', () => { + interface ComplexCircularObj { + a: number; + self?: ComplexCircularObj; + b?: { + c: ComplexCircularObj; + }; + d?: ComplexCircularObj[]; + } + + const original: ComplexCircularObj = { a: 1 }; + original.self = original; + original.b = { c: original }; + original.d = [original]; + + const copy = deepCopy(original); + + expect(copy).not.toBe(original); + expect(copy.a).toBe(1); + expect(copy.self).toBe(copy); + expect(copy.b?.c).toBe(copy); + expect(copy.d?.[0]).toBe(copy); + }); + + it('should handle circular references in arrays', () => { + interface CircularArrayObj { + id: number; + items?: CircularArrayObj[]; + } + + const original: CircularArrayObj = { id: 1 }; + const child: CircularArrayObj = { id: 2 }; + + // Create circular reference: parent -> child -> parent + original.items = [child]; + child.items = [original]; + + const copy = deepCopy(original); + + expect(copy).not.toBe(original); + expect(copy.id).toBe(1); + expect(copy.items).not.toBe(original.items); + expect(copy.items?.[0]).not.toBe(original.items[0]); + expect(copy.items?.[0].id).toBe(2); + expect(copy.items?.[0].items?.[0]).toBe(copy); // Circular reference preserved + }); + + it('should handle complex nested circular references with arrays and objects', () => { + const original = { + name: 'root', + children: [] as any[], + parent: null as any, + }; + + const child1 = { + name: 'child1', + parent: original, + siblings: [] as any[], + }; + + const child2 = { + name: 'child2', + parent: original, + siblings: [child1] as any[], + }; + + child1.siblings = [child2]; + original.children = [child1, child2]; + + const copy = deepCopy(original); + + expect(copy).not.toBe(original); + expect(copy.name).toBe('root'); + expect(copy.children).not.toBe(original.children); + expect(copy.children.length).toBe(2); + + // Check first child + expect(copy.children[0]).not.toBe(original.children[0]); + expect(copy.children[0].name).toBe('child1'); + expect(copy.children[0].parent).toBe(copy); + + // Check second child + expect(copy.children[1]).not.toBe(original.children[1]); + expect(copy.children[1].name).toBe('child2'); + expect(copy.children[1].parent).toBe(copy); + + // Check sibling references + expect(copy.children[0].siblings[0]).toBe(copy.children[1]); + expect(copy.children[1].siblings[0]).toBe(copy.children[0]); + }); +}); diff --git a/packages/devextreme/js/__internal/core/utils/date.test.ts b/packages/devextreme/js/__internal/core/utils/date.test.ts index 4c51edb44d21..50728c0b7782 100644 --- a/packages/devextreme/js/__internal/core/utils/date.test.ts +++ b/packages/devextreme/js/__internal/core/utils/date.test.ts @@ -23,10 +23,10 @@ describe('Date utils', () => { .it('should add ms offsets to date correctly', ({ offsets, expectedResult, - }) => { + }: { offsets: number[]; expectedResult: Date }) => { const date = new Date('2023-09-05T00:00:00Z'); - const result = dateUtilsTs.addOffsets(date, offsets); + const result = dateUtilsTs.addOffsets(date, ...offsets); expect(result).toEqual(expectedResult); }); diff --git a/packages/devextreme/js/__internal/core/utils/date.ts b/packages/devextreme/js/__internal/core/utils/date.ts index 186a5b68ad73..47ee9ce0b610 100644 --- a/packages/devextreme/js/__internal/core/utils/date.ts +++ b/packages/devextreme/js/__internal/core/utils/date.ts @@ -1,5 +1,4 @@ -// TODO Vinogradov: Refactor offsets: number[] -> ...offsets: number[] -const addOffsets = (date: Date, offsets: number[]): Date => { +const addOffsets = (date: Date, ...offsets: number[]): Date => { const newDateMs = offsets.reduce( (result, offset) => result + offset, date.getTime(), @@ -7,8 +6,12 @@ const addOffsets = (date: Date, offsets: number[]): Date => { return new Date(newDateMs); }; -// eslint-disable-next-line @stylistic/max-len -const isValidDate = (date: unknown): date is Date | string | number => Boolean(date && !isNaN(new Date(date as Date).valueOf())); + +const isValidDate = ( + date: unknown, +): date is Date | string | number => Boolean( + date && !isNaN(new Date(date as Date).valueOf()), +); export const dateUtilsTs = { addOffsets, diff --git a/packages/devextreme/js/__internal/core/utils/m_date.ts b/packages/devextreme/js/__internal/core/utils/m_date.ts index 5b252005804f..897db6958bf4 100644 --- a/packages/devextreme/js/__internal/core/utils/m_date.ts +++ b/packages/devextreme/js/__internal/core/utils/m_date.ts @@ -425,24 +425,26 @@ function getDateIntervalByString(intervalString) { return result; } -function sameDate(date1, date2) { +function sameDate(date1, date2): boolean { return sameMonthAndYear(date1, date2) && date1.getDate() === date2.getDate(); } -function sameMonthAndYear(date1, date2) { +function sameMonthAndYear(date1, date2): boolean { return sameYear(date1, date2) && date1.getMonth() === date2.getMonth(); } -function sameYear(date1, date2) { +function sameYear(date1, date2): boolean { return date1 && date2 && date1.getFullYear() === date2.getFullYear(); } -function sameHoursAndMinutes(date1, date2) { +function sameHoursAndMinutes(date1, date2): boolean { return date1 && date2 && date1.getHours() === date2.getHours() && date1.getMinutes() === date2.getMinutes(); } -const sameDecade = function (date1, date2) { - if (!isDefined(date1) || !isDefined(date2)) return; +const sameDecade = function (date1, date2): boolean { + if (!isDefined(date1) || !isDefined(date2)) { + return false; + } const startDecadeDate1 = date1.getFullYear() - date1.getFullYear() % 10; const startDecadeDate2 = date2.getFullYear() - date2.getFullYear() % 10; @@ -450,8 +452,10 @@ const sameDecade = function (date1, date2) { return date1 && date2 && startDecadeDate1 === startDecadeDate2; }; -const sameCentury = function (date1, date2) { - if (!isDefined(date1) || !isDefined(date2)) return; +const sameCentury = function (date1, date2): boolean { + if (!isDefined(date1) || !isDefined(date2)) { + return false; + } const startCenturyDate1 = date1.getFullYear() - date1.getFullYear() % 100; const startCenturyDate2 = date2.getFullYear() - date2.getFullYear() % 100; @@ -540,20 +544,27 @@ function getLastDateInYear(year) { function getDayWeekNumber(date, firstDayOfWeek) { let day = date.getDay() - firstDayOfWeek + 1; - if (day <= 0) { day += DAYS_IN_WEEK; } + if (day <= 0) { + day += DAYS_IN_WEEK; + } return day; } -function getWeekNumber(date, firstDayOfWeek, rule) { - const firstWeekDayInYear = getDayWeekNumber(getFirstDateInYear(date.getFullYear()), firstDayOfWeek); +function getWeekNumber(date, firstDayOfWeek, rule): number { + const firstWeekDayInYear = getDayWeekNumber( + getFirstDateInYear(date.getFullYear()), + firstDayOfWeek, + ); const lastWeekDayInYear = getDayWeekNumber(getLastDateInYear(date.getFullYear()), firstDayOfWeek); const daysInFirstWeek = DAYS_IN_WEEK - firstWeekDayInYear + 1; let weekNumber = Math.ceil((getDayNumber(date) - daysInFirstWeek) / 7); switch (rule) { case 'fullWeek': { - if (daysInFirstWeek === DAYS_IN_WEEK) { weekNumber++; } + if (daysInFirstWeek === DAYS_IN_WEEK) { + weekNumber += 1; + } if (weekNumber === 0) { const lastDateInPreviousYear = getLastDateInYear(date.getFullYear() - 1); return getWeekNumber(lastDateInPreviousYear, firstDayOfWeek, rule); @@ -561,20 +572,28 @@ function getWeekNumber(date, firstDayOfWeek, rule) { return weekNumber; } case 'firstDay': { - if (daysInFirstWeek > 0) { weekNumber++; } + if (daysInFirstWeek > 0) { + weekNumber += 1; + } const isSunday = firstWeekDayInYear === SUNDAY_WEEK_NUMBER || lastWeekDayInYear === SUNDAY_WEEK_NUMBER; - if ((weekNumber > USUAL_WEEK_COUNT_IN_YEAR && !isSunday) || weekNumber === 54) { weekNumber = 1; } + if ((weekNumber > USUAL_WEEK_COUNT_IN_YEAR && !isSunday) || weekNumber === 54) { + weekNumber = 1; + } return weekNumber; } case 'firstFourDays': { - if (daysInFirstWeek > 3) { weekNumber++; } + if (daysInFirstWeek > 3) { + weekNumber += 1; + } const isThursday = firstWeekDayInYear === THURSDAY_WEEK_NUMBER || lastWeekDayInYear === THURSDAY_WEEK_NUMBER; - if (weekNumber > USUAL_WEEK_COUNT_IN_YEAR && !isThursday) { weekNumber = 1; } + if (weekNumber > USUAL_WEEK_COUNT_IN_YEAR && !isThursday) { + weekNumber = 1; + } if (weekNumber === 0) { const lastDateInPreviousYear = getLastDateInYear(date.getFullYear() - 1); @@ -583,6 +602,7 @@ function getWeekNumber(date, firstDayOfWeek, rule) { return weekNumber; } default: + return weekNumber; break; } } @@ -699,7 +719,7 @@ const makeDate = function (date) { }; const getDatesOfInterval = function (startDate, endDate, step) { - const result: any[] = []; + const result: Date[] = []; let currentDate = new Date(startDate.getTime()); while (currentDate < endDate) { diff --git a/packages/devextreme/js/__internal/core/utils/m_public_component.ts b/packages/devextreme/js/__internal/core/utils/m_public_component.ts index 3501b55626e0..a43883b6796d 100644 --- a/packages/devextreme/js/__internal/core/utils/m_public_component.ts +++ b/packages/devextreme/js/__internal/core/utils/m_public_component.ts @@ -43,7 +43,7 @@ export function attachInstanceToElement($element, componentInstance, disposeFn) data[COMPONENT_NAMES_DATA_KEY].push(name); } -export function getInstanceByElement($element, componentClass) { +export function getInstanceByElement($element, componentClass): T { const name = getName(componentClass); return elementData($element.get(0), name); diff --git a/packages/devextreme/js/__internal/core/utils/m_swatch_container.ts b/packages/devextreme/js/__internal/core/utils/swatch_container.ts similarity index 70% rename from packages/devextreme/js/__internal/core/utils/m_swatch_container.ts rename to packages/devextreme/js/__internal/core/utils/swatch_container.ts index 7d33b2e0465b..c426d0b9050d 100644 --- a/packages/devextreme/js/__internal/core/utils/m_swatch_container.ts +++ b/packages/devextreme/js/__internal/core/utils/swatch_container.ts @@ -1,13 +1,19 @@ +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { value } from '@js/core/utils/view_port'; const SWATCH_CONTAINER_CLASS_PREFIX = 'dx-swatch-'; -const getSwatchContainer = (element) => { +const getSwatchContainer = ( + element: Element | dxElementWrapper, +): dxElementWrapper => { const $element = $(element); const swatchContainer = $element.closest(`[class^="${SWATCH_CONTAINER_CLASS_PREFIX}"], [class*=" ${SWATCH_CONTAINER_CLASS_PREFIX}"]`); - const viewport = value(); - if (!swatchContainer.length) return viewport; + const viewport: dxElementWrapper = value(); + + if (!swatchContainer.length) { + return viewport; + } const swatchClassRegex = new RegExp(`(\\s|^)(${SWATCH_CONTAINER_CLASS_PREFIX}.*?)(\\s|$)`); const swatchClass = swatchContainer[0].className.match(swatchClassRegex)[2]; @@ -20,4 +26,4 @@ const getSwatchContainer = (element) => { return viewportSwatchContainer; }; -export default { getSwatchContainer: getSwatchContainer }; +export default { getSwatchContainer }; diff --git a/packages/devextreme/js/__internal/core/widget/dom_component.ts b/packages/devextreme/js/__internal/core/widget/dom_component.ts index 165499881a86..72c3779dd090 100644 --- a/packages/devextreme/js/__internal/core/widget/dom_component.ts +++ b/packages/devextreme/js/__internal/core/widget/dom_component.ts @@ -60,11 +60,9 @@ class DOMComponent< // eslint-disable-next-line @typescript-eslint/no-explicit-any _templateManager!: any; - // eslint-disable-next-line @stylistic/max-len - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types - static getInstance(element: Element | dxElementWrapper) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return getInstanceByElement($(element), this); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static getInstance(element: Element | dxElementWrapper): T { + return getInstanceByElement($(element), this); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types diff --git a/packages/devextreme/js/__internal/core/widget/widget.ts b/packages/devextreme/js/__internal/core/widget/widget.ts index 4d3511dc3f9a..4b9f5e03ee6f 100644 --- a/packages/devextreme/js/__internal/core/widget/widget.ts +++ b/packages/devextreme/js/__internal/core/widget/widget.ts @@ -5,6 +5,7 @@ import '@js/common/core/events/hover'; import { active, focus, hover, keyboard, } from '@js/common/core/events/short'; +import type { DeepPartial } from '@js/core'; import Action from '@js/core/action'; import devices from '@js/core/devices'; import type { DefaultOptionsRule } from '@js/core/options/utils'; @@ -30,6 +31,10 @@ export const FOCUSED_STATE_CLASS = 'dx-state-focused'; export const HOVER_STATE_CLASS = 'dx-state-hover'; const INVISIBLE_STATE_CLASS = 'dx-state-invisible'; +export const EMPTY_ACTIVE_STATE_UNIT = ''; +const DEFAULT_FEEDBACK_HIDE_TIMEOUT = 400; +const DEFAULT_FEEDBACK_SHOW_TIMEOUT = 30; + export type SupportedKeyHandler = ( e: DxEvent, options?: KeyboardKeyDownEvent @@ -69,12 +74,6 @@ export interface WidgetProperties extends WidgetOptions extends DOMComponent, TProperties> { - public _activeStateUnit!: string; - - public _feedbackHideTimeout = 400; - - private readonly _feedbackShowTimeout: number = 30; - _contentReadyAction?: ((event?: Record) => void) | null; protected _keyboardListenerId?: string | null; @@ -96,6 +95,18 @@ class Widget< return options; } + protected _activeStateUnit(): string { + return EMPTY_ACTIVE_STATE_UNIT; + } + + protected _feedbackHideTimeout(): number { + return DEFAULT_FEEDBACK_HIDE_TIMEOUT; + } + + protected _feedbackShowTimeout(): number { + return DEFAULT_FEEDBACK_SHOW_TIMEOUT; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars _supportedKeys(e?: DxEvent): SupportedKeys { return {}; @@ -124,18 +135,22 @@ class Widget< } _defaultOptionsRules(): DefaultOptionsRule[] { - return super._defaultOptionsRules().concat([{ - device(): boolean { - const device = devices.real(); - const { platform } = device; - const { version } = device; - return platform === 'ios' && compareVersions(version, '13.3') <= 0; - }, - // @ts-expect-error - options: { - useResizeObserver: false, + const rules = [ + ...super._defaultOptionsRules(), + { + device(): boolean { + const device = devices.real(); + const { platform } = device; + const { version } = device; + return platform === 'ios' && compareVersions(version, '13.3') <= 0; + }, + options: { + useResizeObserver: false, + } as DeepPartial, }, - }]); + ]; + + return rules; } _init(): void { @@ -286,13 +301,13 @@ class Widget< } _findActiveTarget($element: dxElementWrapper): dxElementWrapper { - return $element.find(this._activeStateUnit).not(`.${DISABLED_STATE_CLASS}`); + return $element.find(this._activeStateUnit()).not(`.${DISABLED_STATE_CLASS}`); } _getActiveElement(): dxElementWrapper { const activeElement = this._eventBindingTarget(); - if (this._activeStateUnit) { + if (this._activeStateUnit()) { return this._findActiveTarget(activeElement); } @@ -435,7 +450,7 @@ class Widget< _attachHoverEvents(): void { const { hoverStateEnabled } = this.option(); - const selector = this._activeStateUnit; + const selector = this._activeStateUnit(); const namespace = 'UIFeedback'; const $el = this._eventBindingTarget(); @@ -454,7 +469,7 @@ class Widget< _attachFeedbackEvents(): void { const { activeStateEnabled } = this.option(); - const selector = this._activeStateUnit; + const selector = this._activeStateUnit(); const namespace = 'UIFeedback'; const $el = this._eventBindingTarget(); @@ -469,8 +484,8 @@ class Widget< { excludeValidators: ['disabled', 'readOnly'] }, ), { - showTimeout: this._feedbackShowTimeout, - hideTimeout: this._feedbackHideTimeout, + showTimeout: this._feedbackShowTimeout(), + hideTimeout: this._feedbackHideTimeout(), selector, namespace, }, @@ -522,7 +537,7 @@ class Widget< } _findHoverTarget($el?: dxElementWrapper): dxElementWrapper | undefined { - return $el?.closest(this._activeStateUnit || this._eventBindingTarget()); + return $el?.closest(this._activeStateUnit() || this._eventBindingTarget()); } _hover($el: dxElementWrapper | undefined, $previous: dxElementWrapper | undefined): void { diff --git a/packages/devextreme/js/__internal/data/m_local_store.ts b/packages/devextreme/js/__internal/data/m_local_store.ts index 6bf755e1db08..fb49e9a1f466 100644 --- a/packages/devextreme/js/__internal/data/m_local_store.ts +++ b/packages/devextreme/js/__internal/data/m_local_store.ts @@ -1,18 +1,34 @@ +/* eslint-disable max-classes-per-file */ import eventsEngine from '@js/common/core/events/core/events_engine'; import ArrayStore from '@js/common/data/array_store'; import { errors } from '@js/common/data/errors'; -import Class from '@js/core/class'; import domAdapter from '@js/core/dom_adapter'; import { getWindow } from '@js/core/utils/window'; +import Store from './m_abstract_store'; + const window = getWindow(); -const { abstract } = Class; -const LocalStoreBackend = Class.inherit({ +class LocalStoreBackend { + _store: any; + + _dirty: boolean; + + _immediate: boolean; - ctor(store, storeOptions) { + _key: string; + + constructor(store, storeOptions) { this._store = store; this._dirty = !!storeOptions.data; + + const { name } = storeOptions; + if (!name) { + throw errors.Error('E4013'); + } + + this._key = `dx-data-localStore-${name}`; + this.save(); const immediate = this._immediate = storeOptions.immediate; @@ -27,105 +43,92 @@ const LocalStoreBackend = Class.inherit({ domAdapter.listen(domAdapter.getDocument(), 'pause', saveProxy, false); } } - }, + } - notifyChanged() { + notifyChanged(): void { this._dirty = true; if (this._immediate) { this.save(); } - }, + } - load() { + load(): void { this._store._array = this._loadImpl(); this._dirty = false; - }, + } - save() { + save(): void { if (!this._dirty) { return; } this._saveImpl(this._store._array); this._dirty = false; - }, + } - _loadImpl: abstract, - _saveImpl: abstract, -}); - -const DomLocalStoreBackend = LocalStoreBackend.inherit({ - - ctor(store, storeOptions) { - const { name } = storeOptions; - if (!name) { - throw errors.Error('E4013'); - } - this._key = `dx-data-localStore-${name}`; - - this.callBase(store, storeOptions); - }, - - _loadImpl() { + _loadImpl(): any { const raw = window.localStorage.getItem(this._key); if (raw) { return JSON.parse(raw); } return []; - }, + } - _saveImpl(array) { + _saveImpl(array): void { if (!array.length) { window.localStorage.removeItem(this._key); } else { window.localStorage.setItem(this._key, JSON.stringify(array)); } - }, + } +} +class LocalStore extends ArrayStore { + _backend: LocalStoreBackend; -}); + _array: any; -const localStoreBackends = { - dom: DomLocalStoreBackend, -}; -const LocalStore = ArrayStore.inherit({ - - ctor(options) { + constructor(options) { if (typeof options === 'string') { options = { name: options }; } else { options = options || {}; } - this.callBase(options); + super(options); + this._array = options.data || []; - this._backend = new localStoreBackends[options.backend || 'dom'](this, options); + this._backend = new LocalStoreBackend(this, options); this._backend.load(); - }, + } - _clearCache() { + _clearCache(): void { this._backend.load(); - }, + } - clear() { - this.callBase(); + clear(): void { + super.clear(); this._backend.notifyChanged(); - }, + } - _insertImpl(values) { + _insertImpl(values): any { const b = this._backend; - return this.callBase(values).done(b.notifyChanged.bind(b)); - }, + return super._insertImpl(values).done(b.notifyChanged.bind(b)); + } - _updateImpl(key, values) { + _updateImpl(key, values): any { const b = this._backend; - return this.callBase(key, values).done(b.notifyChanged.bind(b)); - }, + return super._updateImpl(key, values).done(b.notifyChanged.bind(b)); + } - _removeImpl(key) { + _removeImpl(key): any { const b = this._backend; - return this.callBase(key).done(b.notifyChanged.bind(b)); - }, -}, 'local'); + return super._removeImpl(key).done(b.notifyChanged.bind(b)); + } +} + +// Preserve alias registration used by Store.create('local', ...) +// @ts-expect-error register ES6 class with alias +Store.registerClass(LocalStore, 'local'); export default LocalStore; diff --git a/packages/devextreme/js/__internal/events/core/m_consts.ts b/packages/devextreme/js/__internal/events/core/m_consts.ts new file mode 100644 index 000000000000..152b7a70b6be --- /dev/null +++ b/packages/devextreme/js/__internal/events/core/m_consts.ts @@ -0,0 +1,74 @@ +/* eslint-disable spellcheck/spell-checker */ +export const EMPTY_EVENT_NAME = 'dxEmptyEventType'; +export const NATIVE_EVENTS_TO_SUBSCRIBE = { + mouseenter: 'mouseover', + mouseleave: 'mouseout', + pointerenter: 'pointerover', + pointerleave: 'pointerout', +}; +export const NATIVE_EVENTS_TO_TRIGGER = { + focusin: 'focus', + focusout: 'blur', +}; +export const NO_BUBBLE_EVENTS = ['blur', 'focus', 'load']; + +export const forcePassiveFalseEventNames = ['touchmove', 'wheel', 'mousewheel', 'touchstart']; + +export const EVENT_PROPERTIES = [ + 'altKey', + 'altitudeAngle', + 'azimuthAngle', + 'bubbles', + 'button', + 'buttons', + 'cancelable', + 'cancelBubble', + 'changedTouches', + 'char', + 'charCode', + 'clipboardData', + 'code', + 'composed', + 'ctrlKey', + 'defaultPrevented', + 'delegateTarget', + 'deltaMode', + 'deltaX', + 'deltaY', + 'deltaZ', + 'detail', + 'eventPhase', + 'height', + 'isComposing', + 'isPrimary', + 'key', + 'keyCode', + 'layerX', + 'layerY', + 'location', + 'metaKey', + 'movementX', + 'movementY', + 'offsetX', + 'offsetY', + 'pointerId', + 'pointerType', + 'pressure', + 'relatedTarget', + 'repeat', + 'returnValue', + 'srcElement', + 'shiftKey', + 'tangentialPressure', + 'target', + 'targetTouches', + 'tiltX', + 'tiltY', + 'toElement', + 'touches', + 'twist', + 'view', + 'width', + 'x', + 'y', +] as const; diff --git a/packages/devextreme/js/__internal/events/core/m_events_engine.ts b/packages/devextreme/js/__internal/events/core/m_events_engine.ts index 020feab53402..b11f4ce2a4d4 100644 --- a/packages/devextreme/js/__internal/events/core/m_events_engine.ts +++ b/packages/devextreme/js/__internal/events/core/m_events_engine.ts @@ -11,55 +11,17 @@ import { isFunction, isObject, isString, isWindow, } from '@js/core/utils/type'; import { getWindow, hasWindow } from '@js/core/utils/window'; +import { + EMPTY_EVENT_NAME, + EVENT_PROPERTIES, + forcePassiveFalseEventNames, + NATIVE_EVENTS_TO_SUBSCRIBE, + NATIVE_EVENTS_TO_TRIGGER, + NO_BUBBLE_EVENTS, +} from '@ts/events/core/m_consts'; const window = getWindow(); -/* eslint-disable spellcheck/spell-checker */ -const EMPTY_EVENT_NAME = 'dxEmptyEventType'; -const NATIVE_EVENTS_TO_SUBSCRIBE = { - mouseenter: 'mouseover', - mouseleave: 'mouseout', - pointerenter: 'pointerover', - pointerleave: 'pointerout', -}; -const NATIVE_EVENTS_TO_TRIGGER = { - focusin: 'focus', - focusout: 'blur', -}; -const NO_BUBBLE_EVENTS = ['blur', 'focus', 'load']; - -const forcePassiveFalseEventNames = ['touchmove', 'wheel', 'mousewheel', 'touchstart']; - -const EVENT_PROPERTIES = [ - 'target', - 'relatedTarget', - 'delegateTarget', - 'altKey', - 'bubbles', - 'cancelable', - 'changedTouches', - 'ctrlKey', - 'detail', - 'eventPhase', - 'metaKey', - 'shiftKey', - 'view', - 'char', - 'code', - 'charCode', - 'key', - 'keyCode', - 'button', - 'buttons', - 'offsetX', - 'offsetY', - 'pointerId', - 'pointerType', - 'targetTouches', - 'toElement', - 'touches', -]; - function matchesSafe(target, selector) { return !isWindow(target) && target.nodeName !== '#document' && domAdapter.elementMatches(target, selector); } diff --git a/packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts b/packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts index dba4b049c603..7b004e4b7663 100644 --- a/packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts +++ b/packages/devextreme/js/__internal/filter_builder/m_filter_builder.ts @@ -13,7 +13,7 @@ import Popup from '@js/ui/popup/ui.popup'; import EditorFactoryMixin from '@js/ui/shared/ui.editor_factory_mixin'; import TreeView from '@js/ui/tree_view'; import Widget from '@js/ui/widget/ui.widget'; -import { getElementMaxHeightByWindow } from '@ts/ui/overlay/m_utils'; +import { getElementMaxHeightByWindow } from '@ts/ui/overlay/utils'; import { addItem, convertToInnerStructure, diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index b912953a5bce..96bfc370b10a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -376,25 +376,45 @@ export class ColumnChooserView extends ColumnsView { this._columnChooserList.endUpdate(); } - protected _columnOptionChanged(e) { - super._columnOptionChanged(e); + protected _columnOptionChanged(changes): void { + super._columnOptionChanged(changes); + const { optionNames } = changes; const isSelectMode = this.isSelectMode(); + const onlyVisibleChanged = this.isColumnVisibilityOnlyUpdated(optionNames); + const isOnlyColumnVisibilityUpdated = this._isUpdatingColumnVisibility + && onlyVisibleChanged; - if (isSelectMode && this._columnChooserList && !this._isUpdatingColumnVisibility) { - const { optionNames } = e; - const onlyVisibleChanged = optionNames.visible && optionNames.length === 1; - const columnIndices = isDefined(e.columnIndex) ? [e.columnIndex] : e.columnIndices; - const needUpdate = COLUMN_OPTIONS_USED_IN_ITEMS.some((optionName) => optionNames[optionName]) || (e.changeTypes.columns && optionNames.all); + if (!isSelectMode || !this._columnChooserList || isOnlyColumnVisibilityUpdated) { + return; + } - if (needUpdate) { - this._updateItemsSelection(columnIndices); + const columnIndices = isDefined(changes.columnIndex) + ? [changes.columnIndex] + : changes.columnIndices; + const hasItemsOptionNames = COLUMN_OPTIONS_USED_IN_ITEMS + .some((optionName) => optionNames[optionName]); + const needUpdate: boolean = hasItemsOptionNames + || (changes.changeTypes.columns && optionNames.all); - if (!onlyVisibleChanged) { - this._updateItems(); - } - } + if (!needUpdate) { + return; } + + this._updateItemsSelection(columnIndices); + if (!onlyVisibleChanged) { + this._updateItems(); + } + } + + private isColumnVisibilityOnlyUpdated( + optionNames: { length: number } & Record, + ): boolean { + const optionKeys = Object + .keys(optionNames ?? {}) + .filter((key) => key !== 'length'); + + return optionKeys.length === 1 && optionKeys[0] === 'visible'; } public getColumnElements() { diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/const.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/const.ts index b558b7550e5b..a34db827b193 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/const.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/const.ts @@ -27,3 +27,10 @@ export const COLUMN_INDEX_OPTIONS = { }; export const GROUP_LOCATION = 'group'; export const COLUMN_CHOOSER_LOCATION = 'columnChooser'; + +export const UNSUPPORTED_PROPERTIES_FOR_CHILD_COLUMNS = [ + 'fixed', + 'fixedPosition', + 'type', + 'buttons', +]; diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts index 68a489e4222e..8ef794c110bd 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts @@ -136,6 +136,8 @@ export class ColumnsController extends modules.Controller { protected _stateStoringController!: StateStoringController; + public _isWarnedAboutUnsupportedProperties?: boolean; + public init(isApplyingUserState?): void { this._dataController = this.getController('data'); this._focusController = this.getController('focus'); @@ -613,17 +615,26 @@ export class ColumnsController extends modules.Controller { expandColumn = this.columnOption('command:expand'); } - expandColumns = map(expandColumns, (column) => extend({}, column, { - visibleWidth: null, - minWidth: null, - cellTemplate: !isDefined(column.groupIndex) ? column.cellTemplate : null, - headerCellTemplate: null, - fixed: !isDefined(column.groupIndex) || !isFixedFirstGroupColumn ? isColumnFixing : true, - fixedPosition: rtlEnabled ? 'right' : 'left', - }, expandColumn, { - index: column.index, - type: column.type || GROUP_COMMAND_COLUMN_NAME, - })); + expandColumns = map(expandColumns, (column) => extend( + {}, + { + ...column, + ownerBand: undefined, + }, + { + visibleWidth: null, + minWidth: null, + cellTemplate: !isDefined(column.groupIndex) ? column.cellTemplate : null, + headerCellTemplate: null, + fixed: !isDefined(column.groupIndex) || !isFixedFirstGroupColumn ? isColumnFixing : true, + fixedPosition: rtlEnabled ? 'right' : 'left', + }, + expandColumn, + { + index: column.index, + type: column.type || GROUP_COMMAND_COLUMN_NAME, + }, + )); return expandColumns; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts index d45f21c8a25b..5f1c25683847 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts @@ -12,6 +12,7 @@ import { isDefined, isFunction, isNumeric, isObject, isString, type, } from '@js/core/utils/type'; import variableWrapper from '@js/core/utils/variable_wrapper'; +import errors from '@js/ui/widget/ui.errors'; import { HIDDEN_COLUMNS_WIDTH } from '../adaptivity/const'; import gridCoreUtils from '../m_utils'; @@ -24,11 +25,36 @@ import { GROUP_COMMAND_COLUMN_NAME, GROUP_LOCATION, IGNORE_COLUMN_OPTION_NAMES, + UNSUPPORTED_PROPERTIES_FOR_CHILD_COLUMNS, USER_STATE_FIELD_NAMES, USER_STATE_FIELD_NAMES_15_1, } from './const'; import type { ColumnsController } from './m_columns_controller'; +const warnFixedInChildColumnsOnce = (controller: ColumnsController, childColumns: any[]): void => { + if (controller?._isWarnedAboutUnsupportedProperties) return; + if (!childColumns || !Array.isArray(childColumns) || childColumns?.length === 0) return; + + let unsupportedProperty: string | null = null; + + for (const column of childColumns) { + if (unsupportedProperty) break; + if (!column || typeof column !== 'object' || column === null) continue; + + for (const property of UNSUPPORTED_PROPERTIES_FOR_CHILD_COLUMNS) { + if (property in column) { + unsupportedProperty = property; + break; + } + } + } + + if (unsupportedProperty) { + controller && (controller._isWarnedAboutUnsupportedProperties = true); + errors.log('W1028', unsupportedProperty); + } +}; + export const setFilterOperationsAsDefaultValues = function (column) { column.filterOperations = column.defaultFilterOperations; }; @@ -47,7 +73,7 @@ export const createColumn = function (that: ColumnsController, columnOptions, us that.setName(columnOptions); - let result = { }; + let result = {}; if (columnOptions.command) { result = deepExtendArraySafe(commonColumnOptions, columnOptions); } else { @@ -90,6 +116,7 @@ export const createColumnsFromOptions = function (that: ColumnsController, colum result.push(column); if (column.columns) { + warnFixedInChildColumnsOnce(that, column.columns); result = result.concat(createColumnsFromOptions(that, column.columns, column, result.length)); delete column.columns; column.hasColumns = true; @@ -945,7 +972,7 @@ const isFirstOrLastBandColumn = function ( fixedPosition?: StickyPosition, ): boolean { return bandColumns.every((column, index) => onlyWithinBandColumn && index === 0 - || isFirstOrLastColumnCore(that, column, index, onlyWithinBandColumn, isLast, fixedPosition)); + || isFirstOrLastColumnCore(that, column, index, onlyWithinBandColumn, isLast, fixedPosition)); }; const isFirstOrLastColumnCore = function ( @@ -991,7 +1018,7 @@ export const isFirstOrLastColumn = function ( ): boolean { const targetColumnIndex = targetColumn.index; const bandColumnsCache = that.getBandColumnsCache(); - const parentBandColumns = getParentBandColumns(targetColumnIndex, bandColumnsCache.columnParentByIndex); + const parentBandColumns = !isDefined(targetColumn.type) && getParentBandColumns(targetColumnIndex, bandColumnsCache.columnParentByIndex); if (parentBandColumns?.length) { return isFirstOrLastBandColumn(that, parentBandColumns.concat([targetColumn]), onlyWithinBandColumn, isLast, fixedPosition); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts index 7dbc2e46eec1..0648dd29b8b5 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts @@ -20,7 +20,7 @@ import { setHeight, setWidth, } from '@js/core/utils/size'; import { isDefined, isObject, isString } from '@js/core/utils/type'; -import swatchContainer from '@ts/core/utils/m_swatch_container'; +import swatchContainer from '@ts/core/utils/swatch_container'; import type { EditorFactory } from '@ts/grids/grid_core/editor_factory/m_editor_factory'; import type { ColumnPoint, ModuleType } from '@ts/grids/grid_core/m_types'; import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts index 7f809b58c5a4..508293d40e94 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/method-signature-style */ import ArrayStore from '@js/common/data/array_store'; import { CustomStore } from '@js/common/data/custom_store'; import $ from '@js/core/renderer'; @@ -609,7 +608,6 @@ export class DataController extends DataHelperMixin(modules.Controller) { this.pushed.fire(changes); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public fireError(...args: any[]) { this.dataErrorOccurred.fire(errors.Error.apply(errors, args)); } @@ -1642,7 +1640,7 @@ export class DataController extends DataHelperMixin(modules.Controller) { if (options === true) { options = { reload: true, changesOnly: true }; } else if (!options) { - options = { lookup: true, selection: true, reload: true }; + options = { reload: true, lookup: true }; } const that = this; @@ -1734,7 +1732,7 @@ export class DataController extends DataHelperMixin(modules.Controller) { /** * @extended: editing, virtual_scrolling */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars + public reload(reload?, changesOnly?): any { return this._dataSource?.reload(reload, changesOnly); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.integration.test.ts new file mode 100644 index 000000000000..4454c9640e9a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.integration.test.ts @@ -0,0 +1,103 @@ +import { + afterEach, describe, expect, it, jest, +} from '@jest/globals'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; +import DataGrid from '@js/ui/data_grid'; + +const SELECTORS = { + gridContainer: '#gridContainer', +}; + +const GRID_CONTAINER_ID = 'gridContainer'; + +const createDataGrid = async ( + options: DataGridProperties = {}, +): Promise<{ $container: dxElementWrapper; instance: DataGrid }> => new Promise((resolve) => { + const $container = $('
') + .attr('id', GRID_CONTAINER_ID) + .appendTo(document.body); + + const instance = new DataGrid($container.get(0) as HTMLDivElement, { + // @ts-ignore + loadingTimeout: null, + ...options, + }); + + resolve({ $container, instance }); +}); + +describe('GridCore focus', () => { + afterEach(() => { + const $container = $(SELECTORS.gridContainer); + const dataGrid = ($container as any).dxDataGrid('instance') as DataGrid; + + dataGrid.dispose(); + $container.remove(); + }); + + const testCases: [boolean, 'insert' | 'remove' | 'update', number][] = [ + [true, 'insert', 2], + [true, 'remove', 0], + [true, 'update', 2], + [false, 'insert', 2], + [false, 'remove', 0], + [false, 'update', 2], + ]; + + // T1292991 + describe.each(testCases)( + 'when repaintChangesOnly=%s and performing %s operation', + (repaintChangesOnly, operation, expectedFocusedRowIndex) => { + it('should updates the focused row index correctly', async () => { + const onFocusedRowChanged = jest.fn(); + const { instance } = await createDataGrid({ + dataSource: { + store: { + type: 'array', + data: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + { id: 3, name: 'Item 3' }, + ], + key: 'id', + }, + reshapeOnPush: true, + pushAggregationTimeout: 0, + }, + showBorders: true, + focusedRowEnabled: true, + focusedRowKey: 2, + onFocusedRowChanged, + repaintChangesOnly, + columns: [ + { dataField: 'id', width: 80 }, + { dataField: 'name', caption: 'Name', sortOrder: 'asc' }, + ], + }); + const store = instance.getDataSource().store(); + + onFocusedRowChanged.mockClear(); + + switch (operation) { + case 'insert': + store.push([{ type: 'insert', index: 0, data: { name: 'Item 0' } }]); + break; + case 'remove': + store.push([{ type: 'remove', key: 1 }]); + break; + case 'update': + store.push([{ type: 'update', key: 3, data: { id: 3, name: 'A Item 3' } }]); + break; + default: + break; + } + + expect(onFocusedRowChanged.mock.calls.length).toBe(1); + expect(instance.option('focusedRowKey')).toEqual(2); + expect(instance.option('focusedRowIndex')).toEqual(expectedFocusedRowIndex); + }); + }, + ); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.ts b/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.ts index 3ddc7c5270e2..0535c912428e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.ts @@ -1,9 +1,10 @@ /* eslint-disable max-classes-per-file */ import $ from '@js/core/renderer'; import { equalByValue } from '@js/core/utils/common'; -import { Deferred, when } from '@js/core/utils/deferred'; +import { Deferred, type DeferredObj, when } from '@js/core/utils/deferred'; import { each } from '@js/core/utils/iterator'; import { isBoolean, isDefined } from '@js/core/utils/type'; +import type { Key } from '@ts/grids/new/grid_core/data_controller/types'; import type { ColumnsController } from '../columns_controller/m_columns_controller'; import type { DataController } from '../data_controller/m_data_controller'; @@ -53,8 +54,9 @@ export class FocusController extends core.ViewController { return; } - this._focusRowByKey(value); - this.getKeyboardController()._fireFocusedRowChanged(); + this._focusRowByKey(value).done(() => { + this.getKeyboardController()._fireFocusedRowChanged(); + }); args.handled = true; break; @@ -82,11 +84,11 @@ export class FocusController extends core.ViewController { if (!this.option('focusedRowEnabled')) { return; } - const isEmptyData = this.getDataController().isEmpty(); - const currentIndex = this._getCurrentFocusRowIndex(isEmptyData, index); + + const currentIndex = index !== undefined ? index : this.option('focusedRowIndex'); if (currentIndex < 0) { - if (isEmptyData || this.isAutoNavigateToFocusedRow()) { + if (this.isAutoNavigateToFocusedRow()) { this._resetFocusedRow(); } } else { @@ -94,18 +96,6 @@ export class FocusController extends core.ViewController { } } - private _getCurrentFocusRowIndex(isEmptyData, index?): number { - let currentIndex = index; - if (currentIndex === undefined) { - if (isEmptyData) { - currentIndex = -1; - } else { - currentIndex = this.option('focusedRowIndex'); - } - } - return currentIndex; - } - private _focusRowByIndexCore(index, operationTypes) { const pageSize = this.getDataController().pageSize(); const setKeyByIndex = () => { @@ -166,12 +156,14 @@ export class FocusController extends core.ViewController { } } - private _focusRowByKey(key) { + private _focusRowByKey( + key: Key, + ): DeferredObj { if (!isDefined(key)) { this._resetFocusedRow(); - } else { - this._navigateToRow(key, true); + return Deferred().resolve(); } + return this._navigateToRow(key, true); } public _resetFocusedRow() { @@ -205,10 +197,13 @@ export class FocusController extends core.ViewController { if (!this.isAutoNavigateToFocusedRow()) { this.option('focusedRowIndex', -1); } - return this._navigateToRow(key); + return this._navigateToRow(key, false); } - public _navigateToRow(key, needFocusRow?) { + public _navigateToRow( + key: Key, + needFocusRow: boolean, + ): DeferredObj { const that = this; const isAutoNavigate = that.isAutoNavigateToFocusedRow(); // @ts-expect-error @@ -553,7 +548,7 @@ const columns = (Base: ModuleType) => class FocusColumnsExten }; const data = (Base: ModuleType) => class FocusDataControllerExtender extends Base { - private _needToUpdateFocusedRowByIndex = false; + private _isDataPushed = false; protected _applyChange(change) { if (change && change.changeType === 'updateFocusedRow') return; @@ -565,20 +560,23 @@ const data = (Base: ModuleType) => class FocusDataControllerExte protected _fireChanged(e) { super._fireChanged(e); + const forceUpdateFocusedRow = this._isDataPushed; + + this._isDataPushed = false; + if (this.option('focusedRowEnabled') && this._dataSource) { const isPartialUpdate = e.changeType === 'update' && e.repaintChangesOnly; const isPartialUpdateWithDeleting = isPartialUpdate && e.changeTypes && e.changeTypes.indexOf('remove') >= 0; - if (this._needToUpdateFocusedRowByIndex) { - this._needToUpdateFocusedRowByIndex = false; - this._focusController._focusRowByIndex(); + if (forceUpdateFocusedRow && this.isEmpty()) { + this._focusController._resetFocusedRow(); } else if (e.changeType === 'refresh' && e.items.length || isPartialUpdateWithDeleting) { this._updatePageIndexes(); - this._updateFocusedRow(e); + this._updateFocusedRowIfNeeded(e, forceUpdateFocusedRow); } else if (e.changeType === 'append' || e.changeType === 'prepend') { this._updatePageIndexes(); - } else if (e.changeType === 'update' && e.repaintChangesOnly) { - this._updateFocusedRow(e); + } else if (isPartialUpdate) { + this._updateFocusedRowIfNeeded(e, forceUpdateFocusedRow); } } } @@ -588,7 +586,7 @@ const data = (Base: ModuleType) => class FocusDataControllerExte const focusedRowKey = this.option('focusedRowKey'); - this._needToUpdateFocusedRowByIndex = changes?.some((change) => change.type === 'remove' && equalByValue(change.key, focusedRowKey)); + this._isDataPushed = isDefined(focusedRowKey) && !!changes.length; } private _updatePageIndexes() { @@ -603,7 +601,7 @@ const data = (Base: ModuleType) => class FocusDataControllerExte return this._isPagingByRendering; } - private _updateFocusedRow(e) { + private _updateFocusedRowIfNeeded(e, forceUpdate = false) { const operationTypes = e.operationTypes || {}; const { reload, fullReload, pageIndex, paging, @@ -613,30 +611,44 @@ const data = (Base: ModuleType) => class FocusDataControllerExte const focusedRowKey = this.option('focusedRowKey'); const isAutoNavigate = this._focusController.isAutoNavigateToFocusedRow(); const isReload = reload && pageIndex === false; - if (isReload && !fullReload && isDefined(focusedRowKey)) { - this._focusController._navigateToRow(focusedRowKey, true) - .done((focusedRowIndex) => { - if (focusedRowIndex < 0) { - this._focusController._focusRowByIndex(undefined, operationTypes); - } - }); - } else if (pagingWithoutVirtualScrolling && isAutoNavigate) { - const rowIndexByKey = this.getRowIndexByKey(focusedRowKey); - const focusedRowIndex = this.option('focusedRowIndex')!; - const isValidRowIndexByKey = rowIndexByKey >= 0; - const isValidFocusedRowIndex = focusedRowIndex >= 0; - const isSameRowIndex = focusedRowIndex === rowIndexByKey; - if (isValidFocusedRowIndex && (isSameRowIndex || !isValidRowIndexByKey)) { - this._focusController._focusRowByIndex(focusedRowIndex, operationTypes); + const rowIndexByKey = this.getRowIndexByKey(focusedRowKey); + + switch (true) { + case forceUpdate: { + this._focusController._focusRowByKeyOrIndex(); + break; + } + case isReload && !fullReload && isDefined(focusedRowKey): { + this._focusController._navigateToRow(focusedRowKey, true) + .done((focusedRowIndex) => { + if (focusedRowIndex < 0) { + this._focusController._focusRowByIndex(undefined, operationTypes); + } + }); + break; + } + case pagingWithoutVirtualScrolling && isAutoNavigate: { + const focusedRowIndex = this.option('focusedRowIndex')!; + const isValidRowIndexByKey = rowIndexByKey >= 0; + const isValidFocusedRowIndex = focusedRowIndex >= 0; + const isSameRowIndex = focusedRowIndex === rowIndexByKey; + + if (isValidFocusedRowIndex && (isSameRowIndex || !isValidRowIndexByKey)) { + this._focusController._focusRowByIndex(focusedRowIndex, operationTypes); + } + break; + } + case pagingWithoutVirtualScrolling && !isAutoNavigate && (rowIndexByKey < 0): { + this.option('focusedRowIndex', -1); + break; + } + case operationTypes.fullReload: { + this._focusController._focusRowByKeyOrIndex(); + break; + } + default: { + break; } - } else if ( - pagingWithoutVirtualScrolling - && !isAutoNavigate - && (this.getRowIndexByKey(focusedRowKey) < 0) - ) { - this.option('focusedRowIndex', -1); - } else if (operationTypes.fullReload) { - this._focusController._focusRowByKeyOrIndex(); } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts index 70117051948f..4ce31246b0b9 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts @@ -88,6 +88,7 @@ import { isDetailRow, isEditForm, isEditorCell, + isEditRow, isElementDefined, isGroupFooterRow, isGroupRow, @@ -1415,6 +1416,7 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo private _clickTargetCellHandler(event, $cell) { const column = this._getColumnByCellElement($cell); const isCellEditMode = this._isCellEditMode(); + const isEditing = this._editingController.isEditing(); this.setCellFocusType(); @@ -1449,14 +1451,18 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo const $target = event && $(event.target).closest(`${NON_FOCUSABLE_ELEMENTS_SELECTOR}, td`); const skipFocusEvent = $target && $target.not($cell).is(NON_FOCUSABLE_ELEMENTS_SELECTOR); - const isEditor = !!column && !column.command && $cell.hasClass(EDITOR_CELL_CLASS); - const isDisabled = !isEditor && (!args.isHighlighted || skipFocusEvent); + const isEditCell = !column?.command + && isEditing + && $cell.hasClass(EDITOR_CELL_CLASS); + + const isDisabled = !isEditCell + && (!args.isHighlighted || skipFocusEvent); this._focus($cell, isDisabled, skipFocusEvent); } } else { this.setRowFocusType(); this.setFocusedRowIndex(args.prevRowIndex); - if (this._editingController.isEditing() && isCellEditMode) { + if (isEditing && isCellEditMode) { this._closeEditCell(); } } @@ -1477,10 +1483,10 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo const isHighlighted = this._isCellElement($(element)); if (!element) { - activeElementSelector = '.dx-datagrid-rowsview .dx-row[tabindex]'; + activeElementSelector = `.${this.addWidgetPrefix(ROWS_VIEW_CLASS)} .dx-row[tabindex]`; if (!focusedRowEnabled) { activeElementSelector - += ', .dx-datagrid-rowsview .dx-row > td[tabindex]'; + += `, .${this.addWidgetPrefix(ROWS_VIEW_CLASS)} .dx-row > td[tabindex]`; } element = this.component.$element().find(activeElementSelector).first(); } @@ -1528,6 +1534,7 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo isHighlighted, ); $element = args.$newCellElement; + if (isRowFocusType && !args.isHighlighted) { this.setRowFocusType(); } @@ -1535,7 +1542,10 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo if (!args.cancel) { this._focus($element, !args.isHighlighted); - this._focusInteractiveElement($element); + + if (this._getElementType($element) !== 'row' || isEditRow($element)) { + this._focusInteractiveElement($element); + } } } @@ -2781,11 +2791,7 @@ const rowsView = (Base: ModuleType) => class RowsViewKeyboardExtender const { operationTypes, repaintChangesOnly } = change ?? {}; const { fullReload, pageSize } = operationTypes ?? {}; - const hasInsertsOrRemoves = !!change?.changeTypes?.find( - (changeType) => changeType === 'insert' || changeType === 'remove', - ); - - if (!change || !repaintChangesOnly || fullReload || pageSize || hasInsertsOrRemoves) { + if (!change || !repaintChangesOnly || fullReload || pageSize) { const preventScroll = shouldPreventScroll(this); this.renderFocusState({ preventScroll, @@ -3006,7 +3012,9 @@ const adaptiveColumns = (Base: ModuleType) => class A protected _hideVisibleColumnInView({ view, isCommandColumn, visibleIndex }) { super._hideVisibleColumnInView({ view, isCommandColumn, visibleIndex }); if (view.name === ROWS_VIEW) { - this._rowsView.renderFocusState(null); + this._rowsView.renderFocusState({ + preventScroll: shouldPreventScroll(this), + }); } } }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation_utils.ts index e2136f1fff4e..5b474110cdd0 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation_utils.ts @@ -1,7 +1,7 @@ import devices from '@js/core/devices'; import { isDefined } from '@js/core/utils/type'; -import { EDITOR_CELL_CLASS } from '../editing/const'; +import { EDIT_ROW, EDITOR_CELL_CLASS } from '../editing/const'; import { ADAPTIVE_ITEM_TEXT_CLASS, COMMAND_SELECT_CLASS, DATA_ROW_CLASS, EDIT_FORM_CLASS, FREESPACE_ROW_CLASS, GROUP_ROW_CLASS, HEADER_ROW_CLASS, @@ -24,6 +24,10 @@ export function isAdaptiveItem($element) { return $element && $element.hasClass(ADAPTIVE_ITEM_TEXT_CLASS); } +export function isEditRow($row) { + return $row?.hasClass(EDIT_ROW); +} + export function isEditForm($row) { return $row && $row.hasClass(MASTER_DETAIL_ROW_CLASS) && $row.hasClass(EDIT_FORM_CLASS); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_widget_base.ts b/packages/devextreme/js/__internal/grids/grid_core/m_widget_base.ts index a261a2a30f90..ac87270c3f59 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_widget_base.ts @@ -7,12 +7,15 @@ import Widget from '@js/ui/widget/ui.widget'; const GRID_CORE_ROW_SELECTOR = '.dx-row'; export default class GridCoreWidget extends Widget { - private _activeStateUnit; - private readonly _controllers: any; private readonly _views: any; + // eslint-disable-next-line class-methods-use-this + protected _activeStateUnit(): string { + return GRID_CORE_ROW_SELECTOR; + } + private _getDefaultOptions() { // @ts-expect-error const result = super._getDefaultOptions(); @@ -27,7 +30,6 @@ export default class GridCoreWidget extends Widget { } protected _init() { - this._activeStateUnit = GRID_CORE_ROW_SELECTOR; // @ts-expect-error super._init(); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.integration.test.ts new file mode 100644 index 000000000000..e7acc82162aa --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.integration.test.ts @@ -0,0 +1,176 @@ +import { + afterEach, describe, expect, it, +} from '@jest/globals'; +import { CustomStore } from '@js/common/data'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; + +import type { GridsEditRefreshMode, Properties as DataGridProperties } from '../../../../ui/data_grid'; +import DataGrid from '../../../../ui/data_grid'; + +const SELECTORS = { + gridContainer: '#gridContainer', + detailCell: 'dx-master-detail-cell', + detailContainer: 'dx-datagrid-master-detail-container', +}; + +const GRID_CONTAINER_ID = 'gridContainer'; + +const createDataGrid = async ( + options: DataGridProperties = {}, +): Promise<{ $container: dxElementWrapper; instance: DataGrid }> => new Promise((resolve) => { + const $container = $('
') + .attr('id', GRID_CONTAINER_ID) + .appendTo(document.body); + + const instance = new DataGrid($container.get(0) as HTMLDivElement, options); + + const contentReadyHandler = (): void => { + resolve({ $container, instance }); + instance.off('contentReady', contentReadyHandler); + }; + + instance.on('contentReady', contentReadyHandler); +}); + +describe('GridCore selection', () => { + afterEach(() => { + const $container = $(SELECTORS.gridContainer); + + const dataGrid = ($container as any).dxDataGrid('instance') as DataGrid; + + dataGrid.dispose(); + $container.remove(); + }); + + describe('selectionChanged handler', () => { + [true, false].forEach((repaintChangesOnly) => { + it(`selectRowKeys are updated after refresh if selectedItem is not in dataSource anymore with repaintChangesOnly=${repaintChangesOnly}`, async () => { + const dataSource = [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + ]; + + const { instance } = await createDataGrid({ + dataSource, + columns: ['id', 'name'], + keyExpr: 'id', + selection: { + mode: 'single', + }, + repaintChangesOnly, + }); + + await instance.selectRows([2], false); + expect(instance.getSelectedRowKeys()).toEqual([2]); + + dataSource.splice(1, 1); // Remove the item with id 2 + + await instance.refresh(repaintChangesOnly); + + expect(instance.getSelectedRowKeys()).toEqual([]); + }); + + it(`selectionChanged handler is not called after refresh if selectedItem still present in dataSource with repaintChangesOnly=${repaintChangesOnly}`, async () => { + const dataSource = [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + ]; + + let selectionChangedCount = 0; + + const { instance } = await createDataGrid({ + dataSource, + columns: ['id', 'name'], + keyExpr: 'id', + selection: { + mode: 'single', + }, + repaintChangesOnly, + onSelectionChanged: () => { + selectionChangedCount += 1; + }, + }); + + await instance.selectRows([1], false); + expect(instance.getSelectedRowKeys()).toEqual([1]); + expect(selectionChangedCount).toBe(1); + + dataSource.splice(1, 1); // Remove the item with id 2 + await instance.refresh(repaintChangesOnly); + + expect(instance.getSelectedRowKeys()).toEqual([1]); + expect(selectionChangedCount).toBe(1); + }); + }); + }); + + describe('remote dataSource', () => { + ([ + { refreshMode: 'full', expectedCallCount: 2 }, + { refreshMode: 'reshape', expectedCallCount: 1 }, + { refreshMode: 'repaint', expectedCallCount: 0 }, + ] as { refreshMode: GridsEditRefreshMode; expectedCallCount: number }[]) + .forEach(({ refreshMode, expectedCallCount }) => { + it(`dataSource.load is not called to load selectedRow after data save with editing.refreshMode=${refreshMode}`, async () => { + let data = [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + { id: 3, name: 'Item 3' }, + { id: 4, name: 'Item 4' }, + ]; + + const store = new CustomStore({ + key: 'id', + load: (e) => { + const skip = e.skip ?? 0; + const take = e.take ?? data.length; + const pageData = data.slice(skip, skip + take); + return Promise.resolve({ + data: pageData, + totalCount: data.length, + }); + }, + remove(key) { + data = data.filter((item) => item.id !== key); + return Promise.resolve(); + }, + }); + + const { instance } = await createDataGrid({ + dataSource: store, + editing: { + mode: 'batch', + refreshMode, + allowDeleting: true, + }, + remoteOperations: true, + paging: { + pageSize: 2, + }, + columns: ['id', 'name'], + keyExpr: 'id', + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + }, + }); + + await instance.selectRows([4], false); + + let callCount = 0; + store.on('loading', () => { + callCount += 1; + }); + + instance.option('editing.changes', [{ + type: 'remove', + key: 1, + }]); + await instance.saveEditData(); + + expect(callCount).toBe(expectedCallCount); + }); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts index 1fd3d633204f..c058a835c882 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts @@ -12,7 +12,7 @@ import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; -import { isDefined } from '@js/core/utils/type'; +import { isDefined, isObject } from '@js/core/utils/type'; import errors from '@js/ui/widget/ui.errors'; import supportUtils from '@ts/core/utils/m_support'; import type { ColumnHeadersView } from '@ts/grids/grid_core/column_headers/m_column_headers'; @@ -21,7 +21,7 @@ import type { ContextMenuController } from '@ts/grids/grid_core/context_menu/m_c import type { ModuleType } from '@ts/grids/grid_core/m_types'; import type { StateStoringController } from '@ts/grids/grid_core/state_storing/m_state_storing_core'; import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view'; -import Selection from '@ts/ui/selection/m_selection'; +import Selection from '@ts/ui/selection/selection'; import type { DataController } from '../data_controller/m_data_controller'; import modules from '../m_modules'; @@ -299,6 +299,7 @@ export class SelectionController extends modules.Controller { private _createSelection() { const options = this._getSelectionConfig(); + // @ts-expect-error TKey return new Selection(options); } @@ -650,17 +651,19 @@ export const dataSelectionExtenderMixin = (Base: ModuleType) => return dataItem; } - public refresh(options) { - const that = this; + public refresh(options): any { // @ts-expect-error - const d = new Deferred(); + const d: DeferredObj = new Deferred(); + + super.refresh(options).done(() => { + const skipSelectionRefresh = isObject(options) && !(options as any).selection; - super.refresh.apply(this, arguments as any).done(() => { - if (!options || options.selection) { - that._selectionController.refresh().done(d.resolve).fail(d.reject); - } else { + if (skipSelectionRefresh) { d.resolve(); + return; } + + this._selectionController.refresh().done(d.resolve).fail(d.reject); }).fail(d.reject); return d.promise(); diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts index 8d7494a6f535..052c59ec6cd2 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts @@ -71,22 +71,24 @@ const processLoadState = (that) => { } }; -const DEFAULT_FILTER_VALUE = null; - const getFilterValue = (that, state) => { // TODO: getController const filterSyncController = that.getController('filterSync'); - const columnsController = that.getController('columns'); - const hasFilterState = state.columns || state.filterValue !== undefined; + if (!filterSyncController) { + return null; + } - if (filterSyncController) { - if (hasFilterState) { - return state.filterValue || filterSyncController.getFilterValueFromColumns(state.columns); - } - return that._initialFilterValue || filterSyncController.getFilterValueFromColumns(columnsController.getColumns()); + if (state.filterValue !== undefined) { + return state.filterValue; + } + + const filterValueFromColumns = filterSyncController.getFilterValueFromColumns?.(state.columns); + if (filterValueFromColumns?.length > 0) { + return filterValueFromColumns; } - return DEFAULT_FILTER_VALUE; + const columns = that.getController('columns').getColumns(); + return that._initialFilterValue ?? filterSyncController.getFilterValueFromColumns(columns); }; const rowsView = (Base: ModuleType) => class StateStoringRowsViewExtender extends Base { diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx index fec3cc7833ac..72a953c67139 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx @@ -3,6 +3,7 @@ import messageLocalization from '@js/localization/message'; import type { ValueChangedEvent } from '@js/ui/check_box'; import type * as dxToolbar from '@js/ui/toolbar'; import { isDefined } from '@ts/core/utils/m_type'; +import type { SelectCardOptions } from '@ts/grids/new/card_view/content_view/types'; import type { CardInfo } from '@ts/grids/new/grid_core/columns_controller/types'; import { Toolbar } from '@ts/grids/new/grid_core/inferno_wrappers/toolbar'; import type { DefaultToolbarItem } from '@ts/grids/new/grid_core/toolbar/types'; @@ -10,8 +11,6 @@ import { normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; import type { ComponentType } from 'inferno'; import { Component } from 'inferno'; -import type { SelectCardOptions } from '../../types'; - export const CLASSES = { cardHeader: 'dx-cardview-card-header', cardSelectCheckBox: 'dx-cardview-select-checkbox', diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx index dbf5cee1bfb7..6b3bcfc48b5c 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx @@ -6,7 +6,7 @@ */ import { compileGetter } from '@js/core/utils/data'; import { isDefined } from '@js/core/utils/type'; -import { computed, effect, signal } from '@preact/signals-core'; +import { computed, effect, signal } from '@ts/core/state_manager/index'; import type { OptionsController } from '@ts/grids/new/card_view/options_controller'; import type { CardInfo } from '@ts/grids/new/grid_core/columns_controller/types'; import { diff --git a/packages/devextreme/js/__internal/grids/new/card_view/di.ts b/packages/devextreme/js/__internal/grids/new/card_view/di.ts index d3f75da3ba95..cf1d5be55902 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/di.ts +++ b/packages/devextreme/js/__internal/grids/new/card_view/di.ts @@ -1,5 +1,6 @@ /* eslint-disable spellcheck/spell-checker */ import type { DIContext } from '@ts/core/di'; +import { setupStateManager } from '@ts/core/state_manager/index'; import * as BaseContentViewModule from '../grid_core/content_view/index'; import { BaseContextMenuController } from '../grid_core/context_menu/controller'; @@ -11,6 +12,8 @@ import { HeaderPanelController } from './header_panel/controller'; import { HeaderPanelView } from './header_panel/view'; export function register(diContext: DIContext): void { + setupStateManager({ diContext, componentName: 'CardView' }); + gridCoreDIRegister(diContext); diContext.register(ContentViewModule.View); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx index 7383382eb241..3d3501f982cb 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/view.tsx @@ -1,5 +1,5 @@ /* eslint-disable spellcheck/spell-checker */ -import { computed, type ReadonlySignal } from '@preact/signals-core'; +import { computed, type ReadonlySignal } from '@ts/core/state_manager/index'; import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/columns_controller'; import { View } from '@ts/grids/new/grid_core/core/view'; import { KeyboardNavigationController, NavigationStrategyHorizontalList } from '@ts/grids/new/grid_core/keyboard_navigation/index'; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx index ef691e88589e..f6523bc68c08 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { computed } from '@preact/signals-core'; +import { computed } from '@ts/core/state_manager/index'; import { ColumnChooserView } from '@ts/grids/new/grid_core/column_chooser/index'; import { View } from '@ts/grids/new/grid_core/core/view'; import { FilterPanelView } from '@ts/grids/new/grid_core/filtering/filter_panel/view'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/controller.ts index 630504dadff0..7d71a505890a 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/controller.ts @@ -1,5 +1,5 @@ import messageLocalization from '@js/localization/message'; -import { computed, effect, signal } from '@preact/signals-core'; +import { computed, effect, signal } from '@ts/core/state_manager/index'; import { ColumnsController } from '../columns_controller/columns_controller'; import { DataController } from '../data_controller/index'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.mock.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.mock.ts index 5e3a2ea22612..a4153ff9076f 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.mock.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.mock.ts @@ -1,5 +1,5 @@ import dxTreeView from '@js/ui/tree_view'; -import { effect } from '@preact/signals-core'; +import { effect } from '@ts/core/state_manager/index'; import type { ColumnsController } from '../columns_controller/index'; import type { OptionsController } from '../options_controller/options_controller'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.ts index 7097199af1e8..b6d43f4fad5c 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.ts @@ -1,7 +1,7 @@ import $ from '@js/core/renderer'; import type * as SortableTypes from '@js/ui/sortable_types'; import type { Item as TreeViewItemProperties, SelectionChangedEvent } from '@js/ui/tree_view'; -import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; +import { computed, type ReadonlySignal, signal } from '@ts/core/state_manager/index'; import { sortColumns } from '@ts/grids/grid_core/columns_controller/m_columns_controller_utils'; import type { DraggingColumnData } from '../../card_view/header_panel/column_sortable'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.tsx index 34c05398fa1e..8996f744b62f 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.tsx @@ -5,8 +5,8 @@ import type { Properties as PopupProperties, ToolbarItem } from '@js/ui/popup'; import type dxPopup from '@js/ui/popup'; import type { Properties as TreeViewProperties } from '@js/ui/tree_view'; import type dxTreeView from '@js/ui/tree_view'; -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed, signal } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed, signal } from '@ts/core/state_manager/index'; import { createRef } from 'inferno'; import type { Props as ColumnSortableProps } from '../../card_view/header_panel/column_sortable'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts index f6315693d5fa..6f6876098824 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts @@ -1,5 +1,5 @@ -import type { ReadonlySignal, Signal } from '@preact/signals-core'; -import { computed, effect, signal } from '@preact/signals-core'; +import type { ReadonlySignal, Signal } from '@ts/core/state_manager/index'; +import { computed, effect, signal } from '@ts/core/state_manager/index'; import type { DataObject } from '@ts/grids/new/grid_core/data_controller/types'; import type { HeaderFilterRootOptions } from '@ts/grids/new/grid_core/filtering/header_filter/index'; import { isColumnFilterable, mergeColumnHeaderFilterOptions } from '@ts/grids/new/grid_core/filtering/header_filter/utils'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx index 44a4784dda4b..bd4218ca18c2 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/content_view/view.tsx @@ -5,7 +5,7 @@ import $ from '@js/core/renderer'; import type dxScrollable from '@js/ui/scroll_view/ui.scrollable'; import type { ScrollEventInfo } from '@js/ui/scroll_view/ui.scrollable'; -import { computed, signal } from '@preact/signals-core'; +import { computed, signal } from '@ts/core/state_manager/index'; import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/columns_controller'; import { BaseContextMenuController } from '@ts/grids/new/grid_core/context_menu/controller'; import { View } from '@ts/grids/new/grid_core/core/view'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/view.ts b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/view.ts index cd86c87224c5..b15c39afc281 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/view.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/context_menu/view.ts @@ -1,8 +1,8 @@ import type { InitializedEvent, ItemClickEvent, } from '@js/ui/context_menu'; -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed } from '@ts/core/state_manager/index'; import { View } from '../core/view'; import type { ContextMenuProps } from './context_menu'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/core/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/core/view.tsx index 5ac1341580aa..9b78492c5c7a 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/core/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/core/view.tsx @@ -3,10 +3,10 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable max-classes-per-file */ -import type { ReadonlySignal } from '@preact/signals-core'; -import { effect } from '@preact/signals-core'; import { infernoRenderer } from '@ts/core/m_inferno_renderer'; import { BaseInfernoComponent } from '@ts/core/r1/runtime/inferno/base_component'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { effect } from '@ts/core/state_manager/index'; import { hasWindow } from '@ts/core/utils/m_window'; import { type ComponentType } from 'inferno'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/compatibility.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/compatibility.ts index 5a8d45303865..7c43c541af09 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/compatibility.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/compatibility.ts @@ -1,6 +1,6 @@ import createCallback from '@js/core/utils/callbacks'; import type DataSource from '@js/data/data_source'; -import { effect } from '@preact/signals-core'; +import { effect } from '@ts/core/state_manager/index'; import { DataController } from './data_controller'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts index 02875f1389db..56213109e9ed 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts @@ -3,8 +3,8 @@ import type { FilterDescriptor } from '@js/common/data.types'; import ArrayStore from '@js/common/data/array_store'; import { Deferred } from '@js/core/utils/deferred'; import { isDefined, isPlainObject } from '@js/core/utils/type'; -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed, effect, signal } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed, effect, signal } from '@ts/core/state_manager/index'; import { equalByValue } from '@ts/core/utils/m_common'; import type { PromiseWithResolvers } from '@ts/core/utils/promise'; import { createPromise } from '@ts/core/utils/promise'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/store_load_adapter/store_load_adapter.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/store_load_adapter/store_load_adapter.test.ts index 70220ffb16f9..24fa16e84a68 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/store_load_adapter/store_load_adapter.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/store_load_adapter/store_load_adapter.test.ts @@ -3,7 +3,7 @@ import { } from '@jest/globals'; import type { DataSource } from '@js/common/data'; import { Deferred } from '@js/core/utils/deferred'; -import { signal } from '@preact/signals-core'; +import { signal } from '@ts/core/state_manager/index'; import type { InternalLoadOptions, OperationOptions } from '@ts/grids/new/grid_core/data_controller/types'; import { StoreLoadAdapter } from './store_load_adapter'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/store_load_adapter/store_load_adapter.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/store_load_adapter/store_load_adapter.ts index e355e7d6c467..f414af5d0c81 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/store_load_adapter/store_load_adapter.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/store_load_adapter/store_load_adapter.ts @@ -1,7 +1,7 @@ import type { DataSource, LoadResult } from '@js/common/data'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; -import type { ReadonlySignal } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; import { deferredCache } from '../deferred_cache'; import type { InternalLoadOptions, OperationOptions } from '../types'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/editing/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/editing/controller.ts index 65d4888361ba..a63bf8b75ff0 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/editing/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/editing/controller.ts @@ -2,7 +2,7 @@ /* eslint-disable spellcheck/spell-checker */ import { applyChanges } from '@js/common/data'; import { isDefined } from '@js/core/utils/type'; -import { computed, type Signal } from '@preact/signals-core'; +import { computed, type Signal } from '@ts/core/state_manager/index'; import { generateNewRowTempKey } from '@ts/grids/grid_core/editing/m_editing_utils'; import { OptionsValidationController } from '@ts/grids/new/grid_core/options_validation/index'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/editing/popup/view.ts b/packages/devextreme/js/__internal/grids/new/grid_core/editing/popup/view.ts index 69cb87f73b1a..99120b3276af 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/editing/popup/view.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/editing/popup/view.ts @@ -4,8 +4,8 @@ import type { DataType } from '@js/common'; import $ from '@js/core/renderer'; import type * as dxForm from '@js/ui/form'; -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed, signal } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed, signal } from '@ts/core/state_manager/index'; import { extend } from '@ts/core/utils/m_extend'; import { forEachFormItems } from '@ts/grids/grid_core/editing/m_editing_utils'; import { createRef } from 'inferno'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/error_controller/error_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/error_controller/error_controller.ts index e29a304f14a5..72aa7864f465 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/error_controller/error_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/error_controller/error_controller.ts @@ -1,5 +1,5 @@ -import type { ReadonlySignal } from '@preact/signals-core'; -import { signal } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { signal } from '@ts/core/state_manager/index'; export interface GridError { text: string; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_controller.ts index 204985195e9a..32cf8bb068d3 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_controller.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ -import { computed } from '@preact/signals-core'; +import { computed } from '@ts/core/state_manager/index'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; import { getColumnByIndexOrName } from '@ts/grids/new/grid_core/columns_controller/utils'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/view.tsx index 304adb0660a4..b1cf094ca83f 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_panel/view.tsx @@ -1,4 +1,4 @@ -import { computed, type ReadonlySignal } from '@preact/signals-core'; +import { computed, type ReadonlySignal } from '@ts/core/state_manager/index'; import { FilterBuilderView as OldFilterBuilderView } from '@ts/grids/grid_core/filter/m_filter_builder'; import { FilterPanelView as OldFilterPanelView } from '@ts/grids/grid_core/filter/m_filter_panel'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_sync/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_sync/controller.ts index ed815ca6e8e8..a3918ce0979a 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_sync/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/filter_sync/controller.ts @@ -1,7 +1,7 @@ -// import type { ReadonlySignal } from '@preact/signals-core'; -// import { computed } from '@preact/signals-core'; +// import type { ReadonlySignal } from '@ts/core/state_manager/index'; +// import { computed } from '@ts/core/state_manager/index'; import { equalByValue } from '@js/core/utils/common'; -import { batch, effect } from '@preact/signals-core'; +import { batch, effect } from '@ts/core/state_manager/index'; import { getMatchedConditions } from '@ts/filter_builder/m_utils'; import type { HeaderFilterInfo } from '@ts/grids/new/grid_core/filtering/header_filter/types'; import { SearchController } from '@ts/grids/new/grid_core/search/index'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.ts index 5c2923d584f0..50fff68b5afe 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/controller.ts @@ -1,5 +1,5 @@ -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed } from '@ts/core/state_manager/index'; import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/index'; import type { HeaderFilterInfo } from '@ts/grids/new/grid_core/filtering/header_filter/types'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx index 34a7f3fdae7d..41d9e8608690 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx @@ -11,7 +11,7 @@ import { it, jest, } from '@jest/globals'; -import { signal } from '@preact/signals-core'; +import { signal } from '@ts/core/state_manager/index'; import { render, rerender } from 'inferno'; import type { PopupState } from './types'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.tsx index 05e44f058508..5d7b0ef9c6aa 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.tsx @@ -1,7 +1,7 @@ /* eslint-disable max-classes-per-file */ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import { computed, effect, type ReadonlySignal } from '@preact/signals-core'; +import { computed, effect, type ReadonlySignal } from '@ts/core/state_manager/index'; import { HeaderFilterView as OldHeaderFilterPopup } from '@ts/grids/grid_core/header_filter/m_header_filter_core'; import { View } from '@ts/grids/new/grid_core/core/view'; import { WidgetMock } from '@ts/grids/new/grid_core/widget_mock'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts index 6c3348ac019f..a02f1154ca37 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts @@ -1,6 +1,6 @@ /* eslint-disable spellcheck/spell-checker */ -import type { ReadonlySignal } from '@preact/signals-core'; -import { signal } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { signal } from '@ts/core/state_manager/index'; import { removeFieldConditionsFromFilter } from '@ts/filter_builder/m_utils'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts index 2442478265c2..a164b21bae27 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts @@ -1,6 +1,6 @@ import { equalByValue } from '@js/core/utils/common'; import formatHelper from '@js/format_helper'; -import { computed, signal } from '@preact/signals-core'; +import { computed, signal } from '@ts/core/state_manager/index'; import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/columns_controller'; import { DataController } from '@ts/grids/new/grid_core/data_controller/data_controller'; import { SearchController } from '@ts/grids/new/grid_core/search/index'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/component.mock.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/component.mock.ts index 76fc80d2e249..74e6f67a2962 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/component.mock.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/component.mock.ts @@ -1,5 +1,5 @@ import { Component } from '@js/core/component'; -import { signal } from '@preact/signals-core'; +import { signal } from '@ts/core/state_manager/index'; import { extend } from '@ts/core/utils/m_extend'; // NOTE: We cannot modify the base "_getDefaultOptions" method with Component base class params diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts index ff4100ca85ee..0b41813464a1 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts @@ -4,8 +4,8 @@ import { Component } from '@js/core/component'; import { getPathParts } from '@js/core/utils/data'; import type { ChangedOptionInfo } from '@js/events'; -import type { ReadonlySignal, Signal } from '@preact/signals-core'; -import { computed, effect, signal } from '@preact/signals-core'; +import type { ReadonlySignal, Signal } from '@ts/core/state_manager/index'; +import { computed, effect, signal } from '@ts/core/state_manager/index'; import { extend } from '@ts/core/utils/m_extend'; import type { ComponentType } from 'inferno'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.tsx index e9e0557ad048..060e2010b518 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/pager/view.tsx @@ -1,5 +1,5 @@ -import type { ReadonlySignal, Signal } from '@preact/signals-core'; -import { computed, effect, signal } from '@preact/signals-core'; +import type { ReadonlySignal, Signal } from '@ts/core/state_manager/index'; +import { computed, effect, signal } from '@ts/core/state_manager/index'; import { MAX_PAGES_COUNT } from '@ts/grids/grid_core/pager/m_pager'; import { View } from '../core/view'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/search/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/search/controller.ts index 5d9511246ca4..02b3d706d5ce 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/search/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/search/controller.ts @@ -1,5 +1,5 @@ -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed } from '@ts/core/state_manager/index'; import type { Options as SearchOptions } from '@ts/grids/new/grid_core/search/options'; import type { HighlightedTextItem, HighlightTextOptions } from '@ts/grids/new/grid_core/search/types'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/search/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/search/view.tsx index 6d385be9636b..bc83582fe722 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/search/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/search/view.tsx @@ -1,6 +1,6 @@ import type { TextBoxInstance } from '@js/ui/text_box'; -import type { Signal } from '@preact/signals-core'; -import { effect, signal } from '@preact/signals-core'; +import type { Signal } from '@ts/core/state_manager/index'; +import { effect, signal } from '@ts/core/state_manager/index'; import { ToolbarController } from '@ts/grids/new/grid_core/toolbar/controller'; import { OptionsController } from '../options_controller/options_controller'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts index 1d7b3bf82f83..5f47fca4b12d 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.test.ts @@ -200,6 +200,38 @@ describe('SelectionController', () => { expect(selectionController.getSelectedCardsData()) .toEqual(dataController.items.peek()); }); + + describe('when the selected cards are on different pages', () => { + it('should return data for all selected cards', () => { + const { + selectionController, + dataController, + } = setup({ + keyExpr: 'id', + dataSource: [ + { id: 1, value: 'test1' }, + { id: 2, value: 'test2' }, + { id: 3, value: 'test3' }, + ], + selectedCardKeys: [1, 3], + paging: { + enabled: true, + pageSize: 2, + }, + }); + + expect(dataController.items.peek()) + .toEqual([ + { id: 1, value: 'test1' }, + { id: 2, value: 'test2' }, + ]); + expect(selectionController.getSelectedCardsData()) + .toEqual([ + { id: 1, value: 'test1' }, + { id: 3, value: 'test3' }, + ]); + }); + }); }); describe('clearSelection', () => { diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts index 82fb19a190d4..262bb9e907c5 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/selection/controller.ts @@ -3,12 +3,12 @@ import type { DeferredObj } from '@js/core/utils/deferred'; import messageLocalization from '@js/localization/message'; -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed, effect, signal } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed, effect, signal } from '@ts/core/state_manager/index'; import { DataController } from '@ts/grids/new/grid_core/data_controller/index'; import { OptionsValidationController } from '@ts/grids/new/grid_core/options_validation/index'; import { ShowCheckBoxesMode } from '@ts/grids/new/grid_core/selection/const'; -import Selection from '@ts/ui/selection/m_selection'; +import Selection from '@ts/ui/selection/selection'; import type { CardInfo } from '../columns_controller/types'; import type { DataObject, Key } from '../data_controller/types'; @@ -45,7 +45,8 @@ export class SelectionController { private readonly selectionOption: ReadonlySignal = this.options.oneWay('selection'); - private readonly selectionHelper: ReadonlySignal; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private readonly selectionHelper: ReadonlySignal | undefined>; private readonly _isCheckBoxesRendered = signal(false); @@ -365,12 +366,8 @@ export class SelectionController { } public getSelectedCardsData(): DataObject[] { - const selectedCardKey = this.getSelectedCardKeys(); - - return selectedCardKey - .map((key) => this.itemsController.getCardByKey(key)) - .filter((item): item is CardInfo => !!item) - .map((item) => item.data); + // @ts-expect-error undefined is not assignable to DataObject[] + return this.selectionHelper?.peek()?.getSelectedItems(); } public getSelectedCardKeys(): Key[] { diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/controller.ts index fe9633b6ad00..0bb0af2c3563 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/controller.ts @@ -1,6 +1,6 @@ import type { SortOrder } from '@js/common'; -import type { ReadonlySignal } from '@preact/signals-core'; -import { batch, computed } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { batch, computed } from '@ts/core/state_manager/index'; import { ColumnsController } from '../columns_controller/index'; import type { Column } from '../columns_controller/types'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/controller.test.ts index 21c621a7248d..c25eec1b729c 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/controller.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/controller.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { signal } from '@preact/signals-core'; +import { signal } from '@ts/core/state_manager/index'; import { getContext } from '../di.test_utils'; import type { Options } from '../options'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/controller.ts index 8ba099e5538d..9fc76f4c88ed 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/controller.ts @@ -1,5 +1,5 @@ -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed, effect, signal } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed, effect, signal } from '@ts/core/state_manager/index'; import { OptionsController } from '../options_controller/options_controller'; import { DEFAULT_TOOLBAR_ITEMS } from './const'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/view.tsx index da9cfade7ae6..17b4939fab83 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/view.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/toolbar/view.tsx @@ -1,5 +1,5 @@ -import type { ReadonlySignal } from '@preact/signals-core'; -import { computed } from '@preact/signals-core'; +import type { ReadonlySignal } from '@ts/core/state_manager/index'; +import { computed } from '@ts/core/state_manager/index'; import { BaseContextMenuController } from '../context_menu/controller'; import { View } from '../core/view'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts index 5ad82b3bc8b2..76725b7d6086 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts @@ -4,10 +4,10 @@ // eslint-disable-next-line max-classes-per-file import { extend } from '@js/core/utils/extend'; import Widget from '@js/ui/widget/ui.widget'; -import type { Signal } from '@preact/signals-core'; -import { signal } from '@preact/signals-core'; import { DIContext } from '@ts/core/di/index'; import { infernoRenderer } from '@ts/core/m_inferno_renderer'; +import type { Signal } from '@ts/core/state_manager/index'; +import { signal } from '@ts/core/state_manager/index'; import { SearchView } from '@ts/grids/new/grid_core/search/view'; import { rerender } from 'inferno'; diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/data_controller/m_data_controller.ts b/packages/devextreme/js/__internal/grids/pivot_grid/data_controller/m_data_controller.ts index 9e5369d244da..2975ab9a0892 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/data_controller/m_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/data_controller/m_data_controller.ts @@ -1,4 +1,3 @@ -import Class from '@js/core/class'; import Callbacks from '@js/core/utils/callbacks'; import { deferUpdate } from '@js/core/utils/common'; import { Deferred, when } from '@js/core/utils/deferred'; @@ -22,440 +21,555 @@ const DATA_TYPE = 'D'; const NOT_AVAILABLE = '#N/A'; const CHANGING_DURATION_IF_PAGINATE = 300; -const proxyMethod = function (instance, methodName, defaultResult?) { - if (!instance[methodName]) { - instance[methodName] = function () { +const proxyMethod = function (type, methodName, defaultResult?) { + const target = type.prototype; + if (!target[methodName]) { + target[methodName] = function () { const dataSource = this._dataSource; return dataSource ? dataSource[methodName].apply(dataSource, arguments) : defaultResult; }; } }; -const DataController = Class.inherit((function () { - // - @ts-expect-error - function getHeaderItemText(item, description, options) { - let { text } = item; +class DataController { + _options: any; - if (isDefined(item.displayText)) { - text = item.displayText; - } else if (isDefined(item.caption)) { - text = item.caption; - } else if (item.type === GRAND_TOTAL_TYPE) { - text = options.texts.grandTotal; - } + _dataSource: any; - if (item.isAdditionalTotal) { - text = format(options.texts.total || '', text); + _rowsScrollController: any; + + _columnsScrollController: any; + + _stateStoringController: any; + + _columnsInfo: any; + + _rowsInfo: any; + + _cellsInfo: any; + + expandValueChanging: any; + + loadingChanged: any; + + progressChanged: any; + + scrollChanged: any; + + changed: any; + + _rowPageIndex: any; + + _rowPageSize: any; + + _columnPageIndex: any; + + _columnPageSize: any; + + _changingDuration: any; + + constructor(options) { + const that: any = this; + const virtualScrollControllerChanged = that._fireChanged.bind(that); + + options = that._options = options || {}; + + that.dataSourceChanged = Callbacks(); + that._dataSource = that._createDataSource(options); + + if (options.component && options.component.option('scrolling.mode') === 'virtual') { + that._rowsScrollController = this.createScrollController(that, options.component, { + totalItemsCount() { + return that.totalRowCount(); + }, + pageIndex(index) { + return that.rowPageIndex(index); + }, + pageSize() { + return that.rowPageSize(); + }, + + load() { + if (that._rowsScrollController.pageIndex() >= this.pageCount()) { + that._rowsScrollController.pageIndex(this.pageCount() - 1); + } + return that._rowsScrollController.handleDataChanged(function () { + if (that._dataSource.paginate()) { + that._dataSource.load(); + } else { + virtualScrollControllerChanged.apply(this, arguments); + } + }); + }, + }); + + that._columnsScrollController = this.createScrollController(that, options.component, { + totalItemsCount() { + return that.totalColumnCount(); + }, + pageIndex(index) { + return that.columnPageIndex(index); + }, + pageSize() { + return that.columnPageSize(); + }, + + load() { + if (that._columnsScrollController.pageIndex() >= this.pageCount()) { + that._columnsScrollController.pageIndex(this.pageCount() - 1); + } + + return that._columnsScrollController.handleDataChanged(function () { + if (that._dataSource.paginate()) { + that._dataSource.load(); + } else { + virtualScrollControllerChanged.apply(this, arguments); + } + }); + }, + + }); } - return text; - } + that._stateStoringController = new stateStoring.StateStoringController(options.component).init(); - function formatCellValue(value, dataField, errorText) { - return value === NOT_AVAILABLE ? errorText : formatValue(value, dataField); - } + that._columnsInfo = []; + that._rowsInfo = []; + that._cellsInfo = []; + + that.expandValueChanging = Callbacks(); + that.loadingChanged = Callbacks(); + that.progressChanged = Callbacks(); + that.scrollChanged = Callbacks(); - const createHeaderInfo = (function () { - const getHeaderItemsDepth = function (headerItems) { - let depth = 0; + that.load(); + that._update(); + that.changed = Callbacks(); + } - foreachTree(headerItems, (items) => { - depth = math.max(depth, items.length); + createHeaderInfo = (headerItems, headerDescriptions, cellDescriptions, isHorizontal, options) => { + const info = []; + const depthSize = this._getHeaderItemsDepth(headerItems) || 1; + // @ts-expect-error createHeaderInfo returns a Deferred + const d = new Deferred(); + + this._getViewHeaderItems(headerItems, headerDescriptions, cellDescriptions, depthSize, options) + .done((viewHeaderItems) => { + this._fillHeaderInfo(info, viewHeaderItems, depthSize, isHorizontal, options.layout === 'tree'); + options.notifyProgress(1); + d.resolve(info); }); - return depth; + return d; + }; + + _getHeaderItemsDepth = (headerItems) => { + let depth = 0; + + foreachTree(headerItems, (items) => { + depth = math.max(depth, items.length); + }); + + return depth; + }; + + _createInfoItem = (headerItem, breadth, isHorizontal, isTree) => { + const infoItem: any = { + type: headerItem.type, + text: headerItem.text, }; - const createInfoItem = function (headerItem, breadth, isHorizontal, isTree) { - const infoItem: any = { - type: headerItem.type, - text: headerItem.text, - }; + if (headerItem.path) { + infoItem.path = headerItem.path; + } + if (headerItem.width) { + infoItem.width = headerItem.width; + } + if (isDefined(headerItem.wordWrapEnabled)) { + infoItem.wordWrapEnabled = headerItem.wordWrapEnabled; + } - if (headerItem.path) { - infoItem.path = headerItem.path; - } - if (headerItem.width) { - infoItem.width = headerItem.width; - } - if (isDefined(headerItem.wordWrapEnabled)) { - infoItem.wordWrapEnabled = headerItem.wordWrapEnabled; - } + if (headerItem.isLast) { + infoItem.isLast = true; + } + if (headerItem.sorted) { + infoItem.sorted = true; + } + if (headerItem.isMetric) { + infoItem.dataIndex = headerItem.dataIndex; + } + if (isDefined(headerItem.expanded)) { + infoItem.expanded = headerItem.expanded; + } + if (breadth > 1) { + infoItem[isHorizontal ? 'colspan' : 'rowspan'] = breadth; + } + if (headerItem.depthSize && headerItem.depthSize > 1) { + infoItem[isHorizontal ? 'rowspan' : 'colspan'] = headerItem.depthSize; + } - if (headerItem.isLast) { - infoItem.isLast = true; - } - if (headerItem.sorted) { - infoItem.sorted = true; - } - if (headerItem.isMetric) { - infoItem.dataIndex = headerItem.dataIndex; - } - if (isDefined(headerItem.expanded)) { - infoItem.expanded = headerItem.expanded; - } - if (breadth > 1) { - infoItem[isHorizontal ? 'colspan' : 'rowspan'] = breadth; + if (headerItem.index >= 0) { + infoItem.dataSourceIndex = headerItem.index; + } + + if ( + isTree + && headerItem.children?.length + && !headerItem.children[0].isMetric + ) { + infoItem.width = null; + infoItem.isWhiteSpace = true; + } + + return infoItem; + }; + + _addInfoItem = (info, options) => { + const breadth = (options.lastIndex - options.index) || 1; + const addInfoItemCore = function (info, infoItem, itemIndex, depthIndex, isHorizontal) { + const index = isHorizontal ? depthIndex : itemIndex; + while (!info[index]) { + info.push([]); } - if (headerItem.depthSize && headerItem.depthSize > 1) { - infoItem[isHorizontal ? 'rowspan' : 'colspan'] = headerItem.depthSize; + if (isHorizontal) { + info[index].push(infoItem); + } else { + info[index].unshift(infoItem); } + }; - if (headerItem.index >= 0) { - infoItem.dataSourceIndex = headerItem.index; - } + const itemInfo = this._createInfoItem( + options.headerItem, + breadth, + options.isHorizontal, + options.isTree, + ); + addInfoItemCore(info, itemInfo, options.index, options.depth, options.isHorizontal); + if (!options.headerItem.children || options.headerItem.children.length === 0) { + return options.lastIndex + 1; + } + return options.lastIndex; + }; - if ( - isTree - && headerItem.children?.length - && !headerItem.children[0].isMetric - ) { - infoItem.width = null; - infoItem.isWhiteSpace = true; - } + _isItemSorted = (items, sortBySummaryPath) => { + let path; + const item = items[0]; + const stringValuesUsed = isString(sortBySummaryPath[0]); + const headerItem = item.dataIndex >= 0 ? items[1] : item; - return infoItem; - }; + if ((stringValuesUsed && sortBySummaryPath[0].indexOf('&[') !== -1 && headerItem.key) || !headerItem.key) { + path = createPath(items); + } else { + path = map(items, (it) => (it.dataIndex >= 0 ? it.value : it.text)).reverse(); + } - const addInfoItem = function (info, options) { - const breadth = (options.lastIndex - options.index) || 1; - const addInfoItemCore = function (info, infoItem, itemIndex, depthIndex, isHorizontal) { - const index = isHorizontal ? depthIndex : itemIndex; - while (!info[index]) { - info.push([]); - } - if (isHorizontal) { - info[index].push(infoItem); - } else { - info[index].unshift(infoItem); - } - }; + if (item.type === GRAND_TOTAL_TYPE) { + path = path.slice(1); + } - const itemInfo = createInfoItem( - options.headerItem, - breadth, - options.isHorizontal, - options.isTree, - ); - addInfoItemCore(info, itemInfo, options.index, options.depth, options.isHorizontal); - if (!options.headerItem.children || options.headerItem.children.length === 0) { - return options.lastIndex + 1; + return path.join('/') === sortBySummaryPath.join('/'); + }; + + _getViewHeaderItems = ( + headerItems, + headerDescriptions, + cellDescriptions, + depthSize, + options, + ) => { + const cellDescriptionsCount = cellDescriptions.length; + const viewHeaderItems = this._createViewHeaderItems(headerItems, headerDescriptions); + const { dataFields } = options; + // @ts-expect-error Deferred interop with when() + const d = new Deferred(); + + when(viewHeaderItems).done((items) => { + options.notifyProgress(0.5); + + const viewItems = items; + + if (options.showGrandTotals) { + viewItems[!options.showTotalsPrior ? 'push' : 'unshift']({ + type: GRAND_TOTAL_TYPE, + isEmpty: options.isEmptyGrandTotal, + }); } - return options.lastIndex; - }; - const isItemSorted = function (items, sortBySummaryPath) { - let path; - const item = items[0]; - const stringValuesUsed = isString(sortBySummaryPath[0]); - const headerItem = item.dataIndex >= 0 ? items[1] : item; + const hideTotals = options.showTotals === false + || dataFields.length > 0 + && (dataFields.length === options.hiddenTotals.length); + const hideData = dataFields.length > 0 && options.hiddenValues.length === dataFields.length; - if ((stringValuesUsed && sortBySummaryPath[0].indexOf('&[') !== -1 && headerItem.key) || !headerItem.key) { - path = createPath(items); - } else { - path = map(items, (item) => (item.dataIndex >= 0 ? item.value : item.text)).reverse(); + if (hideData && hideTotals) { + depthSize = 1; } - if (item.type === GRAND_TOTAL_TYPE) { - path = path.slice(1); + if (!hideTotals || options.layout === 'tree') { + this._addAdditionalTotalHeaderItems(viewItems, headerDescriptions, options.showTotalsPrior, options.layout === 'tree'); } - return path.join('/') === sortBySummaryPath.join('/'); - }; + when(foreachTreeAsync(viewItems, (items) => { + const item = items[0]; - const getViewHeaderItems = function ( - headerItems, - headerDescriptions, - cellDescriptions, - depthSize, - options, - ) { - const cellDescriptionsCount = cellDescriptions.length; - const viewHeaderItems = createViewHeaderItems(headerItems, headerDescriptions); - const { dataFields } = options; - // @ts-expect-error - const d = new Deferred(); - - when(viewHeaderItems).done((viewHeaderItems) => { - options.notifyProgress(0.5); - - if (options.showGrandTotals) { - viewHeaderItems[!options.showTotalsPrior ? 'push' : 'unshift']({ - type: GRAND_TOTAL_TYPE, - isEmpty: options.isEmptyGrandTotal, - }); + if (!item.children || item.children.length === 0) { + item.depthSize = depthSize - items.length + 1; } - - const hideTotals = options.showTotals === false - || dataFields.length > 0 - && (dataFields.length === options.hiddenTotals.length); - const hideData = dataFields.length > 0 && options.hiddenValues.length === dataFields.length; - - if (hideData && hideTotals) { - depthSize = 1; + })).done(() => { + if (cellDescriptionsCount > 1) { + this._addMetricHeaderItems(viewItems, cellDescriptions, options); } - if (!hideTotals || options.layout === 'tree') { - addAdditionalTotalHeaderItems(viewHeaderItems, headerDescriptions, options.showTotalsPrior, options.layout === 'tree'); - } + !options.showEmpty && this._removeHiddenItems(viewItems); + + options.notifyProgress(0.75); - when(foreachTreeAsync(viewHeaderItems, (items) => { + when(foreachTreeAsync(viewItems, (items) => { const item = items[0]; + const { isMetric } = item; + const field = headerDescriptions[items.length - 1] || {}; - if (!item.children || item.children.length === 0) { - item.depthSize = depthSize - items.length + 1; + if (item.type === DATA_TYPE && !isMetric) { + item.width = field.width; } - })).done(() => { - if (cellDescriptionsCount > 1) { - addMetricHeaderItems(viewHeaderItems, cellDescriptions, options); - } - - !options.showEmpty && removeHiddenItems(viewHeaderItems); - - options.notifyProgress(0.75); - when(foreachTreeAsync(viewHeaderItems, (items) => { - const item = items[0]; - const { isMetric } = item; - const field = headerDescriptions[items.length - 1] || {}; + if (hideData && item.type === DATA_TYPE) { + const parentChildren = (items[1] ? items[1].children : viewItems) || []; - if (item.type === DATA_TYPE && !isMetric) { - item.width = field.width; - } + parentChildren.splice(parentChildren.indexOf(item), 1); + return; + } - if (hideData && item.type === DATA_TYPE) { - const parentChildren = (items[1] ? items[1].children : viewHeaderItems) || []; + if (isMetric) { + item.wordWrapEnabled = cellDescriptions[item.dataIndex].wordWrapEnabled; + } else { + item.wordWrapEnabled = field.wordWrapEnabled; + } - parentChildren.splice(parentChildren.indexOf(item), 1); - return; - } + item.isLast = !item.children?.length; + if (item.isLast) { + each(options.sortBySummaryPaths, (_, sortBySummaryPath) => { + if (!isDefined(item.dataIndex)) { + sortBySummaryPath = sortBySummaryPath.slice(0); + sortBySummaryPath.pop(); + } - if (isMetric) { - item.wordWrapEnabled = cellDescriptions[item.dataIndex].wordWrapEnabled; - } else { - item.wordWrapEnabled = field.wordWrapEnabled; - } + if (this._isItemSorted(items, sortBySummaryPath)) { + item.sorted = true; + return false; + } - item.isLast = !item.children?.length; - if (item.isLast) { - each(options.sortBySummaryPaths, (_, sortBySummaryPath) => { - if (!isDefined(item.dataIndex)) { - sortBySummaryPath = sortBySummaryPath.slice(0); - sortBySummaryPath.pop(); - } - - if (isItemSorted(items, sortBySummaryPath)) { - item.sorted = true; - return false; - } - - return undefined; - }); - } - item.text = getHeaderItemText(item, field, options); - })).done(() => { - if (!viewHeaderItems.length) { - viewHeaderItems.push({}); - } - options.notifyProgress(1); - d.resolve(viewHeaderItems); - }); + return undefined; + }); + } + item.text = this.getHeaderItemText(item, field, options); + })).done(() => { + if (!viewItems.length) { + viewItems.push({}); + } + options.notifyProgress(1); + d.resolve(viewItems); }); }); + }); - return d; - }; + return d; + }; - function createHeaderItem(childrenStack, depth, index) { - const parent = childrenStack[depth] = childrenStack[depth] || []; - const node: any = parent[index] = {}; + _createHeaderItem = (childrenStack, depth, index) => { + const parent = childrenStack[depth] = childrenStack[depth] || []; + const node: any = parent[index] = {}; - if (childrenStack[depth + 1]) { - node.children = childrenStack[depth + 1]; - // T541266 - for (let i = depth + 1; i < childrenStack.length; i += 1) { - childrenStack[i] = undefined; - } - childrenStack.length = depth + 1; + if (childrenStack[depth + 1]) { + node.children = childrenStack[depth + 1]; + // T541266 + for (let i = depth + 1; i < childrenStack.length; i += 1) { + childrenStack[i] = undefined; } - - return node; + childrenStack.length = depth + 1; } - function createViewHeaderItems(headerItems, headerDescriptions) { - const headerDescriptionsCount = headerDescriptions?.length || 0; - const childrenStack = []; - // @ts-expect-error - const d = new Deferred(); - let headerItem; + return node; + }; - when(foreachTreeAsync(headerItems, (items, index) => { - const item = items[0]; - const path = createPath(items); - - headerItem = createHeaderItem(childrenStack, path.length, index); - - headerItem.type = DATA_TYPE; - headerItem.value = item.value; - headerItem.path = path; - headerItem.text = item.text; - headerItem.index = item.index; - headerItem.displayText = item.displayText; - headerItem.key = item.key; - headerItem.isEmpty = item.isEmpty; - - if (path.length < headerDescriptionsCount - && (!item.children || item.children.length !== 0)) { - headerItem.expanded = !!item.children; - } - })).done(() => { - d.resolve(createHeaderItem(childrenStack, 0, 0).children || []); - }); + _createViewHeaderItems = (headerItems, headerDescriptions) => { + const headerDescriptionsCount = headerDescriptions?.length || 0; + const childrenStack = []; + // @ts-expect-error Deferred interop with when() + const d = new Deferred(); + let headerItem; - return d; - } + when(foreachTreeAsync(headerItems, (items, index) => { + const item = items[0]; + const path = createPath(items); - function addMetricHeaderItems(headerItems, cellDescriptions, options) { - foreachTree(headerItems, (items) => { - const item = items[0]; - let i; + headerItem = this._createHeaderItem(childrenStack, path.length, index); - if (!item.children || item.children.length === 0) { - item.children = []; - for (i = 0; i < cellDescriptions.length; i += 1) { - const isGrandTotal = item.type === GRAND_TOTAL_TYPE; - const isTotal = item.type === TOTAL_TYPE; - const isValue = item.type === DATA_TYPE; - const columnIsHidden = cellDescriptions[i].visible === false - || (isGrandTotal && options.hiddenGrandTotals.includes(i)) - || (isTotal && options.hiddenTotals.includes(i)) - || (isValue && options.hiddenValues.includes(i)); - - if (columnIsHidden) { - continue; - } + headerItem.type = DATA_TYPE; + headerItem.value = item.value; + headerItem.path = path; + headerItem.text = item.text; + headerItem.index = item.index; + headerItem.displayText = item.displayText; + headerItem.key = item.key; + headerItem.isEmpty = item.isEmpty; - item.children.push({ - caption: cellDescriptions[i].caption, - path: item.path, - type: item.type, - value: i, - index: item.index, - dataIndex: i, - isMetric: true, - isEmpty: item.isEmpty?.[i], - }); + if (path.length < headerDescriptionsCount + && (!item.children || item.children.length !== 0)) { + headerItem.expanded = !!item.children; + } + })).done(() => { + d.resolve(this._createHeaderItem(childrenStack, 0, 0).children || []); + }); + + return d; + }; + + _addMetricHeaderItems = (headerItems, cellDescriptions, options) => { + foreachTree(headerItems, (items) => { + const item = items[0]; + let i; + + if (!item.children || item.children.length === 0) { + item.children = []; + for (i = 0; i < cellDescriptions.length; i += 1) { + const isGrandTotal = item.type === GRAND_TOTAL_TYPE; + const isTotal = item.type === TOTAL_TYPE; + const isValue = item.type === DATA_TYPE; + const columnIsHidden = cellDescriptions[i].visible === false + || (isGrandTotal && options.hiddenGrandTotals.includes(i)) + || (isTotal && options.hiddenTotals.includes(i)) + || (isValue && options.hiddenValues.includes(i)); + + if (columnIsHidden) { + continue; } + + item.children.push({ + caption: cellDescriptions[i].caption, + path: item.path, + type: item.type, + value: i, + index: item.index, + dataIndex: i, + isMetric: true, + isEmpty: item.isEmpty?.[i], + }); } - }); - } + } + }); + }; - function addAdditionalTotalHeaderItems( - headerItems, - headerDescriptions, - showTotalsPrior, - isTree, - ) { - showTotalsPrior = showTotalsPrior || isTree; + _addAdditionalTotalHeaderItems = ( + headerItems, + headerDescriptions, + showTotalsPrior, + isTree, + ) => { + showTotalsPrior = showTotalsPrior || isTree; - foreachTree(headerItems, (items, index) => { - const item = items[0]; - const parentChildren = (items[1] ? items[1].children : headerItems) || []; - const dataField = headerDescriptions[items.length - 1]; - - if (item.type === DATA_TYPE - && item.expanded - && (dataField.showTotals !== false || isTree)) { - index !== -1 && parentChildren.splice( - showTotalsPrior - ? index - : index + 1, - 0, - extend({}, item, { - children: null, - type: TOTAL_TYPE, - expanded: showTotalsPrior ? true : null, - isAdditionalTotal: true, - }), - ); - - if (showTotalsPrior) { - item.expanded = null; - } + foreachTree(headerItems, (items, index) => { + const item = items[0]; + const parentChildren = (items[1] ? items[1].children : headerItems) || []; + const dataField = headerDescriptions[items.length - 1]; + + if (item.type === DATA_TYPE + && item.expanded + && (dataField.showTotals !== false || isTree)) { + index !== -1 && parentChildren.splice( + showTotalsPrior + ? index + : index + 1, + 0, + extend({}, item, { + children: null, + type: TOTAL_TYPE, + expanded: showTotalsPrior ? true : null, + isAdditionalTotal: true, + }), + ); + + if (showTotalsPrior) { + item.expanded = null; } - }); + } + }); + }; + + _removeEmptyParent = (items, index) => { + const parent = items[index + 1]; + + if (!items[index].children.length && parent?.children) { + parent.children.splice(parent.children.indexOf(items[index]), 1); + this._removeEmptyParent(items, index + 1); } + }; - const removeEmptyParent = function (items, index) { - const parent = items[index + 1]; + _removeHiddenItems = (headerItems) => { + foreachTree([{ children: headerItems }], (items, index) => { + const item = items[0]; + const parentChildren = (items[1] ? items[1].children : headerItems) || []; + let { isEmpty } = item; - if (!items[index].children.length && parent?.children) { - parent.children.splice(parent.children.indexOf(items[index]), 1); - removeEmptyParent(items, index + 1); + if (isEmpty?.length) { + isEmpty = item.isEmpty.filter((v) => v).length === isEmpty.length; } - }; - function removeHiddenItems(headerItems) { - foreachTree([{ children: headerItems }], (items, index) => { - const item = items[0]; - const parentChildren = (items[1] ? items[1].children : headerItems) || []; - let { isEmpty } = item; + if (item && !item.children && isEmpty) { + parentChildren.splice(index, 1); + this._removeEmptyParent(items, 1); + } + }); + }; - if (isEmpty?.length) { - isEmpty = item.isEmpty.filter((isEmpty) => isEmpty).length === isEmpty.length; - } + _fillHeaderInfo = (info, viewHeaderItems, depthSize, isHorizontal, isTree) => { + let lastIndex = 0; + let index; + let depth; + const indexesByDepth = [0]; - if (item && !item.children && isEmpty) { - parentChildren.splice(index, 1); - removeEmptyParent(items, 1); - } + foreachTree(viewHeaderItems, (items) => { + const headerItem = items[0]; + depth = headerItem.isMetric ? depthSize : items.length - 1; + while (indexesByDepth.length - 1 < depth) { + indexesByDepth.push(indexesByDepth[indexesByDepth.length - 1]); + } + index = indexesByDepth[depth] || 0; + lastIndex = this._addInfoItem(info, { + headerItem, + index, + lastIndex, + depth, + isHorizontal, + isTree, }); - } + indexesByDepth.length = depth; + indexesByDepth.push(lastIndex); + }); + }; - const fillHeaderInfo = function (info, viewHeaderItems, depthSize, isHorizontal, isTree) { - let lastIndex = 0; - let index; - let depth; - const indexesByDepth = [0]; - - foreachTree(viewHeaderItems, (items) => { - const headerItem = items[0]; - depth = headerItem.isMetric ? depthSize : items.length - 1; - while (indexesByDepth.length - 1 < depth) { - indexesByDepth.push(indexesByDepth[indexesByDepth.length - 1]); - } - index = indexesByDepth[depth] || 0; - lastIndex = addInfoItem(info, { - headerItem, - index, - lastIndex, - depth, - isHorizontal, - isTree, - }); - indexesByDepth.length = depth; - indexesByDepth.push(lastIndex); - }); - }; + getHeaderItemText(item, description, options) { + let { text } = item; + + if (isDefined(item.displayText)) { + text = item.displayText; + } else if (isDefined(item.caption)) { + text = item.caption; + } else if (item.type === GRAND_TOTAL_TYPE) { + text = options.texts.grandTotal; + } - return function (headerItems, headerDescriptions, cellDescriptions, isHorizontal, options) { - const info = []; - const depthSize = getHeaderItemsDepth(headerItems) || 1; - // @ts-expect-error - const d = new Deferred(); + if (item.isAdditionalTotal) { + text = format(options.texts.total || '', text); + } - getViewHeaderItems(headerItems, headerDescriptions, cellDescriptions, depthSize, options) - .done((viewHeaderItems) => { - fillHeaderInfo(info, viewHeaderItems, depthSize, isHorizontal, options.layout === 'tree'); - options.notifyProgress(1); - d.resolve(info); - }); + return text; + } - return d; - }; - }()); + formatCellValue(value, dataField, errorText) { + return value === NOT_AVAILABLE ? errorText : formatValue(value, dataField); + } - function createSortPaths(headerFields, dataFields) { + createSortPaths(headerFields, dataFields) { const sortBySummaryPaths: any = []; each(headerFields, (_, headerField) => { @@ -467,7 +581,7 @@ const DataController = Class.inherit((function () { return sortBySummaryPaths; } - function foreachRowInfo(rowsInfo, callback) { + foreachRowInfo(rowsInfo, callback) { let columnOffset = 0; const columnOffsetResetIndexes: any = []; @@ -495,12 +609,12 @@ const DataController = Class.inherit((function () { } } - function createCellsInfo(rowsInfo, columnsInfo, data, dataFields, dataFieldArea, errorText) { + createCellsInfo(rowsInfo, columnsInfo, data, dataFields, dataFieldArea, errorText) { const info: any = []; const dataFieldAreaInRows = dataFieldArea === 'row'; const dataSourceCells = data.values; - dataSourceCells.length && foreachRowInfo(rowsInfo, (rowInfo, rowIndex) => { + dataSourceCells.length && this.foreachRowInfo(rowsInfo, (rowInfo, rowIndex) => { const row: any = info[rowIndex] = []; const dataRow = dataSourceCells[ rowInfo.dataSourceIndex >= 0 @@ -526,7 +640,7 @@ const DataController = Class.inherit((function () { const cellValue = cell[dataIndex]; row[columnIndex] = { - text: formatCellValue(cellValue, dataField, errorText), + text: this.formatCellValue(cellValue, dataField, errorText), value: cellValue, format: dataField.format, dataType: dataField.dataType, @@ -549,7 +663,7 @@ const DataController = Class.inherit((function () { return info; } - function getHeaderIndexedItems(headerItems, options) { + getHeaderIndexedItems(headerItems, options) { let visibleIndex = 0; const indexedItems: any = []; @@ -573,7 +687,7 @@ const DataController = Class.inherit((function () { return indexedItems; } - function createScrollController(dataController, component, dataAdapter) { + createScrollController(dataController, component, dataAdapter) { return new VirtualScrollControllerModule.VirtualScrollController(component, extend({ hasKnownLastPage() { return true; @@ -613,7 +727,7 @@ const DataController = Class.inherit((function () { }, dataAdapter)); } - function getHiddenTotals(dataFields) { + getHiddenTotals(dataFields) { const result: any = []; each(dataFields, (index, field) => { if (field.showTotals === false) { @@ -623,7 +737,7 @@ const DataController = Class.inherit((function () { return result; } - function getHiddenValues(dataFields) { + getHiddenValues(dataFields) { const result: any = []; dataFields.forEach((field, index) => { @@ -639,7 +753,7 @@ const DataController = Class.inherit((function () { return result; } - function getHiddenGrandTotalsTotals(dataFields, columnFields) { + getHiddenGrandTotalsTotals(dataFields, columnFields) { let result: any = []; each(dataFields, (index, field) => { if (field.showGrandTotals === false) { @@ -654,701 +768,622 @@ const DataController = Class.inherit((function () { return result; } - const members = { - ctor(options) { - const that: any = this; - const virtualScrollControllerChanged = that._fireChanged.bind(that); - - options = that._options = options || {}; - - that.dataSourceChanged = Callbacks(); - that._dataSource = that._createDataSource(options); - - if (options.component && options.component.option('scrolling.mode') === 'virtual') { - that._rowsScrollController = createScrollController(that, options.component, { - totalItemsCount() { - return that.totalRowCount(); - }, - pageIndex(index) { - return that.rowPageIndex(index); - }, - pageSize() { - return that.rowPageSize(); - }, - - load() { - if (that._rowsScrollController.pageIndex() >= this.pageCount()) { - that._rowsScrollController.pageIndex(this.pageCount() - 1); - } - return that._rowsScrollController.handleDataChanged(function () { - if (that._dataSource.paginate()) { - that._dataSource.load(); - } else { - // - @ts-expect-error - virtualScrollControllerChanged.apply(this, arguments); - } - }); - }, - }); + _fireChanged() { + const that: any = this; + const startChanging = new Date(); - that._columnsScrollController = createScrollController(that, options.component, { - totalItemsCount() { - return that.totalColumnCount(); - }, - pageIndex(index) { - return that.columnPageIndex(index); - }, - pageSize() { - return that.columnPageSize(); - }, - - load() { - if (that._columnsScrollController.pageIndex() >= this.pageCount()) { - that._columnsScrollController.pageIndex(this.pageCount() - 1); - } - - return that._columnsScrollController.handleDataChanged(function () { - if (that._dataSource.paginate()) { - that._dataSource.load(); - } else { - // - @ts-expect-error - virtualScrollControllerChanged.apply(this, arguments); - } - }); - }, - - }); - } - - that._stateStoringController = new stateStoring.StateStoringController(options.component).init(); - - that._columnsInfo = []; - that._rowsInfo = []; - that._cellsInfo = []; - - that.expandValueChanging = Callbacks(); - that.loadingChanged = Callbacks(); - that.progressChanged = Callbacks(); - that.scrollChanged = Callbacks(); + that.changed && !that._lockChanged && that.changed.fire(); + that._changingDuration = (new Date() as any) - (startChanging as any); + } - that.load(); - that._update(); - that.changed = Callbacks(); - }, - - _fireChanged() { - const that: any = this; - const startChanging = new Date(); - - that.changed && !that._lockChanged && that.changed.fire(); - that._changingDuration = (new Date() as any) - (startChanging as any); - }, - - _correctSkipsTakes(rowIndex, rowSkip, rowSpan, levels, skips, takes) { - const endIndex = rowSpan ? rowIndex + rowSpan - 1 : rowIndex; - skips[levels.length] = skips[levels.length] || 0; - takes[levels.length] = takes[levels.length] || 0; - if (endIndex < rowSkip) { - skips[levels.length] += 1; - } else { - takes[levels.length] += 1; - } - }, - - _calculatePagingForRowExpandedPaths(options, skips, takes, rowExpandedSkips, rowExpandedTakes) { - const rows = this._rowsInfo; - const rowCount = Math.min(options.rowSkip + options.rowTake, rows.length); - const { rowExpandedPaths } = options; - let levels: any = []; - const expandedPathIndexes = {}; - let i; - let j; - let path; + _correctSkipsTakes(rowIndex, rowSkip, rowSpan, levels, skips, takes) { + const endIndex = rowSpan ? rowIndex + rowSpan - 1 : rowIndex; + skips[levels.length] = skips[levels.length] || 0; + takes[levels.length] = takes[levels.length] || 0; + if (endIndex < rowSkip) { + skips[levels.length] += 1; + } else { + takes[levels.length] += 1; + } + } - rowExpandedPaths.forEach((path, index) => { - expandedPathIndexes[path] = index; - }); + _calculatePagingForRowExpandedPaths(options, skips, takes, rowExpandedSkips, rowExpandedTakes) { + const rows = this._rowsInfo; + const rowCount = Math.min(options.rowSkip + options.rowTake, rows.length); + const { rowExpandedPaths } = options; + let levels: any = []; + const expandedPathIndexes = {}; + let i; + let j; + let path; + + rowExpandedPaths.forEach((path, index) => { + expandedPathIndexes[path] = index; + }); - for (i = 0; i < rowCount; i += 1) { - takes.length = skips.length = levels.length + 1; - for (j = 0; j < rows[i].length; j += 1) { - const cell = rows[i][j]; + for (i = 0; i < rowCount; i += 1) { + takes.length = skips.length = levels.length + 1; + for (j = 0; j < rows[i].length; j += 1) { + const cell = rows[i][j]; - if (cell.type === 'D') { - this._correctSkipsTakes(i, options.rowSkip, cell.rowspan, levels, skips, takes); + if (cell.type === 'D') { + this._correctSkipsTakes(i, options.rowSkip, cell.rowspan, levels, skips, takes); - path = cell.path || path; - const expandIndex = path && path.length > 1 - ? expandedPathIndexes[path.slice(0, -1)] - : -1; + path = cell.path || path; + const expandIndex = path && path.length > 1 + ? expandedPathIndexes[path.slice(0, -1)] + : -1; - if (expandIndex >= 0) { - rowExpandedSkips[expandIndex] = skips[levels.length] || 0; - rowExpandedTakes[expandIndex] = takes[levels.length] || 0; - } + if (expandIndex >= 0) { + rowExpandedSkips[expandIndex] = skips[levels.length] || 0; + rowExpandedTakes[expandIndex] = takes[levels.length] || 0; + } - if (cell.rowspan) { - levels.push(cell.rowspan); - } + if (cell.rowspan) { + levels.push(cell.rowspan); } } - levels = levels.map((level) => level - 1).filter((level) => level > 0); } - }, + levels = levels.map((level) => level - 1).filter((level) => level > 0); + } + } - _calculatePagingForColumnExpandedPaths(options, skips, takes, expandedSkips, expandedTakes) { - const skipByPath = {}; - const takeByPath = {}; + _calculatePagingForColumnExpandedPaths(options, skips, takes, expandedSkips, expandedTakes) { + const skipByPath = {}; + const takeByPath = {}; - foreachColumnInfo(this._columnsInfo, (columnInfo, columnIndex) => { - if (columnInfo.type === 'D' && columnInfo.path && columnInfo.dataIndex === undefined) { - const colspan = columnInfo.colspan || 1; - const path = columnInfo.path.slice(0, -1).toString(); + foreachColumnInfo(this._columnsInfo, (columnInfo, columnIndex) => { + if (columnInfo.type === 'D' && columnInfo.path && columnInfo.dataIndex === undefined) { + const colspan = columnInfo.colspan || 1; + const path = columnInfo.path.slice(0, -1).toString(); - skipByPath[path] = skipByPath[path] || 0; - takeByPath[path] = takeByPath[path] || 0; + skipByPath[path] = skipByPath[path] || 0; + takeByPath[path] = takeByPath[path] || 0; - if (columnIndex + colspan <= options.columnSkip) { - skipByPath[path] += 1; - } else if (columnIndex < options.columnSkip + options.columnTake) { - takeByPath[path] += 1; - } + if (columnIndex + colspan <= options.columnSkip) { + skipByPath[path] += 1; + } else if (columnIndex < options.columnSkip + options.columnTake) { + takeByPath[path] += 1; } - }); + } + }); - skips[0] = skipByPath['']; - takes[0] = takeByPath['']; + skips[0] = skipByPath['']; + takes[0] = takeByPath['']; - options.columnExpandedPaths.forEach((path, index) => { - const skip = skipByPath[path]; - const take = takeByPath[path]; + options.columnExpandedPaths.forEach((path, index) => { + const skip = skipByPath[path]; + const take = takeByPath[path]; - if (skip !== undefined) { - expandedSkips[index] = skip; - } - if (take !== undefined) { - expandedTakes[index] = take; - } - }); - }, - - _processPagingForExpandedPaths(options, area, storeLoadOptions, reload) { - const expandedPaths = options[`${area}ExpandedPaths`]; - const expandedSkips = expandedPaths.map(() => 0); - const expandedTakes = expandedPaths.map(() => (reload ? options.pageSize : 0)); - const skips = []; - const takes = []; - - if (!reload) { - if (area === 'row') { - this._calculatePagingForRowExpandedPaths( - options, - skips, - takes, - expandedSkips, - expandedTakes, - ); - } else { - this._calculatePagingForColumnExpandedPaths( - options, - skips, - takes, - expandedSkips, - expandedTakes, - ); - } + if (skip !== undefined) { + expandedSkips[index] = skip; } - this._savePagingForExpandedPaths( - options, - area, - storeLoadOptions, - skips[0], - takes[0], - expandedSkips, - expandedTakes, - ); - }, + if (take !== undefined) { + expandedTakes[index] = take; + } + }); + } - _savePagingForExpandedPaths( + _processPagingForExpandedPaths(options, area, storeLoadOptions, reload) { + const expandedPaths = options[`${area}ExpandedPaths`]; + const expandedSkips = expandedPaths.map(() => 0); + const expandedTakes = expandedPaths.map(() => (reload ? options.pageSize : 0)); + const skips = []; + const takes = []; + + if (!reload) { + if (area === 'row') { + this._calculatePagingForRowExpandedPaths( + options, + skips, + takes, + expandedSkips, + expandedTakes, + ); + } else { + this._calculatePagingForColumnExpandedPaths( + options, + skips, + takes, + expandedSkips, + expandedTakes, + ); + } + } + this._savePagingForExpandedPaths( options, area, storeLoadOptions, - skip, - take, + skips[0], + takes[0], expandedSkips, expandedTakes, - ) { - const expandedPaths = options[`${area}ExpandedPaths`]; - - options[`${area}ExpandedPaths`] = []; - options[`${area}Skip`] = skip !== undefined ? skip : options[`${area}Skip`]; - options[`${area}Take`] = take !== undefined ? take : options[`${area}Take`]; - - for (let i = 0; i < expandedPaths.length; i += 1) { - if (expandedTakes[i]) { - const isOppositeArea = options.area && options.area !== area; - - storeLoadOptions.push(extend({ - area, - headerName: `${area}s`, - }, options, { - [`${area}Skip`]: expandedSkips[i], - [`${area}Take`]: expandedTakes[i], - [isOppositeArea ? 'oppositePath' : 'path']: expandedPaths[i], - })); - } + ); + } + + _savePagingForExpandedPaths( + options, + area, + storeLoadOptions, + skip, + take, + expandedSkips, + expandedTakes, + ) { + const expandedPaths = options[`${area}ExpandedPaths`]; + + options[`${area}ExpandedPaths`] = []; + options[`${area}Skip`] = skip !== undefined ? skip : options[`${area}Skip`]; + options[`${area}Take`] = take !== undefined ? take : options[`${area}Take`]; + + for (let i = 0; i < expandedPaths.length; i += 1) { + if (expandedTakes[i]) { + const isOppositeArea = options.area && options.area !== area; + + storeLoadOptions.push(extend({ + area, + headerName: `${area}s`, + }, options, { + [`${area}Skip`]: expandedSkips[i], + [`${area}Take`]: expandedTakes[i], + [isOppositeArea ? 'oppositePath' : 'path']: expandedPaths[i], + })); } - }, + } + } - _handleCustomizeStoreLoadOptions(storeLoadOptions, reload) { - const options = storeLoadOptions[0]; - const rowsScrollController = this._rowsScrollController; + _handleCustomizeStoreLoadOptions(storeLoadOptions, reload) { + const options = storeLoadOptions[0]; + const rowsScrollController = this._rowsScrollController; + + if (this._dataSource.paginate() && rowsScrollController) { + const rowPageSize = rowsScrollController.pageSize(); + + if (options.headerName === 'rows') { + options.rowSkip = 0; + options.rowTake = rowPageSize; + options.rowExpandedPaths = []; + } else { + options.rowSkip = rowsScrollController.beginPageIndex() * rowPageSize; + options.rowTake = ( + rowsScrollController.endPageIndex() - rowsScrollController.beginPageIndex() + 1 + ) * rowPageSize; + this._processPagingForExpandedPaths(options, 'row', storeLoadOptions, reload); + } + } - if (this._dataSource.paginate() && rowsScrollController) { - const rowPageSize = rowsScrollController.pageSize(); + const columnsScrollController = this._columnsScrollController; - if (options.headerName === 'rows') { - options.rowSkip = 0; - options.rowTake = rowPageSize; - options.rowExpandedPaths = []; + if (this._dataSource.paginate() && columnsScrollController) { + const columnPageSize = columnsScrollController.pageSize(); + storeLoadOptions.forEach((options) => { + if (options.headerName === 'columns') { + options.columnSkip = 0; + options.columnTake = columnPageSize; + options.columnExpandedPaths = []; } else { - options.rowSkip = rowsScrollController.beginPageIndex() * rowPageSize; - options.rowTake = ( - rowsScrollController.endPageIndex() - rowsScrollController.beginPageIndex() + 1 - ) * rowPageSize; - this._processPagingForExpandedPaths(options, 'row', storeLoadOptions, reload); + options.columnSkip = columnsScrollController.beginPageIndex() * columnPageSize; + options.columnTake = ( + columnsScrollController.endPageIndex() - columnsScrollController.beginPageIndex() + 1 + ) * columnPageSize; + this._processPagingForExpandedPaths(options, 'column', storeLoadOptions, reload); } - } + }); + } + } - const columnsScrollController = this._columnsScrollController; + load() { + const that: any = this; + const stateStoringController = this._stateStoringController; - if (this._dataSource.paginate() && columnsScrollController) { - const columnPageSize = columnsScrollController.pageSize(); - storeLoadOptions.forEach((options) => { - if (options.headerName === 'columns') { - options.columnSkip = 0; - options.columnTake = columnPageSize; - options.columnExpandedPaths = []; - } else { - options.columnSkip = columnsScrollController.beginPageIndex() * columnPageSize; - options.columnTake = ( - columnsScrollController.endPageIndex() - columnsScrollController.beginPageIndex() + 1 - ) * columnPageSize; - this._processPagingForExpandedPaths(options, 'column', storeLoadOptions, reload); - } - }); - } - }, + if (stateStoringController.isEnabled() && !stateStoringController.isLoaded()) { + stateStoringController.load().always((state) => { + if (state) { + that._dataSource.state(state); + } else { + that._dataSource.load(); + } + }); + } else { + that._dataSource.load(); + } + } - load() { - const that: any = this; - const stateStoringController = this._stateStoringController; + calculateVirtualContentParams(contentParams) { + const that: any = this; + const rowsScrollController = that._rowsScrollController; + const columnsScrollController = that._columnsScrollController; - if (stateStoringController.isEnabled() && !stateStoringController.isLoaded()) { - stateStoringController.load().always((state) => { - if (state) { - that._dataSource.state(state); - } else { - that._dataSource.load(); - } - }); - } else { - that._dataSource.load(); - } - }, + if (rowsScrollController && columnsScrollController) { + rowsScrollController.viewportItemSize(contentParams.virtualRowHeight); + rowsScrollController.viewportSize( + contentParams.viewportHeight / rowsScrollController.viewportItemSize(), + ); + rowsScrollController.setContentItemSizes(contentParams.itemHeights); - calculateVirtualContentParams(contentParams) { - const that: any = this; - const rowsScrollController = that._rowsScrollController; - const columnsScrollController = that._columnsScrollController; + columnsScrollController.viewportItemSize(contentParams.virtualColumnWidth); + columnsScrollController.viewportSize( + contentParams.viewportWidth / columnsScrollController.viewportItemSize(), + ); + columnsScrollController.setContentItemSizes(contentParams.itemWidths); - if (rowsScrollController && columnsScrollController) { - rowsScrollController.viewportItemSize(contentParams.virtualRowHeight); - rowsScrollController.viewportSize( - contentParams.viewportHeight / rowsScrollController.viewportItemSize(), - ); - rowsScrollController.setContentItemSizes(contentParams.itemHeights); + deferUpdate(() => { + columnsScrollController.loadIfNeed(); + rowsScrollController.loadIfNeed(); + }); - columnsScrollController.viewportItemSize(contentParams.virtualColumnWidth); - columnsScrollController.viewportSize( - contentParams.viewportWidth / columnsScrollController.viewportItemSize(), - ); - columnsScrollController.setContentItemSizes(contentParams.itemWidths); + that.scrollChanged.fire({ + left: columnsScrollController.getViewportPosition(), + top: rowsScrollController.getViewportPosition(), + }); - deferUpdate(() => { - columnsScrollController.loadIfNeed(); - rowsScrollController.loadIfNeed(); - }); + return { + contentTop: rowsScrollController.getContentOffset(), + contentLeft: columnsScrollController.getContentOffset(), + width: columnsScrollController.getVirtualContentSize(), + height: rowsScrollController.getVirtualContentSize(), + }; + } - that.scrollChanged.fire({ - left: columnsScrollController.getViewportPosition(), - top: rowsScrollController.getViewportPosition(), - }); + return undefined; + } - return { - contentTop: rowsScrollController.getContentOffset(), - contentLeft: columnsScrollController.getContentOffset(), - width: columnsScrollController.getVirtualContentSize(), - height: rowsScrollController.getVirtualContentSize(), - }; - } + setViewportPosition(left, top) { + this._rowsScrollController.setViewportPosition(top || 0); + this._columnsScrollController.setViewportPosition(left || 0); + } - return undefined; - }, - - setViewportPosition(left, top) { - this._rowsScrollController.setViewportPosition(top || 0); - this._columnsScrollController.setViewportPosition(left || 0); - }, - - subscribeToWindowScrollEvents($element) { - this._rowsScrollController?.subscribeToWindowScrollEvents($element); - }, - - updateWindowScrollPosition(position) { - this._rowsScrollController?.scrollTo(position); - }, - - updateViewOptions(options) { - extend(this._options, options); - this._update(); - }, - - _handleExpandValueChanging(e) { - this.expandValueChanging.fire(e); - }, - _handleLoadingChanged(isLoading) { - this.loadingChanged.fire(isLoading); - }, - _handleProgressChanged(progress) { - this.progressChanged.fire(progress); - }, - _handleFieldsPrepared(e) { - this._options.onFieldsPrepared?.(e); - }, - _createDataSource(options) { - const that: any = this; - const dataSourceOptions = options.dataSource; - let dataSource; - - that._isSharedDataSource = dataSourceOptions instanceof PivotGridDataSource; - - if (that._isSharedDataSource) { - dataSource = dataSourceOptions; - } else { - dataSource = new PivotGridDataSource(dataSourceOptions); - } + subscribeToWindowScrollEvents($element) { + this._rowsScrollController?.subscribeToWindowScrollEvents($element); + } - that._expandValueChangingHandler = that._handleExpandValueChanging.bind(that); - that._loadingChangedHandler = that._handleLoadingChanged.bind(that); - that._fieldsPreparedHandler = that._handleFieldsPrepared.bind(that); - that._customizeStoreLoadOptionsHandler = that._handleCustomizeStoreLoadOptions.bind(that); - that._changedHandler = function () { - that._update(); - that.dataSourceChanged.fire(); - }; - that._progressChangedHandler = function (progress) { - that._handleProgressChanged(progress * 0.8); - }; + updateWindowScrollPosition(position) { + this._rowsScrollController?.scrollTo(position); + } - dataSource.on('changed', that._changedHandler); - dataSource.on('expandValueChanging', that._expandValueChangingHandler); - dataSource.on('loadingChanged', that._loadingChangedHandler); - dataSource.on('progressChanged', that._progressChangedHandler); - dataSource.on('fieldsPrepared', that._fieldsPreparedHandler); - dataSource.on('customizeStoreLoadOptions', that._customizeStoreLoadOptionsHandler); - - return dataSource; - }, - - getDataSource() { - return this._dataSource; - }, - isLoading() { - return this._dataSource.isLoading(); - }, - beginLoading() { - this._dataSource.beginLoading(); - }, - endLoading() { - this._dataSource.endLoading(); - }, - _update() { - const that: any = this; - const dataSource = that._dataSource; - const options = that._options; - const columnFields = dataSource.getAreaFields('column'); - const rowFields = dataSource.getAreaFields('row'); - const dataFields = dataSource.getAreaFields('data'); - const dataFieldsForRows = options.dataFieldArea === 'row' ? dataFields : []; - const dataFieldsForColumns = options.dataFieldArea !== 'row' ? dataFields : []; - const data = dataSource.getData(); - const hiddenTotals = getHiddenTotals(dataFields); - const hiddenValues = getHiddenValues(dataFields); - const hiddenGrandTotals = getHiddenGrandTotalsTotals(dataFields, columnFields); - const grandTotalsAreHiddenForNotAllDataFields = dataFields.length > 0 - ? hiddenGrandTotals.length !== dataFields.length - : true; - - const commonOptions: any = { - texts: options.texts || {}, - hiddenTotals, - hiddenValues, - hiddenGrandTotals, - showEmpty: !options.hideEmptySummaryCells, - dataFields, - progress: 0, - }; - const rowOptions: any = extend({}, commonOptions, { - isEmptyGrandTotal: data.isEmptyGrandTotalRow, - showTotals: options.showRowTotals, - showTotalsPrior: options.showTotalsPrior === 'rows' || options.showTotalsPrior === 'both', - showGrandTotals: options.showRowGrandTotals !== false - && grandTotalsAreHiddenForNotAllDataFields, - sortBySummaryPaths: createSortPaths(columnFields, dataFields), - layout: options.rowHeaderLayout, - fields: rowFields, - }); - const columnOptions: any = extend({}, commonOptions, { - isEmptyGrandTotal: data.isEmptyGrandTotalColumn, - showTotals: options.showColumnTotals, - showTotalsPrior: options.showTotalsPrior === 'columns' || options.showTotalsPrior === 'both', - showGrandTotals: options.showColumnGrandTotals !== false - && grandTotalsAreHiddenForNotAllDataFields, - sortBySummaryPaths: createSortPaths(rowFields, dataFields), - fields: columnFields, - }); + updateViewOptions(options) { + extend(this._options, options); + this._update(); + } - const notifyProgress = function (progress) { - // - @ts-expect-error - this.progress = progress; - that._handleProgressChanged(0.8 + 0.1 * rowOptions.progress + 0.1 * columnOptions.progress); - }; + _handleExpandValueChanging(e) { + this.expandValueChanging.fire(e); + } - rowOptions.notifyProgress = notifyProgress; - columnOptions.notifyProgress = notifyProgress; + _handleLoadingChanged(isLoading) { + this.loadingChanged.fire(isLoading); + } - if (!isDefined(data.grandTotalRowIndex)) { - data.grandTotalRowIndex = getHeaderIndexedItems(data.rows, rowOptions).length; - } - if (!isDefined(data.grandTotalColumnIndex)) { - data.grandTotalColumnIndex = getHeaderIndexedItems(data.columns, columnOptions).length; - } + _handleProgressChanged(progress) { + this.progressChanged.fire(progress); + } - dataSource._changeLoadingCount(1); - - when( - createHeaderInfo(data.columns, columnFields, dataFieldsForColumns, true, columnOptions), - createHeaderInfo(data.rows, rowFields, dataFieldsForRows, false, rowOptions), - ).always(() => { - dataSource._changeLoadingCount(-1); - }).done((columnsInfo, rowsInfo) => { - that._columnsInfo = columnsInfo; - that._rowsInfo = rowsInfo; - - if (that._rowsScrollController - && that._columnsScrollController - && that.changed - && !that._dataSource.paginate()) { - that._rowsScrollController.reset(true); - that._columnsScrollController.reset(true); - - that._lockChanged = true; - that._rowsScrollController.load(); - that._columnsScrollController.load(); - that._lockChanged = false; - } - }).done(() => { - that._fireChanged(); - if (that._stateStoringController.isEnabled() && !that._dataSource.isLoading()) { - that._stateStoringController.state(that._dataSource.state()); - that._stateStoringController.save(); - } - }); - }, - - getRowsInfo(getAllData) { - const that: any = this; - const rowsInfo = that._rowsInfo; - const scrollController = that._rowsScrollController; - let rowspan; - const isOnePredefinedRow = rowsInfo.length === 1 && ( - !rowsInfo[0].type - || rowsInfo[0].type === GRAND_TOTAL_TYPE - ); + _handleFieldsPrepared(e) { + this._options.onFieldsPrepared?.(e); + } - if (scrollController && !getAllData && !isOnePredefinedRow) { - const startIndex = scrollController.beginPageIndex() * that.rowPageSize(); - const endIndex = scrollController.endPageIndex() * that.rowPageSize() + that.rowPageSize(); - const summaryFields = that._dataSource.getSummaryFields(); - const isRowDataFieldArea = this._options.dataFieldArea === 'row'; - const newRowsInfo: any = []; - let maxDepth = 1; - - foreachRowInfo(rowsInfo, (rowInfo, visibleIndex, rowIndex, _, columnIndex) => { - const isVisible = visibleIndex >= startIndex && rowIndex < endIndex; - const index = rowIndex < startIndex ? 0 : rowIndex - startIndex; - let cell = rowInfo; - - if (isVisible) { - newRowsInfo[index] = newRowsInfo[index] || []; - rowspan = rowIndex < startIndex - ? (rowInfo.rowspan - (startIndex - rowIndex)) || 1 - : rowInfo.rowspan; - - if (startIndex + index + rowspan > endIndex) { - rowspan = (endIndex - (index + startIndex)) || 1; - } + _createDataSource(options) { + const that: any = this; + const dataSourceOptions = options.dataSource; + let dataSource; - if (rowspan !== rowInfo.rowspan) { - cell = extend({}, cell, { - rowspan, - }); - } + that._isSharedDataSource = dataSourceOptions instanceof PivotGridDataSource; - newRowsInfo[index].push(cell); + if (that._isSharedDataSource) { + dataSource = dataSourceOptions; + } else { + dataSource = new PivotGridDataSource(dataSourceOptions); + } - const isSummaryCell = summaryFields.some((field) => field.caption === cell.text); - if (!isRowDataFieldArea || !isSummaryCell) { - maxDepth = math.max(maxDepth, columnIndex + 1); - } - } else { - return false; - } + that._expandValueChangingHandler = that._handleExpandValueChanging.bind(that); + that._loadingChangedHandler = that._handleLoadingChanged.bind(that); + that._fieldsPreparedHandler = that._handleFieldsPrepared.bind(that); + that._customizeStoreLoadOptionsHandler = that._handleCustomizeStoreLoadOptions.bind(that); + that._changedHandler = function () { + that._update(); + that.dataSourceChanged.fire(); + }; + that._progressChangedHandler = function (progress) { + that._handleProgressChanged(progress * 0.8); + }; - return undefined; - }); + dataSource.on('changed', that._changedHandler); + dataSource.on('expandValueChanging', that._expandValueChangingHandler); + dataSource.on('loadingChanged', that._loadingChangedHandler); + dataSource.on('progressChanged', that._progressChangedHandler); + dataSource.on('fieldsPrepared', that._fieldsPreparedHandler); + dataSource.on('customizeStoreLoadOptions', that._customizeStoreLoadOptionsHandler); - foreachRowInfo( - newRowsInfo, - // - @ts-expect-error - (rowInfo, visibleIndex, rowIndex, columnIndex, realColumnIndex) => { - const colspan = rowInfo.colspan || 1; + return dataSource; + } - if (realColumnIndex + colspan > maxDepth) { - newRowsInfo[rowIndex][columnIndex] = extend({}, rowInfo, { - colspan: (maxDepth - realColumnIndex) || 1, - }); - } - }, - ); + getDataSource() { + return this._dataSource; + } - return newRowsInfo; - } + isLoading() { + return this._dataSource.isLoading(); + } - return rowsInfo; - }, + beginLoading() { + this._dataSource.beginLoading(); + } - getColumnsInfo(getAllData) { - const that: any = this; - let info = that._columnsInfo; - const scrollController = that._columnsScrollController; + endLoading() { + this._dataSource.endLoading(); + } - if (scrollController && !getAllData) { - const startIndex = scrollController.beginPageIndex() * that.columnPageSize(); - const endIndex = scrollController.endPageIndex() * that.columnPageSize() - + that.columnPageSize(); + _update() { + const that: any = this; + const dataSource = that._dataSource; + const options = that._options; + const columnFields = dataSource.getAreaFields('column'); + const rowFields = dataSource.getAreaFields('row'); + const dataFields = dataSource.getAreaFields('data'); + const dataFieldsForRows = options.dataFieldArea === 'row' ? dataFields : []; + const dataFieldsForColumns = options.dataFieldArea !== 'row' ? dataFields : []; + const data = dataSource.getData(); + const hiddenTotals = this.getHiddenTotals(dataFields); + const hiddenValues = this.getHiddenValues(dataFields); + const hiddenGrandTotals = this.getHiddenGrandTotalsTotals(dataFields, columnFields); + const grandTotalsAreHiddenForNotAllDataFields = dataFields.length > 0 + ? hiddenGrandTotals.length !== dataFields.length + : true; + + const commonOptions: any = { + texts: options.texts || {}, + hiddenTotals, + hiddenValues, + hiddenGrandTotals, + showEmpty: !options.hideEmptySummaryCells, + dataFields, + progress: 0, + }; + const rowOptions: any = extend({}, commonOptions, { + isEmptyGrandTotal: data.isEmptyGrandTotalRow, + showTotals: options.showRowTotals, + showTotalsPrior: options.showTotalsPrior === 'rows' || options.showTotalsPrior === 'both', + showGrandTotals: options.showRowGrandTotals !== false + && grandTotalsAreHiddenForNotAllDataFields, + sortBySummaryPaths: this.createSortPaths(columnFields, dataFields), + layout: options.rowHeaderLayout, + fields: rowFields, + }); + const columnOptions: any = extend({}, commonOptions, { + isEmptyGrandTotal: data.isEmptyGrandTotalColumn, + showTotals: options.showColumnTotals, + showTotalsPrior: options.showTotalsPrior === 'columns' || options.showTotalsPrior === 'both', + showGrandTotals: options.showColumnGrandTotals !== false + && grandTotalsAreHiddenForNotAllDataFields, + sortBySummaryPaths: this.createSortPaths(rowFields, dataFields), + fields: columnFields, + }); - info = createColumnsInfo(info, startIndex, endIndex); - } + const notifyProgress = function (progress) { + this.progress = progress; + that._handleProgressChanged(0.8 + 0.1 * rowOptions.progress + 0.1 * columnOptions.progress); + }; - return info; - }, + rowOptions.notifyProgress = notifyProgress; + columnOptions.notifyProgress = notifyProgress; - totalRowCount() { - return this._rowsInfo.length; - }, + if (!isDefined(data.grandTotalRowIndex)) { + data.grandTotalRowIndex = this.getHeaderIndexedItems(data.rows, rowOptions).length; + } + if (!isDefined(data.grandTotalColumnIndex)) { + data.grandTotalColumnIndex = this.getHeaderIndexedItems(data.columns, columnOptions).length; + } - rowPageIndex(index) { - if (index !== undefined) { - this._rowPageIndex = index; + dataSource._changeLoadingCount(1); + + when( + this.createHeaderInfo(data.columns, columnFields, dataFieldsForColumns, true, columnOptions), + this.createHeaderInfo(data.rows, rowFields, dataFieldsForRows, false, rowOptions), + ).always(() => { + dataSource._changeLoadingCount(-1); + }).done((columnsInfo, rowsInfo) => { + that._columnsInfo = columnsInfo; + that._rowsInfo = rowsInfo; + + if (that._rowsScrollController + && that._columnsScrollController + && that.changed + && !that._dataSource.paginate()) { + that._rowsScrollController.reset(true); + that._columnsScrollController.reset(true); + + that._lockChanged = true; + that._rowsScrollController.load(); + that._columnsScrollController.load(); + that._lockChanged = false; } - return this._rowPageIndex || 0; - }, - - totalColumnCount() { - let count = 0; - if (this._columnsInfo?.length) { - for (let i = 0; i < this._columnsInfo[0].length; i += 1) { - count += this._columnsInfo[0][i].colspan || 1; - } + }).done(() => { + that._fireChanged(); + if (that._stateStoringController.isEnabled() && !that._dataSource.isLoading()) { + that._stateStoringController.state(that._dataSource.state()); + that._stateStoringController.save(); } + }); + } - return count; - }, + getRowsInfo(getAllData) { + const that: any = this; + const rowsInfo = that._rowsInfo; + const scrollController = that._rowsScrollController; + let rowspan; + const isOnePredefinedRow = rowsInfo.length === 1 && ( + !rowsInfo[0].type + || rowsInfo[0].type === GRAND_TOTAL_TYPE + ); + + if (scrollController && !getAllData && !isOnePredefinedRow) { + const startIndex = scrollController.beginPageIndex() * that.rowPageSize(); + const endIndex = scrollController.endPageIndex() * that.rowPageSize() + that.rowPageSize(); + const summaryFields = that._dataSource.getSummaryFields(); + const isRowDataFieldArea = this._options.dataFieldArea === 'row'; + const newRowsInfo: any = []; + let maxDepth = 1; + + this.foreachRowInfo(rowsInfo, (rowInfo, visibleIndex, rowIndex, _, columnIndex) => { + const isVisible = visibleIndex >= startIndex && rowIndex < endIndex; + const index = rowIndex < startIndex ? 0 : rowIndex - startIndex; + let cell = rowInfo; + + if (isVisible) { + newRowsInfo[index] = newRowsInfo[index] || []; + rowspan = rowIndex < startIndex + ? (rowInfo.rowspan - (startIndex - rowIndex)) || 1 + : rowInfo.rowspan; + + if (startIndex + index + rowspan > endIndex) { + rowspan = (endIndex - (index + startIndex)) || 1; + } - rowPageSize(size) { - if (size !== undefined) { - this._rowPageSize = size; - } - return this._rowPageSize || 20; - }, + if (rowspan !== rowInfo.rowspan) { + cell = extend({}, cell, { + rowspan, + }); + } - columnPageSize(size) { - if (size !== undefined) { - this._columnPageSize = size; - } - return this._columnPageSize || 20; - }, + newRowsInfo[index].push(cell); - columnPageIndex(index) { - if (index !== undefined) { - this._columnPageIndex = index; - } - return this._columnPageIndex || 0; - }, - - getCellsInfo(getAllData) { - const rowsInfo = this.getRowsInfo(getAllData); - const columnsInfo = this.getColumnsInfo(getAllData); - const data = this._dataSource.getData(); - const texts = this._options.texts || {}; - - return createCellsInfo( - rowsInfo, - columnsInfo, - data, - this._dataSource.getAreaFields('data'), - this._options.dataFieldArea, - texts.dataNotAvailable, + const isSummaryCell = summaryFields.some((field) => field.caption === cell.text); + if (!isRowDataFieldArea || !isSummaryCell) { + maxDepth = math.max(maxDepth, columnIndex + 1); + } + } else { + return false; + } + + return undefined; + }); + + this.foreachRowInfo( + newRowsInfo, + (rowInfo, visibleIndex, rowIndex, columnIndex, realColumnIndex) => { + const colspan = rowInfo.colspan || 1; + + if (realColumnIndex + colspan > maxDepth) { + newRowsInfo[rowIndex][columnIndex] = extend({}, rowInfo, { + colspan: (maxDepth - realColumnIndex) || 1, + }); + } + }, ); - }, - - dispose() { - const that: any = this; - if (that._isSharedDataSource) { - that._dataSource.off('changed', that._changedHandler); - that._dataSource.off('expandValueChanging', that._expandValueChangingHandler); - that._dataSource.off('loadingChanged', that._loadingChangedHandler); - that._dataSource.off('progressChanged', that._progressChangedHandler); - that._dataSource.off('fieldsPrepared', that._fieldsPreparedHandler); - that._dataSource.off('customizeStoreLoadOptions', that._customizeStoreLoadOptionsHandler); - } else { - that._dataSource.dispose(); + + return newRowsInfo; + } + + return rowsInfo; + } + + getColumnsInfo(getAllData) { + const that: any = this; + let info = that._columnsInfo; + const scrollController = that._columnsScrollController; + + if (scrollController && !getAllData) { + const startIndex = scrollController.beginPageIndex() * that.columnPageSize(); + const endIndex = scrollController.endPageIndex() * that.columnPageSize() + + that.columnPageSize(); + + info = createColumnsInfo(info, startIndex, endIndex); + } + + return info; + } + + totalRowCount() { + return this._rowsInfo.length; + } + + rowPageIndex(index) { + if (index !== undefined) { + this._rowPageIndex = index; + } + return this._rowPageIndex || 0; + } + + totalColumnCount() { + let count = 0; + if (this._columnsInfo?.length) { + for (let i = 0; i < this._columnsInfo[0].length; i += 1) { + count += this._columnsInfo[0][i].colspan || 1; } + } - that._columnsScrollController?.dispose(); - that._rowsScrollController?.dispose(); + return count; + } - that._stateStoringController.dispose(); + rowPageSize(size) { + if (size !== undefined) { + this._rowPageSize = size; + } + return this._rowPageSize || 20; + } - that.expandValueChanging.empty(); - that.changed.empty(); - that.loadingChanged.empty(); - that.progressChanged.empty(); - that.scrollChanged.empty(); - that.dataSourceChanged.empty(); - }, - }; + columnPageSize(size) { + if (size !== undefined) { + this._columnPageSize = size; + } + return this._columnPageSize || 20; + } + + columnPageIndex(index) { + if (index !== undefined) { + this._columnPageIndex = index; + } + return this._columnPageIndex || 0; + } + + getCellsInfo(getAllData) { + const rowsInfo = this.getRowsInfo(getAllData); + const columnsInfo = this.getColumnsInfo(getAllData); + const data = this._dataSource.getData(); + const texts = this._options.texts || {}; + + return this.createCellsInfo( + rowsInfo, + columnsInfo, + data, + this._dataSource.getAreaFields('data'), + this._options.dataFieldArea, + texts.dataNotAvailable, + ); + } + + dispose() { + const that: any = this; + if (that._isSharedDataSource) { + that._dataSource.off('changed', that._changedHandler); + that._dataSource.off('expandValueChanging', that._expandValueChangingHandler); + that._dataSource.off('loadingChanged', that._loadingChangedHandler); + that._dataSource.off('progressChanged', that._progressChangedHandler); + that._dataSource.off('fieldsPrepared', that._fieldsPreparedHandler); + that._dataSource.off('customizeStoreLoadOptions', that._customizeStoreLoadOptionsHandler); + } else { + that._dataSource.dispose(); + } - proxyMethod(members, 'applyPartialDataSource'); - proxyMethod(members, 'collapseHeaderItem'); - proxyMethod(members, 'expandHeaderItem'); - proxyMethod(members, 'getData'); - proxyMethod(members, 'isEmpty'); + that._columnsScrollController?.dispose(); + that._rowsScrollController?.dispose(); + + that._stateStoringController.dispose(); + + that.expandValueChanging.empty(); + that.changed.empty(); + that.loadingChanged.empty(); + that.progressChanged.empty(); + that.scrollChanged.empty(); + that.dataSourceChanged.empty(); + } +} - return members; -})()); +proxyMethod(DataController, 'applyPartialDataSource'); +proxyMethod(DataController, 'collapseHeaderItem'); +proxyMethod(DataController, 'expandHeaderItem'); +proxyMethod(DataController, 'getData'); +proxyMethod(DataController, 'isEmpty'); // eslint-disable-next-line @typescript-eslint/naming-convention const DataController__internals = { diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts b/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts index ce4da7a1481d..b5b1683bc03f 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts @@ -105,16 +105,95 @@ function isDataExists(data) { return data.rows.length || data.columns.length || data.values.length; } -const PivotGridDataSource = Class.inherit((function () { - const findHeaderItem = function (headerItems, path) { +class PivotGridDataSource { + _eventsStrategy: EventsStrategy; + + _store: any; + + _paginate: any; + + _pageSize: any; + + _data: any; + + _loadingCount: any; + + _isFieldsModified: any; + + _fields: any; + + _descriptions: any; + + _lastLoadOptions: any; + + _retrieveFields: any; + + _storeFields: any; + + _delayedLoadTask: any; + + _isDisposed: any; + + constructor(options) { + options = options || {}; + this._eventsStrategy = new EventsStrategy(this); + + const that: any = this; + const store = this.createStore(options, (progress) => { + that._eventsStrategy.fireEvent('progressChanged', [progress]); + }); + + that._store = store; + that._paginate = !!options.paginate; + that._pageSize = options.pageSize || 40; + that._data = { rows: [], columns: [], values: [] }; + that._loadingCount = 0; + + that._isFieldsModified = false; + + each( + [ + 'changed', + 'loadError', + 'loadingChanged', + 'progressChanged', + 'fieldsPrepared', + 'expandValueChanging', + ], + (_, eventName) => { + const optionName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`; + if (Object.prototype.hasOwnProperty.call(options, optionName)) { + this.on(eventName, options[optionName]); + } + }, + ); + + that._retrieveFields = isDefined(options.retrieveFields) ? options.retrieveFields : true; + + that._fields = options.fields || []; + that._descriptions = options.descriptions + ? extend(that._createDescriptions(), options.descriptions) + : undefined; + + if (!store) { + // TODO create dashboard store + extend(true, that._data, options.store || options); + } + } + + updateCalculatedFieldProperties(field, calculatedProperties) { + updateCalculatedFieldProperties(field, calculatedProperties); + } + + findHeaderItem(headerItems, path) { if (headerItems._cacheByPath) { return headerItems._cacheByPath[path.join('.')] || null; } return undefined; - }; + } - const getHeaderItemsLastIndex = function (headerItems, grandTotalIndex?) { + getHeaderItemsLastIndex(headerItems, grandTotalIndex?) { let i; let lastIndex = -1; let headerItem; @@ -126,10 +205,10 @@ const PivotGridDataSource = Class.inherit((function () { lastIndex = Math.max(lastIndex, headerItem.index); } if (headerItem.children) { - lastIndex = Math.max(lastIndex, getHeaderItemsLastIndex(headerItem.children)); + lastIndex = Math.max(lastIndex, this.getHeaderItemsLastIndex(headerItem.children)); } else if (headerItem.collapsedChildren) { // B232736 - lastIndex = Math.max(lastIndex, getHeaderItemsLastIndex(headerItem.collapsedChildren)); + lastIndex = Math.max(lastIndex, this.getHeaderItemsLastIndex(headerItem.collapsedChildren)); } } } @@ -137,11 +216,11 @@ const PivotGridDataSource = Class.inherit((function () { lastIndex = Math.max(lastIndex, grandTotalIndex); } return lastIndex; - }; + } - const updateHeaderItemChildren = function (headerItems, headerItem, children, grandTotalIndex) { - const applyingHeaderItemsCount = getHeaderItemsLastIndex(children) + 1; - let emptyIndex = getHeaderItemsLastIndex(headerItems, grandTotalIndex) + 1; + updateHeaderItemChildren(headerItems, headerItem, children, grandTotalIndex) { + const applyingHeaderItemsCount = this.getHeaderItemsLastIndex(children) + 1; + let emptyIndex = this.getHeaderItemsLastIndex(headerItems, grandTotalIndex) + 1; let index; const applyingItemIndexesToCurrent: any = []; let needIndexUpdate = false; @@ -178,13 +257,13 @@ const PivotGridDataSource = Class.inherit((function () { d.resolve(applyingItemIndexesToCurrent); }); return d; - }; + } - const updateHeaderItems = function (headerItems, newHeaderItems, grandTotalIndex) { + updateHeaderItems(headerItems, newHeaderItems, grandTotalIndex) { // @ts-expect-errors const d = new Deferred(); let emptyIndex = grandTotalIndex >= 0 - && getHeaderItemsLastIndex(headerItems, grandTotalIndex) + 1; + && this.getHeaderItemsLastIndex(headerItems, grandTotalIndex) + 1; const applyingItemIndexesToCurrent: any = []; @@ -195,12 +274,12 @@ const PivotGridDataSource = Class.inherit((function () { when(foreachTreeAsync(newHeaderItems, (newItems, index) => { const newItem = newItems[0]; if (newItem.index >= 0) { - let headerItem = findHeaderItem(headerItems, createPath(newItems)); + let headerItem = this.findHeaderItem(headerItems, createPath(newItems)); if (headerItem && headerItem.index >= 0) { applyingItemIndexesToCurrent[newItem.index] = headerItem.index; } else if (emptyIndex) { const path = createPath(newItems.slice(1)); - headerItem = findHeaderItem(headerItems, path); + headerItem = this.findHeaderItem(headerItems, path); const parentItems = path.length ? headerItem && headerItem.children : headerItems; if (parentItems) { @@ -216,9 +295,9 @@ const PivotGridDataSource = Class.inherit((function () { }); return d; - }; + } - const updateDataSourceCells = function ( + updateDataSourceCells( dataSource, newDataSourceCells, newRowItemIndexesToCurrent, @@ -243,7 +322,7 @@ const PivotGridDataSource = Class.inherit((function () { if (!dataSourceCells[rowIndex]) { dataSourceCells[rowIndex] = []; } - // eslint-disable-next-line eqeqeq + for (newColumnIndex = 0; newColumnIndex < newRowCells.length; newColumnIndex += 1) { newCell = newRowCells[newColumnIndex]; columnIndex = newColumnItemIndexesToCurrent[newColumnIndex]; @@ -257,9 +336,9 @@ const PivotGridDataSource = Class.inherit((function () { } } } - }; + } - function createLocalOrRemoteStore(dataSourceOptions, notifyProgress) { + createLocalOrRemoteStore(dataSourceOptions, notifyProgress) { const StoreConstructor = dataSourceOptions.remoteOperations || dataSourceOptions.paginate ? RemoteStore : LocalStore; // @ts-expect-error @@ -270,12 +349,12 @@ const PivotGridDataSource = Class.inherit((function () { })); } - function createStore(dataSourceOptions, notifyProgress) { + createStore(dataSourceOptions, notifyProgress) { let store; let storeOptions; if (isPlainObject(dataSourceOptions) && dataSourceOptions.load) { - store = createLocalOrRemoteStore(dataSourceOptions, notifyProgress); + store = this.createLocalOrRemoteStore(dataSourceOptions, notifyProgress); } else { // TODO remove if (dataSourceOptions && !dataSourceOptions.store) { @@ -289,15 +368,15 @@ const PivotGridDataSource = Class.inherit((function () { } else if ((isPlainObject(storeOptions) && storeOptions.type) || (storeOptions instanceof Store) || Array.isArray(storeOptions)) { - store = createLocalOrRemoteStore(dataSourceOptions, notifyProgress); - } else if (storeOptions instanceof Class) { + store = this.createLocalOrRemoteStore(dataSourceOptions, notifyProgress); + } else if (storeOptions instanceof Class || storeOptions instanceof xmlaStore.XmlaStore) { store = storeOptions; } } return store; } - function equalFields(fields, prevFields, count) { + equalFields(fields, prevFields, count) { for (let i = 0; i < count; i += 1) { if (!fields[i] || !prevFields[i] || fields[i].index !== prevFields[i].index) { return false; @@ -307,7 +386,7 @@ const PivotGridDataSource = Class.inherit((function () { return true; } - function getExpandedPaths(dataSource, loadOptions, dimensionName, prevLoadOptions) { + getExpandedPaths(dataSource, loadOptions, dimensionName, prevLoadOptions) { const result: any = []; const fields = (loadOptions && loadOptions[dimensionName]) || []; const prevFields = (prevLoadOptions && prevLoadOptions[dimensionName]) || []; @@ -318,7 +397,7 @@ const PivotGridDataSource = Class.inherit((function () { if (item.children && fields[path.length - 1] && !fields[path.length - 1].expanded) { if (path.length < fields.length - && (!prevLoadOptions || equalFields(fields, prevFields, path.length))) { + && (!prevLoadOptions || this.equalFields(fields, prevFields, path.length))) { result.push(path.slice()); } } @@ -326,7 +405,7 @@ const PivotGridDataSource = Class.inherit((function () { return result; } - function setFieldProperties(field, srcField, skipInitPropertySave, properties) { + setFieldProperties(field, srcField, skipInitPropertySave, properties) { if (srcField) { each(properties, (_, name) => { if (skipInitPropertySave) { @@ -347,11 +426,11 @@ const PivotGridDataSource = Class.inherit((function () { return field; } - function getFieldsState(fields, properties) { + getFieldsState(fields, properties) { const result: any = []; each(fields, (_, field) => { - result.push(setFieldProperties({ + result.push(this.setFieldProperties({ dataField: field.dataField, name: field.name, }, field, true, properties)); @@ -360,18 +439,18 @@ const PivotGridDataSource = Class.inherit((function () { return result; } - function getFieldStateId(field) { + getFieldStateId(field) { if (field.name) { return field.name; } return `${field.dataField}`; } - function getFieldsById(fields, id) { + getFieldsById(fields, id) { const result: any = []; each(fields || [], (_, field) => { - if (getFieldStateId(field) === id) { + if (this.getFieldStateId(field) === id) { result.push(field); } }); @@ -379,36 +458,36 @@ const PivotGridDataSource = Class.inherit((function () { return result; } - function setFieldsStateCore(stateFields, fields) { + setFieldsStateCore(stateFields, fields) { stateFields = stateFields || []; each(fields, (index, field) => { - setFieldProperties(field, stateFields[index], false, STATE_PROPERTIES); - updateCalculatedFieldProperties(field, CALCULATED_PROPERTIES); + this.setFieldProperties(field, stateFields[index], false, STATE_PROPERTIES); + this.updateCalculatedFieldProperties(field, CALCULATED_PROPERTIES); }); return fields; } - function setFieldsState(stateFields, fields) { + setFieldsState(stateFields, fields) { stateFields = stateFields || []; const fieldsById = {}; let id; each(fields, (_, field) => { - id = getFieldStateId(field); + id = this.getFieldStateId(field); if (!fieldsById[id]) { - fieldsById[id] = getFieldsById(fields, getFieldStateId(field)); + fieldsById[id] = this.getFieldsById(fields, this.getFieldStateId(field)); } }); each(fieldsById, (id, fields) => { - setFieldsStateCore(getFieldsById(stateFields, id), fields); + this.setFieldsStateCore(this.getFieldsById(stateFields, id), fields); }); return fields; } - function getFieldsByGroup(fields, groupingField) { + getFieldsByGroup(fields, groupingField) { return fields .filter((field) => field.groupName === groupingField.groupName && isNumeric(field.groupIndex) @@ -431,18 +510,18 @@ const PivotGridDataSource = Class.inherit((function () { })).sort((a, b) => a.groupIndex - b.groupIndex); } - function sortFieldsByAreaIndex(fields) { + sortFieldsByAreaIndex(fields) { fields .sort((field1, field2) => field1.areaIndex - field2.areaIndex || field1.groupIndex - field2.groupIndex); } - function isAreaField(field, area) { + isAreaField(field, area) { const canAddFieldInArea = area === 'data' || field.visible !== false; return field.area === area && !isDefined(field.groupIndex) && canAddFieldInArea; } - function getFieldId(field, retrieveFieldsOptionValue) { + getFieldId(field, retrieveFieldsOptionValue) { const groupName = field.groupName || ''; return (field.dataField || groupName) @@ -450,7 +529,7 @@ const PivotGridDataSource = Class.inherit((function () { + (retrieveFieldsOptionValue ? '' : groupName); } - function mergeFields(fields, storeFields, retrieveFieldsOptionValue) { + mergeFields(fields, storeFields, retrieveFieldsOptionValue) { let result: any = []; const fieldsDictionary: any = {}; const removedFields = {}; @@ -459,11 +538,11 @@ const PivotGridDataSource = Class.inherit((function () { if (storeFields) { each(storeFields, (_, field) => { - fieldsDictionary[getFieldId(field, retrieveFieldsOptionValue)] = field; + fieldsDictionary[this.getFieldId(field, retrieveFieldsOptionValue)] = field; }); each(fields, (_, field) => { - const fieldKey = getFieldId(field, retrieveFieldsOptionValue); + const fieldKey = this.getFieldId(field, retrieveFieldsOptionValue); const storeField = fieldsDictionary[fieldKey] || removedFields[fieldKey]; let mergedField; @@ -496,12 +575,12 @@ const PivotGridDataSource = Class.inherit((function () { result.push.apply(result, mergedGroups); - assignGroupIndexes(result); + this.assignGroupIndexes(result); return result; } - function assignGroupIndexes(fields) { + assignGroupIndexes(fields) { fields.forEach((field) => { if (field.groupName && field.groupInterval && field.groupIndex === undefined) { const maxGroupIndex = fields @@ -514,7 +593,7 @@ const PivotGridDataSource = Class.inherit((function () { }); } - function getFields(that) { + getFields(that) { // @ts-expect-error const result = new Deferred(); const store = that._store; @@ -523,14 +602,14 @@ const PivotGridDataSource = Class.inherit((function () { when(storeFields).done((storeFields) => { that._storeFields = storeFields; - mergedFields = mergeFields(that._fields, storeFields, that._retrieveFields); + mergedFields = this.mergeFields(that._fields, storeFields, that._retrieveFields); result.resolve(mergedFields); }).fail(result.reject); return result; } - function formatHeaderItems(data, loadOptions, headerName) { + formatHeaderItems(data, loadOptions, headerName) { return foreachTreeAsync(data[headerName], (items) => { const item = items[0]; @@ -539,14 +618,14 @@ const PivotGridDataSource = Class.inherit((function () { }); } - function formatHeaders(loadOptions, data) { + formatHeaders(loadOptions, data) { return when( - formatHeaderItems(data, loadOptions, 'columns'), - formatHeaderItems(data, loadOptions, 'rows'), + this.formatHeaderItems(data, loadOptions, 'columns'), + this.formatHeaderItems(data, loadOptions, 'rows'), ); } - function updateCache(headerItems) { + updateCache(headerItems) { // @ts-expect-error const d = new Deferred(); const cacheByPath: any = {}; @@ -562,720 +641,740 @@ const PivotGridDataSource = Class.inherit((function () { return d; } - function getAreaFields(fields, area) { - const areaFields: any = []; - each(fields, function () { - if (isAreaField(this, area)) { - areaFields.push(this); - } - }); - return areaFields; + getData() { + return this._data; } - return { - ctor(options) { - options = options || {}; - this._eventsStrategy = new EventsStrategy(this); + getAreaFields(area, collectGroups?) { + let areaFields: any[] = []; + let descriptions; - const that: any = this; - const store = createStore(options, (progress) => { - that._eventsStrategy.fireEvent('progressChanged', [progress]); + if (collectGroups || area === 'data') { + each(this._fields, (_, field) => { + if (this.isAreaField(field, area)) { + areaFields.push(field); + } }); + this.sortFieldsByAreaIndex(areaFields); + } else { + descriptions = this._descriptions || {}; + areaFields = descriptions[DESCRIPTION_NAME_BY_AREA[area]] || []; + } - that._store = store; - that._paginate = !!options.paginate; - that._pageSize = options.pageSize || 40; - that._data = { rows: [], columns: [], values: [] }; - that._loadingCount = 0; - - that._isFieldsModified = false; - - each( - [ - 'changed', - 'loadError', - 'loadingChanged', - 'progressChanged', - 'fieldsPrepared', - 'expandValueChanging', - ], - (_, eventName) => { - const optionName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`; - if (Object.prototype.hasOwnProperty.call(options, optionName)) { - this.on(eventName, options[optionName]); - } - }, - ); - - that._retrieveFields = isDefined(options.retrieveFields) ? options.retrieveFields : true; - - that._fields = options.fields || []; - that._descriptions = options.descriptions - ? extend(that._createDescriptions(), options.descriptions) - : undefined; - - if (!store) { - // TODO create dashboard store - extend(true, that._data, options.store || options); - } - }, - - getData() { - return this._data; - }, - - getAreaFields(area, collectGroups) { - let areaFields = []; - let descriptions; - - if (collectGroups || area === 'data') { - areaFields = getAreaFields(this._fields, area); - sortFieldsByAreaIndex(areaFields); - } else { - descriptions = this._descriptions || {}; - areaFields = descriptions[DESCRIPTION_NAME_BY_AREA[area]] || []; - } - - return areaFields; - }, + return areaFields; + } - getSummaryFields() { - return this.getAreaFields('data').filter((field) => isDefined(field.summaryType)); - }, + getSummaryFields() { + return this.getAreaFields('data').filter((field) => isDefined(field.summaryType)); + } - fields(fields) { - const that: any = this; - if (fields) { - that._fields = mergeFields(fields, that._storeFields, that._retrieveFields); - that._fieldsPrepared(that._fields); - } + fields(fields) { + const that: any = this; + if (fields) { + that._fields = this.mergeFields(fields, that._storeFields, that._retrieveFields); + that._fieldsPrepared(that._fields); + } - return that._fields; - }, + return that._fields; + } - field(id, options) { - const that: any = this; - const fields = that._fields; - const field = fields && fields[isNumeric(id) ? id : findField(fields, id)]; - let levels; + field(id, options?) { + const that: any = this; + const fields = that._fields; + const field = fields && fields[isNumeric(id) ? id : findField(fields, id)]; + let levels; - if (field && options) { - each(options, (optionName, optionValue) => { - const isInitialization = !STATE_PROPERTIES.includes(optionName as string); + if (field && options) { + each(options, (optionName, optionValue) => { + const isInitialization = !STATE_PROPERTIES.includes(optionName as string); - setFieldProperty(field, optionName, optionValue, isInitialization); + setFieldProperty(field, optionName, optionValue, isInitialization); - if (optionName === 'sortOrder') { - levels = field.levels || []; - for (let i = 0; i < levels.length; i += 1) { - levels[i][optionName] = optionValue; - } + if (optionName === 'sortOrder') { + levels = field.levels || []; + for (let i = 0; i < levels.length; i += 1) { + levels[i][optionName] = optionValue; } - }); - updateCalculatedFieldProperties(field, CALCULATED_PROPERTIES); + } + }); + this.updateCalculatedFieldProperties(field, CALCULATED_PROPERTIES); - that._descriptions = that._createDescriptions(field); - that._isFieldsModified = true; - that._eventsStrategy.fireEvent('fieldChanged', [field]); - } - return field; - }, - - getFieldValues(index, applyFilters, options) { - const that: any = this; - const field = this._fields && this._fields[index]; - const store = this.store(); - const loadFields: any = []; - const loadOptions: any = { - columns: loadFields, - rows: [], - values: this.getAreaFields('data'), - filters: applyFilters - ? this._fields.filter((f) => f !== field + that._descriptions = that._createDescriptions(field); + that._isFieldsModified = true; + that._eventsStrategy.fireEvent('fieldChanged', [field]); + } + return field; + } + + getFieldValues(index, applyFilters, options) { + const that: any = this; + const field = this._fields && this._fields[index]; + const store = this._store; + const loadFields: any = []; + const loadOptions: any = { + columns: loadFields, + rows: [], + values: this.getAreaFields('data'), + filters: applyFilters + ? this._fields.filter((f) => f !== field && f.area && f.filterValues && f.filterValues.length) - : [], - skipValues: true, - }; - let searchValue; - // @ts-expect-error - const d = new Deferred(); - - if (options) { - searchValue = options.searchValue; - loadOptions.columnSkip = options.skip; - loadOptions.columnTake = options.take; - } + : [], + skipValues: true, + }; + let searchValue; + // @ts-expect-error + const d = new Deferred(); - if (field && store) { - each(field.levels || [field], function () { - loadFields.push(extend({}, this, { - expanded: true, filterValues: null, sortOrder: 'asc', sortBySummaryField: null, searchValue, - })); - }); + if (options) { + searchValue = options.searchValue; + loadOptions.columnSkip = options.skip; + loadOptions.columnTake = options.take; + } - store.load(loadOptions).done((data) => { - if (loadOptions.columnSkip) { - data.columns = data.columns.slice(loadOptions.columnSkip); - } - if (loadOptions.columnTake) { - data.columns = data.columns.slice(0, loadOptions.columnTake); - } - formatHeaders(loadOptions, data); - if (!loadOptions.columnTake) { - that._sort(loadOptions, data); - } - d.resolve(data.columns); - }).fail(d); - } else { - d.reject(); - } - return d; - }, + if (field && store) { + each(field.levels || [field], function () { + loadFields.push(extend({}, this, { + expanded: true, filterValues: null, sortOrder: 'asc', sortBySummaryField: null, searchValue, + })); + }); - reload() { - return this.load({ reload: true }); - }, + store.load(loadOptions).done((data) => { + if (loadOptions.columnSkip) { + data.columns = data.columns.slice(loadOptions.columnSkip); + } + if (loadOptions.columnTake) { + data.columns = data.columns.slice(0, loadOptions.columnTake); + } + this.formatHeaders(loadOptions, data); + if (!loadOptions.columnTake) { + that._sort(loadOptions, data); + } + d.resolve(data.columns); + }).fail(d); + } else { + d.reject(); + } + return d; + } - filter() { - const store = this._store; + reload() { + return this.load({ reload: true }); + } - return store.filter.apply(store, arguments); - }, + filter() { + const store = this._store; - // eslint-disable-next-line object-shorthand - load: function (options) { - const that: any = this; - // @ts-expect-error - const d = new Deferred(); - options = options || {}; + return store.filter.apply(store, arguments); + } - that.beginLoading(); + load(options?) { + const that: any = this; + // @ts-expect-error + const d = new Deferred(); + options = options || {}; - d.fail((e) => { - that._eventsStrategy.fireEvent('loadError', [e]); - }).always(() => { - that.endLoading(); - }); + that.beginLoading(); - function loadTask() { - that._delayedLoadTask = undefined; - if (!that._descriptions) { - when(getFields(that)).done((fields) => { - that._fieldsPrepared(fields); - that._loadCore(options, d); - }).fail(d.reject).fail(that._loadErrorHandler); - } else { + d.fail((e) => { + that._eventsStrategy.fireEvent('loadError', [e]); + }).always(() => { + that.endLoading(); + }); + + const loadTask = () => { + that._delayedLoadTask = undefined; + if (!that._descriptions) { + when(this.getFields(that)).done((fields) => { + that._fieldsPrepared(fields); that._loadCore(options, d); - } - } - if (that.store()) { - that._delayedLoadTask = commonUtils.executeAsync(loadTask); + }).fail(d.reject).fail(that._loadErrorHandler); } else { - loadTask(); + that._loadCore(options, d); } + }; - return d; - }, + if (that.store()) { + that._delayedLoadTask = commonUtils.executeAsync(loadTask); + } else { + loadTask(); + } - createDrillDownDataSource(params) { - return this._store.createDrillDownDataSource( - this._descriptions, - params, - ); - }, - - _createDescriptions(currentField) { - const that: any = this; - const fields = that.fields(); - const descriptions: any = { - rows: [], - columns: [], - values: [], - filters: [], - }; + return d; + } - each(['row', 'column', 'data', 'filter'], (_, areaName) => { - normalizeIndexes(getAreaFields(fields, areaName), 'areaIndex', currentField); - }); + createDrillDownDataSource(params) { + return this._store.createDrillDownDataSource( + this._descriptions, + params, + ); + } - each(fields || [], (_, field) => { - const descriptionName = DESCRIPTION_NAME_BY_AREA[field.area]; - const dimension = descriptions[descriptionName]; - const { groupName } = field; + _createDescriptions(currentField) { + const that: any = this; + const fields = that.fields(); + const descriptions: any = { + rows: [], + columns: [], + values: [], + filters: [], + }; + + each(['row', 'column', 'data', 'filter'], (_, areaName) => { + normalizeIndexes(this.getAreaFields(areaName, true), 'areaIndex', currentField); + }); - if (groupName && !isNumeric(field.groupIndex)) { - field.levels = getFieldsByGroup(fields, field); - } + each(fields || [], (_, field) => { + const descriptionName = DESCRIPTION_NAME_BY_AREA[field.area]; + const dimension = descriptions[descriptionName]; + const { groupName } = field; - if (!dimension || groupName && isNumeric(field.groupIndex) || (field.visible === false && (field.area !== 'data' && field.area !== 'filter'))) { - return; - } + if (groupName && !isNumeric(field.groupIndex)) { + field.levels = this.getFieldsByGroup(fields, field); + } + + if (!dimension || groupName && isNumeric(field.groupIndex) || (field.visible === false && (field.area !== 'data' && field.area !== 'filter'))) { + return; + } - if (field.levels + if (field.levels && dimension !== descriptions.filters && dimension !== descriptions.values) { - dimension.push.apply(dimension, field.levels); - if (field.filterValues && field.filterValues.length) { - descriptions.filters.push(field); - } - } else { - dimension.push(field); + dimension.push.apply(dimension, field.levels); + if (field.filterValues && field.filterValues.length) { + descriptions.filters.push(field); } - }); + } else { + dimension.push(field); + } + }); - each(descriptions, (_, fields) => { - sortFieldsByAreaIndex(fields); - }); + each(descriptions, (_, fields) => { + this.sortFieldsByAreaIndex(fields); + }); - const indices = {}; - each(descriptions.values, (_, field) => { - const expression = field.calculateSummaryValue; - if (isFunction(expression)) { - const summaryCell = summaryUtils.createMockSummaryCell(descriptions, fields, indices); - expression(summaryCell); - } - }); + const indices = {}; + each(descriptions.values, (_, field) => { + const expression = field.calculateSummaryValue; + if (isFunction(expression)) { + const summaryCell = summaryUtils.createMockSummaryCell(descriptions, fields, indices); + expression(summaryCell); + } + }); - return descriptions; - }, + return descriptions; + } - _fieldsPrepared(fields) { - const that: any = this; - that._fields = fields; - each(fields, (index, field) => { - field.index = index; - updateCalculatedFieldProperties(field, ALL_CALCULATED_PROPERTIES); - }); + _fieldsPrepared(fields) { + const that: any = this; + that._fields = fields; + each(fields, (index, field) => { + field.index = index; + this.updateCalculatedFieldProperties(field, ALL_CALCULATED_PROPERTIES); + }); - const currentFieldState = getFieldsState(fields, ['caption']); + const currentFieldState = this.getFieldsState(fields, ['caption']); - that._eventsStrategy.fireEvent('fieldsPrepared', [fields]); + that._eventsStrategy.fireEvent('fieldsPrepared', [fields]); - for (let i = 0; i < fields.length; i += 1) { - if (fields[i].caption !== currentFieldState[i].caption) { - setFieldProperty(fields[i], 'caption', fields[i].caption, true); - } + for (let i = 0; i < fields.length; i += 1) { + if (fields[i].caption !== currentFieldState[i].caption) { + setFieldProperty(fields[i], 'caption', fields[i].caption, true); } + } - that._descriptions = that._createDescriptions(); - }, - isLoading() { - return this._loadingCount > 0; - }, - - state(state, skipLoading) { - const that: any = this; - - if (arguments.length) { - state = extend({ - rowExpandedPaths: [], - columnExpandedPaths: [], - }, state); - - if (!that._descriptions) { - that.beginLoading(); - when(getFields(that)).done((fields) => { - that._fields = setFieldsState(state.fields, fields); - that._fieldsPrepared(fields); - !skipLoading && that.load(state); - }).always(() => { - that.endLoading(); - }); - } else { - that._fields = setFieldsState(state.fields, that._fields); - that._descriptions = that._createDescriptions(); - !skipLoading && that.load(state); - } + that._descriptions = that._createDescriptions(); + } + + isLoading() { + return this._loadingCount > 0; + } + + state(state, skipLoading) { + const that: any = this; + + if (arguments.length) { + state = extend({ + rowExpandedPaths: [], + columnExpandedPaths: [], + }, state); - return undefined; + if (!that._descriptions) { + that.beginLoading(); + when(this.getFields(that)).done((fields) => { + that._fields = this.setFieldsState(state.fields, fields); + that._fieldsPrepared(fields); + !skipLoading && that.load(state); + }).always(() => { + that.endLoading(); + }); + } else { + that._fields = this.setFieldsState(state.fields, that._fields); + that._descriptions = that._createDescriptions(); + !skipLoading && that.load(state); } - return { - fields: getFieldsState(that._fields, STATE_PROPERTIES), - columnExpandedPaths: getExpandedPaths(that._data, that._descriptions, 'columns', that._lastLoadOptions), - rowExpandedPaths: getExpandedPaths(that._data, that._descriptions, 'rows', that._lastLoadOptions), - }; - }, - beginLoading() { - this._changeLoadingCount(1); - }, + return undefined; + } + return { + fields: this.getFieldsState(that._fields, STATE_PROPERTIES), + columnExpandedPaths: this.getExpandedPaths(that._data, that._descriptions, 'columns', that._lastLoadOptions), + rowExpandedPaths: this.getExpandedPaths(that._data, that._descriptions, 'rows', that._lastLoadOptions), + }; + } - endLoading() { - this._changeLoadingCount(-1); - }, + beginLoading() { + this._changeLoadingCount(1); + } - _changeLoadingCount(increment) { - const oldLoading = this.isLoading(); + endLoading() { + this._changeLoadingCount(-1); + } - this._loadingCount += increment; - const newLoading = this.isLoading(); + _changeLoadingCount(increment) { + const oldLoading = this.isLoading(); - // - @ts-expect-error - if (oldLoading ^ newLoading) { - this._eventsStrategy.fireEvent('loadingChanged', [newLoading]); - } - }, + this._loadingCount += increment; + const newLoading = this.isLoading(); - _hasPagingValues(options, area, oppositeIndex) { - const takeField = `${area}Take`; - const skipField = `${area}Skip`; - const { values } = this._data; - let items = this._data[`${area}s`]; - const oppositeArea = area === 'row' ? 'column' : 'row'; - const indices: any = []; + if (oldLoading !== newLoading) { + this._eventsStrategy.fireEvent('loadingChanged', [newLoading]); + } + } - if (options.path && options.area === area) { - const headerItem = findHeaderItem(items, options.path); - items = headerItem && headerItem.children; - if (!items) { - return false; - } + _hasPagingValues(options, area, oppositeIndex) { + const takeField = `${area}Take`; + const skipField = `${area}Skip`; + const { values } = this._data; + let items = this._data[`${area}s`]; + const oppositeArea = area === 'row' ? 'column' : 'row'; + const indices: any = []; + + if (options.path && options.area === area) { + const headerItem = this.findHeaderItem(items, options.path); + items = headerItem && headerItem.children; + if (!items) { + return false; } - if (options.oppositePath && options.area === oppositeArea) { - const headerItem = findHeaderItem(items, options.oppositePath); - items = headerItem && headerItem.children; - if (!items) { - return false; - } + } + if (options.oppositePath && options.area === oppositeArea) { + const headerItem = this.findHeaderItem(items, options.oppositePath); + items = headerItem && headerItem.children; + if (!items) { + return false; } + } - for (let i = options[skipField]; i < options[skipField] + options[takeField]; i += 1) { - if (items[i]) { - indices.push(items[i].index); - } + for (let i = options[skipField]; i < options[skipField] + options[takeField]; i += 1) { + if (items[i]) { + indices.push(items[i].index); } + } - return indices.every((index) => { - if (index !== undefined) { - if (area === 'row') { - return (values[index] || [])[oppositeIndex]; - } - return (values[oppositeIndex] || [])[index]; + return indices.every((index) => { + if (index !== undefined) { + if (area === 'row') { + return (values[index] || [])[oppositeIndex]; } + return (values[oppositeIndex] || [])[index]; + } - return undefined; - }); - }, + return undefined; + }); + } - _processPagingCacheByArea(options, pageSize, area) { - const takeField = `${area}Take`; - const skipField = `${area}Skip`; - let items = this._data[`${area}s`]; - const oppositeArea = area === 'row' ? 'column' : 'row'; - let item; + _processPagingCacheByArea(options, pageSize, area) { + const takeField = `${area}Take`; + const skipField = `${area}Skip`; + let items = this._data[`${area}s`]; + const oppositeArea = area === 'row' ? 'column' : 'row'; + let item; - if (options[takeField]) { - if (options.path && options.area === area) { - const headerItem = findHeaderItem(items, options.path); - items = headerItem && headerItem.children || []; - } - if (options.oppositePath && options.area === oppositeArea) { - const headerItem = findHeaderItem(items, options.oppositePath); - items = headerItem && headerItem.children || []; - } + if (options[takeField]) { + if (options.path && options.area === area) { + const headerItem = this.findHeaderItem(items, options.path); + items = headerItem && headerItem.children || []; + } + if (options.oppositePath && options.area === oppositeArea) { + const headerItem = this.findHeaderItem(items, options.oppositePath); + items = headerItem && headerItem.children || []; + } - do { - item = items[options[skipField]]; - if (item && item.index !== undefined) { - if (this._hasPagingValues(options, oppositeArea, item.index)) { - // eslint-disable-next-line no-plusplus - options[skipField]++; - // eslint-disable-next-line no-plusplus - options[takeField]--; - } else { - break; - } + do { + item = items[options[skipField]]; + if (item && item.index !== undefined) { + if (this._hasPagingValues(options, oppositeArea, item.index)) { + // eslint-disable-next-line no-plusplus + options[skipField]++; + // eslint-disable-next-line no-plusplus + options[takeField]--; + } else { + break; } - } while (item && item.index !== undefined && options[takeField]); + } + } while (item && item.index !== undefined && options[takeField]); - if (options[takeField]) { - const start = Math.floor(options[skipField] / pageSize) * pageSize; - const end = Math.ceil((options[skipField] + options[takeField]) / pageSize) * pageSize; + if (options[takeField]) { + const start = Math.floor(options[skipField] / pageSize) * pageSize; + const end = Math.ceil((options[skipField] + options[takeField]) / pageSize) * pageSize; - options[skipField] = start; - options[takeField] = end - start; - } + options[skipField] = start; + options[takeField] = end - start; } - }, + } + } + + _processPagingCache(storeLoadOptions) { + const pageSize = this._pageSize; - _processPagingCache(storeLoadOptions) { - const pageSize = this._pageSize; + if (pageSize < 0) return; - if (pageSize < 0) return; + for (let i = 0; i < storeLoadOptions.length; i += 1) { + this._processPagingCacheByArea(storeLoadOptions[i], pageSize, 'row'); + this._processPagingCacheByArea(storeLoadOptions[i], pageSize, 'column'); + } + } - for (let i = 0; i < storeLoadOptions.length; i += 1) { - this._processPagingCacheByArea(storeLoadOptions[i], pageSize, 'row'); - this._processPagingCacheByArea(storeLoadOptions[i], pageSize, 'column'); + _loadCore(options, deferred) { + const that: any = this; + const store = this._store; + const descriptions = this._descriptions; + const reload = options.reload || (this.paginate() && that._isFieldsModified); + const paginate = this.paginate(); + const headerName = DESCRIPTION_NAME_BY_AREA[options.area]; + + options = options || {}; + + if (store) { + extend(options, descriptions); + options.columnExpandedPaths = options.columnExpandedPaths + || this.getExpandedPaths(this._data, options, 'columns', that._lastLoadOptions); + options.rowExpandedPaths = options.rowExpandedPaths + || this.getExpandedPaths(this._data, options, 'rows', that._lastLoadOptions); + + if (paginate) { + options.pageSize = this._pageSize; } - }, - - _loadCore(options, deferred) { - const that: any = this; - const store = this._store; - const descriptions = this._descriptions; - const reload = options.reload || (this.paginate() && that._isFieldsModified); - const paginate = this.paginate(); - const headerName = DESCRIPTION_NAME_BY_AREA[options.area]; - - options = options || {}; - - if (store) { - extend(options, descriptions); - options.columnExpandedPaths = options.columnExpandedPaths - || getExpandedPaths(this._data, options, 'columns', that._lastLoadOptions); - options.rowExpandedPaths = options.rowExpandedPaths - || getExpandedPaths(this._data, options, 'rows', that._lastLoadOptions); - - if (paginate) { - options.pageSize = this._pageSize; - } - if (headerName) { - options.headerName = headerName; - } + if (headerName) { + options.headerName = headerName; + } - that.beginLoading(); - deferred.always(() => { - that.endLoading(); - }); + that.beginLoading(); + deferred.always(() => { + that.endLoading(); + }); - let storeLoadOptions = [options]; + let storeLoadOptions = [options]; - that._eventsStrategy.fireEvent('customizeStoreLoadOptions', [storeLoadOptions, reload]); + that._eventsStrategy.fireEvent('customizeStoreLoadOptions', [storeLoadOptions, reload]); - if (!reload) { - that._processPagingCache(storeLoadOptions); - } + if (!reload) { + that._processPagingCache(storeLoadOptions); + } - storeLoadOptions = storeLoadOptions - .filter((options) => !(options.rows.length && options.rowTake === 0) + storeLoadOptions = storeLoadOptions + .filter((options) => !(options.rows.length && options.rowTake === 0) && !(options.columns.length && options.columnTake === 0)); - if (!storeLoadOptions.length) { - that._update(deferred); - return; - } - - const results = storeLoadOptions.map((options) => store.load(options)); - when.apply(null, results).done(function () { - const results = arguments; - for (let i = 0; i < results.length; i += 1) { - const options = storeLoadOptions[i]; - const data = results[i]; - const isLast = i === results.length - 1; - - if (options.path) { - that.applyPartialDataSource( - options.area, - options.path, - data, - isLast - ? deferred - : false, - options.oppositePath, - ); - } else if (paginate && !reload && isDataExists(that._data)) { - that.mergePartialDataSource(data, isLast ? deferred : false); - } else { - extend(that._data, data); - that._lastLoadOptions = options; - that._update(isLast ? deferred : false); - } - } - }).fail(deferred.reject); - } else { + if (!storeLoadOptions.length) { that._update(deferred); + return; } - }, - _sort(descriptions, data, getAscOrder?: boolean) { - const store = this._store; + const results = storeLoadOptions.map((options) => store.load(options)); + when.apply(null, results).done(function () { + const results = arguments; + for (let i = 0; i < results.length; i += 1) { + const options = storeLoadOptions[i]; + const data = results[i]; + const isLast = i === results.length - 1; + + if (options.path) { + that.applyPartialDataSource( + options.area, + options.path, + data, + isLast + ? deferred + : false, + options.oppositePath, + ); + } else if (paginate && !reload && isDataExists(that._data)) { + that.mergePartialDataSource(data, isLast ? deferred : false); + } else { + extend(that._data, data); + that._lastLoadOptions = options; + that._update(isLast ? deferred : false); + } + } + }).fail(deferred.reject); + } else { + that._update(deferred); + } + } + + _sort(descriptions, data, getAscOrder?: boolean) { + const store = this._store; - if (store && !this._paginate) { - sort(descriptions, data, getAscOrder); - } - }, + if (store && !this._paginate) { + sort(descriptions, data, getAscOrder); + } + } - sortLocal(): void { - this._sort(this._descriptions, this._data); - this._eventsStrategy.fireEvent('changed'); - }, + sortLocal(): void { + this._sort(this._descriptions, this._data); + this._eventsStrategy.fireEvent('changed', undefined); + } - paginate() { - return this._paginate + paginate() { + return this._paginate && this._store && this._store.supportPaging(); - }, - - isEmpty() { - const dataFields = this.getAreaFields('data').filter((f) => f.visible !== false); - const data = this.getData(); - return !dataFields.length || !data.values.length; - }, - - _update(deferred) { - const that: any = this; - const descriptions = that._descriptions; - const loadedData = that._data; - const dataFields = descriptions.values; - const expressionsUsed = areExpressionsUsed(dataFields); - - when( - formatHeaders(descriptions, loadedData), - updateCache(loadedData.rows), - updateCache(loadedData.columns), - ).done(() => { - if (expressionsUsed) { - that._sort(descriptions, loadedData, expressionsUsed); - !that.isEmpty() && summaryUtils.applyDisplaySummaryMode(descriptions, loadedData); - } + } + + isEmpty() { + const dataFields = this.getAreaFields('data').filter((f) => f.visible !== false); + const data = this.getData(); + return !dataFields.length || !data.values.length; + } + + _update(deferred?) { + const that: any = this; + const descriptions = that._descriptions; + const loadedData = that._data; + const dataFields = descriptions.values; + const expressionsUsed = areExpressionsUsed(dataFields); + + when( + this.formatHeaders(descriptions, loadedData), + this.updateCache(loadedData.rows), + this.updateCache(loadedData.columns), + ).done(() => { + if (expressionsUsed) { + that._sort(descriptions, loadedData, expressionsUsed); + !that.isEmpty() && summaryUtils.applyDisplaySummaryMode(descriptions, loadedData); + } - that._sort(descriptions, loadedData); + that._sort(descriptions, loadedData); - !that.isEmpty() + !that.isEmpty() && isRunningTotalUsed(dataFields) && summaryUtils.applyRunningTotal(descriptions, loadedData); - that._data = loadedData; - deferred !== false && when(deferred).done(() => { - that._isFieldsModified = false; - that._eventsStrategy.fireEvent('changed'); - if (isDefined(that._data.grandTotalRowIndex)) { - loadedData.grandTotalRowIndex = that._data.grandTotalRowIndex; - } - if (isDefined(that._data.grandTotalColumnIndex)) { - loadedData.grandTotalColumnIndex = that._data.grandTotalColumnIndex; - } - }); - deferred && deferred.resolve(that._data); - }); - return deferred; - }, - - store() { - return this._store; - }, - - collapseHeaderItem(area, path) { - const that: any = this; - const headerItems = area === 'column' ? that._data.columns : that._data.rows; - const headerItem = findHeaderItem(headerItems, path); - const field = that.getAreaFields(area)[path.length - 1]; - - if (headerItem && headerItem.children) { - that._eventsStrategy.fireEvent('expandValueChanging', [{ - area, - path, - expanded: false, - }]); - if (field) { - field.expanded = false; + that._data = loadedData; + deferred !== false && when(deferred).done(() => { + that._isFieldsModified = false; + that._eventsStrategy.fireEvent('changed'); + if (isDefined(that._data.grandTotalRowIndex)) { + loadedData.grandTotalRowIndex = that._data.grandTotalRowIndex; } - headerItem.collapsedChildren = headerItem.children; - delete headerItem.children; - that._update(); - if (that.paginate()) { - that.load(); + if (isDefined(that._data.grandTotalColumnIndex)) { + loadedData.grandTotalColumnIndex = that._data.grandTotalColumnIndex; } - return true; + }); + deferred && deferred.resolve(that._data); + }); + return deferred; + } + + store() { + return this._store; + } + + collapseHeaderItem(area, path) { + const that: any = this; + const headerItems = area === 'column' ? that._data.columns : that._data.rows; + const headerItem = this.findHeaderItem(headerItems, path); + const field = that.getAreaFields(area)[path.length - 1]; + + if (headerItem && headerItem.children) { + that._eventsStrategy.fireEvent('expandValueChanging', [{ + area, + path, + expanded: false, + }]); + if (field) { + field.expanded = false; + } + headerItem.collapsedChildren = headerItem.children; + delete headerItem.children; + that._update(); + if (that.paginate()) { + that.load(); } - return false; - }, + return true; + } + return false; + } - collapseAll(id) { - let dataChanged = false; - const field = this.field(id) || {}; - let areaOffsets = [this.getAreaFields(field.area).indexOf(field)]; + collapseAll(id) { + let dataChanged = false; + const field = this.field(id) || {}; + let areaOffsets = [this.getAreaFields(field.area).indexOf(field)]; + + field.expanded = false; + if (field && field.levels) { + areaOffsets = []; + field.levels.forEach((f) => { + areaOffsets.push(this.getAreaFields(field.area).indexOf(f)); + f.expanded = false; + }); + } + + foreachTree(this._data[`${field.area}s`], (items) => { + const item = items[0]; + const path = createPath(items); + + if (item && item.children && areaOffsets.includes(path.length - 1)) { + item.collapsedChildren = item.children; + delete item.children; + dataChanged = true; + } + }, true); + + dataChanged && this._update(); + } - field.expanded = false; + expandAll(id) { + const field = this.field(id); + if (field && field.area) { + field.expanded = true; if (field && field.levels) { - areaOffsets = []; field.levels.forEach((f) => { - areaOffsets.push(this.getAreaFields(field.area).indexOf(f)); - f.expanded = false; + f.expanded = true; }); } + this.load(); + } + } - foreachTree(this._data[`${field.area}s`], (items) => { - const item = items[0]; - const path = createPath(items); - - if (item && item.children && areaOffsets.includes(path.length - 1)) { - item.collapsedChildren = item.children; - delete item.children; - dataChanged = true; - } - }, true); - - dataChanged && this._update(); - }, - - expandAll(id) { - const field = this.field(id); - if (field && field.area) { - field.expanded = true; - if (field && field.levels) { - field.levels.forEach((f) => { - f.expanded = true; - }); - } - this.load(); - } - }, - - expandHeaderItem(area, path) { - const that: any = this; - const headerItems = area === 'column' ? that._data.columns : that._data.rows; - const headerItem = findHeaderItem(headerItems, path); - - if (headerItem && !headerItem.children) { - const hasCache = !!headerItem.collapsedChildren; - const options = { - area, - path, - expanded: true, - needExpandData: !hasCache, - }; - that._eventsStrategy.fireEvent('expandValueChanging', [options]); - if (hasCache) { - headerItem.children = headerItem.collapsedChildren; - delete headerItem.collapsedChildren; - that._update(); - } else if (this.store()) { - that.load(options); - } - return hasCache; + expandHeaderItem(area, path) { + const that: any = this; + const headerItems = area === 'column' ? that._data.columns : that._data.rows; + const headerItem = this.findHeaderItem(headerItems, path); + + if (headerItem && !headerItem.children) { + const hasCache = !!headerItem.collapsedChildren; + const options = { + area, + path, + expanded: true, + needExpandData: !hasCache, + }; + that._eventsStrategy.fireEvent('expandValueChanging', [options]); + if (hasCache) { + headerItem.children = headerItem.collapsedChildren; + delete headerItem.collapsedChildren; + that._update(); + } else if (this.store()) { + that.load(options); } - return false; - }, - - mergePartialDataSource(dataSource, deferred) { - const that: any = this; - const loadedData = that._data; - let newRowItemIndexesToCurrent; - let newColumnItemIndexesToCurrent; - - if (dataSource && dataSource.values) { - dataSource.rows = dataSource.rows || []; - dataSource.columns = dataSource.columns || []; - - newRowItemIndexesToCurrent = updateHeaderItems( - loadedData.rows, - dataSource.rows, - loadedData.grandTotalColumnIndex, - ); - newColumnItemIndexesToCurrent = updateHeaderItems( - loadedData.columns, - dataSource.columns, - loadedData.grandTotalColumnIndex, - ); + return hasCache; + } + return false; + } + + mergePartialDataSource(dataSource, deferred) { + const that: any = this; + const loadedData = that._data; + let newRowItemIndexesToCurrent; + let newColumnItemIndexesToCurrent; + + if (dataSource && dataSource.values) { + dataSource.rows = dataSource.rows || []; + dataSource.columns = dataSource.columns || []; + + newRowItemIndexesToCurrent = this.updateHeaderItems( + loadedData.rows, + dataSource.rows, + loadedData.grandTotalColumnIndex, + ); + newColumnItemIndexesToCurrent = this.updateHeaderItems( + loadedData.columns, + dataSource.columns, + loadedData.grandTotalColumnIndex, + ); + + when(newRowItemIndexesToCurrent, newColumnItemIndexesToCurrent) + .done((newRowItemIndexesToCurrent, newColumnItemIndexesToCurrent) => { + if (newRowItemIndexesToCurrent.length || newColumnItemIndexesToCurrent.length) { + this.updateDataSourceCells( + loadedData, + dataSource.values, + newRowItemIndexesToCurrent, + newColumnItemIndexesToCurrent, + ); + } + that._update(deferred); + }); + } + } + applyPartialDataSource(area, path, dataSource, deferred, oppositePath) { + const that: any = this; + const loadedData = that._data; + const headerItems = area === 'column' ? loadedData.columns : loadedData.rows; + let headerItem; + const oppositeHeaderItems = area === 'column' ? loadedData.rows : loadedData.columns; + let oppositeHeaderItem; + let newRowItemIndexesToCurrent; + let newColumnItemIndexesToCurrent; + + if (dataSource && dataSource.values) { + dataSource.rows = dataSource.rows || []; + dataSource.columns = dataSource.columns || []; + headerItem = this.findHeaderItem(headerItems, path); + oppositeHeaderItem = oppositePath && this.findHeaderItem(oppositeHeaderItems, oppositePath); + if (headerItem) { + if (area === 'column') { + newColumnItemIndexesToCurrent = this.updateHeaderItemChildren( + headerItems, + headerItem, + dataSource.columns, + loadedData.grandTotalColumnIndex, + ); + if (oppositeHeaderItem) { + newRowItemIndexesToCurrent = this.updateHeaderItemChildren( + oppositeHeaderItems, + oppositeHeaderItem, + dataSource.rows, + loadedData.grandTotalRowIndex, + ); + } else { + newRowItemIndexesToCurrent = this.updateHeaderItems( + loadedData.rows, + dataSource.rows, + loadedData.grandTotalRowIndex, + ); + } + } else { + newRowItemIndexesToCurrent = this.updateHeaderItemChildren( + headerItems, + headerItem, + dataSource.rows, + loadedData.grandTotalRowIndex, + ); + if (oppositeHeaderItem) { + newColumnItemIndexesToCurrent = this.updateHeaderItemChildren( + oppositeHeaderItems, + oppositeHeaderItem, + dataSource.columns, + loadedData.grandTotalColumnIndex, + ); + } else { + newColumnItemIndexesToCurrent = this.updateHeaderItems( + loadedData.columns, + dataSource.columns, + loadedData.grandTotalColumnIndex, + ); + } + } when(newRowItemIndexesToCurrent, newColumnItemIndexesToCurrent) .done((newRowItemIndexesToCurrent, newColumnItemIndexesToCurrent) => { - if (newRowItemIndexesToCurrent.length || newColumnItemIndexesToCurrent.length) { - updateDataSourceCells( + if (area === 'row' && newRowItemIndexesToCurrent.length || area === 'column' && newColumnItemIndexesToCurrent.length) { + this.updateDataSourceCells( loadedData, dataSource.values, newRowItemIndexesToCurrent, @@ -1285,108 +1384,34 @@ const PivotGridDataSource = Class.inherit((function () { that._update(deferred); }); } - }, - - applyPartialDataSource(area, path, dataSource, deferred, oppositePath) { - const that: any = this; - const loadedData = that._data; - const headerItems = area === 'column' ? loadedData.columns : loadedData.rows; - let headerItem; - const oppositeHeaderItems = area === 'column' ? loadedData.rows : loadedData.columns; - let oppositeHeaderItem; - let newRowItemIndexesToCurrent; - let newColumnItemIndexesToCurrent; - - if (dataSource && dataSource.values) { - dataSource.rows = dataSource.rows || []; - dataSource.columns = dataSource.columns || []; - headerItem = findHeaderItem(headerItems, path); - oppositeHeaderItem = oppositePath && findHeaderItem(oppositeHeaderItems, oppositePath); - if (headerItem) { - if (area === 'column') { - newColumnItemIndexesToCurrent = updateHeaderItemChildren( - headerItems, - headerItem, - dataSource.columns, - loadedData.grandTotalColumnIndex, - ); - if (oppositeHeaderItem) { - newRowItemIndexesToCurrent = updateHeaderItemChildren( - oppositeHeaderItems, - oppositeHeaderItem, - dataSource.rows, - loadedData.grandTotalRowIndex, - ); - } else { - newRowItemIndexesToCurrent = updateHeaderItems( - loadedData.rows, - dataSource.rows, - loadedData.grandTotalRowIndex, - ); - } - } else { - newRowItemIndexesToCurrent = updateHeaderItemChildren( - headerItems, - headerItem, - dataSource.rows, - loadedData.grandTotalRowIndex, - ); - if (oppositeHeaderItem) { - newColumnItemIndexesToCurrent = updateHeaderItemChildren( - oppositeHeaderItems, - oppositeHeaderItem, - dataSource.columns, - loadedData.grandTotalColumnIndex, - ); - } else { - newColumnItemIndexesToCurrent = updateHeaderItems( - loadedData.columns, - dataSource.columns, - loadedData.grandTotalColumnIndex, - ); - } - } - when(newRowItemIndexesToCurrent, newColumnItemIndexesToCurrent) - .done((newRowItemIndexesToCurrent, newColumnItemIndexesToCurrent) => { - if (area === 'row' && newRowItemIndexesToCurrent.length || area === 'column' && newColumnItemIndexesToCurrent.length) { - updateDataSourceCells( - loadedData, - dataSource.values, - newRowItemIndexesToCurrent, - newColumnItemIndexesToCurrent, - ); - } - that._update(deferred); - }); - } - } - }, + } + } - on(eventName, eventHandler) { - this._eventsStrategy.on(eventName, eventHandler); - return this; - }, + on(eventName, eventHandler) { + this._eventsStrategy.on(eventName, eventHandler); + return this; + } - off(eventName, eventHandler) { - this._eventsStrategy.off(eventName, eventHandler); - return this; - }, + off(eventName, eventHandler) { + this._eventsStrategy.off(eventName, eventHandler); + return this; + } - dispose() { - const that: any = this; - const delayedLoadTask = that._delayedLoadTask; + dispose() { + const that: any = this; + const delayedLoadTask = that._delayedLoadTask; - this._eventsStrategy.dispose(); - if (delayedLoadTask) { - delayedLoadTask.abort(); - } - this._isDisposed = true; - }, - isDisposed() { - return !!this._isDisposed; - }, - }; -})()); + this._eventsStrategy.dispose(); + if (delayedLoadTask) { + delayedLoadTask.abort(); + } + this._isDisposed = true; + } + + isDisposed() { + return !!this._isDisposed; + } +} export default { PivotGridDataSource }; export { PivotGridDataSource }; diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts index 890a6316d9a1..0a72d6c76b9f 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -21,7 +21,7 @@ import Button from '@js/ui/button'; import ContextMenu from '@js/ui/context_menu'; import Popup from '@js/ui/popup/ui.popup'; import { current, isFluent } from '@js/ui/themes'; -import Widget from '@js/ui/widget/ui.widget'; +import Widget from '@ts/core/widget/widget'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; import { ChartIntegrationMixin } from './chart_integration/m_chart_integration'; @@ -106,9 +106,61 @@ function clickedOnFieldsArea($targetElement) { return $targetElement.closest(`.${FIELDS_CLASS}`).length || $targetElement.find(`.${FIELDS_CLASS}`).length; } -const PivotGrid = (Widget as any).inherit({ +class PivotGrid extends Widget { + _dataController: any; + + _scrollLeft: any; + + _scrollTop: any; + + _actions: any; + + _hideLoadingTimeoutID: any; + + _hasHeight: any; + + _scrollUpdating: any; + + _pivotGridContainer: any; + + _contextMenu: any; + + _loadPanel: any; + + _startLoadingTime: any; + + _dataArea: any; + + _fieldChooserPopup: any; + + _fieldChooser: any; + + _columnsArea: any; + + _rowsArea: any; + + _rowFields: any; + + _columnFields: any; + + _filterFields: any; + + _dataFields: any; + + _testResultWidths: any; + + _testResultHeights: any; + + showFilterFields: any; + + __scrollBarUseNative: any; + + __scrollBarWidth: any; + + _createActionByOption: any; + _getDefaultOptions() { - return extend(this.callBase(), { + return extend(super._getDefaultOptions(), { scrolling: { timeout: 300, @@ -228,7 +280,7 @@ const PivotGrid = (Widget as any).inherit({ }, }, }); - }, + } _updateCalculatedOptions(fields) { const that = this; @@ -243,7 +295,7 @@ const PivotGrid = (Widget as any).inherit({ } }); }); - }, + } _getDataControllerOptions() { const that = this; @@ -264,7 +316,7 @@ const PivotGrid = (Widget as any).inherit({ that._updateCalculatedOptions(fields); }, }; - }, + } _initDataController() { const that = this; @@ -285,35 +337,37 @@ const PivotGrid = (Widget as any).inherit({ }); that._dataController.loadingChanged.add(() => { + // @ts-expect-error ts-error that._updateLoading(); }); that._dataController.progressChanged.add(that._updateLoading.bind(that)); that._dataController.dataSourceChanged.add(() => { - that._trigger('onChanged'); + that._trigger('onChanged', undefined); }); const expandValueChanging = that.option('onExpandValueChanging'); if (expandValueChanging) { that._dataController.expandValueChanging.add((e) => { + // @ts-expect-error ts-error expandValueChanging(e); }); } - }, + } _init() { const that = this; - that.callBase(); + super._init(); that._initDataController(); gridCoreUtils.logHeaderFilterDeprecatedWarningIfNeed(this); that._scrollLeft = that._scrollTop = null; that._initActions(); - }, + } _initActions() { const that = this; @@ -324,11 +378,11 @@ const PivotGrid = (Widget as any).inherit({ onExporting: that._createActionByOption('onExporting'), onCellPrepared: that._createActionByOption('onCellPrepared'), }; - }, + } _trigger(eventName, eventArg) { this._actions[eventName](eventArg); - }, + } _optionChanged(args) { const that = this; @@ -347,7 +401,7 @@ const PivotGrid = (Widget as any).inherit({ case 'scrolling': case 'stateStoring': that._initDataController(); - that._fieldChooserPopup.hide(); + that.getFieldChooserPopup().hide(); that._renderFieldChooser(); that._invalidate(); break; @@ -366,7 +420,7 @@ const PivotGrid = (Widget as any).inherit({ case 'renderCellCountLimit': break; case 'rtlEnabled': - that.callBase(args); + super._optionChanged(args); that._renderFieldChooser(); that._renderContextMenu(); hasWindow() && that._renderLoadPanel(that._dataArea.groupElement(), that.$element()); @@ -423,13 +477,13 @@ const PivotGrid = (Widget as any).inherit({ case 'height': case 'width': that._hasHeight = null; - that.callBase(args); + super._optionChanged(args); that.resize(); break; default: - that.callBase(args); + super._optionChanged(args); } - }, + } _updateScrollPosition(columnsArea, rowsArea, dataArea, force = false) { const that = this; @@ -459,7 +513,7 @@ const PivotGrid = (Widget as any).inherit({ } that._scrollUpdating = false; - }, + } _subscribeToEvents(columnsArea, rowsArea, dataArea) { const that = this; @@ -478,6 +532,7 @@ const PivotGrid = (Widget as any).inherit({ that._updateScrollPosition(columnsArea, rowsArea, dataArea); + // @ts-expect-error ts-error if (that.option('scrolling.mode') === 'virtual') { that._dataController.setViewportPosition(that._scrollLeft, that._scrollTop); } @@ -489,19 +544,23 @@ const PivotGrid = (Widget as any).inherit({ }); !that._hasHeight && that._dataController.subscribeToWindowScrollEvents(dataArea.groupElement()); - }, + } - _clean: noop, + _clean() { + noop(); + } _needDelayResizing(cellsInfo) { const cellsCount = cellsInfo.length * (cellsInfo.length ? cellsInfo[0].length : 0); - return cellsCount > this.option('renderCellCountLimit'); - }, + return cellsCount > Number(this.option('renderCellCountLimit')); + } _renderFieldChooser() { const that = this; + const container = that._pivotGridContainer; const fieldChooserOptions = that.option('fieldChooser') || {}; + // @ts-expect-error ts-error const toolbarItems = fieldChooserOptions.applyChangesMode === 'onDemand' ? [ { toolbar: 'bottom', @@ -510,8 +569,8 @@ const PivotGrid = (Widget as any).inherit({ options: { text: localizationMessage.format('OK'), onClick() { - that._fieldChooserPopup.$content().dxPivotGridFieldChooser('applyChanges'); - that._fieldChooserPopup.hide(); + that.getFieldChooserPopup().$content().dxPivotGridFieldChooser('applyChanges'); + that.getFieldChooserPopup().hide(); }, }, }, @@ -522,21 +581,26 @@ const PivotGrid = (Widget as any).inherit({ options: { text: localizationMessage.format('Cancel'), onClick() { - that._fieldChooserPopup.hide(); + that.getFieldChooserPopup().hide(); }, }, }, ] : []; const fieldChooserComponentOptions = { + // @ts-expect-error ts-error layout: fieldChooserOptions.layout, + // @ts-expect-error ts-error texts: fieldChooserOptions.texts || {}, dataSource: that.getDataSource(), + // @ts-expect-error ts-error allowSearch: fieldChooserOptions.allowSearch, + // @ts-expect-error ts-error searchTimeout: fieldChooserOptions.searchTimeout, width: undefined, height: undefined, headerFilter: that.option('headerFilter'), encodeHtml: that.option('fieldChooser.encodeHtml') ?? that.option('encodeHtml'), + // @ts-expect-error ts-error applyChangesMode: fieldChooserOptions.applyChangesMode, rtlEnabled: that.option('rtlEnabled'), onContextMenuPreparing(e) { @@ -545,12 +609,15 @@ const PivotGrid = (Widget as any).inherit({ }; const popupOptions = { shading: false, + // @ts-expect-error ts-error title: fieldChooserOptions.title, width: fieldChooserOptions.width, height: fieldChooserOptions.height, showCloseButton: true, resizeEnabled: true, + // @ts-expect-error ts-error minWidth: fieldChooserOptions.minWidth, + // @ts-expect-error ts-error minHeight: fieldChooserOptions.minHeight, toolbarItems, onResize(e) { @@ -570,7 +637,7 @@ const PivotGrid = (Widget as any).inherit({ }, }; - if (that._fieldChooserPopup) { + if (that.getFieldChooserPopup()) { that._fieldChooserPopup.option(popupOptions); that._fieldChooserPopup.$content().dxPivotGridFieldChooser(fieldChooserComponentOptions); } else { @@ -579,10 +646,11 @@ const PivotGrid = (Widget as any).inherit({ .addClass(FIELD_CHOOSER_POPUP_CLASS) .appendTo(container), Popup, + // @ts-expect-error ts-error popupOptions, ); } - }, + } _renderContextMenu() { const that = this; @@ -620,7 +688,7 @@ const PivotGrid = (Widget as any).inherit({ cssClass: PIVOTGRID_CLASS, target: that.$element(), }); - }, + } _getContextMenuItems(e) { const that = this; @@ -637,12 +705,14 @@ const PivotGrid = (Widget as any).inherit({ items.push({ beginGroup: true, icon: 'none', + // @ts-expect-error ts-error text: texts.expandAll, onItemClick() { dataSource.expandAll(field.index); }, }); items.push({ + // @ts-expect-error ts-error text: texts.collapseAll, icon: 'none', onItemClick() { @@ -664,6 +734,7 @@ const PivotGrid = (Widget as any).inherit({ } const showDataFieldCaption = !isDefined(e.cell.dataIndex) && e.dataFields.length > 1; + // @ts-expect-error ts-error const textFormat = e.area === 'column' ? texts.sortColumnBySummary : texts.sortRowBySummary; const checked = findField(e.dataFields, field.sortBySummaryField) === dataIndex && (e.cell.path || []).join('/') === (field.sortBySummaryPath || []).join('/'); const text = formatString(textFormat, showDataFieldCaption ? `${field.caption} - ${dataField.caption}` : field.caption); @@ -691,6 +762,7 @@ const PivotGrid = (Widget as any).inherit({ items.push({ beginGroup: sortingBySummaryItemCount === 0, icon: 'none', + // @ts-expect-error ts-error text: texts.removeAllSorting, onItemClick() { each(oppositeAreaFields, (_, field) => { @@ -712,9 +784,10 @@ const PivotGrid = (Widget as any).inherit({ items.push({ beginGroup: true, icon: 'columnchooser', + // @ts-expect-error ts-error text: texts.showFieldChooser, onItemClick() { - that._fieldChooserPopup.show(); + that.getFieldChooserPopup().show(); }, }); } @@ -723,8 +796,10 @@ const PivotGrid = (Widget as any).inherit({ items.push({ beginGroup: true, icon: 'xlsxfile', + // @ts-expect-error ts-error text: texts.exportToExcel, onItemClick() { + // @ts-expect-error ts-error that.exportTo(); }, }); @@ -739,7 +814,7 @@ const PivotGrid = (Widget as any).inherit({ } return undefined; - }, + } _createEventArgs(targetElement, dxEvent) { const that = this; @@ -755,7 +830,7 @@ const PivotGrid = (Widget as any).inherit({ return extend(that._createFieldArgs(targetElement), args); } return extend(that._createCellArgs(targetElement), args); - }, + } _createFieldArgs(targetElement) { const field = $(targetElement).children().data('field'); @@ -763,7 +838,7 @@ const PivotGrid = (Widget as any).inherit({ field, }; return isDefined(field) ? args : {}; - }, + } _createCellArgs(cellElement) { const $cellElement = $(cellElement); @@ -780,7 +855,7 @@ const PivotGrid = (Widget as any).inherit({ cell, }; return args; - }, + } _handleCellClick(e) { const that = this; @@ -796,15 +871,19 @@ const PivotGrid = (Widget as any).inherit({ cell && !args.cancel && isDefined(cell.expanded) && setTimeout(() => { that._dataController[cell.expanded ? 'collapseHeaderItem' : 'expandHeaderItem'](args.area, cell.path); }); - }, + } _getNoDataText() { return this.option('texts.noData'); - }, + } - _renderNoDataText: gridCoreUtils.renderNoDataText, + _renderNoDataText(element) { + return gridCoreUtils.renderNoDataText.call(this, element); + } - _renderLoadPanel: gridCoreUtils.renderLoadPanel, + _renderLoadPanel(element, container) { + return gridCoreUtils.renderLoadPanel.call(this, element, container, undefined); + } _updateLoading(progress) { const that = this; @@ -844,7 +923,7 @@ const PivotGrid = (Widget as any).inherit({ that._loadPanel.option(visibilityOptions); that.$element().toggleClass(OVERFLOW_HIDDEN_CLASS, !isLoading); } - }, + } _renderDescriptionArea() { const $element = this.$element(); @@ -856,8 +935,10 @@ const PivotGrid = (Widget as any).inherit({ let $targetContainer; + // @ts-expect-error ts-error if (fieldPanel.visible && fieldPanel.showFilterFields) { $targetContainer = $filterHeader; + // @ts-expect-error ts-error } else if (fieldPanel.visible && (fieldPanel.showDataFields || fieldPanel.showColumnFields)) { $targetContainer = $columnHeader; } else { @@ -866,16 +947,19 @@ const PivotGrid = (Widget as any).inherit({ $columnHeader.toggleClass( BOTTOM_BORDER_CLASS, + // @ts-expect-error ts-error !!(fieldPanel.visible && (fieldPanel.showDataFields || fieldPanel.showColumnFields)), ); $filterHeader.toggleClass( BOTTOM_BORDER_CLASS, + // @ts-expect-error ts-error !!(fieldPanel.visible && fieldPanel.showFilterFields), ); $descriptionCell.toggleClass( 'dx-pivotgrid-background', fieldPanel.visible + // @ts-expect-error ts-error && (fieldPanel.showDataFields || fieldPanel.showColumnFields || fieldPanel.showRowFields), ); @@ -890,6 +974,7 @@ const PivotGrid = (Widget as any).inherit({ .addClass('dx-pivotgrid-field-chooser-button'); const buttonOptions: Properties = { icon: 'columnchooser', + // @ts-expect-error ts-error hint: this.option('texts.showFieldChooser'), stylingMode, onClick: () => { @@ -906,25 +991,29 @@ const PivotGrid = (Widget as any).inherit({ .addClass('dx-pivotgrid-export-button'); const buttonOptions: Properties = { icon: 'xlsxfile', + // @ts-expect-error ts-error hint: this.option('texts.exportToExcel'), stylingMode, onClick: () => { + // @ts-expect-error ts-error this.exportTo(); }, }; this._createComponent($buttonElement, Button, buttonOptions); } - }, + } _detectHasContainerHeight() { const that = this; const element = that.$element(); if (isDefined(that._hasHeight)) { + // @ts-expect-error ts-error const height = that.option('height') || that.$element().get(0).style.height; - if (height && (that._hasHeight ^ (height !== 'auto') as any)) { + // @ts-expect-error ts-error + if (height && (that._hasHeight ^ (height !== 'auto'))) { that._hasHeight = null; } } @@ -940,7 +1029,7 @@ const PivotGrid = (Widget as any).inherit({ that._hasHeight = getHeight(element) !== TEST_HEIGHT; that._pivotGridContainer.removeClass('dx-hidden'); testElement.remove(); - }, + } _renderHeaders( rowHeaderContainer, @@ -963,8 +1052,9 @@ const PivotGrid = (Widget as any).inherit({ that._dataFields = that._dataFields || new FieldsArea(that, 'data'); that._dataFields.render(dataHeaderContainer, dataSource.getAreaFields('data')); + // @ts-expect-error ts-error that.$element().dxPivotGridFieldChooserBase('instance').renderSortable(); - }, + } _createTableElement() { const that = this; @@ -976,7 +1066,7 @@ const PivotGrid = (Widget as any).inherit({ eventsEngine.on($table, addNamespace(clickEventName, 'dxPivotGrid'), 'td', that._handleCellClick.bind(that)); return $table; - }, + } _renderDataArea(dataAreaElement) { const that = this; @@ -985,7 +1075,7 @@ const PivotGrid = (Widget as any).inherit({ dataArea.render(dataAreaElement, that._dataController.getCellsInfo()); return dataArea; - }, + } _renderRowsArea(rowsAreaElement) { const that = this; @@ -994,7 +1084,7 @@ const PivotGrid = (Widget as any).inherit({ rowsArea.render(rowsAreaElement, that._dataController.getRowsInfo()); return rowsArea; - }, + } _renderColumnsArea(columnsAreaElement) { const that = this; @@ -1003,13 +1093,13 @@ const PivotGrid = (Widget as any).inherit({ columnsArea.render(columnsAreaElement, that._dataController.getColumnsInfo()); return columnsArea; - }, + } _initMarkup() { const that = this; - that.callBase.apply(this, arguments); + super._initMarkup(); that.$element().addClass(PIVOTGRID_CLASS); - }, + } _renderContentImpl() { const that = this; @@ -1068,6 +1158,7 @@ const PivotGrid = (Widget as any).inherit({ that._pivotGridContainer.append(tableElement); that.$element().append(that._pivotGridContainer); + // @ts-expect-error ts-error if (that.option('rowHeaderLayout') === 'tree') { rowsAreaElement.addClass('dx-area-tree-view'); } @@ -1081,6 +1172,7 @@ const PivotGrid = (Widget as any).inherit({ allowFieldDragging: that.option('fieldPanel.allowFieldDragging'), headerFilter: that.option('headerFilter'), visible: that.option('visible'), + // @ts-expect-error ts-error remoteSort: that.option('scrolling.mode') === 'virtual', }); @@ -1111,7 +1203,7 @@ const PivotGrid = (Widget as any).inherit({ ); that._update(isFirstDrawing); - }, + } _update(isFirstDrawing) { const that = this; @@ -1123,13 +1215,13 @@ const PivotGrid = (Widget as any).inherit({ } else { updateHandler(); } - }, + } _fireContentReadyAction() { if (!this._dataController.isLoading()) { - this.callBase(); + super._fireContentReadyAction(); } - }, + } getScrollPath(area) { const that = this; @@ -1138,55 +1230,55 @@ const PivotGrid = (Widget as any).inherit({ return that._columnsArea.getScrollPath(that._scrollLeft); } return that._rowsArea.getScrollPath(that._scrollTop); - }, + } getDataSource() { return this._dataController.getDataSource(); - }, + } getFieldChooserPopup() { return this._fieldChooserPopup; - }, + } hasScroll(area) { const that = this; return area === 'column' ? that._columnsArea.hasScroll() : that._rowsArea.hasScroll(); - }, + } _dimensionChanged() { this.updateDimensions(); - }, + } _visibilityChanged(visible) { if (visible) { this.updateDimensions(); } - }, + } _dispose() { const that = this; clearTimeout(that._hideLoadingTimeoutID); - that.callBase.apply(that, arguments); + super._dispose(); if (that._dataController) { that._dataController.dispose(); } - }, + } _tableElement() { return this.$element().find('table').first(); - }, + } addWidgetPrefix(className) { return `dx-pivotgrid-${className}`; - }, + } resize() { this.updateDimensions(); - }, + } isReady() { - return this.callBase() && !this._dataController.isLoading(); - }, + return super.isReady() && !this._dataController.isLoading(); + } updateDimensions() { const that = this; @@ -1213,6 +1305,7 @@ const PivotGrid = (Widget as any).inherit({ return undefined; } + // @ts-expect-error ts-error const needSynchronizeFieldPanel = rowFieldsHeader.isVisible() && that.option('rowHeaderLayout') !== 'tree'; that._detectHasContainerHeight(); @@ -1408,6 +1501,8 @@ const PivotGrid = (Widget as any).inherit({ } const scrollingOptions = that.option('scrolling'); + + // @ts-expect-error ts-error if (scrollingOptions.mode === 'virtual') { that._setVirtualContentParams( scrollingOptions, @@ -1430,6 +1525,7 @@ const PivotGrid = (Widget as any).inherit({ updateScrollableResults.push(area && area.updateScrollable()); }); + // @ts-expect-error ts-error that._updateLoading(); that._renderNoDataText(dataAreaCell); @@ -1446,7 +1542,7 @@ const PivotGrid = (Widget as any).inherit({ }); }); return d; - }, + } _setVirtualContentParams( scrollingOptions, @@ -1486,14 +1582,15 @@ const PivotGrid = (Widget as any).inherit({ width: virtualContentParams.width, height: getHeight(this._columnsArea.groupElement()), }); - }, + } applyPartialDataSource(area, path, dataSource) { this._dataController.applyPartialDataSource(area, path, dataSource); - }, -}) - .inherit(ExportController) - .include(ChartIntegrationMixin); + } +} + +Object.assign(PivotGrid.prototype, ExportController); +Object.assign(PivotGrid.prototype, ChartIntegrationMixin); registerComponent('dxPivotGrid', PivotGrid); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts b/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts index e47794f372e0..3412120c14f2 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts @@ -1,6 +1,5 @@ import { DataSource } from '@js/common/data/data_source/data_source'; import { normalizeLoadResult } from '@js/common/data/data_source/utils'; -import Class from '@js/core/class'; import dateSerialization from '@js/core/utils/date_serialization'; import { Deferred, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; @@ -544,124 +543,126 @@ function prepareFields(fields) { }); } -const RemoteStore = Class.inherit((function () { - return { - ctor(options) { - this._dataSource = new DataSource(options); - this._store = this._dataSource.store(); - }, +class RemoteStore { + _dataSource: any; - getFields(fields) { - // @ts-expect-error - const d = new Deferred(); - - this._store.load({ - skip: 0, - take: 20, - }).done((data) => { - // @ts-expect-error - const normalizedArguments = normalizeLoadResult(data); - d.resolve(pivotGridUtils.discoverObjectFields(normalizedArguments.data, fields)); - }).fail(d.reject); - - return d; - }, - - key() { - return this._store.key(); - }, - - load(options) { - const that: any = this; + _store: any; + + constructor(options) { + this._dataSource = new DataSource(options); + this._store = this._dataSource.store(); + } + + getFields(fields) { + // @ts-expect-error + const d = new Deferred(); + + this._store.load({ + skip: 0, + take: 20, + }).done((data) => { // @ts-expect-error - const d = new Deferred(); - const result = { - rows: [], - columns: [], - values: [], - grandTotalRowIndex: 0, - grandTotalColumnIndex: 0, - - rowHash: {}, - columnHash: {}, - rowIndex: 1, - columnIndex: 1, - }; - const requestsOptions = createRequestsOptions(options); - const deferreds: any = []; + const normalizedArguments = normalizeLoadResult(data); + d.resolve(pivotGridUtils.discoverObjectFields(normalizedArguments.data, fields)); + }).fail(d.reject); - prepareFields(options.rows); - prepareFields(options.columns); - prepareFields(options.filters); + return d; + } - each(requestsOptions, (_, requestOptions) => { - const loadOptions = createLoadOptions(requestOptions, that.filter(), options.rows.length); - const loadDeferred = that._store.load(loadOptions); + key() { + return this._store.key(); + } - deferreds.push(loadDeferred); - }); + load(options) { + const that: any = this; + // @ts-expect-error + const d = new Deferred(); + const result = { + rows: [], + columns: [], + values: [], + grandTotalRowIndex: 0, + grandTotalColumnIndex: 0, + + rowHash: {}, + columnHash: {}, + rowIndex: 1, + columnIndex: 1, + }; + const requestsOptions = createRequestsOptions(options); + const deferreds: any = []; - when.apply(null, deferreds).done(function () { - const args = deferreds.length > 1 ? arguments : [arguments]; - - each(args, (index, argument) => { - const normalizedArguments = normalizeLoadResult(argument[0], argument[1]); - parseResult( - normalizedArguments.data, - normalizedArguments.extra, - requestsOptions[index], - result, - ); - }); + prepareFields(options.rows); + prepareFields(options.columns); + prepareFields(options.filters); - d.resolve({ - rows: result.rows, - columns: result.columns, - values: result.values, - grandTotalRowIndex: result.grandTotalRowIndex, - grandTotalColumnIndex: result.grandTotalColumnIndex, - }); - }).fail(d.reject); + each(requestsOptions, (_, requestOptions) => { + const loadOptions = createLoadOptions(requestOptions, that.filter(), options.rows.length); + const loadDeferred = that._store.load(loadOptions); - return d; - }, + deferreds.push(loadDeferred); + }); - filter() { - return this._dataSource.filter.apply(this._dataSource, arguments); - }, + when.apply(null, deferreds).done(function () { + const args = deferreds.length > 1 ? arguments : [arguments]; + + each(args, (index, argument) => { + const normalizedArguments = normalizeLoadResult(argument[0], argument[1]); + parseResult( + normalizedArguments.data, + normalizedArguments.extra, + requestsOptions[index], + result, + ); + }); - supportPaging() { - return false; - }, + d.resolve({ + rows: result.rows, + columns: result.columns, + values: result.values, + grandTotalRowIndex: result.grandTotalRowIndex, + grandTotalColumnIndex: result.grandTotalColumnIndex, + }); + }).fail(d.reject); - createDrillDownDataSource(loadOptions, params): any { - loadOptions = loadOptions || {}; - params = params || {}; + return d; + } - const store = this._store; - const filters = getFiltersByPath(loadOptions.rows, params.rowPath) - .concat(getFiltersByPath(loadOptions.columns, params.columnPath)) - .concat(getFiltersForDimension(loadOptions.rows)) - .concat(loadOptions.filters || []) - .concat(getFiltersForDimension(loadOptions.columns)); + filter(...args: any[]) { + return this._dataSource.filter.apply(this._dataSource, args); + } - const filterExp = createFilterExpressions(filters); + supportPaging() { + return false; + } - return new DataSource({ - load(loadOptions) { - const filter = mergeFilters([filterExp, loadOptions.filter]); + createDrillDownDataSource(loadOptions, params): any { + loadOptions = loadOptions || {}; + params = params || {}; - const extendedLoadOptions = extend({}, loadOptions, { - filter: filter.length === 0 ? undefined : filter, - select: params.customColumns, - }); + const store = this._store; + const filters = getFiltersByPath(loadOptions.rows, params.rowPath) + .concat(getFiltersByPath(loadOptions.columns, params.columnPath)) + .concat(getFiltersForDimension(loadOptions.rows)) + .concat(loadOptions.filters || []) + .concat(getFiltersForDimension(loadOptions.columns)); - return store.load(extendedLoadOptions); - }, - }); - }, - }; -})()); + const filterExp = createFilterExpressions(filters); + + return new DataSource({ + load(loadOptions) { + const filter = mergeFilters([filterExp, loadOptions.filter]); + + const extendedLoadOptions = extend({}, loadOptions, { + filter: filter.length === 0 ? undefined : filter, + select: params.customColumns, + }); + + return store.load(extendedLoadOptions); + }, + }); + } +} export default { RemoteStore }; export { RemoteStore }; diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/sortable/m_sortable.ts b/packages/devextreme/js/__internal/grids/pivot_grid/sortable/m_sortable.ts index 7971e5e23826..de09915c29c7 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/sortable/m_sortable.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/sortable/m_sortable.ts @@ -10,7 +10,6 @@ import { import { addNamespace } from '@js/common/core/events/utils/index'; import registerComponent from '@js/core/component_registrator'; import domAdapter from '@js/core/dom_adapter'; -import DOMComponent from '@js/core/dom_component'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; @@ -19,7 +18,8 @@ import { setWidth, } from '@js/core/utils/size'; import { isDefined } from '@js/core/utils/type'; -import swatchContainer from '@ts/core/utils/m_swatch_container'; +import swatchContainer from '@ts/core/utils/swatch_container'; +import DOMComponent from '@ts/core/widget/dom_component'; const { getSwatchContainer } = swatchContainer; @@ -165,9 +165,13 @@ function getScrollWrapper(scrollable) { }; } -const Sortable = (DOMComponent as any).inherit({ +class Sortable extends DOMComponent { + _indicator: any; + + _$draggable: any; + _getDefaultOptions() { - return extend(this.callBase(), { + return extend(super._getDefaultOptions(), { onChanged: null, onDragging: null, itemRender: null, @@ -182,13 +186,14 @@ const Sortable = (DOMComponent as any).inherit({ groupFilter: null, useIndicator: false, }); - }, + } _renderItem($sourceItem, target) { const itemRender = this.option('itemRender'); let $item; if (itemRender) { + // @ts-expect-error ts-error $item = itemRender($sourceItem, target); } else { $item = $sourceItem.clone(); @@ -198,7 +203,7 @@ const Sortable = (DOMComponent as any).inherit({ }); } return $item; - }, + } _renderIndicator($item, isVertical, $targetGroup, isLast) { const height = getOuterHeight($item, true); @@ -224,7 +229,7 @@ const Sortable = (DOMComponent as any).inherit({ } else { setHeight(this._indicator, height); } - }, + } _renderDraggable($sourceItem) { this._$draggable && this._$draggable.remove(); @@ -235,7 +240,7 @@ const Sortable = (DOMComponent as any).inherit({ zIndex: 1000000, position: 'absolute', }); - }, + } _detachEventHandlers() { const dragEventsString = [dragEventMove, dragEventStart, dragEventEnd, dragEventEnter, dragEventLeave, dragEventDrop].join(' '); @@ -244,7 +249,7 @@ const Sortable = (DOMComponent as any).inherit({ addNamespace(dragEventsString, SORTABLE_NAMESPACE), undefined, ); - }, + } _getItemOffset(isVertical, itemsOffset, e) { for (let i = 0; i < itemsOffset.length; i += 1) { @@ -267,14 +272,15 @@ const Sortable = (DOMComponent as any).inherit({ } return undefined; - }, + } _getEventListener() { const groupSelector = this.option('groupSelector'); const element = this.$element(); + // @ts-expect-error ts-error return groupSelector ? element.find(groupSelector) : element; - }, + } _attachEventHandlers() { const that = this; @@ -304,13 +310,16 @@ const Sortable = (DOMComponent as any).inherit({ }; const createGroups = function () { + // @ts-expect-error ts-error const root: any = domAdapter.getRootNode(that.$element().get(0)); if (!groupSelector) { return element; } return groupFilter + // @ts-expect-error ts-error ? $(root).find(groupSelector).filter(groupFilter) + // @ts-expect-error ts-error : element.find(groupSelector); }; @@ -328,6 +337,7 @@ const Sortable = (DOMComponent as any).inherit({ targetIndex: $targetGroup.find(itemSelector).index($targetItem), }; + // @ts-expect-error ts-error onDragging && onDragging(draggingArgs); if (draggingArgs.cancel) { @@ -493,6 +503,7 @@ const Sortable = (DOMComponent as any).inherit({ $targetGroup.removeClass(targetClass); changedArgs.targetGroup = $targetGroup.attr('group'); if (sourceGroup !== changedArgs.targetGroup || targetIndex > -1) { + // @ts-expect-error ts-error onChanged && onChanged(changedArgs); changedArgs.removeSourceElement && $sourceItem.remove(); } @@ -510,26 +521,26 @@ const Sortable = (DOMComponent as any).inherit({ $targetItem = null; }); } - }, + } _init() { - this.callBase(); + super._init(); this._attachEventHandlers(); - }, + } _render() { - this.callBase(); + super._render(); this.$element().addClass(SORTABLE_CLASS); - }, + } _dispose() { const that = this; - that.callBase.apply(that, arguments); + super._dispose(); that._$draggable && that._$draggable.detach(); that._indicator && that._indicator.detach(); - }, + } _optionChanged(args) { const that = this; @@ -551,16 +562,17 @@ const Sortable = (DOMComponent as any).inherit({ case 'direction': break; default: - that.callBase(args); + super._optionChanged(args); } - }, + } _useTemplates() { return false; - }, -}); + } +} /// #DEBUG +// @ts-expect-error ts-error Sortable.prototype.__SCROLL_STEP = SCROLL_STEP; /// #ENDDEBUG diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/xmla_store/m_xmla_store.ts b/packages/devextreme/js/__internal/grids/pivot_grid/xmla_store/m_xmla_store.ts index fffca92b9638..42245f38de34 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/xmla_store/m_xmla_store.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/xmla_store/m_xmla_store.ts @@ -1,6 +1,5 @@ import { getLanguageId } from '@js/common/core/localization/language_codes'; import { errors } from '@js/common/data/errors'; -import Class from '@js/core/class'; import $ from '@js/core/renderer'; import { noop } from '@js/core/utils/common'; import { Deferred, when } from '@js/core/utils/deferred'; @@ -14,29 +13,147 @@ import { getWindow } from '@js/core/utils/window'; import pivotGridUtils, { foreachTree, - getExpandedLevel, storeDrillDownMixin, + getExpandedLevel, + storeDrillDownMixin, } from '../m_widget_utils'; +// Utility functions +function union(elements: any[]): string { + const elementsString = elements.join(','); + return elements.length > 1 ? `Union(${elementsString})` : elementsString; +} + +function crossJoinElements(elements: any[]): string { + const elementsString = elements.join(','); + return elements.length > 1 ? stringFormat(MDX_CROSS_JOIN_TEMPLATE, elementsString) : elementsString; +} + +function mdxDescendants(level: string, levelMember: string, nextLevel: string): string { + const memberExpression = levelMember || level; + return `Descendants({${memberExpression}}, ${nextLevel}, SELF_AND_BEFORE)`; +} + +function getAllMember(dimension: any): string { + return `${dimension.hierarchyName || dimension.dataField}.[All]`; +} + +function getAllMembers(field: any): string { + let result = `${field.dataField}.allMembers`; + let { searchValue } = field; + + if (searchValue) { + searchValue = searchValue.replace(/'/g, '\'\''); + result = `Filter(${result}, instr(${field.dataField}.currentmember.member_caption,'${searchValue}') > 0)`; + } + + return result; +} + +function preparePathValue(pathValue: any, dataField?: string): any { + if (pathValue) { + const shouldSkipWrappingPathValue = isString(pathValue) && ( + pathValue.includes('&') || pathValue.startsWith(`${dataField}.`) + ); + pathValue = shouldSkipWrappingPathValue ? pathValue : `[${pathValue}]`; + + if (dataField && pathValue.indexOf(`${dataField}.`) === 0) { + pathValue = pathValue.slice(dataField.length + 1, pathValue.length); + } + } + return pathValue; +} + +function getNumber(str: string): number { + return parseInt(str, 10); +} + +function parseValue(valueText: string): any { + return isNumeric(valueText) ? parseFloat(valueText) : valueText; +} + +function getFirstChild(node: any, tagName: string): any { + return (node.getElementsByTagName(tagName) || [])[0]; +} + +function getFirstChildText(node: any, childTagName: string): string { + return getNodeText(getFirstChild(node, childTagName)); +} + +function getNodeText(node: any): string { + return node && (node.textContent || node.text || node.innerHTML) || ''; +} + +function parseStringWithUnicodeSymbols(str: string): string { + str = str.replace(/_x(....)_/g, (_, group1) => String.fromCharCode(parseInt(group1, 16))); + + const stringArray = str.match(/\[.+?\]/gi); + if (stringArray && stringArray.length) { + str = stringArray[stringArray.length - 1]; + } + + return str + .replace(/\[/gi, '') + .replace(/\]/gi, '') + .replace(/\$/gi, '') + .replace(/\./gi, ' '); +} + +function getLocaleIdProperty(): string { + const languageId = getLanguageId(); + + if (languageId !== undefined) { + return stringFormat('{0}', languageId); + } + return ''; +} + +// Constants const window = getWindow(); -const XmlaStore = Class.inherit((function () { - const discover = '{2}{0}{1}{0}{3}'; - const execute = '{0}{1}TrueMicrosoft SQL Server Management Studio3600{2}'; - const mdx = 'SELECT {2} FROM {0} {1} CELL PROPERTIES VALUE, FORMAT_STRING, LANGUAGE, BACK_COLOR, FORE_COLOR, FONT_FLAGS'; - const mdxFilterSelect = '(SELECT {0} FROM {1})'; - const mdxSubset = 'Subset({0}, {1}, {2})'; - const mdxOrder = 'Order({0}, {1}, {2})'; - const mdxWith = '{0} {1} as {2}'; - const mdxSlice = 'WHERE ({0})'; - const mdxNonEmpty = 'NonEmpty({0}, {1})'; - const mdxAxis = '{0} DIMENSION PROPERTIES PARENT_UNIQUE_NAME,HIERARCHY_UNIQUE_NAME, MEMBER_VALUE ON {1}'; - const mdxCrossJoin = 'CrossJoin({0})'; - const mdxSet = '{{0}}'; - - const MEASURE_DEMENSION_KEY = 'DX_MEASURES'; - const MD_DIMTYPE_MEASURE = '2'; - - function execXMLA(requestOptions, data) { +const XMLA_DISCOVER_TEMPLATE = '{2}{0}{1}{0}{3}'; + +const XMLA_EXECUTE_TEMPLATE = '{0}{1}TrueMicrosoft SQL Server Management Studio3600{2}'; + +const MDX_SELECT_TEMPLATE = 'SELECT {2} FROM {0} {1} CELL PROPERTIES VALUE, FORMAT_STRING, LANGUAGE, BACK_COLOR, FORE_COLOR, FONT_FLAGS'; + +const MDX_FILTER_SELECT_TEMPLATE = '(SELECT {0} FROM {1})'; + +const MDX_SUBSET_TEMPLATE = 'Subset({0}, {1}, {2})'; + +const MDX_ORDER_TEMPLATE = 'Order({0}, {1}, {2})'; + +const MDX_WITH_TEMPLATE = '{0} {1} as {2}'; + +const MDX_SLICE_TEMPLATE = 'WHERE ({0})'; + +const MDX_NON_EMPTY_TEMPLATE = 'NonEmpty({0}, {1})'; + +const MDX_AXIS_TEMPLATE = '{0} DIMENSION PROPERTIES PARENT_UNIQUE_NAME,HIERARCHY_UNIQUE_NAME, MEMBER_VALUE ON {1}'; + +const MDX_CROSS_JOIN_TEMPLATE = 'CrossJoin({0})'; + +const MDX_SET_TEMPLATE = '{{0}}'; + +const MEASURE_DIMENSION_KEY = 'DX_MEASURES'; + +const MD_DIMTYPE_MEASURE = '2'; + +class XmlaStore { + _options: any; + + constructor(options: any) { + this._options = options; + } + + key() { + noop(); + } + + filter() { + noop(); + } + + execXMLA(requestOptions, data) { // @ts-expect-error const deferred = new Deferred(); const { beforeSend } = requestOptions; @@ -84,48 +201,7 @@ const XmlaStore = Class.inherit((function () { return deferred; } - function getLocaleIdProperty() { - const languageId = getLanguageId(); - - if (languageId !== undefined) { - return stringFormat('{0}', languageId); - } - return ''; - } - - function mdxDescendants(level, levelMember, nextLevel) { - const memberExpression = levelMember || level; - - return `Descendants({${memberExpression}}, ${nextLevel}, SELF_AND_BEFORE)`; - } - - function getAllMember(dimension) { - return `${dimension.hierarchyName || dimension.dataField}.[All]`; - } - - function getAllMembers(field) { - let result = `${field.dataField}.allMembers`; - let { searchValue } = field; - - if (searchValue) { - searchValue = searchValue.replace(/'/g, '\'\''); - result = `Filter(${result}, instr(${field.dataField}.currentmember.member_caption,'${searchValue}') > 0)`; - } - - return result; - } - - function crossJoinElements(elements) { - const elementsString = elements.join(','); - return elements.length > 1 ? stringFormat(mdxCrossJoin, elementsString) : elementsString; - } - - function union(elements) { - const elementsString = elements.join(','); - return elements.length > 1 ? `Union(${elementsString})` : elementsString; - } - - function generateCrossJoin( + generateCrossJoin( path, expandLevel, expandAllCount, @@ -195,10 +271,10 @@ const XmlaStore = Class.inherit((function () { } } if (arg) { - arg = stringFormat(mdxSet, arg); + arg = stringFormat(MDX_SET_TEMPLATE, arg); if (take) { const sortBy = (field.hierarchyName || field.dataField) + (field.sortBy === 'displayText' ? '.MEMBER_CAPTION' : '.MEMBER_VALUE'); - arg = stringFormat(mdxOrder, arg, sortBy, field.sortOrder === 'desc' ? 'DESC' : 'ASC'); + arg = stringFormat(MDX_ORDER_TEMPLATE, arg, sortBy, field.sortOrder === 'desc' ? 'DESC' : 'ASC'); } crossJoinArgs.push(arg); } @@ -207,7 +283,7 @@ const XmlaStore = Class.inherit((function () { return crossJoinElements(crossJoinArgs); } - function fillCrossJoins( + fillCrossJoins( crossJoins, path, expandLevel, @@ -226,7 +302,7 @@ const XmlaStore = Class.inherit((function () { do { expandAllCount += 1; dimensionIndex = path.length + expandAllCount + expandIndex; - let crossJoin = generateCrossJoin( + let crossJoin = this.generateCrossJoin( path, expandLevel, expandAllCount, @@ -237,7 +313,7 @@ const XmlaStore = Class.inherit((function () { take, ); if (!take && !totalsOnly) { - crossJoin = stringFormat(mdxNonEmpty, crossJoin, cellsString); + crossJoin = stringFormat(MDX_NON_EMPTY_TEMPLATE, crossJoin, cellsString); } crossJoins.push(crossJoin); } while ( @@ -247,15 +323,15 @@ const XmlaStore = Class.inherit((function () { ); } - function declare(expression, withArray, name?, type?) { + declare(expression, withArray, name?, type?) { name = name || `[DX_Set_${withArray.length}]`; type = type || 'set'; - withArray.push(stringFormat(mdxWith, type, name, expression)); + withArray.push(stringFormat(MDX_WITH_TEMPLATE, type, name, expression)); return name; } - function generateAxisMdx(options, axisName, cells, withArray, parseOptions) { + generateAxisMdx(options, axisName, cells, withArray, parseOptions) { const dimensions = options[axisName]; const crossJoins = []; let path = []; @@ -263,7 +339,7 @@ const XmlaStore = Class.inherit((function () { let expandIndex = 0; let expandLevel = 0; const result: any = []; - const cellsString = stringFormat(mdxSet, cells.join(',')); + const cellsString = stringFormat(MDX_SET_TEMPLATE, cells.join(',')); if (dimensions && dimensions.length) { if (options.headerName === axisName) { @@ -277,9 +353,9 @@ const XmlaStore = Class.inherit((function () { } expandLevel = getExpandedLevel(options, axisName); - fillCrossJoins(crossJoins, [], expandLevel, expandIndex, path, options, axisName, cellsString, axisName === 'rows' ? options.rowTake : options.columnTake, options.totalsOnly); + this.fillCrossJoins(crossJoins, [], expandLevel, expandIndex, path, options, axisName, cellsString, axisName === 'rows' ? options.rowTake : options.columnTake, options.totalsOnly); each(expandedPaths, (_, expandedPath) => { - fillCrossJoins( + this.fillCrossJoins( crossJoins, expandedPath, expandLevel, @@ -306,7 +382,7 @@ const XmlaStore = Class.inherit((function () { let expression = union(crossJoins); if (axisName === 'rows' && options.rowTake) { expression = stringFormat( - mdxSubset, + MDX_SUBSET_TEMPLATE, expression, options.rowSkip > 0 ? options.rowSkip + 1 @@ -318,7 +394,7 @@ const XmlaStore = Class.inherit((function () { } if (axisName === 'columns' && options.columnTake) { expression = stringFormat( - mdxSubset, + MDX_SUBSET_TEMPLATE, expression, options.columnSkip > 0 ? options.columnSkip + 1 @@ -330,10 +406,10 @@ const XmlaStore = Class.inherit((function () { } const axisSet = `[DX_${axisName}]`; - result.push(declare(expression, withArray, axisSet)); + result.push(this.declare(expression, withArray, axisSet)); if (options.totalsOnly) { - result.push(declare(`COUNT(${axisSet})`, withArray, `[DX_${axisName}_count]`, 'member')); + result.push(this.declare(`COUNT(${axisSet})`, withArray, `[DX_${axisName}_count]`, 'member')); } } @@ -341,10 +417,10 @@ const XmlaStore = Class.inherit((function () { result.push(cellsString); } - return stringFormat(mdxAxis, crossJoinElements(result), axisName); + return stringFormat(MDX_AXIS_TEMPLATE, crossJoinElements(result), axisName); } - function generateAxisFieldsFilter(fields) { + generateAxisFieldsFilter(fields) { const filterMembers: any = []; each(fields, (_, field) => { @@ -368,7 +444,7 @@ const XmlaStore = Class.inherit((function () { }); if (filterValues.length) { - filterStringExpression = stringFormat(mdxSet, filterExpression.join(',')); + filterStringExpression = stringFormat(MDX_SET_TEMPLATE, filterExpression.join(',')); if (field.filterType === 'exclude') { filterStringExpression = `Except(${getAllMembers(field)},${filterStringExpression})`; @@ -381,19 +457,19 @@ const XmlaStore = Class.inherit((function () { return filterMembers.length ? crossJoinElements(filterMembers) : ''; } - function generateFrom(columnsFilter, rowsFilter, filter, cubeName) { + generateFrom(columnsFilter, rowsFilter, filter, cubeName) { let from = `[${cubeName}]`; each([columnsFilter, rowsFilter, filter], (_, filter) => { if (filter) { - from = stringFormat(mdxFilterSelect, `${filter}on 0`, from); + from = stringFormat(MDX_FILTER_SELECT_TEMPLATE, `${filter}on 0`, from); } }); return from; } - function generateMdxCore( + generateMdxCore( axisStrings, withArray, columns, @@ -421,14 +497,14 @@ const XmlaStore = Class.inherit((function () { select = axisStrings.join(','); } mdxString = withString + stringFormat( - mdx, - generateFrom( - generateAxisFieldsFilter(columns), - generateAxisFieldsFilter(rows), - generateAxisFieldsFilter(filters || []), + MDX_SELECT_TEMPLATE, + this.generateFrom( + this.generateAxisFieldsFilter(columns), + this.generateAxisFieldsFilter(rows), + this.generateAxisFieldsFilter(filters || []), cubeName, ), - slice.length ? stringFormat(mdxSlice, slice.join(',')) : '', + slice.length ? stringFormat(MDX_SLICE_TEMPLATE, slice.join(',')) : '', select, ); } @@ -436,16 +512,16 @@ const XmlaStore = Class.inherit((function () { return mdxString; } - function prepareDataFields(withArray, valueFields) { + prepareDataFields(withArray, valueFields) { return map(valueFields, (cell) => { if (isString(cell.expression)) { - declare(cell.expression, withArray, cell.dataField, 'member'); + this.declare(cell.expression, withArray, cell.dataField, 'member'); } return cell.dataField; }); } - function addSlices(slices, options, headerName, path) { + addSlices(slices, options, headerName, path) { each(path, (index: number, value) => { const dimension = options[headerName][index]; if ( @@ -453,40 +529,40 @@ const XmlaStore = Class.inherit((function () { || dimension.hierarchyName !== options[headerName][index + 1] .hierarchyName ) { - slices.push(`${dimension.dataField}.${preparePathValue(value, dimension.dataField)}`); + slices.push(`${dimension.dataField}.${this.preparePathValue(value, dimension.dataField)}`); } }); } - function generateMDX(options, cubeName, parseOptions) { + generateMDX(options, cubeName, parseOptions) { const columns = options.columns || []; const rows = options.rows || []; const values = options.values && options.values.length ? options.values : [{ dataField: '[Measures]' }]; const slice = []; const withArray = []; const axisStrings: any = []; - const dataFields = prepareDataFields(withArray, values); + const dataFields = this.prepareDataFields(withArray, values); parseOptions.measureCount = options.skipValues ? 1 : values.length; parseOptions.visibleLevels = {}; if (options.headerName && options.path) { - addSlices(slice, options, options.headerName, options.path); + this.addSlices(slice, options, options.headerName, options.path); } if (options.headerName && options.oppositePath) { - addSlices(slice, options, options.headerName === 'rows' ? 'columns' : 'rows', options.oppositePath); + this.addSlices(slice, options, options.headerName === 'rows' ? 'columns' : 'rows', options.oppositePath); } if (columns.length || dataFields.length) { - axisStrings.push(generateAxisMdx(options, 'columns', dataFields, withArray, parseOptions)); + axisStrings.push(this.generateAxisMdx(options, 'columns', dataFields, withArray, parseOptions)); } if (rows.length) { - axisStrings.push(generateAxisMdx(options, 'rows', dataFields, withArray, parseOptions)); + axisStrings.push(this.generateAxisMdx(options, 'rows', dataFields, withArray, parseOptions)); } - return generateMdxCore( + return this.generateMdxCore( axisStrings, withArray, columns, @@ -498,37 +574,37 @@ const XmlaStore = Class.inherit((function () { ); } - function createDrillDownAxisSlice(slice, fields, path) { + createDrillDownAxisSlice(slice, fields, path) { each(path, (index: number, value) => { const field = fields[index]; if (field.hierarchyName && (fields[index + 1] || {}).hierarchyName === field.hierarchyName) { return; } - slice.push(`${field.dataField}.${preparePathValue(value, field.dataField)}`); + slice.push(`${field.dataField}.${this.preparePathValue(value, field.dataField)}`); }); } - function generateDrillDownMDX(options, cubeName, params) { + generateDrillDownMDX(options, cubeName, params) { const columns = options.columns || []; const rows = options.rows || []; const values = options.values && options.values.length ? options.values : [{ dataField: '[Measures]' }]; const slice: any = []; const withArray: any = []; const axisStrings: any = []; - const dataFields = prepareDataFields(withArray, values); + const dataFields = this.prepareDataFields(withArray, values); const { maxRowCount } = params; const customColumns = params.customColumns || []; const customColumnsString = customColumns.length > 0 ? ` return ${customColumns.join(',')}` : ''; - createDrillDownAxisSlice(slice, columns, params.columnPath || []); + this.createDrillDownAxisSlice(slice, columns, params.columnPath || []); - createDrillDownAxisSlice(slice, rows, params.rowPath || []); + this.createDrillDownAxisSlice(slice, rows, params.rowPath || []); if (columns.length || dataFields.length) { axisStrings.push([`${dataFields[params.dataIndex] || dataFields[0]} on 0`]); } - const coreMDX = generateMdxCore( + const coreMDX = this.generateMdxCore( axisStrings, withArray, columns, @@ -541,24 +617,7 @@ const XmlaStore = Class.inherit((function () { return coreMDX ? `drillthrough${maxRowCount > 0 ? ` maxrows ${maxRowCount}` : ''}${coreMDX}${customColumnsString}` : coreMDX; } - function getNumber(str) { - return parseInt(str, 10); - } - - function parseValue(valueText) { - // @ts-expect-error - return isNumeric(valueText) ? parseFloat(valueText) : valueText; - } - - function getFirstChild(node, tagName) { - return (node.getElementsByTagName(tagName) || [])[0]; - } - - function getFirstChildText(node, childTagName) { - return getNodeText(getFirstChild(node, childTagName)); - } - - function parseAxes(xml, skipValues) { + parseAxes(xml, skipValues) { const axes: any = []; each(xml.getElementsByTagName('Axis'), (_, axisElement) => { @@ -614,11 +673,11 @@ const XmlaStore = Class.inherit((function () { return axes; } - function getNodeText(node) { - return node && (node.textContent || node.text || node.innerHTML) || ''; + getNodeText(node) { + return getNodeText(node); } - function parseCells(xml, axes, measureCount) { + parseCells(xml, axes, measureCount) { const cells: any = []; let cell: any = []; let index = 0; @@ -626,21 +685,21 @@ const XmlaStore = Class.inherit((function () { const cellElements = xml.getElementsByTagName('Cell'); const errorDictionary = {}; + // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < cellElements.length; i += 1) { const xmlCell = cellElements[i]; const valueElement = xmlCell.getElementsByTagName('Value')[0]; const errorElements = valueElement && valueElement.getElementsByTagName('Error') || []; - const text = errorElements.length === 0 ? getNodeText(valueElement) : '#N/A'; + const text = errorElements.length === 0 ? this.getNodeText(valueElement) : '#N/A'; const value = parseFloat(text); - const isNumeric = (text - value + 1) > 0; const cellOrdinal = getNumber(xmlCell.getAttribute('CellOrdinal')); if (errorElements.length) { - errorDictionary[getNodeText(errorElements[0].getElementsByTagName('ErrorCode')[0])] = getNodeText(errorElements[0].getElementsByTagName('Description')[0]); + errorDictionary[this.getNodeText(errorElements[0].getElementsByTagName('ErrorCode')[0])] = this.getNodeText(errorElements[0].getElementsByTagName('Description')[0]); } cellsOriginal[cellOrdinal] = { - value: isNumeric ? value : text || null, + value: isNumeric(text) ? value : text || null, }; } @@ -667,21 +726,11 @@ const XmlaStore = Class.inherit((function () { return cells; } - function preparePathValue(pathValue, dataField?) { - if (pathValue) { - const shouldSkipWrappingPathValue = isString(pathValue) && ( - pathValue.includes('&') || pathValue.startsWith(`${dataField}.`) - ); - pathValue = shouldSkipWrappingPathValue ? pathValue : `[${pathValue}]`; - - if (dataField && pathValue.indexOf(`${dataField}.`) === 0) { - pathValue = pathValue.slice(dataField.length + 1, pathValue.length); - } - } - return pathValue; + preparePathValue(pathValue, dataField?) { + return preparePathValue(pathValue, dataField); } - function getItem(hash, name, member?, index?) { + getItem(hash, name, member?, index?) { let item = hash[name]; if (!item) { @@ -703,7 +752,7 @@ const XmlaStore = Class.inherit((function () { return item; } - function getVisibleChildren(item, visibleLevels) { + getVisibleChildren(item, visibleLevels) { const result = []; const children = item.children && (item.children.length @@ -726,24 +775,24 @@ const XmlaStore = Class.inherit((function () { } if (firstChild) { for (let i = 0; i < children.length; i += 1) { if (children[i].hierarchyName === firstChild.hierarchyName) { - result.push.apply(result, getVisibleChildren(children[i], visibleLevels)); + result.push.apply(result, this.getVisibleChildren(children[i], visibleLevels)); } } } return result; } - function processMember(dataIndex, member, parentItem) { + processMember(dataIndex, member, parentItem) { let children = parentItem.children = parentItem.children || []; const hash = children.hash = children.hash || {}; const grandTotalHash = children.grandTotalHash = children.grandTotalHash || {}; if (member.parentName) { - parentItem = getItem(hash, member.parentName); + parentItem = this.getItem(hash, member.parentName); children = parentItem.children = parentItem.children || []; } - const currentItem = getItem(hash, member.name, member, dataIndex); + const currentItem = this.getItem(hash, member.name, member, dataIndex); if (member.hasValue && !currentItem.added) { currentItem.index = dataIndex; @@ -760,7 +809,7 @@ const XmlaStore = Class.inherit((function () { return currentItem; } - function getGrandTotalIndex(parentItem, visibleLevels) { + getGrandTotalIndex(parentItem, visibleLevels) { let grandTotalIndex; if (parentItem.children.length === 1 && parentItem.children[0].parentName === '') { grandTotalIndex = parentItem.children[0].index; @@ -770,7 +819,7 @@ const XmlaStore = Class.inherit((function () { parentItem.children.grandTotalHash = grandTotalHash; - parentItem.children = getVisibleChildren(parentItem, visibleLevels); + parentItem.children = this.getVisibleChildren(parentItem, visibleLevels); } else if (parentItem.children.length === 0) { grandTotalIndex = 0; } @@ -778,7 +827,7 @@ const XmlaStore = Class.inherit((function () { return grandTotalIndex; } - function fillDataSourceAxes(dataSourceAxis, axisTuples, measureCount, visibleLevels) { + fillDataSourceAxes(dataSourceAxis, axisTuples, measureCount, visibleLevels) { const result = []; each(axisTuples, (tupleIndex, members) => { @@ -790,7 +839,7 @@ const XmlaStore = Class.inherit((function () { : tupleIndex; each(members, (_, member) => { - parentItem = processMember(dataIndex, member, parentItem); + parentItem = this.processMember(dataIndex, member, parentItem); }); }); @@ -798,13 +847,13 @@ const XmlaStore = Class.inherit((function () { children: result, }; - parentItem.children = getVisibleChildren(parentItem, visibleLevels); + parentItem.children = this.getVisibleChildren(parentItem, visibleLevels); - const grandTotalIndex = getGrandTotalIndex(parentItem, visibleLevels); + const grandTotalIndex = this.getGrandTotalIndex(parentItem, visibleLevels); foreachTree(parentItem.children, (items) => { const item = items[0]; - const children = getVisibleChildren(item, visibleLevels); + const children = this.getVisibleChildren(item, visibleLevels); if (children.length) { item.children = children; @@ -825,7 +874,7 @@ const XmlaStore = Class.inherit((function () { return grandTotalIndex; } - function checkError(xml) { + checkError(xml) { const faultElementNS = xml.getElementsByTagName('soap:Fault'); const faultElement = xml.getElementsByTagName('Fault'); const errorElement = $(([] as any).slice.call(faultElement.length ? faultElement : faultElementNS)).find('Error'); @@ -839,7 +888,7 @@ const XmlaStore = Class.inherit((function () { return null; } - function parseResult(xml, parseOptions) { + parseResult(xml, parseOptions) { const dataSource: any = { columns: [], rows: [], @@ -847,28 +896,28 @@ const XmlaStore = Class.inherit((function () { const { measureCount } = parseOptions; - const axes = parseAxes(xml, parseOptions.skipValues); + const axes = this.parseAxes(xml, parseOptions.skipValues); - dataSource.grandTotalColumnIndex = fillDataSourceAxes( + dataSource.grandTotalColumnIndex = this.fillDataSourceAxes( dataSource.columns, axes[0], measureCount, parseOptions.visibleLevels, ); - dataSource.grandTotalRowIndex = fillDataSourceAxes( + dataSource.grandTotalRowIndex = this.fillDataSourceAxes( dataSource.rows, axes[1], undefined, parseOptions.visibleLevels, ); - dataSource.values = parseCells(xml, axes, measureCount); + dataSource.values = this.parseCells(xml, axes, measureCount); return dataSource; } - function parseDiscoverRowSet(xml, schema, dimensions, translatedDisplayFolders?) { + parseDiscoverRowSet(xml, schema, dimensions, translatedDisplayFolders?) { const result: any = []; const isMeasure = schema === 'MEASURE'; const displayFolderField = isMeasure ? 'MEASUREGROUP_NAME' : `${schema}_DISPLAY_FOLDER`; @@ -883,7 +932,7 @@ const XmlaStore = Class.inherit((function () { } if ((levelNumber !== '0' || getFirstChildText(row, `${schema}_IS_VISIBLE`) !== 'true') && (getFirstChildText(row, 'DIMENSION_TYPE') !== MD_DIMTYPE_MEASURE)) { - const dimension = isMeasure ? MEASURE_DEMENSION_KEY : getFirstChildText(row, 'DIMENSION_UNIQUE_NAME'); + const dimension = isMeasure ? MEASURE_DIMENSION_KEY : getFirstChildText(row, 'DIMENSION_UNIQUE_NAME'); const dataField = getFirstChildText(row, `${schema}_UNIQUE_NAME`); result.push({ dimension: dimensions.names[dimension] || dimension, @@ -902,7 +951,7 @@ const XmlaStore = Class.inherit((function () { return result; } - function parseMeasureGroupDiscoverRowSet(xml) { + parseMeasureGroupDiscoverRowSet(xml) { const measureGroups = {}; each(xml.getElementsByTagName('row'), (_, row) => { measureGroups[getFirstChildText(row, 'MEASUREGROUP_NAME')] = getFirstChildText(row, 'MEASUREGROUP_CAPTION'); @@ -910,7 +959,7 @@ const XmlaStore = Class.inherit((function () { return measureGroups; } - function parseDimensionsDiscoverRowSet(xml) { + parseDimensionsDiscoverRowSet(xml) { const result = { names: {}, defaultHierarchies: {}, @@ -919,7 +968,7 @@ const XmlaStore = Class.inherit((function () { each($(xml).find('row'), function () { const $row = $(this); const type = $row.children('DIMENSION_TYPE').text(); - const dimensionName = type === MD_DIMTYPE_MEASURE ? MEASURE_DEMENSION_KEY : $row.children('DIMENSION_UNIQUE_NAME').text(); + const dimensionName = type === MD_DIMTYPE_MEASURE ? MEASURE_DIMENSION_KEY : $row.children('DIMENSION_UNIQUE_NAME').text(); result.names[dimensionName] = $row.children('DIMENSION_CAPTION').text(); result.defaultHierarchies[$row.children('DEFAULT_HIERARCHY').text()] = true; @@ -927,22 +976,11 @@ const XmlaStore = Class.inherit((function () { return result; } - function parseStringWithUnicodeSymbols(str) { - str = str.replace(/_x(....)_/g, (_, group1) => String.fromCharCode(parseInt(group1, 16))); - - const stringArray = str.match(/\[.+?\]/gi); - if (stringArray && stringArray.length) { - str = stringArray[stringArray.length - 1]; - } - - return str - .replace(/\[/gi, '') - .replace(/\]/gi, '') - .replace(/\$/gi, '') - .replace(/\./gi, ' '); + parseStringWithUnicodeSymbols(str) { + return parseStringWithUnicodeSymbols(str); } - function parseDrillDownRowSet(xml) { + parseDrillDownRowSet(xml) { const rows = xml.getElementsByTagName('row'); const result: any = []; const columnNames: any = {}; @@ -954,8 +992,8 @@ const XmlaStore = Class.inherit((function () { for (let j = 0; j < children.length; j += 1) { const { tagName } = children[j]; const name = columnNames[tagName] = columnNames[tagName] - || parseStringWithUnicodeSymbols(tagName); - item[name] = getNodeText(children[j]); + || this.parseStringWithUnicodeSymbols(tagName); + item[name] = this.getNodeText(children[j]); } result.push(item); } @@ -963,15 +1001,15 @@ const XmlaStore = Class.inherit((function () { return result; } - function sendQuery(storeOptions, mdxString) { + sendQuery(storeOptions, mdxString) { mdxString = ($('
') as any).text(mdxString).html(); - return execXMLA( + return this.execXMLA( storeOptions, - stringFormat(execute, mdxString, storeOptions.catalog, getLocaleIdProperty()), + stringFormat(XMLA_EXECUTE_TEMPLATE, mdxString, storeOptions.catalog, getLocaleIdProperty()), ); } - function processTotalCount(data, options, totalCountXml) { + processTotalCount(data, options, totalCountXml) { const axes: any = []; const columnOptions = options.columns || []; const rowOptions = options.rows || []; @@ -982,7 +1020,7 @@ const XmlaStore = Class.inherit((function () { if (rowOptions.length) { axes.push({}); } - const cells = parseCells(totalCountXml, [[{}], [{}, {}]], 1); + const cells = this.parseCells(totalCountXml, [[{}], [{}, {}]], 1); if (!columnOptions.length && rowOptions.length) { data.rowCount = Math.max(cells[0][0][0] - 1, 0); } @@ -1013,145 +1051,139 @@ const XmlaStore = Class.inherit((function () { } } - return { - ctor(options) { - this._options = options; - }, - - getFields() { - const options = this._options; - const { catalog } = options; - const { cube } = options; - const localeIdProperty = getLocaleIdProperty(); - const dimensionsRequest = execXMLA(options, stringFormat(discover, catalog, cube, 'MDSCHEMA_DIMENSIONS', localeIdProperty)); - const measuresRequest = execXMLA(options, stringFormat(discover, catalog, cube, 'MDSCHEMA_MEASURES', localeIdProperty)); - const hierarchiesRequest = execXMLA(options, stringFormat(discover, catalog, cube, 'MDSCHEMA_HIERARCHIES', localeIdProperty)); - const levelsRequest = execXMLA(options, stringFormat(discover, catalog, cube, 'MDSCHEMA_LEVELS', localeIdProperty)); - // @ts-expect-error - const result = new Deferred(); - - when(dimensionsRequest, measuresRequest, hierarchiesRequest, levelsRequest) - .then((dimensionsResponse, measuresResponse, hierarchiesResponse, levelsResponse) => { - execXMLA(options, stringFormat(discover, catalog, cube, 'MDSCHEMA_MEASUREGROUPS', localeIdProperty)) - .done((measureGroupsResponse) => { - const dimensions = parseDimensionsDiscoverRowSet(dimensionsResponse); - const hierarchies = parseDiscoverRowSet(hierarchiesResponse, 'HIERARCHY', dimensions); - const levels = parseDiscoverRowSet(levelsResponse, 'LEVEL', dimensions); - const measureGroups = parseMeasureGroupDiscoverRowSet(measureGroupsResponse); - const fields = parseDiscoverRowSet(measuresResponse, 'MEASURE', dimensions, measureGroups) - .concat(hierarchies); - const levelsByHierarchy = {}; - - each(levels, (_, level) => { - levelsByHierarchy[level.hierarchyName] = levelsByHierarchy[level.hierarchyName] - || []; - levelsByHierarchy[level.hierarchyName].push(level); - }); - - each(hierarchies, (_, hierarchy) => { - if (levelsByHierarchy[hierarchy.dataField] - && levelsByHierarchy[hierarchy.dataField].length > 1) { - hierarchy.groupName = hierarchy.hierarchyName = hierarchy.dataField; - - fields.push.apply(fields, levelsByHierarchy[hierarchy.hierarchyName]); - } - }); - result.resolve(fields); - }).fail(result.reject); - }).fail(result.reject); - - return result; - }, - - load(options) { - // @ts-expect-error - const result = new Deferred(); - const storeOptions = this._options; - const parseOptions = { - skipValues: options.skipValues, - }; - const mdxString = generateMDX(options, storeOptions.cube, parseOptions); - - let rowCountMdx; - if (options.rowSkip || options.rowTake || options.columnTake || options.columnSkip) { - rowCountMdx = generateMDX(extend({}, options, { - totalsOnly: true, - rowSkip: null, - rowTake: null, - columnSkip: null, - columnTake: null, - }), storeOptions.cube, {}); - } + getFields() { + const options = this._options; + const { catalog } = options; + const { cube } = options; + const localeIdProperty = getLocaleIdProperty(); + const dimensionsRequest = this.execXMLA(options, stringFormat(XMLA_DISCOVER_TEMPLATE, catalog, cube, 'MDSCHEMA_DIMENSIONS', localeIdProperty)); + const measuresRequest = this.execXMLA(options, stringFormat(XMLA_DISCOVER_TEMPLATE, catalog, cube, 'MDSCHEMA_MEASURES', localeIdProperty)); + const hierarchiesRequest = this.execXMLA(options, stringFormat(XMLA_DISCOVER_TEMPLATE, catalog, cube, 'MDSCHEMA_HIERARCHIES', localeIdProperty)); + const levelsRequest = this.execXMLA(options, stringFormat(XMLA_DISCOVER_TEMPLATE, catalog, cube, 'MDSCHEMA_LEVELS', localeIdProperty)); + // @ts-expect-error + const result = new Deferred(); + + when(dimensionsRequest, measuresRequest, hierarchiesRequest, levelsRequest) + .then((dimensionsResponse, measuresResponse, hierarchiesResponse, levelsResponse) => { + this.execXMLA(options, stringFormat(XMLA_DISCOVER_TEMPLATE, catalog, cube, 'MDSCHEMA_MEASUREGROUPS', localeIdProperty)) + .done((measureGroupsResponse) => { + const dimensions = this.parseDimensionsDiscoverRowSet(dimensionsResponse); + const hierarchies = this.parseDiscoverRowSet(hierarchiesResponse, 'HIERARCHY', dimensions); + const levels = this.parseDiscoverRowSet(levelsResponse, 'LEVEL', dimensions); + const measureGroups = this.parseMeasureGroupDiscoverRowSet(measureGroupsResponse); + const fields = this.parseDiscoverRowSet(measuresResponse, 'MEASURE', dimensions, measureGroups) + .concat(hierarchies); + const levelsByHierarchy = {}; + + each(levels, (_, level) => { + levelsByHierarchy[level.hierarchyName] = levelsByHierarchy[level.hierarchyName] + || []; + levelsByHierarchy[level.hierarchyName].push(level); + }); + + each(hierarchies, (_, hierarchy) => { + if (levelsByHierarchy[hierarchy.dataField] + && levelsByHierarchy[hierarchy.dataField].length > 1) { + hierarchy.groupName = hierarchy.hierarchyName = hierarchy.dataField; + + fields.push.apply(fields, levelsByHierarchy[hierarchy.hierarchyName]); + } + }); + result.resolve(fields); + }).fail(result.reject); + }).fail(result.reject); - const load = () => { - if (mdxString) { - when( - sendQuery(storeOptions, mdxString), - rowCountMdx && sendQuery(storeOptions, rowCountMdx), - ) - .done((executeXml, rowCountXml) => { - const error = checkError(executeXml) || rowCountXml && checkError(rowCountXml); - if (!error) { - const response = parseResult(executeXml, parseOptions); - if (rowCountXml) { - processTotalCount(response, options, rowCountXml); - } - - result.resolve(response); - } else { - result.reject(error); + return result; + } + + load(options) { + // @ts-expect-error + const result = new Deferred(); + const storeOptions = this._options; + const parseOptions = { + skipValues: options.skipValues, + }; + const mdxString = this.generateMDX(options, storeOptions.cube, parseOptions); + + let rowCountMdx; + if (options.rowSkip || options.rowTake || options.columnTake || options.columnSkip) { + rowCountMdx = this.generateMDX(extend({}, options, { + totalsOnly: true, + rowSkip: null, + rowTake: null, + columnSkip: null, + columnTake: null, + }), storeOptions.cube, {}); + } + + const load = () => { + if (mdxString) { + when( + this.sendQuery(storeOptions, mdxString), + rowCountMdx && this.sendQuery(storeOptions, rowCountMdx), + ) + .done((executeXml, rowCountXml) => { + const error = this.checkError(executeXml) || rowCountXml && this.checkError(rowCountXml); + if (!error) { + const response = this.parseResult(executeXml, parseOptions); + if (rowCountXml) { + this.processTotalCount(response, options, rowCountXml); } - }).fail(result.reject); - } else { - result.resolve({ - columns: [], - rows: [], - values: [], - grandTotalColumnIndex: 0, - grandTotalRowIndex: 0, - }); - } - }; - if (options.delay) { - setTimeout(load, options.delay); + result.resolve(response); + } else { + result.reject(error); + } + }).fail(result.reject); } else { - load(); + result.resolve({ + columns: [], + rows: [], + values: [], + grandTotalColumnIndex: 0, + grandTotalRowIndex: 0, + }); } + }; - return result; - }, + if (options.delay) { + setTimeout(load, options.delay); + } else { + load(); + } - supportPaging() { - return true; - }, + return result; + } - getDrillDownItems(options, params) { - // @ts-expect-error - const result = new Deferred(); - const storeOptions = this._options; - const mdxString = generateDrillDownMDX(options, storeOptions.cube, params); + supportPaging() { + return true; + } - if (mdxString) { - when(sendQuery(storeOptions, mdxString)).done((executeXml) => { - const error = checkError(executeXml); + getDrillDownItems(options, params) { + // @ts-expect-error + const result = new Deferred(); + const storeOptions = this._options; + const mdxString = this.generateDrillDownMDX(options, storeOptions.cube, params); - if (!error) { - result.resolve(parseDrillDownRowSet(executeXml)); - } else { - result.reject(error); - } - }).fail(result.reject); - } else { - result.resolve([]); - } - return result; - }, + if (mdxString) { + when(this.sendQuery(storeOptions, mdxString)).done((executeXml) => { + const error = this.checkError(executeXml); + + if (!error) { + result.resolve(this.parseDrillDownRowSet(executeXml)); + } else { + result.reject(error); + } + }).fail(result.reject); + } else { + result.resolve([]); + } + return result; + } +} - key: noop, - filter: noop, - }; -})()).include(storeDrillDownMixin); +// Apply storeDrillDownMixin methods to XmlaStore prototype +Object.assign(XmlaStore.prototype, storeDrillDownMixin); // NOTE: Exports default object for mocks in QUnit only. export default { XmlaStore }; diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts index 836723f004c7..5d7cb31de545 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts @@ -9,7 +9,6 @@ import './m_grid_view'; import './module_not_extended/header_panel'; import registerComponent from '@js/core/component_registrator'; -import { isDefined } from '@js/core/utils/type'; import { isMaterialBased } from '@js/ui/themes'; import type { Properties as dxTreeListOptions } from '@js/ui/tree_list'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; @@ -106,11 +105,7 @@ class TreeList extends GridCoreWidget { } public focus(element?) { - super.focus(); - - if (isDefined(element)) { - this.getController('keyboardNavigation').focus(element); - } + this.getController('keyboardNavigation').focus(element); } } diff --git a/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts b/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts index 313a50fb737f..9ff5bb8f1797 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts @@ -543,6 +543,8 @@ const selection = (Base: ModuleType) => class SelectionCont const selectedKeys = this.getSelectedRowKeys(mode) || []; const selectedRowsData: any[] = []; + // @ts-expect-error selection may be deferred only in DataGrid, + // we need to improve GridCore types to take it into account selectedKeys.forEach((key) => { // @ts-expect-error const node = dataController.getNodeByKey(key); diff --git a/packages/devextreme/js/__internal/scheduler/__mock__/appointment_data_accessor.mock.ts b/packages/devextreme/js/__internal/scheduler/__mock__/appointment_data_accessor.mock.ts index 18e22b889c20..80d993c60a60 100644 --- a/packages/devextreme/js/__internal/scheduler/__mock__/appointment_data_accessor.mock.ts +++ b/packages/devextreme/js/__internal/scheduler/__mock__/appointment_data_accessor.mock.ts @@ -15,4 +15,18 @@ export const mockFieldExpressions: IFieldExpr = { visibleExpr: 'visible', }; +export const mockUppercaseFieldExpressions: IFieldExpr = { + startDateExpr: 'StartDate', + endDateExpr: 'EndDate', + startDateTimeZoneExpr: 'StartDateTimeZone', + endDateTimeZoneExpr: 'EndDateTimeZone', + allDayExpr: 'AllDay', + textExpr: 'Text', + descriptionExpr: 'Description', + recurrenceRuleExpr: 'RecurrenceRule', + recurrenceExceptionExpr: 'RecurrenceException', + disabledExpr: 'Disabled', + visibleExpr: 'Visible', +}; + export const mockAppointmentDataAccessor = new AppointmentDataAccessor(mockFieldExpressions, true); diff --git a/packages/devextreme/js/__internal/scheduler/__mock__/resourceManager.mock.ts b/packages/devextreme/js/__internal/scheduler/__mock__/resource_manager.mock.ts similarity index 100% rename from packages/devextreme/js/__internal/scheduler/__mock__/resourceManager.mock.ts rename to packages/devextreme/js/__internal/scheduler/__mock__/resource_manager.mock.ts diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_scheduler.ts index cdc4903b5aaa..7fdc73e2a96a 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_scheduler.ts @@ -1,11 +1,15 @@ import Scheduler from '@ts/scheduler/m_scheduler'; +import { createSchedulerModel, type SchedulerModel } from './model/scheduler'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any type Config = any; export const createScheduler = async (config: Config): Promise<{ container: HTMLDivElement; scheduler: Scheduler; + POM: SchedulerModel; + keydown: (element: Element, key: string) => void; }> => { const container = document.createElement('div'); const scheduler = new Scheduler(container, config); @@ -14,5 +18,9 @@ export const createScheduler = async (config: Config): Promise<{ return { container, scheduler, + POM: createSchedulerModel(container), + keydown: (element: Element, key: string): void => { + element.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })); + }, }; }; diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts index 08645037a394..5dd7dd23dd76 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts @@ -3,25 +3,36 @@ import DOMComponent from '@ts/core/widget/dom_component'; import SchedulerWorkSpace from '../../workspaces/m_work_space'; -export const setupSchedulerTestEnvironment = ( - isTimelineView = false, -): void => { - const cellWidth = 250; - const cellHeight = isTimelineView ? 450 : 80; +interface SetupSchedulerTestEnvironmentOptions { + width?: number; + height?: number; +} - (DOMComponent.prototype as any)._isVisible = jest.fn().mockReturnValue(true); - SchedulerWorkSpace.prototype._createCrossScrollingConfig = () => ({ +export const DEFAULT_CELL_WIDTH = 250; +export const DEFAULT_CELL_HEIGHT = 80; +export const DEFAULT_TIMELINE_CELL_HEIGHT = 450; + +export const setupSchedulerTestEnvironment = ({ + width = DEFAULT_CELL_WIDTH, + height = DEFAULT_CELL_HEIGHT, +}: SetupSchedulerTestEnvironmentOptions = {}): void => { + DOMComponent.prototype._isVisible = jest.fn((): boolean => true); + SchedulerWorkSpace.prototype._createCrossScrollingConfig = (): { + direction: string; + onScroll: jest.Mock; + onEnd: jest.Mock; + } => ({ direction: 'both', onScroll: jest.fn(), onEnd: jest.fn(), }); - Element.prototype.getBoundingClientRect = jest.fn(() => ({ - width: cellWidth, - height: cellHeight, + Element.prototype.getBoundingClientRect = jest.fn((): DOMRect => ({ + width, + height, top: 0, left: 0, - bottom: cellHeight, - right: cellWidth, + bottom: height, + right: width, x: 0, y: 0, toJSON: (): void => {}, diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts new file mode 100644 index 000000000000..fae9b5c8203f --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts @@ -0,0 +1,51 @@ +const getAppointmentColor = (container: HTMLDivElement): string => { + const appointment = container.querySelector('.dx-scheduler-appointment') as HTMLDivElement; + return appointment.style.backgroundColor; +}; +const getAgendaAppointmentColor = (container: HTMLDivElement): string => { + const appointment = container.querySelector('.dx-scheduler-agenda-appointment-marker') as HTMLDivElement; + return appointment.style.backgroundColor; +}; +const getTexts = ( + cells: NodeListOf, +): string[] => Array.from(cells).map((cell) => cell.textContent?.trim() ?? ''); + +export interface SchedulerModel { + getAppointment: () => HTMLDivElement | null; + getAppointments: () => NodeListOf; + getAppointmentColor: (view: string) => string; + getDateTableContent: () => string[]; + getHeaderPanelContent: () => string[]; + getTimePanelContent: () => string[]; + getGroupTableContent: () => string[]; +} + +export const createSchedulerModel = (container: HTMLDivElement): SchedulerModel => ({ + getAppointment(): HTMLDivElement | null { + return container.querySelector('.dx-scheduler-appointment'); + }, + getAppointments(): NodeListOf { + return container.querySelectorAll('.dx-scheduler-appointment'); + }, + getAppointmentColor(view: string): string { + return view === 'agenda' + ? getAgendaAppointmentColor(container) + : getAppointmentColor(container); + }, + getDateTableContent(): string[] { + const cells = container.querySelectorAll('.dx-scheduler-date-table-cell'); + return getTexts(cells); + }, + getHeaderPanelContent(): string[] { + const cells = container.querySelectorAll('.dx-scheduler-header-panel-cell'); + return getTexts(cells); + }, + getTimePanelContent(): string[] { + const cells = container.querySelectorAll('.dx-scheduler-time-panel-cell'); + return getTexts(cells); + }, + getGroupTableContent(): string[] { + const cells = container.querySelectorAll('.dx-scheduler-group-header'); + return getTexts(cells); + }, +}); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts new file mode 100644 index 000000000000..73b9551aa348 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments.test.ts @@ -0,0 +1,25 @@ +import { + describe, expect, it, +} from '@jest/globals'; + +import { createScheduler } from './__mock__/create_scheduler'; +import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; + +describe('Appointments', () => { + it('All-day appointment should not be resizable if current view is "day"', async () => { + setupSchedulerTestEnvironment(); + const { POM } = await createScheduler({ + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + allDay: true, + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + const appointment = POM.getAppointment(); + expect(appointment && !appointment.classList.contains('dx-resizable')).toBe(true); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/resources.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/resources.test.ts index 6b3b3a71217d..7b7dbba973b1 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/resources.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/resources.test.ts @@ -22,31 +22,19 @@ const rooms = [ const rooms2 = [ { id: 1, text: 'Room 2', color: 'rgb(60, 154, 205)' }, ]; -const getAppointmentColor = (container: HTMLDivElement): string => { - const appointment = container.querySelector('.dx-scheduler-appointment') as HTMLDivElement; - return appointment.style.backgroundColor; -}; -const getAgendaAppointmentColor = (container: HTMLDivElement): string => { - const appointment = container.querySelector('.dx-scheduler-agenda-appointment-marker') as HTMLDivElement; - return appointment.style.backgroundColor; -}; describe('Resources', () => { describe.each([ 'month', 'agenda', ])('%s view', (view) => { - const getColor = view === 'agenda' - ? getAgendaAppointmentColor - : getAppointmentColor; - it('should render correct appointment color for remote datasource (T1300252)', async () => { setupSchedulerTestEnvironment(); const dataPromise = new Promise((resolve) => { setTimeout(resolve, 100, rooms); }); - const { container } = await createScheduler({ + const { POM } = await createScheduler({ views: [view], currentView: view, currentDate: new Date(2024, 8, 7), @@ -65,13 +53,13 @@ describe('Resources', () => { await dataPromise; await new Promise(process.nextTick); - expect(getColor(container)).toBe(rooms[0].color); + expect(POM.getAppointmentColor(view)).toBe(rooms[0].color); }); it('should render correct appointment color for local datasource (T1300252)', async () => { setupSchedulerTestEnvironment(); - const { container } = await createScheduler({ + const { POM } = await createScheduler({ views: [view], currentView: view, currentDate: new Date(2024, 8, 7), @@ -83,13 +71,13 @@ describe('Resources', () => { }], }); - expect(getColor(container)).toBe(rooms[0].color); + expect(POM.getAppointmentColor(view)).toBe(rooms[0].color); }); it('should render appointments after resources update (T1301345)', async () => { setupSchedulerTestEnvironment(); - const { container, scheduler } = await createScheduler({ + const { POM, scheduler } = await createScheduler({ views: [view], currentView: view, currentDate: new Date(2024, 8, 7), @@ -107,7 +95,7 @@ describe('Resources', () => { }]); await new Promise(process.nextTick); - expect(getColor(container)).toBe(rooms2[0].color); + expect(POM.getAppointmentColor(view)).toBe(rooms2[0].color); }); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/santiago_timezone.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/santiago_timezone.test.ts index 92f28f5f30bd..99218d32e983 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/santiago_timezone.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/santiago_timezone.test.ts @@ -7,7 +7,7 @@ import { } from '@jest/globals'; import { createScheduler } from './__mock__/create_scheduler'; -import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; +import { DEFAULT_TIMELINE_CELL_HEIGHT, setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; const dataSource = [ { @@ -96,16 +96,11 @@ const views = [ }, ]; -const getAppointments = (container: HTMLElement) => container.querySelectorAll('.dx-scheduler-appointment'); -const getTexts = ( - cells: NodeListOf, -) => Array.from(cells).map((cell) => cell.textContent?.trim()); - describe('scheduler', () => { it.each(views)('should render correct workspace in Santiago DST for view: $view.name', async ({ view, result }) => { - setupSchedulerTestEnvironment(true); + setupSchedulerTestEnvironment({ height: DEFAULT_TIMELINE_CELL_HEIGHT }); - const { container } = await createScheduler({ + const { POM } = await createScheduler({ views: [view], currentView: view.name, currentDate: new Date(2024, 8, 8), @@ -116,18 +111,15 @@ describe('scheduler', () => { }); if (result.hasCellContent) { - const cells = container.querySelectorAll('.dx-scheduler-date-table-cell'); - expect(getTexts(cells)).toMatchSnapshot(); + expect(POM.getDateTableContent()).toMatchSnapshot(); } if (result.hasHeaderPanel) { - const cells = container.querySelectorAll('.dx-scheduler-header-panel-cell'); - expect(getTexts(cells)).toMatchSnapshot(); + expect(POM.getHeaderPanelContent()).toMatchSnapshot(); } if (result.hasTimePanel) { - const cells = container.querySelectorAll('.dx-scheduler-time-panel-cell'); - expect(getTexts(cells)).toMatchSnapshot(); + expect(POM.getTimePanelContent()).toMatchSnapshot(); } - expect(getAppointments(container)).toHaveLength(result.appointmentAmount); + expect(POM.getAppointments()).toHaveLength(result.appointmentAmount); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/views.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/views.test.ts index 50ff97a5865d..3bab0df57ff4 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/views.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/views.test.ts @@ -8,7 +8,7 @@ import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; describe('views', () => { it('should render appointment after view change (T1297019)', async () => { setupSchedulerTestEnvironment(); - const { container, scheduler } = await createScheduler({ + const { POM, scheduler } = await createScheduler({ timeZone: 'Etc/UTC', dataSource: [{ text: 'Appointment', @@ -34,7 +34,39 @@ describe('views', () => { scheduler.option('currentView', 'day'); await new Promise(process.nextTick); - const appointment = container.querySelector('.dx-scheduler-appointment'); + const appointment = POM.getAppointment(); expect(appointment !== null).toBe(true); }); + + it('should render all-day appointment correctly with fractional cell values', async () => { + const FRACTIONAL_CELL_WIDTH = 250.4; + setupSchedulerTestEnvironment({ width: FRACTIONAL_CELL_WIDTH }); + const { container } = await createScheduler({ + dataSource: [{ + text: 'Appointment 2', + startDate: new Date('2021-02-02T15:15:00.000Z'), + endDate: new Date('2021-02-02T17:45:00.000Z'), + allDay: true, + resourceId: 1, + }], + currentDate: new Date(2021, 1, 2), + startDayHour: 8, + endDayHour: 20, + allDayPanelMode: 'hidden', + groups: ['resourceId'], + resources: [{ + label: 'User', + dataSource: [ + { title: 'Mark', id: 1 }, + { title: 'Luke', id: 2 }, + ], + displayExpr: 'title', + fieldExpr: 'resourceId', + valueExpr: 'id', + }], + }); + + const appointments = container.querySelectorAll('.dx-item.dx-scheduler-appointment'); + expect(appointments.length).toBe(1); + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index e4a3a525cc47..297f343f1769 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, jest, } from '@jest/globals'; -import { getResourceManagerMock } from '../__mock__/resourceManager.mock'; +import { getResourceManagerMock } from '../__mock__/resource_manager.mock'; import SchedulerTimelineDay from '../workspaces/m_timeline_day'; import SchedulerTimelineMonth from '../workspaces/m_timeline_month'; import SchedulerTimelineWeek from '../workspaces/m_timeline_week'; diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts index 25c9741b811f..eb2dde826975 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts @@ -49,8 +49,8 @@ const getStylingModeFunc = (): string | undefined => (isFluent(current()) ? 'fil const getStartDateWithStartHour = (startDate, startDayHour) => new Date(new Date(startDate).setHours(startDayHour)); const validateAppointmentFormDate = (editor, value, previousValue) => { - const isCurrentDateCorrect = value === null || !!value; - const isPreviousDateCorrect = previousValue === null || !!previousValue; + const isCurrentDateCorrect = value === null || Boolean(value); + const isPreviousDateCorrect = previousValue === null || Boolean(previousValue); if (!isCurrentDateCorrect && isPreviousDateCorrect) { editor.option('value', previousValue); } diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts index a8e0a55862a8..593f835ce949 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts @@ -161,7 +161,7 @@ export class AppointmentPopup { return { ...rawAppointment, ...rawAppointmentGroupValues, - repeat: !!appointment.recurrenceRule, + repeat: Boolean(appointment.recurrenceRule), }; } @@ -251,7 +251,7 @@ export class AppointmentPopup { const clonedAdapter = adapter .clone() .calculateDates(this.scheduler.getTimeZoneCalculator(), 'fromAppointment'); - const shouldClearRecurrenceRule = !repeat && !!clonedAdapter.recurrenceRule; + const shouldClearRecurrenceRule = !repeat && Boolean(clonedAdapter.recurrenceRule); this._addMissingDSTTime(adapter, clonedAdapter); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts index 0d357306ff4d..614b25527161 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts @@ -24,6 +24,7 @@ import { REDUCED_APPOINTMENT_PARTS_CLASSES, } from '../../m_classes'; import { getRecurrenceProcessor } from '../../m_recurrence'; +import type { SubscribeKey, SubscribeMethods } from '../../m_subscribes'; import type { AppointmentDataAccessor } from '../../utils/data_accessor/appointment_data_accessor'; import type { AppointmentProperties } from './m_types'; import { @@ -72,20 +73,24 @@ export class Appointment extends DOMComponent { }); } - notifyObserver(subject, args) { - const observer = this.option('observer'); - if (observer) { - observer.fire(subject, args); - } + notifyObserver( + funcName: Subject, + args: Parameters, + ): void { + this.invoke(funcName, ...args); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - invoke(funcName: string) { - const observer = this.option('observer'); + invoke( + funcName: Subject, + ...args: Parameters + ): ReturnType | undefined { + const notifyScheduler = this.option('notifyScheduler'); - if (observer) { - return observer.fire.apply(observer, arguments); + if (!notifyScheduler) { + return undefined; } + + return notifyScheduler.invoke(funcName, ...args); } _optionChanged(args) { @@ -255,11 +260,11 @@ export class Appointment extends DOMComponent { } _renderAllDayClass() { - (this.$element() as any).toggleClass(ALL_DAY_APPOINTMENT_CLASS, !!this.option('allDay')); + (this.$element() as any).toggleClass(ALL_DAY_APPOINTMENT_CLASS, Boolean(this.option('allDay'))); } _renderDragSourceClass() { - (this.$element() as any).toggleClass(APPOINTMENT_DRAG_SOURCE_CLASS, !!this.option('isDragSource')); + (this.$element() as any).toggleClass(APPOINTMENT_DRAG_SOURCE_CLASS, Boolean(this.option('isDragSource'))); } _renderRecurrenceClass() { diff --git a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts index b48fb8f7decd..42daf2e45316 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts @@ -1,4 +1,5 @@ import type { Orientation } from '@js/common'; +import type NotifyScheduler from '@ts/scheduler/base/m_widget_notify_scheduler'; import type { TimeZoneCalculator } from '@ts/scheduler/r1/timezone_calculator/calculator'; import type { SafeAppointment } from '@ts/scheduler/types'; import type { AppointmentDataAccessor } from '@ts/scheduler/utils/data_accessor/appointment_data_accessor'; @@ -8,7 +9,7 @@ export interface AppointmentProperties extends Record { data: SafeAppointment; groupIndex?: number; groupTexts: string[]; - observer: any; + notifyScheduler: NotifyScheduler | undefined; geometry: any; direction: Orientation; allowResize: boolean; diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts index 07d215b05bc0..0d4a055ca494 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -28,36 +28,55 @@ import { APPOINTMENT_SETTINGS_KEY } from '../constants'; import { APPOINTMENT_CONTENT_CLASSES, APPOINTMENT_DRAG_SOURCE_CLASS, APPOINTMENT_ITEM_CLASS } from '../m_classes'; import { getRecurrenceProcessor } from '../m_recurrence'; import timeZoneUtils from '../m_utils_time_zone'; -import type { AppointmentViewModel } from '../types'; +import type { CompactAppointmentOptions } from '../types'; import { AppointmentAdapter } from '../utils/appointment_adapter/appointment_adapter'; import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; +import { + getTargetedAppointment, + getTargetedAppointmentFromInfo, +} from '../utils/get_targeted_appointment'; import { getAppointmentGroupValues } from '../utils/resource_manager/appointment_groups_utils'; import { getGroupTexts } from '../utils/resource_manager/group_utils'; +import type { ResourceManager } from '../utils/resource_manager/resource_manager'; +import type { + AppointmentAgendaViewModel, + AppointmentCollectorViewModel, + AppointmentItemViewModel, + AppointmentViewModelPlain, +} from '../view_model/generate_view_model/types'; import { AgendaAppointment } from './appointment/agenda_appointment'; import { Appointment } from './appointment/m_appointment'; import { createAgendaAppointmentLayout, createAppointmentLayout } from './m_appointment_layout'; import { getAppointmentDateRange } from './resizing/m_core'; -import { countVisibleAppointments } from './utils/countVisibleAppointments'; +import { countVisibleAppointments } from './utils/count_visible_appointments'; +import { isNeedToAdd } from './utils/get_arrays_diff'; +import { getViewModelDiff } from './utils/get_view_model_diff'; import { getAppointmentTakesSeveralDays, sortAppointmentsByStartDate } from './utils/m_utils'; +import { getNextElement, getPrevElement } from './utils/sorted_index_utils'; const COMPONENT_CLASS = 'dx-scheduler-scrollable-appointments'; const DBLCLICK_EVENT_NAME = addNamespace(dblclickEvent, 'dxSchedulerAppointment'); const toMs = dateUtils.dateToMilliseconds; -const isAllDayAppointment = ( - appointment: AppointmentViewModel, -): boolean => Boolean(appointment.settings[0]?.allDay); + +interface ViewModelDiff { + item: AppointmentViewModelPlain; + element?: dxElementWrapper; + needToAdd?: true; + needToRemove?: true; +} // @ts-expect-error class SchedulerAppointments extends CollectionWidget { - _virtualAppointments: any; + // NOTE: The key of this array is `sortedIndex` of appointment rendered in Element + renderedElementsBySortedIndex: dxElementWrapper[] = []; _appointmentClickTimeout: any; _$currentAppointment: any; - _currentAppointmentSettings: any; + _currentAppointmentSettings?: AppointmentViewModelPlain; _preventSingleAppointmentClick: any; @@ -73,8 +92,8 @@ class SchedulerAppointments extends CollectionWidget { return this.invoke('isVirtualScrolling'); } - get appointmentDataProvider() { - return this.option('getAppointmentDataProvider')(); + get appointmentDataSource() { + return this.option('getAppointmentDataSource')(); } get dataAccessors(): AppointmentDataAccessor { @@ -85,9 +104,8 @@ class SchedulerAppointments extends CollectionWidget { return countVisibleAppointments(this.option('items') ?? []); } - constructor(element, options) { - super(element, options); - this._virtualAppointments = {}; + getResourceManager(): ResourceManager { + return this.option('getResourceManager')(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -96,18 +114,17 @@ class SchedulerAppointments extends CollectionWidget { } notifyObserver(subject, args) { - const observer: any = this.option('observer'); - if (observer) { - observer.fire(subject, args); + const notifyScheduler: any = this.option('notifyScheduler'); + if (notifyScheduler) { + notifyScheduler.invoke(subject, args); } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars invoke(funcName: string, ...args) { - const observer: any = this.option('observer'); + const notifyScheduler: any = this.option('notifyScheduler'); - if (observer) { - return observer.fire.apply(observer, arguments); + if (notifyScheduler) { + return notifyScheduler.invoke(funcName, ...args); } } @@ -124,14 +141,19 @@ class SchedulerAppointments extends CollectionWidget { const navigatableItems = this._getNavigatableItems(); const focusedItem = navigatableItems.filter('.dx-state-focused'); let index = focusedItem.data(APPOINTMENT_SETTINGS_KEY).sortedIndex; + let $nextAppointment = e.shiftKey + ? getPrevElement(index, this.renderedElementsBySortedIndex) + : getNextElement(index, this.renderedElementsBySortedIndex); const lastIndex = navigatableItems.length - 1; - if ((index > 0 && e.shiftKey) || (index < lastIndex && !e.shiftKey)) { + if ($nextAppointment || (index > 0 && e.shiftKey) || (index < lastIndex && !e.shiftKey)) { e.preventDefault(); - e.shiftKey ? index-- : index++; + if (!$nextAppointment) { + e.shiftKey ? index-- : index++; + $nextAppointment = this._getNavigatableItemByIndex(index); + } - const $nextAppointment = this._getNavigatableItemByIndex(index); this._resetTabIndex($nextAppointment); // @ts-expect-error eventsEngine.trigger($nextAppointment, 'focus'); @@ -171,7 +193,7 @@ class SchedulerAppointments extends CollectionWidget { private _getNavigatableItems(): dxElementWrapper { // @ts-expect-error - const appts = this._itemElements().filter(':visible').not('.dx-state-disabled'); + const appts = this._itemElements().not('.dx-state-disabled'); // @ts-expect-error const apptCollectors = this.$element().find('.dx-scheduler-appointment-collector'); return appts.add(apptCollectors); @@ -222,20 +244,38 @@ class SchedulerAppointments extends CollectionWidget { allowResize: true, allowAllDayResize: true, onAppointmentDblClick: null, - _collectorOffset: 0, groups: [], resources: [], }); } + protected getItemsDiff( + previousValue: AppointmentViewModelPlain[] = [], + value: AppointmentViewModelPlain[] = [], + ): ViewModelDiff[] { + const elementsInRenderOrder = previousValue + .map(({ sortedIndex }) => this.renderedElementsBySortedIndex[sortedIndex]); + const diff = getViewModelDiff(previousValue, value, this.appointmentDataSource); + diff + .filter((item) => !isNeedToAdd(item)) + .forEach((item, index) => { + (item as ViewModelDiff).element = elementsInRenderOrder[index]; + }); + + return diff; + } + _optionChanged(args) { switch (args.name) { case 'items': (this as any)._cleanFocusState(); - this._clearDropDownItems(); - this._clearDropDownItemsElements(); - this._repaintAppointments(args.value); - this._renderDropDownAppointments(); + + if (this.isAgendaView) { + this.forceRepaintAllAppointments(args.value || []); + } else { + const diff = this.getItemsDiff(args.previousValue, args.value); + this.repaintAppointments(diff); + } this._attachAppointmentsEvents(); break; @@ -246,7 +286,9 @@ class SchedulerAppointments extends CollectionWidget { case 'allowDrag': case 'allowResize': case 'allowAllDayResize': - (this as any)._invalidate(); + (this as any)._cleanFocusState(); + this.forceRepaintAllAppointments(this.option('items') || []); + this._attachAppointmentsEvents(); break; case 'focusedElement': this._resetTabIndex($(args.value)); @@ -256,7 +298,7 @@ class SchedulerAppointments extends CollectionWidget { break; case 'focusStateEnabled': this._clearDropDownItemsElements(); - this._renderDropDownAppointments(); + this.renderDropDownAppointments(); super._optionChanged(args); break; @@ -265,39 +307,66 @@ class SchedulerAppointments extends CollectionWidget { } } - _isRepaintAll(appointments: AppointmentViewModel[]): boolean { - return this.isAgendaView || appointments.every((item) => item.needRepaint); - } - _applyFragment(fragment: dxElementWrapper, allDay: boolean): void { if (fragment.children().length > 0) { this._getAppointmentContainer(allDay).append(fragment); } } - _repaintAppointments(appointments: AppointmentViewModel[]): void { + protected forceRepaintAllAppointments(items: AppointmentViewModelPlain[]): void { + this.renderedElementsBySortedIndex = []; + this._renderByFragments(($commonFragment, $allDayFragment) => { + this._getAppointmentContainer(true).html(''); + this._getAppointmentContainer(false).html(''); + if (items.length === 0) { + this._cleanItemContainer(); + } + + items.forEach((item, index) => { + const container = item.allDay + ? $allDayFragment + : $commonFragment; + this._renderItem(index, item, container); + }); + }); + } + + protected repaintAppointments(diff: ViewModelDiff[]): void { + this.renderedElementsBySortedIndex = []; this._renderByFragments(($commonFragment, $allDayFragment) => { - const isRepaintAll = this._isRepaintAll(appointments); + const isRepaintAll = this.isAgendaView + || !diff.some((item) => item.needToAdd === undefined && item.needToRemove === undefined); if (isRepaintAll) { this._getAppointmentContainer(true).html(''); this._getAppointmentContainer(false).html(''); } - if (!appointments.length) { + if (diff.length === 0) { this._cleanItemContainer(); } - appointments.forEach((appointment, index) => { - const container = isAllDayAppointment(appointment) - ? $allDayFragment - : $commonFragment; + diff.forEach((item, index) => { + if (isRepaintAll && item.needToRemove) { + return; + } + + if (item.needToRemove) { + item.element?.detach(); + item.element?.remove(); + return; + } + + if (item.needToAdd) { + const container = item.item.allDay + ? $allDayFragment + : $commonFragment; + this._renderItem(index, item.item, container); + return; + } - if (appointment.needRemove) { - this._clearItem(appointment); - } else if (isRepaintAll || appointment.needRepaint) { - appointment.needRepaint = false; - this._clearItem(appointment); - this._renderItem(index, appointment, container); + if (item.element) { + item.element.data(APPOINTMENT_SETTINGS_KEY, item.item); + this.renderedElementsBySortedIndex[item.item.sortedIndex] = item.element; } }); }); @@ -338,22 +407,6 @@ class SchedulerAppointments extends CollectionWidget { (this as any)._attachHoverEvents(); } - _clearItem(item: AppointmentViewModel): void { - const $items = this._findItemElementByItem(item.itemData); - if (!$items.length) { - return; - } - - each($items, (_, $item: any) => { - $item.detach(); - $item.remove(); - }); - } - - _clearDropDownItems() { - this._virtualAppointments = {}; - } - _clearDropDownItemsElements() { this.invoke('clearCompactAppointments'); } @@ -395,8 +448,6 @@ class SchedulerAppointments extends CollectionWidget { if ($allDayContainer) { $allDayContainer.empty(); } - - this._virtualAppointments = {}; } _clean() { @@ -422,10 +473,20 @@ class SchedulerAppointments extends CollectionWidget { ? appointment.html : undefined, }; + let { targetedAppointmentData } = model; + if (this._currentAppointmentSettings && 'isAgendaModel' in this._currentAppointmentSettings) { + targetedAppointmentData = getTargetedAppointmentFromInfo( + this._currentAppointmentSettings.itemData, + this._currentAppointmentSettings, + this.dataAccessors, + this.getResourceManager(), + ); + } + const formatText = this.invoke( - 'getTextAndFormatDate', - model.appointmentData, - this._currentAppointmentSettings?.agendaSettings || model.targetedAppointmentData, + 'createFormattedDateText', + appointment, + targetedAppointmentData, 'TIME', ); @@ -515,23 +576,26 @@ class SchedulerAppointments extends CollectionWidget { _renderItem( index: number, - item: AppointmentViewModel, + item: AppointmentViewModelPlain, container: dxElementWrapper, - ): dxElementWrapper[] { - const { itemData } = item; - const $items: dxElementWrapper[] = []; - - for (let i = 0; i < item.settings.length; i++) { - const setting = item.settings[i]; - this._currentAppointmentSettings = setting; - const $item = super._renderItem(index, itemData, container); + ): dxElementWrapper { + if ('items' in item) { + return this.renderDropDownAppointment(container, item); + } - $item.data(APPOINTMENT_SETTINGS_KEY, setting); + this._currentAppointmentSettings = item; + const $item = super._renderItem(index, item.itemData, container); - $items.push($item); + $item.data(APPOINTMENT_SETTINGS_KEY, item); + if (item.sortedIndex !== -1) { + // NOTE: fallback for integration testing + if (!this.renderedElementsBySortedIndex) { + this.renderedElementsBySortedIndex = []; + } + this.renderedElementsBySortedIndex[item.sortedIndex] = $item; } - return $items; + return $item; } _getItemContent($itemFrame) { @@ -568,67 +632,89 @@ class SchedulerAppointments extends CollectionWidget { } _postprocessRenderItem(args) { - this._renderAppointment(args.itemElement, this._currentAppointmentSettings); + this.renderAppointment( + args.itemElement, + this._currentAppointmentSettings as AppointmentAgendaViewModel | AppointmentItemViewModel, + ); } - _renderAppointment(element, settings) { + renderAppointment( + element: dxElementWrapper, + settings: AppointmentAgendaViewModel | AppointmentItemViewModel, + ): void { element.data(APPOINTMENT_SETTINGS_KEY, settings); - this._applyResourceDataAttr(element); - const rawAppointment = (this as any)._getItemData(element); - const geometry = this.invoke('getAppointmentGeometry', settings); - - if (settings.virtual) { - const appointmentConfig = { - itemData: rawAppointment, - groupIndex: settings.groupIndex, - groups: this.option('groups'), - }; - const deferredColor = this.option('getAppointmentColor')(appointmentConfig); + if (this.isAgendaView) { + this.renderAgendaAppointment(element, settings as AppointmentAgendaViewModel); + return; + } - this._processVirtualAppointment(settings, element, rawAppointment, deferredColor); - } else { - const allowResize = this.option('allowResize') && (!isDefined(settings.skipResizing) || isString(settings.skipResizing)); - const allowDrag = this.option('allowDrag'); - const { allDay } = settings; - const { groups, groupsLeafs, resourceById } = this.option('getResourceManager')(); - const config: any = { - data: rawAppointment, - groupIndex: settings.groupIndex, - groupTexts: getGroupTexts(groups, groupsLeafs, resourceById, settings.groupIndex), - observer: this.option('observer'), - geometry, - direction: settings.direction || 'vertical', - allowResize, - allowDrag, - allDay, - reduced: settings.appointmentReduced, - isCompact: settings.isCompact, - startDate: new Date(settings.info?.appointment.startDate), - cellWidth: this.invoke('getCellWidth'), - cellHeight: this.invoke('getCellHeight'), - resizableConfig: this._resizableConfig(rawAppointment, settings), - groups: this.option('groups'), - partIndex: settings.partIndex, - partTotalCount: settings.partTotalCount, + this.renderGeneralAppointment(element, settings as AppointmentItemViewModel); + } + + renderAgendaAppointment( + element: dxElementWrapper, + settings: AppointmentAgendaViewModel, + ): void { + const { groups, groupsLeafs, resourceById } = this.getResourceManager(); + const config: any = { + data: settings.itemData, + groupIndex: settings.groupIndex, + groupTexts: getGroupTexts(groups, groupsLeafs, resourceById, settings.groupIndex), + notifyScheduler: this.option('notifyScheduler'), + geometry: settings, + allowResize: false, + allowDrag: false, + groups: this.option('groups'), + + dataAccessors: this.option('dataAccessors'), + timeZoneCalculator: this.option('timeZoneCalculator'), + getResourceManager: this.option('getResourceManager'), + }; - dataAccessors: this.dataAccessors, - timeZoneCalculator: this.option('timeZoneCalculator'), - getResizableStep: this.option('getResizableStep'), - getResourceManager: this.option('getResourceManager'), - }; + (this as any)._createComponent(element, AgendaAppointment, config); + } + + renderGeneralAppointment( + element: dxElementWrapper, + settings: AppointmentItemViewModel, + ): void { + const allowResize = this.option('allowResize') && (!isDefined(settings.skipResizing) || isString(settings.skipResizing)); + const allowDrag = this.option('allowDrag'); + const { allDay } = settings; + const { groups, groupsLeafs, resourceById } = this.getResourceManager(); + const config: any = { + data: settings.itemData, + groupIndex: settings.groupIndex, + groupTexts: getGroupTexts(groups, groupsLeafs, resourceById, settings.groupIndex), + notifyScheduler: this.option('notifyScheduler'), + geometry: settings, + direction: settings.direction || 'vertical', + allowResize, + allowDrag, + allDay, + reduced: settings.reduced, + isCompact: settings.isCompact, + startDate: new Date(settings.info?.appointment.startDate), + cellWidth: this.invoke('getCellWidth'), + cellHeight: this.invoke('getCellHeight'), + resizableConfig: this._resizableConfig(settings.itemData, settings), + groups: this.option('groups'), + partIndex: settings.partIndex, + partTotalCount: settings.partTotalCount, + + dataAccessors: this.option('dataAccessors'), + timeZoneCalculator: this.option('timeZoneCalculator'), + getResizableStep: this.option('getResizableStep'), + getResourceManager: this.option('getResourceManager'), + }; - (this as any)._createComponent( - element, - this.isAgendaView ? AgendaAppointment : Appointment, - config, - ); - } + (this as any)._createComponent(element, Appointment, config); } _applyResourceDataAttr($appointment) { - const { resources } = this.option('getResourceManager')(); + const { resources } = this.getResourceManager(); const rawAppointment = (this as any)._getItemData($appointment); const appointmentGroups = getAppointmentGroupValues(rawAppointment, resources); @@ -692,12 +778,12 @@ class SchedulerAppointments extends CollectionWidget { } else { const startDate = this._getEndResizeAppointmentStartDate(e, sourceAppointment, info.appointment); const { endDate } = info.appointment; - const shiftedStartDate = dateUtilsTs.addOffsets(startDate, [-viewOffset]); - const shiftedEndDate = dateUtilsTs.addOffsets(endDate, [-viewOffset]); + const shiftedStartDate = dateUtilsTs.addOffsets(startDate, -viewOffset); + const shiftedEndDate = dateUtilsTs.addOffsets(endDate, -viewOffset); dateRange = this._getDateRange(e, shiftedStartDate, shiftedEndDate); - dateRange.startDate = dateUtilsTs.addOffsets(dateRange.startDate, [viewOffset]); - dateRange.endDate = dateUtilsTs.addOffsets(dateRange.endDate, [viewOffset]); + dateRange.startDate = dateUtilsTs.addOffsets(dateRange.startDate, viewOffset); + dateRange.endDate = dateUtilsTs.addOffsets(dateRange.endDate, viewOffset); } this.updateResizedAppointment( @@ -758,8 +844,8 @@ class SchedulerAppointments extends CollectionWidget { const startDateDelta = gridAdapter.startDate.getTime() - convertedBackAdapter.startDate.getTime(); const endDateDelta = gridAdapter.endDate.getTime() - convertedBackAdapter.endDate.getTime(); - gridAdapter.startDate = dateUtilsTs.addOffsets(gridAdapter.startDate, [startDateDelta]); - gridAdapter.endDate = dateUtilsTs.addOffsets(gridAdapter.endDate, [endDateDelta]); + gridAdapter.startDate = dateUtilsTs.addOffsets(gridAdapter.startDate, startDateDelta); + gridAdapter.endDate = dateUtilsTs.addOffsets(gridAdapter.endDate, endDateDelta); const data = gridAdapter .calculateDates(timeZoneCalculator, 'fromGrid') @@ -899,71 +985,64 @@ class SchedulerAppointments extends CollectionWidget { return result; } - _processVirtualAppointment(appointmentSetting, $appointment, appointmentData, color) { - const virtualAppointment = appointmentSetting.virtual; - const virtualGroupIndex = virtualAppointment.index; - - if (!isDefined(this._virtualAppointments[virtualGroupIndex])) { - this._virtualAppointments[virtualGroupIndex] = { - coordinates: { - top: virtualAppointment.top, - left: virtualAppointment.left, - }, - items: { data: [], colors: [], settings: [] }, - isAllDay: !!virtualAppointment.isAllDay, - buttonColor: color, - sortedIndex: appointmentSetting.sortedIndex, - }; - } - - appointmentSetting.targetedAppointmentData = this.invoke('getTargetedAppointmentData', appointmentData, $appointment); - - this._virtualAppointments[virtualGroupIndex].items.settings.push(appointmentSetting); - this._virtualAppointments[virtualGroupIndex].items.data.push(appointmentData); - this._virtualAppointments[virtualGroupIndex].items.colors.push(color); - - $appointment.remove(); - } - - _renderContentImpl() { - super._renderContentImpl(); - this._renderDropDownAppointments(); - } - - _renderDropDownAppointments() { + protected renderDropDownAppointments(): void { this._renderByFragments(($commonFragment, $allDayFragment) => { - each(this._virtualAppointments, (groupIndex) => { - const virtualGroup = this._virtualAppointments[groupIndex]; - const virtualItems = virtualGroup.items; - const virtualCoordinates = virtualGroup.coordinates; - const $fragment = virtualGroup.isAllDay ? $allDayFragment : $commonFragment; - const { left } = virtualCoordinates; - const buttonWidth = this.invoke('getDropDownAppointmentWidth', virtualGroup.isAllDay); - const buttonHeight = this.invoke('getDropDownAppointmentHeight'); - const rtlOffset = this.option('rtlEnabled') ? buttonWidth : 0; - - this.notifyObserver('renderCompactAppointments', { - $container: $fragment, - coordinates: { - top: virtualCoordinates.top, - left: left + rtlOffset, - }, - items: virtualItems, - buttonColor: virtualGroup.buttonColor, - sortedIndex: virtualGroup.sortedIndex, - width: buttonWidth - this.option('_collectorOffset'), - height: buttonHeight, - onAppointmentClick: this.option('onItemClick'), - allowDrag: this.option('allowDrag'), - cellWidth: this.invoke('getCellWidth'), - isCompact: this.invoke('isAdaptive') || this._isGroupCompact(virtualGroup), - }); + const items: AppointmentViewModelPlain[] = this.option('items') || []; + items.forEach((item) => { + if ('items' in item) { + const $fragment = item.allDay ? $allDayFragment : $commonFragment; + this.renderDropDownAppointment($fragment, item); + } }); }); } - _isGroupCompact(virtualGroup) { - return !virtualGroup.isAllDay && this.invoke('supportCompactDropDownAppointments'); + protected renderDropDownAppointment( + $fragment: dxElementWrapper, + appointment: AppointmentCollectorViewModel, + ): dxElementWrapper { + const virtualItems = appointment.items; + const items: CompactAppointmentOptions['items'] = []; + virtualItems.forEach((item) => { + const appointmentConfig = { + itemData: item.itemData, + groupIndex: appointment.groupIndex, + groups: this.option('groups'), + }; + const resourceManager = this.getResourceManager(); + + items.push({ + appointment: item.itemData, + targetedAppointment: getTargetedAppointment( + item.itemData, + item, + this.dataAccessors, + this.option('timeZoneCalculator'), + resourceManager, + ), + color: resourceManager.getAppointmentColor(appointmentConfig), + settings: item, + }); + }); + + const $item = this.invoke('renderCompactAppointments', { + $container: $fragment, + coordinates: { + top: appointment.top, + left: appointment.left, + }, + items, + buttonColor: items[0].color, + sortedIndex: appointment.sortedIndex, + width: appointment.width, + height: appointment.height, + onAppointmentClick: this.option('onItemClick'), + allowDrag: this.option('allowDrag'), + isCompact: appointment.isCompact, + }); + this.renderedElementsBySortedIndex[appointment.sortedIndex] = $item; + + return $item; } _sortAppointmentsByStartDate(appointments) { diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_layout.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_layout.ts index 00d1e4700d78..984229540f04 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_layout.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_layout.ts @@ -50,7 +50,6 @@ export const createAgendaAppointmentLayout = (formatText, config) => { .addClass('dx-scheduler-agenda-appointment-right-layout') .appendTo(result); - // eslint-disable-next-line no-unused-vars const marker = $('
') .addClass(APPOINTMENT_CONTENT_CLASSES.AGENDA_MARKER) .appendTo(leftLayoutContainer); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/resizing/get_delta_time.test.ts b/packages/devextreme/js/__internal/scheduler/appointments/resizing/get_delta_time.test.ts new file mode 100644 index 000000000000..dbe57be82787 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/get_delta_time.test.ts @@ -0,0 +1,87 @@ +import { + describe, expect, it, +} from '@jest/globals'; + +import { VIEW_TYPES } from '../../utils/options/constants_view'; +import type { ViewType } from '../../utils/options/types'; +import { getDeltaTime } from './get_delta_time'; + +describe('getDeltaTime', () => { + VIEW_TYPES.forEach((view) => { + it(`should return zero for not resized appointment in ${view} view`, () => { + expect(getDeltaTime( + { width: 100, height: 100 }, + { width: 100, height: 100 }, + { + viewType: view, + cellSize: { width: 50, height: 50 }, + resizableStep: 50, + cellDurationInMinutes: 30, + isAllDay: true, + }, + )).toBe(0); + }); + }); + + ['day', 'week', 'workWeek'].forEach((view) => { + it(`should return correct delta in px for resized appointment in vertical ${view} view`, () => { + expect(getDeltaTime( + { width: 100, height: 50 }, + { width: 100, height: 100 }, + { + viewType: view as ViewType, + cellSize: { width: 50, height: 50 }, + resizableStep: 50, + cellDurationInMinutes: 30, + isAllDay: false, + }, + )).toBe(-30 * 60_000); + }); + + it(`should return correct delta in px for resized all day appointment in vertical ${view} view`, () => { + expect(getDeltaTime( + { width: 50, height: 100 }, + { width: 100, height: 100 }, + { + viewType: view as ViewType, + cellSize: { width: 50, height: 50 }, + resizableStep: 50, + cellDurationInMinutes: 30, + isAllDay: true, + }, + )).toBe(-24 * 3600_000); + }); + }); + + ['timelineMonth', 'month'].forEach((view) => { + it(`should return correct delta in px for resized appointment in ${view} view`, () => { + expect(getDeltaTime( + { width: 50, height: 100 }, + { width: 100, height: 100 }, + { + viewType: view as ViewType, + cellSize: { width: 50, height: 50 }, + resizableStep: 50, + cellDurationInMinutes: 30, + isAllDay: false, + }, + )).toBe(-24 * 3600_000); + }); + }); + + ['timelineDay', 'timelineWeek', 'timelineWorkWeek'].forEach((view) => { + it(`should return zero for not resized appointment in horizontal ${view} view`, () => { + expect(getDeltaTime( + { width: 50, height: 100 }, + { width: 100, height: 100 }, + { + viewType: view as ViewType, + cellSize: { width: 50, height: 50 }, + resizableStep: 50, + cellDurationInMinutes: 30, + isAllDay: false, + }, + )).toBe(-30 * 60_000); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/resizing/get_delta_time.ts b/packages/devextreme/js/__internal/scheduler/appointments/resizing/get_delta_time.ts new file mode 100644 index 000000000000..1f2682b5aaed --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/get_delta_time.ts @@ -0,0 +1,60 @@ +import dateUtils from '@js/core/utils/date'; + +import { VERTICAL_VIEW_TYPES } from '../../constants'; +import type { ViewType } from '../../types'; + +const toMs = dateUtils.dateToMilliseconds; + +interface Size { + width: number; + height: number; +} +interface Options { + viewType: ViewType; + cellSize: Size; + cellDurationInMinutes: number; + resizableStep: number; + isAllDay: boolean; +} + +const MIN_RESIZABLE_STEP = 2; +const getAllDayDeltaWidth = (args: Size, initialSize: Size, resizableStep: number): number => { + const intervalWidth = resizableStep || MIN_RESIZABLE_STEP; + const initialWidth = initialSize.width; + + return Math.round((args.width - initialWidth) / intervalWidth); +}; +const getHorizontalDeltaTime = (args: Size, initialSize: Size, { + cellSize, + cellDurationInMinutes, +}: Options): number => { + const deltaWidth = args.width - initialSize.width; + const deltaTime = toMs('minute') * Math.round((deltaWidth * cellDurationInMinutes) / cellSize.width); + return deltaTime; +}; +const getVerticalDeltaTime = (args: Size, initialSize: Size, { + cellSize, + cellDurationInMinutes, +}: Options): number => { + const deltaHeight = args.height - initialSize.height; + const deltaTime = toMs('minute') * Math.round((deltaHeight * cellDurationInMinutes) / cellSize.height); + return deltaTime; +}; + +export const getDeltaTime = ( + args: Size, + initialSize: Size, + options: Options, +): number => { + const { viewType, resizableStep, isAllDay } = options; + switch (true) { + case ['timelineMonth', 'month'].includes(viewType) || Boolean(isAllDay): + return getAllDayDeltaWidth(args, initialSize, resizableStep) * toMs('day'); + case viewType === 'agenda': + return 0; + case VERTICAL_VIEW_TYPES.includes(viewType) && !isAllDay: + return getVerticalDeltaTime(args, initialSize, options); + default: + return getHorizontalDeltaTime(args, initialSize, options); + } +}; diff --git a/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts b/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts index ecade3444554..a235bc0515b4 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts @@ -32,7 +32,7 @@ const getCellData = ( // but long appointments are not. So for all day appointments endDate === startDate, // for long appointments endDate = startDate + 1 day. if (!isAllDay) { - cellData.endDate = dateUtilsTs.addOffsets(cellData.startDate, [toMs('day')]); + cellData.endDate = dateUtilsTs.addOffsets(cellData.startDate, toMs('day')); } return cellData; @@ -173,12 +173,11 @@ const getAppointmentCellsInfo = (options: GetAppointmentDateRangeOptions): Cells ? [DOMMetaData.allDayPanelCellsMeta] : DOMMetaData.dateTableCellsMeta; - const { positionByMap } = appointmentSettings; const { height: cellHeight, width: cellWidth, - } = DOMMetaTable[positionByMap.rowIndex][positionByMap.columnIndex]; - const cellCountInRow = DOMMetaTable[positionByMap.rowIndex].length; + } = DOMMetaTable[appointmentSettings.rowIndex][appointmentSettings.columnIndex]; + const cellCountInRow = DOMMetaTable[appointmentSettings.rowIndex].length; return { cellWidth, diff --git a/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts b/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts index 5137d3faf857..743ec8db2ba4 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts @@ -1,6 +1,7 @@ import type { TimeZoneCalculator } from '../../r1/timezone_calculator'; -import type { SafeAppointment, ViewDataProviderType } from '../../types'; +import type { ViewDataProviderType } from '../../types'; import type { AppointmentDataAccessor } from '../../utils/data_accessor/appointment_data_accessor'; +import type { AppointmentItemViewModel } from '../../view_model/generate_view_model/types'; export type Rect = Pick; @@ -9,16 +10,7 @@ export interface GetAppointmentDateRangeOptions { left: boolean; right: boolean; }; - appointmentSettings: { - allDay: boolean; - info: SafeAppointment & { - appointment: SafeAppointment; - }; - positionByMap: { - rowIndex: number; - columnIndex: number; - }; - }; + appointmentSettings: AppointmentItemViewModel; isVerticalGroupedWorkSpace: boolean; appointmentRect: Rect; parentAppointmentRect: Rect; diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/countVisibleAppointments.test.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/countVisibleAppointments.test.ts deleted file mode 100644 index 3c2511146f69..000000000000 --- a/packages/devextreme/js/__internal/scheduler/appointments/utils/countVisibleAppointments.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; - -import { countVisibleAppointments } from './countVisibleAppointments'; - -describe('countVisibleAppointments', () => { - it('should return correct number of visible appointments', () => { - expect(countVisibleAppointments([ - { needRepaint: true, needRemove: false, settings: [{}, {}, {}] }, - { needRepaint: true, needRemove: true, settings: [{}, {}, {}] }, - { needRepaint: true, needRemove: false, settings: [{}] }, - ])).toBe(4); - }); - - it('should return correct number of visible appointments with parts', () => { - expect(countVisibleAppointments([ - { - needRepaint: true, - needRemove: false, - settings: [ - { partIndex: 1, partTotalCount: 2 }, - {}, - { partIndex: 0, partTotalCount: 2 }, - { partIndex: 1, partTotalCount: 2 }, - {}, - { partIndex: 0, partTotalCount: 2 }, - ], - }, - { needRepaint: true, needRemove: true, settings: [{}, {}, {}] }, - { - needRepaint: true, - needRemove: false, - settings: [ - { partIndex: 0, partTotalCount: 2 }], - }, - ])).toBe(6); - }); -}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/countVisibleAppointments.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/countVisibleAppointments.ts deleted file mode 100644 index 8dcc07b40980..000000000000 --- a/packages/devextreme/js/__internal/scheduler/appointments/utils/countVisibleAppointments.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { isDefined } from '@ts/core/utils/m_type'; - -interface SettingsItem { - partIndex?: number; - partTotalCount?: number; -} -interface Item { - needRepaint: boolean; - needRemove: boolean; - settings: SettingsItem[]; -} - -const countVisibleRepeats = (settings: SettingsItem[]): number => { - let isPreviousPart = false; - - return settings.reduce((total, settingsItem) => { - const result = isPreviousPart ? total : total + 1; - const { partIndex, partTotalCount } = settingsItem; - - isPreviousPart = isDefined(partTotalCount) && partIndex !== (partTotalCount - 1); - - return result; - }, 0); -}; - -export const countVisibleAppointments = (items: Item[]): number => items - .filter(({ needRemove }) => !needRemove) - .reduce((total, item) => total + countVisibleRepeats(item.settings), 0); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/count_visible_appointments.test.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/count_visible_appointments.test.ts new file mode 100644 index 000000000000..8b3544a53845 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/utils/count_visible_appointments.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from '@jest/globals'; + +import { countVisibleAppointments } from './count_visible_appointments'; + +const createAppointment = (date: number) => ({ + startDate: new Date(date), + endDate: new Date(date + 1), +}); + +describe('countVisibleAppointments', () => { + it('should return correct number of agenda appointments', () => { + expect(countVisibleAppointments([ + { itemData: 1, isAgendaModel: true } as any, + { itemData: 2, isAgendaModel: true } as any, + { itemData: 3, isAgendaModel: true } as any, + ])).toBe(3); + }); + + it('should return correct number of appointments with collectors', () => { + expect(countVisibleAppointments([ + { itemData: 1, info: { appointment: createAppointment(1) } } as any, + { + itemData: 2, + items: [ + { itemData: 3, info: { appointment: createAppointment(2) } } as any, + { itemData: 4, info: { appointment: createAppointment(3) } } as any, + ], + } as any, + ])).toBe(3); + }); + + it('should return correct number of appointments with parts', () => { + expect(countVisibleAppointments([ + { + itemData: 1, + info: { appointment: createAppointment(1) }, + partIndex: 0, + partTotalCount: 2, + } as any, + { + itemData: 1, + info: { appointment: createAppointment(1) }, + partIndex: 1, + partTotalCount: 2, + } as any, + { itemData: 2, info: { appointment: createAppointment(1) } } as any, + { itemData: 3, info: { appointment: createAppointment(2) } } as any, + ])).toBe(3); + }); + + it('should return correct number of appointments with parts and collectors', () => { + expect(countVisibleAppointments([ + { + itemData: 3, + items: [ + { + itemData: 3, + info: { appointment: createAppointment(2) }, + partIndex: 0, + partTotalCount: 2, + } as any, + ], + } as any, + { + itemData: 1, + info: { appointment: createAppointment(1) }, + partIndex: 0, + partTotalCount: 3, + } as any, + { + itemData: 1, + info: { appointment: createAppointment(1) }, + partIndex: 1, + partTotalCount: 3, + } as any, + { itemData: 2, info: { appointment: createAppointment(1) } } as any, + { + itemData: 3, + info: { appointment: createAppointment(2) }, + partIndex: 1, + partTotalCount: 2, + } as any, + { + itemData: 2, + items: [ + { itemData: 4, info: { appointment: createAppointment(1) } } as any, + { itemData: 5, info: { appointment: createAppointment(2) } } as any, + { + itemData: 1, + info: { appointment: createAppointment(1) }, + partIndex: 2, + partTotalCount: 3, + } as any, + { itemData: 6, info: { appointment: createAppointment(3) } } as any, + ], + } as any, + ])).toBe(6); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/count_visible_appointments.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/count_visible_appointments.ts new file mode 100644 index 000000000000..2a16329e3d2d --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/utils/count_visible_appointments.ts @@ -0,0 +1,40 @@ +import type { SafeAppointment } from '../../types'; +import type { + AppointmentItemViewModel, + AppointmentViewModelPlain, +} from '../../view_model/generate_view_model/types'; + +export const countVisibleAppointments = (items: AppointmentViewModelPlain[]): number => { + const alreadyCountedPartHash = new Map(); + const countPart = (item: AppointmentItemViewModel): boolean => { + if (!item.partTotalCount) { + return true; + } + + const key = `${item.info.appointment.startDate.getTime()}${item.info.appointment.endDate.getTime()}`; + const savedItems = alreadyCountedPartHash.get(key) ?? []; + + if (savedItems.includes(item.itemData)) { + return false; + } + + alreadyCountedPartHash.set(key, [...savedItems, item.itemData]); + return true; + }; + + return items.reduce((count, item) => { + if ('items' in item) { + return count + item.items.filter(countPart).length; + } + + if ('isAgendaModel' in item) { + return count + 1; + } + + if ('info' in item && !countPart(item)) { + return count; + } + + return count + 1; + }, 0); +}; diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/get_arrays_diff.test.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/get_arrays_diff.test.ts new file mode 100644 index 000000000000..cd4eaa0500a6 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/utils/get_arrays_diff.test.ts @@ -0,0 +1,205 @@ +import { + describe, expect, it, +} from '@jest/globals'; + +import { getArraysDiff, isNeedToAdd, isNeedToRemove } from './get_arrays_diff'; + +interface Obj { id: number; name: string } + +const compare = (a: Obj, b: Obj): boolean => a.id === b.id && a.name === b.name; + +const getOperations = (items: ReturnType>): string => items + .map((item) => { + if (isNeedToAdd(item)) { + return '+'; + } + return isNeedToRemove(item) ? '-' : '='; + }) + .join(''); + +describe('getArraysDiff', () => { + it('should process both empty arrays', () => { + const diff = getArraysDiff([], [], compare); + expect(diff).toEqual([]); + }); + + it('should no mark for no changes', () => { + const a: Obj[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 3, name: 'C' }, + ]; + const b: Obj[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 3, name: 'C' }, + ]; + + const diff = getArraysDiff(a, b, compare); + + expect(getOperations(diff)).toBe('==='); + expect(diff).toEqual([ + { item: a[0] }, + { item: a[1] }, + { item: a[2] }, + ]); + }); + + it('should mark insertion from empty to something', () => { + const a: Obj[] = []; + const b: Obj[] = [ + { id: 10, name: 'X' }, + { id: 11, name: 'Y' }, + ]; + + const diff = getArraysDiff(a, b, compare); + + expect(getOperations(diff)).toBe('++'); + expect(diff).toEqual([ + { item: b[0], needToAdd: true }, + { item: b[1], needToAdd: true }, + ]); + }); + + it('should removal from something to empty', () => { + const a: Obj[] = [ + { id: 5, name: 'A' }, + { id: 6, name: 'B' }, + ]; + const b: Obj[] = []; + + const diff = getArraysDiff(a, b, compare); + + expect(getOperations(diff)).toBe('--'); + expect(diff).toEqual([ + { item: a[0], needToRemove: true }, + { item: a[1], needToRemove: true }, + ]); + }); + + it('should mark remove and add for one object replacement', () => { + const a: Obj[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 4, name: 'D' }, + ]; + const b: Obj[] = [ + { id: 1, name: 'A' }, + { id: 3, name: 'C' }, + { id: 4, name: 'D' }, + ]; + + const diff = getArraysDiff(a, b, compare); + + expect(getOperations(diff)).toBe('=+-='); + expect(diff).toEqual([ + { item: a[0] }, + { item: b[1], needToAdd: true }, + { item: a[1], needToRemove: true }, + { item: a[2] }, + ]); + }); + + it('should mark remove and add for changes inside object', () => { + const a: Obj[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 4, name: 'D' }, + ]; + const b: Obj[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'C' }, + { id: 4, name: 'D' }, + ]; + + const diff = getArraysDiff(a, b, compare); + + expect(getOperations(diff)).toBe('=+-='); + expect(diff).toEqual([ + { item: a[0] }, + { item: b[1], needToAdd: true }, + { item: a[1], needToRemove: true }, + { item: a[2] }, + ]); + }); + + it('should choose optimum operations for reordering', () => { + const a: Obj[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 3, name: 'C' }, + { id: 4, name: 'D' }, + ]; + const b: Obj[] = [ + { id: 4, name: 'D' }, + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 3, name: 'C' }, + ]; + + const diff = getArraysDiff(a, b, compare); + + expect(getOperations(diff)).toBe('+===-'); + expect(diff).toEqual([ + { item: b[0], needToAdd: true }, + { item: a[0] }, + { item: a[1] }, + { item: a[2] }, + { item: a[3], needToRemove: true }, + ]); + }); + + it('should choose optimum operations for reordering, insertion and removal', () => { + const a: Obj[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 3, name: 'C' }, + { id: 4, name: 'D' }, + ]; + const b: Obj[] = [ + { id: 4, name: 'D' }, + { id: 1, name: 'A' }, + { id: 5, name: 'E' }, + { id: 3, name: 'C' }, + ]; + + const diff = getArraysDiff(a, b, compare); + + expect(getOperations(diff)).toBe('+=+-=-'); + expect(diff).toEqual([ + { item: b[0], needToAdd: true }, + { item: a[0] }, + { item: b[2], needToAdd: true }, + { item: a[1], needToRemove: true }, + { item: a[2] }, + { item: a[3], needToRemove: true }, + ]); + }); + + it('should save additional props in second object', () => { + const a: Obj[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 3, name: 'C' }, + { id: 4, name: 'D' }, + ]; + const b: (Obj & { extra: number })[] = [ + { id: 4, name: 'D', extra: 10 }, + { id: 1, name: 'A', extra: 20 }, + { id: 5, name: 'E', extra: 30 }, + { id: 3, name: 'C', extra: 40 }, + ]; + + const diff = getArraysDiff(a, b, compare); + + expect(getOperations(diff)).toBe('+=+-=-'); + expect(diff).toEqual([ + { item: b[0], needToAdd: true }, + { item: b[1] }, + { item: b[2], needToAdd: true }, + { item: a[1], needToRemove: true }, + { item: b[3] }, + { item: a[3], needToRemove: true }, + ]); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/get_arrays_diff.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/get_arrays_diff.ts new file mode 100644 index 000000000000..3aaac3902732 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/utils/get_arrays_diff.ts @@ -0,0 +1,66 @@ +interface NoChanges { item: A } +interface ToRemove { item: A; needToRemove: true } +interface ToAdd { item: B; needToAdd: true } + +export type DiffItem = + | NoChanges + | ToRemove + | ToAdd; + +export const isNeedToRemove = ( + item: DiffItem, +): item is ToRemove => (item as ToRemove).needToRemove; + +export const isNeedToAdd = ( + item: DiffItem, +): item is ToAdd => (item as ToAdd).needToAdd; + +export function getArraysDiff( + a: A[], + b: B[], + equal: (x: A, y: B) => boolean, +): DiffItem[] { + const n = a.length; + const m = b.length; + + const dp: number[][] = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); + + for (let i = 1; i <= n; i += 1) { + const ai = a[i - 1]; + for (let j = 1; j <= m; j += 1) { + dp[i][j] = equal(ai, b[j - 1]) + ? dp[i - 1][j - 1] + 1 + : Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + + const result: DiffItem[] = []; + let i = n; + let j = m; + + while (i > 0 && j > 0) { + if (equal(a[i - 1], b[j - 1])) { + result.push({ item: b[j - 1] }); + i -= 1; + j -= 1; + } else if (dp[i - 1][j] >= dp[i][j - 1]) { + result.push({ item: a[i - 1], needToRemove: true }); + i -= 1; + } else { + result.push({ item: b[j - 1], needToAdd: true }); + j -= 1; + } + } + + while (i > 0) { + result.push({ item: a[i - 1], needToRemove: true }); + i -= 1; + } + while (j > 0) { + result.push({ item: b[j - 1], needToAdd: true }); + j -= 1; + } + + result.reverse(); + return result; +} diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/get_view_model_diff.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/get_view_model_diff.ts new file mode 100644 index 000000000000..31982210c34c --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/utils/get_view_model_diff.ts @@ -0,0 +1,68 @@ +import { equalByValue } from '@js/core/utils/common'; + +import type { SafeAppointment } from '../../types'; +import type { AppointmentDataSource } from '../../view_model/generate_view_model/data_provider/m_appointment_data_source'; +import type { AppointmentViewModelPlain } from '../../view_model/generate_view_model/types'; +import type { DiffItem } from './get_arrays_diff'; +import { getArraysDiff } from './get_arrays_diff'; + +const getObjectToCompare = ( + item: AppointmentViewModelPlain, +): object => { + if ('isAgendaModel' in item) { + return {}; + } + + if ('items' in item) { + return { + allDay: item.allDay, + groupIndex: item.groupIndex, + top: item.top, + left: item.left, + items: item.items.length, + }; + } + + return { + allDay: item.allDay, + groupIndex: item.groupIndex, + direction: item.direction, + left: item.left, + top: item.top, + height: item.height, + width: item.width, + reduced: item.reduced, + partIndex: item.partIndex, + partTotalCount: item.partTotalCount, + rowIndex: item.rowIndex, + columnIndex: item.columnIndex, + }; +}; + +const isDataChanged = ( + data: SafeAppointment, + appointmentDataSource: AppointmentDataSource, +): boolean => { + const updatedData = appointmentDataSource.getUpdatedAppointment(); + + return updatedData === data || appointmentDataSource + .getUpdatedAppointmentKeys() + .some((item) => data[item.key] === item.value); +}; + +const compareViewModel = (appointmentDataSource: AppointmentDataSource) => ( + viewModelOld: AppointmentViewModelPlain, + viewModelNext: AppointmentViewModelPlain, +): boolean => viewModelOld.itemData === viewModelNext.itemData + && !isDataChanged(viewModelNext.itemData, appointmentDataSource) + && equalByValue(getObjectToCompare(viewModelOld), getObjectToCompare(viewModelNext)); + +export const getViewModelDiff = ( + viewModelOld: AppointmentViewModelPlain[], + viewModelNext: AppointmentViewModelPlain[], + appointmentDataSource: AppointmentDataSource, +): DiffItem[] => getArraysDiff( + viewModelOld, + viewModelNext, + compareViewModel(appointmentDataSource), +); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/sorted_index_utils.test.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/sorted_index_utils.test.ts new file mode 100644 index 000000000000..dea90ab102e7 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/utils/sorted_index_utils.test.ts @@ -0,0 +1,97 @@ +import { + describe, expect, it, jest, +} from '@jest/globals'; +import $ from '@js/core/renderer'; + +import { getNextElement, getPrevElement, isElementCanBeFocused } from './sorted_index_utils'; + +const createContainer = () => { + const container = document.createElement('div'); + const $element = $(container); + jest.spyOn($element, 'is').mockImplementation((selector) => selector === ':visible'); + return $element; +}; +const createDisabledContainer = () => { + const $container = createContainer(); + $container.addClass('dx-state-disabled'); + return $container; +}; + +describe('sorted index utils', () => { + describe('isElementCanBeFocused', () => { + it('should return true for pure div', () => { + expect(isElementCanBeFocused(createContainer())).toBe(true); + }); + + it('should return false for invisible div', () => { + const container = document.createElement('div'); + expect(isElementCanBeFocused($(container))).toBe(false); + }); + + it('should return false for disabled div', () => { + expect(isElementCanBeFocused(createDisabledContainer())).toBe(false); + }); + }); + + describe('getPrevElement', () => { + it('should return prev element', () => { + const elements = [ + createContainer(), + createContainer(), + createContainer(), + ]; + + expect(getPrevElement(2, elements)).toBe(elements[1]); + }); + + it('should return prev element that exist and not disabled', () => { + const elements = [ + createContainer(), + undefined, + createDisabledContainer(), + createContainer(), + ]; + + expect(getPrevElement(3, elements as any)).toBe(elements[0]); + }); + + it('should return undefined', () => { + const elements = [ + createContainer(), + ]; + + expect(getPrevElement(0, elements)).toBe(undefined); + }); + }); + + describe('getNextElement', () => { + it('should return next element', () => { + const elements = [ + createContainer(), + createContainer(), + createContainer(), + ]; + + expect(getNextElement(2, elements)).toBe(elements[3]); + }); + + it('should return next element that exist and not disabled', () => { + const elements = [ + createContainer(), + undefined, + createDisabledContainer(), + createContainer(), + ]; + + expect(getNextElement(0, elements as any)).toBe(elements[3]); + }); + + it('should return undefined', () => { + const elements = [ + createContainer(), + ]; + + expect(getNextElement(0, elements)).toBe(undefined); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/utils/sorted_index_utils.ts b/packages/devextreme/js/__internal/scheduler/appointments/utils/sorted_index_utils.ts new file mode 100644 index 000000000000..8e78379255a0 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/utils/sorted_index_utils.ts @@ -0,0 +1,37 @@ +import type { dxElementWrapper } from '@js/core/renderer'; + +export const isElementCanBeFocused = ($element: dxElementWrapper): boolean => Boolean( + $element && $element.is(':visible') && !$element.hasClass('dx-state-disabled'), +); + +export const getPrevElement = ( + sortedIndex: number, + renderedElementsBySortedIndex: dxElementWrapper[] = [], +): dxElementWrapper | undefined => { + let index = sortedIndex - 1; + while (index >= 0) { + const $nextElement = renderedElementsBySortedIndex[index]; + if (isElementCanBeFocused($nextElement)) { + return $nextElement; + } + index -= 1; + } + + return undefined; +}; + +export const getNextElement = ( + sortedIndex: number, + renderedElementsBySortedIndex: dxElementWrapper[] = [], +): dxElementWrapper | undefined => { + let index = sortedIndex + 1; + while (index < renderedElementsBySortedIndex.length) { + const $nextElement = renderedElementsBySortedIndex[index]; + if (isElementCanBeFocused($nextElement)) { + return $nextElement; + } + index += 1; + } + + return undefined; +}; diff --git a/packages/devextreme/js/__internal/scheduler/base/m_widget_notify_scheduler.ts b/packages/devextreme/js/__internal/scheduler/base/m_widget_notify_scheduler.ts new file mode 100644 index 000000000000..8312f57b98ef --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/base/m_widget_notify_scheduler.ts @@ -0,0 +1,19 @@ +import type Scheduler from '../m_scheduler'; +import type { SubscribeKey, SubscribeMethods } from '../m_subscribes'; + +class NotifyScheduler { + scheduler: Scheduler; + + constructor({ scheduler }: { scheduler: Scheduler }) { + this.scheduler = scheduler; + } + + invoke( + funcName: Subject, + ...args: Parameters + ): ReturnType { + return this.scheduler.fire(funcName, ...args); + } +} + +export default NotifyScheduler; diff --git a/packages/devextreme/js/__internal/scheduler/base/m_widget_observer.ts b/packages/devextreme/js/__internal/scheduler/base/m_widget_observer.ts deleted file mode 100644 index 4a79561d25f4..000000000000 --- a/packages/devextreme/js/__internal/scheduler/base/m_widget_observer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Widget from '@js/ui/widget/ui.widget'; - -class WidgetObserver extends Widget { - notifyObserver(subject, args) { - const observer = this.option('observer') as any; - - if (observer) { - observer.fire(subject, args); - } - } - - invoke() { - const observer = this.option('observer') as any; - - if (observer) { - return observer.fire.apply(observer, arguments); - } - } -} - -export default WidgetObserver; diff --git a/packages/devextreme/js/__internal/scheduler/constants.ts b/packages/devextreme/js/__internal/scheduler/constants.ts index 2cb419677572..5842b1ab5d58 100644 --- a/packages/devextreme/js/__internal/scheduler/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/constants.ts @@ -4,3 +4,5 @@ export const APPOINTMENT_SETTINGS_KEY = 'dxAppointmentSettings'; export const VERTICAL_GROUP_ORIENTATION = 'vertical'; export const HORIZONTAL_GROUP_ORIENTATION = 'horizontal'; + +export const VERTICAL_VIEW_TYPES = ['day', 'week', 'workWeek']; diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index dacacb004559..285827c1a6d7 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -163,7 +163,7 @@ export const getNextIntervalDate = (options, direction: Direction): Date => { // eslint-disable-next-line default-case switch (step) { case 'day': - dayDuration = 1 * intervalCount; + dayDuration = Number(intervalCount); break; case 'week': case 'workWeek': diff --git a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts index 9264760039dd..6d8d0447ac8a 100644 --- a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts +++ b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts @@ -5,6 +5,7 @@ import Draggable from '@js/ui/draggable'; import { APPOINTMENT_SETTINGS_KEY, LIST_ITEM_DATA_KEY } from './constants'; import { isSchedulerComponent } from './utils/is_scheduler_component'; +import type { AppointmentViewModelPlain } from './view_model/generate_view_model/types'; const APPOINTMENT_ITEM_CLASS = 'dx-scheduler-appointment'; @@ -85,9 +86,9 @@ export default class AppointmentDragBehavior { return itemDataFromTooltip || itemDataFromGrid; } - getItemSettings(appointment) { + getItemSettings(appointment): AppointmentViewModelPlain | undefined { const itemData: any = $(appointment).data(LIST_ITEM_DATA_KEY); - return itemData?.settings || []; + return itemData?.settings; } createDragStartHandler(options, appointmentDragging) { diff --git a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts index a2f533bb9ae4..19e7db48c384 100644 --- a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts +++ b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts @@ -1,35 +1,32 @@ import { locate, move } from '@js/common/core/animation/translator'; import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; -import $ from '@js/core/renderer'; +import $, { type dxElementWrapper } from '@js/core/renderer'; import { FunctionTemplate } from '@js/core/templates/function_template'; import Button from '@js/ui/button'; import { APPOINTMENT_SETTINGS_KEY, LIST_ITEM_CLASS, LIST_ITEM_DATA_KEY } from './constants'; -import { AppointmentTooltipInfo } from './m_data_structures'; +import type { AppointmentTooltipItem, CompactAppointmentOptions } from './types'; const APPOINTMENT_COLLECTOR_CLASS = 'dx-scheduler-appointment-collector'; const COMPACT_APPOINTMENT_COLLECTOR_CLASS = `${APPOINTMENT_COLLECTOR_CLASS}-compact`; const APPOINTMENT_COLLECTOR_CONTENT_CLASS = `${APPOINTMENT_COLLECTOR_CLASS}-content`; -const WEEK_VIEW_COLLECTOR_OFFSET = 5; -const COMPACT_THEME_WEEK_VIEW_COLLECTOR_OFFSET = 1; - export class CompactAppointmentsHelper { elements: any[] = []; constructor(public instance) { } - render(options) { + render(options: CompactAppointmentOptions): dxElementWrapper { const { isCompact, items } = options; - const template = this._createTemplate(items.data.length, isCompact); + const template = this._createTemplate(items.length, isCompact); const button = this._createCompactButton(template, options); const $button = button.$element(); this.elements.push($button); - $button.data('items', this._createTooltipInfos(items)); + $button.data('items', items); return $button; } @@ -42,21 +39,7 @@ export class CompactAppointmentsHelper { this.elements = []; } - _createTooltipInfos(items) { - return items.data.map((appointment, index) => { - const targeted = { ...appointment }; - - if (items.settings?.length > 0) { - const { info } = items.settings[index]; - this.instance._dataAccessors.set('startDate', targeted, info.sourceAppointment.startDate); - this.instance._dataAccessors.set('endDate', targeted, info.sourceAppointment.endDate); - } - - return new AppointmentTooltipInfo(appointment, targeted, items.colors[index], items.settings[index]); - }); - } - - _onButtonClick(e, options) { + _onButtonClick(e, options: CompactAppointmentOptions) { const $button = $(e.element); this.instance.showAppointmentTooltipCore( $button, @@ -65,7 +48,7 @@ export class CompactAppointmentsHelper { ); } - _getExtraOptionsForTooltip(options, $appointmentCollector) { + _getExtraOptionsForTooltip(options: CompactAppointmentOptions, $appointmentCollector) { return { clickEvent: this._clickEvent(options.onAppointmentClick).bind(this), dragBehavior: options.allowDrag && this._createTooltipDragBehavior($appointmentCollector).bind(this), @@ -104,16 +87,6 @@ export class CompactAppointmentsHelper { }; } - _getCollectorOffset(width, cellWidth) { - return cellWidth - width - this._getCollectorRightOffset(); - } - - _getCollectorRightOffset() { - return this.instance.getRenderingStrategyInstance()._isCompactTheme() - ? COMPACT_THEME_WEEK_VIEW_COLLECTOR_OFFSET - : WEEK_VIEW_COLLECTOR_OFFSET; - } - _setPosition(element, position) { move(element, { top: position.top, @@ -121,7 +94,7 @@ export class CompactAppointmentsHelper { }); } - _createCompactButton(template, options) { + _createCompactButton(template, options: CompactAppointmentOptions) { const $button = this._createCompactButtonElement(options); return this.instance._createComponent($button, Button, { @@ -135,8 +108,8 @@ export class CompactAppointmentsHelper { _createCompactButtonElement({ isCompact, $container, coordinates, sortedIndex, items, - }) { - const appointmentDate = this._getDateText(items.data[0]); + }: CompactAppointmentOptions) { + const appointmentDate = this._getDateText(items[0].appointment); const result = $('
') .addClass(APPOINTMENT_COLLECTOR_CLASS) .attr('aria-roledescription', appointmentDate) @@ -150,11 +123,11 @@ export class CompactAppointmentsHelper { return result; } - _renderTemplate(template, items, isCompact) { + _renderTemplate(template, items: AppointmentTooltipItem[], isCompact) { return new (FunctionTemplate as any)((options) => template.render({ model: { - appointmentCount: items.data.length, - items: items.data, + appointmentCount: items.length, + items: items.map((item) => item.appointment), isCompact, }, container: options.container, diff --git a/packages/devextreme/js/__internal/scheduler/m_data_structures.ts b/packages/devextreme/js/__internal/scheduler/m_data_structures.ts deleted file mode 100644 index 4b62d3d1e767..000000000000 --- a/packages/devextreme/js/__internal/scheduler/m_data_structures.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable @typescript-eslint/no-extraneous-class */ -export class AppointmentTooltipInfo { - constructor( - public appointment: any, - public targetedAppointment: any = undefined, - public color: any[] = [], - public settings: any[] = [], - ) { // TODO - } -} diff --git a/packages/devextreme/js/__internal/scheduler/m_date_adapter.ts b/packages/devextreme/js/__internal/scheduler/m_date_adapter.ts deleted file mode 100644 index a1da2e6928ea..000000000000 --- a/packages/devextreme/js/__internal/scheduler/m_date_adapter.ts +++ /dev/null @@ -1,60 +0,0 @@ -import dateUtils from '@js/core/utils/date'; - -const toMs = dateUtils.dateToMilliseconds; - -class DateAdapterCore { - _source: Date; - - constructor(source) { - this._source = new Date(source.getTime ? source.getTime() : source); - } - - get source() { // TODO - return this._source; - } - - result() { - return this._source; - } - - getTimezoneOffset(format: any = undefined) { - const value = this._source.getTimezoneOffset(); - if (format === 'minute') { - return value * toMs('minute'); - } - return value; - } - - getTime() { - return this._source.getTime(); - } - - setTime(value) { - this._source.setTime(value); - return this; - } - - addTime(value) { - this._source.setTime(this._source.getTime() + value); - return this; - } - - setMinutes(value) { - this._source.setMinutes(value); - return this; - } - - addMinutes(value) { - this._source.setMinutes(this._source.getMinutes() + value); - return this; - } - - subtractMinutes(value) { - this._source.setMinutes(this._source.getMinutes() - value); - return this; - } -} - -const DateAdapter = (date) => new DateAdapterCore(date); - -export default DateAdapter; diff --git a/packages/devextreme/js/__internal/scheduler/m_publisher_mixin.ts b/packages/devextreme/js/__internal/scheduler/m_publisher_mixin.ts deleted file mode 100644 index 10c67212df1b..000000000000 --- a/packages/devextreme/js/__internal/scheduler/m_publisher_mixin.ts +++ /dev/null @@ -1,16 +0,0 @@ -const publisherMixin = { - notifyObserver(subject, args) { - const observer = this.option('observer'); - if (observer) { - observer.fire(subject, args); - } - }, - invoke() { - const observer = this.option('observer'); - - if (observer) { - return observer.fire.apply(observer, arguments); - } - }, -}; -export default publisherMixin; diff --git a/packages/devextreme/js/__internal/scheduler/m_recurrence.ts b/packages/devextreme/js/__internal/scheduler/m_recurrence.ts index 11579b31383f..b98981e00c23 100644 --- a/packages/devextreme/js/__internal/scheduler/m_recurrence.ts +++ b/packages/devextreme/js/__internal/scheduler/m_recurrence.ts @@ -2,11 +2,13 @@ import errors from '@js/core/errors'; import dateUtils from '@js/core/utils/date'; import { each } from '@js/core/utils/iterator'; +import { dateUtilsTs } from '@ts/core/utils/date'; import { RRule, RRuleSet } from 'rrule'; import timeZoneUtils from './m_utils_time_zone'; const toMs = dateUtils.dateToMilliseconds; +const { addOffsets } = dateUtilsTs; const ruleNames = ['freq', 'interval', 'byday', 'byweekno', 'byyearday', 'bymonth', 'bymonthday', 'count', 'until', 'byhour', 'byminute', 'bysecond', 'bysetpos', 'wkst']; const freqNames = ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY', 'SECONDLY', 'MINUTELY', 'HOURLY']; @@ -81,11 +83,11 @@ class RecurrenceProcessor { const duration = options.end ? options.end.getTime() - options.start.getTime() : 0; // NOTE: Remove local timezone offsets from Rrule date params. - const startIntervalDate = timeZoneUtils.setOffsetsToDate(options.start, [-clientOffsets.startDate, appointmentTimezoneOffset]); + const startIntervalDate = addOffsets(options.start, -clientOffsets.startDate, appointmentTimezoneOffset); const minViewTime = options.min.getTime() - clientOffsets.minViewDate + appointmentTimezoneOffset; // NOTE: Shift minViewDate, because recurrent appointment may start before start view date. const minViewDate = new Date(minViewTime - duration); - const maxViewDate = timeZoneUtils.setOffsetsToDate(options.max, [-clientOffsets.maxViewDate, appointmentTimezoneOffset]); + const maxViewDate = addOffsets(options.max, -clientOffsets.maxViewDate, appointmentTimezoneOffset); // NOTE: Check DST after start date without local timezone offset conversion. const startDateDSTDifferenceMs = timeZoneUtils.getDiffBetweenClientTimezoneOffsets(options.start, startIntervalDate); @@ -102,14 +104,15 @@ class RecurrenceProcessor { } _convertRruleResult(rruleIntervalParams, options, rruleDate) { - const convertedBackDate = timeZoneUtils.setOffsetsToDate(rruleDate, [ + const convertedBackDate = addOffsets( + rruleDate, ...this._getLocalMachineOffset(rruleDate), -options.appointmentTimezoneOffset, rruleIntervalParams.startIntervalDateDSTShift, - ]); + ); const convertedDateDSTShift = timeZoneUtils.getDiffBetweenClientTimezoneOffsets(convertedBackDate, rruleDate); const switchToSummerTime = convertedDateDSTShift < 0; - const resultDate = timeZoneUtils.setOffsetsToDate(convertedBackDate, [convertedDateDSTShift]); + const resultDate = addOffsets(convertedBackDate, convertedDateDSTShift); const resultDateDSTShift = timeZoneUtils.getDiffBetweenClientTimezoneOffsets(resultDate, convertedBackDate); if (resultDateDSTShift && switchToSummerTime) { @@ -142,7 +145,7 @@ class RecurrenceProcessor { } hasRecurrence(options) { - return !!this.generateDates(options).length; + return Boolean(this.generateDates(options).length); } evalRecurrenceRule(rule) { @@ -176,8 +179,8 @@ class RecurrenceProcessor { return result.map((item) => { const match = item.match(/[A-Za-z]+/); - return !!match && match[0]; - }).filter((item) => !!item); + return Boolean(match) && match[0]; + }).filter((item) => Boolean(item)); } getAsciiStringByDate(date) { @@ -281,9 +284,10 @@ class RecurrenceProcessor { } if (until) { - ruleOptions.until = timeZoneUtils.setOffsetsToDate( + ruleOptions.until = addOffsets( until, - [-timeZoneUtils.getClientTimezoneOffset(until), options.appointmentTimezoneOffset], + -timeZoneUtils.getClientTimezoneOffset(until), + options.appointmentTimezoneOffset, ); } @@ -299,9 +303,9 @@ class RecurrenceProcessor { const rruleTimezoneOffsets = typeof options.getExceptionDateTimezoneOffsets === 'function' ? options.getExceptionDateTimezoneOffsets(date) : [-timeZoneUtils.getClientTimezoneOffset(date), options.appointmentTimezoneOffset]; - const exceptionDateInPseudoUtc = timeZoneUtils.setOffsetsToDate( + const exceptionDateInPseudoUtc = addOffsets( date, - rruleTimezoneOffsets, + ...rruleTimezoneOffsets, ); this.rRuleSet!.exdate(exceptionDateInPseudoUtc); diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index df5238d29e16..5266bc80c9e7 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -37,15 +37,17 @@ import { getA11yStatusText } from './a11y_status/a11y_status_text'; import { AppointmentForm } from './appointment_popup/m_form'; import { ACTION_TO_APPOINTMENT, AppointmentPopup } from './appointment_popup/m_popup'; import AppointmentCollection from './appointments/m_appointment_collection'; +import NotifyScheduler from './base/m_widget_notify_scheduler'; import { SchedulerHeader } from './header/m_header'; import type { HeaderOptions } from './header/types'; import { CompactAppointmentsHelper } from './m_compact_appointments_helper'; -import { AppointmentTooltipInfo } from './m_data_structures'; import { hide as hideLoading, show as showLoading } from './m_loading'; import { getRecurrenceProcessor } from './m_recurrence'; +import type { SubscribeKey, SubscribeMethods } from './m_subscribes'; import subscribes from './m_subscribes'; import { utils } from './m_utils'; import timeZoneUtils, { type TimezoneLabel } from './m_utils_time_zone'; +import { combineRemoteFilter } from './r1/filterting/remote'; import { createTimeZoneCalculator } from './r1/timezone_calculator/index'; import { excludeFromRecurrence, @@ -57,19 +59,24 @@ import { import { SchedulerOptionsBaseWidget } from './scheduler_options_base_widget'; import { DesktopTooltipStrategy } from './tooltip_strategies/m_desktop_tooltip_strategy'; import { MobileTooltipStrategy } from './tooltip_strategies/m_mobile_tooltip_strategy'; -import type { AppointmentViewModel } from './types'; +import type { + AppointmentTooltipItem, + SafeAppointment, + TargetedAppointment, +} from './types'; import { AppointmentAdapter } from './utils/appointment_adapter/appointment_adapter'; import { AppointmentDataAccessor } from './utils/data_accessor/appointment_data_accessor'; +import { getTargetedAppointment } from './utils/get_targeted_appointment'; import type { IFieldExpr } from './utils/index'; import { macroTaskArray } from './utils/index'; import { isAgendaWorkspaceComponent } from './utils/is_agenda_workpace_component'; import { VIEWS } from './utils/options/constants_view'; import type { NormalizedView } from './utils/options/types'; import { setAppointmentGroupValues } from './utils/resource_manager/appointment_groups_utils'; -import { getLeafGroupValues } from './utils/resource_manager/group_utils'; import { createResourceEditorModel } from './utils/resource_manager/popup_utils'; import { ResourceManager } from './utils/resource_manager/resource_manager'; -import { AppointmentDataProvider } from './view_model/generate_view_model/data_provider/m_appointment_data_provider'; +import { AppointmentDataSource } from './view_model/generate_view_model/data_provider/m_appointment_data_source'; +import type { AppointmentViewModelPlain } from './view_model/generate_view_model/types'; import AppointmentLayoutManager from './view_model/m_appointments_layout_manager'; import SchedulerAgenda from './workspaces/m_agenda'; import SchedulerTimelineDay from './workspaces/m_timeline_day'; @@ -163,7 +170,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { _appointments: any; - appointmentDataProvider!: AppointmentDataProvider; + appointmentDataSource!: AppointmentDataSource; _dataSource: any; @@ -175,7 +182,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { _createActionByOption: any; - _appointmentTooltip: any; + _appointmentTooltip!: MobileTooltipStrategy | DesktopTooltipStrategy; _readyToRenderAppointments?: boolean; @@ -193,6 +200,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { _subscribes: any; + _notifyScheduler!: NotifyScheduler; + _recurrenceDialog: any; _layoutManager!: AppointmentLayoutManager; @@ -231,9 +240,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { // @ts-expect-error const resolveCallbacks = new Deferred(); - whenLoaded.done((groupsResources) => { - this.option('loadedResources', groupsResources); - resolveCallbacks.resolve(groupsResources); + whenLoaded.done(() => { + resolveCallbacks.resolve(); }); this._postponeDataSourceLoading(whenLoaded); @@ -271,7 +279,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._initDataSource(); this._postponeResourceLoading().done(() => { - this.appointmentDataProvider.setDataSource(this._dataSource); + this.appointmentDataSource.setDataSource(this._dataSource); this._filterAppointmentsByDate(); this._updateOption('workSpace', 'showAllDayPanel', this.option('showAllDayPanel')); }); @@ -331,7 +339,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { case 'resources': this.resourceManager?.dispose(); this.resourceManager = new ResourceManager(this.option('resources')); - this.updateInstances(); + this.updateAppointmentDataSource(); this._postponeResourceLoading().done(() => { this._appointments.option('items', []); @@ -342,7 +350,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { break; case 'startDayHour': case 'endDayHour': - this.updateInstances(); + this.updateAppointmentDataSource(); this._appointments.option('items', []); this._updateOption('workSpace', name, value); @@ -354,7 +362,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { // TODO Vinogradov refactoring: merge it with startDayHour / endDayHour case 'offset': - this.updateInstances(); + this.updateAppointmentDataSource(); this._appointments.option('items', []); this._updateOption('workSpace', 'viewOffset', this.normalizeViewOffsetValue(value)); @@ -449,7 +457,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { break; } case 'showAllDayPanel': - this.updateInstances(); + this.updateAppointmentDataSource(); this.repaint(); break; case 'showCurrentTimeIndicator': @@ -470,7 +478,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { case 'recurrenceEditMode': case 'remoteFiltering': case 'timeZone': - this.updateInstances(); + this.updateAppointmentDataSource(); this.repaint(); break; case 'dropDownAppointmentTemplate': @@ -496,8 +504,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { case 'recurrenceExceptionExpr': case 'disabledExpr': this._updateExpression(name, value); - this.appointmentDataProvider.updateDataAccessors(this._dataAccessors); - this._initAppointmentTemplate(); this.repaint(); break; @@ -511,7 +517,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._updateOption('workSpace', args.fullName, value); break; case 'allDayPanelMode': - this.updateInstances(); + this.updateAppointmentDataSource(); this._updateOption('workSpace', args.fullName, value); break; case 'renovateRender': @@ -525,8 +531,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { ? this._header.onToolbarOptionChanged(args.fullName, value) : this.repaint(); break; - case 'loadedResources': - break; default: // @ts-expect-error super._optionChanged(args); @@ -582,7 +586,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const startDate = this.timeZoneCalculator.createDate(dateRange[0], 'fromGrid'); const endDate = this.timeZoneCalculator.createDate(dateRange[1], 'fromGrid'); - this.appointmentDataProvider.filterByDate( + this.setRemoteFilter( startDate, endDate, this.option('remoteFiltering'), @@ -590,6 +594,27 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } + setRemoteFilter(min, max, remoteFiltering = false, dateSerializationFormat?) { + const dataSource = this._dataSource; + const dataAccessors = this._dataAccessors; + + if (!dataSource || !remoteFiltering) { + return; + } + + const dataSourceFilter = dataSource.filter(); + const filter = combineRemoteFilter({ + dataSourceFilter, + dataAccessors, + min, + max, + dateSerializationFormat, + forceIsoDateParsing: config().forceIsoDateParsing, + }); + + dataSource.filter(filter); + } + _reloadDataSource() { // @ts-expect-error const result = new Deferred(); @@ -726,7 +751,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._initEditing(); - this.updateInstances(); + this.updateAppointmentDataSource(); this._initActions(); @@ -739,37 +764,20 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._subscribes = subscribes; this.resourceManager = new ResourceManager(this.option('resources')); + + this._notifyScheduler = new NotifyScheduler({ scheduler: this }); } - createAppointmentDataProvider() { - this.appointmentDataProvider?.destroy(); - this.appointmentDataProvider = new AppointmentDataProvider({ - dataSource: this._dataSource, - dataAccessors: this._dataAccessors, - timeZoneCalculator: this.timeZoneCalculator, - dateSerializationFormat: this.option('dateSerializationFormat'), - resources: this.option('resources'), - startDayHour: () => this.getViewOption('startDayHour'), - endDayHour: () => this.getViewOption('endDayHour'), - viewOffset: () => this.getViewOffsetMs(), - allDayPanelMode: () => this.getViewOption('allDayPanelMode'), - showAllDayPanel: () => this.option('showAllDayPanel'), - getResourceManager: () => this.resourceManager, - getIsVirtualScrolling: () => this.isVirtualScrolling(), - getSupportAllDayRow: () => this._workSpace.supportAllDayRow(), - getViewType: () => this._workSpace.type, - getViewDirection: () => this._workSpace.viewDirection, - getDateRange: () => this._workSpace.getDateRange(), - getGroupCount: () => this._workSpace._getGroupCount(), - getViewDataProvider: () => this._workSpace.viewDataProvider, - }); + createAppointmentDataSource() { + this.appointmentDataSource?.destroy(); + this.appointmentDataSource = new AppointmentDataSource(this._dataSource); } - updateInstances() { + updateAppointmentDataSource() { this._timeZoneCalculator = null; if (this.getWorkSpace()) { - this.createAppointmentDataProvider(); + this.createAppointmentDataSource(); } } @@ -867,21 +875,17 @@ class Scheduler extends SchedulerOptionsBaseWidget { workspace.option('allDayExpanded', this._isAllDayExpanded()); // @ts-expect-error - const viewModel: AppointmentViewModel[] = this._isVisible() + const viewModel: AppointmentViewModelPlain[] = this._isVisible() ? this._getAppointmentsToRepaint() : []; this._appointments.option('items', viewModel); - this.appointmentDataProvider.cleanState(); + this.appointmentDataSource.cleanState(); } - _getAppointmentsToRepaint(): AppointmentViewModel[] { + _getAppointmentsToRepaint(): AppointmentViewModelPlain[] { const appointmentsMap = this._layoutManager.createAppointmentsMap(); - - return this._layoutManager.getRepaintedAppointments( - appointmentsMap, - this.getAppointmentsInstance().option('items'), - ); + return appointmentsMap; } _initExpressions(fields: IFieldExpr) { @@ -900,11 +904,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { const editing = this.option('editing'); this._editing = { - allowAdding: !!editing, - allowUpdating: !!editing, - allowDeleting: !!editing, - allowResizing: !!editing, - allowDragging: !!editing, + allowAdding: Boolean(editing), + allowUpdating: Boolean(editing), + allowDeleting: Boolean(editing), + allowResizing: Boolean(editing), + allowDragging: Boolean(editing), }; if (isObject(editing)) { @@ -982,9 +986,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.setAria({ role: 'group' }); } - _initMarkupOnResourceLoaded(groupsResources) { + _initMarkupOnResourceLoaded() { if (!(this as any)._disposed) { - this.option('loadedResources', groupsResources); this._initMarkupCore(); this._reloadDataSource(); } @@ -1019,9 +1022,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { if (groups?.length) { this.resourceManager.loadGroupResources(groups, true) - .then((groupsResources) => this._initMarkupOnResourceLoaded(groupsResources)); + .then(() => this._initMarkupOnResourceLoaded()); } else { - this._initMarkupOnResourceLoaded([]); + this._initMarkupOnResourceLoaded(); } } } @@ -1100,7 +1103,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { checkAndDeleteAppointment: that.checkAndDeleteAppointment.bind(that), isAppointmentInAllDayPanel: that.isAppointmentInAllDayPanel.bind(that), - createFormattedDateText: (appointment, targetedAppointment, format) => (this.fire as any)('getTextAndFormatDate', appointment, targetedAppointment, format), + createFormattedDateText: (appointment, targetedAppointment, format) => this.fire('createFormattedDateText', appointment, targetedAppointment, format), getAppointmentDisabled: (appointment) => this._dataAccessors.get('disabled', appointment), onItemContextMenu: that._createActionByOption('onAppointmentContextMenu'), createEventArgs: that._createEventArgs.bind(that), @@ -1167,7 +1170,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); this._waitAsyncTemplate(() => this._workSpaceRecalculation?.resolve()); - this.createAppointmentDataProvider(); + this.createAppointmentDataSource(); this._filterAppointmentsByDate(); this._validateKeyFieldIfAgendaExist(); this._updateA11yStatus(); @@ -1227,11 +1230,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { _appointmentsConfig() { const config = { getResourceManager: () => this.resourceManager, - getAppointmentColor: this.createGetAppointmentColor(), - getAppointmentDataProvider: () => this.appointmentDataProvider, + getAppointmentDataSource: () => this.appointmentDataSource, dataAccessors: this._dataAccessors, - observer: this, + notifyScheduler: this._notifyScheduler, onItemRendered: this._getAppointmentRenderedAction(), onItemClick: this._createActionByOption('onAppointmentClick'), onItemContextMenu: this._createActionByOption('onAppointmentContextMenu'), @@ -1299,8 +1301,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { if (currentViewOptions.startDate) { this._updateOption('header', 'currentDate', this._workSpace._getHeaderDate()); } - - this._appointments.option('_collectorOffset', this.getCollectorOffset()); } _recalculateWorkspace() { @@ -1313,7 +1313,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { } _workSpaceConfig(currentViewOptions: NormalizedView) { - const groupsResources = this.option('loadedResources'); const scrolling = this.getViewOption('scrolling'); const isVirtualScrolling = scrolling.mode === 'virtual'; const horizontalVirtualScrollingAllowed = isVirtualScrolling @@ -1371,8 +1370,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { renovateRender: this._isRenovatedRender(isVirtualScrolling), }, currentViewOptions); - result.observer = this; - result.groups = groupsResources; + result.notifyScheduler = this._notifyScheduler; + result.groups = this.resourceManager.groupResources(); result.onCellClick = this._createActionByOption('onCellClick'); result.onCellContextMenu = this._createActionByOption('onCellContextMenu'); result.currentDate = this.getViewOption('currentDate'); @@ -1382,7 +1381,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { result.timeCellTemplate = result.timeCellTemplate ? this._getTemplate(result.timeCellTemplate) : null; result.resourceCellTemplate = result.resourceCellTemplate ? this._getTemplate(result.resourceCellTemplate) : null; result.dateCellTemplate = result.dateCellTemplate ? this._getTemplate(result.dateCellTemplate) : null; - result.getAppointmentDataProvider = () => this.appointmentDataProvider; return result; } @@ -1519,7 +1517,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { delete singleRawAppointment[this._dataAccessors.expr.recurrenceExceptionExpr]; delete singleRawAppointment[this._dataAccessors.expr.recurrenceRuleExpr]; - const keyPropertyName = this.appointmentDataProvider.keyName; + const keyPropertyName = this.appointmentDataSource.keyName; delete singleRawAppointment[keyPropertyName]; /* eslint-enable @typescript-eslint/no-dynamic-delete */ @@ -1599,7 +1597,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } const result = this.timeZoneCalculator.createDate(date, 'fromGrid'); - return dateUtilsTs.addOffsets(result, [-viewOffset]); + return dateUtilsTs.addOffsets(result, -viewOffset); }; const targetCell = this.getTargetCellData(); @@ -1612,9 +1610,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { const cellEndDate = getConvertedFromGrid(targetCell.endDate); let appointmentStartDate = new Date(appointment.startDate); - appointmentStartDate = dateUtilsTs.addOffsets(appointmentStartDate, [-viewOffset]); + appointmentStartDate = dateUtilsTs.addOffsets(appointmentStartDate, -viewOffset); let appointmentEndDate = new Date(appointment.endDate); - appointmentEndDate = dateUtilsTs.addOffsets(appointmentEndDate, [-viewOffset]); + appointmentEndDate = dateUtilsTs.addOffsets(appointmentEndDate, -viewOffset); let resultedStartDate = cellStartDate ?? appointmentStartDate; if (!dateUtilsTs.isValidDate(appointmentStartDate)) { @@ -1635,7 +1633,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const startDate = this.timeZoneCalculator.createDate(appointmentStartDate, 'toGrid'); const timeInMs = startDate.getTime() - dateUtils.trimTime(startDate).getTime(); - const targetCellStartDate = dateUtilsTs.addOffsets(targetCell.startDate, [-viewOffset]); + const targetCellStartDate = dateUtilsTs.addOffsets(targetCell.startDate, -viewOffset); resultedStartDate = new Date(dateUtils.trimTime(targetCellStartDate).getTime() + timeInMs); resultedStartDate = this.timeZoneCalculator.createDate(resultedStartDate, 'fromGrid'); } @@ -1667,8 +1665,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - result.startDate = dateUtilsTs.addOffsets(result.startDate, [viewOffset]); - result.endDate = dateUtilsTs.addOffsets(resultedEndDate, [viewOffset]); + result.startDate = dateUtilsTs.addOffsets(result.startDate, viewOffset); + result.endDate = dateUtilsTs.addOffsets(resultedEndDate, viewOffset); const rawResult = result.source; setAppointmentGroupValues(rawResult, this.resourceManager.resourceById, targetCell.groups); @@ -1676,55 +1674,32 @@ class Scheduler extends SchedulerOptionsBaseWidget { return rawResult; } - getTargetedAppointment(appointment, element) { - const settings: any = utils.dataAccessors.getAppointmentSettings(element); - const info = utils.dataAccessors.getAppointmentInfo(element); - - const appointmentIndex = $(element).data(this._appointments._itemIndexKey()); - - const adapter = new AppointmentAdapter( + getTargetedAppointment(appointment: SafeAppointment, element: dxElementWrapper): TargetedAppointment { + const settings = utils.dataAccessors.getAppointmentSettings(element)!; + return getTargetedAppointment( appointment, + settings, this._dataAccessors, + this.timeZoneCalculator, + this.resourceManager, ); - - const targetedAdapter = adapter.clone(); - - if (this._isAgenda() && adapter.isRecurrent) { - const { agendaSettings } = settings; - - targetedAdapter.startDate = this._dataAccessors.get('startDate', agendaSettings); - targetedAdapter.endDate = this._dataAccessors.get('endDate', agendaSettings); - } else if (settings) { - targetedAdapter.startDate = info ? info.sourceAppointment.startDate : adapter.startDate; // TODO: in agenda we haven't info field - targetedAdapter.endDate = info ? info.sourceAppointment.endDate : adapter.endDate; - } - - const rawTargetedAppointment = targetedAdapter.source; - if (element) { - this.setTargetedAppointmentResources(rawTargetedAppointment, element, appointmentIndex); - } - - if (info) { - rawTargetedAppointment.displayStartDate = new Date(info.appointment.startDate); - rawTargetedAppointment.displayEndDate = new Date(info.appointment.endDate); - } - - return rawTargetedAppointment; } subscribe(subject, action) { this._subscribes[subject] = subscribes[subject] = action; } - fire(subject) { + fire( + subject: Subject, + ...args: Parameters + ): ReturnType { const callback = this._subscribes[subject]; - const args = Array.prototype.slice.call(arguments); if (!isFunction(callback)) { throw errors.Error('E1031', subject); } - return callback.apply(this, args.slice(1)); + return callback.call(this, ...args); } getTargetCellData() { @@ -1763,7 +1738,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._expandAllDayPanel(rawAppointment); try { - deferred = this.appointmentDataProvider + deferred = this.appointmentDataSource .update(target, rawAppointment) .done(() => { dragEvent?.cancel.resolve(false); @@ -1838,10 +1813,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._layoutManager; } - getRenderingStrategyInstance() { - return this._layoutManager.getRenderingStrategyInstance(); - } - getActions() { return this._actions; } @@ -1889,20 +1860,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { return startDateTimeStamp <= dayTimeStamp && dayTimeStamp <= endDateTimeStamp; } - setTargetedAppointmentResources(rawAppointment, element, appointmentIndex) { - const groups = this.getViewOption('groups'); - - if (groups?.length) { - const { resourceById, groupsLeafs } = this.resourceManager; - const appointmentSettings = this._isAgenda() - ? this._layoutManager._positionMap[appointmentIndex][0] - : utils.dataAccessors.getAppointmentSettings(element) || {}; - const cellGroups = getLeafGroupValues(groupsLeafs, appointmentSettings.groupIndex); - - setAppointmentGroupValues(rawAppointment, resourceById, cellGroups); - } - } - getStartViewDate() { return this._workSpace?.getStartViewDate(); } @@ -1977,49 +1934,37 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - showAppointmentTooltip(appointment, element, targetedAppointment) { + // NOTE: public API + showAppointmentTooltip( + appointment: SafeAppointment, + element: dxElementWrapper, + targetedAppointment?: SafeAppointment, + ) { if (appointment) { const settings: any = utils.dataAccessors.getAppointmentSettings(element); - const appointmentConfig = { - itemData: targetedAppointment || appointment, + itemData: targetedAppointment ?? appointment, groupIndex: settings?.groupIndex, - groups: this.option('groups'), }; - const getAppointmentColor = this.createGetAppointmentColor(); - const deferredColor = getAppointmentColor(appointmentConfig) as any; + const info: AppointmentTooltipItem = { + appointment, + targetedAppointment, + color: this.resourceManager.getAppointmentColor(appointmentConfig), + }; - const info = new AppointmentTooltipInfo(appointment, targetedAppointment, deferredColor); this.showAppointmentTooltipCore(element, [info]); } } - createGetAppointmentColor() { - return (appointmentConfig) => fromPromise( - this.resourceManager.getAppointmentColor(appointmentConfig), - ); - } - - showAppointmentTooltipCore(target: dxElementWrapper, data, options?: any) { + showAppointmentTooltipCore(target: dxElementWrapper, data: AppointmentTooltipItem[], options?: any) { const arg: Omit = { cancel: false, - appointments: data.map((item) => { - const result = { - appointmentData: item.appointment, - currentAppointmentData: { ...item.targetedAppointment }, - color: item.color, - }; - - if (item.settings.info) { - const { startDate, endDate } = item.settings.info.appointment; - - result.currentAppointmentData.displayStartDate = startDate; - result.currentAppointmentData.displayEndDate = endDate; - } - - return result; - }), + appointments: data.map((item) => ({ + appointmentData: item.appointment, + currentAppointmentData: { ...item.targetedAppointment }, + color: item.color as Promise, + })), targetElement: getPublicElement(target), }; @@ -2029,7 +1974,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.hideAppointmentTooltip(); } else { this._processActionResult(arg, (canceled) => { - !canceled && this._appointmentTooltip.show(target, data, { ...this._getExtraAppointmentTooltipOptions(), ...options }); + !canceled && this._appointmentTooltip.show(target, data, { + ...this._getExtraAppointmentTooltipOptions(), + ...options, + }); }); } } @@ -2081,7 +2029,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._expandAllDayPanel(serializedAppointment); - return this.appointmentDataProvider + return this.appointmentDataSource .add(serializedAppointment) .always((storeAppointment) => this._onDataPromiseCompleted(StoreEventNames.ADDED, storeAppointment)); }); @@ -2111,7 +2059,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { processDeleteAppointment(rawAppointment, deletingOptions) { this._processActionResult(deletingOptions, function (canceled) { if (!canceled) { - this.appointmentDataProvider + this.appointmentDataSource .remove(rawAppointment) .always((storeAppointment) => this._onDataPromiseCompleted( StoreEventNames.DELETED, @@ -2162,12 +2110,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { } _validateKeyFieldIfAgendaExist() { - if (!this.appointmentDataProvider.isDataSourceInit) { + if (!this.appointmentDataSource.isDataSourceInit) { return; } const hasAgendaView = this.hasAgendaView(); - const isKeyNotExist = !this.appointmentDataProvider.keyName; + const isKeyNotExist = !this.appointmentDataSource.keyName; if (hasAgendaView && isKeyNotExist) { errors.log('W1023'); diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index e3be1887fc7a..b7729a458ae8 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -1,3 +1,4 @@ +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import dateUtils from '@js/core/utils/date'; import { extend } from '@js/core/utils/extend'; @@ -5,32 +6,48 @@ import { each } from '@js/core/utils/iterator'; import { isPlainObject } from '@js/core/utils/type'; import { formatDates, getFormatType } from './appointments/m_text_utils'; +import { getDeltaTime } from './appointments/resizing/get_delta_time'; +import { VERTICAL_VIEW_TYPES } from './constants'; import { AGENDA_LAST_IN_DATE_APPOINTMENT_CLASS } from './m_classes'; +import type Scheduler from './m_scheduler'; import { utils } from './m_utils'; +import { isAppointmentTakesAllDay } from './r1/utils/base'; +import type { + AppointmentTooltipItem, + CompactAppointmentOptions, + SafeAppointment, + TargetedAppointment, +} from './types'; import { AppointmentAdapter } from './utils/appointment_adapter/appointment_adapter'; +import type { AppointmentItemViewModel } from './view_model/generate_view_model/types'; const toMs = dateUtils.dateToMilliseconds; +const isAllDay = ( + scheduler: Scheduler, + appointmentData: SafeAppointment, +): boolean => { + const adapter = new AppointmentAdapter(appointmentData, scheduler._dataAccessors); + + if (scheduler.currentView.type === 'agenda') { + return false; + } + + if (VERTICAL_VIEW_TYPES.includes(scheduler.currentView.type)) { + return isAppointmentTakesAllDay(adapter, scheduler.option('allDayPanelMode')); + } + + return adapter.allDay; +}; const subscribes = { isCurrentViewAgenda() { return this.currentView.type === 'agenda'; }, - currentViewUpdated(currentView) { - this.option('currentView', currentView); - }, - - currentDateUpdated(date) { - this.option('currentDate', date); - }, getOption(name) { return this.option(name); }, - getWorkspaceOption(name) { - return this.getWorkSpace().option(name); - }, - isVirtualScrolling() { return this.isVirtualScrolling(); }, @@ -39,7 +56,7 @@ const subscribes = { return this.getWorkSpace().isGroupedByDate(); }, - showAppointmentTooltip(options) { + showAppointmentTooltip(options: { data: SafeAppointment; target: dxElementWrapper }) { const targetedAppointment = this.getTargetedAppointment(options.data, options.target); this.showAppointmentTooltip(options.data, options.target, targetedAppointment); }, @@ -54,10 +71,10 @@ const subscribes = { }, updateAppointmentAfterResize(options) { - const info = utils.dataAccessors.getAppointmentInfo(options.$appointment); - const { exceptionDate } = info.sourceAppointment; + const { info } = utils.dataAccessors.getAppointmentSettings(options.$appointment) as AppointmentItemViewModel; + const { startDate } = info.sourceAppointment; - this._checkRecurringAppointment(options.target, options.data, exceptionDate, () => { + this._checkRecurringAppointment(options.target, options.data, startDate, () => { this._updateAppointment(options.target, options.data, function () { this._appointments.moveAppointmentBack(); }); @@ -71,7 +88,7 @@ const subscribes = { updateAppointmentAfterDrag({ event, element, rawAppointment, isDropToTheSameCell, isDropToSelfScheduler, }) { - const info = utils.dataAccessors.getAppointmentInfo(element); + const { info } = utils.dataAccessors.getAppointmentSettings(element) as AppointmentItemViewModel; // NOTE: enrich target appointment with additional data from the source // in case of one appointment of series will change const targetedRawAppointment = extend({}, rawAppointment, this._getUpdatedData(rawAppointment)); @@ -106,16 +123,20 @@ const subscribes = { this.hideAppointmentTooltip(); }, - getTextAndFormatDate(appointmentRaw, targetedAppointmentRaw, format) { // TODO: rename to createFormattedDateText + createFormattedDateText( + appointment: AppointmentTooltipItem['appointment'], + targetedAppointmentRaw: AppointmentTooltipItem['targetedAppointment'], + format?: string, + ) { const targetedAppointment = { - ...appointmentRaw, + ...appointment, ...targetedAppointmentRaw, - }; - // pull out time zone converting from appointment adapter for knockout(T947938) + } as TargetedAppointment; const adapter = new AppointmentAdapter(targetedAppointment, this._dataAccessors); - const { startDate, endDate } = adapter.getCalculatedDates(this.timeZoneCalculator, 'toGrid'); - - const formatType = format || getFormatType(startDate, endDate, adapter.allDay, this.currentView.type !== 'month'); + // pull out time zone converting from appointment adapter for knockout (T947938) + const startDate = targetedAppointment.displayStartDate || this.timeZoneCalculator.createDate(adapter.startDate, 'toGrid'); + const endDate = targetedAppointment.displayEndDate || this.timeZoneCalculator.createDate(adapter.endDate, 'toGrid'); + const formatType = format ?? getFormatType(startDate, endDate, adapter.allDay, this.currentView.type !== 'month'); return { text: adapter.text, @@ -136,7 +157,7 @@ const subscribes = { const groups = this.getViewOption('groups'); if (groups?.length) { - if (allDay || this.getLayoutManager().getRenderingStrategyInstance()._needHorizontalGroupBounds()) { + if (allDay || !VERTICAL_VIEW_TYPES.includes(this.currentView.type)) { const horizontalGroupBounds = this._workSpace.getGroupBounds(options.coordinates); return { left: horizontalGroupBounds.left, @@ -146,7 +167,7 @@ const subscribes = { }; } - if (this.getLayoutManager().getRenderingStrategyInstance()._needVerticalGroupBounds(allDay) && this._workSpace._isVerticalGroupedWorkSpace()) { + if (!allDay && VERTICAL_VIEW_TYPES.includes(this.currentView.type) && this._workSpace._isVerticalGroupedWorkSpace()) { const verticalGroupBounds = this._workSpace.getGroupBounds(options.coordinates); return { left: 0, @@ -164,29 +185,21 @@ const subscribes = { return this.getWorkSpace().needRecalculateResizableArea(); }, - getAppointmentGeometry(settings) { - return this.getLayoutManager().getRenderingStrategyInstance().getAppointmentGeometry(settings); - }, - - isAllDay(appointmentData) { - return this.getLayoutManager().getRenderingStrategyInstance().isAllDay(appointmentData); + isAllDay(appointmentData): boolean { + return isAllDay(this, appointmentData); }, getDeltaTime(e, initialSize, itemData) { - return this.getLayoutManager().getRenderingStrategyInstance().getDeltaTime(e, initialSize, itemData); - }, - - getDropDownAppointmentWidth(isAllDay) { - return this.getLayoutManager() - .getRenderingStrategyInstance() - .getDropDownAppointmentWidth( - this.currentView.intervalCount, - isAllDay, - ); - }, - - getDropDownAppointmentHeight() { - return this.getLayoutManager().getRenderingStrategyInstance().getDropDownAppointmentHeight(); + return getDeltaTime(e, initialSize, { + viewType: this.currentView.type, + cellSize: { + width: this.getWorkSpace().getCellWidth(), + height: this.getWorkSpace().getCellHeight(), + }, + cellDurationInMinutes: this.getWorkSpace().option('cellDuration'), + resizableStep: this.getWorkSpace().positionHelper.getResizableStep(), + isAllDay: isAllDay(this, itemData), + }); }, getCellWidth() { @@ -197,16 +210,12 @@ const subscribes = { return this.getWorkSpace().getCellHeight(); }, - getMaxAppointmentCountPerCellByType(isAllDay) { - return this.getRenderingStrategyInstance()._getMaxAppointmentCountPerCellByType(isAllDay); - }, - needCorrectAppointmentDates() { - return this.getRenderingStrategyInstance().needCorrectAppointmentDates(); + return !['month', 'timelineMonth'].includes(this.currentView.type); }, getRenderingStrategyDirection() { - return this.getRenderingStrategyInstance().getDirection(); + return VERTICAL_VIEW_TYPES.includes(this.currentView.type) ? 'vertical' : 'horizontal'; }, updateAppointmentEndDate(options) { @@ -225,18 +234,14 @@ const subscribes = { return updatedEndDate; }, - renderCompactAppointments(options) { - this._compactAppointmentsHelper.render(options); + renderCompactAppointments(options: CompactAppointmentOptions): dxElementWrapper { + return this._compactAppointmentsHelper.render(options); }, clearCompactAppointments() { this._compactAppointmentsHelper.clear(); }, - supportCompactDropDownAppointments() { - return this.getLayoutManager().getRenderingStrategyInstance().supportCompactDropDownAppointments(); - }, - getGroupCount() { return this._workSpace._getGroupCount(); }, @@ -318,5 +323,8 @@ const subscribes = { removeDroppableCellClass() { this._workSpace.removeDroppableCellClass(); }, -}; +} as const; + export default subscribes; +export type SubscribeMethods = typeof subscribes; +export type SubscribeKey = keyof typeof subscribes; diff --git a/packages/devextreme/js/__internal/scheduler/m_utils.ts b/packages/devextreme/js/__internal/scheduler/m_utils.ts index 65aec06ca642..6b1880fd4cb8 100644 --- a/packages/devextreme/js/__internal/scheduler/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/m_utils.ts @@ -3,15 +3,12 @@ import $ from '@js/core/renderer'; import { getOuterHeight, setHeight, setWidth } from '@js/core/utils/size'; import { APPOINTMENT_SETTINGS_KEY } from './constants'; +import type { AppointmentViewModelPlain } from './view_model/generate_view_model/types'; export const utils = { dataAccessors: { - getAppointmentSettings: (element) => $(element).data(APPOINTMENT_SETTINGS_KEY), - - getAppointmentInfo: (element) => { - const settings: any = utils.dataAccessors.getAppointmentSettings(element); - return settings?.info; - }, + getAppointmentSettings: (element) => $(element) + .data(APPOINTMENT_SETTINGS_KEY) as unknown as AppointmentViewModelPlain | undefined, }, DOM: { getHeaderHeight: (header) => (header diff --git a/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts b/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts index 8dfa9ee9ed11..5d820f44a8b5 100644 --- a/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts +++ b/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts @@ -5,8 +5,6 @@ import { macroTaskArray } from '@ts/scheduler/utils/index'; import dateUtils from '../../core/utils/date'; import { globalCache } from './global_cache'; -import DateAdapter from './m_date_adapter'; -import timeZoneDataUtils from './timezones/m_utils_timezones_data'; import timeZoneList from './timezones/timezone_list'; export interface TimezoneLabel { @@ -43,23 +41,14 @@ const createUTCDateWithLocalOffset = (date) => { )); }; -const createDateFromUTCWithLocalOffset = (date) => { - const result = DateAdapter(date); - - const timezoneOffsetBeforeInMin = result.getTimezoneOffset(); - result.addTime(result.getTimezoneOffset('minute')); - result.subtractMinutes(timezoneOffsetBeforeInMin - result.getTimezoneOffset()); - - return result.source; -}; - -const createUTCDate = (date) => new Date(Date.UTC( +const createDateFromUTCWithLocalOffset = (date: Date): Date => new Date( date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), -)); + date.getUTCSeconds(), +); const getTimezoneOffsetChangeInMinutes = (startDate, endDate, updatedStartDate, updatedEndDate) => getDaylightOffset(updatedStartDate, updatedEndDate) - getDaylightOffset(startDate, endDate); @@ -69,16 +58,6 @@ const getDaylightOffset = (startDate, endDate) => new Date(startDate).getTimezon const getDaylightOffsetInMs = (startDate, endDate) => getDaylightOffset(startDate, endDate) * toMs('minute'); -const calculateTimezoneByValueOld = (timezone: string, date = new Date()): number | undefined => { - const customTimezones = timeZoneDataUtils.getTimeZonesOld(); - if (customTimezones.length === 0) { - return undefined; - } - - const dateUtc = createUTCDate(date); - return timeZoneDataUtils.getTimeZoneOffsetById(timezone, dateUtc.getTime()); -}; - const calculateTimezoneByValueCore = (timeZone: string, date = new Date()): number | undefined => { const offset = getStringOffset(timeZone, date); @@ -113,12 +92,7 @@ const calculateTimezoneByValue = (timeZone: string | undefined, date = new Date( return undefined; } - let result = calculateTimezoneByValueOld(timeZone, date); - if (result === undefined) { - result = calculateTimezoneByValueCore(timeZone, date); - } - - return result; + return calculateTimezoneByValueCore(timeZone, date); }; // 'GMT±XX:YY' or 'GMT' format @@ -223,104 +197,23 @@ const getDiffBetweenClientTimezoneOffsets = (firstDate = new Date(), secondDate const getMachineTimezoneName = () => globalCache.timezones.memo('localTimezone', () => dateUtils.getMachineTimezoneName()); -const isEqualLocalTimeZone = (timeZoneName, date = new Date()) => { +const isEqualLocalTimeZone = (timeZoneName: string) => { const localTimeZoneName = getMachineTimezoneName(); if (localTimeZoneName && localTimeZoneName === timeZoneName) { return true; } - - return isEqualLocalTimeZoneByDeclaration(timeZoneName, date); -}; - -// TODO: Not used anywhere, if it isn't use in the future, then it must be removed -const hasDSTInLocalTimeZone = () => { - const [startDate, endDate] = getExtremeDates(); - return startDate.getTimezoneOffset() !== endDate.getTimezoneOffset(); -}; - -const getOffset = (date) => -date.getTimezoneOffset() / MINUTES_IN_HOUR; - -const getDateAndMoveHourBack = (dateStamp) => new Date(dateStamp - toMs('hour')); - -const isEqualLocalTimeZoneByDeclarationOld = (timeZoneName: string, date: Date): boolean => { - const year = date.getFullYear(); - - const configTuple = timeZoneDataUtils.getTimeZoneDeclarationTuple(timeZoneName, year); - const [summerTime, winterTime] = configTuple; - - const noDSTInTargetTimeZone = configTuple.length < 2; - if (noDSTInTargetTimeZone) { - const targetTimeZoneOffset = timeZoneDataUtils.getTimeZoneOffsetById(timeZoneName, date); - const localTimeZoneOffset = getOffset(date); - - if (targetTimeZoneOffset !== localTimeZoneOffset) { - return false; - } - - return !hasDSTInLocalTimeZone(); - } - - const localSummerOffset = getOffset(new Date(summerTime.date)); - const localWinterOffset = getOffset(new Date(winterTime.date)); - - if (localSummerOffset !== summerTime.offset) { - return false; - } - - if (localSummerOffset === getOffset(getDateAndMoveHourBack(summerTime.date))) { - return false; - } - - if (localWinterOffset !== winterTime.offset) { - return false; - } - - if (localWinterOffset === getOffset(getDateAndMoveHourBack(winterTime.date))) { - return false; - } - - return true; -}; - -const isEqualLocalTimeZoneByDeclaration = (timeZoneName: string, date: Date): boolean => { - const customTimezones = timeZoneDataUtils.getTimeZonesOld(); - const targetTimezoneData = customTimezones.filter((tz) => tz.id === timeZoneName); - - if (targetTimezoneData.length === 1) { - return isEqualLocalTimeZoneByDeclarationOld(timeZoneName, date); - } - return false; }; -// Getting two dates in january or june is the standard mechanism for determining that an offset has occurred. -const getExtremeDates = () => { - const nowDate = new Date(Date.now()); - - const startDate = new Date(); - const endDate = new Date(); - - startDate.setFullYear(nowDate.getFullYear(), 0, 1); - endDate.setFullYear(nowDate.getFullYear(), 6, 1); - - return [startDate, endDate]; -}; - -// TODO Vinogradov refactoring: Change to date utils. -const setOffsetsToDate = (targetDate, offsetsArray) => { - const newDateMs = offsetsArray.reduce((result, offset) => result + offset, targetDate.getTime()); - return new Date(newDateMs); -}; - const addOffsetsWithoutDST = (date: Date, ...offsets: number[]): Date => { - const newDate = dateUtilsTs.addOffsets(date, offsets); + const newDate = dateUtilsTs.addOffsets(date, ...offsets); const daylightShift = getDaylightOffsetInMs(date, newDate); if (!daylightShift) { return newDate; } - const correctLocalDate = dateUtilsTs.addOffsets(newDate, [-daylightShift]); + const correctLocalDate = dateUtilsTs.addOffsets(newDate, -daylightShift); const daylightSecondShift = getDaylightOffsetInMs(newDate, correctLocalDate); return !daylightSecondShift @@ -374,16 +267,12 @@ const utils = { createUTCDateWithLocalOffset, createDateFromUTCWithLocalOffset, - createUTCDate, isTimezoneChangeInDate, getDateWithoutTimezoneChange, - hasDSTInLocalTimeZone, getMachineTimezoneName, isEqualLocalTimeZone, - isEqualLocalTimeZoneByDeclaration, - setOffsetsToDate, addOffsetsWithoutDST, getTimeZones, diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/all_day_panel_table_body.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/all_day_panel_table_body.tsx index a7581743be24..4b038adfcacd 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/components/base/all_day_panel_table_body.tsx +++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/all_day_panel_table_body.tsx @@ -43,7 +43,7 @@ export class AllDayPanelTableBody extends BaseInfernoComponent { [cellSizeHorizontalClass]: true, 'dx-scheduler-header-panel-current-time-cell': today, 'dx-scheduler-header-panel-week-cell': isWeekDayCell, - [className ?? '']: !!className, + [className ?? '']: Boolean(className), }); const classes = renderUtils .getGroupCellClasses(isFirstGroupCell, isLastGroupCell, cellClasses); - const useTemplate = (!isTimeCellTemplate && !!dateCellTemplate) - || (isTimeCellTemplate && !!timeCellTemplate); + const useTemplate = (!isTimeCellTemplate && Boolean(dateCellTemplate)) + || (isTimeCellTemplate && Boolean(timeCellTemplate)); const children = useTemplate ? ( // this is a workaround for https://github.com/DevExpress/devextreme-renovation/issues/574 diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/date_table_cell_base.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/date_table_cell_base.tsx index 4272c5241fea..e3be21126ee8 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/components/base/date_table_cell_base.tsx +++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/date_table_cell_base.tsx @@ -54,7 +54,7 @@ export class DateTableCellBase extends BaseInfernoComponent) } diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/row.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/row.tsx index 410dbaca5952..048df3b2e32d 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/components/base/row.tsx +++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/row.tsx @@ -35,8 +35,8 @@ export class Row extends BaseInfernoComponent { rightVirtualCellWidth = RowDefaultProps.rightVirtualCellWidth, styles, } = this.props; - const hasLeftVirtualCell = !!leftVirtualCellCount; - const hasRightVirtualCell = !!rightVirtualCellCount; + const hasLeftVirtualCell = Boolean(leftVirtualCellCount); + const hasRightVirtualCell = Boolean(rightVirtualCellCount); return ( { tableRef, virtualCellsCount, } = this.props; - const hasTopVirtualRow = !!topVirtualRowHeight; - const hasBottomVirtualRow = !!bottomVirtualRowHeight; + const hasTopVirtualRow = Boolean(topVirtualRowHeight); + const hasBottomVirtualRow = Boolean(bottomVirtualRowHeight); const resultStyles = this.getResultStyles(); return ( diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/time_panel_cell.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/time_panel_cell.tsx index 3a3948073c46..dcc4b745a28f 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/components/base/time_panel_cell.tsx +++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/time_panel_cell.tsx @@ -67,7 +67,7 @@ export class TimePanelCell extends BaseInfernoComponent { const classes = combineClasses({ 'dx-scheduler-time-panel-cell': true, [cellSizeVerticalClass]: true, - 'dx-scheduler-time-panel-current-time-cell': !!highlighted, + 'dx-scheduler-time-panel-current-time-cell': Boolean(highlighted), [className ?? '']: true, }); const timeCellTemplateProps = this.getTimeCellTemplateProps(); diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/month/date_table_month_cell.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/month/date_table_month_cell.tsx index 6bd1e61e444d..ad3a9c83b9c6 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/components/month/date_table_month_cell.tsx +++ b/packages/devextreme/js/__internal/scheduler/r1/components/month/date_table_month_cell.tsx @@ -53,10 +53,10 @@ export class DateTableMonthCell extends BaseInfernoComponent { describe('combineRemoteFilter', () => { diff --git a/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/calculator.ts b/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/calculator.ts index 88d5444afc35..dc37f2cfb172 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/calculator.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/calculator.ts @@ -62,11 +62,11 @@ export class TimeZoneCalculator { protected getOffsetInHours(date: Date, timezone: string | undefined, isUTCDate: boolean): number { const { client, appointment, common } = this.getOffsets(date, timezone); - if (!!timezone && isUTCDate) { + if (Boolean(timezone) && isUTCDate) { return appointment - client; } - if (!!timezone && !isUTCDate) { + if (Boolean(timezone) && !isUTCDate) { return appointment - common; } @@ -102,9 +102,10 @@ export class TimeZoneCalculator { const targetOffsetName = appointmentTimezone ? 'appointment' : 'common'; const direction = isBack ? -1 : 1; - return dateUtilsTs.addOffsets(newDate, [ + return dateUtilsTs.addOffsets( + newDate, direction * toMs('hour') * offsets[targetOffsetName], -direction * toMs('hour') * offsets.client, - ]); + ); } } diff --git a/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/types.ts b/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/types.ts index 002121d0bb3d..f470713c913a 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/types.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/types.ts @@ -1,6 +1,7 @@ export type DateType = Date | string; export interface TimeZoneCalculatorOptions { + timeZone?: string; getClientOffset: (date: Date) => number; tryGetCommonOffset: (date: Date, timeZone?: string) => number | undefined; tryGetAppointmentOffset: ( diff --git a/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/utils.ts b/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/utils.ts index 968dab37d104..9b5c4c591a1c 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/timezone_calculator/utils.ts @@ -4,6 +4,7 @@ import { TimeZoneCalculator } from './calculator'; export const createTimeZoneCalculator = ( currentTimeZone: string, ): TimeZoneCalculator => new TimeZoneCalculator({ + timeZone: currentTimeZone, getClientOffset: (date: Date): number => timeZoneUtils .getClientTimezoneOffset(date), tryGetCommonOffset: (date: Date): number | undefined => timeZoneUtils diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/excludeFromRecurrence.test.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/exclude_from_recurrence.test.ts similarity index 100% rename from packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/excludeFromRecurrence.test.ts rename to packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/exclude_from_recurrence.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts index 9bb4a8b36693..539349ca0cf8 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts @@ -151,7 +151,7 @@ export const getValidCellDateForLocalTimeFormat = ( viewOffset: number; }, ): Date => { - const originDate = dateUtilsTs.addOffsets(date, [-viewOffset]); + const originDate = dateUtilsTs.addOffsets(date, -viewOffset); const localTimeZoneChangedInOriginDate = timeZoneUtils.isTimezoneChangeInDate(originDate); if (!localTimeZoneChangedInOriginDate) { @@ -170,7 +170,9 @@ export const getValidCellDateForLocalTimeFormat = ( const startViewDateOffset = getStartViewDateTimeOffset(startViewDate, startDayHour); return dateUtilsTs.addOffsets( startViewDateWithoutDST, - [viewOffset, cellIndexShift, -startViewDateOffset], + viewOffset, + cellIndexShift, + -startViewDateOffset, ); }; @@ -198,7 +200,7 @@ export const isVerticalGroupingApplied = ( groups: unknown[], groupOrientation?: GroupOrientation, ): boolean => groupOrientation === VERTICAL_GROUP_ORIENTATION - && !!groups.length; + && Boolean(groups.length); // TODO(9): Get rid of it as soon as you can. More parameters then needed export const getHorizontalGroupCount = ( @@ -331,7 +333,7 @@ export const getKeyByGroup = ( groupIndex: number | undefined, isVerticalGrouping: boolean, ): string => { - if (isVerticalGrouping && !!groupIndex) { + if (isVerticalGrouping && groupIndex !== undefined) { return groupIndex.toString(); } @@ -355,7 +357,7 @@ export const getCalculatedFirstDayOfWeek = ( export const isHorizontalGroupingApplied = ( groups: unknown[], groupOrientation?: GroupOrientation, -): boolean => groupOrientation === HORIZONTAL_GROUP_ORIENTATION && !!groups.length; +): boolean => groupOrientation === HORIZONTAL_GROUP_ORIENTATION && Boolean(groups.length); export const isGroupingByDate = ( groups: unknown[], @@ -398,22 +400,34 @@ export const getSkippedHoursInRange = ( const endDateHours = endDate.getHours() + (endDate.getTime() % HOUR_IN_MS) / HOUR_IN_MS; if (viewDataProvider.isSkippedDate(startDate)) { - if (isAllDay) { - result += DAY_HOURS; - } else if (startDateHours < startDayHour) { - result += dayHours; - } else if (startDateHours < endDayHour) { - result += endDayHour - startDateHours; + switch (true) { + case isAllDay: + result += DAY_HOURS; + break; + case startDateHours < startDayHour: + result += dayHours; + break; + case startDateHours < endDayHour: + result += endDayHour - startDateHours; + break; + default: + break; } } if (viewDataProvider.isSkippedDate(endDate)) { - if (isAllDay) { - result += DAY_HOURS; - } else if (endDateHours > endDayHour) { - result += dayHours; - } else if (endDateHours > startDayHour) { - result += endDateHours - startDayHour; + switch (true) { + case isAllDay: + result += DAY_HOURS; + break; + case endDateHours > endDayHour: + result += dayHours; + break; + case endDateHours > startDayHour: + result += endDateHours - startDayHour; + break; + default: + break; } } diff --git a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts index 0c35abdb09a5..aea012d3de00 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -69,6 +69,7 @@ export class SchedulerOptionsBaseWidget extends Widget { // @ts-expect-error super._initMarkup(); this.updateViews(); + this.validateOptions(); } protected schedulerOptionChanged(args: { diff --git a/packages/devextreme/js/__internal/scheduler/timezones/m_utils_timezones_data.ts b/packages/devextreme/js/__internal/scheduler/timezones/m_utils_timezones_data.ts deleted file mode 100644 index 7dbbb7ca20d1..000000000000 --- a/packages/devextreme/js/__internal/scheduler/timezones/m_utils_timezones_data.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* eslint-disable radix */ -import { sign } from '@js/core/utils/math'; - -import GlobalConfig from '../../../core/config'; - -const getConvertedUntils = (value) => value.split('|').map((until) => { - if (until === 'Infinity') { - return null; - } - return parseInt(until, 36) * 1000; -}); - -const parseTimezone = (timeZoneConfig) => { - const { offsets } = timeZoneConfig; - const { offsetIndices } = timeZoneConfig; - const { untils } = timeZoneConfig; - - const offsetList = offsets.split('|').map((value) => parseInt(value)); - const offsetIndexList = offsetIndices.split('').map((value) => parseInt(value)); - const dateList = getConvertedUntils(untils) - // eslint-disable-next-line - .map(((accumulator) => (value) => accumulator += value)(0)); - - return { offsetList, offsetIndexList, dateList }; -}; - -class TimeZoneCache { - map: Map; - - constructor() { - this.map = new Map(); - } - - tryGet(id) { - if (!this.map.get(id)) { - const config = timeZoneDataUtils.getTimezoneById(id); - if (!config) { - return false; - } - - const timeZoneInfo = parseTimezone(config); - this.map.set(id, timeZoneInfo); - } - - return this.map.get(id); - } -} - -const tzCache = new TimeZoneCache(); - -const timeZoneDataUtils = { - _tzCache: tzCache, - - getTimeZonesOld(): any { - return GlobalConfig().timezones ?? []; - }, - - formatOffset(offset) { - const hours = Math.floor(offset); - const minutesInDecimal = offset - hours; - - const signString = sign(offset) >= 0 ? '+' : '-'; - const hoursString = `0${Math.abs(hours)}`.slice(-2); - const minutesString = minutesInDecimal > 0 ? `:${minutesInDecimal * 60}` : ':00'; - - return signString + hoursString + minutesString; - }, - - formatId(id) { - return id.split('/').join(' - ').split('_').join(' '); - }, - - getTimezoneById(id) { - if (!id) { - return undefined; - } - - const tzList = this.getTimeZonesOld(); - - for (let i = 0; i < tzList.length; i++) { - const currentId = tzList[i].id; - if (currentId === id) { - return tzList[i]; - } - } - - return undefined; - }, - - getTimeZoneOffsetById(id, timestamp) { - const timeZoneInfo = tzCache.tryGet(id); - - return timeZoneInfo ? this.getUtcOffset(timeZoneInfo, timestamp) : undefined; - }, - - getTimeZoneDeclarationTuple(id, year) { - const timeZoneInfo = tzCache.tryGet(id); - - return timeZoneInfo ? this.getTimeZoneDeclarationTupleCore(timeZoneInfo, year) : []; - }, - - getTimeZoneDeclarationTupleCore(timeZoneInfo, year) { - const { offsetList } = timeZoneInfo; - const { offsetIndexList } = timeZoneInfo; - const { dateList } = timeZoneInfo; - - const tupleResult: any = []; - - for (let i = 0; i < dateList.length; i++) { - const currentDate = dateList[i]; - const currentYear = new Date(currentDate).getFullYear(); - - if (currentYear === year) { - const offset = offsetList[offsetIndexList[i + 1]]; - tupleResult.push({ date: currentDate, offset: -offset / 60 }); - } - - if (currentYear > year) { - break; - } - } - - return tupleResult; - }, - - getUtcOffset(timeZoneInfo, dateTimeStamp) { - const { offsetList } = timeZoneInfo; - const { offsetIndexList } = timeZoneInfo; - const { dateList } = timeZoneInfo; - - const infinityUntilCorrection = 1; - const lastIntervalStartIndex = dateList.length - 1 - infinityUntilCorrection; - - let index = lastIntervalStartIndex; - while (index >= 0 && dateTimeStamp < dateList[index]) { - index--; - } - - const offset = offsetList[offsetIndexList[index + 1]]; - return -offset / 60 || offset; - }, -}; - -export default timeZoneDataUtils; diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts index b872fd0f3cdc..cdda3463f244 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts @@ -147,7 +147,7 @@ export class TooltipStrategyBase { } _createFunctionTemplate(template, appointmentData, targetedAppointmentData, index) { - const isButtonClicked = !!this._extraOptions.isButtonClick; + const isButtonClicked = Boolean(this._extraOptions.isButtonClick); const isEmptyDropDownAppointmentTemplate = this._isEmptyDropDownAppointmentTemplate(); // @ts-expect-error @@ -208,7 +208,11 @@ export class TooltipStrategyBase { const $markerBody = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_MARKER_BODY); $marker.append($markerBody); - color && color.done((value) => $markerBody.css('background', value)); + color.then((value) => { + if (value) { + $markerBody.css('background', value); + } + }); return $marker; } diff --git a/packages/devextreme/js/__internal/scheduler/types.ts b/packages/devextreme/js/__internal/scheduler/types.ts index f295f2f13317..f0a05b920f5b 100644 --- a/packages/devextreme/js/__internal/scheduler/types.ts +++ b/packages/devextreme/js/__internal/scheduler/types.ts @@ -1,4 +1,6 @@ -import type { Appointment } from '@js/ui/scheduler'; +import type { dxElementWrapper } from '@js/core/renderer'; +import type { Appointment, Properties } from '@js/ui/scheduler'; +import type { AppointmentViewModelPlain } from '@ts/scheduler/view_model/generate_view_model/types'; import type { ResourceLoader } from './utils/loader/resource_loader'; @@ -14,6 +16,10 @@ export interface SafeAppointment extends Appointment { startDate: Date | string; endDate: Date | string; } +export interface TargetedAppointment extends SafeAppointment { + displayStartDate: Date; + displayEndDate: Date; +} export interface AppointmentDataItem { startDate: Date; @@ -28,60 +34,12 @@ export interface AppointmentDataItem { rawAppointment: SafeAppointment; } -export interface BaseAppointmentViewModelSettings { - allDay?: boolean; - direction: string; - groupIndex: number; - sortedIndex: number; -} - -export interface AppointmentViewModelSettings extends BaseAppointmentViewModelSettings { - index: number; // current level in stack - count: number; // max level including appointments not in stack - info: { - sourceAppointment: { - startDate: Date; - endDate: Date; - }; - appointment: { - startDate: Date; - endDate: Date; - }; - }; - left: number; - top: number; - height: number; - width: number; - isCompact: boolean; - appointmentReduced: 'head' | 'body' | 'tail' | undefined; - partIndex: number; - partTotalCount: number; -} - -export interface AgendaViewModelSettings extends BaseAppointmentViewModelSettings { - agendaSettings: { - startDate: Date; - endDate: Date; - }; - height: number; - width: string; -} - -export interface AppointmentViewModel { - itemData: SafeAppointment; - needRepaint: boolean; - needRemove: boolean; - settings: (AppointmentViewModelSettings & AgendaViewModelSettings)[]; -} - export interface AppointmentGeometry { empty: boolean; left: number; top: number; width: number; height: number; - leftVirtualWidth: number; - topVirtualHeight: number; } export type GetDateForHeaderText = ( @@ -290,3 +248,24 @@ export interface ViewDataProviderType { getCellsBetween: (first: ViewCellData, last: ViewCellData) => ViewCellData[]; viewType: ViewType; } + +export interface AppointmentTooltipItem { + appointment: Appointment; + targetedAppointment?: Appointment | TargetedAppointment; + color: Promise; +} + +export interface CompactAppointmentOptions { + $container: dxElementWrapper; + coordinates: { top: number; left: number }; + items: (AppointmentTooltipItem & { + settings: AppointmentViewModelPlain; + })[]; + buttonColor: Promise; + sortedIndex: number; + width: number; + height: number; + onAppointmentClick: Properties['onAppointmentClick']; + allowDrag: boolean; + isCompact: boolean; +} diff --git a/packages/devextreme/js/__internal/scheduler/utils/appointment_adapter/appointment_adapter.ts b/packages/devextreme/js/__internal/scheduler/utils/appointment_adapter/appointment_adapter.ts index 84e308baa044..69ce9fc25f8e 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/appointment_adapter/appointment_adapter.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/appointment_adapter/appointment_adapter.ts @@ -2,7 +2,6 @@ import type { Appointment } from '@js/ui/scheduler'; import { deepExtendArraySafe } from '@ts/core/utils/m_object'; import type { PathTimeZoneConversion, TimeZoneCalculator } from '@ts/scheduler/r1/timezone_calculator'; -import { getRecurrenceProcessor } from '../../m_recurrence'; import type { AppointmentDataAccessor } from '../data_accessor/appointment_data_accessor'; export class AppointmentAdapter { @@ -53,7 +52,7 @@ export class AppointmentAdapter { } get isRecurrent(): boolean { - return getRecurrenceProcessor().isValidRecurrenceRule(this.recurrenceRule); + return this.dataAccessors.isRecurrent(this.source); } clone(): AppointmentAdapter { diff --git a/packages/devextreme/js/__internal/scheduler/utils/data_accessor/appointment_data_accessor.ts b/packages/devextreme/js/__internal/scheduler/utils/data_accessor/appointment_data_accessor.ts index a2d6867a96b4..523f3d6122cb 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/data_accessor/appointment_data_accessor.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/data_accessor/appointment_data_accessor.ts @@ -2,6 +2,7 @@ import { compileGetter, compileSetter } from '@js/core/utils/data'; import dateSerialization from '@js/core/utils/date_serialization'; import type { Appointment } from '@js/ui/scheduler'; +import { getRecurrenceProcessor } from '../../m_recurrence'; import { DataAccessor } from './data_accessor'; import type { DataAccessorGetter, DataAccessorSetter, IFieldExpr } from './types'; @@ -111,4 +112,10 @@ export class AppointmentDataAccessor extends DataAccessor(appointment: T): boolean { + const recurrenceRule = this.get('recurrenceRule', appointment); + const isRecurrent = getRecurrenceProcessor().isValidRecurrenceRule(recurrenceRule); + return isRecurrent; + } } diff --git a/packages/devextreme/js/__internal/scheduler/utils/get_targeted_appointment.test.ts b/packages/devextreme/js/__internal/scheduler/utils/get_targeted_appointment.test.ts new file mode 100644 index 000000000000..c6775edfd4db --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/utils/get_targeted_appointment.test.ts @@ -0,0 +1,152 @@ +/** + * @timezone Europe/Belgrade + */ + +import { + describe, expect, it, +} from '@jest/globals'; +import { getResourceManagerMock } from '@ts/scheduler/__mock__/resource_manager.mock'; + +import { mockUppercaseFieldExpressions } from '../__mock__/appointment_data_accessor.mock'; +import { createTimeZoneCalculator } from '../r1/timezone_calculator/utils'; +import { AppointmentDataAccessor } from './data_accessor/appointment_data_accessor'; +import { getTargetedAppointment } from './get_targeted_appointment'; + +const dataAccessor = new AppointmentDataAccessor(mockUppercaseFieldExpressions, true); +const timeZoneCalculator = createTimeZoneCalculator('America/Los_Angeles'); +const appointment: any = { + StartDate: new Date(200, 0, 0), + EndDate: new Date(200, 0, 1), +}; +const appointmentRecurrence: any = { + ...appointment, + RecurrenceRule: 'FREQ=DAILY', +}; +const getInfo = () => ({ + sourceAppointment: { + startDate: new Date(200, 0, 5), + endDate: new Date(200, 0, 6), + }, + appointment: { + startDate: new Date(200, 0, 5, 10), + endDate: new Date(200, 0, 6, 11), + }, +}); + +describe('getTargetedAppointment', () => { + it('should return collector targeted appointment', () => { + expect(getTargetedAppointment( + appointment, + {} as any, + dataAccessor, + timeZoneCalculator, + getResourceManagerMock(), + )).toEqual({ + ...appointment, + displayStartDate: new Date(200, 0, 0), + displayEndDate: new Date(200, 0, 1), + }); + }); + + it('should return grid item targeted appointment', () => { + expect(getTargetedAppointment( + appointmentRecurrence, + { + info: getInfo(), + groupIndex: 0, + } as any, + dataAccessor, + timeZoneCalculator, + getResourceManagerMock(), + )).toEqual({ + ...appointmentRecurrence, + StartDate: new Date(200, 0, 5), + EndDate: new Date(200, 0, 6), + displayStartDate: new Date(200, 0, 5, 10), + displayEndDate: new Date(200, 0, 6, 11), + }); + }); + + it('should return grid item targeted appointment with resources', async () => { + const resourceManager = getResourceManagerMock(); + await resourceManager.loadGroupResources(['roomId', 'assigneeId']); + expect(getTargetedAppointment( + appointmentRecurrence, + { + info: getInfo(), + groupIndex: 5, // 0,1; 0,2; 0,3; 0,4; 1,1; 1,2; <- 5 + } as any, + dataAccessor, + timeZoneCalculator, + resourceManager, + )).toEqual({ + ...appointmentRecurrence, + assigneeId: [2], + roomId: 1, + StartDate: new Date(200, 0, 5), + EndDate: new Date(200, 0, 6), + displayStartDate: new Date(200, 0, 5, 10), + displayEndDate: new Date(200, 0, 6, 11), + }); + }); + + it('should return agenda item targeted recurrence appointment', () => { + expect(getTargetedAppointment( + appointmentRecurrence, + { + isAgendaModel: true, + info: getInfo(), + groupIndex: 0, + } as any, + dataAccessor, + timeZoneCalculator, + getResourceManagerMock(), + )).toEqual({ + ...appointmentRecurrence, + StartDate: new Date(200, 0, 5), + EndDate: new Date(200, 0, 6), + displayStartDate: new Date(200, 0, 5, 10), + displayEndDate: new Date(200, 0, 6, 11), + }); + }); + + it('should return agenda item targeted full appointment', () => { + expect(getTargetedAppointment( + appointment, + { + isAgendaModel: true, + info: getInfo(), + groupIndex: 0, + } as any, + dataAccessor, + timeZoneCalculator, + getResourceManagerMock(), + )).toEqual({ + ...appointment, + displayStartDate: appointment.StartDate, + displayEndDate: appointment.EndDate, + }); + }); + + it('should return agenda item targeted full appointment with resources', async () => { + const resourceManager = getResourceManagerMock(); + await resourceManager.loadGroupResources(['roomId', 'assigneeId']); + expect(getTargetedAppointment( + appointment, + { + isAgendaModel: true, + info: getInfo(), + groupIndex: 3, // 0,1; 0,2; 0,3; 0,4; <- 3 + } as any, + dataAccessor, + timeZoneCalculator, + resourceManager, + )).toEqual({ + ...appointment, + assigneeId: [4], + roomId: 0, + displayStartDate: appointment.StartDate, + displayEndDate: appointment.EndDate, + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/utils/get_targeted_appointment.ts b/packages/devextreme/js/__internal/scheduler/utils/get_targeted_appointment.ts new file mode 100644 index 000000000000..061bb44ab45c --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/utils/get_targeted_appointment.ts @@ -0,0 +1,78 @@ +import type { TimeZoneCalculator } from '../r1/timezone_calculator'; +import type { SafeAppointment, TargetedAppointment } from '../types'; +import type { + AppointmentAgendaViewModel, + AppointmentItemViewModel, + AppointmentViewModelPlain, +} from '../view_model/generate_view_model/types'; +import type { AppointmentDataAccessor } from './data_accessor/appointment_data_accessor'; +import { setAppointmentGroupValues } from './resource_manager/appointment_groups_utils'; +import { getLeafGroupValues } from './resource_manager/group_utils'; +import type { ResourceManager } from './resource_manager/resource_manager'; + +const setTargetedAppointmentResources = ( + rawAppointment: SafeAppointment, + settings: AppointmentAgendaViewModel | AppointmentItemViewModel, + resourceManager: ResourceManager, +): void => { + const { groups, resourceById, groupsLeafs } = resourceManager; + if (groups.length) { + const cellGroups = getLeafGroupValues(groupsLeafs, settings.groupIndex); + setAppointmentGroupValues(rawAppointment, resourceById, cellGroups); + } +}; + +export const getTargetedAppointmentFromInfo = ( + rawAppointment: SafeAppointment, + settings: AppointmentAgendaViewModel | AppointmentItemViewModel, + dataAccessor: AppointmentDataAccessor, + resourceManager: ResourceManager, +): TargetedAppointment => { + const { info } = settings; + + const rawTargetedAppointment = { ...rawAppointment } as TargetedAppointment; + dataAccessor.set('startDate', rawTargetedAppointment, new Date(info.sourceAppointment.startDate)); + dataAccessor.set('endDate', rawTargetedAppointment, new Date(info.sourceAppointment.endDate)); + rawTargetedAppointment.displayStartDate = new Date(info.appointment.startDate); + rawTargetedAppointment.displayEndDate = new Date(info.appointment.endDate); + setTargetedAppointmentResources(rawTargetedAppointment, settings, resourceManager); + + return rawTargetedAppointment; +}; + +export const getTargetedAppointment = ( + rawAppointment: SafeAppointment, + settings: AppointmentViewModelPlain, + dataAccessor: AppointmentDataAccessor, + timeZoneCalculator: TimeZoneCalculator, + resourceManager: ResourceManager, +): TargetedAppointment => { + const startDate = dataAccessor.get('startDate', rawAppointment); + const endDate = dataAccessor.get('endDate', rawAppointment); + + if (!('info' in settings)) { + return { + ...rawAppointment, + displayStartDate: startDate, + displayEndDate: endDate, + }; + } + + if ('isAgendaModel' in settings && !dataAccessor.isRecurrent(rawAppointment)) { + const rawTargetedAppointment = { + ...rawAppointment, + displayStartDate: timeZoneCalculator.createDate(startDate, 'toGrid'), + displayEndDate: timeZoneCalculator.createDate(endDate, 'toGrid'), + }; + + setTargetedAppointmentResources(rawTargetedAppointment, settings, resourceManager); + return rawTargetedAppointment; + } + + return getTargetedAppointmentFromInfo( + rawAppointment, + settings, + dataAccessor, + resourceManager, + ); +}; diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index 8fdcb0979027..f8b65aa9eea2 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -13,7 +13,6 @@ const DEFAULT_APPOINTMENT_COLLECTOR_TEMPLATE_NAME = 'appointmentCollector'; const DEFAULT_DROP_DOWN_APPOINTMENT_TEMPLATE_NAME = 'dropDownAppointment'; export const DEFAULT_SCHEDULER_INTERNAL_OPTIONS: SchedulerInternalOptions = { - loadedResources: [], indicatorTime: undefined, renovateRender: true, _draggingMode: 'outlook', diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts index 5ee0dafbfd04..cb6b95d26bdc 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts @@ -1,8 +1,6 @@ import type { template } from '@js/common'; import type { Properties } from '@js/ui/scheduler'; -import type { ResourceLoader } from '../loader/resource_loader'; - export type RawViewType = Required['views'][number]; export type ViewType = Extract; export type ViewObject = Extract; @@ -21,7 +19,6 @@ export type AgendaView = ViewObject & Required { + async loadGroupResources(groups: string[] = [], forceReload = false): Promise { await this.load(groups, forceReload); const { groupTree, groupLeafs } = groupResources(this.resourceById, groups); @@ -48,9 +48,6 @@ export class ResourceManager { this.groups = groups; this.groupsLeafs = groupLeafs; this.groupsTree = groupTree; - - // TODO(9): Get rid of it as soon as you can. Fallback, this class has all necessary - return this.groupResources(); } public groupCount(): number { diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointment.dataProcessor.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointment.dataProcessor.test.ts index 4e03c3863e2d..516aa6c7acf5 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointment.dataProcessor.test.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointment.dataProcessor.test.ts @@ -34,7 +34,7 @@ describe('data processor', () => { currentDate, }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 10, 10), new Date(2015, 1, 10, 13), true, @@ -82,7 +82,7 @@ describe('data processor', () => { ], ]; - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 9, 0), new Date(2015, 1, 10, 13), true, @@ -100,8 +100,8 @@ describe('data processor', () => { const changedDataSource = new DataSource({ store: data, }); - scheduler.appointmentDataProvider.setDataSource(changedDataSource); - scheduler.appointmentDataProvider.filterByDate( + scheduler.option('dataSource', changedDataSource); + scheduler.setRemoteFilter( new Date(2015, 1, 9, 0), new Date(2015, 1, 10, 13), true, @@ -142,7 +142,7 @@ describe('data processor', () => { endDateTimeZoneExpr: 'EndDateTimeZone', }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 0, 1, 1), new Date(2015, 0, 2), ); @@ -150,7 +150,7 @@ describe('data processor', () => { dataSource.load().done(() => { dataSource.filter('priorityId', '=', 1); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 0, 1, 1), new Date(2015, 0, 2), true, @@ -161,7 +161,7 @@ describe('data processor', () => { dataSource.filter(null); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 0, 1, 1), new Date(2015, 0, 2), true, @@ -194,7 +194,7 @@ describe('data processor', () => { currentDate, }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 9, 0), new Date(2015, 1, 10, 13), true, @@ -244,7 +244,7 @@ describe('data processor', () => { currentDate, }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 10, 10), new Date(2015, 1, 10, 13), true, @@ -295,7 +295,7 @@ describe('data processor', () => { currentDate, }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 10, 10), new Date(2015, 1, 10, 13), true, @@ -343,7 +343,7 @@ describe('data processor', () => { currentDate, }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 10, 11, 5), new Date(2015, 1, 10, 11, 45), true, @@ -368,7 +368,7 @@ describe('data processor', () => { currentDate, }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 9), new Date(2015, 1, 20), ); @@ -395,7 +395,7 @@ describe('data processor', () => { allDayExpr: 'AllDay', }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 10, 12), new Date(2015, 1, 11), true, @@ -422,7 +422,7 @@ describe('data processor', () => { currentDate, }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 11), new Date(2015, 1, 11, 11), true, @@ -457,7 +457,7 @@ describe('data processor', () => { recurrenceRuleExpr: '_recurrenceRule', }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 10), new Date(2015, 1, 10, 13), true, @@ -491,7 +491,7 @@ describe('data processor', () => { recurrenceRuleExpr: null, }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 10), new Date(2015, 1, 10, 13), true, @@ -526,7 +526,7 @@ describe('data processor', () => { recurrenceRuleExpr: '', }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 10), new Date(2015, 1, 10, 13), true, @@ -554,7 +554,7 @@ describe('data processor', () => { recurrenceRuleExpr: '', }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 1, 9, 0), new Date(2015, 1, 9, 23, 59), ); @@ -592,7 +592,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 0, 1, 0), new Date(2015, 0, 3), ); @@ -637,7 +637,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2015, 0, 1, 0), new Date(2015, 0, 3), true, @@ -649,7 +649,7 @@ describe('data processor', () => { existingFilter[1] = newUserFilter; dataSource.filter(existingFilter); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2014, 11, 29, 0), new Date(2014, 11, 30), true, @@ -678,9 +678,9 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ text: 'a', StartDate: new Date(2015, 0, 1, 1).toString(), EndDate: new Date(2015, 0, 1, 2).toString() }); - scheduler.appointmentDataProvider.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 30).toString(), EndDate: new Date(2015, 0, 1, 6, 0).toString() }); - scheduler.appointmentDataProvider.add({ text: 'c', StartDate: new Date(2015, 0, 1, 8).toString(), EndDate: new Date(2015, 0, 1, 9).toString() }); + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 1, 1).toString(), EndDate: new Date(2015, 0, 1, 2).toString() }); + scheduler.appointmentDataSource.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 30).toString(), EndDate: new Date(2015, 0, 1, 6, 0).toString() }); + scheduler.appointmentDataSource.add({ text: 'c', StartDate: new Date(2015, 0, 1, 8).toString(), EndDate: new Date(2015, 0, 1, 9).toString() }); scheduler.repaint(); const appts = scheduler._layoutManager.filteredItems; @@ -703,8 +703,8 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ text: 'a', StartDate: new Date(2015, 0, 1, 1).toString(), EndDate: new Date(2015, 0, 1, 3).toString() }); - scheduler.appointmentDataProvider.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 45).toString(), EndDate: new Date(2015, 0, 1, 3, 50).toString() }); + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 1, 1).toString(), EndDate: new Date(2015, 0, 1, 3).toString() }); + scheduler.appointmentDataSource.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 45).toString(), EndDate: new Date(2015, 0, 1, 3, 50).toString() }); scheduler.repaint(); const appts = scheduler._layoutManager.filteredItems; @@ -728,9 +728,9 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ text: 'a', StartDate: new Date(2015, 0, 1, 3).toString(), EndDate: new Date(2015, 0, 1, 3, 10).toString() }); - scheduler.appointmentDataProvider.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 40).toString(), EndDate: new Date(2015, 0, 1, 7, 20).toString() }); - scheduler.appointmentDataProvider.add({ text: 'c', StartDate: new Date(2015, 0, 1, 7, 35).toString(), EndDate: new Date(2015, 0, 1, 9).toString() }); + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 1, 3).toString(), EndDate: new Date(2015, 0, 1, 3, 10).toString() }); + scheduler.appointmentDataSource.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 40).toString(), EndDate: new Date(2015, 0, 1, 7, 20).toString() }); + scheduler.appointmentDataSource.add({ text: 'c', StartDate: new Date(2015, 0, 1, 7, 35).toString(), EndDate: new Date(2015, 0, 1, 9).toString() }); scheduler.repaint(); const appts = scheduler._layoutManager.filteredItems; @@ -753,13 +753,13 @@ describe('data processor', () => { recurrenceExceptionExpr: 'RecException', }); - scheduler.appointmentDataProvider.add({ text: 'a', StartDate: new Date(2015, 0, 1, 1).toString(), EndDate: new Date(2015, 0, 1, 2).toString() }); - scheduler.appointmentDataProvider.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 30).toString(), EndDate: new Date(2015, 0, 1, 6).toString() }); - scheduler.appointmentDataProvider.add({ text: 'c', StartDate: new Date(2015, 0, 1, 8).toString(), EndDate: new Date(2015, 0, 1, 9).toString() }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 1, 1).toString(), EndDate: new Date(2015, 0, 1, 2).toString() }); + scheduler.appointmentDataSource.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 30).toString(), EndDate: new Date(2015, 0, 1, 6).toString() }); + scheduler.appointmentDataSource.add({ text: 'c', StartDate: new Date(2015, 0, 1, 8).toString(), EndDate: new Date(2015, 0, 1, 9).toString() }); + scheduler.appointmentDataSource.add({ text: 'd', StartDate: new Date(2014, 11, 31).toString(), EndDate: new Date(2015, 11, 31, 4).toString(), RecRule: 'FREQ=WEEKLY;BYDAY=WE', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'e', StartDate: new Date(2015, 11, 27).toString(), EndDate: new Date(2015, 11, 27, 4).toString(), RecRule: 'FREQ=WEEKLY,BYDAY=TH', }); @@ -789,10 +789,10 @@ describe('data processor', () => { recurrenceExceptionExpr: 'RecException', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 5, 2, 0).toString(), EndDate: new Date(2015, 0, 5, 4, 0).toString(), RecRule: 'FREQ=WEEKLY;BYDAY=MO', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'b', StartDate: new Date(2015, 0, 5, 6, 0).toString(), EndDate: new Date(2015, 0, 5, 8, 0).toString(), RecRule: 'FREQ=WEEKLY;BYDAY=MO', }); @@ -824,10 +824,10 @@ describe('data processor', () => { recurrenceExceptionExpr: 'RecException', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 5, 2, 0).toString(), EndDate: new Date(2015, 0, 5, 4, 0).toString(), RecRule: 'FREQ=WEEKLY;BYDAY=MO', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'b', StartDate: new Date(2015, 0, 5, 6, 0).toString(), EndDate: new Date(2015, 0, 5, 8, 0).toString(), RecRule: 'FREQ=WEEKLY;BYDAY=MO', }); @@ -870,16 +870,16 @@ describe('data processor', () => { ], }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 2, 16, 2), EndDate: new Date(2015, 2, 16, 2, 30), ownerId: [1, 2], }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'b', StartDate: new Date(2015, 2, 16, 2), EndDate: new Date(2015, 2, 16, 2, 30), ownerId: 1, roomId: [1, 2], managerId: 4, }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'c', StartDate: new Date(2015, 2, 16, 2), EndDate: new Date(2015, 2, 16, 2, 30), ownerId: 3, roomId: [1, 2], }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'd', StartDate: new Date(2015, 2, 16, 2), EndDate: new Date(2015, 2, 16, 2, 30), ownerId: 1, roomId: [1, 2, 3], }); @@ -912,14 +912,14 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 1, 4).toString(), EndDate: new Date(2015, 0, 1, 6).toString(), AllDay: true, }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'b', StartDate: new Date(2015, 0, 1, 3, 30).toString(), EndDate: new Date(2015, 0, 1, 6).toString(), AllDay: false, }); - scheduler.appointmentDataProvider.add({ text: 'c', StartDate: new Date(2015, 0, 1, 8).toString(), EndDate: new Date(2015, 0, 1, 9).toString() }); - scheduler.appointmentDataProvider.add({ text: 'd', StartDate: new Date(2015, 0, 1, 4).toString(), EndDate: new Date(2015, 0, 3, 6).toString() }); + scheduler.appointmentDataSource.add({ text: 'c', StartDate: new Date(2015, 0, 1, 8).toString(), EndDate: new Date(2015, 0, 1, 9).toString() }); + scheduler.appointmentDataSource.add({ text: 'd', StartDate: new Date(2015, 0, 1, 4).toString(), EndDate: new Date(2015, 0, 3, 6).toString() }); scheduler.repaint(); const appts = scheduler._layoutManager.filteredItems; @@ -944,7 +944,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 1).toString(), EndDate: new Date(2015, 0, 2).toString(), AllDay: true, RecurrenceRule: 'FREQ=DAILY', }); @@ -977,7 +977,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 0, 1).toString(), EndDate: new Date(2015, 0, 2).toString(), @@ -989,7 +989,7 @@ describe('data processor', () => { scheduler.repaint(); const appts = scheduler._layoutManager.filteredItems; - expect(!!appts.length).toBe(expectedVisibility); + expect(Boolean(appts.length)).toBe(expectedVisibility); }); }); @@ -1008,7 +1008,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', startDate: new Date(2020, 6, 16, 0), endDate: new Date(2020, 6, 16, 1), @@ -1035,7 +1035,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 2, 1, 10, 30), EndDate: new Date(2015, 2, 2, 5, 0), @@ -1062,7 +1062,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 2, 1, 7, 0), EndDate: new Date(2015, 2, 2, 0, 30), @@ -1089,7 +1089,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 2, 1, 11, 0), EndDate: new Date(2015, 2, 2, 1, 0), @@ -1117,7 +1117,7 @@ describe('data processor', () => { cellDuration: 60, }); - scheduler.appointmentDataProvider.add({ + scheduler.appointmentDataSource.add({ text: 'a', StartDate: new Date(2015, 2, 1, 11, 0), EndDate: new Date(2015, 2, 1, 1, 0), @@ -1168,7 +1168,7 @@ describe('data processor', () => { cellDuration: 60, }); - scheduler.appointmentDataProvider.add(item); + scheduler.appointmentDataSource.add(item); const result = scheduler._layoutManager.hasAllDayAppointments(); @@ -1205,7 +1205,7 @@ describe('data processor', () => { recurrenceExceptionExpr: 'Exception', }); - scheduler.appointmentDataProvider.filterByDate( + scheduler.setRemoteFilter( new Date(2021, 8, 6, 9), new Date(2021, 8, 6, 12), ); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/detect_changes/get_repainted_appointments.ts b/packages/devextreme/js/__internal/scheduler/view_model/detect_changes/get_repainted_appointments.ts deleted file mode 100644 index ff219dc9ad75..000000000000 --- a/packages/devextreme/js/__internal/scheduler/view_model/detect_changes/get_repainted_appointments.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { equalByValue } from '@js/core/utils/common'; -import type { - AgendaViewModelSettings, - AppointmentViewModel, - AppointmentViewModelSettings, - SafeAppointment, -} from '@ts/scheduler/types'; - -import type { - AppointmentDataProvider, -} from '../generate_view_model/data_provider/m_appointment_data_provider'; - -type Settings = AppointmentViewModelSettings & AgendaViewModelSettings & { - columnIndex: number; - rowIndex: number; - positionByMap: number; - topVirtualCellCount: number; - leftVirtualCellCount: number; - leftVirtualWidth: number; - topVirtualHeight: number; - hMax: number; - vMax: number; - reduced?: string; -}; - -const isDataChanged = ( - data: SafeAppointment, - appointmentDataProvider: AppointmentDataProvider, -): boolean => { - const updatedData = appointmentDataProvider.getUpdatedAppointment(); - - return updatedData === data || appointmentDataProvider - .getUpdatedAppointmentKeys() - .some((item) => data[item.key] === item.value); -}; - -const isAppointmentShouldAppear = ( - currentAppointment: AppointmentViewModel, - sourceAppointment: AppointmentViewModel, -): boolean => currentAppointment.needRepaint && sourceAppointment.needRemove; - -const createSettingsToCompare = ( - currentSetting: Settings, -): Partial => { - const leftVirtualCellCount = currentSetting.leftVirtualCellCount || 0; - const topVirtualCellCount = currentSetting.topVirtualCellCount || 0; - const columnIndex = currentSetting.columnIndex + leftVirtualCellCount; - const rowIndex = currentSetting.rowIndex + topVirtualCellCount; - const hMax = currentSetting.reduced ? currentSetting.hMax : undefined; - const vMax = currentSetting.reduced ? currentSetting.vMax : undefined; - - return { - ...currentSetting, - columnIndex, - rowIndex, - positionByMap: undefined, - topVirtualCellCount: undefined, - leftVirtualCellCount: undefined, - leftVirtualWidth: undefined, - topVirtualHeight: undefined, - hMax, - vMax, - info: undefined, - }; -}; - -const isSettingChanged = ( - settings: Settings[], - sourceSetting: Settings[], -): boolean => { - if (settings.length !== sourceSetting.length) { - return true; - } - - for (let i = 0; i < settings.length; i += 1) { - const newSettings = createSettingsToCompare(settings[i]); - const oldSettings = createSettingsToCompare(sourceSetting[i]); - - if (oldSettings) { // exclude sortedIndex property for comparison in commonUtils.equalByValue - oldSettings.sortedIndex = newSettings.sortedIndex; - } - - if (!equalByValue(newSettings, oldSettings)) { - return true; - } - } - - return false; -}; - -const getAssociatedSourceAppointment = ( - currentAppointment: AppointmentViewModel, - sourceAppointments: AppointmentViewModel[], -): AppointmentViewModel | undefined => sourceAppointments - .find((item) => item.itemData === currentAppointment.itemData); - -const getDeletedAppointments = ( - currentAppointments: AppointmentViewModel[], - sourceAppointments: AppointmentViewModel[], -): AppointmentViewModel[] => { - const result: AppointmentViewModel[] = []; - - sourceAppointments.forEach((sourceAppointment) => { - const currentAppointment = getAssociatedSourceAppointment( - sourceAppointment, - currentAppointments, - ); - if (!currentAppointment) { - sourceAppointment.needRemove = true; - result.push(sourceAppointment); - } - }); - - return result; -}; - -export const getRepaintedAppointments = ( - currentAppointments: AppointmentViewModel[], - sourceAppointments: AppointmentViewModel[], - { - appointmentRenderingStrategyName, - appointmentDataProvider, - }: { - appointmentRenderingStrategyName: string; - appointmentDataProvider: AppointmentDataProvider; - }, -): AppointmentViewModel[] => { - if (sourceAppointments.length === 0 || appointmentRenderingStrategyName === 'agenda') { - return currentAppointments; - } - - currentAppointments.forEach((appointment) => { - const sourceAppointment = getAssociatedSourceAppointment(appointment, sourceAppointments); - - if (sourceAppointment) { - const isDataChangedBool = isDataChanged(appointment.itemData, appointmentDataProvider); - const isSettingChangedBool = isSettingChanged( - appointment.settings as unknown as Settings[], - sourceAppointment.settings as unknown as Settings[], - ); - const isAppointmentShouldAppearBool = isAppointmentShouldAppear( - appointment, - sourceAppointment, - ); - - appointment.needRepaint = isDataChangedBool - || isSettingChangedBool - || isAppointmentShouldAppearBool; - } - }); - - return currentAppointments - .concat(getDeletedAppointments(currentAppointments, sourceAppointments)); -}; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtering/m_appointment_filter.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtering/m_appointment_filter.ts index bbcf5cd1540c..be84e84590a4 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtering/m_appointment_filter.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtering/m_appointment_filter.ts @@ -40,8 +40,6 @@ export class AppointmentFilterBaseStrategy { get showAllDayPanel() { return this._resolveOption('showAllDayPanel'); } - get loadedResources() { return this.options.getResourceManager().groupResources(); } - get supportAllDayRow() { return this._resolveOption('supportAllDayRow'); } get viewType() { return this._resolveOption('viewType'); } @@ -92,7 +90,7 @@ export class AppointmentFilterBaseStrategy { return { ...compareOptions, ...this.getIntervals(compareOptions), - resources: this.loadedResources, + resources: this.options.getResourceManager().groupResources(), firstDayOfWeek: this.firstDayOfWeek, allDayPanelFilter, allDayPanelMode: this.allDayPanelMode, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtering/m_appointment_filter_virtual.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtering/m_appointment_filter_virtual.ts index d1e62956d7e1..b9098cfe3d8d 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtering/m_appointment_filter_virtual.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtering/m_appointment_filter_virtual.ts @@ -24,7 +24,7 @@ export class AppointmentFilterVirtualStrategy extends AppointmentFilterBaseStrat const hourMs = toMs('hour'); const isCalculateStartAndEndDayHour = isDateAndTimeView(this.viewType); const endViewDate = this.viewDataProvider.getLastViewDateByEndDayHour(this.viewEndDayHour); - const shiftedEndViewDate = dateUtilsTs.addOffsets(endViewDate, [viewOffset]); + const shiftedEndViewDate = dateUtilsTs.addOffsets(endViewDate, viewOffset); const filterOptions: FilterOptions[] = []; const groupsInfo = this.viewDataProvider.getCompletedGroupsInfo(); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/get_appointment_filter/get_appointment_occurrence_dates.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/get_appointment_filter/get_appointment_occurrence_dates.ts index 03d3ea5cdbf3..46b9a7b56ef5 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/get_appointment_filter/get_appointment_occurrence_dates.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/get_appointment_filter/get_appointment_occurrence_dates.ts @@ -12,14 +12,14 @@ export const getShiftedAllDayStartDate = ( viewOffset: number, ): Date => { const trimmedDate = dateUtils.trimTime(originalStartDate); - const startOfDay = dateUtilsTs.addOffsets(trimmedDate, [viewOffset]); - const endOfDay = dateUtilsTs.addOffsets(trimmedDate, [DAY_WITHOUT_ONE_SECOND_MS, viewOffset]); + const startOfDay = dateUtilsTs.addOffsets(trimmedDate, viewOffset); + const endOfDay = dateUtilsTs.addOffsets(trimmedDate, DAY_WITHOUT_ONE_SECOND_MS, viewOffset); switch (true) { case originalStartDate > endOfDay: - return dateUtilsTs.addOffsets(endOfDay, [SECOND_MS]); + return dateUtilsTs.addOffsets(endOfDay, SECOND_MS); case originalStartDate < startOfDay: - return dateUtilsTs.addOffsets(startOfDay, [-DAY_MS]); + return dateUtilsTs.addOffsets(startOfDay, -DAY_MS); // NOTE: originalStartDate in interval [startOfDay, endOfDay] // (include border points) default: @@ -32,14 +32,14 @@ export const getShiftedAllDayEndDate = ( viewOffset: number, ): Date => { const trimmedDate = dateUtils.trimTime(originalEndDate); - const startOfDay = dateUtilsTs.addOffsets(trimmedDate, [viewOffset]); - const endOfDay = dateUtilsTs.addOffsets(trimmedDate, [DAY_WITHOUT_ONE_SECOND_MS, viewOffset]); + const startOfDay = dateUtilsTs.addOffsets(trimmedDate, viewOffset); + const endOfDay = dateUtilsTs.addOffsets(trimmedDate, DAY_WITHOUT_ONE_SECOND_MS, viewOffset); switch (true) { case originalEndDate > endOfDay: - return dateUtilsTs.addOffsets(endOfDay, [DAY_MS]); + return dateUtilsTs.addOffsets(endOfDay, DAY_MS); case originalEndDate < startOfDay: - return dateUtilsTs.addOffsets(startOfDay, [-SECOND_MS]); + return dateUtilsTs.addOffsets(startOfDay, -SECOND_MS); // NOTE: originalEndDate in interval [startOfDay, endOfDay] // (include border points) default: @@ -70,7 +70,7 @@ export const getAppointmentOccurrenceDates = ( startDate: dateUtils.trimTime(originalStartDate), endDate: dateUtilsTs.addOffsets( dateUtils.trimTime(originalEndDate), - [DAY_WITHOUT_ONE_SECOND_MS], + DAY_WITHOUT_ONE_SECOND_MS, ), }; // NOTE: allDay appointment + viewOffset is set case diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/get_appointment_filter/get_appointments_occurrences.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/get_appointment_filter/get_appointments_occurrences.ts index b6d8e96e6840..cb0057cbf5a5 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/get_appointment_filter/get_appointments_occurrences.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/get_appointment_filter/get_appointments_occurrences.ts @@ -49,6 +49,6 @@ export const getAppointmentsOccurrences = ( return startDates.map((startDate) => ({ ...appointment, startDate, - endDate: dateUtilsTs.addOffsets(startDate, [duration]), + endDate: dateUtilsTs.addOffsets(startDate, duration), })); }; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/shiftIntervals.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/shift_intervals.test.ts similarity index 100% rename from packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/shiftIntervals.test.ts rename to packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/shift_intervals.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/shift_intervals.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/shift_intervals.ts index 9e0d3f109f65..7944c6e2ac42 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/shift_intervals.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtering/utils/shift_intervals.ts @@ -6,6 +6,6 @@ export const shiftIntervals = ( intervals: DateInterval[], viewOffset: number, ): DateInterval[] => intervals.map((interval) => ({ - min: dateUtilsTs.addOffsets(interval.min, [viewOffset]), - max: dateUtilsTs.addOffsets(interval.max, [viewOffset]), + min: dateUtilsTs.addOffsets(interval.min, viewOffset), + max: dateUtilsTs.addOffsets(interval.max, viewOffset), })); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/adapt_agenda_settings.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/adapt_agenda_settings.ts new file mode 100644 index 000000000000..9149a9fedf88 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/adapt_agenda_settings.ts @@ -0,0 +1,43 @@ +import type { TimeZoneCalculator } from '../../r1/timezone_calculator'; +import { AppointmentAdapter } from '../../utils/appointment_adapter/appointment_adapter'; +import type { AppointmentDataAccessor } from '../../utils/data_accessor/appointment_data_accessor'; +import { plainViewModel } from './plain_view_model'; +import type { AppointmentAgendaViewModel, AppointmentViewModelInternal } from './types'; + +export const adaptAgendaSettings = ( + viewModel: AppointmentViewModelInternal[], + dataAccessor: AppointmentDataAccessor, + timeZoneCalculator: TimeZoneCalculator, +): AppointmentAgendaViewModel[] => { + const settings = plainViewModel(viewModel); + + return settings.map((item) => { + const { agendaSettings } = item; + const adapter = new AppointmentAdapter( + agendaSettings ?? item.itemData, + dataAccessor, + ); + + return { + isAgendaModel: true, + itemData: item.itemData, + allDay: Boolean(item.allDay), + groupIndex: item.groupIndex, + sortedIndex: item.sortedIndex, + direction: item.direction, + height: item.height, + width: item.width, + info: { + sourceAppointment: { + allDay: item.allDay, + startDate: adapter.startDate, + endDate: adapter.endDate, + }, + appointment: { + allDay: item.allDay, + ...adapter.getCalculatedDates(timeZoneCalculator, 'toGrid'), + }, + }, + }; + }); +}; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/add_collector.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/add_collector.ts new file mode 100644 index 000000000000..2997b03b3ece --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/add_collector.ts @@ -0,0 +1,114 @@ +import { isDefined } from '@js/core/utils/type'; +import type { TimeZoneCalculator } from '@ts/scheduler/r1/timezone_calculator'; +import type { SafeAppointment } from '@ts/scheduler/types'; + +import { plainViewModel } from './plain_view_model'; +import type { + AppointmentCollectorViewModel, + AppointmentItemViewModel, + AppointmentViewModelInternal, + AppointmentViewModelPlain, + AppointmentViewModelSettingsInternal, +} from './types'; + +const cropSettingsProps = ( + setting: AppointmentViewModelSettingsInternal & { + itemData: SafeAppointment; + }, +): AppointmentItemViewModel => ({ + itemData: setting.itemData, + allDay: Boolean(setting.allDay), + direction: setting.direction, + groupIndex: setting.groupIndex, + sortedIndex: setting.sortedIndex, + skipResizing: setting.skipResizing, + level: setting.index, + maxLevel: setting.count, + info: { + sourceAppointment: setting.info.sourceAppointment, + appointment: setting.info.appointment, + }, + empty: setting.geometry.empty, + left: setting.geometry.left, + top: setting.geometry.top, + height: setting.geometry.height, + width: setting.geometry.width, + isCompact: setting.isCompact, + reduced: setting.appointmentReduced, + partIndex: setting.partIndex, + partTotalCount: setting.partTotalCount, + rowIndex: setting.positionByMap.rowIndex, + columnIndex: setting.positionByMap.columnIndex, +}); + +const processVirtualAppointment = ( + virtualAppointments: Record, + internalViewModelItem: AppointmentViewModelSettingsInternal & { + itemData: SafeAppointment; + }, +): void => { + if (!internalViewModelItem.virtual) { + return; + } + + const virtualAppointment = internalViewModelItem.virtual; + const virtualGroupIndex = virtualAppointment.index; + + if (!isDefined(virtualAppointments[virtualGroupIndex])) { + virtualAppointments[virtualGroupIndex] = { + itemData: internalViewModelItem.itemData, + allDay: Boolean(virtualAppointment.isAllDay), + groupIndex: internalViewModelItem.groupIndex, + sortedIndex: internalViewModelItem.sortedIndex, + top: virtualAppointment.top, + left: virtualAppointment.left, + width: virtualAppointment.width, + height: virtualAppointment.height, + isCompact: virtualAppointment.isCompact, + items: [], + }; + } + + virtualAppointments[virtualGroupIndex].items.push(cropSettingsProps(internalViewModelItem)); +}; + +export const addCollector = ( + viewModel: AppointmentViewModelInternal[], + timeZoneCalculator: TimeZoneCalculator, +): AppointmentViewModelPlain[] => { + const internalViewModelItems = plainViewModel(viewModel); + const result: AppointmentViewModelPlain[] = []; + const virtualAppointments: Record = {}; + + internalViewModelItems.map((item) => ({ + ...item, + info: { + sourceAppointment: item.info.sourceAppointment, + appointment: { + ...item.info.appointment, + startDate: timeZoneCalculator.createDate(item.info.sourceAppointment.startDate, 'toGrid'), + endDate: timeZoneCalculator.createDate(item.info.sourceAppointment.endDate, 'toGrid'), + }, + }, + })).forEach((item) => { + switch (true) { + case Boolean(item.virtual): + processVirtualAppointment(virtualAppointments, item); + break; + default: + result.push(cropSettingsProps(item)); + } + }); + + const combined = [ + ...result, + ...Object.values(virtualAppointments), + ]; + + return combined + .sort((a, b) => a.sortedIndex - b.sortedIndex) + .map((item, sortedIndex) => ({ + ...item, + sortedIndex, + })); +}; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/data_provider/m_appointment_data_provider.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/data_provider/m_appointment_data_provider.ts deleted file mode 100644 index e136734fa3f9..000000000000 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/data_provider/m_appointment_data_provider.ts +++ /dev/null @@ -1,85 +0,0 @@ -import config from '@js/core/config'; - -import { combineRemoteFilter } from '../../../r1/filterting/index'; -import type { AppointmentDataAccessor } from '../../../utils/data_accessor/appointment_data_accessor'; -import { AppointmentDataSource } from './m_appointment_data_source'; - -export class AppointmentDataProvider { - options: any; - - dataSource: any; - - dataAccessors: AppointmentDataAccessor; - - timeZoneCalculator: any; - - appointmentDataSource: AppointmentDataSource; - - constructor(options) { - this.options = options; - this.dataSource = this.options.dataSource; - this.dataAccessors = this.options.dataAccessors; - this.timeZoneCalculator = this.options.timeZoneCalculator; - - this.appointmentDataSource = new AppointmentDataSource(this.dataSource); - } - - get keyName() { - return this.appointmentDataSource.keyName; - } - - get isDataSourceInit() { - return !!this.dataSource; - } - - setDataSource(dataSource): void { - this.dataSource = dataSource; - this.appointmentDataSource.setDataSource(this.dataSource); - } - - updateDataAccessors(dataAccessors): void { - this.dataAccessors = dataAccessors; - } - - // TODO rename to the setRemoteFilter - filterByDate(min, max, remoteFiltering = false, dateSerializationFormat?) { - if (!this.dataSource || !remoteFiltering) { - return; - } - - const dataSourceFilter = this.dataSource.filter(); - const filter = combineRemoteFilter({ - dataSourceFilter, - dataAccessors: this.dataAccessors, - min, - max, - dateSerializationFormat, - forceIsoDateParsing: config().forceIsoDateParsing, - }); - - this.dataSource.filter(filter); - } - - // Appointment data source mappings - cleanState() { this.appointmentDataSource.cleanState(); } - - getUpdatedAppointment() { return this.appointmentDataSource._updatedAppointment; } - - getUpdatedAppointmentKeys() { return this.appointmentDataSource._updatedAppointmentKeys; } - - add(rawAppointment) { - return this.appointmentDataSource.add(rawAppointment); - } - - update(target, rawAppointment) { - return this.appointmentDataSource.update(target, rawAppointment); - } - - remove(rawAppointment) { - return this.appointmentDataSource.remove(rawAppointment); - } - - destroy() { - this.appointmentDataSource.destroy(); - } -} diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/data_provider/m_appointment_data_source.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/data_provider/m_appointment_data_source.ts index fa131fd58b47..af4ee4029f5b 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/data_provider/m_appointment_data_source.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/data_provider/m_appointment_data_source.ts @@ -6,53 +6,53 @@ const STORE_EVENTS = { }; export class AppointmentDataSource { - _updatedAppointmentKeys: any[]; + protected updatedAppointmentKeys: any[]; - _dataSource: any; + protected dataSource: any; - _updatedAppointment: any; + protected updatedAppointment: any; constructor(dataSource) { this.setDataSource(dataSource); - this._updatedAppointmentKeys = []; + this.updatedAppointmentKeys = []; } get keyName() { - const store = this._dataSource.store(); + const store = this.dataSource.store(); return store.key(); } get isDataSourceInit() { - return !!this._dataSource; + return Boolean(this.dataSource); } _getStoreKey(target) { - const store = this._dataSource.store(); + const store = this.dataSource.store(); return store.keyOf(target); } setDataSource(dataSource) { - this._dataSource = dataSource; + this.dataSource = dataSource; this.cleanState(); this._initStoreChangeHandlers(); } _initStoreChangeHandlers() { - const dataSource = this._dataSource; + const { dataSource } = this; const store = dataSource?.store(); if (store) { store.on(STORE_EVENTS.updating, (key) => { const keyName = store.key(); if (keyName) { - this._updatedAppointmentKeys.push({ + this.updatedAppointmentKeys.push({ key: keyName, value: key, }); } else { - this._updatedAppointment = key; + this.updatedAppointment = key; } }); @@ -64,7 +64,7 @@ export class AppointmentDataSource { const itemExists = items.filter((item) => item[keyName] === pushItem.key).length !== 0; if (itemExists) { - this._updatedAppointmentKeys.push({ + this.updatedAppointmentKeys.push({ key: keyName, value: pushItem.key, }); @@ -80,20 +80,20 @@ export class AppointmentDataSource { } getUpdatedAppointment() { - return this._updatedAppointment; + return this.updatedAppointment; } getUpdatedAppointmentKeys() { - return this._updatedAppointmentKeys; + return this.updatedAppointmentKeys; } cleanState() { - this._updatedAppointment = null; - this._updatedAppointmentKeys = []; + this.updatedAppointment = null; + this.updatedAppointmentKeys = []; } add(rawAppointment) { - return this._dataSource.store().insert(rawAppointment).done(() => this._dataSource.load()); + return this.dataSource.store().insert(rawAppointment).done(() => this.dataSource.load()); } update(target, data) { @@ -101,8 +101,8 @@ export class AppointmentDataSource { // @ts-expect-error const d = new Deferred(); - this._dataSource.store().update(key, data) - .done((result) => this._dataSource.load() + this.dataSource.store().update(key, data) + .done((result) => this.dataSource.load() .done(() => d.resolve(result)) .fail(d.reject)) .fail(d.reject); @@ -112,11 +112,11 @@ export class AppointmentDataSource { remove(rawAppointment) { const key = this._getStoreKey(rawAppointment); - return this._dataSource.store().remove(key).done(() => this._dataSource.load()); + return this.dataSource.store().remove(key).done(() => this.dataSource.load()); } destroy() { - const store = this._dataSource?.store(); + const store = this.dataSource?.store(); if (store) { store.off(STORE_EVENTS.updating); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_cell_position_calculator.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_cell_position_calculator.ts index 3590c5f8f819..43a2a4c5fffe 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_cell_position_calculator.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_cell_position_calculator.ts @@ -53,7 +53,7 @@ class BaseStrategy { }); coordinates.forEach((item) => { - !!item && result.push( + Boolean(item) && result.push( this._prepareObject(item, index), ); }); @@ -208,7 +208,7 @@ class BaseStrategy { const { cellDuration, viewOffset } = this.options; const { rowIndex, columnIndex } = positionByMap; const matchedCell = this.viewDataProvider.viewDataMap.dateTableMap[rowIndex][columnIndex]; - const matchedCellStartDate = dateUtilsTs.addOffsets(matchedCell.cellData.startDate, [-viewOffset]); + const matchedCellStartDate = dateUtilsTs.addOffsets(matchedCell.cellData.startDate, -viewOffset); const result = (appointmentDate.getTime() - matchedCellStartDate.getTime()) / cellDuration; // NOTE: Hande DST summer time change issue. diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_settings_generator.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_settings_generator.ts index f0464202955d..d491811f0e14 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_settings_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_settings_generator.ts @@ -114,8 +114,8 @@ export class DateGeneratorBaseStrategy { ...item, source: { ...item.source, - startDate: dateUtilsTs.addOffsets(item.source.startDate, [viewOffset]), - endDate: dateUtilsTs.addOffsets(item.source.endDate, [viewOffset]), + startDate: dateUtilsTs.addOffsets(item.source.startDate, viewOffset), + endDate: dateUtilsTs.addOffsets(item.source.endDate, viewOffset), }, })); } @@ -170,8 +170,6 @@ export class DateGeneratorBaseStrategy { return { ...item, - // TODO: Check usages & delete this field. - exceptionDate: new Date(item.startDate), }; }); @@ -189,7 +187,7 @@ export class DateGeneratorBaseStrategy { return false; } - return !timeZoneUtils.isEqualLocalTimeZone(this.timeZone, appointment.startDate); + return !timeZoneUtils.isEqualLocalTimeZone(this.timeZone); } _getDateOffsetDST(date) { @@ -238,8 +236,6 @@ export class DateGeneratorBaseStrategy { ...item, startDate: newStartDate, endDate: newEndDate, - // TODO: Check usages & delete this field. - exceptionDate: new Date(newStartDate), }; }); } @@ -319,14 +315,13 @@ export class DateGeneratorBaseStrategy { const offsetDifference = appointmentAdapter.startDate.getTimezoneOffset() - source.startDate.getTimezoneOffset(); if (offsetDifference !== 0 && this._canProcessNotNativeTimezoneDates(appointmentAdapter)) { - source.startDate = dateUtilsTs.addOffsets(source.startDate, [offsetDifference * toMs('minute')]); - source.endDate = dateUtilsTs.addOffsets(source.endDate, [offsetDifference * toMs('minute')]); - source.exceptionDate = new Date(source.startDate); + source.startDate = dateUtilsTs.addOffsets(source.startDate, offsetDifference * toMs('minute')); + source.endDate = dateUtilsTs.addOffsets(source.endDate, offsetDifference * toMs('minute')); } const duration = source.endDate.getTime() - source.startDate.getTime(); const startDate = this.timeZoneCalculator.createDate(source.startDate, 'toGrid'); - const endDate = dateUtilsTs.addOffsets(startDate, [duration]); + const endDate = dateUtilsTs.addOffsets(startDate, duration); return { startDate, @@ -364,15 +359,15 @@ export class DateGeneratorBaseStrategy { const { viewOffset } = this.options; // NOTE: For creating a recurrent appointments, // we should use original appointment's dates (without view offset). - const originalAppointmentStartDate = dateUtilsTs.addOffsets(appointment.startDate, [viewOffset]); - const originalAppointmentEndDate = dateUtilsTs.addOffsets(appointment.endDate, [viewOffset]); + const originalAppointmentStartDate = dateUtilsTs.addOffsets(appointment.startDate, viewOffset); + const originalAppointmentEndDate = dateUtilsTs.addOffsets(appointment.endDate, viewOffset); const [ minRecurrenceDate, maxRecurrenceDate, ] = this._createExtremeRecurrenceDates(groupIndex); - const shiftedMinRecurrenceDate = dateUtilsTs.addOffsets(minRecurrenceDate, [viewOffset]); - const shiftedMaxRecurrenceDate = dateUtilsTs.addOffsets(maxRecurrenceDate, [viewOffset]); + const shiftedMinRecurrenceDate = dateUtilsTs.addOffsets(minRecurrenceDate, viewOffset); + const shiftedMaxRecurrenceDate = dateUtilsTs.addOffsets(maxRecurrenceDate, viewOffset); return { rule: appointment.recurrenceRule, @@ -434,8 +429,8 @@ export class DateGeneratorBaseStrategy { // NOTE: For the next calculations, // we should shift recurrence appointments by view offset. .map(({ startDate, endDate }) => ({ - startDate: dateUtilsTs.addOffsets(startDate, [-viewOffset]), - endDate: dateUtilsTs.addOffsets(endDate, [-viewOffset]), + startDate: dateUtilsTs.addOffsets(startDate, -viewOffset), + endDate: dateUtilsTs.addOffsets(endDate, -viewOffset), })); } @@ -444,15 +439,15 @@ export class DateGeneratorBaseStrategy { return appointments.map((appointment: any): Date => { const tableFirstDate = this._getAppointmentFirstViewDate({ ...appointment, - startDate: dateUtilsTs.addOffsets(appointment.startDate, [viewOffset]), - endDate: dateUtilsTs.addOffsets(appointment.endDate, [viewOffset]), + startDate: dateUtilsTs.addOffsets(appointment.startDate, viewOffset), + endDate: dateUtilsTs.addOffsets(appointment.endDate, viewOffset), }); if (!tableFirstDate) { return appointment.startDate as Date; } - const firstDate = dateUtilsTs.addOffsets(tableFirstDate, [-viewOffset]); + const firstDate = dateUtilsTs.addOffsets(tableFirstDate, -viewOffset); return firstDate > appointment.startDate ? firstDate diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_view_model_generator.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_view_model_generator.ts index b3cb7703a388..2452ff2648b2 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_view_model_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/m_view_model_generator.ts @@ -1,7 +1,8 @@ import { dateUtilsTs } from '@ts/core/utils/date'; -import { getAppointmentKey } from '@ts/scheduler/r1/utils/index'; -import type { AppointmentViewModel, RenderStrategyName, SafeAppointment } from '@ts/scheduler/types'; +import type { RenderStrategyName, SafeAppointment } from '@ts/scheduler/types'; +import { adaptAgendaSettings } from './adapt_agenda_settings'; +import { addCollector } from './add_collector'; import AgendaAppointmentsStrategy from './rendering_strategies/m_strategy_agenda'; import type BaseAppointmentsStrategy from './rendering_strategies/m_strategy_base'; import HorizontalAppointmentsStrategy from './rendering_strategies/m_strategy_horizontal'; @@ -9,6 +10,7 @@ import HorizontalMonthAppointmentsStrategy from './rendering_strategies/m_strate import HorizontalMonthLineAppointmentsStrategy from './rendering_strategies/m_strategy_horizontal_month_line'; import VerticalAppointmentsStrategy from './rendering_strategies/m_strategy_vertical'; import WeekAppointmentRenderingStrategy from './rendering_strategies/m_strategy_week'; +import type { AppointmentViewModelInternal } from './types'; const RENDERING_STRATEGIES: Record = { horizontal: HorizontalAppointmentsStrategy, @@ -32,7 +34,12 @@ export class AppointmentViewModelGenerator { } generate(filteredItems: SafeAppointment[], options) { - const { viewOffset } = options; + const { + viewOffset, + appointmentRenderingStrategyName, + dataAccessors, + timeZoneCalculator, + } = options; const appointments = filteredItems ? filteredItems.slice() : []; @@ -43,14 +50,24 @@ export class AppointmentViewModelGenerator { const positionMap = renderingStrategy.createTaskPositionMap(appointments); // appointments are mutated inside! const shiftedViewModel = this.postProcess(appointments, positionMap); const viewModel = this.unshiftViewModelAppointmentsByViewOffset(shiftedViewModel, viewOffset); + viewModel.forEach((item) => { + item.settings.forEach((settings) => { + settings.geometry = appointmentRenderingStrategyName === 'agenda' + ? undefined + : renderingStrategy.getAppointmentGeometry(settings); + }); + }); + const viewModelPlain = appointmentRenderingStrategyName === 'agenda' + ? adaptAgendaSettings(viewModel, dataAccessors, timeZoneCalculator) + : addCollector(viewModel, timeZoneCalculator); return { positionMap, - viewModel, + viewModel: viewModelPlain, }; } - postProcess(filteredItems: SafeAppointment[], positionMap): AppointmentViewModel[] { + postProcess(filteredItems: SafeAppointment[], positionMap): AppointmentViewModelInternal[] { const renderingStrategy = this.getRenderingStrategy(); return filteredItems.map((data, index) => { @@ -67,7 +84,7 @@ export class AppointmentViewModelGenerator { : 'horizontal'; }); - const item: AppointmentViewModel = { + const item: AppointmentViewModelInternal = { itemData: data, settings: appointmentSettings, needRepaint: true, @@ -78,146 +95,32 @@ export class AppointmentViewModelGenerator { }); } - makeRenovatedViewModels(viewModel, supportAllDayRow, isVerticalGrouping) { - const strategy = this.getRenderingStrategy(); - const regularViewModels: any = []; - const allDayViewModels: any = []; - const compactOptions: any = []; - const isAllDayPanel = supportAllDayRow && !isVerticalGrouping; - - viewModel.forEach(({ itemData, settings }) => { - settings.forEach((options) => { - const item = this.prepareViewModel(options, strategy, itemData); - if (options.isCompact) { - compactOptions.push({ - compactViewModel: options.virtual, - appointmentViewModel: item, - }); - } else if (options.allDay && isAllDayPanel) { - allDayViewModels.push(item); - } else { - regularViewModels.push(item); - } - }); - }); - - const compactViewModels = this.prepareCompactViewModels(compactOptions, supportAllDayRow); - - const result = { - allDay: allDayViewModels, - regular: regularViewModels, - ...compactViewModels, - }; - - return result; - } - - prepareViewModel(options, strategy, itemData) { - const geometry = strategy.getAppointmentGeometry(options); - - const viewModel = { - key: getAppointmentKey(geometry), - appointment: itemData, - geometry: { - ...geometry, - // TODO move to the rendering strategies - leftVirtualWidth: options.leftVirtualWidth, - topVirtualHeight: options.topVirtualHeight, - }, - info: { - ...options.info, - allDay: options.allDay, - direction: options.direction, - appointmentReduced: options.appointmentReduced, - groupIndex: options.groupIndex, - }, - }; - - return viewModel; - } - - getCompactViewModelFrame(compactViewModel) { - return { - isAllDay: !!compactViewModel.isAllDay, - isCompact: compactViewModel.isCompact, - groupIndex: compactViewModel.groupIndex, - geometry: { - left: compactViewModel.left, - top: compactViewModel.top, - width: compactViewModel.width, - height: compactViewModel.height, - }, - items: { - colors: [], - data: [], - settings: [], - }, - }; - } - - prepareCompactViewModels(compactOptions, supportAllDayRow) { - const regularCompact = {}; - const allDayCompact = {}; - - compactOptions.forEach(({ compactViewModel, appointmentViewModel }) => { - const { - index, - isAllDay, - } = compactViewModel; - const viewModel = isAllDay && supportAllDayRow - ? allDayCompact - : regularCompact; - - if (!viewModel[index]) { - viewModel[index] = this.getCompactViewModelFrame(compactViewModel); - } - - const { - settings, - data, - colors, - } = viewModel[index].items; - - settings.push(appointmentViewModel); - data.push(appointmentViewModel.appointment); - colors.push(appointmentViewModel.info.resourceColor); - }); - - const toArray = (items) => Object - .keys(items) - .map((key) => ({ key, ...items[key] })); - - const allDayViewModels = toArray(allDayCompact); - const regularViewModels = toArray(regularCompact); - - return { - allDayCompact: allDayViewModels, - regularCompact: regularViewModels, - }; - } - // NOTE: Unfortunately, we cannot implement immutable behavior here // because in this case it will break the refs (keys) of dataSource's appointments, // and it will break appointment updates :( private unshiftViewModelAppointmentsByViewOffset( - viewModel: AppointmentViewModel[], + viewModel: AppointmentViewModelInternal[], viewOffset: number, - ): AppointmentViewModel[] { + ): AppointmentViewModelInternal[] { const processedAppointments = new Set(); // eslint-disable-next-line no-restricted-syntax for (const model of viewModel) { // eslint-disable-next-line no-restricted-syntax for (const setting of model.settings ?? []) { - const appointment = (setting as any)?.info?.appointment; + const appointment = setting?.info?.appointment as { + startDate: Date; + endDate: Date; + normalizedEndDate: Date; + } | undefined; if (appointment && !processedAppointments.has(appointment)) { appointment.startDate = dateUtilsTs - .addOffsets(appointment.startDate, [viewOffset]); + .addOffsets(appointment.startDate, viewOffset); appointment.endDate = dateUtilsTs - .addOffsets(appointment.endDate, [viewOffset]); + .addOffsets(appointment.endDate, viewOffset); appointment.normalizedEndDate = dateUtilsTs - .addOffsets(appointment.normalizedEndDate, [viewOffset]); + .addOffsets(appointment.normalizedEndDate, viewOffset); processedAppointments.add(appointment); } } diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/plain_view_model.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/plain_view_model.ts new file mode 100644 index 000000000000..7b4f7d5d60e1 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/plain_view_model.ts @@ -0,0 +1,18 @@ +import type { SafeAppointment } from '@ts/scheduler/types'; + +import type { + AgendaViewModelSettingsInternal, + AppointmentViewModelInternal, + AppointmentViewModelSettingsInternal, +} from './types'; + +export const plainViewModel = ( + viewModel: AppointmentViewModelInternal[], +): (AppointmentViewModelSettingsInternal & AgendaViewModelSettingsInternal & { + itemData: SafeAppointment; +})[] => viewModel.flatMap((appointment) => appointment.settings.map( + (setting) => ({ + ...setting, + itemData: appointment.itemData, + }), +)); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_agenda.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_agenda.ts index 489c3b251dca..8fbd21b59979 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_agenda.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_agenda.ts @@ -17,9 +17,6 @@ class AgendaRenderingStrategy extends BaseRenderingStrategy { getAppointmentMinSize() { } - getDeltaTime() { - } - keepAppointmentSettings() { return true; } @@ -120,10 +117,6 @@ class AgendaRenderingStrategy extends BaseRenderingStrategy { return undefined; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _getDeltaWidth(args, initialSize) { - } - _getAppointmentMaxWidth() { return this.cellWidth; } diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_base.ts index e3506ae42a2f..a7c722f27515 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_base.ts @@ -128,18 +128,10 @@ class BaseRenderingStrategy { return false; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getDeltaTime(args, initialSize, appointment) { - } - getAppointmentGeometry(coordinates) { return coordinates; } - needCorrectAppointmentDates() { - return true; - } - getDirection() { return 'horizontal'; } @@ -175,13 +167,6 @@ class BaseRenderingStrategy { return this._getExtendedPositionMap(map, resultPositions); } - _getDeltaWidth(args, initialSize): any { - const intervalWidth = this.resizableStep || this.getAppointmentMinSize(); - const initialWidth = initialSize.width; - - return Math.round((args.width - initialWidth) / intervalWidth); - } - _correctRtlCoordinates(coordinates) { const width = coordinates[0].width || this._getAppointmentMaxWidth(); @@ -411,7 +396,7 @@ class BaseRenderingStrategy { } _isItemsCross(firstItem, secondItem) { - const areItemsInTheSameTable = !!firstItem.allDay === !!secondItem.allDay; + const areItemsInTheSameTable = Boolean(firstItem.allDay) === Boolean(secondItem.allDay); const areItemsAllDay = firstItem.allDay && secondItem.allDay; if (areItemsInTheSameTable) { @@ -611,8 +596,8 @@ class BaseRenderingStrategy { endDate: Date, ): number { const { viewOffset } = this.options; - const originalStartDate = dateUtilsTs.addOffsets(startDate, [viewOffset]); - const originalEndDate = dateUtilsTs.addOffsets(endDate, [viewOffset]); + const originalStartDate = dateUtilsTs.addOffsets(startDate, viewOffset); + const originalEndDate = dateUtilsTs.addOffsets(endDate, viewOffset); const daylightDiff = timeZoneUtils.getDaylightOffset(originalStartDate, originalEndDate); const correctedDuration: number = this._needAdjustDuration(daylightDiff) ? this._calculateDurationByDaylightDiff(duration, daylightDiff) @@ -647,11 +632,14 @@ class BaseRenderingStrategy { if ((coordinates.count - countFullWidthAppointmentInCell) > 0) { const { top, left } = coordinates; const compactRender = this.isAdaptive || !isAllDay && this.supportCompactDropDownAppointments(); + const width = this.getDropDownAppointmentWidth(this.intervalCount, isAllDay) - this.options._collectorOffset; + const height = this.getDropDownAppointmentHeight(); + const rtlOffset = this.rtlEnabled ? width : 0; coordinates.virtual = { - left: left + this._getCollectorLeftOffset(isAllDay), + left: left + this._getCollectorLeftOffset(isAllDay) + rtlOffset, top, - width: this.getDropDownAppointmentWidth(this.intervalCount, isAllDay), - height: this.getDropDownAppointmentHeight(), + width, + height, index: this._generateAppointmentCollectorIndex(coordinates, isAllDay), isAllDay, groupIndex: coordinates.groupIndex, @@ -854,15 +842,6 @@ class BaseRenderingStrategy { return this._getAppointmentDefaultWidth(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _needVerticalGroupBounds(allDay) { - return false; - } - - _needHorizontalGroupBounds() { - return false; - } - getAppointmentDurationInMs(apptStartDate, apptEndDate, allDay) { if (allDay) { const appointmentDuration = apptEndDate.getTime() - apptStartDate.getTime(); @@ -909,8 +888,8 @@ class BaseRenderingStrategy { let startDate = this.dataAccessors.get('startDate', appointment); let endDate = this.dataAccessors.get('endDate', appointment); - startDate = dateUtilsTs.addOffsets(startDate, [-viewOffset]); - endDate = dateUtilsTs.addOffsets(endDate, [-viewOffset]); + startDate = dateUtilsTs.addOffsets(startDate, -viewOffset); + endDate = dateUtilsTs.addOffsets(endDate, -viewOffset); return { ...appointment, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal.ts index 6a047c24eb5c..9a9156e98bd6 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal.ts @@ -99,15 +99,6 @@ class HorizontalRenderingStrategy extends BaseAppointmentsStrategy { return this.cellWidth - DROP_DOWN_BUTTON_OFFSET * 2; } - getDeltaTime(args, initialSize) { - let deltaTime = 0; - const deltaWidth = args.width - initialSize.width; - - deltaTime = toMs('minute') * Math.round(deltaWidth / this.cellWidth * this.cellDurationInMinutes); - - return deltaTime; - } - isAllDay(appointmentData) { return this.dataAccessors.get('allDay', appointmentData); } diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal_month.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal_month.ts index bd7cea7cfbfd..bf00c54d5d50 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal_month.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal_month.ts @@ -174,18 +174,6 @@ class HorizontalMonthRenderingStrategy extends HorizontalMonthLineRenderingStrat const offset = intervalCount > 1 ? MONTH_DROPDOWN_APPOINTMENT_MAX_RIGHT_OFFSET : MONTH_DROPDOWN_APPOINTMENT_MIN_RIGHT_OFFSET; return this.cellWidth - offset; } - - needCorrectAppointmentDates() { - return false; - } - - _needVerticalGroupBounds() { - return false; - } - - _needHorizontalGroupBounds() { - return true; - } } export default HorizontalMonthRenderingStrategy; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal_month_line.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal_month_line.ts index 17a98d5cde43..10bf48dc6695 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal_month_line.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_horizontal_month_line.ts @@ -5,9 +5,6 @@ import type { SafeAppointment } from '@ts/scheduler/types'; import { sortAppointmentsByStartDate } from '../../../appointments/utils/m_utils'; import HorizontalAppointmentsStrategy from './m_strategy_horizontal'; -const HOURS_IN_DAY = 24; -const MINUTES_IN_HOUR = 60; -const MILLISECONDS_IN_MINUTE = 60000; const ZERO_APPOINTMENT_DURATION_IN_DAYS = 1; class HorizontalMonthLineRenderingStrategy extends HorizontalAppointmentsStrategy { @@ -46,10 +43,6 @@ class HorizontalMonthLineRenderingStrategy extends HorizontalAppointmentsStrateg return (adjustedDuration / dateUtils.dateToMilliseconds('day')) || ZERO_APPOINTMENT_DURATION_IN_DAYS; } - getDeltaTime(args, initialSize) { - return HOURS_IN_DAY * MINUTES_IN_HOUR * MILLISECONDS_IN_MINUTE * this._getDeltaWidth(args, initialSize); - } - isAllDay() { return false; } @@ -75,10 +68,6 @@ class HorizontalMonthLineRenderingStrategy extends HorizontalAppointmentsStrateg return result; } - needCorrectAppointmentDates() { - return false; - } - getPositionShift(timeShift) { return { top: 0, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_vertical.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_vertical.ts index daca674c6173..1c669b8b55a7 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_vertical.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/rendering_strategies/m_strategy_vertical.ts @@ -14,18 +14,6 @@ const ALLDAY_APPOINTMENT_MAX_VERTICAL_OFFSET = 20; const toMs = dateUtils.dateToMilliseconds; class VerticalRenderingStrategy extends BaseAppointmentsStrategy { - getDeltaTime(args, initialSize, appointment) { - let deltaTime = 0; - - if (this.isAllDay(appointment)) { - deltaTime = this._getDeltaWidth(args, initialSize) * toMs('day'); - } else { - const deltaHeight = args.height - initialSize.height; - deltaTime = toMs('minute') * Math.round(deltaHeight / this.cellHeight * this.cellDurationInMinutes); - } - return deltaTime; - } - _correctCollectorCoordinatesInAdaptive(coordinates, isAllDay) { if (isAllDay) { super._correctCollectorCoordinatesInAdaptive(coordinates, isAllDay); @@ -216,7 +204,7 @@ class VerticalRenderingStrategy extends BaseAppointmentsStrategy { let tailHeight = this._getTailHeight(appointmentGeometry, appointmentSettings); let { columnIndex } = appointmentSettings; - while (tailHeight > 0 && left < hMax) { + while (tailHeight > 0 && left < Math.round(hMax)) { tailHeight = Math.max(minHeight, tailHeight); columnIndex += cellsDiff; const height = Math.min(tailHeight, maxHeight); @@ -380,7 +368,7 @@ class VerticalRenderingStrategy extends BaseAppointmentsStrategy { } _sortCondition(a, b) { - if (!!a.allDay !== !!b.allDay) { + if (Boolean(a.allDay) !== Boolean(b.allDay)) { return a.allDay ? 1 : -1; } @@ -435,15 +423,6 @@ class VerticalRenderingStrategy extends BaseAppointmentsStrategy { return this.allDayHeight || this.getAppointmentMinSize(); } - // eslint-disable-next-line class-methods-use-this - _needVerticalGroupBounds(allDay) { - return !allDay; - } - - _needHorizontalGroupBounds() { - return false; - } - getPositionShift(timeShift, isAllDay) { if (!isAllDay && this.isAdaptive && this._getMaxAppointmentCountPerCellByType(isAllDay) === 0) { return { diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/types.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/types.ts new file mode 100644 index 000000000000..0f529abf912d --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/types.ts @@ -0,0 +1,114 @@ +import type { AppointmentGeometry, SafeAppointment } from '@ts/scheduler/types'; + +interface SimpleAppointment { + allDay?: boolean; + startDate: Date; + endDate: Date; +} + +export interface BaseViewModelSettingsInternal { + allDay?: boolean; + direction: string; + groupIndex: number; + sortedIndex: number; + // added by getAppointmentGeometry + skipResizing?: true; + geometry: AppointmentGeometry; + virtual?: { + left: number; + top: number; + width: number; + height: number; + index: string; + isAllDay?: boolean; + groupIndex: number; + isCompact: true; + }; +} + +export interface AppointmentViewModelSettingsInternal extends BaseViewModelSettingsInternal { + index: number; + count: number; + info: { + sourceAppointment: SimpleAppointment; + appointment: SimpleAppointment; + }; + positionByMap: { + rowIndex: number; + columnIndex: number; + }; + left: number; + top: number; + height: number; + width: number; + isCompact: boolean; + appointmentReduced: 'head' | 'body' | 'tail' | undefined; + partIndex: number; + partTotalCount: number; +} + +export interface AgendaViewModelSettingsInternal extends BaseViewModelSettingsInternal { + agendaSettings?: SafeAppointment; + height: number; + width: string; +} + +export interface AppointmentViewModelInternal { + itemData: SafeAppointment; // it will save in DOM by key: dxItemData + needRepaint: boolean; + needRemove: boolean; + settings: (AppointmentViewModelSettingsInternal & AgendaViewModelSettingsInternal)[]; +} + +interface AppointmentInfo { + info: { + sourceAppointment: SimpleAppointment; + appointment: SimpleAppointment; + }; +} + +export interface BaseAppointmentViewModel { + itemData: SafeAppointment; + allDay: boolean; + groupIndex: number; + sortedIndex: number; +} + +export interface AppointmentCollectorViewModel extends BaseAppointmentViewModel { + top: number; + left: number; + height: number; + width: number; + isCompact: boolean; + items: AppointmentItemViewModel[]; +} + +export interface AppointmentAgendaViewModel extends BaseAppointmentViewModel, AppointmentInfo { + isAgendaModel: true; + direction: string; + height: number; + width: string; +} + +export interface AppointmentItemViewModel extends BaseAppointmentViewModel, AppointmentInfo { + direction: string; + skipResizing?: true; + level: number; + maxLevel: number; + empty: boolean; + left: number; + top: number; + height: number; + width: number; + isCompact: boolean; + reduced: 'head' | 'body' | 'tail' | undefined; + partIndex: number; + partTotalCount: number; + rowIndex: number; + columnIndex: number; +} + +export type AppointmentViewModelPlain = + | AppointmentAgendaViewModel + | AppointmentItemViewModel + | AppointmentCollectorViewModel; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/m_appointments_layout_manager.ts b/packages/devextreme/js/__internal/scheduler/view_model/m_appointments_layout_manager.ts index 6369602af747..0fe6d2aa2e9b 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/m_appointments_layout_manager.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/m_appointments_layout_manager.ts @@ -5,16 +5,15 @@ import type Scheduler from '../m_scheduler'; import { getCellDuration } from '../r1/utils/index'; import type { AppointmentDataItem, - AppointmentViewModel, RenderStrategyName, SafeAppointment, ViewType, } from '../types'; import type { ResourceManager } from '../utils/resource_manager/resource_manager'; import { getAllDayHeight, getCellHeight, getCellWidth } from '../workspaces/helpers/m_position_helper'; -import { getRepaintedAppointments } from './detect_changes/get_repainted_appointments'; import { createAppointmentFilter } from './filtering/create_appointment_filter'; import { AppointmentViewModelGenerator } from './generate_view_model/m_view_model_generator'; +import type { AppointmentViewModelPlain } from './generate_view_model/types'; import { getAppointmentDataItems } from './preparation/get_appointment_data_items'; const toMs = dateUtils.dateToMilliseconds; @@ -94,9 +93,8 @@ class AppointmentLayoutManager { ); return { resources: this.instance.option('resources'), - loadedResources: this.instance.option('loadedResources'), getResourceManager: (): ResourceManager => this.instance.resourceManager, - getAppointmentColor: this.instance.createGetAppointmentColor(), + getAppointmentColor: (config) => this.instance.resourceManager.getAppointmentColor(config), dataAccessors: this.instance._dataAccessors, appointmentRenderingStrategyName: this.appointmentRenderingStrategyName, adaptivityEnabled: this.instance.option('adaptivityEnabled'), @@ -146,6 +144,7 @@ class AppointmentLayoutManager { intervalDuration: workspace.getIntervalDuration(), allDayIntervalDuration: workspace.getIntervalDuration(true), isVerticalGroupOrientation: workspace.isVerticalOrientation(), + _collectorOffset: this.instance.getCollectorOffset(), DOMMetaData, // agenda only instance: this.instance, @@ -153,7 +152,7 @@ class AppointmentLayoutManager { }; } - public createAppointmentsMap(): AppointmentViewModel[] { + public createAppointmentsMap(): AppointmentViewModelPlain[] { const renderingStrategyOptions = this._getRenderingStrategyOptions(); const { @@ -161,21 +160,11 @@ class AppointmentLayoutManager { positionMap, } = this.appointmentViewModel.generate(this.filteredItems, renderingStrategyOptions) as any; - this._positionMap = positionMap; // TODO get rid of this after remove old render + this._positionMap = positionMap; return viewModel; } - public getRepaintedAppointments( - currentAppointments: AppointmentViewModel[], - sourceAppointments: AppointmentViewModel[], - ): AppointmentViewModel[] { - return getRepaintedAppointments(currentAppointments, sourceAppointments, { - appointmentRenderingStrategyName: this.appointmentRenderingStrategyName, - appointmentDataProvider: this.instance.appointmentDataProvider, - }); - } - public getRenderingStrategyInstance() { const renderingStrategy = this.appointmentViewModel.getRenderingStrategy(); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts index 491be82ee06f..003258dcf3a6 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts @@ -8,6 +8,7 @@ import dateUtils from '@js/core/utils/date'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; import { setHeight, setOuterHeight } from '@js/core/utils/size'; +import { EMPTY_ACTIVE_STATE_UNIT } from '@ts/core/widget/widget'; import { DATE_TABLE_CLASS, @@ -47,19 +48,21 @@ class SchedulerAgenda extends WorkSpace { _$noDataContainer: any; + // eslint-disable-next-line class-methods-use-this + protected _activeStateUnit(): string { + return EMPTY_ACTIVE_STATE_UNIT; + } + get type() { return VIEWS.AGENDA; } get renderingStrategy() { return (this.invoke as any)('getLayoutManager').getRenderingStrategyInstance(); } - get appointmentDataProvider() { return (this.option('getAppointmentDataProvider') as any)(); } - getStartViewDate() { return this._startViewDate; } _init() { super._init(); - this._activeStateUnit = undefined; } _getDefaultOptions() { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index d1d52839ab73..249116199f90 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -228,14 +228,23 @@ class SchedulerTimeline extends SchedulerWorkSpace { let result = cellCount * (this.option('hoursInterval') as any) * toMs('hour'); if (!allDay) { - if (currentDate.getHours() < startDayHour) { - tailDelta = tailDuration - hiddenInterval + gapBeforeAppt; - } else if (currentDate.getHours() >= startDayHour && currentDate.getHours() < endDayHour) { - tailDelta = tailDuration; - } else if (currentDate.getHours() >= startDayHour && currentDate.getHours() >= endDayHour) { - tailDelta = tailDuration - (gapBeforeAppt - endDayHour * toMs('hour')); - } else if (!fullDays) { - result = fullInterval; + const hour = currentDate.getHours(); + + switch (true) { + case hour < startDayHour: + tailDelta = tailDuration - hiddenInterval + gapBeforeAppt; + break; + case hour >= startDayHour && hour < endDayHour: + tailDelta = tailDuration; + break; + case hour >= endDayHour: + tailDelta = tailDuration - (gapBeforeAppt - endDayHour * toMs('hour')); + break; + case !fullDays: + result = fullInterval; + break; + default: + break; } result += tailDelta; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_virtual_scrolling.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_virtual_scrolling.ts index ce3f558bc8d6..944f7e3b8616 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_virtual_scrolling.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_virtual_scrolling.ts @@ -235,8 +235,8 @@ export class VirtualScrollingDispatcher { } createVirtualScrolling() { - const isVerticalVirtualScrollingCreated = !!this.verticalVirtualScrolling; - const isHorizontalVirtualScrollingCreated = !!this.horizontalVirtualScrolling; + const isVerticalVirtualScrollingCreated = Boolean(this.verticalVirtualScrolling); + const isHorizontalVirtualScrollingCreated = Boolean(this.horizontalVirtualScrolling); if (this.verticalScrollingAllowed !== isVerticalVirtualScrollingCreated || this.horizontalScrollingAllowed !== isHorizontalVirtualScrollingCreated) { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index 2147260659c7..5a56217990c5 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -31,6 +31,7 @@ import { getWindow, hasWindow } from '@js/core/utils/window'; import type { dxSchedulerOptions } from '@js/ui/scheduler'; import Scrollable from '@js/ui/scroll_view/ui.scrollable'; import errors from '@js/ui/widget/ui.errors'; +import Widget from '@js/ui/widget/ui.widget'; import { getMemoizeScrollTo } from '@ts/core/utils/scroll'; import { AllDayPanelTitleComponent, @@ -51,7 +52,7 @@ import { } from '@ts/scheduler/r1/utils/index'; import type { SafeAppointment, ViewType } from '@ts/scheduler/types'; -import WidgetObserver from '../base/m_widget_observer'; +import type NotifyScheduler from '../base/m_widget_notify_scheduler'; import { APPOINTMENT_SETTINGS_KEY } from '../constants'; import { Cache } from '../global_cache'; import AppointmentDragBehavior from '../m_appointment_drag_behavior'; @@ -66,6 +67,7 @@ import { VERTICAL_GROUP_COUNT_CLASSES, VIRTUAL_CELL_CLASS, } from '../m_classes'; +import type { SubscribeKey, SubscribeMethods } from '../m_subscribes'; import tableCreatorModule from '../m_table_creator'; import { utils } from '../m_utils'; import VerticalShader from '../shaders/m_current_time_shader_vertical'; @@ -109,8 +111,8 @@ const { tableCreator } = tableCreatorModule; // The constant is needed so that the dragging is not sharp. To prevent small twitches const DRAGGING_MOUSE_FAULT = 10; -// @ts-expect-error -const { abstract } = WidgetObserver; +// @ts-expect-error Widget exposes a static abstract() helper not typed in its d.ts +const { abstract } = Widget; const toMs = dateUtils.dateToMilliseconds; const COMPONENT_CLASS = 'dx-scheduler-work-space'; @@ -202,7 +204,7 @@ type WorkspaceOptionsInternal = Omit & { startDayHour: number; endDayHour: number; }; -class SchedulerWorkSpace extends WidgetObserver { +class SchedulerWorkSpace extends Widget { _viewDataProvider: any; _cache: any; @@ -271,8 +273,6 @@ class SchedulerWorkSpace extends WidgetObserver { _$timePanel: any; - _activeStateUnit: any; - positionHelper!: PositionHelper; _$headerPanelContainer: any; @@ -311,6 +311,13 @@ class SchedulerWorkSpace extends WidgetObserver { renovatedHeaderPanel: any; + readonly viewDirection: 'vertical' | 'horizontal' = 'vertical'; + + // eslint-disable-next-line class-methods-use-this + protected _activeStateUnit(): string { + return CELL_SELECTOR; + } + // eslint-disable-next-line @typescript-eslint/class-literal-property-style get type(): string { return ''; @@ -381,8 +388,6 @@ class SchedulerWorkSpace extends WidgetObserver { get verticalGroupTableClass() { return WORKSPACE_VERTICAL_GROUP_TABLE_CLASS; } - readonly viewDirection: 'vertical' | 'horizontal' = 'vertical'; - get renovatedHeaderPanelComponent() { return HeaderPanelComponent; } get timeZoneCalculator(): any { @@ -393,6 +398,26 @@ class SchedulerWorkSpace extends WidgetObserver { return this.option('draggingMode') === 'default'; } + notifyObserver( + funcName: Subject, + args: Parameters, + ): void { + this.invoke(funcName, ...args); + } + + invoke( + funcName: Subject, + ...args: Parameters + ): ReturnType | undefined { + const notifyScheduler = this.option('notifyScheduler') as NotifyScheduler | undefined; + + if (!notifyScheduler) { + return undefined; + } + + return notifyScheduler.invoke(funcName, ...args); + } + _supportedKeys() { const clickHandler = function (e) { e.preventDefault(); @@ -401,7 +426,7 @@ class SchedulerWorkSpace extends WidgetObserver { const selectedCells = this._getSelectedCellsData(); if (selectedCells?.length) { - const selectedCellsElement = selectedCells.map((cellData) => this._getCellByData(cellData)).filter((cell) => !!cell); + const selectedCellsElement = selectedCells.map((cellData) => this._getCellByData(cellData)).filter((cell) => Boolean(cell)); e.target = selectedCellsElement; this._showPopup = true; @@ -594,11 +619,11 @@ class SchedulerWorkSpace extends WidgetObserver { } _isVerticalGroupedWorkSpace() { // TODO move to the Model - return !!this.option('groups')?.length && this.option('groupOrientation') === 'vertical'; + return Boolean(this.option('groups')?.length) && this.option('groupOrientation') === 'vertical'; } _isHorizontalGroupedWorkSpace() { - return !!this.option('groups')?.length && this.option('groupOrientation') === 'horizontal'; + return Boolean(this.option('groups')?.length) && this.option('groupOrientation') === 'horizontal'; } _isWorkSpaceWithCount() { @@ -1889,7 +1914,7 @@ class SchedulerWorkSpace extends WidgetObserver { view: { type: this.type as ViewType, }, - crossScrollingEnabled: !!this.option('crossScrollingEnabled'), + crossScrollingEnabled: Boolean(this.option('crossScrollingEnabled')), }; } @@ -2414,7 +2439,6 @@ class SchedulerWorkSpace extends WidgetObserver { this._scrollSync = {}; this._viewDataProvider = null; this._cellsSelectionState = null; - this._activeStateUnit = CELL_SELECTOR; // @ts-expect-error super._init(); @@ -3235,16 +3259,15 @@ const createDragBehaviorConfig = ( const createDragAppointment = (itemData, settings, appointments) => { const appointmentIndex = appointments.option('items').length; - - settings.isCompact = false; - settings.virtual = false; - - const items = appointments._renderItem(appointmentIndex, { + const $item = appointments._renderItem(appointmentIndex, { itemData, - settings: [settings], + ...settings, + isCompact: false, + virtual: false, + sortedIndex: -1, }); - return items[0]; + return $item; }; const onDragStart = (e) => { @@ -3307,7 +3330,7 @@ const createDragBehaviorConfig = ( const elements = getElementsFromPoint(); - const isMoveUnderControl = !!elements.find((el) => el === rootElement.get(0)); + const isMoveUnderControl = Boolean(elements.find((el) => el === rootElement.get(0))); const dateTables = getDateTables(); const droppableCell = elements.find((el) => { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts index a23fd9e7b445..4a2743ec2ede 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts @@ -24,7 +24,7 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { _getToday() { const viewOffset = this.option('viewOffset') as number; const today = getToday(this.option('indicatorTime') as Date, this.timeZoneCalculator); - return dateUtilsTs.addOffsets(today, [-viewOffset]); + return dateUtilsTs.addOffsets(today, -viewOffset); } isIndicationOnView(): boolean { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_grouped_data_map_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_grouped_data_map_provider.ts index 8d76b8189fb0..2d0e890e3f58 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_grouped_data_map_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_grouped_data_map_provider.ts @@ -119,8 +119,8 @@ export class GroupedDataMapProvider { const cellData = isAppointmentRender ? { ...originCellData, - startDate: dateUtilsTs.addOffsets(cell.cellData.startDate, [-viewOffset]), - endDate: dateUtilsTs.addOffsets(cell.cellData.endDate, [-viewOffset]), + startDate: dateUtilsTs.addOffsets(cell.cellData.startDate, -viewOffset), + endDate: dateUtilsTs.addOffsets(cell.cellData.endDate, -viewOffset), } : originCellData; @@ -178,7 +178,7 @@ export class GroupedDataMapProvider { cellStartDate: Date, cellEndDate: Date, ): number { - const nextHourCellStartDate = dateUtilsTs.addOffsets(cellStartDate, [toMs('hour')]); + const nextHourCellStartDate = dateUtilsTs.addOffsets(cellStartDate, toMs('hour')); const cellTimezoneDiff = timezoneUtils.getDaylightOffset(cellStartDate, cellEndDate); const cellNextHourTimezoneDiff = timezoneUtils.getDaylightOffset( cellStartDate, @@ -199,8 +199,8 @@ export class GroupedDataMapProvider { cellStartDate: Date, cellEndDate: Date, ): boolean { - const nextIntervalCellStartDate = dateUtilsTs.addOffsets(cellStartDate, [secondIntervalOffset]); - const nextIntervalCellEndDate = dateUtilsTs.addOffsets(cellEndDate, [secondIntervalOffset]); + const nextIntervalCellStartDate = dateUtilsTs.addOffsets(cellStartDate, secondIntervalOffset); + const nextIntervalCellEndDate = dateUtilsTs.addOffsets(cellEndDate, secondIntervalOffset); const isInOriginInterval = startDate >= cellStartDate && startDate < cellEndDate; @@ -241,7 +241,7 @@ export class GroupedDataMapProvider { startDate: this.getGroupStartDate(groupIndex), endDate: this.getGroupEndDate(groupIndex), }; - }).filter(({ startDate }) => !!startDate); + }).filter(({ startDate }) => Boolean(startDate)); } getGroupIndices() { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_time_panel_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_time_panel_data_generator.ts index ae5164f94cd1..194691ba89c9 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_time_panel_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_time_panel_data_generator.ts @@ -168,7 +168,7 @@ export class TimePanelDataGenerator { if (currentGroupIndex !== previousGroupIndex) { previousGroupedData.push({ dateTable: [], - isGroupedAllDayPanel: getIsGroupedAllDayPanel(!!cellData.allDay, isVerticalGrouping), + isGroupedAllDayPanel: getIsGroupedAllDayPanel(Boolean(cellData.allDay), isVerticalGrouping), groupIndex: currentGroupIndex, key: getKeyByGroup(currentGroupIndex, isVerticalGrouping), }); @@ -198,10 +198,10 @@ export class TimePanelDataGenerator { ): boolean { // NOTE: today date value shifted by -viewOffset for the render purposes. // Therefore, we roll-backing here this shift. - const realToday = dateUtilsTs.addOffsets(today, [viewOffset]); + const realToday = dateUtilsTs.addOffsets(today, viewOffset); // NOTE: start view date value calculated from the render options and hasn't viewOffset. // So, we must shift it by viewOffset to get the real start view date here. - const realStartViewDate = dateUtilsTs.addOffsets(startViewDate, [viewOffset]); + const realStartViewDate = dateUtilsTs.addOffsets(startViewDate, viewOffset); if ( !showCurrentTimeIndicator diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index 3c07d2a41876..7f0ea1b5b862 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -327,7 +327,7 @@ export class ViewDataGenerator { if (currentGroupIndex !== previousGroupIndex) { groupedData.push({ dateTable: [], - isGroupedAllDayPanel: getIsGroupedAllDayPanel(!!isAllDayRow, isVerticalGrouping), + isGroupedAllDayPanel: getIsGroupedAllDayPanel(Boolean(isAllDayRow), isVerticalGrouping), groupIndex: currentGroupIndex, key: getKeyByGroup(currentGroupIndex, isVerticalGrouping), }); @@ -479,7 +479,7 @@ export class ViewDataGenerator { }, rowIndex, columnIndex); const { viewOffset } = options; const startDate = dateUtils.trimTime(data.startDate); - const shiftedStartDate = dateUtilsTs.addOffsets(startDate, [viewOffset]); + const shiftedStartDate = dateUtilsTs.addOffsets(startDate, viewOffset); return { ...data, @@ -715,9 +715,9 @@ export class ViewDataGenerator { && (index === selectedCellIndex || (selectedCellIndex === undefined && startDate.getTime() === selectedCellStartDate.getTime())) - && !!allDay === !!selectedCellAllDay); + && Boolean(allDay) === Boolean(selectedCellAllDay)); - const isFocused = !!focusedCell + const isFocused = Boolean(focusedCell) && index === focusedCell.cellData.index && groupIndex === focusedCell.cellData.groupIndex && allDay === focusedCell.cellData.allDay; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts index 86249cd91c68..f012c62becf8 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts @@ -268,7 +268,7 @@ export default class ViewDataProvider { hasGroupAllDayPanel(groupIndex) { if (this._options.isVerticalGrouping) { - return !!this.groupedDataMap.dateTableGroupedMap[groupIndex]?.[0][0].cellData.allDay; + return Boolean(this.groupedDataMap.dateTableGroupedMap[groupIndex]?.[0][0].cellData.allDay); } return this.groupedDataMap.allDayPanelGroupedMap[groupIndex]?.length > 0; @@ -299,7 +299,7 @@ export default class ViewDataProvider { } = cellData; if (groupIndex === currentGroupIndex - && allDay === !!currentAllDay + && allDay === Boolean(currentAllDay) && this._compareDatesAndAllDay(date, currentStartDate, currentEndDate, allDay)) { return { position: { @@ -443,7 +443,7 @@ export default class ViewDataProvider { const lastEndDate = new Date( this.getLastViewDate().getTime() - dateUtils.dateToMilliseconds('minute'), ); - return dateUtilsTs.addOffsets(lastEndDate, [-this._options.viewOffset]); + return dateUtilsTs.addOffsets(lastEndDate, -this._options.viewOffset); } getLastViewDateByEndDayHour(endDayHour: number): Date { diff --git a/packages/devextreme/js/__internal/tsconfig.json b/packages/devextreme/js/__internal/tsconfig.json index 9d7a7da7d45c..ef7b75886f34 100644 --- a/packages/devextreme/js/__internal/tsconfig.json +++ b/packages/devextreme/js/__internal/tsconfig.json @@ -39,6 +39,12 @@ "./*" ], }, + "lib": [ + "es2020", + "es2021.weakref", + "dom", + "dom.iterable" + ], "types": [] }, "tsc-alias": { diff --git a/packages/devextreme/js/__internal/ui/accordion.ts b/packages/devextreme/js/__internal/ui/accordion.ts index 4dd8f0e8c004..4769c25427d0 100644 --- a/packages/devextreme/js/__internal/ui/accordion.ts +++ b/packages/devextreme/js/__internal/ui/accordion.ts @@ -53,6 +53,10 @@ class Accordion extends CollectionWidgetLiveUpdate[]; + protected _activeStateUnit(): string { + return `.${ACCORDION_ITEM_CLASS}`; + } + _getDefaultOptions(): AccordionProperties { return { ...super._getDefaultOptions(), @@ -101,7 +105,6 @@ class Accordion extends CollectionWidgetLiveUpdate { + event: DxEvent; + value: Date; +} + +export interface WeekNumberClickEvent extends DefaultActionArgs { + event: DxEvent; + rowDates: Date[]; +} + export interface BaseViewProperties extends WidgetProperties { date: Date; value?: Date | Date[]; + min?: Date; + + max?: Date; + contouredDate?: Date; _todayDate: () => Date; @@ -83,6 +100,14 @@ export interface BaseViewProperties extends WidgetProperties { range: Date[]; hoveredRange: Date[]; + + cellTemplate?: template | ( + (itemData: CellTemplateData, itemIndex: number, itemElement: Element) => dxElementWrapper + ); + + onCellClick?: (e: CellEvent) => void; + onCellHover?: (e: CellEvent) => void; + onWeekNumberClick?: (e: WeekNumberClickEvent) => void; } class BaseView< @@ -92,13 +117,13 @@ class BaseView< $body!: dxElementWrapper; - _disabledDatesHandler?: Date[] | ((data: DisabledDate) => boolean); + _disabledDatesHandler!: ((data: DisabledDate) => boolean); - _cellClickAction!: (e: Record) => void; + _cellClickAction!: (e: CellEvent) => void; - _cellHoverAction!: (e: Record) => void; + _cellHoverAction!: (e: CellEvent) => void; - _weekNumberCellClickAction!: (e: Record) => void; + _weekNumberCellClickAction!: (e: WeekNumberClickEvent) => void; _$rangeEndHoverCell!: dxElementWrapper; @@ -106,15 +131,14 @@ class BaseView< _$rangeEndDateCell!: dxElementWrapper; - _$rangeCells!: dxElementWrapper; + _$rangeCells!: dxElementWrapper[]; - _$hoveredRangeCells!: dxElementWrapper; + _$hoveredRangeCells!: dxElementWrapper[]; _$rangeStartHoverCell!: dxElementWrapper; - _$selectedCells!: dxElementWrapper; + _$selectedCells!: dxElementWrapper[]; - // eslint-disable-next-line class-methods-use-this _getViewName(): string { return 'base'; } @@ -154,7 +178,6 @@ class BaseView< this._updateTableAriaLabel(); } - // eslint-disable-next-line class-methods-use-this _getLocalizedWidgetName(): string { const localizedWidgetName = messageLocalization.format('dxCalendar-ariaWidgetName'); @@ -165,8 +188,8 @@ class BaseView< const { value } = this.option(); const localizedWidgetName = this._getLocalizedWidgetName(); - // @ts-expect-error - const formattedDate = dateLocalization.format(value, ARIA_LABEL_DATE_FORMAT); + const formattedDate = dateLocalization + .format(value as Date | undefined, ARIA_LABEL_DATE_FORMAT); // @ts-expect-error ts-error const selectedDatesText = messageLocalization.format('dxCalendar-selectedDate', formattedDate); @@ -179,8 +202,7 @@ class BaseView< const { value } = this.option(); const localizedWidgetName = this._getLocalizedWidgetName(); - // @ts-expect-error ts-error - const [startDate, endDate] = value; + const [startDate, endDate] = value as [Date, Date]; const formattedStartDate = dateLocalization.format(startDate, ARIA_LABEL_DATE_FORMAT); const formattedEndDate = dateLocalization.format(endDate, ARIA_LABEL_DATE_FORMAT); @@ -207,8 +229,8 @@ class BaseView< _getMultipleRangesText(): string { const { value } = this.option(); - // @ts-expect-error ts-error - const ranges = coreDateUtils.getRangesByDates(value.map((date) => new Date(date))); + const rangeValue = value as Date[]; + const ranges = coreDateUtils.getRangesByDates(rangeValue.map((date) => new Date(date))); if (ranges.length > 2) { // @ts-expect-error ts-error @@ -227,7 +249,7 @@ class BaseView< return result; } - _getRangeText(range) { + _getRangeText(range: [Date | undefined, Date | undefined]): string { const [startDate, endDate] = range; const formattedStartDate = dateLocalization.format(startDate, ARIA_LABEL_DATE_FORMAT); @@ -238,20 +260,18 @@ class BaseView< ? messageLocalization.format('dxCalendar-selectedMultipleDateRange', formattedStartDate, formattedEndDate) : formattedStartDate; - return selectedDatesText; + return `${selectedDatesText}`; } - // @ts-expect-error ts-error - _getTableAriaLabel() { + _getTableAriaLabel(): string { const { value, selectionMode } = this.option(); - const isValueEmpty = !value || Array.isArray(value) && !value.filter(Boolean).length; + const isValueEmpty = !value || (Array.isArray(value) && !value.filter(Boolean).length); if (isValueEmpty) { return this._getLocalizedWidgetName(); } - // eslint-disable-next-line default-case, @typescript-eslint/switch-exhaustiveness-check switch (selectionMode) { case SELECTION_MODE.single: return this._getSingleModeAriaLabel(); @@ -259,10 +279,12 @@ class BaseView< return this._getRangeModeAriaLabel(); case SELECTION_MODE.multiple: return this._getMultipleModeAriaLabel(); + default: + return this._getSingleModeAriaLabel(); } } - _updateTableAriaLabel() { + _updateTableAriaLabel(): void { const label = this._getTableAriaLabel(); this.setAria({ label }, this._$table); @@ -279,17 +301,17 @@ class BaseView< _renderBody(): void { this.$body = $('').appendTo(this._$table); - const rowData = { + const rowData: { cellDate: Date; prevCellDate: Date | null; row: HTMLElement | undefined } = { cellDate: this._getFirstCellData(), prevCellDate: null, + row: undefined, }; const { rowCount: rowsCount, colCount: colsCount } = this.option(); - for (let rowIndex = 0, rowCount = rowsCount; rowIndex < rowCount; rowIndex++) { - // @ts-expect-error ts-error + for (let rowIndex = 0, rowCount = rowsCount; rowIndex < rowCount; rowIndex += 1) { rowData.row = this._createRow(); - for (let colIndex = 0, colCount = colsCount; colIndex < colCount; colIndex++) { + for (let colIndex = 0, colCount = colsCount; colIndex < colCount; colIndex += 1) { this._renderCell(rowData, colIndex); } @@ -298,9 +320,10 @@ class BaseView< } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _renderWeekNumberCell(rowData) {} + _renderWeekNumberCell(rowData?: unknown): void { + } - _createRow() { + _createRow(): HTMLElement { const row = domAdapter.createElement('tr'); this.setAria('role', 'row', $(row)); @@ -309,7 +332,7 @@ class BaseView< return row; } - _createCell(cellDate, cellIndex) { + _createCell(cellDate: Date, cellIndex: number): { cell: HTMLElement; $cell: dxElementWrapper } { const cell = domAdapter.createElement('td'); const $cell = $(cell); @@ -327,7 +350,10 @@ class BaseView< return { cell, $cell }; } - _renderCell(params, cellIndex) { + _renderCell( + params: { cellDate: Date; prevCellDate: Date | null; row: HTMLElement | undefined }, + cellIndex: number, + ): void { const { cellDate, prevCellDate, row } = params; // T425127 @@ -339,7 +365,7 @@ class BaseView< const { cell, $cell } = this._createCell(cellDate, cellIndex); - const cellTemplate = this.option('cellTemplate'); + const { cellTemplate } = this.option(); $(row).append(cell); @@ -347,14 +373,13 @@ class BaseView< // @ts-expect-error ts-error cellTemplate.render(this._prepareCellTemplateData(cellDate, cellIndex, $cell)); } else { - // @ts-expect-error ts-error cell.innerHTML = this._getCellText(cellDate); } params.cellDate = this._getNextCellData(cellDate); } - _getClassNameByDate(cellDate, cellIndex) { + _getClassNameByDate(cellDate: Date, cellIndex: number): string { let className = CALENDAR_CELL_CLASS; if (this._isTodayCell(cellDate)) { @@ -394,7 +419,11 @@ class BaseView< return className; } - _prepareCellTemplateData(cellDate, cellIndex, $cell) { + _prepareCellTemplateData(cellDate: Date, cellIndex: number, $cell: dxElementWrapper): { + model: CellTemplateData; + container: Element; + index: number; + } { const isDateCell = cellDate instanceof Date; const text = isDateCell ? this._getCellText(cellDate) : cellDate; const date = isDateCell ? cellDate : undefined; @@ -415,6 +444,7 @@ class BaseView< if (!$(e.currentTarget).hasClass(CALENDAR_EMPTY_CELL_CLASS)) { this._cellClickAction({ event: e, + // @ts-expect-error ts-error value: $(e.currentTarget).data(CALENDAR_DATE_VALUE_KEY), }); } @@ -426,14 +456,20 @@ class BaseView< if (selectionMode === SELECTION_MODE.range) { this._createCellHoverAction(); - eventsEngine.on(this._$table, CALENDAR_DXHOVERSTART_EVENT_NAME, NOT_WEEK_CELL_SELECTOR, (e) => { - if (!$(e.currentTarget).hasClass(CALENDAR_EMPTY_CELL_CLASS)) { - this._cellHoverAction({ - event: e, - value: $(e.currentTarget).data(CALENDAR_DATE_VALUE_KEY), - }); - } - }); + eventsEngine.on( + this._$table, + CALENDAR_DXHOVERSTART_EVENT_NAME, + NOT_WEEK_CELL_SELECTOR, + (e) => { + if (!$(e.currentTarget).hasClass(CALENDAR_EMPTY_CELL_CLASS)) { + this._cellHoverAction({ + event: e, + // @ts-expect-error ts-error + value: $(e.currentTarget).data(CALENDAR_DATE_VALUE_KEY), + }); + } + }, + ); } if (selectionMode !== SELECTION_MODE.single) { @@ -443,11 +479,15 @@ class BaseView< const $row = $(e.currentTarget).closest('tr'); const firstDateInRow = $row.find(`.${CALENDAR_CELL_CLASS}`).first().data(CALENDAR_DATE_VALUE_KEY); - const lastDateInRow = $row.find(`.${CALENDAR_CELL_CLASS}`).last().data(CALENDAR_DATE_VALUE_KEY); - const rowDates = [...coreDateUtils.getDatesOfInterval(firstDateInRow, lastDateInRow, DAY_INTERVAL), lastDateInRow]; + const lastDateInRow = $row.find(`.${CALENDAR_CELL_CLASS}`).last().data(CALENDAR_DATE_VALUE_KEY) ; + const rowDates = [ + ...coreDateUtils.getDatesOfInterval(firstDateInRow, lastDateInRow, DAY_INTERVAL), + lastDateInRow, + ]; this._weekNumberCellClickAction({ event: e, + // @ts-expect-error ts-error rowDates, }); }); @@ -455,99 +495,90 @@ class BaseView< } _createCellClickAction(): void { - // @ts-expect-error ts-error this._cellClickAction = this._createActionByOption('onCellClick'); } _createCellHoverAction(): void { - // @ts-expect-error ts-error this._cellHoverAction = this._createActionByOption('onCellHover'); } _createWeekNumberCellClickAction(): void { - // @ts-expect-error ts-error this._weekNumberCellClickAction = this._createActionByOption('onWeekNumberClick'); } _createDisabledDatesHandler(): void { const { disabledDates } = this.option(); - // @ts-expect-error ts-error + this._disabledDatesHandler = Array.isArray(disabledDates) ? this._getDefaultDisabledDatesHandler(disabledDates) - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - : disabledDates || noop; + : disabledDates ?? ((): boolean => false); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _getDefaultDisabledDatesHandler(disabledDates): (args) => void { - return noop; + _getDefaultDisabledDatesHandler(disabledDates: Date[]): (args) => boolean { + return () => false; } - // @ts-expect-error ts-error // eslint-disable-next-line @typescript-eslint/no-unused-vars - _isTodayCell(cellDate): boolean { - Class.abstract(); + _isTodayCell(cellDate: Date): boolean { + return false; } - // @ts-expect-error ts-error // eslint-disable-next-line @typescript-eslint/no-unused-vars - _isDateOutOfRange(cellDate): boolean { - Class.abstract(); + _isDateOutOfRange(cellDate: Date): boolean { + return false; } - isDateDisabled(cellDate): boolean { + isDateDisabled(cellDate: Date): boolean { const dateParts = { date: cellDate, view: this._getViewName(), }; - // @ts-expect-error ts-error - return this._disabledDatesHandler(dateParts); + + return this._disabledDatesHandler(dateParts as DisabledDate); } - // @ts-expect-error ts-error // eslint-disable-next-line @typescript-eslint/no-unused-vars - _isOtherView(cellDate): boolean { - Class.abstract(); + _isOtherView(cellDate: Date): boolean { + return false; } - // @ts-expect-error ts-error // eslint-disable-next-line @typescript-eslint/no-unused-vars - _isStartDayOfMonth(cellDate): boolean { - Class.abstract(); + _isStartDayOfMonth(cellDate: Date): boolean { + return false; } - // @ts-expect-error ts-error // eslint-disable-next-line @typescript-eslint/no-unused-vars - _isEndDayOfMonth(cellDate): boolean { - Class.abstract(); + _isEndDayOfMonth(cellDate: Date): boolean { + return false; } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _getCellText(cellDate) { - Class.abstract(); + _getCellText(cellDate: Date): string { + return ''; } - _getFirstCellData() { - Class.abstract(); + _getFirstCellData(): Date { + return new Date(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _getNextCellData(date) { - Class.abstract(); + _getNextCellData(date: Date): Date { + return new Date(date); } - _renderContouredDate(contouredDate?) { - if (!this.option('focusStateEnabled')) { + _renderContouredDate(contouredDate?: Date | undefined): void { + const { focusStateEnabled } = this.option(); + if (!focusStateEnabled) { return; } - - contouredDate = contouredDate || this.option('contouredDate'); + const { contouredDate: currentContouredDate } = this.option(); + const newContouredDate = contouredDate ?? currentContouredDate; const $oldContouredCell = this._getContouredCell(); - const $newContouredCell = this._getCellByDate(contouredDate); + const $newContouredCell = this._getCellByDate(newContouredDate); $oldContouredCell.removeClass(CALENDAR_CONTOURED_DATE_CLASS); - if (contouredDate) { + if (newContouredDate) { $newContouredCell.addClass(CALENDAR_CONTOURED_DATE_CLASS); } } @@ -561,72 +592,73 @@ class BaseView< return; } - let value = this.option('value'); + let { value = [] } = this.option(); if (!Array.isArray(value)) { - // @ts-expect-error ts-error value = [value]; } this._updateSelectedClass(value); } - _updateSelectedClass(value): void { + _updateSelectedClass(value: Date[]): void { if (this._isRangeMode() && !this._isMonthView()) { return; } - // @ts-expect-error ts-error - this._$selectedCells?.forEach(($cell) => { $cell.removeClass(CALENDAR_SELECTED_DATE_CLASS); }); - this._$selectedCells = value.map((value) => this._getCellByDate(value)); - // @ts-expect-error ts-error + + this._$selectedCells?.forEach(($cell: dxElementWrapper) => { + $cell.removeClass(CALENDAR_SELECTED_DATE_CLASS); + }); + + this._$selectedCells = value.map((date: Date) => this._getCellByDate(date)); this._$selectedCells.forEach(($cell) => { $cell.addClass(CALENDAR_SELECTED_DATE_CLASS); }); } _renderRange(): void { - const { allowValueSelection, value, range } = this.option(); + const { allowValueSelection, value = [], range } = this.option(); if (!allowValueSelection || !this._isRangeMode() || !this._isMonthView()) { return; } - // @ts-expect-error ts-error + this._$rangeCells?.forEach(($cell) => { $cell.removeClass(CALENDAR_CELL_IN_RANGE_CLASS); }); - // @ts-expect-error ts-error - this._$hoveredRangeCells?.forEach(($cell) => { $cell.removeClass(CALENDAR_CELL_RANGE_HOVER_CLASS); }); + this._$hoveredRangeCells?.forEach(($cell) => { + $cell.removeClass(CALENDAR_CELL_RANGE_HOVER_CLASS); + }); this._$rangeStartHoverCell?.removeClass(CALENDAR_CELL_RANGE_HOVER_START_CLASS); this._$rangeEndHoverCell?.removeClass(CALENDAR_CELL_RANGE_HOVER_END_CLASS); this._$rangeStartDateCell?.removeClass(CALENDAR_RANGE_START_DATE_CLASS); this._$rangeEndDateCell?.removeClass(CALENDAR_RANGE_END_DATE_CLASS); - // @ts-expect-error ts-error - this._$rangeCells = range.map((value) => this._getCellByDate(value)); - // @ts-expect-error ts-error + + this._$rangeCells = range.map((date) => this._getCellByDate(date)); this._$rangeStartDateCell = this._getCellByDate(value[0]); - // @ts-expect-error ts-error this._$rangeEndDateCell = this._getCellByDate(value[1]); - // @ts-expect-error ts-error - this._$rangeCells.forEach(($cell) => { $cell.addClass(CALENDAR_CELL_IN_RANGE_CLASS); }); + this._$rangeCells.forEach(($cell) => { $cell.addClass(CALENDAR_CELL_IN_RANGE_CLASS); }); this._$rangeStartDateCell?.addClass(CALENDAR_RANGE_START_DATE_CLASS); this._$rangeEndDateCell?.addClass(CALENDAR_RANGE_END_DATE_CLASS); } - _renderHoveredRange() { + _renderHoveredRange(): void { const { allowValueSelection, hoveredRange } = this.option(); if (!allowValueSelection || !this._isRangeMode() || !this._isMonthView()) { return; } - // @ts-expect-error ts-error - this._$hoveredRangeCells?.forEach(($cell) => { $cell.removeClass(CALENDAR_CELL_RANGE_HOVER_CLASS); }); + + this._$hoveredRangeCells?.forEach(($cell) => { + $cell.removeClass(CALENDAR_CELL_RANGE_HOVER_CLASS); + }); this._$rangeStartHoverCell?.removeClass(CALENDAR_CELL_RANGE_HOVER_START_CLASS); this._$rangeEndHoverCell?.removeClass(CALENDAR_CELL_RANGE_HOVER_END_CLASS); - // @ts-expect-error ts-error + this._$hoveredRangeCells = hoveredRange - .map((value) => this._getCellByDate(value)); + .map((date) => this._getCellByDate(date)); this._$rangeStartHoverCell = this._getCellByDate(hoveredRange[0]); this._$rangeEndHoverCell = this._getCellByDate(hoveredRange[hoveredRange.length - 1]); - // @ts-expect-error ts-error + this._$hoveredRangeCells.forEach(($cell) => { $cell.addClass(CALENDAR_CELL_RANGE_HOVER_CLASS); }); @@ -651,13 +683,13 @@ class BaseView< return null; } - getCellAriaLabel(date) { + getCellAriaLabel(date: Date): string { const viewName = this._getViewName(); const isToday = this._isTodayCell(date); const format = this._getCurrentDateFormat(); const dateRangeText = format - ? dateLocalization.format(date, format) + ? `${dateLocalization.format(date, format)}` : this._getCellText(date); const ariaLabel = isToday @@ -668,23 +700,23 @@ class BaseView< } _getFirstAvailableDate(): Date { - let date = this.option('date'); - const min = this.option('min'); - // @ts-expect-error ts-error - date = coreDateUtils.getViewFirstCellDate(this._getViewName(), date); - // @ts-expect-error ts-error - return new Date(min && date < min ? min : date); + const { date, min } = this.option(); + const firstAvailableDate = coreDateUtils.getViewFirstCellDate( + this._getViewName(), + date, + ) ?? date; + + return new Date(min && firstAvailableDate < min ? min : firstAvailableDate); } - // @ts-expect-error ts-error // eslint-disable-next-line @typescript-eslint/no-unused-vars - _getCellByDate(contouredDate): dxElementWrapper { - Class.abstract(); + _getCellByDate(contouredDate: Date | undefined): dxElementWrapper { + return $(); } - // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars - isBoundary(date?) { - Class.abstract(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isBoundary(date?: Date): boolean { + return false; } _optionChanged(args: OptionChanged): void { @@ -701,7 +733,7 @@ class BaseView< this._renderHoveredRange(); break; case 'contouredDate': - this._renderContouredDate(value); + this._renderContouredDate(value as Date | undefined); break; case 'onCellClick': this._createCellClickAction(); diff --git a/packages/devextreme/js/__internal/ui/calendar/calendar.multiple.selection.strategy.ts b/packages/devextreme/js/__internal/ui/calendar/calendar.multiple.selection.strategy.ts new file mode 100644 index 000000000000..d622b5711afa --- /dev/null +++ b/packages/devextreme/js/__internal/ui/calendar/calendar.multiple.selection.strategy.ts @@ -0,0 +1,80 @@ +import type { DxEvent } from '@js/events'; + +import type Calendar from './calendar'; +import type { WeekNumberClickEvent } from './calendar.base_view'; +import CalendarSelectionStrategy from './calendar.selection.strategy'; + +class CalendarMultiSelectionStrategy extends CalendarSelectionStrategy { + constructor(component: Calendar) { + super(component); + this.NAME = 'MultiSelection'; + } + + dateOption(optionName: 'value'): (Date | null)[]; + dateOption(optionName: 'min' | 'max'): Date | null; + dateOption(optionName: 'min' | 'max' | 'value'): Date | null | (Date | null)[] { + if (optionName === 'value') { + return this.calendar._getDateOption('value') as Date[] | null; + } + return this.calendar._getDateOption(optionName); + } + + getViewOptions(): { + value: (Date | null)[]; + range: Date[]; + selectionMode: 'multiple'; + onWeekNumberClick?: ((e: WeekNumberClickEvent) => void) | null; + } { + return { + value: this.dateOption('value'), + range: [], + selectionMode: 'multiple', + onWeekNumberClick: this._shouldHandleWeekNumberClick() + ? this._weekNumberClickHandler.bind(this) + : null, + }; + } + + selectValue(selectedValue: Date, e: DxEvent): void { + const value = [...this.dateOption('value')]; + const alreadySelectedIndex = value + .findIndex((date) => date?.toDateString() === selectedValue.toDateString()); + + if (alreadySelectedIndex > -1) { + value.splice(alreadySelectedIndex, 1); + } else { + value.push(selectedValue); + } + + this.skipNavigate(); + this._updateCurrentDate(selectedValue); + this._currentDateChanged = true; + this.dateValue(value, e); + } + + updateAriaSelected(val?: (Date | null)[], previousVal?: (Date | null)[]): void { + const value = val ?? this.dateOption('value'); + const previousValue = previousVal ?? []; + + super.updateAriaSelected(value, previousValue); + } + + getDefaultCurrentDate(): Date | null { + const value = this.dateOption('value'); + const dates = value.filter((date) => date !== null); + + return this._getLowestDateInArray(dates); + } + + restoreValue(): void { + this.calendar.option('value', []); + } + + _weekNumberClickHandler({ rowDates, event }: WeekNumberClickEvent): void { + const selectedDates = rowDates.filter((date) => !this._isDateDisabled(date)); + + this.dateValue(selectedDates, event); + } +} + +export default CalendarMultiSelectionStrategy; diff --git a/packages/devextreme/js/__internal/ui/calendar/m_calendar.navigator.ts b/packages/devextreme/js/__internal/ui/calendar/calendar.navigator.ts similarity index 98% rename from packages/devextreme/js/__internal/ui/calendar/m_calendar.navigator.ts rename to packages/devextreme/js/__internal/ui/calendar/calendar.navigator.ts index 11d486a7e460..df4ed75f69cb 100644 --- a/packages/devextreme/js/__internal/ui/calendar/m_calendar.navigator.ts +++ b/packages/devextreme/js/__internal/ui/calendar/calendar.navigator.ts @@ -17,7 +17,7 @@ const CALENDAR_NAVIGATOR_CAPTION_BUTTON_CLASS = 'dx-calendar-caption-button'; const BUTTON_TEXT_CLASS = 'dx-button-text'; export interface NavigatorOptions extends WidgetOptions { - onClick?: ((e: ClickEvent) => void); + onClick?: ((e: { direction: number; event: ClickEvent }) => void); onCaptionClick?: ((e: ClickEvent) => void); type?: ButtonType; stylingMode?: ButtonStyle; diff --git a/packages/devextreme/js/__internal/ui/calendar/calendar.range.selection.strategy.ts b/packages/devextreme/js/__internal/ui/calendar/calendar.range.selection.strategy.ts new file mode 100644 index 000000000000..ede782301589 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/calendar/calendar.range.selection.strategy.ts @@ -0,0 +1,207 @@ +import dateUtils from '@js/core/utils/date'; +import type { DxEvent } from '@js/events'; + +import type Calendar from './calendar'; +import type { CellEvent, WeekNumberClickEvent } from './calendar.base_view'; +import CalendarSelectionStrategy from './calendar.selection.strategy'; + +const DAY_INTERVAL = 86400000; + +class CalendarRangeSelectionStrategy extends CalendarSelectionStrategy { + constructor(component: Calendar) { + super(component); + this.NAME = 'RangeSelection'; + } + + dateOption(optionName: 'value'): (Date | null)[]; + dateOption(optionName: 'min' | 'max'): Date | null; + dateOption(optionName: 'min' | 'max' | 'value'): Date | null | (Date | null)[] { + if (optionName === 'value') { + return this.calendar._getDateOption('value') as (Date | null)[] || null; + } + return this.calendar._getDateOption(optionName); + } + + getViewOptions(): { + value: (Date | null)[]; + range: Date[]; + selectionMode: 'range'; + onCellHover?: (e: CellEvent) => void; + onWeekNumberClick?: ((e: WeekNumberClickEvent) => void) | null; + } { + const value = this._getValue(); + const range = this._getDaysInRange(value[0], value[1]); + + return { + value, + range, + selectionMode: 'range', + onCellHover: this._cellHoverHandler.bind(this), + onWeekNumberClick: this._shouldHandleWeekNumberClick() + ? this._weekNumberClickHandler.bind(this) + : null, + }; + } + + selectValue(selectedValue: Date, e: DxEvent): void { + const [startDate, endDate] = this._getValue(); + + this.skipNavigate(); + this._updateCurrentDate(selectedValue); + this._currentDateChanged = true; + const { allowChangeSelectionOrder, currentSelection } = this.calendar.option(); + + if (allowChangeSelectionOrder === true) { + this.calendar._valueSelected = true; + const convertedSelectedValue = this.calendar._convertToDate(selectedValue) as Date; + if (currentSelection === 'startDate') { + if (convertedSelectedValue > (this.calendar._convertToDate(endDate) ?? new Date(0))) { + this.dateValue([selectedValue, null], e); + } else { + this.dateValue([selectedValue, endDate], e); + } + } else if (convertedSelectedValue + >= (this.calendar._convertToDate(startDate) ?? new Date(0))) { + this.dateValue([startDate, selectedValue], e); + } else { + this.dateValue([selectedValue, null], e); + } + } else if (!startDate || endDate) { + this.dateValue([selectedValue, null], e); + } else { + this.dateValue( + startDate < selectedValue + ? [startDate, selectedValue] + : [selectedValue, startDate], + e, + ); + } + } + + updateAriaSelected(val?: (Date | null)[] | null, previousVal?: (Date | null)[] | null): void { + const value = val ?? this._getValue(); + const previousValue = previousVal ?? []; + super.updateAriaSelected(value, previousValue); + } + + processValueChanged(value: (Date | null)[], previousValue: (Date | null)[]): void { + super.processValueChanged(value, previousValue); + + const range = this._getRange(); + this._updateViewsOption('range', range); + } + + getDefaultCurrentDate(): Date | null { + const { allowChangeSelectionOrder, currentSelection } = this.calendar.option(); + const value = this.dateOption('value'); + + if (allowChangeSelectionOrder) { + if (currentSelection === 'startDate' && value[0]) { + return value[0]; + } + + if (currentSelection === 'endDate' && value[1]) { + return value[1]; + } + } + + const dates = value.filter((date) => date !== null); + + return this._getLowestDateInArray(dates); + } + + restoreValue(): void { + this.calendar.option('value', [null, null]); + } + + _getValue(): (Date | null)[] { + const value = this.dateOption('value'); + + if (!value.length) { + return value; + } + + let [startDate, endDate] = value; + + if (startDate && endDate && startDate > endDate) { + [startDate, endDate] = [endDate, startDate]; + } + + return [startDate, endDate]; + } + + _getRange(): Date[] { + const [startDate, endDate] = this._getValue(); + return this._getDaysInRange(startDate, endDate); + } + + _getDaysInRange(startDate: Date | null, endDate: Date | null): Date[] { + if (!startDate || !endDate) { + return []; + } + + const { currentDate, viewsCount } = this.calendar.option(); + const isAdditionalViewDate = this.calendar._isAdditionalViewDate(currentDate); + const firstDateInViews = dateUtils.getFirstMonthDate( + currentDate, + isAdditionalViewDate ? -2 : -1, + ) as Date; + const lastDateInViews = dateUtils.getLastMonthDate( + currentDate, + isAdditionalViewDate ? 1 : viewsCount, + ) as Date; + + const rangeStartDate = new Date(Math.max(firstDateInViews.getTime(), startDate.getTime())); + const rangeEndDate = new Date(Math.min(lastDateInViews.getTime(), endDate.getTime())); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return [ + ...dateUtils.getDatesOfInterval(rangeStartDate, rangeEndDate, DAY_INTERVAL), + rangeEndDate, + ]; + } + + _cellHoverHandler(e: CellEvent): void { + const isMaxZoomLevel = this._isMaxZoomLevel(); + const [startDate, endDate] = this._getValue(); + + const { allowChangeSelectionOrder, currentSelection } = this.calendar.option(); + + if (isMaxZoomLevel) { + const skipHoveredRange = allowChangeSelectionOrder && currentSelection === 'startDate'; + + if (startDate && !endDate && !skipHoveredRange) { + if (e.value > startDate) { + this._updateViewsOption('hoveredRange', this._getDaysInRange(startDate, e.value)); + return; + } + } else if (!startDate && endDate && !(allowChangeSelectionOrder && currentSelection === 'endDate')) { + if (e.value < endDate) { + this._updateViewsOption('hoveredRange', this._getDaysInRange(e.value, endDate)); + return; + } + } else if (startDate && endDate) { + if (currentSelection === 'startDate' && e.value < startDate) { + this._updateViewsOption('hoveredRange', this._getDaysInRange(e.value, startDate)); + return; + } if (currentSelection === 'endDate' && e.value > endDate) { + this._updateViewsOption('hoveredRange', this._getDaysInRange(endDate, e.value)); + return; + } + } + + this._updateViewsOption('hoveredRange', []); + } + } + + _weekNumberClickHandler({ rowDates, event }: WeekNumberClickEvent): void { + const selectedDates = rowDates.filter((date) => !this._isDateDisabled(date)); + const value = selectedDates.length + ? [selectedDates[0], selectedDates[selectedDates.length - 1]] + : [null, null]; + + this.dateValue(value, event); + } +} + +export default CalendarRangeSelectionStrategy; diff --git a/packages/devextreme/js/__internal/ui/calendar/m_calendar.selection.strategy.ts b/packages/devextreme/js/__internal/ui/calendar/calendar.selection.strategy.ts similarity index 53% rename from packages/devextreme/js/__internal/ui/calendar/m_calendar.selection.strategy.ts rename to packages/devextreme/js/__internal/ui/calendar/calendar.selection.strategy.ts index de434e828e48..3ab59f1456a6 100644 --- a/packages/devextreme/js/__internal/ui/calendar/m_calendar.selection.strategy.ts +++ b/packages/devextreme/js/__internal/ui/calendar/calendar.selection.strategy.ts @@ -1,48 +1,54 @@ +import type { DateLike } from '@js/common'; import dateUtils from '@js/core/utils/date'; import { isDefined } from '@js/core/utils/type'; +import type { DxEvent } from '@js/events'; + +import type Calendar from './calendar'; class CalendarSelectionStrategy { - public NAME?: string; + public NAME!: string; - public calendar; + public calendar!: Calendar; public _currentDateChanged?: boolean; - constructor(component) { + constructor(component: Calendar) { this.calendar = component; } - dateOption(optionName) { - return this.calendar._getDateOption(optionName); - } - - dateValue(value, e) { + dateValue(value: Date | null | (Date | null)[], e: DxEvent): void { this.calendar._dateValue(value, e); } - skipNavigate() { + skipNavigate(): void { this.calendar._skipNavigate = true; } - updateAriaSelected(value, previousValue) { + updateAriaSelected(value: (Date | null)[], previousValue: (Date | null)[]): void { this.calendar._updateAriaSelected(value, previousValue); + const { currentDate = new Date() } = this.calendar.option(); - if (value[0] && this.calendar.option('currentDate').getTime() === value[0].getTime()) { + if (value[0] && currentDate.getTime() === value[0].getTime()) { this.calendar._updateAriaId(value[0]); } } - processValueChanged(value, previousValue) { + processValueChanged( + val: Date | null | (Date | null)[], + previousVal: Date | null | (Date | null)[], + ): void { + let value = val; + let previousValue = previousVal; if (isDefined(value) && !Array.isArray(value)) { value = [value]; } if (isDefined(previousValue) && !Array.isArray(previousValue)) { previousValue = [previousValue]; } - value = value?.map((item) => this._convertToDate(item)) || []; - previousValue = previousValue?.map((item) => this._convertToDate(item)) || []; + value = value?.map((item) => this._convertToDate(item)) ?? []; + previousValue = previousValue?.map((item) => this._convertToDate(item)) ?? []; - this._updateViewsValue(value); + this._updateViewsValue(value.filter((item): item is Date => item !== null)); this.updateAriaSelected(value, previousValue); if (!this._currentDateChanged) { @@ -51,7 +57,7 @@ class CalendarSelectionStrategy { this._currentDateChanged = false; } - _isDateDisabled(date) { + _isDateDisabled(date: Date): boolean { const min = this.calendar._getDateOption('min'); const max = this.calendar._getDateOption('max'); const isLessThanMin = isDefined(min) && date < min && !dateUtils.sameDate(min, date); @@ -60,37 +66,38 @@ class CalendarSelectionStrategy { return this.calendar._view.isDateDisabled(date) || isLessThanMin || isBiggerThanMax; } - // @ts-expect-error - _getLowestDateInArray(dates) { + _getLowestDateInArray(dates: (Date | null)[]): Date | null { if (dates.length) { - return new Date(Math.min(...dates)); + return new Date(Math.min(...dates.map((date) => date?.getTime() ?? Infinity))); } + + return null; } - _convertToDate(value) { + _convertToDate(value: DateLike): Date | null { return this.calendar._convertToDate(value); } - _isMaxZoomLevel() { + _isMaxZoomLevel(): boolean { return this.calendar._isMaxZoomLevel(); } - _updateViewsOption(optionName, optionValue) { + _updateViewsOption(optionName: string, optionValue: Date | Date[]): void { this.calendar._updateViewsOption(optionName, optionValue); } - _updateViewsValue(value) { + _updateViewsValue(value: Date | Date[]): void { this._updateViewsOption('value', value); } - _updateCurrentDate(value) { + _updateCurrentDate(value: Date | null): void { this.calendar.option('currentDate', value ?? new Date()); } - _shouldHandleWeekNumberClick() { + _shouldHandleWeekNumberClick(): boolean { const { selectionMode, selectWeekOnClick } = this.calendar.option(); - return selectWeekOnClick && selectionMode !== 'single'; + return selectWeekOnClick === true && selectionMode !== 'single'; } } diff --git a/packages/devextreme/js/__internal/ui/calendar/calendar.single.selection.strategy.ts b/packages/devextreme/js/__internal/ui/calendar/calendar.single.selection.strategy.ts new file mode 100644 index 000000000000..9a3d3873c2a3 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/calendar/calendar.single.selection.strategy.ts @@ -0,0 +1,58 @@ +import type { DxEvent } from '@js/events'; + +import type Calendar from './calendar'; +import CalendarSelectionStrategy from './calendar.selection.strategy'; + +class CalendarSingleSelectionStrategy extends CalendarSelectionStrategy { + constructor(component: Calendar) { + super(component); + this.NAME = 'SingleSelection'; + } + + dateOption(optionName: 'min' | 'max' | 'value'): Date | null { + if (optionName === 'value') { + return this.calendar._getDateOption('value') as Date | null; + } + return this.calendar._getDateOption(optionName); + } + + getViewOptions(): { + value: Date | undefined; + range: Date[]; + selectionMode: 'single'; + } { + const value = this.dateOption('value') ?? undefined; + + return { + value, + range: [], + selectionMode: 'single', + }; + } + + selectValue(selectedValue: Date, e: DxEvent): void { + this.skipNavigate(); + this.dateValue(selectedValue, e); + } + + updateAriaSelected(val?: (Date | null)[], previousVal?: (Date | null)[]): void { + const value = val ?? [this.dateOption('value')]; + const previousValue = previousVal ?? []; + + super.updateAriaSelected(value, previousValue); + } + + getDefaultCurrentDate(): Date | null { + return this.dateOption('value'); + } + + restoreValue(): void { + this.calendar.option('value', null); + } + + _updateViewsValue(value: Date[]): void { + this._updateViewsOption('value', value[0]); + } +} + +export default CalendarSingleSelectionStrategy; diff --git a/packages/devextreme/js/__internal/ui/calendar/m_calendar.ts b/packages/devextreme/js/__internal/ui/calendar/calendar.ts similarity index 92% rename from packages/devextreme/js/__internal/ui/calendar/m_calendar.ts rename to packages/devextreme/js/__internal/ui/calendar/calendar.ts index 6b23a99a34ed..225fe84912f2 100644 --- a/packages/devextreme/js/__internal/ui/calendar/m_calendar.ts +++ b/packages/devextreme/js/__internal/ui/calendar/calendar.ts @@ -1,4 +1,3 @@ -import type { AnimationConfig } from '@js/common/core/animation'; import { fx } from '@js/common/core/animation'; import { move } from '@js/common/core/animation/translator'; import eventsEngine from '@js/common/core/events/core/events_engine'; @@ -37,16 +36,16 @@ import type { SwipeEndEvent, SwipeStartEvent, SwipeUpdateEvent } from '@ts/event import Button from '@ts/ui/button/wrapper'; import Editor from '@ts/ui/editor/editor'; -import type { BaseViewProperties } from './m_calendar.base_view'; -import CalendarMultipleSelectionStrategy from './m_calendar.multiple.selection.strategy'; -import type { NavigatorOptions } from './m_calendar.navigator'; -import Navigator from './m_calendar.navigator'; -import CalendarRangeSelectionStrategy from './m_calendar.range.selection.strategy'; -import CalendarSingleSelectionStrategy from './m_calendar.single.selection.strategy'; +import type { BaseViewProperties, CellEvent } from './calendar.base_view'; +import CalendarMultipleSelectionStrategy from './calendar.multiple.selection.strategy'; +import type { NavigatorOptions } from './calendar.navigator'; +import Navigator from './calendar.navigator'; +import CalendarRangeSelectionStrategy from './calendar.range.selection.strategy'; +import CalendarSingleSelectionStrategy from './calendar.single.selection.strategy'; import type { CenturyView, DecadeView, MonthView, MonthViewProperties, YearView, -} from './m_calendar.views'; -import Views from './m_calendar.views'; +} from './calendar.views'; +import Views from './calendar.views'; const CALENDAR_CLASS = 'dx-calendar'; const CALENDAR_BODY_CLASS = 'dx-calendar-body'; @@ -96,9 +95,13 @@ export interface CalendarProperties extends Properties { todayButtonText?: string; - _rangeMin?: Date; - _rangeMax?: Date; + rangeMin?: Date; + rangeMax?: Date; + allowChangeSelectionOrder?: boolean; + currentSelection?: 'startDate' | 'endDate'; _todayDate: () => Date; + onCellClick?: (e: CellEvent) => void; + onContouredChanged?: (e: { activeElement: string }) => void; } class Calendar< @@ -112,9 +115,9 @@ class Calendar< _skipNavigate?: boolean; - _onContouredChanged?: (arg) => void; + _onContouredChanged?: (activeElement: string) => void; - _cellClickAction?: (e) => void; + _cellClickAction?: (e: CellEvent) => void; _view!: MonthView | YearView | DecadeView | CenturyView; @@ -138,7 +141,7 @@ class Calendar< _alreadyViewRender?: boolean; - _waitRenderViewTimeout?: ReturnType; + _waitRenderViewTimeout?: NodeJS.Timeout; _$footer?: dxElementWrapper; @@ -150,6 +153,12 @@ class Calendar< _isOtherViewCellClicked?: boolean; + _valueSelected?: boolean; + + protected _activeStateUnit(): string { + return `.${CALENDAR_CELL_CLASS}`; + } + _getDefaultOptions(): TProperties { return { ...super._getDefaultOptions(), @@ -217,7 +226,7 @@ class Calendar< if (isCommandKeyPressed(e)) { this._navigateUp(); } else { - if (fx.isAnimating(this._view.$element())) { + if (fx.isAnimating(this._view.$element().get(0))) { return; } this._moveCurrentDateByOffset(-1 * this._view.option('colCount')); @@ -228,7 +237,7 @@ class Calendar< if (isCommandKeyPressed(e)) { this._navigateDown(); } else { - if (fx.isAnimating(this._view.$element())) { + if (fx.isAnimating(this._view.$element().get(0))) { return; } this._moveCurrentDateByOffset(1 * this._view.option('colCount')); @@ -307,14 +316,16 @@ class Calendar< return undefined; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateSerialization.getDateSerializationFormat(value); } - _convertToDate(value): Date | null { + _convertToDate(value: DateLike | undefined): Date | null { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateSerialization.deserializeDate(value); } - _dateValue(value: Date | Date[], event: DxEvent): void { + _dateValue(value: Date | null | (Date | null)[], event: DxEvent): void { if (event) { if (event.type === 'keydown') { const cellElement = this._view._getContouredCell().get(0); @@ -336,6 +347,7 @@ class Calendar< ): void { const serializationFormat = this._getSerializationFormat(optionName); const serializedValue = this._isArrayValue(optionName, optionValue) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return ? optionValue.map((value) => dateSerialization.serializeDate(value, serializationFormat)) : dateSerialization.serializeDate(optionValue, serializationFormat); @@ -345,15 +357,15 @@ class Calendar< _getDateOption(optionName: 'value'): Date | null | (Date | null)[]; _getDateOption(optionName: 'min' | 'max'): Date | null; _getDateOption(optionName: 'value' | 'min' | 'max'): Date | null | (Date | null)[] { - const { value } = this.option(); - - if (!this._isArrayValue(optionName, value)) { - const { [optionName]: optionValue } = this.option(); - + let { [optionName]: optionValue } = this.option(); + if (!this._isArrayValue(optionName, optionValue)) { + if (optionValue === '') { + optionValue = null; + } return this._convertToDate(optionValue); } - const valueArray = value ?? []; + const valueArray = optionValue ?? []; return valueArray.map((item) => this._convertToDate(item)); } @@ -539,7 +551,6 @@ class Calendar< _init(): void { super._init(); - this._activeStateUnit = `.${CALENDAR_CELL_CLASS}`; this._initSelectionStrategy(); this._correctZoomLevel(); this._initCurrentDate(); @@ -599,21 +610,22 @@ class Calendar< _initCurrentDate(): void { const { currentDate = new Date() } = this.option(); - const date = this._getNormalizedDate(this._selectionStrategy.getDefaultCurrentDate()) + const defaultCurrentDate = this._selectionStrategy.getDefaultCurrentDate(); + const date = (defaultCurrentDate ? this._getNormalizedDate(defaultCurrentDate) : null) ?? this._getNormalizedDate(currentDate); this.option('currentDate', date); } - _getNormalizedDate(date: Date): Date { + _getNormalizedDate(date: Date): Date; + _getNormalizedDate(date: null): null; + _getNormalizedDate(date: Date | null): Date | null { const normalizedDate = dateUtils.normalizeDate(date, this._getMinDate(), this._getMaxDate()); return isDefined(normalizedDate) ? this._getDate(normalizedDate) : date; } - _initActions() { - // @ts-expect-error ts-error + _initActions(): void { this._cellClickAction = this._createActionByOption('onCellClick'); - // @ts-expect-error ts-error this._onContouredChanged = this._createActionByOption('onContouredChanged'); } @@ -701,7 +713,7 @@ class Calendar< } _getMinDate(): Date { - const { _rangeMin: rangeMin } = this.option(); + const { rangeMin } = this.option(); if (rangeMin) { return rangeMin; } @@ -715,7 +727,7 @@ class Calendar< } _getMaxDate(): Date { - const { _rangeMax: rangeMax } = this.option(); + const { rangeMax } = this.option(); if (rangeMax) { return rangeMax; } @@ -771,6 +783,7 @@ class Calendar< this._moveToClosestAvailableDate(date); + // eslint-disable-next-line no-restricted-globals this._waitRenderViewTimeout = setTimeout(() => { this._alreadyViewRender = false; }); @@ -1008,7 +1021,7 @@ class Calendar< return `${coefficient * 100 * rtlCorrection}%`; } - _cellClickHandler(e: { event: DxEvent; value: Date }): void { + _cellClickHandler(e: CellEvent): void { const zoomLevel = this.option('zoomLevel'); const nextView = dateUtils.getViewDown(zoomLevel); @@ -1098,7 +1111,7 @@ class Calendar< }; } - _navigatorClickHandler(e): void { + _navigatorClickHandler(e: { direction: number; event: ClickEvent }): void { const { currentDate, viewsCount } = this.option(); let offset = e.direction; @@ -1165,8 +1178,7 @@ class Calendar< } _swipeStartHandler(event: SwipeStartEvent): void { - // @ts-expect-error ts-error - fx.stop(this._$viewsWrapper, true); + fx.stop(this._$viewsWrapper.get(0), true); const { viewsCount } = this.option(); this._toggleGestureCoverCursor('grabbing'); @@ -1209,6 +1221,7 @@ class Calendar< && (rtlEnabled ? moveOffset === -1 : moveOffset === 1); if (moveOffset === 0) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._animateWrapper(0, ANIMATION_DURATION_SHOW_VIEW); return; } @@ -1324,10 +1337,15 @@ class Calendar< .appendTo(this.$element()); const { value } = this.option(); + this._setSubmitValue(value); } - _setSubmitValue(value): void { + _setSubmitValue(value: DateLike | DateLike[] | undefined): void { + if (this._isArrayValue('value', value)) { + return; + } + const dateValue = this._convertToDate(value); this._getSubmitElement() .val(dateSerialization.serializeDate(dateValue, CALENDAR_INPUT_STANDARD_PATTERN)); @@ -1338,8 +1356,8 @@ class Calendar< } _animateShowView(): void { - // @ts-expect-error ts-error - fx.stop(this._view.$element(), true); + fx.stop(this._view.$element().get(0), true); + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._popAnimationView( this._view, POP_ANIMATION_FROM, @@ -1350,8 +1368,8 @@ class Calendar< const { viewsCount } = this.option(); if (viewsCount > 1) { - // @ts-expect-error ts-error - fx.stop(this._additionalView.$element(), true); + fx.stop(this._additionalView.$element().get(0), true); + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._popAnimationView( this._additionalView, POP_ANIMATION_FROM, @@ -1363,12 +1381,11 @@ class Calendar< _popAnimationView( view: MonthView | YearView | DecadeView | CenturyView, - from: AnimationConfig['from'], - to: AnimationConfig['to'], + from: number, + to: number, duration: number, ): Promise { - // @ts-expect-error ts-error - return fx.animate(view.$element(), { + return fx.animate(view.$element().get(0), { type: 'pop', from: { scale: from, @@ -1414,9 +1431,8 @@ class Calendar< } } - _animateWrapper(to: AnimationConfig['to'], duration: number): Promise { - // @ts-expect-error ts-error - return fx.animate(this._$viewsWrapper, { + _animateWrapper(to: number, duration: number): Promise { + return fx.animate(this._$viewsWrapper.get(0), { type: 'slide', // @ts-expect-error ts-error from: { left: this._$viewsWrapper.position().left }, @@ -1429,10 +1445,11 @@ class Calendar< return new Date(value); } - _toTodayView(args: ClickEvent): void { + _toTodayView(args: DxEvent): void { const today = new Date(); if (this._isMaxZoomLevel()) { + // @ts-expect-error ts-error this._selectionStrategy.selectValue(today, args.event); return; } @@ -1440,6 +1457,7 @@ class Calendar< this._preventViewChangeAnimation = true; this.option('zoomLevel', this.option('maxZoomLevel')); + // @ts-expect-error this._selectionStrategy.selectValue(today, args.event); this._animateShowView(); @@ -1463,11 +1481,11 @@ class Calendar< } const { viewsCount } = this.option(); - let viewOffset; - let viewToCreateKey; - let viewToRemoveKey; - let viewBeforeCreateKey; - let viewAfterRemoveKey; + let viewOffset = -1; + let viewToCreateKey = '_afterView'; + let viewToRemoveKey = '_beforeView'; + let viewBeforeCreateKey = viewsCount === 1 ? '_view' : '_additionalView'; + let viewAfterRemoveKey = '_view'; if (offset < 0) { viewOffset = 1; @@ -1475,12 +1493,6 @@ class Calendar< viewToRemoveKey = '_afterView'; viewBeforeCreateKey = '_view'; viewAfterRemoveKey = viewsCount === 1 ? '_view' : '_additionalView'; - } else { - viewOffset = -1; - viewToCreateKey = '_afterView'; - viewToRemoveKey = '_beforeView'; - viewBeforeCreateKey = viewsCount === 1 ? '_view' : '_additionalView'; - viewAfterRemoveKey = '_view'; } if (!this[viewToCreateKey]) { @@ -1599,21 +1611,21 @@ class Calendar< _setViewsMinOption(min: Date): void { this._restoreViewsMinMaxOptions(); - this.option('_rangeMin', this._convertToDate(min)); + this.option('rangeMin', this._convertToDate(min)); this._updateViewsOption('min', this._getMinDate()); } _setViewsMaxOption(max: Date): void { this._restoreViewsMinMaxOptions(); - this.option('_rangeMax', this._convertToDate(max)); + this.option('rangeMax', this._convertToDate(max)); this._updateViewsOption('max', this._getMaxDate()); } _restoreViewsMinMaxOptions(): void { this._resetActiveState(); this.option({ - _rangeMin: null, - _rangeMax: null, + rangeMin: null, + rangeMax: null, }); this._updateViewsOption('min', this._getMinDate()); @@ -1634,23 +1646,38 @@ class Calendar< this.setAria('label', localizedNextButtonLabel, this._navigator._nextButton.$element()); } - _updateAriaSelected(value: Date[], previousValue: Date[]): void { - previousValue.forEach((item) => { + _updateAriaSelected( + value: (Date | null)[] | null, + previousValue: (Date | null)[] | null, + ): void { + previousValue?.forEach((item) => { + if (!item) { + return; + } this.setAria('selected', false, this._view._getCellByDate(item)); }); - value.forEach((item) => { + value?.forEach((item) => { + if (!item) { + return; + } this.setAria('selected', true, this._view._getCellByDate(item)); }); const { viewsCount } = this.option(); if (viewsCount > 1) { - previousValue.forEach((item) => { + previousValue?.forEach((item) => { + if (!item) { + return; + } this.setAria('selected', false, this._additionalView._getCellByDate(item)); }); - value.forEach((item) => { + value?.forEach((item) => { + if (!item) { + return; + } this.setAria('selected', true, this._additionalView._getCellByDate(item)); }); } @@ -1710,7 +1737,7 @@ class Calendar< this._invalidate(); break; case 'currentDate': - this.setAria('id', undefined, this._view._getCellByDate(previousValue)); + this.setAria('id', undefined, this._view._getCellByDate(previousValue as Date)); this._updateCurrentDate(value as Date); break; case 'zoomLevel': @@ -1731,10 +1758,12 @@ class Calendar< const isSameValue = dateUtils.sameDatesArrays(value, previousValue); if (!isSameValue) { - this._selectionStrategy.processValueChanged(value, previousValue); + this._selectionStrategy.processValueChanged( + value as (Date | null)[], + previousValue as (Date | null)[], + ); } - - this._setSubmitValue(value); + this._setSubmitValue(value as DateLike | DateLike[] | undefined); super._optionChanged(args); break; } @@ -1746,7 +1775,6 @@ class Calendar< this._view.option('onCellClick', value); break; case 'onContouredChanged': - // @ts-expect-error ts-error this._onContouredChanged = this._createActionByOption('onContouredChanged'); break; case 'disabledDates': diff --git a/packages/devextreme/js/__internal/ui/calendar/m_calendar.views.ts b/packages/devextreme/js/__internal/ui/calendar/calendar.views.ts similarity index 68% rename from packages/devextreme/js/__internal/ui/calendar/m_calendar.views.ts rename to packages/devextreme/js/__internal/ui/calendar/calendar.views.ts index 337d5f807f26..2f8803b52e83 100644 --- a/packages/devextreme/js/__internal/ui/calendar/m_calendar.views.ts +++ b/packages/devextreme/js/__internal/ui/calendar/calendar.views.ts @@ -1,5 +1,4 @@ /* eslint-disable max-classes-per-file */ -import type { template } from '@js/common'; import dateLocalization from '@js/common/core/localization/date'; import domAdapter from '@js/core/dom_adapter'; import type { dxElementWrapper } from '@js/core/renderer'; @@ -7,11 +6,11 @@ import $ from '@js/core/renderer'; import dateUtils from '@js/core/utils/date'; import dateSerialization from '@js/core/utils/date_serialization'; import type { - CalendarSelectionMode, CellTemplateData, FirstDayOfWeek, WeekNumberRule, + CalendarSelectionMode, FirstDayOfWeek, WeekNumberRule, } from '@js/ui/calendar'; -import type { BaseViewProperties } from './m_calendar.base_view'; -import BaseView from './m_calendar.base_view'; +import type { BaseViewProperties } from './calendar.base_view'; +import BaseView from './calendar.base_view'; const CALENDAR_OTHER_MONTH_CLASS = 'dx-calendar-other-month'; const CALENDAR_OTHER_VIEW_CLASS = 'dx-calendar-other-view'; @@ -28,10 +27,6 @@ export interface MonthViewProperties extends BaseViewProperties { selectionMode?: CalendarSelectionMode; selectWeekOnClick?: boolean; - - cellTemplate?: template | ( - (itemData: CellTemplateData, itemIndex: number, itemElement: Element) => dxElementWrapper - ); } export class MonthView extends BaseView { @@ -73,7 +68,7 @@ export class MonthView extends BaseView { const { colCount: columnsCount, showWeekNumbers } = this.option(); - for (let colIndex = 0, colCount = columnsCount; colIndex < colCount; colIndex++) { + for (let colIndex = 0, colCount = columnsCount; colIndex < colCount; colIndex += 1) { this._renderHeaderCell(colIndex, $headerRow); } @@ -82,8 +77,8 @@ export class MonthView extends BaseView { } } - _renderHeaderCell(cellIndex, $headerRow): void { - const { firstDayOfWeek } = this.option(); + _renderHeaderCell(cellIndex: number, $headerRow: dxElementWrapper): void { + const { firstDayOfWeek = 0 } = this.option(); const { full: fullCaption, @@ -100,7 +95,7 @@ export class MonthView extends BaseView { $headerRow.append($cell); } - _renderWeekHeaderCell($headerRow): void { + _renderWeekHeaderCell($headerRow: dxElementWrapper): void { const $weekNumberHeaderCell = $('') // @ts-expect-error ts-error .attr({ @@ -112,7 +107,7 @@ export class MonthView extends BaseView { $headerRow.prepend($weekNumberHeaderCell); } - _renderWeekNumberCell(rowData): void { + _renderWeekNumberCell(rowData: { cellDate: Date; prevCellDate: Date; row: HTMLElement }): void { const { showWeekNumbers, cellTemplate, @@ -139,7 +134,7 @@ export class MonthView extends BaseView { // @ts-expect-error ts-error cellTemplate.render(this._prepareCellTemplateData(weekNumber, -1, $cell)); } else { - cell.innerHTML = weekNumber; + cell.innerHTML = `${weekNumber}`; } rowData.row.prepend(cell); @@ -150,55 +145,63 @@ export class MonthView extends BaseView { }, $cell); } - _getWeekNumber(date) { - const { weekNumberRule, firstDayOfWeek } = this.option(); + _getWeekNumber(date: Date): number { + const { weekNumberRule = 'auto', firstDayOfWeek } = this.option(); if (weekNumberRule === 'auto') { - return dateUtils.getWeekNumber(date, firstDayOfWeek, firstDayOfWeek === 1 ? 'firstFourDays' : 'firstDay'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return dateUtils.getWeekNumber( + date, + firstDayOfWeek, + firstDayOfWeek === 1 ? 'firstFourDays' : 'firstDay', + ); } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.getWeekNumber(date, firstDayOfWeek, weekNumberRule); } getNavigatorCaption(): string { const { date } = this.option(); - // @ts-expect-error ts-error - return dateLocalization.format(date, 'monthandyear'); + return `${dateLocalization.format(date, 'monthandyear')}`; } - _isTodayCell(cellDate): boolean { + _isTodayCell(cellDate: Date): boolean { const { _todayDate: today } = this.option(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameDate(cellDate, today()); } - _isDateOutOfRange(cellDate) { + _isDateOutOfRange(cellDate: Date): boolean { const minDate = this.option('min'); const maxDate = this.option('max'); return !dateUtils.dateInRange(cellDate, minDate, maxDate, 'date'); } - _isOtherView(cellDate): boolean { + _isOtherView(cellDate: Date): boolean { const { date } = this.option(); return cellDate.getMonth() !== date.getMonth(); } - _isStartDayOfMonth(cellDate) { + _isStartDayOfMonth(cellDate: Date): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameDate(cellDate, dateUtils.getFirstMonthDate(this.option('date'))); } - _isEndDayOfMonth(cellDate) { + _isEndDayOfMonth(cellDate: Date): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameDate(cellDate, dateUtils.getLastMonthDate(this.option('date'))); } - _getCellText(cellDate) { - return dateLocalization.format(cellDate, 'd'); + _getCellText(cellDate: Date): string { + return `${dateLocalization.format(cellDate, 'd')}`; } - _getDayCaption(day) { + _getDayCaption(day: number): { full: string; abbreviated: string } { const { colCount: daysInWeek } = this.option(); const dayIndex = day % daysInWeek; @@ -208,11 +211,9 @@ export class MonthView extends BaseView { }; } - _getFirstCellData() { - const { firstDayOfWeek } = this.option(); - - const firstDay = dateUtils.getFirstMonthDate(this.option('date')); - // @ts-expect-error ts-error + _getFirstCellData(): Date { + const { firstDayOfWeek = 0, date } = this.option(); + const firstDay = dateUtils.getFirstMonthDate(date) as Date; let firstMonthDayOffset = firstDayOfWeek - firstDay.getDay(); const { colCount: daysInWeek } = this.option(); @@ -220,39 +221,35 @@ export class MonthView extends BaseView { firstMonthDayOffset -= daysInWeek; } - // @ts-expect-error ts-error firstDay.setDate(firstDay.getDate() + firstMonthDayOffset); return firstDay; } - _getNextCellData(date?) { - date = new Date(date); - date.setDate(date.getDate() + 1); - return date; + _getNextCellData(date: Date): Date { + const newDate = new Date(date); + newDate.setDate(newDate.getDate() + 1); + + return newDate; } - _getCellByDate(date) { + _getCellByDate(date: Date): dxElementWrapper { return this._$table.find(`td[data-value='${dateSerialization.serializeDate(date, dateUtils.getShortDateFormat())}']`); } - isBoundary(date) { + isBoundary(date: Date): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameMonthAndYear(date, this.option('min')) || dateUtils.sameMonthAndYear(date, this.option('max')); } - _getDefaultDisabledDatesHandler(disabledDates) { - // @ts-expect-error - return function (args) { - const isDisabledDate = disabledDates.some((item) => dateUtils.sameDate(item, args.date)); - - if (isDisabledDate) { - return true; - } - }; + _getDefaultDisabledDatesHandler( + disabledDates: Date[], + ): (args: { date: Date }) => boolean { + return (args) => disabledDates.some((item) => dateUtils.sameDate(item, args.date)); } } export class YearView extends BaseView { - _getViewName() { + _getViewName(): string { return 'year'; } @@ -260,30 +257,30 @@ export class YearView extends BaseView { return 'monthandyear'; } - _isTodayCell(cellDate) { + _isTodayCell(cellDate: Date): boolean { const { _todayDate: today } = this.option(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameMonthAndYear(cellDate, today()); } - _isDateOutOfRange(cellDate) { + _isDateOutOfRange(cellDate: Date): boolean { return !dateUtils.dateInRange(cellDate, dateUtils.getFirstMonthDate(this.option('min')), dateUtils.getLastMonthDate(this.option('max'))); } - _isOtherView() { + _isOtherView(): boolean { return false; } - _isStartDayOfMonth() { + _isStartDayOfMonth(): boolean { return false; } - _isEndDayOfMonth() { + _isEndDayOfMonth(): boolean { return false; } - // eslint-disable-next-line class-methods-use-this - _getCellText(cellDate): string { + _getCellText(cellDate: Date): string { return dateLocalization.getMonthNames('abbreviated')[cellDate.getMonth()]; } @@ -297,13 +294,14 @@ export class YearView extends BaseView { return data; } - _getNextCellData(date) { - date = new Date(date); - date.setMonth(date.getMonth() + 1); - return date; + _getNextCellData(date: Date): Date { + const newDate = new Date(date); + newDate.setMonth(newDate.getMonth() + 1); + + return newDate; } - _getCellByDate(date) { + _getCellByDate(date: Date): dxElementWrapper { const foundDate = new Date(date); foundDate.setDate(1); @@ -316,59 +314,61 @@ export class YearView extends BaseView { return `${dateLocalization.format(date, 'yyyy')}`; } - isBoundary(date) { + isBoundary(date: Date): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameYear(date, this.option('min')) || dateUtils.sameYear(date, this.option('max')); } - _renderWeekNumberCell() {} + _renderWeekNumberCell(): void {} } export class DecadeView extends BaseView { - _getViewName() { + _getViewName(): string { return 'decade'; } - _isTodayCell(cellDate) { + _isTodayCell(cellDate: Date): boolean { const { _todayDate: today } = this.option(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameYear(cellDate, today()); } - _isDateOutOfRange(cellDate): boolean { - const min = this.option('min'); - const max = this.option('max'); - // @ts-expect-error ts-error + _isDateOutOfRange(cellDate: Date): boolean { + const { min, max } = this.option(); + return !dateUtils.dateInRange(cellDate.getFullYear(), min?.getFullYear(), max?.getFullYear()); } - _isOtherView(cellDate) { + _isOtherView(cellDate: Date): boolean { const date = new Date(cellDate); date.setMonth(1); return !dateUtils.sameDecade(date, this.option('date')); } - _isStartDayOfMonth() { + _isStartDayOfMonth(): boolean { return false; } - _isEndDayOfMonth() { + _isEndDayOfMonth(): boolean { return false; } - _getCellText(cellDate) { - return dateLocalization.format(cellDate, 'yyyy'); + _getCellText(cellDate: Date): string { + return `${dateLocalization.format(cellDate, 'yyyy')}`; } - _getFirstCellData() { + _getFirstCellData(): Date { const year = dateUtils.getFirstYearInDecade(this.option('date')) - 1; return dateUtils.createDateWithFullYear(year, 0, 1); } - _getNextCellData(date): Date { - date = new Date(date); - date.setFullYear(date.getFullYear() + 1); - return date; + _getNextCellData(date: Date): Date { + const newDate = new Date(date); + newDate.setFullYear(newDate.getFullYear() + 1); + + return newDate; } getNavigatorCaption(): string { @@ -383,11 +383,12 @@ export class DecadeView extends BaseView { return `${dateLocalization.format(startDate, 'yyyy')}-${dateLocalization.format(endDate, 'yyyy')}`; } - _isValueOnCurrentView(currentDate, value) { + _isValueOnCurrentView(currentDate: Date, value: Date): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameDecade(currentDate, value); } - _getCellByDate(date) { + _getCellByDate(date: Date): dxElementWrapper { const foundDate = new Date(date); foundDate.setDate(1); foundDate.setMonth(0); @@ -395,25 +396,27 @@ export class DecadeView extends BaseView { return this._$table.find(`td[data-value='${dateSerialization.serializeDate(foundDate, dateUtils.getShortDateFormat())}']`); } - isBoundary(date) { + isBoundary(date: Date): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameDecade(date, this.option('min')) || dateUtils.sameDecade(date, this.option('max')); } - _renderWeekNumberCell() {} + _renderWeekNumberCell(): void {} } export class CenturyView extends BaseView { - _getViewName() { + _getViewName(): string { return 'century'; } - _isTodayCell(cellDate) { + _isTodayCell(cellDate: Date): boolean { const { _todayDate: today } = this.option(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameDecade(cellDate, today()); } - _isDateOutOfRange(cellDate) { + _isDateOutOfRange(cellDate: Date): boolean { const decade = dateUtils.getFirstYearInDecade(cellDate); const minDecade = dateUtils.getFirstYearInDecade(this.option('min')); const maxDecade = dateUtils.getFirstYearInDecade(this.option('max')); @@ -421,22 +424,22 @@ export class CenturyView extends BaseView { return !dateUtils.dateInRange(decade, minDecade, maxDecade); } - _isOtherView(cellDate) { + _isOtherView(cellDate: Date): boolean { const date = new Date(cellDate); date.setMonth(1); return !dateUtils.sameCentury(date, this.option('date')); } - _isStartDayOfMonth() { + _isStartDayOfMonth(): boolean { return false; } - _isEndDayOfMonth() { + _isEndDayOfMonth(): boolean { return false; } - _getCellText(cellDate): string { + _getCellText(cellDate: Date): string { const startDate = dateLocalization.format(cellDate, 'yyyy'); const endDate = new Date(cellDate); @@ -445,18 +448,19 @@ export class CenturyView extends BaseView { return `${startDate} - ${dateLocalization.format(endDate, 'yyyy')}`; } - _getFirstCellData() { + _getFirstCellData(): Date { const decade = dateUtils.getFirstDecadeInCentury(this.option('date')) - 10; return dateUtils.createDateWithFullYear(decade, 0, 1); } - _getNextCellData(date) { - date = new Date(date); - date.setFullYear(date.getFullYear() + 10); - return date; + _getNextCellData(date: Date): Date { + const newDate = new Date(date); + newDate.setFullYear(newDate.getFullYear() + 10); + + return newDate; } - _getCellByDate(date) { + _getCellByDate(date: Date): dxElementWrapper { const foundDate = new Date(date); foundDate.setDate(1); foundDate.setMonth(0); @@ -477,7 +481,8 @@ export class CenturyView extends BaseView { return `${dateLocalization.format(startDate, 'yyyy')}-${dateLocalization.format(endDate, 'yyyy')}`; } - isBoundary(date) { + isBoundary(date: Date): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return dateUtils.sameCentury(date, this.option('min')) || dateUtils.sameCentury(date, this.option('max')); } diff --git a/packages/devextreme/js/__internal/ui/calendar/m_calendar.multiple.selection.strategy.ts b/packages/devextreme/js/__internal/ui/calendar/m_calendar.multiple.selection.strategy.ts deleted file mode 100644 index 61961bf0b882..000000000000 --- a/packages/devextreme/js/__internal/ui/calendar/m_calendar.multiple.selection.strategy.ts +++ /dev/null @@ -1,57 +0,0 @@ -import CalendarSelectionStrategy from './m_calendar.selection.strategy'; - -class CalendarMultiSelectionStrategy extends CalendarSelectionStrategy { - constructor(component) { - super(component); - this.NAME = 'MultiSelection'; - } - - getViewOptions() { - return { - value: this.dateOption('value'), - range: [], - selectionMode: 'multiple', - onWeekNumberClick: this._shouldHandleWeekNumberClick() ? this._weekNumberClickHandler.bind(this) : null, - }; - } - - selectValue(selectedValue, e) { - const value = [...this.dateOption('value')]; - const alreadySelectedIndex = value.findIndex((date) => date?.toDateString() === selectedValue.toDateString()); - - if (alreadySelectedIndex > -1) { - value.splice(alreadySelectedIndex, 1); - } else { - value.push(selectedValue); - } - - this.skipNavigate(); - this._updateCurrentDate(selectedValue); - this._currentDateChanged = true; - this.dateValue(value, e); - } - - updateAriaSelected(value?, previousValue?) { - value ??= this.dateOption('value'); - previousValue ??= []; - - super.updateAriaSelected(value, previousValue); - } - - getDefaultCurrentDate() { - const dates = this.dateOption('value').filter(Boolean); - return this._getLowestDateInArray(dates); - } - - restoreValue() { - this.calendar.option('value', []); - } - - _weekNumberClickHandler({ rowDates, event }) { - const selectedDates = rowDates.filter((date) => !this._isDateDisabled(date)); - - this.dateValue(selectedDates, event); - } -} - -export default CalendarMultiSelectionStrategy; diff --git a/packages/devextreme/js/__internal/ui/calendar/m_calendar.range.selection.strategy.ts b/packages/devextreme/js/__internal/ui/calendar/m_calendar.range.selection.strategy.ts deleted file mode 100644 index b866abc2474e..000000000000 --- a/packages/devextreme/js/__internal/ui/calendar/m_calendar.range.selection.strategy.ts +++ /dev/null @@ -1,172 +0,0 @@ -import dateUtils from '@js/core/utils/date'; - -import CalendarSelectionStrategy from './m_calendar.selection.strategy'; - -const DAY_INTERVAL = 86400000; - -class CalendarRangeSelectionStrategy extends CalendarSelectionStrategy { - constructor(component) { - super(component); - this.NAME = 'RangeSelection'; - } - - getViewOptions() { - const value = this._getValue(); - const range = this._getDaysInRange(value[0], value[1]); - - return { - value, - range, - selectionMode: 'range', - onCellHover: this._cellHoverHandler.bind(this), - onWeekNumberClick: this._shouldHandleWeekNumberClick() ? this._weekNumberClickHandler.bind(this) : null, - }; - } - - selectValue(selectedValue, e) { - const [startDate, endDate] = this._getValue(); - - this.skipNavigate(); - this._updateCurrentDate(selectedValue); - this._currentDateChanged = true; - - if (this.calendar.option('_allowChangeSelectionOrder') === true) { - this.calendar._valueSelected = true; - - if (this.calendar.option('_currentSelection') === 'startDate') { - if (this.calendar._convertToDate(selectedValue) > this.calendar._convertToDate(endDate)) { - this.dateValue([selectedValue, null], e); - } else { - this.dateValue([selectedValue, endDate], e); - } - } else if (this.calendar._convertToDate(selectedValue) >= this.calendar._convertToDate(startDate)) { - this.dateValue([startDate, selectedValue], e); - } else { - this.dateValue([selectedValue, null], e); - } - } else if (!startDate || endDate) { - this.dateValue([selectedValue, null], e); - } else { - this.dateValue(startDate < selectedValue ? [startDate, selectedValue] : [selectedValue, startDate], e); - } - } - - updateAriaSelected(value?, previousValue?) { - value ??= this._getValue(); - previousValue ??= []; - - super.updateAriaSelected(value, previousValue); - } - - processValueChanged(value, previousValue) { - super.processValueChanged(value, previousValue); - - const range = this._getRange(); - this._updateViewsOption('range', range); - } - - getDefaultCurrentDate() { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { _allowChangeSelectionOrder, _currentSelection } = this.calendar.option(); - const value = this.dateOption('value'); - - if (_allowChangeSelectionOrder) { - if (_currentSelection === 'startDate' && value[0]) { - return value[0]; - } - - if (_currentSelection === 'endDate' && value[1]) { - return value[1]; - } - } - - const dates = value.filter((value) => value); - - return this._getLowestDateInArray(dates); - } - - restoreValue() { - this.calendar.option('value', [null, null]); - } - - _getValue() { - const value = this.dateOption('value'); - - if (!value.length) { - return value; - } - - let [startDate, endDate] = value; - - if (startDate && endDate && startDate > endDate) { - [startDate, endDate] = [endDate, startDate]; - } - - return [startDate, endDate]; - } - - _getRange() { - const [startDate, endDate] = this._getValue(); - return this._getDaysInRange(startDate, endDate); - } - - _getDaysInRange(startDate, endDate) { - if (!startDate || !endDate) { - return []; - } - - const { currentDate, viewsCount } = this.calendar.option(); - const isAdditionalViewDate = this.calendar._isAdditionalViewDate(currentDate); - const firstDateInViews = dateUtils.getFirstMonthDate(currentDate, isAdditionalViewDate ? -2 : -1); - const lastDateInViews = dateUtils.getLastMonthDate(currentDate, isAdditionalViewDate ? 1 : viewsCount); - - // @ts-expect-error - const rangeStartDate = new Date(Math.max(firstDateInViews, startDate)); - // @ts-expect-error - const rangeEndDate = new Date(Math.min(lastDateInViews, endDate)); - - return [...dateUtils.getDatesOfInterval(rangeStartDate, rangeEndDate, DAY_INTERVAL), rangeEndDate]; - } - - _cellHoverHandler(e) { - const isMaxZoomLevel = this._isMaxZoomLevel(); - const [startDate, endDate] = this._getValue(); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { _allowChangeSelectionOrder, _currentSelection } = this.calendar.option(); - - if (isMaxZoomLevel) { - const skipHoveredRange = _allowChangeSelectionOrder && _currentSelection === 'startDate'; - - if (startDate && !endDate && !skipHoveredRange) { - if (e.value > startDate) { - this._updateViewsOption('hoveredRange', this._getDaysInRange(startDate, e.value)); - return; - } - } else if (!startDate && endDate && !(_allowChangeSelectionOrder && _currentSelection === 'endDate')) { - if (e.value < endDate) { - this._updateViewsOption('hoveredRange', this._getDaysInRange(e.value, endDate)); - return; - } - } else if (startDate && endDate) { - if (_currentSelection === 'startDate' && e.value < startDate) { - this._updateViewsOption('hoveredRange', this._getDaysInRange(e.value, startDate)); - return; - } if (_currentSelection === 'endDate' && e.value > endDate) { - this._updateViewsOption('hoveredRange', this._getDaysInRange(endDate, e.value)); - return; - } - } - - this._updateViewsOption('hoveredRange', []); - } - } - - _weekNumberClickHandler({ rowDates, event }) { - const selectedDates = rowDates.filter((date) => !this._isDateDisabled(date)); - const value = selectedDates.length ? [selectedDates[0], selectedDates[selectedDates.length - 1]] : [null, null]; - - this.dateValue(value, event); - } -} - -export default CalendarRangeSelectionStrategy; diff --git a/packages/devextreme/js/__internal/ui/calendar/m_calendar.single.selection.strategy.ts b/packages/devextreme/js/__internal/ui/calendar/m_calendar.single.selection.strategy.ts deleted file mode 100644 index 73196c216cad..000000000000 --- a/packages/devextreme/js/__internal/ui/calendar/m_calendar.single.selection.strategy.ts +++ /dev/null @@ -1,48 +0,0 @@ -import CalendarSelectionStrategy from './m_calendar.selection.strategy'; - -class CalendarSingleSelectionStrategy extends CalendarSelectionStrategy { - constructor(component) { - super(component); - this.NAME = 'SingleSelection'; - } - - getViewOptions() { - return { - value: this.dateOption('value'), - range: [], - selectionMode: 'single', - }; - } - - selectValue(selectedValue, e) { - this.skipNavigate(); - this.dateValue(selectedValue, e); - } - - updateAriaSelected(value?, previousValue?) { - value ??= [this.dateOption('value')]; - previousValue ??= []; - - super.updateAriaSelected(value, previousValue); - } - - getDefaultCurrentDate() { - const date = this.dateOption('value'); - - if (date === '') { - return new Date(); - } - - return date; - } - - restoreValue(): void { - this.calendar.option('value', null); - } - - _updateViewsValue(value) { - this._updateViewsOption('value', value[0]); - } -} - -export default CalendarSingleSelectionStrategy; diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 49f11f1c2c4d..9c13d2607d32 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -31,6 +31,7 @@ import type { } from '@ts/ui/chat/messagebox'; import MessageBox from '@ts/ui/chat/messagebox'; import type { + EmptyViewTemplate, MessageEditingEvent, MessageTemplate, Properties as MessageListProperties, @@ -89,6 +90,7 @@ class Chat extends Widget { dayHeaderFormat: 'shortdate', messageTemplate: null, messageTimestampFormat: 'shorttime', + emptyViewTemplate: null, alerts: [], showAvatar: true, showUserName: true, @@ -201,6 +203,7 @@ class Chat extends Widget { allowDeleting: (message: Message): boolean => this._allowDeleteAction(message), isEditActionDisabled: (message) => this._messageToEdit === message, messageTemplate: this._getMessageTemplate(), + emptyViewTemplate: this._getEmptyViewTemplate(), showDayHeaders, showAvatar, showUserName, @@ -259,17 +262,23 @@ class Chat extends Widget { return allowDeleting ?? false; } - _getMessageTemplate(): MessageTemplate { - const { messageTemplate } = this.option(); - if (messageTemplate) { - return (message, $container): void => { - const template = this._getTemplateByOption('messageTemplate'); + _getRenderTemplateFunction(optionName: 'messageTemplate'): MessageTemplate; + _getRenderTemplateFunction(optionName: 'emptyViewTemplate'): EmptyViewTemplate; + _getRenderTemplateFunction( + optionName: 'messageTemplate' | 'emptyViewTemplate', + ): MessageTemplate | EmptyViewTemplate { + const { [optionName]: templateOption } = this.option(); + + if (templateOption) { + return (data, $container): void => { + const template = this._getTemplateByOption(optionName); + const dataFieldName = optionName === 'messageTemplate' ? 'message' : 'texts'; template.render({ container: $container, model: { component: this, - message, + [dataFieldName]: data, }, }); }; @@ -278,6 +287,14 @@ class Chat extends Widget { return null; } + _getMessageTemplate(): MessageTemplate { + return this._getRenderTemplateFunction('messageTemplate'); + } + + _getEmptyViewTemplate(): EmptyViewTemplate { + return this._getRenderTemplateFunction('emptyViewTemplate'); + } + _messageEditingStartHandler(e: MessageEditingEvent): void { if (this._messageToEdit) { this._messageEditCanceledAction?.({ message: this._messageToEdit }); @@ -615,6 +632,9 @@ class Chat extends Widget { case 'messageTemplate': this._messageList.option(name, this._getMessageTemplate()); break; + case 'emptyViewTemplate': + this._messageList.option(name, this._getEmptyViewTemplate()); + break; case 'reloadOnChange': break; default: diff --git a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts index dccf56e4b2e1..a31a9dd8af7c 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts @@ -113,11 +113,10 @@ class MessageGroup extends Widget { _renderMessageBubble(message: Message): void { const $bubble = $('
') - .data(MESSAGE_DATA_KEY, message); + .data(MESSAGE_DATA_KEY, message) + .appendTo(this._$messageBubbleContainer); this._createComponent($bubble, MessageBubble, this._getMessageBubbleOptions(message)); - - this._$messageBubbleContainer.append($bubble); } _getMessageBubbleOptions(message: Message): MessageBubbleProperties { @@ -145,7 +144,9 @@ class MessageGroup extends Widget { } _renderMessageBubbles(items: Message[]): void { - this._$messageBubbleContainer = $('
').addClass(CHAT_MESSAGEGROUP_CONTENT_CLASS); + this._$messageBubbleContainer = $('
') + .addClass(CHAT_MESSAGEGROUP_CONTENT_CLASS) + .appendTo(this.element()); items.forEach((message, index) => { const shouldCreateEditedElement = index !== 0 && message.type !== 'image' && (message as TextMessage).isEdited === true && !message.isDeleted; @@ -158,8 +159,6 @@ class MessageGroup extends Widget { this._renderMessageBubble(message); }); - - this._$messageBubbleContainer.appendTo(this.element()); } _renderMessageGroupInformation(message: Message, shouldRenderEditedMessage?: boolean): void { diff --git a/packages/devextreme/js/__internal/ui/chat/messagelist.ts b/packages/devextreme/js/__internal/ui/chat/messagelist.ts index 5692e4186a3c..1066eb4d0cf6 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagelist.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagelist.ts @@ -17,8 +17,10 @@ import type { Message, TextMessage, User } from '@js/ui/chat'; import type { Item as ContextMenuItem } from '@js/ui/context_menu'; import type dxContextMenu from '@js/ui/context_menu'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; +import { getPublicElement } from '@ts/core/m_element'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; +import type { ClickableCollectionWidgetItem } from '@ts/ui/collection/item'; import ContextMenu from '@ts/ui/context_menu/context_menu'; import type { ScrollView as ScrollViewType, @@ -64,6 +66,10 @@ const ESCAPE_KEY = 'escape'; export const MESSAGEGROUP_TIMEOUT = 5 * 1000 * 60; export type MessageTemplate = ((data: Message, messageBubbleContainer: Element) => void) | null; +export type EmptyViewTemplate = (( + data: { message: string; prompt: string }, + emptyViewContainer: Element) => void +) | null; export type ItemClick = NativeEventInfo & { readonly itemData?: ContextMenuItem; @@ -88,6 +94,7 @@ export interface Properties extends WidgetOptions { currentUserId: number | string | undefined; showDayHeaders: boolean; messageTemplate?: MessageTemplate; + emptyViewTemplate?: EmptyViewTemplate; dayHeaderFormat?: Format; messageTimestampFormat?: Format; typingUsers: User[]; @@ -131,6 +138,7 @@ class MessageList extends Widget { showAvatar: true, showUserName: true, showMessageTimestamp: true, + emptyViewTemplate: null, messageTemplate: null, }; } @@ -198,23 +206,34 @@ class MessageList extends Widget { } _renderEmptyViewContent(): void { + const messageText = messageLocalization.format('dxChat-emptyListMessage'); + const promptText = messageLocalization.format('dxChat-emptyListPrompt'); + const { emptyViewTemplate } = this.option(); + const $emptyView = $('
') .addClass(CHAT_MESSAGELIST_EMPTY_VIEW_CLASS) .attr('id', `dx-${new Guid()}`); + if (emptyViewTemplate) { + const data = { + message: messageText, + prompt: promptText, + }; + emptyViewTemplate(data, getPublicElement($emptyView)); + $emptyView.appendTo(this._$content); + + return; + } + $('
') .appendTo($emptyView) .addClass(CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS); - const messageText = messageLocalization.format('dxChat-emptyListMessage'); - $('
') .appendTo($emptyView) .addClass(CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS) .text(messageText); - const promptText = messageLocalization.format('dxChat-emptyListPrompt'); - $('
') .appendTo($emptyView) .addClass(CHAT_MESSAGELIST_EMPTY_PROMPT_CLASS) @@ -283,14 +302,14 @@ class MessageList extends Widget { const editText = messageLocalization.format('dxChat-editingEditMessage'); const deleteText = messageLocalization.format('dxChat-editingDeleteMessage'); - const buttons: ContextMenuItem[] = []; + const buttons: ClickableCollectionWidgetItem[] = []; if (allowUpdating(message) && message.type !== 'image') { buttons.push({ icon: 'edit', text: editText, disabled: isEditActionDisabled(message), - // @ts-expect-error ts-error + // @ts-expect-error itemElement onClick: (e: ItemClick): void => { const onMessageEditStarted = onMessageEditingStart?.({ event: e.event, message: message as TextMessage, @@ -310,7 +329,7 @@ class MessageList extends Widget { buttons.push({ icon: 'trash', text: deleteText, - // @ts-expect-error ts-error + // @ts-expect-error itemElement onClick(e: ItemClick): void { onMessageDeleting?.({ event: e.event, message }); }, @@ -527,7 +546,7 @@ class MessageList extends Widget { const $lastMessageGroup = this._$content.find(`.${CHAT_MESSAGEGROUP_CLASS}`).last(); if ($lastMessageGroup.length) { - return MessageGroup.getInstance($lastMessageGroup) as MessageGroup; + return MessageGroup.getInstance($lastMessageGroup); } return undefined; @@ -809,6 +828,7 @@ class MessageList extends Widget { case 'showUserName': case 'showMessageTimestamp': case 'messageTemplate': + case 'emptyViewTemplate': case 'dayHeaderFormat': case 'messageTimestampFormat': this._invalidate(); diff --git a/packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts b/packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts index de159e5aec48..f5527ae02f36 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts @@ -37,6 +37,7 @@ import type { ActionConfig } from '@ts/core/widget/component'; import type { OptionChanged } from '@ts/core/widget/types'; import type { SupportedKeys, WidgetProperties } from '@ts/core/widget/widget'; import Widget from '@ts/core/widget/widget'; +import type { ClickableCollectionWidgetItem, ItemClickEvent } from '@ts/ui/collection/item'; import type CollectionItem from '@ts/ui/collection/item'; import CollectionWidgetItem from '@ts/ui/collection/item'; @@ -117,8 +118,7 @@ export type Constructor = new (...args: unknown[]) => T; export interface CollectionWidgetBaseProperties< // eslint-disable-next-line @typescript-eslint/no-explicit-any TComponent extends CollectionWidget | any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TItem extends ItemLike = any, + TItem extends ItemLike = CollectionWidgetItemProperties, // eslint-disable-next-line @typescript-eslint/no-explicit-any TKey extends CollectionItemKey = any, > extends CollectionWidgetOptions, Omit< @@ -145,8 +145,7 @@ export interface CollectionWidgetBaseProperties< class CollectionWidget< // eslint-disable-next-line @typescript-eslint/no-explicit-any TProperties extends CollectionWidgetBaseProperties, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TItem extends ItemLike = any, + TItem extends ItemLike = CollectionWidgetItemProperties, // eslint-disable-next-line @typescript-eslint/no-explicit-any TKey extends CollectionItemKey = any, > extends Widget { @@ -180,6 +179,10 @@ class CollectionWidget< }) => void; }; + protected _activeStateUnit(): string { + return `.${ITEM_CLASS}`; + } + _supportedKeys(): SupportedKeys { const space = (e: DxEvent): void => { e.preventDefault(); @@ -231,12 +234,10 @@ class CollectionWidget< } const itemData = this._getItemData($itemElement); - // @ts-expect-error ts-error - if (itemData?.onClick) { + if (CollectionWidgetItem.isClickableItem(itemData)) { const actionArgs: ActionArgs = { event: e, }; - // @ts-expect-error this._itemEventHandlerByHandler($itemElement, itemData.onClick, actionArgs); } // @ts-expect-error ts-error @@ -285,8 +286,6 @@ class CollectionWidget< this._initDataController(); super._init(); - this._activeStateUnit = `.${ITEM_CLASS}`; - this._cleanRenderedItems(); // @ts-expect-error ts-error this._refreshDataSource(); @@ -435,7 +434,7 @@ class CollectionWidget< } _findActiveTarget($element: dxElementWrapper): dxElementWrapper { - return $element.find(this._activeStateUnit); + return $element.find(this._activeStateUnit()); } _getActiveItem(last?: boolean): dxElementWrapper { @@ -1240,8 +1239,7 @@ class CollectionWidget< } _attachItemClickEvent(itemData: TItem, $itemElement: dxElementWrapper): void { - // @ts-expect-error ts-error - if (!itemData || !itemData.onClick) { + if (!itemData || !CollectionWidgetItem.isClickableItem(itemData)) { return; } @@ -1252,7 +1250,6 @@ class CollectionWidget< const actionArgs = { event: e, }; - // @ts-expect-error ts-error this._itemEventHandlerByHandler($itemElement, itemData.onClick, actionArgs); }, ); @@ -1487,7 +1484,7 @@ class CollectionWidget< _itemEventHandlerByHandler( initiator: dxElementWrapper | Element, - handler: () => void, + handler: (e: ItemClickEvent) => void, actionArgs: ActionArgs, actionConfig?: ActionConfig, ): void { 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 bde69d1b62c9..5d76958d74df 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts @@ -14,7 +14,7 @@ import { when, } from '@js/core/utils/deferred'; import { each } from '@js/core/utils/iterator'; -import { isDefined } from '@js/core/utils/type'; +import { isDefined, isObject } from '@js/core/utils/type'; import type { DxEvent } from '@js/events'; import type { ItemLike, SelectionChangeInfo } from '@js/ui/collection/ui.collection_widget.base'; import errors from '@js/ui/widget/ui.errors'; @@ -29,7 +29,7 @@ import type { 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 Selection from '@ts/ui/selection/m_selection'; +import Selection from '@ts/ui/selection/selection'; import type { CollectionItemIndex } from './collection_widget.edit.strategy'; @@ -76,7 +76,8 @@ class CollectionWidget< > extends BaseCollectionWidget { static _userOptions = {}; - _selection!: Selection; + // @ts-expect-error TItem + _selection!: Selection; _editStrategy!: PlainEditStrategy; @@ -169,7 +170,7 @@ class CollectionWidget< return this._editStrategy.getItemsByKeys(selectedItemKeys, selectedItems); } - _getKeyByIndex(index: CollectionItemIndex): unknown { + _getKeyByIndex(index: CollectionItemIndex): TKey { return this._editStrategy.getKeyByIndex(index); } @@ -224,7 +225,8 @@ class CollectionWidget< const { itemsGetter } = this._editStrategy; const { selectionMode, maxFilterLengthInRequest } = this.option(); - this._selection = new Selection({ + // @ts-expect-error TItem + this._selection = new Selection({ allowNullValue: this._nullValueSelectionSupported(), mode: selectionMode, maxFilterLengthInRequest, @@ -259,7 +261,7 @@ class CollectionWidget< }, key: this.key.bind(this), keyOf: this.keyOf.bind(this), - load(options): DeferredObj { + load(options): DeferredObj { const dataController = that._dataController; options.customQueryParams = dataController.loadOptions()?.customQueryParams; options.userData = dataController.userData(); @@ -276,7 +278,7 @@ class CollectionWidget< dataController.applyMapFunction(items); }); } - return Deferred().resolve(this.plainItems()); + 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 @@ -381,7 +383,8 @@ class CollectionWidget< break; } case 'selectedItem': { - const { selectedItem = {} as TItem, selectionRequired } = this.option(); + const { selectedItem, selectionRequired } = this.option(); + // @ts-expect-error const selectedIndex = this._editStrategy.getIndexByItemData(selectedItem); if (selectionRequired && !indexExists(selectedIndex)) { @@ -479,7 +482,8 @@ class CollectionWidget< const { grouped } = this.option(); - if (grouped && normalizedSelection?.items) { + const hasSubItems = (item: TItem): item is TItem & { items: TItem[] } => isObject(item) && 'items' in item && Array.isArray(item.items); + if (grouped && hasSubItems(normalizedSelection)) { normalizedSelection.items = [normalizedSelection.items[0]]; } diff --git a/packages/devextreme/js/__internal/ui/collection/item.ts b/packages/devextreme/js/__internal/ui/collection/item.ts index 606484d6751b..e40040b377fb 100644 --- a/packages/devextreme/js/__internal/ui/collection/item.ts +++ b/packages/devextreme/js/__internal/ui/collection/item.ts @@ -1,8 +1,10 @@ +import type { ItemInfo, NativeEventInfo } from '@js/common/core/events'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { each } from '@js/core/utils/iterator'; import { attachInstanceToElement, getInstanceByElement } from '@js/core/utils/public_component'; -import type { CollectionWidgetItem } from '@js/ui/collection/ui.collection_widget.base'; +import { isObject } from '@js/core/utils/type'; +import type { CollectionWidgetItem, ItemLike } from '@js/ui/collection/ui.collection_widget.base'; const INVISIBLE_STATE_CLASS = 'dx-state-invisible'; const DISABLED_STATE_CLASS = 'dx-state-disabled'; @@ -51,6 +53,17 @@ export interface ItemExtraOption { ) => () => void; } +export type ItemClickEvent = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + NativeEventInfo + & ItemInfo; + +export type ClickableCollectionWidgetItem< + TProperties extends CollectionWidgetItem = CollectionWidgetItem, +> = TProperties & { + onClick: (e: ItemClickEvent) => void; +}; + class CollectionItem< TProperties extends CollectionWidgetItem = CollectionWidgetItem, > { @@ -159,9 +172,14 @@ class CollectionItem< // eslint-disable-next-line @typescript-eslint/no-explicit-any static getInstance>($element: dxElementWrapper): T { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return getInstanceByElement($element, this); } + + static isClickableItem( + item: ItemLike, + ): item is ClickableCollectionWidgetItem { + return isObject(item) && 'onClick' in item; + } } export default CollectionItem; diff --git a/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts b/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts index d3c682be812b..c0583fe7ccaf 100644 --- a/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts +++ b/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts @@ -24,6 +24,9 @@ const COLOR_BOX_BUTTONS_CONTAINER_CLASS = 'dx-colorview-buttons-container'; const COLOR_BOX_APPLY_BUTTON_CLASS = 'dx-colorview-apply-button'; const COLOR_BOX_CANCEL_BUTTON_CLASS = 'dx-colorview-cancel-button'; +export const DX_ICON_CLASS = 'dx-icon'; +export const DX_ICON_COLOR_DISMISS = 'dx-icon-colordismiss'; + const colorEditorPrototype = ColorView.prototype; const colorUtils = { makeTransparentBackground: colorEditorPrototype._makeTransparentBackground.bind(colorEditorPrototype), @@ -47,6 +50,8 @@ class ColorBox extends DropDownEditor { _$colorResultPreview!: dxElementWrapper; + _$noColorIcon?: dxElementWrapper | null; + _$colorBoxInputContainer!: dxElementWrapper; _supportedKeys(): Record boolean | undefined> { @@ -156,9 +161,7 @@ class ColorBox extends DropDownEditor { _applyNewColor(value): void { this.option('value', value); - if (value) { - colorUtils.makeTransparentBackground(this._$colorResultPreview, value); - } + this._updateNoColorIndicator(); if (this._colorViewEnterKeyPressed) { this.close(); @@ -280,6 +283,30 @@ class ColorBox extends DropDownEditor { this._renderColorPreview(); } + _renderNoColorIcon(): void { + if (!this._$noColorIcon || !this._$noColorIcon.length) { + this._$noColorIcon = $('') + .addClass(`${DX_ICON_CLASS} ${DX_ICON_COLOR_DISMISS}`) + .appendTo(this._$colorResultPreview); + } + } + + _updateNoColorIndicator(): void { + const { value } = this.option(); + const hasValue = Boolean(value); + + this._$colorBoxInputContainer.toggleClass(COLOR_BOX_COLOR_IS_NOT_DEFINED, !hasValue); + + if (hasValue) { + this._cleanNoColorIcon(); + + colorUtils.makeTransparentBackground(this._$colorResultPreview, value); + } else { + this._$colorResultPreview.removeAttr('style'); + this._renderNoColorIcon(); + } + } + _renderColorPreview(): void { this.$element().wrapInner($('
').addClass(COLOR_BOX_INPUT_CONTAINER_CLASS)); this._$colorBoxInputContainer = this.$element().children().eq(0); @@ -288,11 +315,7 @@ class ColorBox extends DropDownEditor { .addClass(COLOR_BOX_COLOR_RESULT_PREVIEW_CLASS) .appendTo(this._$textEditorInputContainer); - if (!this.option('value')) { - this._$colorBoxInputContainer.addClass(COLOR_BOX_COLOR_IS_NOT_DEFINED); - } else { - colorUtils.makeTransparentBackground(this._$colorResultPreview, this.option('value')); - } + this._updateNoColorIndicator(); } _renderValue() { @@ -349,9 +372,34 @@ class ColorBox extends DropDownEditor { return value; } + // eslint-disable-next-line class-methods-use-this + _shouldLogFieldTemplateDeprecationWarning(): boolean { + return true; + } + + _cleanNoColorIcon(): void { + this._$noColorIcon?.remove(); + this._$noColorIcon = undefined; + } + _clean(): void { super._clean(); delete this._shouldSaveEmptyValue; + + this._cleanNoColorIcon(); + } + + _valueOptionChangeHandler(): void { + const { value } = this.option(); + + if (value === null) { + this._shouldSaveEmptyValue = true; + } + + this._updateNoColorIndicator(); + this._updateColorViewValue(value); + + this._shouldSaveEmptyValue = false; } _optionChanged(args: OptionChanged): void { @@ -359,20 +407,7 @@ class ColorBox extends DropDownEditor { switch (name) { case 'value': - this._$colorBoxInputContainer.toggleClass(COLOR_BOX_COLOR_IS_NOT_DEFINED, !value); - - if (value) { - colorUtils.makeTransparentBackground(this._$colorResultPreview, value); - } else { - this._$colorResultPreview.removeAttr('style'); - } - - if (value === null) { - this._shouldSaveEmptyValue = true; - } - this._updateColorViewValue(value); - this._shouldSaveEmptyValue = false; - + this._valueOptionChangeHandler(); super._optionChanged(args); break; case 'applyButtonText': diff --git a/packages/devextreme/js/__internal/ui/context_menu/context_menu.ts b/packages/devextreme/js/__internal/ui/context_menu/context_menu.ts index 22f59dc4a01e..fd41a5febf33 100644 --- a/packages/devextreme/js/__internal/ui/context_menu/context_menu.ts +++ b/packages/devextreme/js/__internal/ui/context_menu/context_menu.ts @@ -29,13 +29,12 @@ import type { HiddenEvent, HidingEvent, Item, - ItemClickEvent, PositioningEvent, Properties, ShowingEvent, ShownEvent, } from '@js/ui/context_menu'; -import type dxContextMenu from '@js/ui/context_menu'; +import type dxMenuBase from '@js/ui/context_menu/ui.menu_base'; import type { dxMenuBaseItem, SubmenuHiddenEvent, @@ -45,10 +44,11 @@ import type { } from '@js/ui/menu'; import type { Properties as OverlayProperties } from '@js/ui/overlay'; import { current as currentTheme, isGeneric } from '@js/ui/themes'; -import type { ActionArguments } from '@ts/core/m_action'; import type { OptionChanged } from '@ts/core/widget/types'; import type { SupportedKeys } from '@ts/core/widget/widget'; -import type { ClickEvent, HoverEvent, MenuBaseProperties } from '@ts/ui/context_menu/menu_base'; +import type { + ClickEvent, HoverEvent, ItemClickActionArguments, MenuBaseProperties, +} from '@ts/ui/context_menu/menu_base'; import MenuBase from '@ts/ui/context_menu/menu_base'; import type { InternalNode } from '@ts/ui/hierarchical_collection/data_converter'; import Overlay from '@ts/ui/overlay/overlay'; @@ -109,8 +109,6 @@ interface SubmenuCreatedEvent { submenuElement: Element; itemData: TItem; } -type ItemClickActionArguments = - ActionArguments, ItemClickEvent>; interface ContextMenuActions { onShowing?: ((e: ShowingEvent | SubmenuShowingEvent | ChatMenuShowingEvent) => void); @@ -127,20 +125,18 @@ interface ContextMenuActions { type ContextMenuPropertiesKeys = Exclude; -export interface ContextMenuProperties< - TItem extends dxMenuBaseItem = Item, -> extends +export interface ContextMenuProperties extends MenuBaseProperties, Pick, ContextMenuPropertiesKeys> { hideOnParentScroll?: boolean; visualContainer?: string | Element | Window | null; overlayContainer?: string | Element | null; boundaryOffset?: PositionConfig['boundaryOffset']; - onSubmenuCreated?: ((e) => void) | null; - onLeftFirstItem?: ((e) => void) | null; - onLeftLastItem?: ((e) => void) | null; - onCloseRootSubmenu?: ((e) => void) | null; - onExpandLastSubmenu?: ((e) => void) | null; + onSubmenuCreated?: ((e: SubmenuCreatedEvent) => void) | null; + onLeftFirstItem?: (($item?: dxElementWrapper) => void) | null; + onLeftLastItem?: (($item?: dxElementWrapper) => void) | null; + onCloseRootSubmenu?: (($item?: dxElementWrapper) => void) | null; + onExpandLastSubmenu?: (($item?: dxElementWrapper) => void) | null; } class ContextMenu< @@ -588,7 +584,7 @@ class ContextMenu< } } - _hoverEndHandler(e: DxEvent): void { + _hoverEndHandler(e: HoverEvent): void { super._hoverEndHandler(e); e.stopPropagation(); } @@ -973,8 +969,9 @@ class ContextMenu< } // TODO: try to simplify it - // @ts-expect-error ts-error - _updateSubmenuVisibilityOnClick(actionArgs: ItemClickActionArguments): void { + _updateSubmenuVisibilityOnClick( + actionArgs: ItemClickActionArguments, Item>, + ): void { if (!actionArgs.args?.length) { return; } @@ -1005,7 +1002,6 @@ class ContextMenu< return; } - // @ts-expect-error ts-error this._updateSelectedItemOnClick(actionArgs); // T238943. Give the workaround with e.cancel and remove this hack @@ -1181,7 +1177,6 @@ class ContextMenu< this._setAriaAttributes(); // T983617. Prevent the browser's context menu appears on desktop touch screens. - // @ts-expect-error ts-error if (event?.originalEvent?.type === holdEvent.name) { this.preventShowingDefaultContextMenuAboveOverlay(); } diff --git a/packages/devextreme/js/__internal/ui/context_menu/menu_base.ts b/packages/devextreme/js/__internal/ui/context_menu/menu_base.ts index 7af857eb0db0..cae9c2d57dc6 100644 --- a/packages/devextreme/js/__internal/ui/context_menu/menu_base.ts +++ b/packages/devextreme/js/__internal/ui/context_menu/menu_base.ts @@ -12,7 +12,6 @@ import type dxMenuBase from '@js/ui/context_menu/ui.menu_base'; import type { dxMenuBaseItem, Item, - ItemClickEvent as MenuItemClickEvent, SubmenuShowMode, } from '@js/ui/menu'; import type { ActionArguments } from '@ts/core/m_action'; @@ -51,20 +50,22 @@ const DX_ICON_WITH_URL_CLASS = 'dx-icon-with-url'; const ITEM_URL_CLASS = 'dx-item-url'; const DX_MENU_ITEM_DATA_KEY = 'dxMenuItemDataKey'; -type BaseItemClickEvent = - NativeEventInfo, MouseEvent | PointerEvent | TouchEvent> - & ItemInfo; +type ItemClickEvent = + NativeEventInfo + & ItemInfo; export type HoverEvent = DxEvent; export type ClickEvent = DxEvent; -export type ItemClickActionArguments = ActionArguments< - dxMenuBase, - BaseItemClickEvent | MenuItemClickEvent +export type ItemClickActionArguments< + TComponent extends dxMenuBase = dxMenuBase, + TItem extends dxMenuBaseItem = dxMenuBaseItem, +> = ActionArguments< + TComponent, + ItemClickEvent >; type MenuBaseNode = InternalNode & dxMenuBaseItem; export interface MenuBaseProperties< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TItem extends dxMenuBaseItem | any = any, + TItem extends dxMenuBaseItem = dxMenuBaseItem, // @ts-expect-error ts-error > extends dxMenuBaseOptions { focusedElement?: Element | null; @@ -84,6 +85,10 @@ class MenuBase< // eslint-disable-next-line no-restricted-globals _showSubmenusTimeout?: ReturnType; + protected _activeStateUnit(): string { + return `.${ITEM_CLASS}`; + } + _getDefaultOptions(): TProperties { return { ...super._getDefaultOptions(), @@ -179,7 +184,6 @@ class MenuBase< _init(): void { super._init(); - this._activeStateUnit = `.${ITEM_CLASS}`; this._renderSelectedItem(); this._initActions(); @@ -359,7 +363,7 @@ class MenuBase< } } - _hoverStartHandler(e: DxEvent): void { + _hoverStartHandler(e: HoverEvent): void { const $itemElement = this._getItemElementByEventArgs(e); if (!$itemElement || this._isItemDisabled($itemElement)) return; @@ -419,7 +423,7 @@ class MenuBase< } _getItemElementByEventArgs( - eventArgs: HoverEvent | ClickEvent | DxEvent, + eventArgs: DxEvent, ): dxElementWrapper | null { let $target = $(eventArgs.target); @@ -439,7 +443,7 @@ class MenuBase< } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _hoverEndHandler(event: DxEvent): void { + _hoverEndHandler(event: HoverEvent): void { clearTimeout(this._showSubmenusTimeout); } diff --git a/packages/devextreme/js/__internal/ui/date_range_box/m_date_range_box.ts b/packages/devextreme/js/__internal/ui/date_range_box/m_date_range_box.ts index f0eecbaca3fa..87ccccac5fc4 100644 --- a/packages/devextreme/js/__internal/ui/date_range_box/m_date_range_box.ts +++ b/packages/devextreme/js/__internal/ui/date_range_box/m_date_range_box.ts @@ -149,7 +149,7 @@ class DateRangeBox extends Editor { value: [null, null], valueChangeEvent: 'change', _internalValidationErrors: [], - _currentSelection: 'startDate', + currentSelection: 'startDate', }); } @@ -1061,7 +1061,7 @@ class DateRangeBox extends Editor { break; } - case '_currentSelection': + case 'currentSelection': break; default: // @ts-expect-error diff --git a/packages/devextreme/js/__internal/ui/date_range_box/m_multiselect_date_box.ts b/packages/devextreme/js/__internal/ui/date_range_box/m_multiselect_date_box.ts index ba789d13cdbb..de786de86282 100644 --- a/packages/devextreme/js/__internal/ui/date_range_box/m_multiselect_date_box.ts +++ b/packages/devextreme/js/__internal/ui/date_range_box/m_multiselect_date_box.ts @@ -159,10 +159,10 @@ class MultiselectDateBox extends TypedDateBox { const dateRangeBox = this._getDateRangeBox(); const [startDateInput, endDateInput] = dateRangeBox.field(); if ($(target).is($(startDateInput))) { - dateRangeBox.option('_currentSelection', 'startDate'); + dateRangeBox.option('currentSelection', 'startDate'); } if ($(target).is($(endDateInput))) { - dateRangeBox.option('_currentSelection', 'endDate'); + dateRangeBox.option('currentSelection', 'endDate'); } if (!dateRangeBox.getStartDateBox().getStrategy().getWidget()) { @@ -181,7 +181,7 @@ class MultiselectDateBox extends TypedDateBox { calendar.option('currentDate', startDate); } this.getStrategy().setActiveStartDateBox(); - calendar.option('_currentSelection', 'startDate'); + calendar.option('currentSelection', 'startDate'); if (dateRangeBox.option('disableOutOfRangeSelection')) { // @ts-expect-error @@ -203,7 +203,7 @@ class MultiselectDateBox extends TypedDateBox { calendar.option('currentDate', endDate); } dateRangeBox.getStartDateBox().getStrategy().setActiveEndDateBox(); - calendar.option('_currentSelection', 'endDate'); + calendar.option('currentSelection', 'endDate'); if (dateRangeBox.option('disableOutOfRangeSelection')) { // @ts-expect-error diff --git a/packages/devextreme/js/__internal/ui/date_range_box/strategy/m_rangeCalendar.ts b/packages/devextreme/js/__internal/ui/date_range_box/strategy/m_rangeCalendar.ts index 47475137df67..179a411b42c1 100644 --- a/packages/devextreme/js/__internal/ui/date_range_box/strategy/m_rangeCalendar.ts +++ b/packages/devextreme/js/__internal/ui/date_range_box/strategy/m_rangeCalendar.ts @@ -133,8 +133,8 @@ class RangeCalendarStrategy extends CalendarStrategy { value, selectionMode: 'range', viewsCount: multiView ? 2 : 1, - _allowChangeSelectionOrder: true, - _currentSelection: this.getCurrentSelection(), + allowChangeSelectionOrder: true, + currentSelection: this.getCurrentSelection(), }); } @@ -234,11 +234,11 @@ class RangeCalendarStrategy extends CalendarStrategy { } getCurrentSelection() { - return this.getDateRangeBox().option('_currentSelection'); + return this.getDateRangeBox().option('currentSelection'); } _getCalendarCurrentSelection() { - return this.getWidget().option('_currentSelection'); + return this.getWidget().option('currentSelection'); } _closeDropDownByEnter(): boolean { diff --git a/packages/devextreme/js/__internal/ui/m_dialog.ts b/packages/devextreme/js/__internal/ui/dialog.ts similarity index 100% rename from packages/devextreme/js/__internal/ui/m_dialog.ts rename to packages/devextreme/js/__internal/ui/dialog.ts diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts index 83465ddca614..42d39abad884 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts @@ -47,6 +47,8 @@ const DROP_DOWN_EDITOR_OVERLAY_FLIPPED = 'dx-dropdowneditor-overlay-flipped'; const DROP_DOWN_EDITOR_ACTIVE = 'dx-dropdowneditor-active'; const DROP_DOWN_EDITOR_FIELD_CLICKABLE = 'dx-dropdowneditor-field-clickable'; const DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER = 'dx-dropdowneditor-field-template-wrapper'; +export const DROP_DOWN_EDITOR_BEFORE_FIELD_ADDON = 'dx-dropdowneditor-field-before-template'; +export const DROP_DOWN_EDITOR_AFTER_FIELD_ADDON = 'dx-dropdowneditor-field-after-template'; const OVERLAY_CONTENT_LABEL = 'Dropdown'; @@ -54,16 +56,52 @@ const isIOs = devices.current().platform === 'ios'; type HideOnOutsideClickEvent = DxEvent; -export interface DropDownEditorProperties extends Omit { +export const DROP_DOWN_EDITOR_DEPRECATED_OPTIONS = { + fieldTemplate: { + since: '25.2', + message: 'Use the \'fieldAddons\' option instead', + }, +}; + +export interface DropDownEditorProperties extends Omit< + Properties, + | 'onChange' + | 'onCopy' + | 'onCut' + | 'onEnterKey' + | 'onFocusIn' + | 'onFocusOut' + | 'onInput' + | 'onKeyDown' + | 'onKeyUp' + | 'onPaste' + | 'onValueChanged' + | 'validationMessagePosition' + | 'onContentReady' + | 'onDisposing' + | 'onOptionChanged' + | 'onInitialized' +> { buttonsLocation?: string; + fieldTemplate?: string | Element | Function | null; + _onMarkupRendered?: () => void; onPopupInitialized?: (e: { component: DropDownEditor; popup: Popup }) => void; } +interface TemplateRenderPayload { + model: Properties['value']; + container: Element; + onRendered?: () => void; +} + +interface FieldAddonsTemplates { + beforeTemplate?: { render: (payload: TemplateRenderPayload) => void }; + afterTemplate?: { render: (payload: TemplateRenderPayload) => void }; +} + function createTemplateWrapperElement(): dxElementWrapper { return $('
').addClass(DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER); } @@ -81,6 +119,10 @@ class DropDownEditor< _$templateWrapper?: dxElementWrapper; + _$beforeFieldAddon?: dxElementWrapper | null; + + _$afterFieldAddon?: dxElementWrapper | null; + _openAction!: (event?: Record) => void; _closeAction!: (event?: Record) => void; @@ -298,6 +340,7 @@ class DropDownEditor< _renderInput(): void { super._renderInput(); this._renderTemplateWrapper(); + this._renderFieldAddons(); this._wrapInput(); this._setDefaultAria(); @@ -339,18 +382,27 @@ class DropDownEditor< _cleanFocusState(): void { super._cleanFocusState(); + const { fieldTemplate } = this.option(); - if (this.option('fieldTemplate')) { + if (fieldTemplate) { this._detachFocusEvents(); } } _getFieldTemplate() { - return this.option('fieldTemplate') && this._getTemplateByOption('fieldTemplate'); + const { fieldTemplate } = this.option(); + + if (!fieldTemplate) { + return; + } + + return this._getTemplate(fieldTemplate); } _renderMask(): void { - if (this.option('fieldTemplate')) { + const { fieldTemplate } = this.option(); + + if (fieldTemplate) { return; } @@ -358,6 +410,14 @@ class DropDownEditor< } _renderField(): void { + const fieldAddonsTemplates = this._getFieldAddonsTemplates(); + + if (fieldAddonsTemplates) { + this._renderFieldAddonsContent(fieldAddonsTemplates); + + return; + } + const fieldTemplate = this._getFieldTemplate(); if (fieldTemplate) { @@ -388,6 +448,33 @@ class DropDownEditor< return fieldTemplate ? this._$container : this._$textEditorContainer; } + _renderBeforeFieldAddon(): void { + if (!this._$beforeFieldAddon) { + this._$beforeFieldAddon = $('
') + .addClass(DROP_DOWN_EDITOR_BEFORE_FIELD_ADDON) + .insertBefore(this._$textEditorContainer); + } + } + + _renderAfterFieldAddon(): void { + if (!this._$afterFieldAddon) { + this._$afterFieldAddon = $('
') + .addClass(DROP_DOWN_EDITOR_AFTER_FIELD_ADDON) + .insertAfter(this._$textEditorContainer); + } + } + + _renderFieldAddons(): void { + const { fieldAddons } = this.option(); + + if (!fieldAddons) { + return; + } + + this._renderBeforeFieldAddon(); + this._renderAfterFieldAddon(); + } + _renderTemplateWrapper(): void { const fieldTemplate = this._getFieldTemplate(); if (!fieldTemplate) { @@ -447,6 +534,65 @@ class DropDownEditor< }); } + _getFieldAddonsTemplates(): FieldAddonsTemplates | null { + const { fieldAddons } = this.option(); + + if (!fieldAddons) { + return null; + } + + const { beforeTemplate: before, afterTemplate: after } = fieldAddons; + + const beforeTemplate = before ? this._getTemplate(before) : null; + const afterTemplate = after ? this._getTemplate(after) : null; + + return { + beforeTemplate, + afterTemplate, + }; + } + + _clearFieldAddons(removeField?: boolean): void { + this._$beforeFieldAddon?.empty(); + this._$afterFieldAddon?.empty(); + + if (removeField) { + this._$beforeFieldAddon = null; + this._$afterFieldAddon = null; + } + } + + _renderBeforeFieldAddonContent(beforeTemplate?: FieldAddonsTemplates['beforeTemplate'] | null): void { + if (beforeTemplate && this._$beforeFieldAddon) { + beforeTemplate.render({ + model: this._fieldRenderData(), + container: getPublicElement(this._$beforeFieldAddon), + }); + } + } + + _renderAfterFieldAddonContent(afterTemplate?: FieldAddonsTemplates['afterTemplate'] | null): void { + if (afterTemplate && this._$afterFieldAddon) { + afterTemplate.render({ + model: this._fieldRenderData(), + container: getPublicElement(this._$afterFieldAddon), + }); + } + } + + _renderFieldAddonsContent(fieldAddonsTemplates: FieldAddonsTemplates): void { + this._clearFieldAddons(); + + if (!fieldAddonsTemplates) { + return; + } + + const { beforeTemplate, afterTemplate } = fieldAddonsTemplates; + + this._renderBeforeFieldAddonContent(beforeTemplate); + this._renderAfterFieldAddonContent(afterTemplate); + } + _integrateInput(): void { const { isValid } = this.option(); @@ -468,8 +614,9 @@ class DropDownEditor< this._renderEmptinessEvent(); } - _fieldRenderData(): any { - return this.option('value'); + _fieldRenderData(): Properties['value'] { + const { value } = this.option(); + return value; } _initTemplates(): void { @@ -858,11 +1005,14 @@ class DropDownEditor< delete this._openOnFieldClickAction; delete this._$templateWrapper; + this._clearFieldAddons(true); + if (this._$popup) { this._$popup.remove(); delete this._$popup; delete this._popup; } + super._clean(); } @@ -985,6 +1135,19 @@ class DropDownEditor< return super._getSubmitElement(); } + // eslint-disable-next-line class-methods-use-this + _shouldLogFieldTemplateDeprecationWarning(): boolean { + return false; + } + + _setDeprecatedOptions(): void { + super._setDeprecatedOptions(); + + if (this._shouldLogFieldTemplateDeprecationWarning()) { + extend(this._deprecatedOptions, DROP_DOWN_EDITOR_DEPRECATED_OPTIONS); + } + } + _dispose(): void { this._detachFocusOutEvents(); super._dispose(); @@ -1009,6 +1172,7 @@ class DropDownEditor< case 'onPopupInitialized': // for dashboards this._initPopupInitializedAction(); break; + case 'fieldAddons': case 'fieldTemplate': case 'acceptCustomValue': case 'openOnFieldClick': diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts index 10fff4a9a5ee..29986d3fdb47 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts @@ -658,7 +658,8 @@ class DropDownList< | DataSourceOptions> | null | undefined { - const { dataSource, grouped } = this.option(); + const { grouped } = this.option(); + const dataSource = this.option('dataSource'); if (dataSource && grouped) { return getDataSourceOptions(dataSource); diff --git a/packages/devextreme/js/__internal/ui/m_file_uploader.ts b/packages/devextreme/js/__internal/ui/file_uploader.ts similarity index 96% rename from packages/devextreme/js/__internal/ui/m_file_uploader.ts rename to packages/devextreme/js/__internal/ui/file_uploader.ts index 66a9100d1ff1..3f255e009c8e 100644 --- a/packages/devextreme/js/__internal/ui/m_file_uploader.ts +++ b/packages/devextreme/js/__internal/ui/file_uploader.ts @@ -234,7 +234,6 @@ class FileBlobReader { }; } - // eslint-disable-next-line class-methods-use-this sliceFile(file: File, startPos: number, length: number): Blob | null { if (file.slice) { return file.slice(startPos, startPos + length); @@ -303,7 +302,7 @@ class FileUploadStrategyBase { file.request = xhr; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + // eslint-disable-next-line @typescript-eslint/no-unused-vars _createUploadArgument(_file: FileUploaderItem): UploadChunkInfo { // This is an abstract method and should be implemented in subclasses. // Returning a default object to satisfy the return type. @@ -316,7 +315,7 @@ class FileUploadStrategyBase { }; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + // eslint-disable-next-line @typescript-eslint/no-unused-vars _uploadCore(_file: FileUploaderItem): void { } @@ -335,7 +334,6 @@ class FileUploadStrategyBase { this._handleProgressCore(file, e); } - // eslint-disable-next-line class-methods-use-this _handleProgressCore( // eslint-disable-next-line @typescript-eslint/no-unused-vars _file: FileUploaderItem, @@ -344,7 +342,6 @@ class FileUploadStrategyBase { ): void { } - // eslint-disable-next-line class-methods-use-this _handleFileError(file: FileUploaderItem, error: unknown): void { file._isError = true; file.onError.fire(error); @@ -372,7 +369,6 @@ class FileUploadStrategyBase { return (this._isStatusError(e.status) || !file._isProgressStarted) && !file.isAborted; } - // eslint-disable-next-line class-methods-use-this _isStatusError(status: number): boolean { return (status >= 400 && status < 500) || (status >= 500 && status < 600); } @@ -454,7 +450,6 @@ class FileUploadStrategyBase { } } - // eslint-disable-next-line class-methods-use-this _getLoadedData( loaded: number, total: number, @@ -544,7 +539,6 @@ class ChunksFileUploadStrategyBase extends FileUploadStrategyBase { } } - // eslint-disable-next-line class-methods-use-this _sendChunkCore( // eslint-disable-next-line @typescript-eslint/no-unused-vars _file: FileUploaderItem, @@ -558,7 +552,6 @@ class ChunksFileUploadStrategyBase extends FileUploadStrategyBase { return Deferred().reject(); } - // eslint-disable-next-line class-methods-use-this _tryRaiseStartLoad(file: FileUploaderItem): void { if (!file.isStartLoad) { file.isStartLoad = true; @@ -566,7 +559,7 @@ class ChunksFileUploadStrategyBase extends FileUploadStrategyBase { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + // eslint-disable-next-line @typescript-eslint/no-unused-vars _getEvent(_e: Event): null { return null; } @@ -575,7 +568,6 @@ class ChunksFileUploadStrategyBase extends FileUploadStrategyBase { return this._createChunksInfo(file.chunksData); } - // eslint-disable-next-line class-methods-use-this _createChunksInfo(chunksData?: FileUploaderChunksData): UploadChunkInfo { return { bytesUploaded: chunksData?.loadedBytes ?? 0, @@ -655,7 +647,7 @@ class CustomChunksFileUploadStrategy extends ChunksFileUploadStrategyBase { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + // eslint-disable-next-line @typescript-eslint/no-unused-vars _shouldHandleError(_file: FileUploaderItem, _error: unknown): boolean { return true; } @@ -681,14 +673,13 @@ class WholeFileUploadStrategyBase extends FileUploadStrategyBase { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + // eslint-disable-next-line @typescript-eslint/no-unused-vars _uploadFile(_file: FileUploaderItem): PromiseLike | DeferredObj { // Abstract method: subclasses should override this. // Return a rejected Deferred to satisfy the return type. return Deferred().reject(); } - // eslint-disable-next-line class-methods-use-this _handleProgressCore( file: FileUploaderItem, e: Event | { loaded?: number; total?: number }, @@ -760,7 +751,7 @@ class CustomWholeFileUploadStrategy extends WholeFileUploadStrategyBase { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + // eslint-disable-next-line @typescript-eslint/no-unused-vars _shouldHandleError(_file: FileUploaderItem, _e: unknown): boolean { return true; } @@ -1074,7 +1065,6 @@ class FileUploader extends Editor { this.option({ value: files?.concat(value) }); } - // eslint-disable-next-line class-methods-use-this _getFiles(fileList: FileList): File[] { return [...fileList]; } @@ -1218,7 +1208,6 @@ class FileUploader extends Editor { return minFileSize > 0 ? fileSize >= minFileSize : true; } - // eslint-disable-next-line class-methods-use-this _isFileExtensionAllowed(file: File, allowedExtensions: string[]): boolean { for (let i = 0, n = allowedExtensions.length; i < n; i += 1) { let allowedExtension = allowedExtensions[i]; @@ -1276,7 +1265,6 @@ class FileUploader extends Editor { this._dropZoneLeaveAction = this._createActionByOption('onDropZoneLeave'); } - // eslint-disable-next-line class-methods-use-this _createFile(value: File): FileUploaderItem { return { value, @@ -1298,7 +1286,6 @@ class FileUploader extends Editor { }; } - // eslint-disable-next-line class-methods-use-this _resetFileState(file: FileUploaderItem): void { file.isAborted = false; file.uploadStarted = false; @@ -1531,12 +1518,10 @@ class FileUploader extends Editor { ); } - // eslint-disable-next-line class-methods-use-this _hasInvalidFile(files: FileUploaderItem[]): boolean { return files.some((file) => !file.isValid()); } - // eslint-disable-next-line class-methods-use-this _getFileSize(size: number): string { const labels = [ messageLocalization.format('dxFileUploader-bytes'), @@ -1622,7 +1607,6 @@ class FileUploader extends Editor { ); } - // eslint-disable-next-line class-methods-use-this _detachSelectFileDialogHandlers(target: FileDialogEventTarget): void { if (!isDefined(target)) { return; @@ -1763,20 +1747,16 @@ class FileUploader extends Editor { _getDropZoneElement(isCustomTarget: boolean, e: DxEvent): Element | undefined { if (!e.currentTarget) { - return; + return undefined; } const { dropZone } = this.option(); - // @ts-expect-error dropZone option public type: it can be an array of Elements or NodeList - const targetList = isCustomTarget ? Array.from(dropZone) : [this._$inputWrapper]; - - const targetListElements = targetList.map( - (element: dxElementWrapper | string) => $(element).get(0), - ); + const targetList = isCustomTarget ? $(dropZone).toArray() : [this._$inputWrapper]; + const targetListElements = targetList.map((element) => $(element).get(0)); + const currentTargetIndex = targetListElements.indexOf(e.currentTarget); - // eslint-disable-next-line consistent-return - return targetListElements[targetListElements.indexOf(e.currentTarget)]; + return targetListElements[currentTargetIndex]; } // eslint-disable-next-line @typescript-eslint/no-invalid-void-type, consistent-return @@ -2007,12 +1987,10 @@ class FileUploader extends Editor { this._setLoadedSize(totalLoadedFilesSize); } - // eslint-disable-next-line class-methods-use-this _getProgressValue(ratio: number): number { return Math.floor(ratio * 100); } - // eslint-disable-next-line class-methods-use-this _initStatusMessage(file: FileUploaderItem): void { file.$statusMessage?.css('display', 'none'); } @@ -2120,7 +2098,6 @@ class FileUploader extends Editor { : (e as MouseEvent).clientY + this._getDocumentScrollTop(); } - // eslint-disable-next-line class-methods-use-this _getTouchEventX(e: TouchEvent): number { let touchPoint: TouchList | null = null; if (e.changedTouches.length > 0) { @@ -2131,7 +2108,6 @@ class FileUploader extends Editor { return touchPoint ? touchPoint[0].pageX : 0; } - // eslint-disable-next-line class-methods-use-this _getTouchEventY(e: TouchEvent): number { let touchPoint: TouchList | null = null; if (e.changedTouches.length > 0) { @@ -2142,13 +2118,11 @@ class FileUploader extends Editor { return touchPoint ? touchPoint[0].pageY : 0; } - // eslint-disable-next-line class-methods-use-this _getDocumentScrollTop(): number { const document = domAdapter.getDocument(); return document.documentElement.scrollTop || document.body.scrollTop; } - // eslint-disable-next-line class-methods-use-this _getDocumentScrollLeft(): number { const document = domAdapter.getDocument(); return document.documentElement.scrollLeft || document.body.scrollLeft; diff --git a/packages/devextreme/js/__internal/ui/form/components/button_item.ts b/packages/devextreme/js/__internal/ui/form/components/button_item.ts index 443cf2d201c4..a8f25c0ab6c1 100644 --- a/packages/devextreme/js/__internal/ui/form/components/button_item.ts +++ b/packages/devextreme/js/__internal/ui/form/components/button_item.ts @@ -7,7 +7,7 @@ import type { HorizontalAlignment, VerticalAlignment } from '@js/ui/form'; import type Button from '@ts/ui/button/wrapper'; import type { TemplatesInfo } from '@ts/ui/form/form.layout_manager'; -const FIELD_BUTTON_ITEM_CLASS = 'dx-field-button-item'; +export const FIELD_BUTTON_ITEM_CLASS = 'dx-field-button-item'; type ButtonItemRenderInfo = TemplatesInfo & { validationGroup?: string; diff --git a/packages/devextreme/js/__internal/ui/form/constants.ts b/packages/devextreme/js/__internal/ui/form/constants.ts index fd11fb636f8a..53ada169d8c6 100644 --- a/packages/devextreme/js/__internal/ui/form/constants.ts +++ b/packages/devextreme/js/__internal/ui/form/constants.ts @@ -21,5 +21,7 @@ export const GROUP_COL_COUNT_CLASS = 'dx-group-colcount-'; export const GROUP_COL_COUNT_ATTR = 'group-col-count'; export const FORM_VALIDATION_SUMMARY = 'dx-form-validation-summary'; export const FORM_UNDERLINED_CLASS = 'dx-form-styling-mode-underlined'; +export const FORM_LOAD_PANEL_CLASS = 'dx-form-loadpanel'; +export const FORM_LOAD_PANEL_WRAPPER_CLASS = 'dx-form-loadpanel-wrapper'; export const SIMPLE_ITEM_TYPE = 'simple'; diff --git a/packages/devextreme/js/__internal/ui/form/form.ai.utils.ts b/packages/devextreme/js/__internal/ui/form/form.ai.utils.ts new file mode 100644 index 000000000000..151fa29e4e39 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/form/form.ai.utils.ts @@ -0,0 +1,119 @@ +import Color from '@js/color'; +import type { SmartPasteCommandResult } from '@js/common/ai-integration'; +import { isObject } from '@js/core/utils/type'; +import type { FormItemComponent, SimpleItem } from '@js/ui/form'; +import errors from '@js/ui/widget/ui.errors'; +import { dateUtilsTs } from '@ts/core/utils/date'; + +export type ParsedAIValue = string | string[] | boolean; + +const getEditorTypeInfo = (editorType: FormItemComponent | undefined): string => { + switch (editorType) { + case 'dxDateBox': + case 'dxCalendar': + return 'date in ISO format'; + case 'dxDateRangeBox': + return 'date range in ISO format, use pattern {start}:::{end}'; + case 'dxColorBox': + return 'color in hex format'; + case 'dxCheckBox': + case 'dxSwitch': + return 'boolean value, true or false'; + case 'dxNumberBox': + case 'dxSlider': + return 'numeric value'; + case 'dxRangeSlider': + return 'numeric range, use pattern {start}:::{end}'; + default: + return 'text'; + } +}; + +export const parseResultForEditorType = ( + dataField: string, + editorType: FormItemComponent | undefined, + value: SmartPasteCommandResult[number]['value'], +): ParsedAIValue => { + const errorValue = JSON.stringify(value); + switch (editorType) { + case 'dxDateBox': + case 'dxCalendar': + if (!dateUtilsTs.isValidDate(value)) { + throw errors.Error('E1064', dataField, errorValue, 'date'); + } + + return value; + case 'dxDateRangeBox': + if ( + !Array.isArray(value) + || value.length > 2 + || value.some((item) => !dateUtilsTs.isValidDate(item)) + ) { + throw errors.Error('E1064', dataField, errorValue, 'date range'); + } + + return value; + case 'dxColorBox': + if (new Color(value).colorIsInvalid) { + throw errors.Error('E1064', dataField, errorValue, 'color'); + } + return value; + case 'dxCheckBox': + case 'dxSwitch': + if (value === 'false') { + return false; + } + if (value === 'true') { + return true; + } + throw errors.Error('E1064', dataField, errorValue, 'boolean'); + case 'dxNumberBox': + case 'dxSlider': + if (Array.isArray(value) || isNaN(parseFloat(value))) { + throw errors.Error('E1064', dataField, errorValue, 'number'); + } + return value; + case 'dxRangeSlider': + if ( + !Array.isArray(value) + || value.length > 2 + || value.some((item) => isNaN(parseFloat(item))) + ) { + throw errors.Error('E1064', dataField, errorValue, 'number range'); + } + return value; + case 'dxHtmlEditor': + if (Array.isArray(value)) { + throw errors.Error('E1064', dataField, errorValue, 'string'); + } + return value; + default: + return value; + } +}; + +const getItemsAcceptedValuesInfo = (editorOptions: SimpleItem['editorOptions']): string => { + if (!editorOptions?.items) { + return ''; + } + + const items = editorOptions.items.map((item: { text?: string } | string) => { + if (isObject(item)) { + return item.text; + } + + return item; + }); + + const acceptedValues = `, accepted values: ${items.join(', ')}, split values with :::`; + const customItemsAllowed = editorOptions?.acceptCustomValue ? ' (custom values are allowed)' : ''; + + return `${acceptedValues}${customItemsAllowed}`; +}; + +export const getItemFormatInfo = ({ editorType, editorOptions }: SimpleItem): string => { + const dataType = getEditorTypeInfo(editorType); + const acceptedValues = getItemsAcceptedValuesInfo(editorOptions); + + return `${dataType}${acceptedValues}`; +}; diff --git a/packages/devextreme/js/__internal/ui/form/form.item_option_action.ts b/packages/devextreme/js/__internal/ui/form/form.item_option_action.ts index 422f59ec44c3..3feed0912429 100644 --- a/packages/devextreme/js/__internal/ui/form/form.item_option_action.ts +++ b/packages/devextreme/js/__internal/ui/form/form.item_option_action.ts @@ -14,7 +14,6 @@ export interface ItemOptionActionOptions { optionName?: string; value?: unknown; previousValue?: unknown; - } export interface ValidationRulesItemOptionActionOption extends ItemOptionActionOptions { diff --git a/packages/devextreme/js/__internal/ui/form/form.items_runtime_info.ts b/packages/devextreme/js/__internal/ui/form/form.items_runtime_info.ts index 648ebac1ecb6..c0089467ce50 100644 --- a/packages/devextreme/js/__internal/ui/form/form.items_runtime_info.ts +++ b/packages/devextreme/js/__internal/ui/form/form.items_runtime_info.ts @@ -23,6 +23,7 @@ export type PreparedItem = T & { export type TabItem = NonNullable[number]; export type PreparedTabItem = PreparedItem; +export type SimpleItemWithDataField = SimpleItem & Required>; export interface PreparedGroupedItem extends PreparedItem { _prepareGroupCaptionTemplate?: (captionTemplate?: template | (( @@ -212,4 +213,34 @@ export default class FormItemsRunTimeInfo { }); filteredKeys.forEach((key) => this.removeItemByKey(key)); } + + _isEditableItem(item: SimpleItem): boolean { + const { visible: itemVisible, editorOptions } = item; + const { readOnly, disabled, visible } = editorOptions ?? {}; + + return itemVisible !== false && !readOnly && !disabled && visible !== false; + } + + _isItemAIEnabled(item: SimpleItem): boolean { + return !item.aiOptions?.disabled; + } + + _isDataItem(item: PreparedItem): item is SimpleItemWithDataField { + return 'dataField' in item; + } + + getVisibleItems(): FormItemRuntimeInfo[] { + const allItems = Object.values(this._map); + + return allItems.filter(({ $itemContainer }) => $itemContainer?.css('visibility') === 'visible'); + } + + getItemsForDataExtraction(): SimpleItemWithDataField[] { + const visibleItems = this.getVisibleItems().map(({ item }) => item); + + return visibleItems + .filter(this._isDataItem) + .filter(this._isItemAIEnabled) + .filter(this._isEditableItem); + } } diff --git a/packages/devextreme/js/__internal/ui/form/form.layout_manager.ts b/packages/devextreme/js/__internal/ui/form/form.layout_manager.ts index 40cc4a55bf3b..a1bfa4b955b1 100644 --- a/packages/devextreme/js/__internal/ui/form/form.layout_manager.ts +++ b/packages/devextreme/js/__internal/ui/form/form.layout_manager.ts @@ -23,6 +23,7 @@ import { getCurrentScreenFactor, hasWindow } from '@js/core/utils/window'; import type { EventInfo } from '@js/events'; import type { Properties as ButtonProperties, Properties } from '@js/ui/button'; import type { + ButtonItem, FieldDataChangedEvent, FormItemComponent, Item, SimpleItem, TabbedItem, } from '@js/ui/form'; @@ -684,9 +685,64 @@ class LayoutManager extends Widget { return result; } + private _handleSmartPasteClick(): void { + const form = this._getFormOrThis(); + // @ts-expect-error + form?.smartPaste(); + } + + private _handleResetClick(): void { + const form = this._getFormOrThis(); + // @ts-expect-error + form?.reset(); + } + + private _configureDefaultButton(item: ButtonItem): void { + if (!item.name) { + return; + } + + const buttonConfigs = { + smartPaste: { + icon: 'clipboardpastesparkle', + text: messageLocalization.format('dxForm-smartPasteButtonText'), + stylingMode: 'outlined', + type: 'normal', + onClick: (): void => { + this._handleSmartPasteClick(); + }, + }, + reset: { + text: messageLocalization.format('dxForm-resetButtonText'), + stylingMode: 'outlined', + type: 'normal', + onClick: (): void => { + this._handleResetClick(); + }, + }, + submit: { + text: messageLocalization.format('dxForm-submitButtonText'), + stylingMode: 'contained', + type: 'default', + useSubmitBehavior: true, + }, + }; + + const config = buttonConfigs[item.name]; + if (config) { + item.buttonOptions = { + ...config, + ...item.buttonOptions ?? {}, + }; + } + } + _renderButtonItem(info: TemplatesInfo): void { const { item, $parent, rootElementCssClassList } = info; const { validationGroup } = this.option(); + + this._configureDefaultButton(item); + const { $rootElement, buttonInstance } = renderButtonItem({ item, $parent, @@ -828,6 +884,7 @@ class LayoutManager extends Widget { _getFormOrThis(): Form | LayoutManager { const { form } = this.option(); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return form || this; } diff --git a/packages/devextreme/js/__internal/ui/form/form.load_panel.ts b/packages/devextreme/js/__internal/ui/form/form.load_panel.ts new file mode 100644 index 000000000000..949e0efa0e6b --- /dev/null +++ b/packages/devextreme/js/__internal/ui/form/form.load_panel.ts @@ -0,0 +1,99 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { + FORM_LOAD_PANEL_CLASS, + FORM_LOAD_PANEL_WRAPPER_CLASS, +} from '@ts/ui/form/constants'; +import LoadIndicator, { AnimationType } from '@ts/ui/load_indicator'; +import type { LoadPanelProperties } from '@ts/ui/load_panel'; +import type LoadPanel from '@ts/ui/load_panel'; + +const FORM_LOAD_INDICATOR_SIZE = 120; + +interface FormLoadPanelDependencies { + $container: dxElementWrapper; + onLoadPanelCreate: ($element: dxElementWrapper, options: LoadPanelProperties) => LoadPanel; +} + +export class FormLoadPanel { + private readonly _dependencies: FormLoadPanelDependencies; + + private _loadPanel?: LoadPanel; + + constructor(dependencies: FormLoadPanelDependencies) { + this._dependencies = dependencies; + } + + show(): void { + this._ensureLoadPanel(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._loadPanel?.show(); + } + + hide(): void { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._loadPanel?.hide(); + } + + dispose(): void { + if (!this._loadPanel) { + return; + } + + this._loadPanel.dispose(); + this._loadPanel.$element().remove(); + this._loadPanel = undefined; + } + + get instance(): LoadPanel | undefined { + return this._loadPanel; + } + + option(name: string): unknown { + return this._loadPanel?.option(name); + } + + private _ensureLoadPanel(): void { + if (this._loadPanel) { + return; + } + + const $loadPanel = $('
') + .addClass(FORM_LOAD_PANEL_CLASS) + .appendTo(this._dependencies.$container); + + this._loadPanel = this._dependencies.onLoadPanelCreate($loadPanel, { + position: { + of: this._dependencies.$container.get(0), + }, + visible: false, + showIndicator: true, + showPane: false, + shading: false, + hideOnOutsideClick: false, + hideOnParentScroll: false, + deferRendering: false, + disabled: false, + message: '', + wrapperAttr: { + class: FORM_LOAD_PANEL_WRAPPER_CLASS, + }, + }); + + this._configureLoadIndicator(); + } + + private _configureLoadIndicator(): void { + const $loadIndicator = this._loadPanel?._$indicator; + + if ($loadIndicator?.length) { + const loadIndicator = LoadIndicator.getInstance($loadIndicator.get(0)); + + loadIndicator.option({ + animationType: AnimationType.Sparkle, + width: FORM_LOAD_INDICATOR_SIZE, + height: FORM_LOAD_INDICATOR_SIZE, + }); + } + } +} diff --git a/packages/devextreme/js/__internal/ui/form/form.ts b/packages/devextreme/js/__internal/ui/form/form.ts index 9c06c4bdffb1..c3ff40f2c043 100644 --- a/packages/devextreme/js/__internal/ui/form/form.ts +++ b/packages/devextreme/js/__internal/ui/form/form.ts @@ -2,6 +2,7 @@ import '@js/ui/validation_summary'; import '@js/ui/validation_group'; import type { EditorStyle } from '@js/common'; +import type { RequestCallbacks, SmartPasteCommandParams, SmartPasteCommandResult } from '@js/common/ai-integration'; import eventsEngine from '@js/common/core/events/core/events_engine'; import { triggerResizeEvent, triggerShownEvent } from '@js/common/core/events/visibility_change'; import messageLocalization from '@js/common/core/localization/message'; @@ -26,10 +27,21 @@ import type { ChangedOptionInfo, EventInfo } from '@js/events'; import type { FieldDataChangedEvent, FormItemType, - GroupItem, Item, LabelLocation, Properties, SimpleItemTemplateData, TabbedItem, + GroupItem, + Item, + LabelLocation, + Properties, + SimpleItem, + SimpleItemTemplateData, + SmartPastedEvent, + SmartPastingEvent, + TabbedItem, } from '@js/ui/form'; import { current, isMaterial, isMaterialBased } from '@js/ui/themes'; import type { ValidationResult } from '@js/ui/validation_group'; +import errors from '@js/ui/widget/ui.errors'; +import { invokeConditionally } from '@ts/core/utils/conditional_invoke'; +import { logger } from '@ts/core/utils/m_console'; import type { Component } from '@ts/core/widget/component'; import type { OptionChanged } from '@ts/core/widget/types'; import type { SupportedKeyHandler } from '@ts/core/widget/widget'; @@ -37,9 +49,7 @@ import Widget, { FOCUSED_STATE_CLASS } from '@ts/core/widget/widget'; import type { Button } from '@ts/ui/button/button'; import { DROP_DOWN_EDITOR_CLASS } from '@ts/ui/drop_down_editor/m_drop_down_editor'; import Editor from '@ts/ui/editor/editor'; -import { - setLabelWidthByMaxLabelWidth, -} from '@ts/ui/form/components/label'; +import { setLabelWidthByMaxLabelWidth } from '@ts/ui/form/components/label'; import { FIELD_ITEM_CLASS, FIELD_ITEM_CONTENT_CLASS, @@ -58,6 +68,11 @@ import { GROUP_COL_COUNT_CLASS, ROOT_SIMPLE_ITEM_CLASS, } from '@ts/ui/form/constants'; +import { + getItemFormatInfo, + type ParsedAIValue, + parseResultForEditorType, +} from '@ts/ui/form/form.ai.utils'; import type { ItemOptionActionType } from '@ts/ui/form/form.item_options_actions'; import tryCreateItemOptionAction from '@ts/ui/form/form.item_options_actions'; import type { @@ -70,6 +85,7 @@ import type { import FormItemsRunTimeInfo from '@ts/ui/form/form.items_runtime_info'; import type { ExtendedLayoutManagerProperties, LayoutManagerProperties } from '@ts/ui/form/form.layout_manager'; import LayoutManager from '@ts/ui/form/form.layout_manager'; +import { FormLoadPanel } from '@ts/ui/form/form.load_panel'; import { concatPaths, convertToLayoutManagerOptions, @@ -82,6 +98,8 @@ import { isFullPathContainsTabs, tryGetTabPath, } from '@ts/ui/form/form.utils'; +import type { LoadPanelProperties } from '@ts/ui/load_panel'; +import LoadPanel from '@ts/ui/load_panel'; import ValidationEngine from '@ts/ui/m_validation_engine'; import ValidationSummary from '@ts/ui/m_validation_summary'; import type { ScreenSizeQualifier } from '@ts/ui/responsive_box'; @@ -91,6 +109,21 @@ import TabPanel from '@ts/ui/tab_panel/tab_panel'; import { TEXTEDITOR_CLASS, TEXTEDITOR_INPUT_CLASS } from '@ts/ui/text_box/m_text_editor.base'; import { TOOLBAR_CLASS } from '@ts/ui/toolbar/constants'; +export type FormAICommandName = 'smartPaste'; +export interface AICommandParamsMap { + smartPaste: SmartPasteCommandParams; +} + +export interface AICommandResultMap { + smartPaste: SmartPasteCommandResult; +} + +interface AICommandWithParams { + command: T; + params: AICommandParamsMap[T]; + callbacks: RequestCallbacks; +} + const ITEM_OPTIONS_FOR_VALIDATION_UPDATING = ['items', 'isRequired', 'validationRules', 'visible']; export interface FormProperties extends Properties { @@ -104,6 +137,10 @@ export interface FormProperties extends Properties { } class Form extends Widget { + private _abort?: () => void; + + private _currentAICommand?: AICommandWithParams = undefined; + _targetScreenFactor?: ScreenSizeQualifier; _lastMarkupScreenFactor!: ScreenSizeQualifier; @@ -133,6 +170,12 @@ class Form extends Widget { _$validationSummary?: dxElementWrapper; + _loadPanel?: FormLoadPanel; + + _smartPastingAction?: (e: Partial) => void; + + _smartPastedAction?: (e: Partial) => void; + _init(): void { super._init(); @@ -142,6 +185,8 @@ class Form extends Widget { this._groupsColCount = []; this._attachSyncSubscriptions(); + this._createSmartPastingAction(); + this._createSmartPastedAction(); } _getDefaultOptions(): FormProperties { @@ -175,6 +220,10 @@ class Form extends Widget { stylingMode: config().editorStylingMode, labelMode: 'outside', isDirty: false, + // @ts-expect-error ts-error + onSmartPasting: null, + // @ts-expect-error ts-error + onSmartPasted: null, }; } @@ -1023,6 +1072,20 @@ class Form extends Widget { }); } + _createSmartPastingAction(): void { + this._smartPastingAction = this._createActionByOption( + 'onSmartPasting', + { excludeValidators: ['disabled'] }, + ); + } + + _createSmartPastedAction(): void { + this._smartPastedAction = this._createActionByOption( + 'onSmartPasted', + { excludeValidators: ['disabled'] }, + ); + } + _optionChanged(args: OptionChanged): void { const { fullName } = args; const splitFullName = fullName.split('.'); @@ -1100,6 +1163,15 @@ class Form extends Widget { ValidationEngine.removeGroup(args.previousValue || this); this._invalidate(); break; + case 'aiIntegration': + this._processAIIntegrationUpdate(); + break; + case 'onSmartPasting': + this._createSmartPastingAction(); + break; + case 'onSmartPasted': + this._createSmartPastedAction(); + break; default: super._optionChanged(args); } @@ -1625,8 +1697,33 @@ class Form extends Widget { } } + private _ensureLoadPanel(): void { + if (!this._loadPanel) { + this._loadPanel = new FormLoadPanel({ + $container: this.$element(), + onLoadPanelCreate: ( + $element: dxElementWrapper, + options: LoadPanelProperties, + ): LoadPanel => this._createComponent($element, LoadPanel, options), + }); + } + } + + private _showLoadPanel(): void { + this._ensureLoadPanel(); + this.option('disabled', true); + this._loadPanel?.show(); + } + + private _hideLoadPanel(): void { + this._loadPanel?.hide(); + this.option('disabled', false); + } + _dispose(): void { this._clearAutoColCountChangedTimeout(); + this._processCommandCompletion(); + this._loadPanel?.dispose(); ValidationEngine.removeGroup(this._getValidationGroup()); super._dispose(); } @@ -1636,7 +1733,7 @@ class Form extends Widget { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - reset(editorsData: Record): void { + reset(editorsData?: Record): void { this.updateRunTimeInfoForEachEditor((editor) => { const { name = '' } = editor.option(); if (editorsData && name in editorsData) { @@ -1760,6 +1857,143 @@ class Form extends Widget { getTargetScreenFactor(): ScreenSizeQualifier | undefined { return this._targetScreenFactor; } + + private _processCommandCompletion(): void { + this._abort?.(); + this._abort = undefined; + this._currentAICommand = undefined; + } + + private _processAIIntegrationUpdate(): void { + if (this._currentAICommand) { + const { command, params, callbacks } = this._currentAICommand; + const { aiIntegration } = this.option(); + + this._processCommandCompletion(); + + if (!aiIntegration) { + this._hideLoadPanel(); + throw errors.Error('E1063'); + } else { + this._executeAICommand(command, params, callbacks); + } + } + } + + private _executeAICommand( + command: T, + params: AICommandParamsMap[T], + callbacks: RequestCallbacks, + ): void { + const { aiIntegration } = this.option(); + + if (!aiIntegration) { + this._hideLoadPanel(); + throw errors.Error('E1063'); + } + + this._currentAICommand = { + command, + params, + callbacks, + }; + this._abort = aiIntegration[command](params, callbacks); + } + + private _updateFieldWithSmartPasteValue(dataField: string, value: SmartPasteCommandResult[number]['value'], item?: SimpleItem): void { + const { formData } = this.option(); + + if (isDefined(formData)) { + let resultValue: ParsedAIValue = value; + + resultValue = parseResultForEditorType(dataField, item?.editorType, value); + if (typeof resultValue !== undefined) { + this._updateFieldValue(dataField, resultValue); + } + } + } + + private _getEditorItemsMap(): Record { + const dataItems = this._itemsRunTimeInfo.getItemsForDataExtraction(); + return dataItems.reduce((itemsMap, item) => { + itemsMap[item.dataField] = item; + return itemsMap; + }, {}); + } + + private _getSmartPasteCommandCallbacks(): RequestCallbacks { + return { + onComplete: (fieldsData: SmartPasteCommandResult): void => { + const aiResult = Object.fromEntries(fieldsData.map((field) => [field.name, field.value])); + + const smartPastingArgs: Pick = { + aiResult, + cancel: false, + }; + this._smartPastingAction?.(smartPastingArgs); + + invokeConditionally( + smartPastingArgs.cancel, + (): void => { + this._hideLoadPanel(); + this.beginUpdate(); + + const editorTypesMap = this._getEditorItemsMap(); + fieldsData.forEach(({ name, value }: SmartPasteCommandResult[number]) => { + try { + const currentItem = editorTypesMap[name]; + this._updateFieldWithSmartPasteValue(name, value, currentItem); + } catch (error) { + logger.error(error); + } + }); + this.endUpdate(); + + this._smartPastedAction?.({ aiResult }); + }, + ); + + this._processCommandCompletion(); + }, + onError: (error): void => { + logger.error(error); + this._hideLoadPanel(); + this._processCommandCompletion(); + }, + }; + } + + async smartPaste(text?: string): Promise { + if (this._currentAICommand?.command === 'smartPaste') { + this._processCommandCompletion(); + } + + const { aiIntegration } = this.option(); + if (!aiIntegration) { + throw errors.Error('E1063'); + } + + const smartPasteText = text ?? await navigator.clipboard.readText(); + + if (isDefined(smartPasteText)) { + this._showLoadPanel(); + } + + const dataItems = this._itemsRunTimeInfo.getItemsForDataExtraction(); + const fields = dataItems.map((item) => ({ + name: item.dataField, + format: getItemFormatInfo(item), + instruction: item.aiOptions?.instruction, + })); + + const smartPasteParams = { + text: smartPasteText, + fields, + }; + const smartPasteCallbacks = this._getSmartPasteCommandCallbacks(); + + this._executeAICommand('smartPaste', smartPasteParams, smartPasteCallbacks); + } } registerComponent('dxForm', Form); diff --git a/packages/devextreme/js/__internal/ui/gallery.ts b/packages/devextreme/js/__internal/ui/gallery.ts index 1404ac73ac4b..05274f29070e 100644 --- a/packages/devextreme/js/__internal/ui/gallery.ts +++ b/packages/devextreme/js/__internal/ui/gallery.ts @@ -150,6 +150,10 @@ class Gallery extends CollectionWidget { // eslint-disable-next-line @typescript-eslint/no-explicit-any _selectionChangeEventInstance?: any; + protected _feedbackShowTimeout(): number { + return LIST_FEEDBACK_SHOW_TIMEOUT; + } + _supportedKeys(): SupportedKeys { return { ...super._supportedKeys(), @@ -495,7 +499,7 @@ export class ListBase extends CollectionWidget { return true; } - _updateActiveStateUnit(): void { + protected _activeStateUnit(): string { const { collapsibleGroups } = this.option(); const selectors = [ @@ -507,20 +511,17 @@ export class ListBase extends CollectionWidget { selectors.push(`.${LIST_GROUP_HEADER_CLASS}`); } - this._activeStateUnit = selectors.join(','); + return selectors.join(','); } _init(): void { super._init(); - this._updateActiveStateUnit(); this._dataController.resetDataSourcePageIndex(); this._$container = this.$element(); this._$listContainer = $('
').addClass(LIST_ITEMS_CLASS); this._initScrollView(); - // @ts-expect-error ts-error - this._feedbackShowTimeout = LIST_FEEDBACK_SHOW_TIMEOUT; this._createGroupRenderAction(); } @@ -550,7 +551,8 @@ export class ListBase extends CollectionWidget { | DataSourceOptions> | null | undefined { - const { dataSource, grouped } = this.option(); + const { grouped } = this.option(); + const dataSource = this.option('dataSource'); if (dataSource && grouped) { return getDataSourceOptions(dataSource); @@ -1363,7 +1365,6 @@ export class ListBase extends CollectionWidget { this._invalidate(); break; case 'collapsibleGroups': - this._updateActiveStateUnit(); this._invalidate(); break; case 'wrapItemText': diff --git a/packages/devextreme/js/__internal/ui/m_load_panel.ts b/packages/devextreme/js/__internal/ui/load_panel.ts similarity index 85% rename from packages/devextreme/js/__internal/ui/m_load_panel.ts rename to packages/devextreme/js/__internal/ui/load_panel.ts index ac3a84f0d09f..21eee9bfb1f1 100644 --- a/packages/devextreme/js/__internal/ui/m_load_panel.ts +++ b/packages/devextreme/js/__internal/ui/load_panel.ts @@ -4,10 +4,11 @@ import type { DefaultOptionsRule } from '@js/core/options/utils'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { noop } from '@js/core/utils/common'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; import LoadIndicator from '@js/ui/load_indicator'; import type { Properties } from '@js/ui/load_panel'; -import { isFluent, isMaterial } from '@js/ui/themes'; +import { current, isFluent, isMaterial } from '@js/ui/themes'; import type { OptionChanged } from '@ts/core/widget/types'; import type { SupportedKeys } from '@ts/core/widget/widget'; import Overlay from '@ts/ui/overlay/overlay'; @@ -29,6 +30,7 @@ class LoadPanel extends Overlay { _$loadPanelContentWrapper?: dxElementWrapper; + // eslint-disable-next-line no-restricted-globals -- needed for delayed panel show _showTimeout?: ReturnType; _supportedKeys(): SupportedKeys { @@ -44,7 +46,7 @@ class LoadPanel extends Overlay { message: messageLocalization.format('Loading'), width: 222, height: 90, - // @ts-expect-error ts-error + // @ts-expect-error 'null' is not assignable animation: null, showIndicator: true, indicatorSrc: '', @@ -68,8 +70,7 @@ class LoadPanel extends Overlay { }, { device(): boolean { - // @ts-expect-error ts-error - return isMaterial(); + return isMaterial(current()); }, options: { message: '', @@ -81,8 +82,7 @@ class LoadPanel extends Overlay { }, { device(): boolean { - // @ts-expect-error ts-error - return isFluent(); + return isFluent(current()); }, options: { width: 'auto', @@ -93,8 +93,7 @@ class LoadPanel extends Overlay { } _init(): void { - // @ts-expect-error ts-error - super._init.apply(this, arguments); + super._init(); } _render(): void { @@ -113,13 +112,15 @@ class LoadPanel extends Overlay { const showIndicator = this.option('showIndicator'); if (!showIndicator) { const aria = this._getAriaAttributes(); - // @ts-expect-error ts-error + + // @ts-expect-error attr should have overload this.$wrapper().attr(aria); } } - _getAriaAttributes() { + _getAriaAttributes(): Record { const { message } = this.option(); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const label = message || messageLocalization.format('Loading'); @@ -131,9 +132,8 @@ class LoadPanel extends Overlay { return aria; } - // @ts-expect-error ts-error - _renderContentImpl(): void { - super._renderContentImpl(); + _renderContentImpl(): Promise { + const result = super._renderContentImpl(); this.$content().addClass(LOADPANEL_CONTENT_CLASS); @@ -145,9 +145,11 @@ class LoadPanel extends Overlay { this._cleanPreviousContent(); this._renderLoadIndicator(); this._renderMessage(); + + return result; } - _show() { + _show(): DeferredObj | Promise { const { delay } = this.option(); if (!delay) { @@ -158,8 +160,10 @@ class LoadPanel extends Overlay { const callBase = super._show.bind(this); this._clearShowTimeout(); + + // eslint-disable-next-line no-restricted-globals -- needed for delayed panel show this._showTimeout = setTimeout(() => { - // @ts-expect-error ts-error + // @ts-expect-error done should be typed callBase().done(() => { deferred.resolve(); }); @@ -168,8 +172,9 @@ class LoadPanel extends Overlay { return deferred.promise(); } - _hide() { + _hide(): DeferredObj | Promise { this._clearShowTimeout(); + return super._hide(); } @@ -184,9 +189,12 @@ class LoadPanel extends Overlay { const { message } = this.option(); - if (!message) return; + if (!message) { + return; + } - const $message = $('
').addClass(LOADPANEL_MESSAGE_CLASS) + const $message = $('
') + .addClass(LOADPANEL_MESSAGE_CLASS) .text(message); this._$loadPanelContentWrapper.append($message); @@ -212,7 +220,8 @@ class LoadPanel extends Overlay { _cleanPreviousContent(): void { this.$content().find(`.${LOADPANEL_MESSAGE_CLASS}`).remove(); this.$content().find(`.${LOADPANEL_INDICATOR_CLASS}`).remove(); - delete this._$indicator; + + this._$indicator = undefined; } _togglePaneVisible(): void { diff --git a/packages/devextreme/js/__internal/ui/m_drop_down_box.ts b/packages/devextreme/js/__internal/ui/m_drop_down_box.ts index a6386a34a722..f966109aa09f 100644 --- a/packages/devextreme/js/__internal/ui/m_drop_down_box.ts +++ b/packages/devextreme/js/__internal/ui/m_drop_down_box.ts @@ -19,7 +19,7 @@ import DataExpressionMixin from '@js/ui/editor/ui.data_expression'; import type { Properties as PopupProperties } from '@js/ui/popup'; import { tabbable } from '@ts/core/utils/m_selectors'; import DropDownEditor from '@ts/ui/drop_down_editor/m_drop_down_editor'; -import { getElementMaxHeightByWindow } from '@ts/ui/overlay/m_utils'; +import { getElementMaxHeightByWindow } from '@ts/ui/overlay/utils'; const { getActiveElement } = domAdapter; @@ -311,6 +311,11 @@ class DropDownBox< // eslint-disable-next-line class-methods-use-this _setCollectionWidgetOption(): void {} + // eslint-disable-next-line class-methods-use-this + _shouldLogFieldTemplateDeprecationWarning(): boolean { + return true; + } + _optionChanged(args) { // @ts-expect-error ts-error this._dataExpressionOptionChanged(args); diff --git a/packages/devextreme/js/__internal/ui/m_lookup.ts b/packages/devextreme/js/__internal/ui/m_lookup.ts index 8bafbc2d973f..76759eb59d62 100644 --- a/packages/devextreme/js/__internal/ui/m_lookup.ts +++ b/packages/devextreme/js/__internal/ui/m_lookup.ts @@ -148,7 +148,6 @@ class Lookup extends DropDownList { fullScreen: false, }, dropDownCentered: false, - _scrollToSelectedItemEnabled: false, useHiddenSubmitElement: true, }; @@ -322,9 +321,11 @@ class Lookup extends DropDownList { } _renderField() { + const { fieldTemplate: fieldTemplateOption } = this.option(); + const fieldTemplate = this._getTemplateByOption('fieldTemplate'); - if (fieldTemplate && this.option('fieldTemplate')) { + if (fieldTemplate && fieldTemplateOption) { this._renderFieldTemplate(fieldTemplate); return; } diff --git a/packages/devextreme/js/__internal/ui/m_notify.ts b/packages/devextreme/js/__internal/ui/m_notify.ts deleted file mode 100644 index 6ff1229898cf..000000000000 --- a/packages/devextreme/js/__internal/ui/m_notify.ts +++ /dev/null @@ -1,166 +0,0 @@ -import $ from '@js/core/renderer'; -import { extend } from '@js/core/utils/extend'; -import { isPlainObject, isString } from '@js/core/utils/type'; -import { value as viewPort } from '@js/core/utils/view_port'; -import { getWindow } from '@js/core/utils/window'; -import Toast from '@js/ui/toast'; - -const window = getWindow(); -let $notify = null; -const $containers = {}; - -function notify(message, /* optional */ typeOrStack, displayTime) { - const options = isPlainObject(message) ? message : { message }; - const stack = isPlainObject(typeOrStack) ? typeOrStack : undefined; - const type = isPlainObject(typeOrStack) ? undefined : typeOrStack; - const { onHidden: userOnHidden } = options; - - if (stack?.position) { - const { position } = stack; - const direction = stack.direction || getDefaultDirection(position); - const containerKey = isString(position) - ? position - : `${position.top}-${position.left}-${position.bottom}-${position.right}`; - - const { onShowing: userOnShowing } = options; - const $container = getStackContainer(containerKey); - setContainerClasses($container, direction); - - extend(options, { - container: $container, - _skipContentPositioning: true, - onShowing(args) { - setContainerStyles($container, direction, position); - userOnShowing?.(args); - }, - }); - } - - extend(options, { - type, - displayTime, - onHidden(args) { - $(args.element).remove(); - userOnHidden?.(args); - }, - }); - // @ts-expect-error - $notify = $('
').appendTo(viewPort()); - // @ts-expect-error - new Toast($notify, options).show(); -} - -const getDefaultDirection = (position) => (isString(position) && position.includes('top') ? 'down-push' : 'up-push'); - -const createStackContainer = (key) => { - const $container = $('
').appendTo(viewPort()); - $containers[key] = $container; - - return $container; -}; - -const getStackContainer = (key) => { - const $container = $containers[key]; - - return $container || createStackContainer(key); -}; - -const setContainerClasses = (container, direction) => { - const containerClasses = `dx-toast-stack dx-toast-stack-${direction}-direction`; - container.removeAttr('class').addClass(containerClasses); -}; - -const setContainerStyles = (container, direction, position) => { - const { offsetWidth: toastWidth, offsetHeight: toastHeight } = container.children().first().get(0); - - const dimensions = { - toastWidth, - toastHeight, - windowHeight: window.innerHeight, - windowWidth: window.innerWidth, - }; - - const coordinates = isString(position) ? getCoordinatesByAlias(position, dimensions) : position; - - const styles = getPositionStylesByCoordinates(direction, coordinates, dimensions); - - container.css(styles); -}; - -const getCoordinatesByAlias = (alias, { - toastWidth, toastHeight, windowHeight, windowWidth, -}) => { - switch (alias) { - case 'top left': - return { top: 10, left: 10 }; - case 'top right': - return { top: 10, right: 10 }; - case 'bottom left': - return { bottom: 10, left: 10 }; - case 'bottom right': - return { bottom: 10, right: 10 }; - case 'top center': - return { top: 10, left: Math.round(windowWidth / 2 - toastWidth / 2) }; - case 'left center': - return { top: Math.round(windowHeight / 2 - toastHeight / 2), left: 10 }; - case 'right center': - return { top: Math.round(windowHeight / 2 - toastHeight / 2), right: 10 }; - case 'center': - return { - top: Math.round(windowHeight / 2 - toastHeight / 2), - left: Math.round(windowWidth / 2 - toastWidth / 2), - }; - case 'bottom center': - default: - return { bottom: 10, left: Math.round(windowWidth / 2 - toastWidth / 2) }; - } -}; -// @ts-expect-error -const getPositionStylesByCoordinates = (direction, coordinates, dimensions) => { - const { - toastWidth, toastHeight, windowHeight, windowWidth, - } = dimensions; - - // eslint-disable-next-line default-case - switch (direction.replace(/-push|-stack/g, '')) { - case 'up': - return { - bottom: coordinates.bottom ?? windowHeight - toastHeight - coordinates.top, - top: '', - left: coordinates.left ?? '', - right: coordinates.right ?? '', - }; - case 'down': - return { - top: coordinates.top ?? windowHeight - toastHeight - coordinates.bottom, - bottom: '', - left: coordinates.left ?? '', - right: coordinates.right ?? '', - }; - case 'left': - return { - right: coordinates.right ?? windowWidth - toastWidth - coordinates.left, - left: '', - top: coordinates.top ?? '', - bottom: coordinates.bottom ?? '', - }; - case 'right': - return { - left: coordinates.left ?? windowWidth - toastWidth - coordinates.right, - right: '', - top: coordinates.top ?? '', - bottom: coordinates.bottom ?? '', - }; - } -}; - -/// #DEBUG -Object.setPrototypeOf(notify, { - _resetContainers() { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - Object.keys($containers).forEach((key) => delete $containers[key]); - }, -}); -/// #ENDDEBUG - -export default notify; diff --git a/packages/devextreme/js/__internal/ui/m_select_box.ts b/packages/devextreme/js/__internal/ui/m_select_box.ts index fcda7bf4209b..63a9b37287ff 100644 --- a/packages/devextreme/js/__internal/ui/m_select_box.ts +++ b/packages/devextreme/js/__internal/ui/m_select_box.ts @@ -444,9 +444,10 @@ class SelectBox< } _updateField(item): void { + const { fieldTemplate: fieldTemplateOption } = this.option(); const fieldTemplate = this._getTemplateByOption('fieldTemplate'); - if (!(fieldTemplate && this.option('fieldTemplate'))) { + if (!(fieldTemplate && fieldTemplateOption)) { // @ts-expect-error ts-error const text = this._displayGetter(item); @@ -963,6 +964,11 @@ class SelectBox< this._caret({ start: valueLength, end: displayValue.length }); } + // eslint-disable-next-line class-methods-use-this + _shouldLogFieldTemplateDeprecationWarning(): boolean { + return true; + } + _dispose(): void { this._renderInputValueAsync = noop; delete this._loadItemDeferred; diff --git a/packages/devextreme/js/__internal/ui/m_tag_box.ts b/packages/devextreme/js/__internal/ui/m_tag_box.ts index 02dbd2e17a1b..8a1c7596a3cd 100644 --- a/packages/devextreme/js/__internal/ui/m_tag_box.ts +++ b/packages/devextreme/js/__internal/ui/m_tag_box.ts @@ -562,7 +562,8 @@ class TagBox< } _renderField(): void { - const isDefaultFieldTemplate = !isDefined(this.option('fieldTemplate')); + const { fieldTemplate } = this.option(); + const isDefaultFieldTemplate = !isDefined(fieldTemplate); this.$element() .toggleClass(TAGBOX_DEFAULT_FIELD_TEMPLATE_CLASS, isDefaultFieldTemplate) diff --git a/packages/devextreme/js/__internal/ui/map/m_map.ts b/packages/devextreme/js/__internal/ui/map/m_map.ts index 1f7dbe3fbb51..7e1361210a17 100644 --- a/packages/devextreme/js/__internal/ui/map/m_map.ts +++ b/packages/devextreme/js/__internal/ui/map/m_map.ts @@ -1,3 +1,4 @@ +import type { DefaultOptionsRule } from '@js/common'; import eventsEngine from '@js/common/core/events/core/events_engine'; import pointerEvents from '@js/common/core/events/pointer'; import { addNamespace } from '@js/common/core/events/utils/index'; @@ -6,21 +7,22 @@ import devices from '@js/core/devices'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { wrapToArray } from '@js/core/utils/array'; -// @ts-expect-error -import { fromPromise } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { titleize } from '@js/core/utils/inflector'; -import { each } from '@js/core/utils/iterator'; import { isNumeric } from '@js/core/utils/type'; +import type { DxEvent } from '@js/events'; import type { Properties } from '@js/ui/map'; import errors from '@js/ui/widget/ui.errors'; +import { fromPromise } from '@ts/core/utils/m_deferred'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; +import type { LocationOption } from './m_provider.dynamic'; import azure from './m_provider.dynamic.azure'; import bing from './m_provider.dynamic.bing'; import google from './m_provider.dynamic.google'; -// NOTE external urls must have protocol explicitly specified (because inside Cordova package the protocol is "file:") +// NOTE external urls must have protocol explicitly specified +// (because inside Cordova package the protocol is "file:") import googleStatic from './m_provider.google_static'; const PROVIDERS = { @@ -37,11 +39,18 @@ const MAP_SHIELD_CLASS = 'dx-map-shield'; export interface MapProperties extends Properties { onUpdated?: () => {}; - bounds?: Record; + bounds?: { + southWest?: LocationOption | null; + northEast?: LocationOption | null; + }; } class Map extends Widget { - _optionChangeBag?: Record | null; + _optionChangeBag?: { + resolve: (value?: unknown) => void; + added: MapProperties['markers'] | MapProperties['routes']; + removed: MapProperties['markers'] | MapProperties['routes']; + } | null; _lastAsyncAction!: Promise; @@ -51,9 +60,12 @@ class Map extends Widget { _suppressAsyncAction?: boolean; - _rendered!: Record; + _rendered!: { + markers?: MapProperties['markers'] | MapProperties['routes']; + routes?: MapProperties['routes'] | MapProperties['markers']; + }; - _$container?: dxElementWrapper; + _$container!: dxElementWrapper; _getDefaultOptions(): MapProperties { return { @@ -104,10 +116,10 @@ class Map extends Widget { }; } - _defaultOptionsRules() { + _defaultOptionsRules(): DefaultOptionsRule[] { return super._defaultOptionsRules().concat([ { - device() { + device(): boolean { return devices.real().deviceType === 'desktop' && !devices.isSimulator(); }, options: { @@ -117,7 +129,7 @@ class Map extends Widget { ]); } - ctor(element, options) { + ctor(element: Element, options: MapProperties): void { super.ctor(element, options); if (options) { @@ -165,7 +177,7 @@ class Map extends Widget { return false; } - _checkOption(option): void { + _checkOption(option: 'markers' | 'routes' | 'provider'): void { const value = this.option(option); if (option === 'markers' && !Array.isArray(value)) { @@ -190,16 +202,16 @@ class Map extends Widget { eventsEngine.on(this.$element(), eventName, this._cancelEvent.bind(this)); } - _cancelEvent(e): void { - const cancelByProvider = this._provider?.isEventsCanceled(e) && !this.option('disabled'); + _cancelEvent(e: DxEvent): void { + const { disabled } = this.option(); + const cancelByProvider = this._provider?.isEventsCanceled(e) && !disabled; if (cancelByProvider) { e.stopPropagation(); } } - _saveRendered(option): void { - const value = this.option(option); - // @ts-expect-error ts-error + _saveRendered(option: 'markers' | 'routes'): void { + const { [option]: value = [] } = this.option(); this._rendered[option] = value.slice(); } @@ -211,28 +223,34 @@ class Map extends Widget { this._saveRendered('markers'); this._saveRendered('routes'); - const { provider } = this.option(); + const { provider = 'google' } = this.option(); + const Provider = PROVIDERS[provider]; - // @ts-expect-error ts-error - this._provider = new PROVIDERS[provider](this, this._$container); + this._provider = new Provider(this, this._$container); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('render', this._rendered.markers, this._rendered.routes); } _renderShield(): void { - let $shield; + const { disabled } = this.option(); - if (this.option('disabled')) { - $shield = $('
').addClass(MAP_SHIELD_CLASS); + if (disabled) { + const $shield = $('
').addClass(MAP_SHIELD_CLASS); this.$element().append($shield); - } else { - $shield = this.$element().find(`.${MAP_SHIELD_CLASS}`); - $shield.remove(); + + return; } + + this.$element() + .find(`.${MAP_SHIELD_CLASS}`) + .remove(); } _clean(): void { this._cleanFocusState(); if (this._provider) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._provider.clean(); } // @ts-expect-error ts-error @@ -253,6 +271,7 @@ class Map extends Widget { case 'disabled': this._renderShield(); super._optionChanged(args); + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('updateDisabled'); break; case 'width': @@ -271,21 +290,27 @@ class Map extends Widget { errors.log('W1001'); break; case 'bounds': + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('updateBounds'); break; case 'center': + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('updateCenter'); break; case 'zoom': + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('updateZoom'); break; case 'type': + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('updateMapType'); break; case 'controls': + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('updateControls', this._rendered.markers, this._rendered.routes); break; case 'autoAdjust': + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('adjustViewport'); break; case 'markers': @@ -294,19 +319,20 @@ class Map extends Widget { const prevValue = this._rendered[name]; this._saveRendered(name); + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction( - `update${titleize(name)}`, + `${name === 'markers' ? 'updateMarkers' : 'updateRoutes'}`, changeBag ? changeBag.removed : prevValue, changeBag ? changeBag.added : this._rendered[name], ).then((result) => { if (changeBag) { - // @ts-expect-error ts-error changeBag.resolve(result); } }); break; } case 'markerIconSrc': + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('updateMarkers', this._rendered.markers, this._rendered.markers); break; case 'providerConfig': @@ -322,8 +348,7 @@ class Map extends Widget { case 'onClick': break; default: - // @ts-expect-error ts-error - super._optionChanged.apply(this, arguments); + super._optionChanged(args); } } @@ -334,12 +359,16 @@ class Map extends Widget { } _dimensionChanged(): void { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._queueAsyncAction('updateDimensions'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _queueAsyncAction(name, markers?, routers?) { - const options = [].slice.call(arguments).slice(1); + _queueAsyncAction( + name: string, + markers?: MapProperties['markers'] | MapProperties['routes'], + routes?: MapProperties['routes'] | MapProperties['markers'], + ): Promise { + const markerAndRoutes = [markers, routes].filter(Boolean); const isActionSuppressed = this._suppressAsyncAction; this._lastAsyncAction = this._lastAsyncAction.then(() => { @@ -350,10 +379,11 @@ class Map extends Widget { return Promise.resolve(); } - return this._provider[name].apply(this._provider, options).then((result) => { - result = wrapToArray(result); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this._provider[name](...markerAndRoutes).then((result) => { + const arrayResult = wrapToArray(result); - const mapRefreshed = result[0]; + const mapRefreshed = arrayResult[0]; if (mapRefreshed && !this._disposed) { this._triggerReadyAction(); } @@ -363,7 +393,8 @@ class Map extends Widget { } /// #ENDDEBUG - return result[1]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return arrayResult[1]; }); }); @@ -378,58 +409,84 @@ class Map extends Widget { this._createActionByOption('onUpdated')(); } - setOptionSilent(name, value): void { + setOptionSilent(name: string, value: unknown): void { this._setOptionWithoutOptionChange(name, value); } - addMarker(marker) { + addMarker(marker: MapProperties['markers']): Promise { return this._addFunction('markers', marker); } - removeMarker(marker) { + removeMarker(marker: MapProperties['markers'] | number): Promise { return this._removeFunction('markers', marker); } - addRoute(route) { + addRoute(route: MapProperties['routes']): Promise { return this._addFunction('routes', route); } - removeRoute(route) { + removeRoute(route: MapProperties['routes'] | number): Promise { return this._removeFunction('routes', route); } - _addFunction(optionName, addingValue) { - const optionValue = this.option(optionName); + _addFunction( + optionName: 'markers', + addingValue: MapProperties['markers'] + ): Promise; + _addFunction( + optionName: 'routes', + addingValue: MapProperties['routes'] + ): Promise; + _addFunction( + optionName: 'markers' | 'routes', + addingValue: MapProperties['markers'] | MapProperties['routes'], + ): Promise { + const { [optionName]: optionValue = [] } = this.option(); const addingValues = wrapToArray(addingValue); - // @ts-expect-error ts-error - optionValue.push.apply(optionValue, addingValues); + optionValue.push(...addingValues); return this._partialArrayOptionChange(optionName, optionValue, addingValues, []); } - _removeFunction(optionName, removingValue) { - const optionValue = this.option(optionName); + _removeFunction( + optionName: 'markers', + removingValue: MapProperties['markers'] | number, + ): Promise; + _removeFunction( + optionName: 'routes', + removingValue: MapProperties['routes' ] | number, + ): Promise; + _removeFunction( + optionName: 'markers' | 'routes', + removingValue: MapProperties['markers'] | MapProperties['routes' ] | number, + ): Promise { + const { [optionName]: optionValue = [] } = this.option(); const removingValues = wrapToArray(removingValue); - each(removingValues, (removingIndex, removingValue) => { - const index = isNumeric(removingValue) - ? removingValue - // @ts-expect-error ts-error - : optionValue?.indexOf(removingValue); + removingValues.forEach((value, removingIndex) => { + const index = isNumeric(value) + ? value + : optionValue.indexOf(value); if (index !== -1) { - // @ts-expect-error ts-error const removing = optionValue.splice(index, 1)[0]; removingValues.splice(removingIndex, 1, removing); } else { - throw errors.log('E1021', titleize(optionName.substring(0, optionName.length - 1)), removingValue); + throw errors.log('E1021', titleize(optionName.substring(0, optionName.length - 1)), value); } }); return this._partialArrayOptionChange(optionName, optionValue, [], removingValues); } - _partialArrayOptionChange(optionName, optionValue, addingValues, removingValues) { + _partialArrayOptionChange( + optionName: 'markers' | 'routes', + optionValue: MapProperties['markers'] | MapProperties['routes'], + addingValues: MapProperties['markers'] | MapProperties['routes'], + removingValues: MapProperties['markers'] | MapProperties['routes'], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise | Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return fromPromise(new Promise((resolve) => { this._optionChangeBag = { resolve, @@ -437,8 +494,12 @@ class Map extends Widget { removed: removingValues, }; this.option(optionName, optionValue); - // @ts-expect-error - }).then((result) => (result && result.length === 1 ? result[0] : result)), this); + }).then((result) => { + const resultArray = Array.isArray(result) ? result : [result]; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return resultArray.length === 1 ? resultArray[0] : resultArray; + }), this); } } diff --git a/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.azure.ts b/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.azure.ts index 07f1434da8fa..b61ac9a27ca3 100644 --- a/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.azure.ts +++ b/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.azure.ts @@ -3,48 +3,59 @@ import Color from '@js/color'; import $ from '@js/core/renderer'; import ajax from '@js/core/utils/ajax'; import { noop } from '@js/core/utils/common'; -import { map } from '@js/core/utils/iterator'; import { isDefined } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; +import type { MapType, RouteMode } from '@js/ui/map'; import errors from '@js/ui/widget/ui.errors'; +import type { + LocationOption, + MarkerObject, MarkerOptions, RouteObject, RouteOptions, +} from './m_provider.dynamic'; import DynamicProvider from './m_provider.dynamic'; const window = getWindow(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let atlas: any; const AZURE_BASE_LINK = 'https://atlas.microsoft.com/'; let AZURE_JS_URL = `${AZURE_BASE_LINK}sdk/javascript/mapcontrol/3/atlas.min.js`; let AZURE_CSS_URL = `${AZURE_BASE_LINK}/sdk/javascript/mapcontrol/3/atlas.min.css`; +// eslint-disable-next-line @typescript-eslint/init-declarations let CUSTOM_URL; const MAP_MARKER_TOOLTIP_CLASS = 'dx-map-marker-tooltip'; const CAMERA_PADDING = 50; +// @ts-expect-error ts-error +const azureMapsLoaded = (): boolean => Boolean(window.atlas?.Map); -const azureMapsLoaded = function () { - // @ts-expect-error - return window.atlas?.Map; -}; +export type AzureLocation = [number, number]; +// eslint-disable-next-line @typescript-eslint/init-declarations let azureMapsLoader; class AzureProvider extends DynamicProvider { _preventZoomChangeEvent?: boolean; - _mapReadyPromise?: Promise; + _mapReadyPromise!: Promise; - _mapType(type) { + _mapType(type?: MapType): string { const mapTypes = { roadmap: 'road', satellite: 'satellite', hybrid: 'satellite_road_labels', }; - return mapTypes[type] || mapTypes.roadmap; + + if (!type) { + return mapTypes.roadmap; + } + + return mapTypes[type] ?? mapTypes.roadmap; } - _movementMode(type) { - const movementTypes = { + _movementMode(type: RouteMode | string = ''): string { + const movementTypes: Record = { driving: 'car', walking: 'pedestrian', }; @@ -56,20 +67,23 @@ class AzureProvider extends DynamicProvider { return movementTypes[type] ?? type; } - _resolveLocation(location) { + _resolveLocation(location?: LocationOption | null): Promise { return new Promise((resolve) => { const latLng = this._getLatLng(location); if (latLng) { resolve(new atlas.data.Position(latLng.lng, latLng.lat)); } else { - this._geocodeLocation(location).then((geocodedLocation) => { - resolve(geocodedLocation); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._geocodeLocation(location as string).then((geocodedLocation) => { + resolve(geocodedLocation as AzureLocation); }); } }); } - _geocodeLocationImpl(location) { + _geocodeLocationImpl( + location: string, + ): Promise { return new Promise((resolve) => { if (!isDefined(location)) { resolve(new atlas.data.Position(0, 0)); @@ -93,14 +107,19 @@ class AzureProvider extends DynamicProvider { }); } - _normalizeLocation(location) { + _normalizeLocation(location: AzureLocation): { + lat: number; + lng: number; + } { return { lat: location[1], lng: location[0], }; } - _normalizeLocationRect(locationRect) { + _normalizeLocationRect( + locationRect: [number, number, number, number], + ): { northEast: { lat: number; lng: number }; southWest: { lat: number; lng: number } } { return { northEast: { lat: locationRect[1], @@ -113,7 +132,7 @@ class AzureProvider extends DynamicProvider { }; } - _loadImpl() { + _loadImpl(): Promise { return new Promise((resolve) => { if (azureMapsLoaded()) { resolve(); @@ -129,20 +148,22 @@ class AzureProvider extends DynamicProvider { resolve(); return; } - // @ts-expect-error ts-error - this._loadMapResources().then(resolve); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._loadMapResources() + .then(resolve); }); }); } - _loadMapResources() { + _loadMapResources(): Promise { return Promise.all([ this._loadMapScript(), this._loadMapStyles(), - ]); + ]).then(() => {}); } - _loadMapScript() { + _loadMapScript(): Promise { return new Promise((resolve) => { ajax.sendRequest({ url: AZURE_JS_URL, @@ -153,7 +174,7 @@ class AzureProvider extends DynamicProvider { }); } - _loadMapStyles() { + _loadMapStyles(): Promise { return new Promise((resolve) => { ajax.sendRequest({ url: AZURE_CSS_URL, @@ -165,20 +186,21 @@ class AzureProvider extends DynamicProvider { }); } - _init() { + _init(): Promise { this._createMap(); return this._mapReadyPromise; } - _createMap() { + _createMap(): void { + const type = this._option('type'); this._map = new atlas.Map(this._$container[0], { authOptions: { authType: 'subscriptionKey', subscriptionKey: this._keyOption('azure'), }, zoom: this._option('zoom'), - style: this._mapType(this._option('type')), + style: this._mapType(type), interactive: !this._option('disabled'), }); @@ -188,15 +210,16 @@ class AzureProvider extends DynamicProvider { }); }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.updateControls(); } - _attachHandlers() { + _attachHandlers(): void { this._map.events.add('move', this._viewChangeHandler.bind(this)); this._map.events.add('click', this._clickActionHandler.bind(this)); } - _viewChangeHandler() { + _viewChangeHandler(): void { const { bounds } = this._map.getCamera(); this._option('bounds', this._normalizeLocationRect(bounds)); @@ -208,7 +231,11 @@ class AzureProvider extends DynamicProvider { } } - _clickActionHandler(e) { + _clickActionHandler(e: { + type: string; + position: AzureLocation; + originalEvent: MouseEvent; + }): void { if (e.type === 'click') { this._fireClickAction({ location: this._normalizeLocation(e.position), @@ -217,13 +244,13 @@ class AzureProvider extends DynamicProvider { } } - updateDimensions() { + updateDimensions(): Promise { this._map.resize(); return Promise.resolve(); } - updateDisabled() { + updateDisabled(): Promise { const disabled = this._option('disabled'); this._map.setUserInteraction({ @@ -233,8 +260,9 @@ class AzureProvider extends DynamicProvider { return Promise.resolve(); } - updateMapType() { - const newType = this._mapType(this._option('type')); + updateMapType(): Promise { + const type = this._option('type'); + const newType = this._mapType(type); const currentType = this._map.getStyle().style; if (newType !== currentType) { @@ -246,14 +274,14 @@ class AzureProvider extends DynamicProvider { return Promise.resolve(); } - updateBounds() { + updateBounds(): Promise { + const bounds = this._option('bounds'); return Promise.all([ - this._resolveLocation(this._option('bounds.northEast')), - this._resolveLocation(this._option('bounds.southWest')), + this._resolveLocation(bounds?.northEast), + this._resolveLocation(bounds?.southWest), ]).then((result) => { this._map.setCamera({ bounds: [ - // @ts-expect-error ts-error result[1][0], result[1][1], result[0][0], result[0][1], ], padding: 50, @@ -261,7 +289,7 @@ class AzureProvider extends DynamicProvider { }); } - updateCenter() { + updateCenter(): Promise { return this._resolveLocation(this._option('center')).then((center) => { this._map.setCamera({ center, @@ -269,7 +297,7 @@ class AzureProvider extends DynamicProvider { }); } - updateZoom() { + updateZoom(): Promise { this._map.setCamera({ zoom: this._option('zoom'), }); @@ -277,8 +305,8 @@ class AzureProvider extends DynamicProvider { return Promise.resolve(); } - updateControls() { - const { controls } = this._option(); + updateControls(): Promise { + const controls = this._option('controls'); if (controls) { this._map.controls.add([ @@ -300,11 +328,13 @@ class AzureProvider extends DynamicProvider { return Promise.resolve(); } - _renderMarker(options) { - return this._resolveLocation(options.location).then((location) => { - const markerOptions: any = { + _renderMarker(options: MarkerOptions): Promise { + const { location: markerLocation } = options; + return this._resolveLocation(markerLocation).then((location) => { + const markerOptions: Record = { position: location, }; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const icon = options.iconSrc || this._option('markerIconSrc'); if (icon) { markerOptions.htmlContent = this._createIconTemplate(icon); @@ -313,9 +343,10 @@ class AzureProvider extends DynamicProvider { this._map.markers.add(marker); const popup = this._renderTooltip(location, options.tooltip); + // eslint-disable-next-line @typescript-eslint/init-declarations let handler; if (options.onClick || options.tooltip) { - const markerClickAction = this._mapWidget._createAction(options.onClick || noop); + const markerClickAction = this._mapWidget._createAction(options.onClick ?? noop); const markerNormalizedLocation = this._normalizeLocation(location); handler = this._map.events.add('click', marker, () => { @@ -342,14 +373,15 @@ class AzureProvider extends DynamicProvider { }); } - _renderTooltip(location, options) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _renderTooltip(location: AzureLocation, options: MarkerOptions['tooltip']): any { if (!options) { - return; + return undefined; } - options = this._parseTooltipOptions(options); + const parsedOptions = this._parseTooltipOptions(options); - const $content = $('
').html(options.text).addClass(MAP_MARKER_TOOLTIP_CLASS); + const $content = $('
').html(parsedOptions.text).addClass(MAP_MARKER_TOOLTIP_CLASS); const popup = new atlas.Popup({ content: $content[0], position: location, @@ -358,14 +390,14 @@ class AzureProvider extends DynamicProvider { this._map.popups.add(popup); - if (options.visible) { + if (parsedOptions.visible) { popup.open(); } return popup; } - _destroyMarker(marker) { + _destroyMarker(marker: { marker: unknown; popup: unknown; handler: () => void }): void { this._map.markers.remove(marker.marker); if (marker.popup) { this._map.popups.remove(marker.popup); @@ -375,11 +407,14 @@ class AzureProvider extends DynamicProvider { } } - _renderRoute(options) { + _renderRoute(options: RouteOptions): Promise { + const routeLocations = options.locations ?? []; return Promise.all( - map(options.locations, (point) => this._resolveLocation(point)), + routeLocations.map((point) => this._resolveLocation(point)), ).then((locations) => new Promise((resolve) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const routeColor = new Color(options.color || this._defaultRouteColor()).toHex(); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const routeOpacity = options.opacity || this._defaultRouteOpacity(); const queryCoordinates = locations.map((location) => `${location[1]},${location[0]}`); const query = queryCoordinates.join(':'); @@ -393,7 +428,11 @@ class AzureProvider extends DynamicProvider { if (result?.routes && result.routes.length > 0) { const route = result.routes[0]; const routeCoordinates = route.legs - .flatMap((leg) => leg.points.map((point) => [point.longitude, point.latitude])); + .flatMap( + (leg: { points: { longitude: number; latitude: number }[] }) => leg.points.map( + (point) => [point.longitude, point.latitude], + ), + ); const dataSource = new atlas.source.DataSource(); dataSource.add(new atlas.data.Feature(new atlas.data.LineString(routeCoordinates), {})); @@ -401,6 +440,7 @@ class AzureProvider extends DynamicProvider { const lineLayer = new atlas.layer.LineLayer(dataSource, null, { strokeColor: routeColor, strokeOpacity: routeOpacity, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing strokeWidth: options.weight || this._defaultRouteWeight(), }); @@ -434,12 +474,12 @@ class AzureProvider extends DynamicProvider { })); } - _destroyRoute(routeObject) { + _destroyRoute(routeObject: { instance: { dataSource: unknown; lineLayer: unknown } }): void { this._map.layers.remove(routeObject.instance.lineLayer); this._map.sources.remove(routeObject.instance.dataSource); } - _fitBounds() { + _fitBounds(): Promise { this._updateBounds(); if (this._bounds && this._option('autoAdjust')) { @@ -465,7 +505,7 @@ class AzureProvider extends DynamicProvider { return Promise.resolve(); } - _extendBounds(location) { + _extendBounds(location: AzureLocation): void { const [longitude, latitude] = location; const delta = 0.0001; if (this._bounds) { @@ -481,7 +521,7 @@ class AzureProvider extends DynamicProvider { } } - clean() { + clean(): Promise { if (this._map) { this._map.events.remove('move', this._viewChangeHandler); this._map.events.remove('click', this._clickActionHandler); diff --git a/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.bing.ts b/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.bing.ts index 4f1ed09c056c..71db8fb8de3c 100644 --- a/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.bing.ts +++ b/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.bing.ts @@ -3,18 +3,23 @@ import Color from '@js/color'; import ajax from '@js/core/utils/ajax'; import { noop } from '@js/core/utils/common'; import { extend } from '@js/core/utils/extend'; -import { each, map } from '@js/core/utils/iterator'; import { getHeight, getWidth } from '@js/core/utils/size'; import { isDefined } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; +import type { MapType, RouteMode } from '@js/ui/map'; import errors from '@js/ui/widget/ui.errors'; +import type { + LocationOption, + MarkerObject, MarkerOptions, RouteObject, RouteOptions, +} from './m_provider.dynamic'; import DynamicProvider from './m_provider.dynamic'; const window = getWindow(); // eslint-disable-next-line @typescript-eslint/no-unused-vars /* global Microsoft */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Microsoft: any; const BING_MAP_READY = '_bingScriptReady'; @@ -23,30 +28,38 @@ let BING_URL_V8 = `https://www.bing.com/api/maps/mapcontrol?callback=${BING_MAP_ const INFOBOX_V_OFFSET_V8 = 13; const MIN_LOCATION_RECT_LENGTH = 0.0000000000000001; -const msMapsLoaded = function () { - // @ts-expect-error - return window.Microsoft?.Maps; -}; +// @ts-expect-error ts-error +const msMapsLoaded = (): boolean => Boolean(window.Microsoft?.Maps); +export interface BingLocation { + latitude: number; + longitude: number; +} + +// eslint-disable-next-line @typescript-eslint/init-declarations let msMapsLoader; class BingProvider extends DynamicProvider { - _providerClickHandler?: any; + _providerClickHandler?: (e: { targetType: string; location: BingLocation }) => void; - _providerViewChangeHandler?: any; + _providerViewChangeHandler?: () => void; _preventZoomChangeEvent?: boolean; - _mapType(type) { + _mapType(type?: MapType): unknown { const mapTypes = { roadmap: Microsoft.Maps.MapTypeId.road, hybrid: Microsoft.Maps.MapTypeId.aerial, satellite: Microsoft.Maps.MapTypeId.aerial, }; - // @ts-expect-error - return mapTypes[type] || mapTypes.road; + + if (!type) { + return mapTypes.roadmap; + } + + return mapTypes[type] ?? mapTypes.roadmap; } - _movementMode(type) { + _movementMode(type: RouteMode | string = ''): unknown { if (!type) { return Microsoft.Maps.Directions.RouteMode.driving; } @@ -54,20 +67,23 @@ class BingProvider extends DynamicProvider { return Microsoft.Maps.Directions.RouteMode[type]; } - _resolveLocation(location) { + _resolveLocation(location?: LocationOption | null): Promise { return new Promise((resolve) => { const latLng = this._getLatLng(location); if (latLng) { resolve(new Microsoft.Maps.Location(latLng.lat, latLng.lng)); } else { - this._geocodeLocation(location).then((geocodedLocation) => { - resolve(geocodedLocation); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._geocodeLocation(location as string).then((geocodedLocation) => { + resolve(geocodedLocation as BingLocation); }); } }); } - _geocodeLocationImpl(location) { + _geocodeLocationImpl( + location: string, + ): Promise { return new Promise((resolve) => { if (!isDefined(location)) { resolve(new Microsoft.Maps.Location(0, 0)); @@ -78,7 +94,7 @@ class BingProvider extends DynamicProvider { const searchRequest = { where: location, count: 1, - callback(searchResponse) { + callback(searchResponse): void { const result = searchResponse.results[0]; if (result) { const boundsBox = searchResponse.results[0].location; @@ -94,14 +110,21 @@ class BingProvider extends DynamicProvider { }); } - _normalizeLocation(location) { + _normalizeLocation( + location: BingLocation, + ): { lat: number; lng: number } { return { lat: location.latitude, lng: location.longitude, }; } - _normalizeLocationRect(locationRect) { + _normalizeLocationRect( + locationRect: { + getNorthwest: () => BingLocation; + getSoutheast: () => BingLocation; + }, + ): { northEast: { lat: number; lng: number }; southWest: { lat: number; lng: number } } { const northWest = this._normalizeLocation(locationRect.getNorthwest()); const southEast = this._normalizeLocation(locationRect.getSoutheast()); @@ -117,10 +140,9 @@ class BingProvider extends DynamicProvider { }; } - _loadImpl() { - return new Promise((resolve) => { + _loadImpl(): Promise { + return new Promise((resolve) => { if (msMapsLoaded()) { - // @ts-expect-error resolve(); } else { if (!msMapsLoader) { @@ -129,11 +151,11 @@ class BingProvider extends DynamicProvider { msMapsLoader.then(() => { if (msMapsLoaded()) { - // @ts-expect-error resolve(); return; } + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._loadMapScript().then(resolve); }); } @@ -144,10 +166,10 @@ class BingProvider extends DynamicProvider { new Promise((resolve) => { Microsoft.Maps.loadModule('Microsoft.Maps.Directions', { callback: resolve }); }), - ])); + ])).then(() => {}); } - _loadMapScript() { + _loadMapScript(): Promise { return new Promise((resolve) => { window[BING_MAP_READY] = resolve; ajax.sendRequest({ @@ -164,13 +186,13 @@ class BingProvider extends DynamicProvider { }); } - _init() { + _init(): Promise { this._createMap(); return Promise.resolve(); } - _createMap() { + _createMap(): void { const controls = this._option('controls'); this._map = new Microsoft.Maps.Map(this._$container[0], { @@ -182,12 +204,12 @@ class BingProvider extends DynamicProvider { }); } - _attachHandlers() { + _attachHandlers(): void { this._providerViewChangeHandler = Microsoft.Maps.Events.addHandler(this._map, 'viewchange', this._viewChangeHandler.bind(this)); this._providerClickHandler = Microsoft.Maps.Events.addHandler(this._map, 'click', this._clickActionHandler.bind(this)); } - _viewChangeHandler() { + _viewChangeHandler(): void { const bounds = this._map.getBounds(); this._option('bounds', this._normalizeLocationRect(bounds)); @@ -199,13 +221,15 @@ class BingProvider extends DynamicProvider { } } - _clickActionHandler(e) { + _clickActionHandler( + e: { targetType: string; location: BingLocation }, + ): void { if (e.targetType === 'map') { this._fireClickAction({ location: this._normalizeLocation(e.location) }); } } - updateDimensions() { + updateDimensions(): Promise { const $container = this._$container; this._map.setOptions({ @@ -216,7 +240,7 @@ class BingProvider extends DynamicProvider { return Promise.resolve(); } - updateMapType() { + updateMapType(): Promise { const type = this._option('type'); const labelOverlay = Microsoft.Maps.LabelOverlay; @@ -229,10 +253,11 @@ class BingProvider extends DynamicProvider { return Promise.resolve(); } - updateBounds() { + updateBounds(): Promise { + const boundsOption = this._option('bounds'); return Promise.all([ - this._resolveLocation(this._option('bounds.northEast')), - this._resolveLocation(this._option('bounds.southWest')), + this._resolveLocation(boundsOption?.northEast), + this._resolveLocation(boundsOption?.southWest), ]).then((result) => { // eslint-disable-next-line new-cap const bounds = new Microsoft.Maps.LocationRect.fromLocations(result[0], result[1]); @@ -244,7 +269,7 @@ class BingProvider extends DynamicProvider { }); } - updateCenter() { + updateCenter(): Promise { return this._resolveLocation(this._option('center')).then((center) => { this._map.setView({ animate: false, @@ -253,7 +278,7 @@ class BingProvider extends DynamicProvider { }); } - updateZoom() { + updateZoom(): Promise { this._map.setView({ animate: false, zoom: this._option('zoom'), @@ -262,15 +287,18 @@ class BingProvider extends DynamicProvider { return Promise.resolve(); } - updateControls() { + updateControls(markers: MarkerOptions[], routes: RouteOptions[]): Promise { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.clean(); - // @ts-expect-error ts-error - return this.render.apply(this, arguments); + + return this.render(markers, routes); } - _renderMarker(options) { - return this._resolveLocation(options.location).then((location) => { + _renderMarker(options: MarkerOptions): Promise { + const { location: markerLocation } = options; + return this._resolveLocation(markerLocation).then((location) => { const pushpinOptions = { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing icon: options.iconSrc || this._option('markerIconSrc'), }; if (options.html) { @@ -282,7 +310,7 @@ class BingProvider extends DynamicProvider { const { htmlOffset } = options; if (htmlOffset) { - // @ts-expect-error + // @ts-expect-error ts-error pushpinOptions.anchor = new Microsoft.Maps.Point(-htmlOffset.left, -htmlOffset.top); } } @@ -291,9 +319,10 @@ class BingProvider extends DynamicProvider { this._map.entities.push(pushpin); const infobox = this._renderTooltip(location, options.tooltip); + // eslint-disable-next-line @typescript-eslint/init-declarations let handler; if (options.onClick || options.tooltip) { - const markerClickAction = this._mapWidget._createAction(options.onClick || noop); + const markerClickAction = this._mapWidget._createAction(options.onClick ?? noop); const markerNormalizedLocation = this._normalizeLocation(location); handler = Microsoft.Maps.Events.addHandler(pushpin, 'click', () => { @@ -316,17 +345,18 @@ class BingProvider extends DynamicProvider { }); } - _renderTooltip(location, options) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _renderTooltip(location: BingLocation, options: MarkerOptions['tooltip']): any { if (!options) { - return; + return undefined; } - options = this._parseTooltipOptions(options); + const parsedOptions = this._parseTooltipOptions(options); const infobox = new Microsoft.Maps.Infobox(location, { - description: options.text, + description: parsedOptions.text, offset: new Microsoft.Maps.Point(0, INFOBOX_V_OFFSET_V8), - visible: options.visible, + visible: parsedOptions.visible, }); infobox.setMap(this._map); @@ -334,7 +364,9 @@ class BingProvider extends DynamicProvider { return infobox; } - _destroyMarker(marker) { + _destroyMarker(marker: { + marker: unknown; infobox: { setMap: (arg: unknown) => void }; handler: () => void; + }): void { this._map.entities.remove(marker.marker); if (marker.infobox) { marker.infobox.setMap(null); @@ -344,75 +376,81 @@ class BingProvider extends DynamicProvider { } } - _renderRoute(options) { - return Promise.all(map(options.locations, (point) => this._resolveLocation(point))).then((locations) => new Promise((resolve) => { - const direction = new Microsoft.Maps.Directions.DirectionsManager(this._map); - const color = new Color(options.color || this._defaultRouteColor()).toHex(); - // eslint-disable-next-line new-cap - const routeColor = new Microsoft.Maps.Color.fromHex(color); - routeColor.a = (options.opacity || this._defaultRouteOpacity()) * 255; - - direction.setRenderOptions({ - autoUpdateMapView: false, - displayRouteSelector: false, - waypointPushpinOptions: { visible: false }, - drivingPolylineOptions: { - strokeColor: routeColor, - strokeThickness: options.weight || this._defaultRouteWeight(), - }, - walkingPolylineOptions: { - strokeColor: routeColor, - strokeThickness: options.weight || this._defaultRouteWeight(), - }, - }); - direction.setRequestOptions({ - routeMode: this._movementMode(options.mode), - routeDraggable: false, - }); + _renderRoute(options: RouteOptions): Promise { + const routeLocations = options.locations ?? []; + return Promise.all(routeLocations.map((point) => this._resolveLocation(point))) + .then((locations) => new Promise((resolve) => { + const direction = new Microsoft.Maps.Directions.DirectionsManager(this._map); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const color = new Color(options.color || this._defaultRouteColor()).toHex(); + // eslint-disable-next-line new-cap + const routeColor = new Microsoft.Maps.Color.fromHex(color); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + routeColor.a = (options.opacity || this._defaultRouteOpacity()) * 255; + + direction.setRenderOptions({ + autoUpdateMapView: false, + displayRouteSelector: false, + waypointPushpinOptions: { visible: false }, + drivingPolylineOptions: { + strokeColor: routeColor, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + strokeThickness: options.weight || this._defaultRouteWeight(), + }, + walkingPolylineOptions: { + strokeColor: routeColor, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + strokeThickness: options.weight || this._defaultRouteWeight(), + }, + }); + direction.setRequestOptions({ + routeMode: this._movementMode(options.mode), + routeDraggable: false, + }); - each(locations, (_, location) => { - const waypoint = new Microsoft.Maps.Directions.Waypoint({ location }); - direction.addWaypoint(waypoint); - }); + locations.forEach((location) => { + const waypoint = new Microsoft.Maps.Directions.Waypoint({ location }); + direction.addWaypoint(waypoint); + }); - const directionHandlers = []; - // @ts-expect-error - directionHandlers.push(Microsoft.Maps.Events.addHandler(direction, 'directionsUpdated', (args) => { - while (directionHandlers.length) { - Microsoft.Maps.Events.removeHandler(directionHandlers.pop()); - } + const directionHandlers = []; + // @ts-expect-error ts-error + directionHandlers.push(Microsoft.Maps.Events.addHandler(direction, 'directionsUpdated', (args) => { + while (directionHandlers.length) { + Microsoft.Maps.Events.removeHandler(directionHandlers.pop()); + } - const routeSummary = args.routeSummary[0]; + const routeSummary = args.routeSummary[0]; - resolve({ - instance: direction, - northEast: routeSummary.northEast, - southWest: routeSummary.southWest, - }); - })); - // @ts-expect-error - directionHandlers.push(Microsoft.Maps.Events.addHandler(direction, 'directionsError', (args) => { - while (directionHandlers.length) { - Microsoft.Maps.Events.removeHandler(directionHandlers.pop()); - } + resolve({ + instance: direction, + northEast: routeSummary.northEast, + southWest: routeSummary.southWest, + }); + })); + // @ts-expect-error ts-error + directionHandlers.push(Microsoft.Maps.Events.addHandler(direction, 'directionsError', (args) => { + while (directionHandlers.length) { + Microsoft.Maps.Events.removeHandler(directionHandlers.pop()); + } - const status = `RouteResponseCode: ${args.responseCode} - ${args.message}`; - errors.log('W1006', status); + const status = `RouteResponseCode: ${args.responseCode} - ${args.message}`; + errors.log('W1006', status); - resolve({ - instance: direction, - }); - })); + resolve({ + instance: direction, + }); + })); - direction.calculateDirections(); - })); + direction.calculateDirections(); + })); } - _destroyRoute(routeObject) { + _destroyRoute(routeObject: { instance: { dispose: () => void } }): void { routeObject.instance.dispose(); } - _fitBounds() { + _fitBounds(): Promise { this._updateBounds(); if (this._bounds && this._option('autoAdjust')) { @@ -443,16 +481,24 @@ class BingProvider extends DynamicProvider { return Promise.resolve(); } - _extendBounds(location): void { + _extendBounds(location: { latitude: number; longitude: number }): void { if (this._bounds) { - // eslint-disable-next-line @stylistic/max-len, new-cap - this._bounds = new Microsoft.Maps.LocationRect.fromLocations(this._bounds.getNorthwest(), this._bounds.getSoutheast(), location); + // eslint-disable-next-line new-cap + this._bounds = new Microsoft.Maps.LocationRect.fromLocations( + this._bounds.getNorthwest(), + this._bounds.getSoutheast(), + location, + ); } else { - this._bounds = new Microsoft.Maps.LocationRect(location, MIN_LOCATION_RECT_LENGTH, MIN_LOCATION_RECT_LENGTH); + this._bounds = new Microsoft.Maps.LocationRect( + location, + MIN_LOCATION_RECT_LENGTH, + MIN_LOCATION_RECT_LENGTH, + ); } } - clean() { + clean(): Promise { if (this._map) { Microsoft.Maps.Events.removeHandler(this._providerViewChangeHandler); Microsoft.Maps.Events.removeHandler(this._providerClickHandler); @@ -469,7 +515,7 @@ class BingProvider extends DynamicProvider { /// #DEBUG // @ts-expect-error ts-error -BingProvider.remapConstant = function (newValue) { +BingProvider.remapConstant = function remapConstant(newValue: string): void { BING_URL_V8 = newValue; }; diff --git a/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.google.ts b/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.google.ts index 5b4ea8ddb41b..362655a45a9b 100644 --- a/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.google.ts +++ b/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.google.ts @@ -8,13 +8,19 @@ import $ from '@js/core/renderer'; import ajax from '@js/core/utils/ajax'; import { noop } from '@js/core/utils/common'; import { extend } from '@js/core/utils/extend'; -import { map } from '@js/core/utils/iterator'; import { isDefined } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; +import type { DxEvent } from '@js/events'; +import type { MapType, RouteMode } from '@js/ui/map'; import errors from '@js/ui/widget/ui.errors'; +import type { + LocationOption, + MarkerObject, MarkerOptions, RouteObject, RouteOptions, +} from './m_provider.dynamic'; import DynamicProvider from './m_provider.dynamic'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let google: any; const window = getWindow(); @@ -23,10 +29,16 @@ const GOOGLE_MAP_READY = '_googleScriptReady'; let GOOGLE_URL = `https://maps.googleapis.com/maps/api/js?callback=${GOOGLE_MAP_READY}&libraries=marker&loading=async`; const INFO_WINDOW_CLASS = 'gm-style-iw'; +// eslint-disable-next-line @typescript-eslint/init-declarations let CustomMarker; -const initCustomMarkerClass = function () { - CustomMarker = function (options) { +export interface GoogleLocation { + lat: () => number; + lng: () => number; +} + +const initCustomMarkerClass = function initCustomMarkerClass(): void { + CustomMarker = function CreateCustomMarker(options): void { this._position = options.position; this._offset = options.offset; @@ -43,7 +55,7 @@ const initCustomMarkerClass = function () { CustomMarker.prototype = new google.maps.OverlayView(); - CustomMarker.prototype.onAdd = function () { + CustomMarker.prototype.onAdd = function onAdd(): void { const $pane = $(this.getPanes().overlayMouseTarget); $pane.append(this._$overlayContainer); @@ -55,13 +67,13 @@ const initCustomMarkerClass = function () { this.draw(); }; - CustomMarker.prototype.onRemove = function () { + CustomMarker.prototype.onRemove = function onRemove(): void { google.maps.event.removeListener(this._clickListener); this._$overlayContainer.remove(); }; - CustomMarker.prototype.draw = function () { + CustomMarker.prototype.draw = function draw(): void { const position = this.getProjection().fromLatLngToDivPixel(this._position); this._$overlayContainer.css({ @@ -72,30 +84,34 @@ const initCustomMarkerClass = function () { }; }; -const googleMapsLoaded = function () { - // @ts-expect-error - return window.google?.maps; -}; +// @ts-expect-error ts-error +const googleMapsLoaded = (): boolean => Boolean(window.google?.maps); +// eslint-disable-next-line @typescript-eslint/init-declarations let googleMapsLoader; class GoogleProvider extends DynamicProvider { - _clickListener?: any; + _clickListener?: (e: { latLng: GoogleLocation; domEvent: Event }) => void; - _preventZoomChangeEvent?: any; + _preventZoomChangeEvent?: boolean; - _boundsChangeListener?: any; + _boundsChangeListener?: () => void; - _mapType(type) { + _mapType(type?: MapType): unknown { const mapTypes = { hybrid: google.maps.MapTypeId.HYBRID, roadmap: google.maps.MapTypeId.ROADMAP, satellite: google.maps.MapTypeId.SATELLITE, }; - return mapTypes[type] || mapTypes.hybrid; + + if (!type) { + return mapTypes.hybrid; + } + + return mapTypes[type] ?? mapTypes.hybrid; } - _movementMode(type) { + _movementMode(type: RouteMode | string = ''): unknown { const movementTypes = { driving: google.maps.TravelMode.DRIVING, walking: google.maps.TravelMode.WALKING, @@ -108,20 +124,23 @@ class GoogleProvider extends DynamicProvider { return movementTypes[type] ?? type; } - _resolveLocation(location) { + _resolveLocation(location?: LocationOption | null): Promise { return new Promise((resolve) => { const latLng = this._getLatLng(location); if (latLng) { resolve(new google.maps.LatLng(latLng.lat, latLng.lng)); } else { - this._geocodeLocation(location).then((geocodedLocation) => { - resolve(geocodedLocation); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._geocodeLocation(location as string).then((geocodedLocation) => { + resolve(geocodedLocation as GoogleLocation); }); } }); } - _geocodeLocationImpl(location) { + _geocodeLocationImpl( + location: string, + ): Promise { return new Promise((resolve) => { if (!isDefined(location)) { resolve(new google.maps.LatLng(0, 0)); @@ -140,24 +159,33 @@ class GoogleProvider extends DynamicProvider { }); } - _normalizeLocation(location) { + _normalizeLocation( + location: GoogleLocation, + ): { lat: number; lng: number } { return { lat: location.lat(), lng: location.lng(), }; } - _normalizeLocationRect(locationRect) { + _normalizeLocationRect( + locationRect: { + getNorthEast: () => GoogleLocation; + getSouthWest: () => GoogleLocation; + }, + ): { + northEast: { lat: number; lng: number }; + southWest: { lat: number; lng: number }; + } { return { northEast: this._normalizeLocation(locationRect.getNorthEast()), southWest: this._normalizeLocation(locationRect.getSouthWest()), }; } - _loadImpl() { - return new Promise((resolve) => { + _loadImpl(): Promise { + return new Promise((resolve) => { if (googleMapsLoaded()) { - // @ts-expect-error resolve(); } else { if (!googleMapsLoader) { @@ -166,12 +194,13 @@ class GoogleProvider extends DynamicProvider { googleMapsLoader.then(() => { if (googleMapsLoaded()) { - // @ts-expect-error resolve(); return; } - this._loadMapScript().then(resolve); + this._loadMapScript() + .then(resolve) + .catch(() => { }); }); } }).then(() => { @@ -179,7 +208,7 @@ class GoogleProvider extends DynamicProvider { }); } - _loadMapScript() { + _loadMapScript(): Promise { return new Promise((resolve) => { const key = this._keyOption('google'); @@ -198,7 +227,7 @@ class GoogleProvider extends DynamicProvider { }); } - _init() { + _init(): Promise { return new Promise((resolve) => { this._resolveLocation(this._option('center')).then((center) => { const disableDefaultUI = !this._option('controls'); @@ -214,7 +243,7 @@ class GoogleProvider extends DynamicProvider { const listener = google.maps.event.addListener(this._map, 'idle', () => { resolve(listener); }); - }); + }).catch(() => { }); }).then((listener) => { google.maps.event.removeListener(listener); }); @@ -237,11 +266,13 @@ class GoogleProvider extends DynamicProvider { } } - _clickActionHandler(e) { + _clickActionHandler( + e: { latLng: GoogleLocation; domEvent: Event }, + ): void { this._fireClickAction({ location: this._normalizeLocation(e.latLng), event: e.domEvent }); } - updateDimensions() { + updateDimensions(): Promise { const center = this._option('center'); google.maps.event.trigger(this._map, 'resize'); this._option('center', center); @@ -249,39 +280,41 @@ class GoogleProvider extends DynamicProvider { return this.updateCenter(); } - updateMapType() { - this._map.setMapTypeId(this._mapType(this._option('type'))); + updateMapType(): Promise { + const type = this._option('type'); + this._map.setMapTypeId(this._mapType(type)); return Promise.resolve(); } - updateBounds() { + updateBounds(): Promise { + const bounds = this._option('bounds'); return Promise.all([ - this._resolveLocation(this._option('bounds.northEast')), - this._resolveLocation(this._option('bounds.southWest')), + this._resolveLocation(bounds?.northEast), + this._resolveLocation(bounds?.southWest), ]).then((result) => { - const bounds = new google.maps.LatLngBounds(); - bounds.extend(result[0]); - bounds.extend(result[1]); + const mapBounds = new google.maps.LatLngBounds(); + mapBounds.extend(result[0]); + mapBounds.extend(result[1]); - this._map.fitBounds(bounds); + this._map.fitBounds(mapBounds); }); } - updateCenter() { + updateCenter(): Promise { return this._resolveLocation(this._option('center')).then((center) => { this._map.setCenter(center); this._option('center', this._normalizeLocation(center)); }); } - updateZoom() { + updateZoom(): Promise { this._map.setZoom(this._option('zoom')); return Promise.resolve(); } - updateControls() { + updateControls(): Promise { const showDefaultUI = this._option('controls'); this._map.setOptions({ @@ -291,17 +324,19 @@ class GoogleProvider extends DynamicProvider { return Promise.resolve(); } - isEventsCanceled(e): boolean { + isEventsCanceled(e: DxEvent): boolean { const gestureHandling = this._map?.get('gestureHandling'); const isInfoWindowContent = $(e.target).closest(`.${INFO_WINDOW_CLASS}`).length > 0; - if (isInfoWindowContent || devices.real().deviceType !== 'desktop' && gestureHandling === 'cooperative') { + if (isInfoWindowContent || (devices.real().deviceType !== 'desktop' && gestureHandling === 'cooperative')) { return false; } return super.isEventsCanceled(e); } - _renderMarker(options) { - return this._resolveLocation(options.location).then((location) => { + _renderMarker(options: MarkerOptions): Promise { + const { location: markerLocation } = options; + return this._resolveLocation(markerLocation).then((location) => { + // eslint-disable-next-line @typescript-eslint/init-declarations let marker; if (options.html) { marker = new CustomMarker({ @@ -316,6 +351,7 @@ class GoogleProvider extends DynamicProvider { } else { const providerConfig = this._option('providerConfig'); const useAdvancedMarkers = providerConfig?.useAdvancedMarkers ?? true; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const icon = options.iconSrc || this._option('markerIconSrc'); if (useAdvancedMarkers) { const content = icon ? this._createIconTemplate(icon) : undefined; @@ -333,10 +369,15 @@ class GoogleProvider extends DynamicProvider { } } - const infoWindow = this._renderTooltip(marker, options.tooltip); + const infoWindow = this._renderTooltip( + marker, + options.tooltip, + ) as { open: (map: unknown, marker: unknown) => void }; + + // eslint-disable-next-line @typescript-eslint/init-declarations let listener; if (options.onClick || options.tooltip) { - const markerClickAction = this._mapWidget._createAction(options.onClick || noop); + const markerClickAction = this._mapWidget._createAction(options.onClick ?? noop); const markerNormalizedLocation = this._normalizeLocation(location); listener = google.maps.event.addListener(marker, 'click', () => { @@ -358,82 +399,88 @@ class GoogleProvider extends DynamicProvider { }); } - _renderTooltip(marker, options) { + _renderTooltip(marker: unknown, options: MarkerOptions['tooltip']): unknown { if (!options) { - return; + return undefined; } - options = this._parseTooltipOptions(options); + const parsedOptions = this._parseTooltipOptions(options); const infoWindow = new google.maps.InfoWindow({ - content: options.text, + content: parsedOptions.text, }); - if (options.visible) { + if (parsedOptions.visible) { infoWindow.open(this._map, marker); } return infoWindow; } - _destroyMarker(marker) { + _destroyMarker(marker: { marker: { setMap: (arg: unknown) => void }; listener?: unknown }): void { marker.marker.setMap(null); if (marker.listener) { google.maps.event.removeListener(marker.listener); } } - _renderRoute(options) { - return Promise.all(map(options.locations, (point) => this._resolveLocation(point))).then((locations) => new Promise((resolve) => { - const origin = locations.shift(); - const destination = locations.pop(); - const waypoints = map(locations, (location) => ({ location, stopover: true })); - - const request = { - origin, - destination, - waypoints, - optimizeWaypoints: true, - travelMode: this._movementMode(options.mode), - }; - - new google.maps.DirectionsService().route(request, (response, status) => { - if (status === google.maps.DirectionsStatus.OK) { - const color = new Color(options.color || this._defaultRouteColor()).toHex(); - const directionOptions = { - directions: response, - map: this._map, - suppressMarkers: true, - preserveViewport: true, - polylineOptions: { - strokeWeight: options.weight || this._defaultRouteWeight(), - strokeOpacity: options.opacity || this._defaultRouteOpacity(), - strokeColor: color, - }, - }; - - const route = new google.maps.DirectionsRenderer(directionOptions); - const { bounds } = response.routes[0]; - - resolve({ - instance: route, - northEast: bounds.getNorthEast(), - southWest: bounds.getSouthWest(), - }); - } else { - errors.log('W1006', status); - resolve({ - instance: new google.maps.DirectionsRenderer({}), - }); - } - }); - })); + _renderRoute(options: RouteOptions): Promise { + const locations = options.locations ?? []; + return Promise + .all(locations.map((point) => this._resolveLocation(point))) + .then((resolvedLocations) => new Promise((resolve) => { + const origin = resolvedLocations.shift(); + const destination = resolvedLocations.pop(); + const waypoints = resolvedLocations.map((location) => ({ location, stopover: true })); + + const request = { + origin, + destination, + waypoints, + optimizeWaypoints: true, + travelMode: this._movementMode(options.mode ?? ''), + }; + + new google.maps.DirectionsService().route(request, (response, status) => { + if (status === google.maps.DirectionsStatus.OK) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const color = new Color(options.color || this._defaultRouteColor()).toHex(); + const directionOptions = { + directions: response, + map: this._map, + suppressMarkers: true, + preserveViewport: true, + polylineOptions: { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + strokeWeight: options.weight || this._defaultRouteWeight(), + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + strokeOpacity: options.opacity || this._defaultRouteOpacity(), + strokeColor: color, + }, + }; + + const route = new google.maps.DirectionsRenderer(directionOptions); + const { bounds } = response.routes[0]; + + resolve({ + instance: route, + northEast: bounds.getNorthEast(), + southWest: bounds.getSouthWest(), + }); + } else { + errors.log('W1006', status); + resolve({ + instance: new google.maps.DirectionsRenderer({}), + }); + } + }); + })); } - _destroyRoute(routeObject) { + _destroyRoute(routeObject: { instance: { setMap: (arg: unknown) => void } }): void { routeObject.instance.setMap(null); } - _fitBounds() { + _fitBounds(): Promise { this._updateBounds(); if (this._bounds && this._option('autoAdjust')) { @@ -455,7 +502,7 @@ class GoogleProvider extends DynamicProvider { return Promise.resolve(); } - _extendBounds(location) { + _extendBounds(location: { lat: () => number; lng: () => number }): void { if (this._bounds) { this._bounds.extend(location); } else { @@ -464,7 +511,7 @@ class GoogleProvider extends DynamicProvider { } } - clean() { + clean(): Promise { if (this._map) { google.maps.event.removeListener(this._boundsChangeListener); google.maps.event.removeListener(this._clickListener); @@ -482,7 +529,7 @@ class GoogleProvider extends DynamicProvider { /// #DEBUG // @ts-expect-error ts-error -GoogleProvider.remapConstant = function (newValue) { +GoogleProvider.remapConstant = (newValue: string): void => { GOOGLE_URL = newValue; }; diff --git a/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.ts b/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.ts index d0d22e2b19fa..6771ae268be9 100644 --- a/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.ts +++ b/packages/devextreme/js/__internal/ui/map/m_provider.dynamic.ts @@ -1,48 +1,99 @@ -/* eslint-disable @typescript-eslint/no-misused-promises */ import Class from '@js/core/class'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; -import { each, map } from '@js/core/utils/iterator'; +import type { DxEvent } from '@js/events'; +import type { RouteMode } from '@js/ui/map'; +import type Map from './m_map'; import Provider from './m_provider'; +import type { AzureLocation } from './m_provider.dynamic.azure'; +import type { BingLocation } from './m_provider.dynamic.bing'; +import type { GoogleLocation } from './m_provider.dynamic.google'; const MAP_MARKER_CLASS = 'dx-map-marker'; +export type LocationOption = string | [number, number] | { lat: number; lng: number }; + +export interface LocationCoords { + lat: number; + lng: number; +} + +export interface MarkerOptions { + iconSrc?: string; + location?: LocationCoords; + onClick?: () => {}; + tooltip?: string | { + isShown?: boolean; + text?: string; + }; + html?: string; + htmlOffset?: { top: number; left: number }; +} + +export interface MarkerObject { + marker: unknown; + location: unknown; + listener?: () => void; + handler?: () => void; + infoBox?: unknown; + popup?: unknown; +} + +export interface RouteOptions { + color?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + locations?: any[]; + mode?: RouteMode | string; + opacity?: number; + weight?: number; +} + +export interface RouteObject { + instance?: unknown; + northEast?: [number, number]; + southWest?: [number, number]; +} + class DynamicProvider extends Provider { + // eslint-disable-next-line @typescript-eslint/no-explicit-any _bounds?: any; - _markers!: any[]; + _markers!: (MarkerObject & { options: MarkerOptions })[]; - _routes!: any[]; + _routes!: (RouteObject & { options: RouteOptions })[]; - _geocodedLocations?: any; + _geocodedLocations: Record; - _mapsLoader?: any; + _mapsLoader?: Promise; - ctor(map, $container: dxElementWrapper): void { - this._geocodedLocations = {}; + constructor(map: Map, $container: dxElementWrapper) { + super(map, $container); - super.ctor(map, $container); + this._geocodedLocations = {}; } - _geocodeLocation(location) { + _geocodeLocation( + location: string, + ): Promise { return new Promise((resolve) => { const cache = this._geocodedLocations; const cachedLocation = cache[location]; if (cachedLocation) { resolve(cachedLocation); } else { - // @ts-expect-error ts-error - this._geocodeLocationImpl(location).then((geocodedLocation) => { - cache[location] = geocodedLocation; - resolve(geocodedLocation); - }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._geocodeLocationImpl(location) + .then((geocodedLocation) => { + cache[location] = geocodedLocation; + resolve(geocodedLocation); + }); } }); } - _renderImpl() { + _renderImpl(): Promise { return this._load().then(() => this._init()).then(() => Promise.all([ this.updateMapType(), this._areBoundsSet() ? this.updateBounds() : this.updateCenter(), @@ -50,17 +101,17 @@ class DynamicProvider extends Provider { this._attachHandlers(); // NOTE: setTimeout is needed by providers to correctly initialize bounds - return new Promise((resolve) => { + return new Promise((resolve) => { + // eslint-disable-next-line no-restricted-globals const timeout = setTimeout(() => { clearTimeout(timeout); - // @ts-expect-error resolve(); }); }); }); } - _load() { + _load(): Promise { if (!this._mapsLoader) { this._mapsLoader = this._loadImpl(); } @@ -71,29 +122,30 @@ class DynamicProvider extends Provider { return this._mapsLoader; } - _loadImpl() { - Class.abstract(); + _loadImpl(): Promise { + return Promise.resolve(); } - _init() { - Class.abstract(); + _init(): Promise { + return Promise.resolve(); } - _attachHandlers() { + _attachHandlers(): void { Class.abstract(); } - addMarkers(options) { - return Promise.all(map(options, (options) => this._addMarker(options))).then((markerObjects) => { - this._fitBounds(); + addMarkers(markers: MarkerOptions[]): Promise<[boolean, unknown[]]> { + return Promise + .all(markers.map((options) => this._addMarker(options))) + .then((markerObjects: MarkerObject[]) => { + this._fitBounds(); - return [false, map(markerObjects, (markerObject) => markerObject.marker)]; - }); + return [false, markerObjects.map((markerObject) => markerObject.marker)]; + }); } - _addMarker(options) { + _addMarker(options: MarkerOptions): Promise { return this._renderMarker(options) - // @ts-expect-error ts-error .then((markerObject) => { this._markers.push(extend({ options, @@ -109,43 +161,42 @@ class DynamicProvider extends Provider { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _renderMarker(options) { - Class.abstract(); + _renderMarker(options: MarkerOptions): Promise { + return Promise.resolve({ + marker: {}, + location: { lat: 0, lng: 0 }, + }); } - _createIconTemplate(iconSrc: string) { + _createIconTemplate(iconSrc: string): Element { const $img = $(''); $img.attr('src', iconSrc); $img.attr('alt', 'Marker icon'); $img.addClass(MAP_MARKER_CLASS); - return $img[0]; + return $img.get(0); } - removeMarkers(markersOptionsToRemove) { - const that = this; - - each(markersOptionsToRemove, (_, markerOptionToRemove) => { - that._removeMarker(markerOptionToRemove); + removeMarkers(markersOptionsToRemove: MarkerOptions[]): Promise { + markersOptionsToRemove.forEach((markerOptionToRemove) => { + this._removeMarker(markerOptionToRemove); }); return Promise.resolve(); } - _removeMarker(markersOptionToRemove) { - const that = this; - - each(this._markers, (markerIndex, markerObject) => { + _removeMarker(markersOptionToRemove: MarkerOptions): void { + this._markers.forEach((markerObject, markerIndex) => { if (markerObject.options !== markersOptionToRemove) { return true; } - that._destroyMarker(markerObject); + this._destroyMarker(markerObject); - that._markers.splice(markerIndex, 1); + this._markers.splice(markerIndex, 1); - that._fireMarkerRemovedAction({ + this._fireMarkerRemovedAction({ options: markerObject.options, }); @@ -154,27 +205,26 @@ class DynamicProvider extends Provider { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _destroyMarker(marker) { + _destroyMarker(marker: unknown): void { Class.abstract(); } - _clearMarkers() { + _clearMarkers(): void { while (this._markers.length > 0) { this._removeMarker(this._markers[0].options); } } - addRoutes(options) { - return Promise.all(map(options, (options) => this._addRoute(options))).then((routeObjects) => { + addRoutes(routes: RouteOptions[]): Promise<[boolean, unknown[]]> { + return Promise.all(routes.map((options) => this._addRoute(options))).then((routeObjects) => { this._fitBounds(); - return [false, map(routeObjects, (routeObject) => routeObject.instance)]; + return [false, routeObjects.map((routeObject) => routeObject.instance)]; }); } - _addRoute(options) { - // @ts-expect-error ts-error - return this._renderRoute(options).then((routeObject) => { + _addRoute(options: RouteOptions): Promise { + return this._renderRoute(options).then((routeObject: RouteObject) => { this._routes.push(extend({ options, }, routeObject)); @@ -188,34 +238,35 @@ class DynamicProvider extends Provider { }); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _renderRoute(options) { - Class.abstract(); + _renderRoute(options: RouteOptions): Promise { + return Promise.resolve({ + options, + instance: {}, + northEast: [0, 0], + southWest: [0, 0], + }); } - removeRoutes(options) { - const that = this; - - each(options, (routeIndex, options) => { - that._removeRoute(options); + removeRoutes(routes: RouteOptions[] = []): Promise { + routes.forEach((routeObject) => { + this._removeRoute(routeObject); }); return Promise.resolve(); } - _removeRoute(options) { - const that = this; - - each(this._routes, (routeIndex, routeObject) => { + _removeRoute(options: RouteOptions): void { + const routes = this._routes; + routes.forEach((routeObject, routeIndex) => { if (routeObject.options !== options) { return true; } - that._destroyRoute(routeObject); + this._destroyRoute(routeObject); - that._routes.splice(routeIndex, 1); + this._routes.splice(routeIndex, 1); - that._fireRouteRemovedAction({ + this._fireRouteRemovedAction({ options, }); @@ -224,50 +275,54 @@ class DynamicProvider extends Provider { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _destroyRoute(routeObject) { + _destroyRoute(routeObject: RouteObject): void { Class.abstract(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _geocodeLocationImpl(location) { - Class.abstract(); + _geocodeLocationImpl( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + location: string, + ): Promise { + return Promise.resolve([0, 0]); } - _clearRoutes() { + _clearRoutes(): void { while (this._routes.length > 0) { this._removeRoute(this._routes[0].options); } } - adjustViewport() { + adjustViewport(): void { return this._fitBounds(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - isEventsCanceled(e): boolean { + isEventsCanceled(e: DxEvent): boolean { return true; } - _fitBounds() { + _fitBounds(): void { Class.abstract(); } - _updateBounds() { - const that = this; - + _updateBounds(): void { this._clearBounds(); if (!this._option('autoAdjust')) { return; } - each(this._markers, (_, markerObject) => { - that._extendBounds(markerObject.location); + this._markers.forEach((markerObject) => { + this._extendBounds(markerObject.location); }); - each(this._routes, (_, routeObject) => { - routeObject.northEast && that._extendBounds(routeObject.northEast); - routeObject.southWest && that._extendBounds(routeObject.southWest); + this._routes.forEach((routeObject) => { + if (routeObject.northEast) { + this._extendBounds(routeObject.northEast); + } + if (routeObject.southWest) { + this._extendBounds(routeObject.southWest); + } }); } @@ -276,7 +331,7 @@ class DynamicProvider extends Provider { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _extendBounds(location): void { + _extendBounds(location: unknown): void { Class.abstract(); } } diff --git a/packages/devextreme/js/__internal/ui/map/m_provider.google_static.ts b/packages/devextreme/js/__internal/ui/map/m_provider.google_static.ts index 799005812004..27ee0828f5e0 100644 --- a/packages/devextreme/js/__internal/ui/map/m_provider.google_static.ts +++ b/packages/devextreme/js/__internal/ui/map/m_provider.google_static.ts @@ -2,16 +2,17 @@ import Color from '@js/color'; import { name as clickEventName } from '@js/common/core/events/click'; import eventsEngine from '@js/common/core/events/core/events_engine'; -import { each } from '@js/core/utils/iterator'; import { getHeight, getWidth } from '@js/core/utils/size'; import Provider from './m_provider'; +import type { LocationOption, MarkerOptions, RouteOptions } from './m_provider.dynamic'; let GOOGLE_STATIC_URL = 'https://maps.google.com/maps/api/staticmap?'; class GoogleStaticProvider extends Provider { - _locationToString(location) { + _locationToString(location: LocationOption): string { const latLng = this._getLatLng(location); + // eslint-disable-next-line @typescript-eslint/no-base-to-string return latLng ? `${latLng.lat},${latLng.lng}` : location.toString().replace(/ /g, '+'); } @@ -43,12 +44,10 @@ class GoogleStaticProvider extends Provider { return Promise.resolve(); } - addMarkers(options) { - const that = this; - + addMarkers(markers: MarkerOptions[] = []): Promise { return this._updateMap().then((result) => { - each(options, (_, options) => { - that._fireMarkerAddedAction({ + markers.forEach((options) => { + this._fireMarkerAddedAction({ options, }); }); @@ -56,12 +55,10 @@ class GoogleStaticProvider extends Provider { }); } - removeMarkers(options) { - const that = this; - + removeMarkers(markers: MarkerOptions[] = []): Promise { return this._updateMap().then((result) => { - each(options, (_, options) => { - that._fireMarkerRemovedAction({ + markers.forEach((options) => { + this._fireMarkerRemovedAction({ options, }); }); @@ -73,12 +70,10 @@ class GoogleStaticProvider extends Provider { return Promise.resolve(); } - addRoutes(options) { - const that = this; - + addRoutes(routes: RouteOptions[] = []): Promise { return this._updateMap().then((result) => { - each(options, (_, options) => { - that._fireRouteAddedAction({ + routes.forEach((options) => { + this._fireRouteAddedAction({ options, }); }); @@ -86,12 +81,10 @@ class GoogleStaticProvider extends Provider { }); } - removeRoutes(options) { - const that = this; - + removeRoutes(routes: RouteOptions[] = []): Promise { return this._updateMap().then((result) => { - each(options, (_, options) => { - that._fireRouteRemovedAction({ + routes.forEach((options) => { + this._fireRouteRemovedAction({ options, }); }); @@ -119,12 +112,14 @@ class GoogleStaticProvider extends Provider { const requestOptions = [ 'sensor=false', `size=${Math.round(getWidth($container))}x${Math.round(getHeight($container))}`, + `maptype=${this._option('type')}`, `center=${this._locationToString(this._option('center'))}`, + `zoom=${this._option('zoom')}`, this._markersSubstring(), ]; - requestOptions.push.apply(requestOptions, this._routeSubstrings()); + requestOptions.push(...this._routeSubstrings()); if (key) { requestOptions.push(`key=${key}`); } @@ -142,38 +137,36 @@ class GoogleStaticProvider extends Provider { return Promise.resolve(true); } - _markersSubstring() { - const that = this; - const markers = []; + _markersSubstring(): string { + const markers: string[] = []; const markerIcon = this._option('markerIconSrc'); - + const markersOption = this._option('markers') ?? []; if (markerIcon) { - // @ts-expect-error markers.push(`icon:${markerIcon}`); } - each(this._option('markers'), (_, marker) => { - // @ts-expect-error - markers.push(that._locationToString(marker.location)); + markersOption.forEach((marker) => { + markers.push(this._locationToString(marker.location)); }); return `markers=${markers.join('|')}`; } - _routeSubstrings() { - const that = this; - const routes = []; + _routeSubstrings(): string[] { + const routes: string[] = []; + const routesOptions = this._option('routes') ?? []; - each(this._option('routes'), (_, route) => { - const color = new Color(route.color || that._defaultRouteColor()).toHex().replace('#', '0x'); - const opacity = Math.round((route.opacity || that._defaultRouteOpacity()) * 255).toString(16); - const width = route.weight || that._defaultRouteWeight(); - const locations = []; - each(route.locations, (_, routePoint) => { - // @ts-expect-error - locations.push(that._locationToString(routePoint)); + routesOptions.forEach((route) => { + const color = new Color(route.color ?? this._defaultRouteColor()).toHex().replace('#', '0x'); + const opacity = Math.round((route.opacity ?? this._defaultRouteOpacity()) * 255).toString(16); + const width = route.weight ?? this._defaultRouteWeight(); + const locations: string[] = []; + const routeLocations = route.locations ?? []; + + routeLocations.forEach((routePoint) => { + locations.push(this._locationToString(routePoint)); }); - // @ts-expect-error + routes.push(`path=color:${color}${opacity}|weight:${width}|${locations.join('|')}`); }); @@ -181,19 +174,18 @@ class GoogleStaticProvider extends Provider { } _attachClickEvent(): void { - const that = this; const eventName = this._addEventNamespace(clickEventName); eventsEngine.off(this._$container, eventName); eventsEngine.on(this._$container, eventName, (e) => { - that._fireClickAction({ event: e }); + this._fireClickAction({ event: e }); }); } } /// #DEBUG // @ts-expect-error ts-error -GoogleStaticProvider.remapConstant = function (newValue) { +GoogleStaticProvider.remapConstant = (newValue: string): void => { GOOGLE_STATIC_URL = newValue; }; diff --git a/packages/devextreme/js/__internal/ui/map/m_provider.ts b/packages/devextreme/js/__internal/ui/map/m_provider.ts index 92aab333cb76..3929892d8eee 100644 --- a/packages/devextreme/js/__internal/ui/map/m_provider.ts +++ b/packages/devextreme/js/__internal/ui/map/m_provider.ts @@ -1,17 +1,28 @@ import { addNamespace } from '@js/common/core/events/utils/index'; import Class from '@js/core/class'; import type { dxElementWrapper } from '@js/core/renderer'; -import { map } from '@js/core/utils/iterator'; import { isNumeric, isPlainObject } from '@js/core/utils/type'; +import type { DxEvent } from '@js/events'; +import type { MapProvider } from '@js/ui/map'; +import { isDefined } from '@ts/core/utils/m_type'; -// @ts-expect-error dxClass inheritance issue -class Provider extends (Class.inherit({}) as new() => {}) { - _mapWidget?: any; +import type Map from './m_map'; +import type { MapProperties } from './m_map'; +import type { LocationOption, MarkerOptions, RouteOptions } from './m_provider.dynamic'; +class Provider { + _mapWidget!: Map; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any _map?: any; _$container!: dxElementWrapper; + constructor(map: Map, $container: dxElementWrapper) { + this._mapWidget = map; + this._$container = $container; + } + _defaultRouteWeight(): number { return 5; } @@ -24,21 +35,18 @@ class Provider extends (Class.inherit({}) as new() => {}) { return '#0000FF'; } - ctor(map, $container: dxElementWrapper): void { - this._mapWidget = map; - this._$container = $container; - } - - render(markerOptions, routeOptions) { - // @ts-expect-error ts-error + render( + markerOptions: MarkerOptions[], + routeOptions: RouteOptions[], + ): Promise { return this._renderImpl().then(() => Promise.all([ this._applyFunctionIfNeeded('addMarkers', markerOptions), this._applyFunctionIfNeeded('addRoutes', routeOptions), ]).then(() => true)); } - _renderImpl(): void { - Class.abstract(); + _renderImpl(): Promise { + return Promise.resolve(); } updateDimensions(): void { @@ -65,141 +73,193 @@ class Provider extends (Class.inherit({}) as new() => {}) { Class.abstract(); } - updateControls(): void { - Class.abstract(); - } - - updateMarkers(markerOptionsToRemove, markerOptionsToAdd) { - // eslint-disable-next-line no-promise-executor-return - return new Promise((resolve) => this._applyFunctionIfNeeded('removeMarkers', markerOptionsToRemove).then((removeValue) => { - this._applyFunctionIfNeeded('addMarkers', markerOptionsToAdd).then((addValue) => { - resolve(addValue || removeValue); - }); - })); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateControls(markers: MarkerOptions[], routes: RouteOptions[]): Promise { + return Promise.resolve(); + } + + updateMarkers( + markerOptionsToRemove: MarkerOptions[], + markerOptionsToAdd: MarkerOptions[], + ): Promise { + return new Promise((resolve) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._applyFunctionIfNeeded('removeMarkers', markerOptionsToRemove) + .then((removeValue) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._applyFunctionIfNeeded('addMarkers', markerOptionsToAdd) + .then((addValue) => { + resolve(addValue || removeValue); + }); + }); + }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - addMarkers(options): void { - Class.abstract(); + addMarkers(options: MarkerOptions[]): Promise { + return Promise.resolve(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - removeMarkers(options): void { - Class.abstract(); + removeMarkers(options: MarkerOptions[]): Promise { + return Promise.resolve(); } adjustViewport(): void { Class.abstract(); } - updateRoutes(routeOptionsToRemove, routeOptionsToAdd) { - // eslint-disable-next-line no-promise-executor-return - return new Promise((resolve) => this._applyFunctionIfNeeded('removeRoutes', routeOptionsToRemove).then((removeValue) => { - this._applyFunctionIfNeeded('addRoutes', routeOptionsToAdd).then((addValue) => { - resolve(addValue || removeValue); - }); - })); + updateRoutes( + routeOptionsToRemove: RouteOptions[], + routeOptionsToAdd: RouteOptions[], + ): Promise { + return new Promise((resolve) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._applyFunctionIfNeeded('removeRoutes', routeOptionsToRemove) + .then((removeValue) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._applyFunctionIfNeeded('addRoutes', routeOptionsToAdd) + .then((addValue) => { + resolve(addValue || removeValue); + }); + }); + }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - addRoutes(options) { - Class.abstract(); + addRoutes(options: RouteOptions[]): Promise { + return Promise.resolve(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - removeRoutes(options) { - Class.abstract(); + removeRoutes(options: RouteOptions[]): Promise { + return Promise.resolve(); } clean(): void { Class.abstract(); } - map() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + map(): any { return this._map; } // eslint-disable-next-line @typescript-eslint/no-unused-vars - isEventsCanceled(e) { + isEventsCanceled(e: DxEvent): boolean { return false; } - _option(name?, value?) { + _option(name: K): MapProperties[K]; + _option(name: K, value: MapProperties[K]): undefined; + _option( + name: K, + value?: MapProperties[K], + ): MapProperties[K] | undefined { if (value === undefined) { - return this._mapWidget.option(name); + const mapOptions = this._mapWidget.option(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return mapOptions[name]; } this._mapWidget.setOptionSilent(name, value); - } - _keyOption(providerName) { - const key = this._option('apiKey'); + return undefined; + } - return key[providerName] === undefined ? key : key[providerName]; + _keyOption(providerName: MapProvider): string { + const key = this._option('apiKey') ?? ''; + if (typeof key === 'string') { + return key; + } + if (isPlainObject(key)) { + return key[providerName] ?? ''; + } + return ''; } - _parseTooltipOptions(option) { + _parseTooltipOptions( + option: string | { text?: string; isShown?: boolean }, + ): { text: string; visible: boolean } { + const isStringOption = typeof option === 'string'; + return { - text: option.text || option, - visible: option.isShown || false, + text: isStringOption ? option : option.text ?? '', + visible: isStringOption ? false : option.isShown ?? false, }; } - _getLatLng(location) { + _getLatLng(location?: LocationOption | null): { lat: number; lng: number } | null { if (typeof location === 'string') { - const coords = map(location.split(','), (item) => item.trim()); + const coords = location.split(',').map((item) => item.trim()); const numericRegex = /^[-+]?[0-9]*\.?[0-9]*$/; - if (coords.length === 2 && coords[0].match(numericRegex) && coords[1].match(numericRegex)) { + if (coords.length === 2 && numericRegex.exec(coords[0]) && numericRegex.exec(coords[1])) { return { lat: parseFloat(coords[0]), lng: parseFloat(coords[1]) }; } } else if (Array.isArray(location) && location.length === 2) { return { lat: location[0], lng: location[1] }; } else if (isPlainObject(location) && isNumeric(location.lat) && isNumeric(location.lng)) { - return location; + return location as { lat: number; lng: number }; } return null; } - _areBoundsSet() { - return this._option('bounds.northEast') && this._option('bounds.southWest'); + _areBoundsSet(): boolean { + const bounds = this._option('bounds'); + + return isDefined(bounds?.northEast) && isDefined(bounds?.southWest); } - _addEventNamespace(name) { + _addEventNamespace(name: string): string { + // @ts-expect-error ts-error return addNamespace(name, this._mapWidget.NAME); } - _applyFunctionIfNeeded(fnName, array) { + _applyFunctionIfNeeded( + fnName: 'addMarkers' | 'removeMarkers' | 'addRoutes' | 'removeRoutes', + array: MarkerOptions[] | RouteOptions[], + ): Promise { if (!array.length) { return Promise.resolve(); } - return this[fnName](array); - } - - _fireAction(name, actionArguments): void { - this._mapWidget._createActionByOption(name)(actionArguments); + const isMarkersUpdate = fnName === 'addMarkers' || fnName === 'removeMarkers'; + if (isMarkersUpdate) { + return this[fnName](array as MarkerOptions[]); + } + return this[fnName](array as RouteOptions[]); } - _fireClickAction(actionArguments): void { - this._fireAction('onClick', actionArguments); + _fireClickAction( + actionArguments: { location?: { lat: number; lng: number }; event?: Event }, + ): void { + this._mapWidget._createActionByOption('onClick')(actionArguments); } - _fireMarkerAddedAction(actionArguments): void { - this._fireAction('onMarkerAdded', actionArguments); + _fireMarkerAddedAction( + actionArguments: { options: MarkerOptions; originalMarker?: unknown }, + ): void { + this._mapWidget._createActionByOption('onMarkerAdded')(actionArguments); } - _fireMarkerRemovedAction(actionArguments): void { - this._fireAction('onMarkerRemoved', actionArguments); + _fireMarkerRemovedAction( + actionArguments: { options: MarkerOptions; originalMarker?: unknown }, + ): void { + this._mapWidget._createActionByOption('onMarkerRemoved')(actionArguments); } - _fireRouteAddedAction(actionArguments): void { - this._fireAction('onRouteAdded', actionArguments); + _fireRouteAddedAction( + actionArguments: { options: RouteOptions; originalRoute?: unknown }, + ): void { + this._mapWidget._createActionByOption('onRouteAdded')(actionArguments); } - _fireRouteRemovedAction(actionArguments): void { - this._fireAction('onRouteRemoved', actionArguments); + _fireRouteRemovedAction( + actionArguments: { options: RouteOptions; originalRoute?: unknown }, + ): void { + this._mapWidget._createActionByOption('onRouteRemoved')(actionArguments); } } diff --git a/packages/devextreme/js/__internal/ui/menu/menu.ts b/packages/devextreme/js/__internal/ui/menu/menu.ts index 332187b47954..b96330148809 100644 --- a/packages/devextreme/js/__internal/ui/menu/menu.ts +++ b/packages/devextreme/js/__internal/ui/menu/menu.ts @@ -46,7 +46,7 @@ import type { } from '@ts/ui/context_menu/menu_base'; import MenuBase from '@ts/ui/context_menu/menu_base'; import type { InternalNode } from '@ts/ui/hierarchical_collection/data_converter'; -import { getElementMaxHeightByWindow } from '@ts/ui/overlay/m_utils'; +import { getElementMaxHeightByWindow } from '@ts/ui/overlay/utils'; import type { TreeViewBaseProperties } from '@ts/ui/tree_view/tree_view.base'; import type { SubmenuProperties } from './submenu'; @@ -956,7 +956,7 @@ class Menu extends MenuBase { return undefined; } - return Submenu.getInstance($submenu) as Submenu; + return Submenu.getInstance($submenu); } getSubmenuPosition($rootItem: dxElementWrapper): PositionConfig { diff --git a/packages/devextreme/js/__internal/ui/multi_view/multi_view.ts b/packages/devextreme/js/__internal/ui/multi_view/multi_view.ts index 4ec16760dab0..fa6ded303e23 100644 --- a/packages/devextreme/js/__internal/ui/multi_view/multi_view.ts +++ b/packages/devextreme/js/__internal/ui/multi_view/multi_view.ts @@ -72,6 +72,10 @@ class MultiView< _itemWidthValue?: number; + protected _activeStateUnit(): string { + return `.${MULTIVIEW_ITEM_CLASS}`; + } + _supportedKeys(): Record void> { return { ...super._supportedKeys(), @@ -187,8 +191,6 @@ class MultiView< _init(): void { super._init(); - this._activeStateUnit = `.${MULTIVIEW_ITEM_CLASS}`; - const $element = this.$element(); $element.addClass(MULTIVIEW_CLASS); diff --git a/packages/devextreme/js/__internal/ui/notify.ts b/packages/devextreme/js/__internal/ui/notify.ts new file mode 100644 index 000000000000..f3ab8cf28fc3 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/notify.ts @@ -0,0 +1,275 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { isPlainObject, isString } from '@js/core/utils/type'; +import { value as viewPort } from '@js/core/utils/view_port'; +import { getWindow } from '@js/core/utils/window'; +import type { + HiddenEvent, + Properties as ToastProperties, + ShowingEvent, +} from '@js/ui/toast'; +import Toast from '@js/ui/toast'; + +type DefaultDirection = 'down-push' | 'up-push'; + +type NotifyType = 'info' | 'warning' | 'error' | 'success'; + +interface Dimensions { + toastWidth: number; + toastHeight: number; + windowHeight: number; + windowWidth: number; +} + +interface NotifyCoordinates { + top?: number; + bottom?: number; + left?: number; + right?: number; +} + +interface PositionStyles { + top: number | string; + bottom: number | string; + left: number | string; + right: number | string; +} + +type CoordinateFunction = (dimensions: Dimensions) => NotifyCoordinates; +type PositionStylesFunction = ( + coordinates: NotifyCoordinates, + dimensions: Dimensions, +) => PositionStyles; + +const window = getWindow(); + +let $notify: dxElementWrapper | null = null; + +const $containers: Record = {}; + +const COORDINATE_ALIASES: Record = { + 'top left': { top: 10, left: 10 }, + 'top right': { top: 10, right: 10 }, + 'bottom left': { bottom: 10, left: 10 }, + 'bottom right': { bottom: 10, right: 10 }, + 'top center': (dimensions: Dimensions): NotifyCoordinates => ({ + top: 10, + left: Math.round(dimensions.windowWidth / 2 - dimensions.toastWidth / 2), + }), + 'left center': (dimensions: Dimensions): NotifyCoordinates => ({ + top: Math.round(dimensions.windowHeight / 2 - dimensions.toastHeight / 2), + left: 10, + }), + 'right center': (dimensions: Dimensions): NotifyCoordinates => ({ + top: Math.round(dimensions.windowHeight / 2 - dimensions.toastHeight / 2), + right: 10, + }), + center: (dimensions: Dimensions): NotifyCoordinates => ({ + top: Math.round(dimensions.windowHeight / 2 - dimensions.toastHeight / 2), + left: Math.round(dimensions.windowWidth / 2 - dimensions.toastWidth / 2), + }), + 'bottom center': (dimensions: Dimensions): NotifyCoordinates => ({ + bottom: 10, + left: Math.round(dimensions.windowWidth / 2 - dimensions.toastWidth / 2), + }), +}; + +const POSITION_STYLES_MAP: Record = { + up: (coordinates: NotifyCoordinates, dimensions: Dimensions): PositionStyles => ({ + bottom: coordinates.bottom + ?? dimensions.windowHeight - dimensions.toastHeight - (coordinates.top ?? 0), + top: '', + left: coordinates.left ?? '', + right: coordinates.right ?? '', + }), + down: (coordinates: NotifyCoordinates, dimensions: Dimensions): PositionStyles => ({ + top: coordinates.top + ?? dimensions.windowHeight - dimensions.toastHeight - (coordinates.bottom ?? 0), + bottom: '', + left: coordinates.left ?? '', + right: coordinates.right ?? '', + }), + left: (coordinates: NotifyCoordinates, dimensions: Dimensions): PositionStyles => ({ + right: coordinates.right + ?? dimensions.windowWidth - dimensions.toastWidth - (coordinates.left ?? 0), + left: '', + top: coordinates.top ?? '', + bottom: coordinates.bottom ?? '', + }), + right: (coordinates: NotifyCoordinates, dimensions: Dimensions): PositionStyles => ({ + left: coordinates.left + ?? dimensions.windowWidth - dimensions.toastWidth - (coordinates.right ?? 0), + right: '', + top: coordinates.top ?? '', + bottom: coordinates.bottom ?? '', + }), +}; + +const getDefaultDirection = (position: string | NotifyCoordinates): DefaultDirection => { + const condition = isString(position) && position.includes('top'); + + return condition ? 'down-push' : 'up-push'; +}; + +const createStackContainer = (key: string): dxElementWrapper => { + const $container = $('
').appendTo(viewPort()); + + $containers[key] = $container; + + return $container; +}; + +const getStackContainer = (key: string): dxElementWrapper => { + const $container = $containers[key]; + + return $container || createStackContainer(key); +}; + +const setContainerClasses = (container: dxElementWrapper, direction: string): void => { + const containerClasses = `dx-toast-stack dx-toast-stack-${direction}-direction`; + container.removeAttr('class').addClass(containerClasses); +}; + +const getNotifyCoordinatesByAlias = (alias: string, dimensions: Dimensions): NotifyCoordinates => { + const coordinate = alias + ? COORDINATE_ALIASES[alias] + : COORDINATE_ALIASES['bottom center']; + + return typeof coordinate === 'function' ? coordinate(dimensions) : coordinate; +}; + +const getPositionStylesByNotifyCoordinates = ( + direction: string, + coordinates: NotifyCoordinates, + dimensions: Dimensions, +): PositionStyles => { + const directionKey = direction.replace(/-push|-stack/g, ''); + const styleFunction = POSITION_STYLES_MAP[directionKey]; + + return styleFunction ? styleFunction(coordinates, dimensions) : { + top: '', + bottom: '', + left: '', + right: '', + }; +}; + +const setContainerStyles = ( + container: dxElementWrapper, + direction: string, + position: string | NotifyCoordinates, +): void => { + const { + // @ts-expect-error 'offsetWidth' does not exist on type 'Element' + offsetWidth: toastWidth, + // @ts-expect-error 'offsetHeight' does not exist on type 'Element' + offsetHeight: toastHeight, + } = container.children().first().get(0); + + const dimensions: Dimensions = { + toastWidth, + toastHeight, + windowHeight: window.innerHeight, + windowWidth: window.innerWidth, + }; + + const coordinates = isString(position) + ? getNotifyCoordinatesByAlias(position, dimensions) + : position; + + const styles = getPositionStylesByNotifyCoordinates(direction, coordinates, dimensions); + + container.css(styles); +}; + +const getToastOptions = ( + message: ToastProperties | string, + typeOrStack?: NotifyType, + displayTime?: number, +): ToastProperties => { + const userOptions = isPlainObject(message) ? message : { message }; + + const stack = isPlainObject(typeOrStack) ? typeOrStack : undefined; + const type = isPlainObject(typeOrStack) ? undefined : typeOrStack; + + const { + onHidden: userOnHidden, + onShowing: userOnShowing, + } = userOptions; + + const defaultConfiguration: Partial = { + onHidden: (e: HiddenEvent): void => { + $(e.element).remove(); + userOnHidden?.(e); + }, + }; + + if (type !== undefined) { + defaultConfiguration.type = type; + } + + if (displayTime !== undefined) { + defaultConfiguration.displayTime = displayTime; + } + + if (stack?.position) { + const { position } = stack; + + const direction = stack.direction || getDefaultDirection(position); + + const containerKey = isString(position) + ? position + : `${position.top}-${position.left}-${position.bottom}-${position.right}`; + + const $container = getStackContainer(containerKey); + + setContainerClasses($container, direction); + + const options = { + ...userOptions, + ...defaultConfiguration, + container: $container, + _skipContentPositioning: true, + onShowing: (e: ShowingEvent): void => { + setContainerStyles($container, direction, position); + userOnShowing?.(e); + }, + }; + + return options; + } + + const options = { + ...userOptions, + ...defaultConfiguration, + }; + + return options; +}; + +const notify = ( + message: ToastProperties | string, + typeOrStack?: NotifyType, + displayTime?: number, +): void => { + const options = getToastOptions(message, typeOrStack, displayTime); + + $notify = $('
').appendTo(viewPort()); + + // @ts-expect-error Toast constructor accepts jQuery element + const toast = new Toast($notify, options); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + toast.show(); +}; + +/// #DEBUG +Object.setPrototypeOf(notify, { + _resetContainers() { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + Object.keys($containers).forEach((key) => delete $containers[key]); + }, +}); +/// #ENDDEBUG + +export default notify; diff --git a/packages/devextreme/js/__internal/ui/overlay/overlay.ts b/packages/devextreme/js/__internal/ui/overlay/overlay.ts index 9453ede821e5..0ba5e7d73d13 100644 --- a/packages/devextreme/js/__internal/ui/overlay/overlay.ts +++ b/packages/devextreme/js/__internal/ui/overlay/overlay.ts @@ -10,6 +10,7 @@ import pointerEvents from '@js/common/core/events/pointer'; import { keyboard } from '@js/common/core/events/short'; import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '@js/common/core/events/utils'; import { triggerHidingEvent, triggerResizeEvent, triggerShownEvent } from '@js/common/core/events/visibility_change'; +import type { DeepPartial } from '@js/core'; import registerComponent from '@js/core/component_registrator'; import devices from '@js/core/devices'; import domAdapter from '@js/core/dom_adapter'; @@ -45,11 +46,11 @@ import type { ControllerOverlayElements, ControllerProperties, PositionControllerConstructor, -} from '@ts/ui/overlay/m_overlay_position_controller'; +} from '@ts/ui/overlay/overlay_position_controller'; import { OVERLAY_POSITION_ALIASES, OverlayPositionController, -} from '@ts/ui/overlay/m_overlay_position_controller'; +} from '@ts/ui/overlay/overlay_position_controller'; import * as zIndexPool from '@ts/ui/overlay/z_index'; const ready = readyCallbacks.add; @@ -72,7 +73,8 @@ const PREVENT_SAFARI_SCROLLING_CLASS = 'dx-prevent-safari-scrolling'; type AnimationDirection = 'to' | 'from'; -export type PointerLikeEvent = DxEvent; +type PointerLikeNativeEvents = MouseEvent | PointerEvent | TouchEvent; +export type PointerLikeEvent = DxEvent; type EventHandler = (e: PointerLikeEvent) => boolean | undefined; @@ -135,7 +137,7 @@ export type PositioningEvent< TPosition = OverlayProperties['position'], > = NativeEventInfo< Overlay, - PointerLikeEvent + PointerLikeNativeEvents > & { readonly position: TPosition; }; @@ -301,18 +303,22 @@ class Overlay< } _defaultOptionsRules(): DefaultOptionsRule[] { - return super._defaultOptionsRules().concat([{ - device(): boolean { - return !windowUtils.hasWindow(); - }, - // @ts-expect-error overload - options: { - width: null, - height: null, - animation: null, - _checkParentVisibility: false, + const rules = [ + ...super._defaultOptionsRules(), + { + device(): boolean { + return !windowUtils.hasWindow(); + }, + options: { + width: null, + height: null, + animation: null, + _checkParentVisibility: false, + } as DeepPartial, }, - }]); + ]; + + return rules; } _setOptionsByReference(): void { diff --git a/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts b/packages/devextreme/js/__internal/ui/overlay/overlay_position_controller.ts similarity index 97% rename from packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts rename to packages/devextreme/js/__internal/ui/overlay/overlay_position_controller.ts index 1c6372aaf7ec..d103cfb4d87c 100644 --- a/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/overlay/overlay_position_controller.ts @@ -10,7 +10,7 @@ import { isString, isWindow, } from '@js/core/utils/type'; -import swatch from '@ts/core/utils/m_swatch_container'; +import swatch from '@ts/core/utils/swatch_container'; import type { OverlayActions, OverlayProperties, @@ -210,9 +210,11 @@ export class OverlayPositionController< this._properties.container = element; } - this._$markupContainer = element - ? $(element) - : swatch.getSwatchContainer(this._$root); + if (element) { + this._$markupContainer = $(element); + } else if (this._$root) { + this._$markupContainer = swatch.getSwatchContainer(this._$root); + } this.updateVisualContainer(this._properties.visualContainer); } @@ -372,7 +374,6 @@ export class OverlayPositionController< return defaultConfiguration; } - // eslint-disable-next-line class-methods-use-this _positionToObject(position: TPosition): OverlayPosition { if (isPositionAlignment(position)) { const configuration: OverlayPosition = { diff --git a/packages/devextreme/js/__internal/ui/overlay/m_utils.ts b/packages/devextreme/js/__internal/ui/overlay/utils.ts similarity index 100% rename from packages/devextreme/js/__internal/ui/overlay/m_utils.ts rename to packages/devextreme/js/__internal/ui/overlay/utils.ts diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover.ts b/packages/devextreme/js/__internal/ui/popover/m_popover.ts index e3d847c2d0dd..0b683bb5b9bc 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover.ts @@ -26,11 +26,11 @@ import type { PopoverControllerElements, PopoverControllerProperties, PopoverPositionControllerConstructor, -} from './m_popover_position_controller'; +} from './popover_position_controller'; import { POPOVER_POSITION_ALIASES, PopoverPositionController, -} from './m_popover_position_controller'; +} from './popover_position_controller'; // STYLE popover diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts b/packages/devextreme/js/__internal/ui/popover/popover_position_controller.ts similarity index 94% rename from packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts rename to packages/devextreme/js/__internal/ui/popover/popover_position_controller.ts index 2b3cbcdbaf8e..db8f2ab3d4c8 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/popover/popover_position_controller.ts @@ -9,13 +9,13 @@ import { isDefined, isString } from '@js/core/utils/type'; import type { ControllerOverlayElements, OverlayPosition, -} from '@ts/ui/overlay/m_overlay_position_controller'; -import { OverlayPositionController } from '@ts/ui/overlay/m_overlay_position_controller'; +} from '@ts/ui/overlay/overlay_position_controller'; +import { OverlayPositionController } from '@ts/ui/overlay/overlay_position_controller'; import type { PopoverProperties } from '@ts/ui/popover/m_popover'; import type { PopupControllerProperties, PopupPositionControllerConstructor, -} from '@ts/ui/popup/m_popup_position_controller'; +} from '@ts/ui/popup/popup_position_controller'; import { borderWidthStyles } from '@ts/ui/resizable/utils'; export interface PopoverControllerElements extends ControllerOverlayElements { @@ -124,7 +124,6 @@ export class PopoverPositionController< this.updatePosition(this._properties.position); } - // eslint-disable-next-line class-methods-use-this _renderBoundaryOffset(): void {} _getContainerPosition(): PopoverPosition { @@ -184,7 +183,6 @@ export class PopoverPositionController< return side === 'left' || side === 'right'; } - // eslint-disable-next-line class-methods-use-this _getDisplaySide(position: PopoverPosition): CommonPosition { const my = positionUtils.setup.normalizeAlign(position.my); const at = positionUtils.setup.normalizeAlign(position.at); @@ -220,7 +218,6 @@ export class PopoverPositionController< return resultPosition; } - // eslint-disable-next-line class-methods-use-this _positionToObject(position: TPosition): PopoverPosition { if (isCommonPosition(position)) { const configuration = { diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup.ts b/packages/devextreme/js/__internal/ui/popup/m_popup.ts index bb90b9dc3314..ceedf2d06553 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup.ts +++ b/packages/devextreme/js/__internal/ui/popup/m_popup.ts @@ -44,12 +44,12 @@ import type Toolbar from '@js/ui/toolbar'; import windowUtils from '@ts/core/utils/m_window'; import type { OptionChanged } from '@ts/core/widget/types'; import type { SupportedKeys } from '@ts/core/widget/widget'; +import type { GeometryOptions, OverlayActions } from '@ts/ui/overlay/overlay'; +import Overlay from '@ts/ui/overlay/overlay'; import type { ControllerOverlayElements, ControllerProperties, -} from '@ts/ui/overlay/m_overlay_position_controller'; -import type { GeometryOptions, OverlayActions } from '@ts/ui/overlay/overlay'; -import Overlay from '@ts/ui/overlay/overlay'; +} from '@ts/ui/overlay/overlay_position_controller'; import * as zIndexPool from '@ts/ui/overlay/z_index'; import { TOOLBAR_CLASS } from '@ts/ui/toolbar/constants'; import type { ToolbarBaseProperties } from '@ts/ui/toolbar/toolbar.base'; @@ -60,8 +60,8 @@ import { createBodyOverflowManager } from './m_popup_overflow_manager'; import type { PopupControllerProperties, PopupPositionControllerConstructor, -} from './m_popup_position_controller'; -import { PopupPositionController } from './m_popup_position_controller'; +} from './popup_position_controller'; +import { PopupPositionController } from './popup_position_controller'; // STYLE popup @@ -520,7 +520,8 @@ class Popup< this._$topToolbar?.toggleClass(POPUP_HAS_CLOSE_BUTTON_CLASS, this._hasCloseButton()); } else { - this._$topToolbar?.detach(); + this._$topToolbar?.remove(); + this._$topToolbar = undefined; } this._toggleAriaLabel(); @@ -556,7 +557,8 @@ class Popup< const items = this._getToolbarItems('bottom'); if (!items.length) { - this._$bottomToolbar?.detach(); + this._$bottomToolbar?.remove(); + this._$bottomToolbar = undefined; return; } diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts b/packages/devextreme/js/__internal/ui/popup/popup_position_controller.ts similarity index 97% rename from packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts rename to packages/devextreme/js/__internal/ui/popup/popup_position_controller.ts index 875caf072a74..a1cd2b5eae4b 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/popup/popup_position_controller.ts @@ -8,8 +8,8 @@ import type { OverlayPosition, Position, PositionControllerConstructor, -} from '@ts/ui/overlay/m_overlay_position_controller'; -import { OverlayPositionController } from '@ts/ui/overlay/m_overlay_position_controller'; +} from '@ts/ui/overlay/overlay_position_controller'; +import { OverlayPositionController } from '@ts/ui/overlay/overlay_position_controller'; import type { PopupProperties } from '@ts/ui/popup/m_popup'; import windowUtils from '../../core/utils/m_window'; diff --git a/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts b/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts index 61d76184157c..048e6cfe8330 100644 --- a/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts +++ b/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts @@ -32,10 +32,21 @@ class RadioGroup extends Editor { private _$submitElement!: dxElementWrapper; - _dataSourceOptions() { + // eslint-disable-next-line class-methods-use-this + _dataSourceOptions(): { paginate: boolean } { return { paginate: false }; } + // eslint-disable-next-line class-methods-use-this + protected _activeStateUnit(): string { + return `.${RADIO_BUTTON_CLASS}`; + } + + // eslint-disable-next-line class-methods-use-this + protected _feedbackHideTimeout(): number { + return RADIO_FEEDBACK_HIDE_TIMEOUT; + } + _defaultOptionsRules(): DefaultOptionsRule[] { const defaultOptionsRules = super._defaultOptionsRules(); @@ -88,8 +99,6 @@ class RadioGroup extends Editor { _init(): void { super._init(); - this._activeStateUnit = `.${RADIO_BUTTON_CLASS}`; - this._feedbackHideTimeout = RADIO_FEEDBACK_HIDE_TIMEOUT; // @ts-expect-error this._initDataExpressions(); } diff --git a/packages/devextreme/js/__internal/ui/scroll_view/scroll_view.ts b/packages/devextreme/js/__internal/ui/scroll_view/scroll_view.ts index 61f5db8f13c9..96937d0cf73e 100644 --- a/packages/devextreme/js/__internal/ui/scroll_view/scroll_view.ts +++ b/packages/devextreme/js/__internal/ui/scroll_view/scroll_view.ts @@ -11,7 +11,7 @@ import LoadIndicator from '@js/ui/load_indicator'; import type { Properties } from '@js/ui/scroll_view'; import { current, isMaterialBased } from '@js/ui/themes'; import type { OptionChanged } from '@ts/core/widget/types'; -import LoadPanel from '@ts/ui/m_load_panel'; +import LoadPanel from '@ts/ui/load_panel'; import PullDownStrategy from '@ts/ui/scroll_view/scroll_view.native.pull_down'; import SwipeDownStrategy from '@ts/ui/scroll_view/scroll_view.native.swipe_down'; import SimulatedStrategy from '@ts/ui/scroll_view/scroll_view.simulated'; diff --git a/packages/devextreme/js/__internal/ui/selection/m_selection.strategy.deferred.ts b/packages/devextreme/js/__internal/ui/selection/m_selection.strategy.deferred.ts deleted file mode 100644 index 046985478215..000000000000 --- a/packages/devextreme/js/__internal/ui/selection/m_selection.strategy.deferred.ts +++ /dev/null @@ -1,358 +0,0 @@ -import dataQuery from '@js/common/data/query'; -import type { DeferredObj } from '@js/core/utils/deferred'; -import { Deferred } from '@js/core/utils/deferred'; -import { isString } from '@js/core/utils/type'; -import errors from '@js/ui/widget/ui.errors'; - -import SelectionStrategy from './m_selection.strategy'; - -export default class DeferredStrategy extends SelectionStrategy { - getSelectedItems() { - return this._loadFilteredData(this.options.selectionFilter); - } - - getSelectedItemKeys() { - const d = Deferred(); - const that = this; - const key = this.options.key(); - const select = isString(key) ? [key] : key; - - this._loadFilteredData(this.options.selectionFilter, null, select).done((items) => { - // @ts-expect-error - const keys = items.map((item) => that.options.keyOf(item)); - - d.resolve(keys); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - }).fail(d.reject); - - return d.promise(); - } - - selectedItemKeys(keys, preserve, isDeselect, isSelectAll) { - if (isSelectAll) { - const filter = this.options.filter(); - const needResetSelectionFilter = !filter || JSON.stringify(filter) === JSON.stringify(this.options.selectionFilter) && isDeselect; - - if (needResetSelectionFilter) { - this._setOption('selectionFilter', isDeselect ? [] : null); - } else { - this._addSelectionFilter(isDeselect, filter, isSelectAll); - } - } else { - if (!preserve) { - this._setOption('selectionFilter', []); - } - - for (let i = 0; i < keys.length; i++) { - if (isDeselect) { - this.removeSelectedItem(keys[i]); - } else { - this.addSelectedItem(keys[i], isSelectAll, !preserve); - } - } - } - - this.onSelectionChanged(); - - return Deferred().resolve(); - } - - setSelectedItems(keys) { - this._setOption('selectionFilter', null); - for (let i = 0; i < keys.length; i++) { - this.addSelectedItem(keys[i]); - } - } - - isItemDataSelected(itemData) { - return this.isItemKeySelected(itemData); - } - - isItemKeySelected(itemData) { - const { selectionFilter } = this.options; - - if (!selectionFilter) { - return true; - } - - const queryParams = this._getQueryParams(); - - // @ts-expect-error - return !!dataQuery([itemData], queryParams).filter(selectionFilter).toArray().length; - } - - _getKeyExpr() { - const keyField = this.options.key(); - if (Array.isArray(keyField) && keyField.length === 1) { - return keyField[0]; - } - return keyField; - } - - _normalizeKey(key) { - const keyExpr = this.options.key(); - if (Array.isArray(keyExpr) && keyExpr.length === 1) { - return key[keyExpr[0]]; - } - return key; - } - - _getFilterByKey(key) { - const keyField = this._getKeyExpr(); - let filter = [keyField, '=', this._normalizeKey(key)]; - - if (Array.isArray(keyField)) { - filter = []; - for (let i = 0; i < keyField.length; i++) { - filter.push([keyField[i], '=', key[keyField[i]]]); - if (i !== keyField.length - 1) { - filter.push('and'); - } - } - } - - return filter; - } - - addSelectedItem(key, isSelectAll?: boolean, skipFilter?: boolean) { - const filter = this._getFilterByKey(key); - - this._addSelectionFilter(false, filter, isSelectAll, skipFilter); - } - - removeSelectedItem(key) { - const filter = this._getFilterByKey(key); - - this._addSelectionFilter(true, filter); - } - - validate() { - const { key } = this.options; - - if (key && key() === undefined) { - throw errors.Error('E1042', 'Deferred selection'); - } - } - - _findSubFilter(selectionFilter, filter) { - if (!selectionFilter) return -1; - const filterString = JSON.stringify(filter); - - for (let index = 0; index < selectionFilter.length; index++) { - const subFilter = selectionFilter[index]; - if (subFilter && JSON.stringify(subFilter) === filterString) { - return index; - } - } - - return -1; - } - - _isLastSubFilter(selectionFilter, filter) { - if (selectionFilter && filter) { - return this._findSubFilter(selectionFilter, filter) === selectionFilter.length - 1 || this._findSubFilter([selectionFilter], filter) === 0; - } - return false; - } - - _addFilterOperator(selectionFilter, filterOperator) { - if (selectionFilter.length > 1 && isString(selectionFilter[1]) && selectionFilter[1] !== filterOperator) { - selectionFilter = [selectionFilter]; - } - if (selectionFilter.length) { - selectionFilter.push(filterOperator); - } - return selectionFilter; - } - - _denormalizeFilter(filter) { - if (filter && isString(filter[0])) { - filter = [filter]; - } - return filter; - } - - _isOnlyNegativeFiltersLeft(filters) { - return filters.every((filterItem, i) => { - if (i % 2 === 0) { - return Array.isArray(filterItem) && filterItem[0] === '!'; - } - return filterItem === 'and'; - }); - } - - _addSelectionFilter( - isDeselect, - filter, - isSelectAll?: boolean, - skipFilter?: boolean, - ) { - const that = this; - const currentFilter = isDeselect ? ['!', filter] : filter; - const currentOperation = isDeselect ? 'and' : 'or'; - let needAddFilter = true; - let selectionFilter = that.options.selectionFilter || []; - - selectionFilter = that._denormalizeFilter(selectionFilter); - if (selectionFilter?.length && !skipFilter) { - const removedIndex = that._removeSameFilter(selectionFilter, filter, isDeselect, isSelectAll); - const filterIndex = that._removeSameFilter(selectionFilter, filter, !isDeselect); - - const shouldCleanFilter = isDeselect - && (removedIndex !== -1 || filterIndex !== -1) - && this._isOnlyNegativeFiltersLeft(selectionFilter); - - if (shouldCleanFilter) { - selectionFilter = []; - } - - const isKeyOperatorsAfterRemoved = this._isKeyFilter(filter) && this._hasKeyFiltersOnlyStartingFromIndex(selectionFilter, filterIndex); - - needAddFilter = filter.length && !isKeyOperatorsAfterRemoved; - } - - if (needAddFilter) { - selectionFilter = that._addFilterOperator(selectionFilter, currentOperation); - selectionFilter.push(currentFilter); - } - - selectionFilter = that._normalizeFilter(selectionFilter); - - that._setOption('selectionFilter', !isDeselect && !selectionFilter.length ? null : selectionFilter); - } - - _normalizeFilter(filter) { - if (filter && filter.length === 1) { - // eslint-disable-next-line prefer-destructuring - filter = filter[0]; - } - return filter; - } - - _removeFilterByIndex(filter, filterIndex, isSelectAll) { - const operation = filter[1]; - - if (filterIndex > 0) { - filter.splice(filterIndex - 1, 2); - } else { - filter.splice(filterIndex, 2); - } - - if (isSelectAll && operation === 'and') { - filter.splice(0, filter.length); - } - } - - _isSimpleKeyFilter(filter, key) { - return filter.length === 3 && filter[0] === key && filter[1] === '='; - } - - _isKeyFilter(filter) { - if (filter.length === 2 && filter[0] === '!') { - return this._isKeyFilter(filter[1]); - } - const keyField = this._getKeyExpr(); - - if (Array.isArray(keyField)) { - if (filter.length !== keyField.length * 2 - 1) { - return false; - } - for (let i = 0; i < keyField.length; i++) { - if (i > 0 && filter[i * 2 - 1] !== 'and') { - return false; - } - if (!this._isSimpleKeyFilter(filter[i * 2], keyField[i])) { - return false; - } - } - return true; - } - - return this._isSimpleKeyFilter(filter, keyField); - } - - _hasKeyFiltersOnlyStartingFromIndex(selectionFilter, filterIndex) { - if (filterIndex >= 0) { - for (let i = filterIndex; i < selectionFilter.length; i++) { - if (typeof selectionFilter[i] !== 'string' && !this._isKeyFilter(selectionFilter[i])) { - return false; - } - } - - return true; - } - - return false; - } - - _removeSameFilter(selectionFilter, filter, inverted, isSelectAll?: boolean) { - filter = inverted ? ['!', filter] : filter; - - if (JSON.stringify(filter) === JSON.stringify(selectionFilter)) { - selectionFilter.splice(0, selectionFilter.length); - return 0; - } - - const filterIndex = this._findSubFilter(selectionFilter, filter); - - if (filterIndex >= 0) { - this._removeFilterByIndex(selectionFilter, filterIndex, isSelectAll); - return filterIndex; - } - for (let i = 0; i < selectionFilter.length; i++) { - if (Array.isArray(selectionFilter[i]) && selectionFilter[i].length > 2) { - const filterIndex = this._removeSameFilter(selectionFilter[i], filter, false, isSelectAll); - if (filterIndex >= 0) { - if (!selectionFilter[i].length) { - this._removeFilterByIndex(selectionFilter, i, isSelectAll); - } else if (selectionFilter[i].length === 1) { - // eslint-disable-next-line prefer-destructuring - selectionFilter[i] = selectionFilter[i][0]; - } - return filterIndex; - } - } - } - return -1; - } - - getSelectAllState() { - const filter = this.options.filter(); - let { selectionFilter } = this.options; - - if (!selectionFilter) return true; - if (!selectionFilter.length) return false; - if (!filter || !filter.length) return undefined; - - selectionFilter = this._denormalizeFilter(selectionFilter); - - if (this._isLastSubFilter(selectionFilter, filter)) { - return true; - } - - if (this._isLastSubFilter(selectionFilter, ['!', filter])) { - return false; - } - - return undefined; - } - - loadSelectedItemsWithFilter() { - const componentFilter = this.options.filter(); - const { selectionFilter } = this.options; - - const filter = componentFilter - ? [componentFilter, 'and', selectionFilter] - : selectionFilter; - - return this._loadFilteredData(filter); - } - - _onePageSelectAll(isDeselect: boolean): DeferredObj { - this._selectAllPlainItems(isDeselect); - - this.onSelectionChanged(); - - return Deferred().resolve(); - } -} diff --git a/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts new file mode 100644 index 000000000000..d6e370a5d233 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts @@ -0,0 +1,406 @@ +import dataQuery from '@js/common/data/query'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { Deferred } from '@js/core/utils/deferred'; +import { isString } from '@js/core/utils/type'; +import errors from '@js/ui/widget/ui.errors'; +import SelectionStrategy from '@ts/ui/selection/selection.strategy'; +import type { KeyExpr, SelectionFilter, SelectionItem } from '@ts/ui/selection/types'; + +export default class DeferredStrategy< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey = any, +> extends SelectionStrategy { + getSelectedItems(): DeferredObj { + return this._loadFilteredData(this.options.selectionFilter); + } + + getSelectedItemKeys(): Promise { + const d = Deferred(); + const key = this.options.key(); + const select = isString(key) ? [key] : key; + const getKey = (item: TItem): TKey => this.options.keyOf(item); + + this._loadFilteredData(this.options.selectionFilter, null, select).done((items) => { + const keys = (Array.isArray(items) ? items : []).map(getKey); + + d.resolve(keys); + }).fail((error) => { + // @ts-expect-error error + d.reject(error); + }); + + return d.promise(); + } + + selectedItemKeys( + keys: TKey[], + preserve?: boolean, + isDeselect?: boolean, + isSelectAll?: boolean, + ): DeferredObj { + if (isSelectAll) { + const filter = this.options.filter(); + const needResetSelectionFilter = !filter + || (JSON.stringify(filter) === JSON.stringify(this.options.selectionFilter) && isDeselect); + + if (needResetSelectionFilter) { + this._setOption('selectionFilter', isDeselect ? [] : null); + } else { + this._addSelectionFilter(isDeselect, filter, isSelectAll); + } + } else { + if (!preserve) { + this._setOption('selectionFilter', []); + } + + keys.forEach((key) => { + if (isDeselect) { + this.removeSelectedItem(key); + } else { + this.addSelectedItem(key, isSelectAll, !preserve); + } + }); + } + + this.onSelectionChanged(); + + return Deferred().resolve(); + } + + setSelectedItems(keys: TKey[]): void { + this._setOption('selectionFilter', null); + keys.forEach((key) => { + this.addSelectedItem(key); + }); + } + + isItemDataSelected(itemData: TItem | TKey): boolean { + return this.isItemKeySelected(itemData); + } + + isItemKeySelected(itemData: TItem | TKey): boolean { + const { selectionFilter } = this.options; + + if (!selectionFilter) { + return true; + } + + const queryParams = this._getQueryParams(); + + // @ts-expect-error dataQuery + return !!dataQuery([itemData], queryParams).filter(selectionFilter).toArray().length; + } + + _getKeyExpr(): KeyExpr | Function | undefined { + const keyField = this.options.key(); + if (Array.isArray(keyField) && keyField.length === 1) { + return keyField[0]; + } + return keyField; + } + + _normalizeKey(key: TKey): TKey { + const keyExpr = this.options.key(); + if (Array.isArray(keyExpr) && keyExpr.length === 1) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return key[keyExpr[0]]; + } + return key; + } + + _getFilterByKey(key: TKey): SelectionFilter { + const keyField = this._getKeyExpr(); + let filter: SelectionFilter = [keyField, '=', this._normalizeKey(key)]; + + if (Array.isArray(keyField)) { + filter = []; + for (let i = 0; i < keyField.length; i += 1) { + filter.push([keyField[i], '=', key[keyField[i]]]); + if (i !== keyField.length - 1) { + filter.push('and'); + } + } + } + + return filter; + } + + addSelectedItem(key: TKey, isSelectAll?: boolean, skipFilter?: boolean): void { + const filter = this._getFilterByKey(key); + + this._addSelectionFilter(false, filter, isSelectAll, skipFilter); + } + + removeSelectedItem(key: TKey): void { + const filter = this._getFilterByKey(key); + + this._addSelectionFilter(true, filter); + } + + validate(): void { + const { key } = this.options; + + if (key && key() === undefined) { + throw errors.Error('E1042', 'Deferred selection'); + } + } + + _findSubFilter( + selectionFilter: SelectionFilter | undefined, + filter: SelectionFilter | undefined, + ): number { + if (!selectionFilter) return -1; + const filterString = JSON.stringify(filter); + + for (let index = 0; index < selectionFilter.length; index += 1) { + const subFilter = selectionFilter[index]; + if (subFilter && JSON.stringify(subFilter) === filterString) { + return index; + } + } + + return -1; + } + + _isLastSubFilter( + selectionFilter: SelectionFilter | undefined, + filter: SelectionFilter | undefined, + ): boolean { + if (selectionFilter && filter) { + return this._findSubFilter(selectionFilter, filter) === selectionFilter.length - 1 + || this._findSubFilter([selectionFilter], filter) === 0; + } + return false; + } + + _addFilterOperator(selectionFilter: SelectionFilter, filterOperator: 'and' | 'or'): SelectionFilter { + let filter = selectionFilter; + if ( + filter.length > 1 + && isString(filter[1]) + && filter[1] !== filterOperator + ) { + filter = [filter]; + } + if (Array.isArray(filter) && filter.length) { + filter.push(filterOperator); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return filter; + } + + _denormalizeFilter(filter: SelectionFilter): SelectionFilter { + let resultFilter = filter; + if (resultFilter && isString(resultFilter[0])) { + resultFilter = [resultFilter]; + } + return resultFilter; + } + + _isOnlyNegativeFiltersLeft(filters: SelectionFilter): boolean { + return filters.every((filterItem, i) => { + if (i % 2 === 0) { + return Array.isArray(filterItem) && filterItem[0] === '!'; + } + return filterItem === 'and'; + }); + } + + _addSelectionFilter( + isDeselect: boolean | undefined, + filter: SelectionFilter | undefined, + isSelectAll?: boolean, + skipFilter?: boolean, + ): void { + const currentOperation = isDeselect ? 'and' : 'or'; + let needAddFilter = true; + let selectionFilter: SelectionFilter = this.options.selectionFilter || []; + + selectionFilter = this._denormalizeFilter(selectionFilter); + if (selectionFilter?.length && !skipFilter) { + const removedIndex = this._removeSameFilter(selectionFilter, filter, isDeselect, isSelectAll); + const filterIndex = this._removeSameFilter(selectionFilter, filter, !isDeselect); + + const shouldCleanFilter = isDeselect + && (removedIndex !== -1 || filterIndex !== -1) + && this._isOnlyNegativeFiltersLeft(selectionFilter); + + if (shouldCleanFilter) { + selectionFilter = []; + } + + const isKeyOperatorsAfterRemoved = this._isKeyFilter(filter) + && this._hasKeyFiltersOnlyStartingFromIndex(selectionFilter, filterIndex); + + needAddFilter = !!filter?.length && !isKeyOperatorsAfterRemoved; + } + + if (needAddFilter) { + selectionFilter = this._addFilterOperator(selectionFilter, currentOperation); + if (Array.isArray(selectionFilter) && filter) { + const currentFilter = isDeselect ? ['!', filter] : filter; + selectionFilter.push(currentFilter); + } + } + + selectionFilter = this._normalizeFilter(selectionFilter); + + this._setOption('selectionFilter', !isDeselect && !selectionFilter.length ? null : selectionFilter); + } + + _normalizeFilter(filter: SelectionFilter): SelectionFilter { + let resultFilter = filter; + if (resultFilter && resultFilter.length === 1) { + [resultFilter] = resultFilter; + } + return resultFilter; + } + + _removeFilterByIndex(filter: SelectionFilter, filterIndex: number, isSelectAll?: boolean): void { + const operation = filter[1]; + + if (filterIndex > 0) { + filter.splice(filterIndex - 1, 2); + } else { + filter.splice(filterIndex, 2); + } + + if (isSelectAll && operation === 'and') { + filter.splice(0, filter.length); + } + } + + _isSimpleKeyFilter( + filter: SelectionFilter | undefined, + key: string | Function | undefined, + ): boolean { + return filter?.length === 3 && filter[0] === key && filter[1] === '='; + } + + _isKeyFilter(filter: SelectionFilter | undefined): boolean { + if (filter?.length === 2 && filter?.[0] === '!') { + return this._isKeyFilter(filter[1]); + } + const keyField = this._getKeyExpr(); + + if (Array.isArray(keyField)) { + if (filter?.length !== keyField.length * 2 - 1) { + return false; + } + for (let i = 0; i < keyField.length; i += 1) { + if (i > 0 && filter?.[i * 2 - 1] !== 'and') { + return false; + } + if (!this._isSimpleKeyFilter(filter?.[i * 2], keyField[i])) { + return false; + } + } + return true; + } + + return this._isSimpleKeyFilter(filter, keyField); + } + + _hasKeyFiltersOnlyStartingFromIndex( + selectionFilter: SelectionFilter, + filterIndex: number, + ): boolean { + if (filterIndex >= 0) { + for (let i = filterIndex; i < selectionFilter.length; i += 1) { + if (typeof selectionFilter[i] !== 'string' && !this._isKeyFilter(selectionFilter[i])) { + return false; + } + } + + return true; + } + + return false; + } + + _removeSameFilter( + selectionFilter: SelectionFilter, + filter: SelectionFilter | undefined, + inverted?: boolean, + isSelectAll?: boolean, + ): number { + const sameFilter = inverted ? ['!', filter] : filter; + + if (JSON.stringify(sameFilter) === JSON.stringify(selectionFilter)) { + selectionFilter.splice(0, selectionFilter.length); + return 0; + } + + const filterIndex = this._findSubFilter(selectionFilter, sameFilter); + + if (filterIndex >= 0) { + this._removeFilterByIndex(selectionFilter, filterIndex, isSelectAll); + return filterIndex; + } + + for (let i = 0; i < selectionFilter.length; i += 1) { + if (Array.isArray(selectionFilter[i]) && selectionFilter[i].length > 2) { + const innerFilterIndex = this._removeSameFilter( + selectionFilter[i], + sameFilter, + false, + isSelectAll, + ); + if (innerFilterIndex >= 0) { + // eslint-disable-next-line max-depth + if (!selectionFilter[i].length) { + this._removeFilterByIndex(selectionFilter, i, isSelectAll); + } else if (selectionFilter[i].length === 1) { + const [firstFilter] = selectionFilter[i]; + selectionFilter[i] = firstFilter; + } + return innerFilterIndex; + } + } + } + return -1; + } + + getSelectAllState(): boolean | undefined { + const filter = this.options.filter(); + let { selectionFilter } = this.options; + + if (!selectionFilter) return true; + if (!selectionFilter.length) return false; + if (!filter?.length) return undefined; + + selectionFilter = this._denormalizeFilter(selectionFilter); + + if (this._isLastSubFilter(selectionFilter, filter)) { + return true; + } + + if (this._isLastSubFilter(selectionFilter, ['!', filter])) { + return false; + } + + return undefined; + } + + loadSelectedItemsWithFilter(): DeferredObj { + const componentFilter = this.options.filter(); + const { selectionFilter } = this.options; + + const filter = componentFilter + ? [componentFilter, 'and', selectionFilter] + : selectionFilter; + + return this._loadFilteredData(filter); + } + + _onePageSelectAll(isDeselect: boolean): DeferredObj { + this._selectAllPlainItems(isDeselect); + + this.onSelectionChanged(); + + return Deferred().resolve(); + } +} diff --git a/packages/devextreme/js/__internal/ui/selection/m_selection.strategy.standard.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts similarity index 58% rename from packages/devextreme/js/__internal/ui/selection/m_selection.strategy.standard.ts rename to packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts index 7f8ea7c23963..4e1ae4bd8b5d 100644 --- a/packages/devextreme/js/__internal/ui/selection/m_selection.strategy.standard.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts @@ -5,50 +5,64 @@ import { getKeyHash } from '@js/core/utils/common'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; import { SelectionFilterCreator } from '@js/core/utils/selection_filter'; -import { isDefined, isObject } from '@js/core/utils/type'; +import { isDefined, isNumeric, isObject } from '@js/core/utils/type'; import errors from '@js/ui/widget/ui.errors'; +import SelectionStrategy from '@ts/ui/selection/selection.strategy'; +import type { + PendingOptions, + RequestData, + RequestItems, + SelectionFilter, + SelectionItem, + SelectionOptions, +} from '@ts/ui/selection/types'; + +interface KeyIndicesToRemoveMap { + [index: number]: boolean; +} -import SelectionStrategy from './m_selection.strategy'; - -export default class StandardStrategy extends SelectionStrategy { +export default class StandardStrategy< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + TKey extends string | number = string | number, +> extends SelectionStrategy { _shouldMergeWithLastRequest?: boolean; - _lastLoadDeferred?: any; + _lastLoadDeferred?: DeferredObj; - _lastRequestData?: any; + _lastRequestData?: RequestData; _isCancelingInProgress?: boolean; _lastSelectAllPageDeferred = Deferred().reject(); _storedSelectionState?: { - selectedItems: any; - selectedItemKeys: any; - keyHashIndices: any; + selectedItems: TItem[]; + selectedItemKeys: TKey[]; + keyHashIndices: string; }; - constructor(options) { + constructor(options: SelectionOptions) { super(options); this._initSelectedItemKeyHash(); } - _initSelectedItemKeyHash() { + _initSelectedItemKeyHash(): void { this._setOption('keyHashIndices', this.options.equalByReference ? null : {}); } - getSelectedItemKeys() { + getSelectedItemKeys(): TKey[] { return this.options.selectedItemKeys.slice(0); } - getSelectedItems() { + getSelectedItems(): TItem[] { return this.options.selectedItems.slice(0); } - _preserveSelectionUpdate(items, isDeselect) { + _preserveSelectionUpdate(items: TItem[], isDeselect?: boolean): void { const { keyOf } = this.options; - let keyIndicesToRemoveMap; - let keyIndex; - let i; + // eslint-disable-next-line @typescript-eslint/init-declarations + let keyIndicesToRemoveMap: KeyIndicesToRemoveMap | undefined; if (!keyOf) return; @@ -58,32 +72,36 @@ export default class StandardStrategy extends SelectionStrategy { keyIndicesToRemoveMap = {}; } - for (i = 0; i < items.length; i++) { - const item = items[i]; + items.forEach((item) => { const key = keyOf(item); if (isDeselect) { - keyIndex = this.removeSelectedItem(key, keyIndicesToRemoveMap, item?.disabled); - if (keyIndicesToRemoveMap && keyIndex >= 0) { + const keyIndex = this.removeSelectedItem( + key, + keyIndicesToRemoveMap, + item && typeof item === 'object' && 'disabled' in item ? !!item.disabled : false, + ); + if (keyIndicesToRemoveMap && isNumeric(keyIndex) && keyIndex >= 0) { keyIndicesToRemoveMap[keyIndex] = true; } } else { this.addSelectedItem(key, item); } - } + }); if (isBatchDeselect) { - this._batchRemoveSelectedItems(keyIndicesToRemoveMap); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._batchRemoveSelectedItems(keyIndicesToRemoveMap!); } } - _batchRemoveSelectedItems(keyIndicesToRemoveMap) { + _batchRemoveSelectedItems(keyIndicesToRemoveMap: KeyIndicesToRemoveMap): void { const selectedItemKeys = this.options.selectedItemKeys.slice(0); const selectedItems = this.options.selectedItems.slice(0); this.options.selectedItemKeys.length = 0; this.options.selectedItems.length = 0; - for (let i = 0; i < selectedItemKeys.length; i++) { + for (let i = 0; i < selectedItemKeys.length; i += 1) { if (!keyIndicesToRemoveMap[i]) { this.options.selectedItemKeys.push(selectedItemKeys[i]); this.options.selectedItems.push(selectedItems[i]); @@ -94,8 +112,14 @@ export default class StandardStrategy extends SelectionStrategy { this.updateSelectedItemKeyHash(this.options.selectedItemKeys); } - _loadSelectedItemsCore(keys, isDeselect, isSelectAll, filter, forceCombinedFilter = false) { - let deferred = Deferred(); + _loadSelectedItemsCore( + keys: TKey[], + isDeselect?: boolean, + isSelectAll?: boolean, + filter?: SelectionFilter, + forceCombinedFilter = false, + ): DeferredObj { + let deferred = Deferred(); const key = this.options.key(); if (!keys.length && !isSelectAll) { @@ -109,20 +133,33 @@ export default class StandardStrategy extends SelectionStrategy { } const selectionFilterCreator = new SelectionFilterCreator(keys, isSelectAll); - const combinedFilter = selectionFilterCreator.getCombinedFilter(key, filter, forceCombinedFilter); + const combinedFilter = selectionFilterCreator.getCombinedFilter( + key, + filter, + forceCombinedFilter, + ); let deselectedItems = []; if (isDeselect) { const { selectedItems } = this.options; deselectedItems = combinedFilter && keys.length !== selectedItems.length - // @ts-expect-error + // @ts-expect-error dataQuery ? dataQuery(selectedItems).filter(combinedFilter).toArray() : selectedItems.slice(0); } - let filteredItems = deselectedItems.length ? deselectedItems : this.options.plainItems(true).filter(this.options.isSelectableItem).map(this.options.getItemData); + let filteredItems = deselectedItems.length + ? deselectedItems + : this.options.plainItems(true) + .filter(this.options.isSelectableItem) + .map(this.options.getItemData); - const localFilter = selectionFilterCreator.getLocalFilter(this.options.keyOf, this.equalKeys.bind(this), this.options.equalByReference, key); + const localFilter = selectionFilterCreator.getLocalFilter( + this.options.keyOf, + this.equalKeys.bind(this), + this.options.equalByReference, + key, + ); filteredItems = filteredItems.filter(localFilter); @@ -135,50 +172,48 @@ export default class StandardStrategy extends SelectionStrategy { return deferred; } - _replaceSelectionUpdate(items) { - const internalKeys = []; + _replaceSelectionUpdate(items: TItem[]): void { const { keyOf } = this.options; if (!keyOf) return; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - const key = keyOf(item); - // @ts-expect-error - internalKeys.push(key); - } + const internalKeys: TKey[] = items.map((item) => keyOf(item)); this.setSelectedItems(internalKeys, items); } - _warnOnIncorrectKeys(keys) { + _warnOnIncorrectKeys(keys: TKey[]): void { const { allowNullValue } = this.options; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - + keys.forEach((key) => { if ((!allowNullValue || key !== null) && !this.isItemKeySelected(key)) { errors.log('W1002', key); } - } + }); } - _isMultiSelectEnabled() { + _isMultiSelectEnabled(): boolean { const { mode } = this.options; return mode === 'all' || mode === 'multiple'; } - _requestInProgress() { + _requestInProgress(): boolean { return this._lastLoadDeferred?.state() === 'pending'; } - _concatRequestsItems(keys, isDeselect, oldRequestItems, updatedKeys) { - let selectedItems; + _concatRequestsItems( + keys: TKey[], + oldRequestItems: RequestItems, + isDeselect?: boolean, + updatedKeys?: TKey[], + ): RequestData { + let selectedItems: TKey[] = []; const deselectedItems = isDeselect ? keys : []; if (updatedKeys) { selectedItems = updatedKeys; } else { + // @ts-expect-error removeDuplicates selectedItems = removeDuplicates(keys, this.options.selectedItemKeys); } @@ -189,75 +224,126 @@ export default class StandardStrategy extends SelectionStrategy { }; } - _collectLastRequestData(keys, isDeselect, isSelectAll, updatedKeys) { + _collectLastRequestData( + keys: TKey[], + isDeselect?: boolean, + isSelectAll?: boolean, + updatedKeys?: TKey[], + ): RequestData { const isDeselectAll = isDeselect && isSelectAll; - const oldRequestItems = { + const oldRequestItems: RequestItems = { added: [], removed: [], }; const multiSelectEnabled = this._isMultiSelectEnabled(); - let lastRequestData = multiSelectEnabled ? this._lastRequestData : {}; - - if (multiSelectEnabled) { - if (this._shouldMergeWithLastRequest) { - if (isDeselectAll) { - this._lastLoadDeferred.reject(); - lastRequestData = {}; - } else if (!isKeysEqual(keys, this.options.selectedItemKeys)) { - oldRequestItems.added = lastRequestData.addedItems; - oldRequestItems.removed = lastRequestData.removedItems; - - if (!isDeselect) { - this._lastLoadDeferred.reject(); - } + const emptyData: RequestData = { + addedItems: [], + removedItems: [], + keys: [], + }; + + if (!multiSelectEnabled) { + return emptyData; + } + + let lastRequestData: RequestData = this._lastRequestData ?? emptyData; + + if (this._shouldMergeWithLastRequest) { + if (isDeselectAll) { + this._lastLoadDeferred?.reject(); + lastRequestData = {} as RequestData; + } else if (!isKeysEqual(keys, this.options.selectedItemKeys)) { + oldRequestItems.added = lastRequestData?.addedItems; + oldRequestItems.removed = lastRequestData?.removedItems; + + if (!isDeselect) { + this._lastLoadDeferred?.reject(); } } - - lastRequestData = this._concatRequestsItems(keys, isDeselect, oldRequestItems, this._shouldMergeWithLastRequest ? undefined : updatedKeys); } + lastRequestData = this._concatRequestsItems( + keys, + oldRequestItems, + isDeselect, + this._shouldMergeWithLastRequest ? undefined : updatedKeys, + ); + return lastRequestData; } - _updateKeysByLastRequestData(keys, isDeselect, isSelectAll) { + _updateKeysByLastRequestData(keys: TKey[], isDeselect?: boolean, isSelectAll?: boolean): TKey[] { let currentKeys = keys; - if (this._isMultiSelectEnabled() && this._shouldMergeWithLastRequest && !isDeselect && !isSelectAll) { - currentKeys = removeDuplicates(keys.concat(this._lastRequestData?.addedItems), this._lastRequestData?.removedItems); + if ( + this._isMultiSelectEnabled() + && this._shouldMergeWithLastRequest + && this._lastRequestData + && !isDeselect + && !isSelectAll + ) { + currentKeys = removeDuplicates( + // @ts-expect-error removeDuplicates + [ + ...keys, + ...this._lastRequestData.addedItems, + ], + this._lastRequestData?.removedItems, + ); + // @ts-expect-error getUniqueValues currentKeys = getUniqueValues(currentKeys); } return currentKeys; } - _loadSelectedItems(keys, isDeselect, isSelectAll, updatedKeys, forceCombinedFilter = false) { - const that = this; - const deferred = Deferred(); - const filter = that.options.filter(); + _loadSelectedItems( + keys: TKey[], + isDeselect?: boolean, + isSelectAll?: boolean, + updatedKeys?: TKey[], + forceCombinedFilter = false, + ): DeferredObj { + const deferred = Deferred(); + const filter = this.options.filter(); this._shouldMergeWithLastRequest = this._requestInProgress(); - this._lastRequestData = this._collectLastRequestData(keys, isDeselect, isSelectAll, updatedKeys); + this._lastRequestData = this._collectLastRequestData( + keys, + isDeselect, + isSelectAll, + updatedKeys, + ); - when(that._lastLoadDeferred).always(() => { - const currentKeys = that._updateKeysByLastRequestData(keys, isDeselect, isSelectAll); + when(this._lastLoadDeferred).always(() => { + const currentKeys = this._updateKeysByLastRequestData(keys, isDeselect, isSelectAll); - that._shouldMergeWithLastRequest = false; + this._shouldMergeWithLastRequest = false; - that._loadSelectedItemsCore(currentKeys, isDeselect, isSelectAll, filter, forceCombinedFilter) - // eslint-disable-next-line @typescript-eslint/no-misused-promises - .done(deferred.resolve) - // eslint-disable-next-line @typescript-eslint/no-misused-promises - .fail(deferred.reject); + this._loadSelectedItemsCore(currentKeys, isDeselect, isSelectAll, filter, forceCombinedFilter) + .done((result) => { + deferred.resolve(result); + }) + .fail((error) => { + deferred.reject(error); + }); }); - that._lastLoadDeferred = deferred; + this._lastLoadDeferred = deferred; return deferred; } - selectedItemKeys(keys, preserve, isDeselect, isSelectAll, updatedKeys, forceCombinedFilter = false) { + selectedItemKeys( + keys: TKey[], + preserve?: boolean, + isDeselect?: boolean, + isSelectAll?: boolean, + updatedKeys?: TKey[], + forceCombinedFilter?: boolean, + ): DeferredObj { if (this._isCancelingInProgress) { - return Deferred().reject(); + return Deferred().reject(); } const loadingDeferred = this._loadSelectedItems( @@ -268,7 +354,7 @@ export default class StandardStrategy extends SelectionStrategy { forceCombinedFilter, ); - const selectionDeferred = Deferred(); + const selectionDeferred = Deferred(); loadingDeferred.done((items) => { this._storeSelectionState(); @@ -299,9 +385,13 @@ export default class StandardStrategy extends SelectionStrategy { return selectionDeferred; } - addSelectedItem(key, itemData) { - if (isDefined(itemData) && !this.options.ignoreDisabledItems && itemData.disabled) { - if (this.options.disabledItemKeys.indexOf(key) === -1) { + addSelectedItem(key: TKey, item: TItem): void { + if ( + isDefined(item) + && !this.options.ignoreDisabledItems + && item.disabled + ) { + if (!this.options.disabledItemKeys.includes(key)) { this.options.disabledItemKeys.push(key); } return; @@ -316,23 +406,21 @@ export default class StandardStrategy extends SelectionStrategy { this.options.selectedItemKeys.push(key); this.options.addedItemKeys.push(key); - this.options.addedItems.push(itemData); - this.options.selectedItems.push(itemData); + this.options.addedItems.push(item); + this.options.selectedItems.push(item); } } - _getSelectedIndexByKey(key, ignoreIndicesMap) { + _getSelectedIndexByKey(key: TKey, ignoreIndicesMap?: KeyIndicesToRemoveMap): number { const { selectedItemKeys } = this.options; - for (let index = 0; index < selectedItemKeys.length; index++) { - if ((!ignoreIndicesMap || !ignoreIndicesMap[index]) && this.equalKeys(selectedItemKeys[index], key)) { - return index; - } - } - return -1; + return selectedItemKeys + .findIndex( + (_, index) => !ignoreIndicesMap?.[index] && this.equalKeys(selectedItemKeys[index], key), + ); } - _getSelectedIndexByHash(key, ignoreIndicesMap) { + _getSelectedIndexByHash(key: TKey, ignoreIndicesMap?: KeyIndicesToRemoveMap): number { let indices = this.options.keyHashIndices[key]; if (indices && indices.length > 1 && ignoreIndicesMap) { @@ -342,8 +430,8 @@ export default class StandardStrategy extends SelectionStrategy { return indices && indices[0] >= 0 ? indices[0] : -1; } - _indexOfSelectedItemKey(key, ignoreIndicesMap?: any[]) { - let selectedIndex; + _indexOfSelectedItemKey(key: TKey, ignoreIndicesMap?: KeyIndicesToRemoveMap): number { + let selectedIndex = -1; if (this.options.equalByReference) { selectedIndex = this.options.selectedItemKeys.indexOf(key); @@ -356,29 +444,34 @@ export default class StandardStrategy extends SelectionStrategy { return selectedIndex; } - _shiftSelectedKeyIndices(keyIndex) { - for (let currentKeyIndex = keyIndex; currentKeyIndex < this.options.selectedItemKeys.length; currentKeyIndex++) { + _shiftSelectedKeyIndices(keyIndex: number): void { + for ( + let currentKeyIndex = keyIndex; + currentKeyIndex < this.options.selectedItemKeys.length; + currentKeyIndex += 1 + ) { const currentKey = this.options.selectedItemKeys[currentKeyIndex]; const currentKeyHash = getKeyHash(currentKey); const currentKeyIndices = this.options.keyHashIndices[currentKeyHash]; + // eslint-disable-next-line no-continue if (!currentKeyIndices) continue; - for (let i = 0; i < currentKeyIndices.length; i++) { + for (let i = 0; i < currentKeyIndices.length; i += 1) { if (currentKeyIndices[i] > keyIndex) { - currentKeyIndices[i]--; + currentKeyIndices[i] -= 1; } } } } removeSelectedItem( - key, - keyIndicesToRemoveMap?: any[], + key: TKey, + keyIndicesToRemoveMap?: KeyIndicesToRemoveMap, isDisabled?: boolean, - ) { + ): number | undefined { if (!this.options.ignoreDisabledItems && isDisabled) { - return; + return undefined; } const keyHash = this._getKeyHash(key); @@ -421,8 +514,8 @@ export default class StandardStrategy extends SelectionStrategy { return keyIndex; } - _updateAddedItemKeys(keys, items) { - for (let i = 0; i < keys.length; i++) { + _updateAddedItemKeys(keys: TKey[], items: TItem[]): void { + for (let i = 0; i < keys.length; i += 1) { if (!this.isItemKeySelected(keys[i])) { this.options.addedItemKeys.push(keys[i]); this.options.addedItems.push(items[i]); @@ -430,8 +523,8 @@ export default class StandardStrategy extends SelectionStrategy { } } - _updateRemovedItemKeys(keys, oldSelectedKeys, oldSelectedItems) { - for (let i = 0; i < oldSelectedKeys.length; i++) { + _updateRemovedItemKeys(_: TKey[], oldSelectedKeys: TKey[], oldSelectedItems: TItem[]): void { + for (let i = 0; i < oldSelectedKeys.length; i += 1) { if (!this.isItemKeySelected(oldSelectedKeys[i])) { this.options.removedItemKeys.push(oldSelectedKeys[i]); this.options.removedItems.push(oldSelectedItems[i]); @@ -439,20 +532,21 @@ export default class StandardStrategy extends SelectionStrategy { } } - _isItemSelectionInProgress(key, checkPending) { + _isItemSelectionInProgress(key: TKey, checkPending?: boolean): boolean { const shouldCheckPending = checkPending && this._lastRequestData && this._requestInProgress(); if (shouldCheckPending) { - const addedItems = this._lastRequestData.addedItems ?? []; + const addedItems = this._lastRequestData?.addedItems ?? []; return addedItems.includes(key); } return false; } - _getKeyHash(key) { + _getKeyHash(key: TKey): TKey { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.options.equalByReference ? key : getKeyHash(key); } - setSelectedItems(keys, items) { + setSelectedItems(keys: TKey[], items: TItem[]): void { this._updateAddedItemKeys(keys, items); const oldSelectedKeys = this.options.selectedItemKeys; @@ -469,12 +563,12 @@ export default class StandardStrategy extends SelectionStrategy { this._updateRemovedItemKeys(keys, oldSelectedKeys, oldSelectedItems); } - isItemDataSelected(itemData, options = {}) { + isItemDataSelected(itemData: TItem, options: PendingOptions = {}): boolean { const key = this.options.keyOf(itemData); return this.isItemKeySelected(key, options); } - isItemKeySelected(key, options: { checkPending?: boolean } = {}) { + isItemKeySelected(key: TKey, options: PendingOptions = {}): boolean { let result = this._isItemSelectionInProgress(key, options.checkPending); if (!result) { @@ -486,20 +580,20 @@ export default class StandardStrategy extends SelectionStrategy { return result; } - getSelectAllState(visibleOnly) { + getSelectAllState(visibleOnly: boolean): boolean | undefined { if (visibleOnly) { return this._getVisibleSelectAllState(); } return this._getFullSelectAllState(); } - loadSelectedItemsWithFilter() { + loadSelectedItemsWithFilter(): DeferredObj { const keyExpr = this.options.key(); const keys = this.getSelectedItemKeys(); const filter = this.options.filter(); if (!keys.length) { - return Deferred().resolve([]); + return Deferred().resolve([]); } const selectionFilterCreator = new SelectionFilterCreator(keys); @@ -521,7 +615,11 @@ export default class StandardStrategy extends SelectionStrategy { _restoreSelectionState(): void { this._clearItemKeys(); - const { selectedItemKeys, selectedItems, keyHashIndices } = this._storedSelectionState!; + if (!this._storedSelectionState) { + return; + } + + const { selectedItemKeys, selectedItems, keyHashIndices } = this._storedSelectionState; this._setOption('selectedItemKeys', selectedItemKeys); this._setOption('selectedItems', selectedItems); this._setOption('keyHashIndices', JSON.parse(keyHashIndices)); diff --git a/packages/devextreme/js/__internal/ui/selection/m_selection.strategy.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts similarity index 58% rename from packages/devextreme/js/__internal/ui/selection/m_selection.strategy.ts rename to packages/devextreme/js/__internal/ui/selection/selection.strategy.ts index 78ef807767b3..932eb2111af9 100644 --- a/packages/devextreme/js/__internal/ui/selection/m_selection.strategy.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts @@ -1,36 +1,47 @@ +import type { LoadOptions, SelectDescriptor } from '@js/common/data.types'; import dataQuery from '@js/common/data/query'; import { equalByValue, getKeyHash, noop, } from '@js/core/utils/common'; -import { Deferred } from '@js/core/utils/deferred'; +import { Deferred, type DeferredObj } from '@js/core/utils/deferred'; import { isObject, isPlainObject, isPromise } from '@js/core/utils/type'; - -export default class SelectionStrategy { - options: any; +import type { + QueryParams, + RemoteFilter, + SelectionFilter, + SelectionItem, + SelectionOptions, +} from '@ts/ui/selection/types'; + +export default class SelectionStrategy< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey = any, +> { + options: SelectionOptions; _lastSelectAllPageDeferred = Deferred().reject(); - constructor(options) { + constructor(options: SelectionOptions) { this.options = options; this._setOption('disabledItemKeys', []); this._clearItemKeys(); } - _clearItemKeys() { + _clearItemKeys(): void { this._setOption('addedItemKeys', []); this._setOption('removedItemKeys', []); this._setOption('removedItems', []); this._setOption('addedItems', []); } - validate() { - - } + validate(): void {} - _setOption(name, value) { + _setOption(name: string, value: unknown): void { this.options[name] = value; } @@ -81,7 +92,7 @@ export default class SelectionStrategy { } } - onSelectionChanged() { + onSelectionChanged(): void { const { selectedItems, selectedItemKeys, @@ -103,7 +114,7 @@ export default class SelectionStrategy { }); } - equalKeys(key1, key2) { + equalKeys(key1: TKey, key2: TKey): boolean { if (this.options.equalByReference) { if (isObject(key1) && isObject(key2)) { return key1 === key2; @@ -113,19 +124,23 @@ export default class SelectionStrategy { return equalByValue(key1, key2); } - getSelectableItems(items) { + getSelectableItems(items: TItem[]): TItem[] { return items.filter((item) => !item?.disabled); } - _clearSelection(keys, preserve, isDeselect, isSelectAll) { - keys = keys || []; - keys = Array.isArray(keys) ? keys : [keys]; + _clearSelection( + keys: TKey[] | TKey, + preserve?: boolean, + isDeselect?: boolean, + isSelectAll?: boolean, + ): DeferredObj { + let normalizedKeys = keys || []; + normalizedKeys = Array.isArray(normalizedKeys) ? normalizedKeys : [normalizedKeys]; this.validate(); - // @ts-expect-error - return this.selectedItemKeys(keys, preserve, isDeselect, isSelectAll); + return this.selectedItemKeys(normalizedKeys, preserve, isDeselect, isSelectAll); } - _removeTemplateProperty(remoteFilter: { template: any }) { + _removeTemplateProperty(remoteFilter: RemoteFilter): RemoteFilter { if (Array.isArray(remoteFilter)) { return remoteFilter.map((f) => this._removeTemplateProperty(f)); } @@ -137,11 +152,11 @@ export default class SelectionStrategy { return remoteFilter; } - _getQueryParams() { + _getQueryParams(): QueryParams | undefined { const { sensitivity } = this.options; if (!sensitivity) { - return; + return undefined; } return { @@ -153,42 +168,54 @@ export default class SelectionStrategy { }; } - _loadFilteredData(remoteFilter, localFilter?: any, select?: any, isSelectAll?: boolean) { - const filterLength = encodeURI(JSON.stringify(this._removeTemplateProperty(remoteFilter))).length; - const needLoadAllData = this.options.maxFilterLengthInRequest && (filterLength > this.options.maxFilterLengthInRequest); - const deferred = Deferred(); + _loadFilteredData( + remoteFilter: SelectionFilter, + localFilter?: Function | null, + select?: SelectDescriptor | null, + isSelectAll?: boolean, + ): DeferredObj { + const filterLength = encodeURI( + JSON.stringify(this._removeTemplateProperty(remoteFilter)), + ).length; + const needLoadAllData = this.options.maxFilterLengthInRequest + && (filterLength > this.options.maxFilterLengthInRequest); + const deferred = Deferred(); const queryParams = this._getQueryParams(); - const loadOptions = { + const loadOptions: LoadOptions = { filter: needLoadAllData ? undefined : remoteFilter, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing select: needLoadAllData ? this.options.dataFields() : select || this.options.dataFields(), ...queryParams, }; - if (remoteFilter && remoteFilter.length === 0) { + if (remoteFilter && Array.isArray(remoteFilter) && remoteFilter.length === 0) { deferred.resolve([]); } else { this.options.load(loadOptions) .done((items) => { - let filteredItems = isPlainObject(items) ? items.data : items; + let filteredItems = !Array.isArray(items) && isPlainObject(items) ? items.data : items; if (localFilter && !isSelectAll) { filteredItems = filteredItems.filter(localFilter); } else if (needLoadAllData) { - // @ts-expect-error + // @ts-expect-error dataQuary filteredItems = dataQuery(filteredItems).filter(remoteFilter).toArray(); } deferred.resolve(filteredItems); }) - .fail(deferred.reject.bind(deferred)); + .fail((error) => { + // @ts-expect-error error + deferred.reject(error); + }); } return deferred; } - updateSelectedItemKeyHash(keys) { - for (let i = 0; i < keys.length; i++) { + updateSelectedItemKeyHash(keys: TKey[]): void { + for (let i = 0; i < keys.length; i += 1) { const keyHash = getKeyHash(keys[i]); if (!isObject(keyHash)) { @@ -200,23 +227,24 @@ export default class SelectionStrategy { } } - _isAnyItemSelected(items) { - for (let i = 0; i < items.length; i++) { - if (this.options.isItemSelected(items[i])) { - return undefined; - } + _isAnyItemSelected(items: TItem[]): boolean | undefined { + if (items.find((item) => this.options.isItemSelected(item))) { + return undefined; } return false; } - _getFullSelectAllState() { + _getFullSelectAllState(): boolean | undefined { const items = this.options.plainItems(); - const dataFilter = this.options.filter(); - let selectedItems = this.options.ignoreDisabledItems ? this.options.selectedItems : this.options.selectedItems.filter((item) => !item?.disabled); + const { filter } = this.options; + const dataFilter = filter(); + let selectedItems = this.options.ignoreDisabledItems + ? this.options.selectedItems + : this.options.selectedItems.filter((item) => !item?.disabled); if (dataFilter) { - // @ts-expect-error + // @ts-expect-error dataQuery selectedItems = dataQuery(selectedItems).filter(dataFilter).toArray(); } @@ -233,13 +261,12 @@ export default class SelectionStrategy { return undefined; } - _getVisibleSelectAllState() { + _getVisibleSelectAllState(): boolean | undefined { const items = this.getSelectableItems(this.options.plainItems()); let hasSelectedItems = false; let hasUnselectedItems = false; - for (let i = 0; i < items.length; i++) { - const item = items[i]; + items.forEach((item) => { const itemData = this.options.getItemData(item); const key = this.options.keyOf(itemData); @@ -250,7 +277,7 @@ export default class SelectionStrategy { hasUnselectedItems = true; } } - } + }); if (hasSelectedItems) { return !hasUnselectedItems ? true : undefined; @@ -258,26 +285,48 @@ export default class SelectionStrategy { return false; } + selectedItemKeys( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + keys: TKey[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + preserve?: boolean, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isDeselect?: boolean, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isSelectAll?: boolean, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updatedKeys?: TKey[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + forceCombinedFilter?: boolean, + ): DeferredObj { + throw new Error('selectedItemKeys method should be overriden'); + } + + isItemKeySelected(itemKey: TKey | TItem): boolean; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isItemKeySelected(itemKey: TKey | TItem, options: { checkPending?: boolean } = {}): boolean { + throw new Error('isItemKeySelected method should be overriden'); + } + + isItemDataSelected(itemKey: TKey | TItem): boolean; // eslint-disable-next-line @typescript-eslint/no-unused-vars - isItemKeySelected(itemKey): boolean { + isItemDataSelected(itemKey: TKey | TItem, options: { checkPending?: boolean } = {}): boolean { throw new Error('isItemKeySelected method should be overriden'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - addSelectedItem(itemKey, itemData): void { + addSelectedItem(itemKey: TKey, dataOrIsSelectAll?: TItem | boolean, skipFilter?: boolean): void { throw new Error('addSelectedItem method should be overriden'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - removeSelectedItem(itemKey): void { + removeSelectedItem(itemKey: TKey): void { throw new Error('removeSelectedItem method should be overriden'); } _selectAllPlainItems(isDeselect: boolean): void { const items = this.getSelectableItems(this.options.plainItems()); - for (let i = 0; i < items.length; i++) { - const item = items[i]; - + items.forEach((item) => { if (this.options.isSelectableItem(item)) { const itemData = this.options.getItemData(item); const itemKey = this.options.keyOf(itemData); @@ -291,6 +340,6 @@ export default class SelectionStrategy { this.removeSelectedItem(itemKey); } } - } + }); } } diff --git a/packages/devextreme/js/__internal/ui/selection/m_selection.ts b/packages/devextreme/js/__internal/ui/selection/selection.ts similarity index 52% rename from packages/devextreme/js/__internal/ui/selection/m_selection.ts rename to packages/devextreme/js/__internal/ui/selection/selection.ts index a5f7e7fd32ea..cf5ba98cbb3f 100644 --- a/packages/devextreme/js/__internal/ui/selection/m_selection.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.ts @@ -1,30 +1,42 @@ import { noop } from '@js/core/utils/common'; -import { Deferred, when } from '@js/core/utils/deferred'; +import { Deferred, type DeferredObj, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; -import { isDefined } from '@js/core/utils/type'; - -import deferredStrategy from './m_selection.strategy.deferred'; -import standardStrategy from './m_selection.strategy.standard'; - -export default class Selection { - options: any; - - _selectionStrategy: deferredStrategy | standardStrategy; +import { isDefined, isPlainObject } from '@js/core/utils/type'; +import DeferredStrategy from '@ts/ui/selection/selection.strategy.deferred'; +import StandardStrategy from '@ts/ui/selection/selection.strategy.standard'; +import type { + DefaultOptions, + PendingOptions, + SelectionFilter, + SelectionItem, + SelectionOptions, + SelectionStrategy, +} from '@ts/ui/selection/types'; + +export default class Selection< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey extends string | number = any, + TDeferred extends boolean = boolean, +> { + options: SelectionOptions; + + _selectionStrategy: SelectionStrategy; _focusedItemIndex: number; - _shiftFocusedItemIndex!: number; + _shiftFocusedItemIndex?: number; - constructor(options) { + constructor(options: Partial>) { this.options = extend(this._getDefaultOptions(), options, { - selectedItemKeys: options.selectedKeys || [], + selectedItemKeys: options.selectedKeys ?? [], }); - this._selectionStrategy = this.options.deferred - // eslint-disable-next-line new-cap - ? new deferredStrategy(this.options) - // eslint-disable-next-line new-cap - : new standardStrategy(this.options); + this._selectionStrategy = (this.options.deferred + ? new DeferredStrategy(this.options) + : new StandardStrategy(this.options) + ) as SelectionStrategy; this._focusedItemIndex = -1; @@ -33,8 +45,8 @@ export default class Selection { } } - _getDefaultOptions() { - return { + _getDefaultOptions(): DefaultOptions { + const defaultOptions: DefaultOptions = { allowNullValue: false, deferred: false, equalByReference: false, @@ -43,109 +55,140 @@ export default class Selection { selectionFilter: [], maxFilterLengthInRequest: 0, onSelectionChanged: noop, - key: noop, - keyOf(item) { return item; }, - load() { return Deferred().resolve([]); }, + key() { return undefined; }, + keyOf(item) { return item as unknown as TKey; }, + load() { return Deferred().resolve([]); }, totalCount() { return -1; }, isSelectableItem() { return true; }, isItemSelected() { return false; }, getItemData(item) { return item; }, - dataFields: noop, - filter: noop, + dataFields() { return undefined; }, + filter() { return undefined; }, }; + return defaultOptions; } - validate() { + validate(): void { this._selectionStrategy.validate(); } - getSelectedItemKeys() { - return this._selectionStrategy.getSelectedItemKeys(); + getSelectedItemKeys(): TDeferred extends true ? Promise : TKey[] { + return this._selectionStrategy.getSelectedItemKeys() as TDeferred extends true + ? Promise + : TKey[]; + } + + _isStandardStrategy( + strategy: StandardStrategy | DeferredStrategy, + ): strategy is StandardStrategy { + return this.options.deferred; } - getSelectedItems() { - return this._selectionStrategy.getSelectedItems(); + getSelectedItems(): TDeferred extends true ? Promise : TItem[] { + return this._selectionStrategy.getSelectedItems() as TDeferred extends true + ? Promise + : TItem[]; } - selectionFilter(value?: any) { + selectionFilter(value?: SelectionFilter): SelectionFilter | undefined { if (value === undefined) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.options.selectionFilter; } - const filterIsChanged = this.options.selectionFilter !== value && JSON.stringify(this.options.selectionFilter) !== JSON.stringify(value); + const filterIsChanged = this.options.selectionFilter !== value + && JSON.stringify(this.options.selectionFilter) !== JSON.stringify(value); this.options.selectionFilter = value; - filterIsChanged && this.onSelectionChanged(); + if (filterIsChanged) { + this.onSelectionChanged(); + } + + return undefined; } - setSelection(keys, updatedKeys?) { + setSelection(keys: TKey[], updatedKeys?: TKey[]): DeferredObj { return this.selectedItemKeys(keys, false, false, false, updatedKeys); } - select(keys) { + select(keys: TKey[]): DeferredObj { return this.selectedItemKeys(keys, true); } - deselect(keys) { + deselect(keys: TKey[]): DeferredObj { return this.selectedItemKeys(keys, true, true); } selectedItemKeys( - keys, + keys: TKey[], preserve?: boolean, isDeselect?: boolean, isSelectAll?: boolean, - updatedKeys?: any[], - ) { - const that = this; - - keys = keys ?? []; - keys = Array.isArray(keys) ? keys : [keys]; - that.validate(); - - return this._selectionStrategy.selectedItemKeys(keys, preserve, isDeselect, isSelectAll, updatedKeys); + updatedKeys?: TKey[], + ): DeferredObj { + let normalizedKeys = keys ?? []; + normalizedKeys = Array.isArray(normalizedKeys) ? normalizedKeys : [normalizedKeys]; + + this.validate(); + + return this._selectionStrategy.selectedItemKeys( + normalizedKeys, + preserve, + isDeselect, + isSelectAll, + updatedKeys, + ); } - clearSelection() { + clearSelection(): DeferredObj { return this.selectedItemKeys([]); } - _addSelectedItem(itemData, key) { + _addSelectedItem(itemData: TItem, key: TKey): void { + // @ts-expect-error addSelectedItem this._selectionStrategy.addSelectedItem(key, itemData); } - _removeSelectedItem(key) { + _removeSelectedItem(key: TKey): void { this._selectionStrategy.removeSelectedItem(key); } - _setSelectedItems(keys, items) { + _setSelectedItems(keys: TKey[], items: TItem[]): void { this._selectionStrategy.setSelectedItems(keys, items); } - onSelectionChanged() { + onSelectionChanged(): void { this._selectionStrategy.onSelectionChanged(); } - // @ts-expect-error - changeItemSelection(itemIndex, keys, setFocusOnly) { - let isSelectedItemsChanged; + changeItemSelection( + itemIndex: number, + keys: { control?: boolean; shift?: boolean } = {}, + setFocusOnly?: boolean, + ): boolean | undefined { + let isSelectedItemsChanged = false; const items = this.options.plainItems(); const item = items[itemIndex]; - let deferred; + let focusedItemIndex = itemIndex; + // eslint-disable-next-line @typescript-eslint/init-declarations + let deferred: Promise | undefined; const { isVirtualPaging } = this.options; const allowLoadByRange = this.options.allowLoadByRange?.(); const { alwaysSelectByShift } = this.options; - let indexOffset; + // eslint-disable-next-line @typescript-eslint/init-declarations + let indexOffset: number | undefined; let focusedItemNotInLoadedRange = false; let shiftFocusedItemNotInLoadedRange = false; - const itemIsNotInLoadedRange = (index) => index >= 0 && !items.filter((it) => it.loadIndex === index).length; + const itemIsNotInLoadedRange = (index: number): boolean => index >= 0 && !items.filter( + (it) => it.loadIndex === index, + ).length; if (isVirtualPaging && isDefined(item)) { if (allowLoadByRange) { - indexOffset = item.loadIndex - itemIndex; - itemIndex = item.loadIndex; + indexOffset = item.loadIndex - focusedItemIndex; + focusedItemIndex = item.loadIndex; } focusedItemNotInLoadedRange = itemIsNotInLoadedRange(this._focusedItemIndex); if (isDefined(this._shiftFocusedItemIndex)) { @@ -160,22 +203,27 @@ export default class Selection { const itemData = this.options.getItemData(item); const itemKey = this.options.keyOf(itemData); - keys = keys || {}; let allowSelectByShift = keys.shift; if (alwaysSelectByShift === false && allowSelectByShift) { - allowSelectByShift = allowLoadByRange !== false || (!focusedItemNotInLoadedRange && !shiftFocusedItemNotInLoadedRange); + allowSelectByShift = allowLoadByRange !== false + || (!focusedItemNotInLoadedRange && !shiftFocusedItemNotInLoadedRange); } if (allowSelectByShift && this.options.mode === 'multiple' && this._focusedItemIndex >= 0) { if (allowLoadByRange && (focusedItemNotInLoadedRange || shiftFocusedItemNotInLoadedRange)) { - isSelectedItemsChanged = itemIndex !== this._shiftFocusedItemIndex || this._focusedItemIndex !== this._shiftFocusedItemIndex; + isSelectedItemsChanged = focusedItemIndex !== this._shiftFocusedItemIndex + || this._focusedItemIndex !== this._shiftFocusedItemIndex; if (isSelectedItemsChanged) { - deferred = this.changeItemSelectionWhenShiftKeyInVirtualPaging(itemIndex); + deferred = this.changeItemSelectionWhenShiftKeyInVirtualPaging(focusedItemIndex); } } else { - isSelectedItemsChanged = this.changeItemSelectionWhenShiftKeyPressed(itemIndex, items, indexOffset); + isSelectedItemsChanged = this.changeItemSelectionWhenShiftKeyPressed( + focusedItemIndex, + items, + indexOffset, + ); } } else if (keys.control) { this._resetItemSelectionWhenShiftKeyPressed(); @@ -193,7 +241,10 @@ export default class Selection { isSelectedItemsChanged = true; } else { this._resetItemSelectionWhenShiftKeyPressed(); - const isKeysEqual = this._selectionStrategy.equalKeys(this.options.selectedItemKeys[0], itemKey); + const isKeysEqual = this._selectionStrategy.equalKeys( + this.options.selectedItemKeys[0], + itemKey, + ); if (this.options.selectedItemKeys.length !== 1 || !isKeysEqual) { this._setSelectedItems([itemKey], [itemData]); isSelectedItemsChanged = true; @@ -202,45 +253,53 @@ export default class Selection { if (isSelectedItemsChanged) { when(deferred).done(() => { - this._focusedItemIndex = itemIndex; - !setFocusOnly && this.onSelectionChanged(); + this._focusedItemIndex = focusedItemIndex; + if (!setFocusOnly) { + this.onSelectionChanged(); + } }); return true; } + + return undefined; } - isDataItem(item) { + isDataItem(item: TItem): boolean { return this.options.isSelectableItem(item); } - isSelectable() { + isSelectable(): boolean { return this.options.mode === 'single' || this.options.mode === 'multiple'; } - isItemDataSelected(data) { + isItemDataSelected(data: TItem): boolean { return this._selectionStrategy.isItemDataSelected(data, { checkPending: true }); } - isItemSelected(arg, options?: any): boolean { + isItemSelected(arg: TKey, options: PendingOptions = {}): boolean { return this._selectionStrategy.isItemKeySelected(arg, options); } - _resetItemSelectionWhenShiftKeyPressed() { - // @ts-expect-error + _resetItemSelectionWhenShiftKeyPressed(): void { delete this._shiftFocusedItemIndex; } - _resetFocusedItemIndex() { + _resetFocusedItemIndex(): void { this._focusedItemIndex = -1; } - changeItemSelectionWhenShiftKeyInVirtualPaging(loadIndex) { - const loadOptions = this.options.getLoadOptions(loadIndex, this._focusedItemIndex, this._shiftFocusedItemIndex); + changeItemSelectionWhenShiftKeyInVirtualPaging(loadIndex: number): Promise { + const loadOptions = this.options.getLoadOptions?.( + loadIndex, + this._focusedItemIndex, + this._shiftFocusedItemIndex, + ) ?? {}; const deferred = Deferred(); const indexOffset = loadOptions.skip; this.options.load(loadOptions).done((items) => { - this.changeItemSelectionWhenShiftKeyPressed(loadIndex, items, indexOffset); + const filteredItems = !Array.isArray(items) && isPlainObject(items) ? items.data : items; + this.changeItemSelectionWhenShiftKeyPressed(loadIndex, filteredItems, indexOffset); deferred.resolve(); }); @@ -248,9 +307,12 @@ export default class Selection { return deferred.promise(); } - changeItemSelectionWhenShiftKeyPressed(itemIndex, items, indexOffset) { + changeItemSelectionWhenShiftKeyPressed( + itemIndex: number, + items: TItem[], + indexOffset?: number, + ): boolean { let isSelectedItemsChanged = false; - let itemIndexStep; const indexOffsetDefined = isDefined(indexOffset); let index = indexOffsetDefined ? this._focusedItemIndex - indexOffset : this._focusedItemIndex; const { keyOf } = this.options; @@ -263,15 +325,20 @@ export default class Selection { this._shiftFocusedItemIndex = this._focusedItemIndex; } - let data; - let itemKey; - let startIndex; - let endIndex; + let itemIndexStep = 0; + // eslint-disable-next-line @typescript-eslint/init-declarations + let itemKey: TKey; + let startIndex = 0; + let endIndex = 0; if (this._shiftFocusedItemIndex !== this._focusedItemIndex) { itemIndexStep = this._focusedItemIndex < this._shiftFocusedItemIndex ? 1 : -1; - startIndex = indexOffsetDefined ? this._focusedItemIndex - indexOffset : this._focusedItemIndex; - endIndex = indexOffsetDefined ? this._shiftFocusedItemIndex - indexOffset : this._shiftFocusedItemIndex; + startIndex = indexOffsetDefined + ? this._focusedItemIndex - indexOffset + : this._focusedItemIndex; + endIndex = indexOffsetDefined + ? this._shiftFocusedItemIndex - indexOffset + : this._shiftFocusedItemIndex; for (index = startIndex; index !== endIndex; index += itemIndexStep) { if (indexOffsetDefined || this.isDataItem(items[index])) { itemKey = keyOf(this.options.getItemData(items[index])); @@ -289,7 +356,7 @@ export default class Selection { : this._shiftFocusedItemIndex; for (index = startIndex; index !== endIndex; index += itemIndexStep) { if (indexOffsetDefined || this.isDataItem(items[index])) { - data = this.options.getItemData(items[index]); + const data = this.options.getItemData(items[index]); itemKey = keyOf(data); this._addSelectedItem(data, itemKey); @@ -306,11 +373,11 @@ export default class Selection { return isSelectedItemsChanged; } - clearSelectedItems() { + clearSelectedItems(): void { this._setSelectedItems([], []); } - selectAll(isOnePage) { + selectAll(isOnePage: boolean): DeferredObj { this._resetFocusedItemIndex(); if (isOnePage) { @@ -319,7 +386,7 @@ export default class Selection { return this.selectedItemKeys([], true, false, true); } - deselectAll(isOnePage) { + deselectAll(isOnePage: boolean): DeferredObj { this._resetFocusedItemIndex(); if (isOnePage) { @@ -328,11 +395,11 @@ export default class Selection { return this.selectedItemKeys([], true, true, true); } - getSelectAllState(visibleOnly) { + getSelectAllState(visibleOnly: boolean): boolean | undefined { return this._selectionStrategy.getSelectAllState(visibleOnly); } - loadSelectedItemsWithFilter() { + loadSelectedItemsWithFilter(): DeferredObj { return this._selectionStrategy.loadSelectedItemsWithFilter(); } } diff --git a/packages/devextreme/js/__internal/ui/selection/types.ts b/packages/devextreme/js/__internal/ui/selection/types.ts new file mode 100644 index 000000000000..7056d5aced2a --- /dev/null +++ b/packages/devextreme/js/__internal/ui/selection/types.ts @@ -0,0 +1,153 @@ +import type { LoadResult } from '@js/common/data'; +import type { FilterDescriptor, LoadOptions, SelectDescriptor } from '@js/common/data.types'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import type { Cancelable } from '@js/events'; +import type DeferredStrategy from '@ts/ui/selection/selection.strategy.deferred'; +import type StandardStrategy from '@ts/ui/selection/selection.strategy.standard'; + +export type SelectionFilter = FilterDescriptor[]; + +type Filter = () => SelectionFilter | undefined; +export interface SelectionItem { + disabled?: boolean; + loadIndex: number; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Sensitivity = 'case' | 'base' | 'variant' | any; + +export type KeyExpr = string | string[]; +export type KeyHash = string | number | symbol; + +interface SelectionChangeEvent { + selectedItems: TItem[]; + selectedItemKeys: TKey[]; + addedItemKeys: TKey[]; + removedItemKeys: TKey[]; + addedItems: TItem[]; + removedItems: TItem[]; +} + +export interface PendingOptions { checkPending?: boolean } + +export interface DefaultOptions< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey = any, + TDeferred extends boolean = boolean, +> { + onSelectionChanged: (event: SelectionChangeEvent) => void; + key: () => KeyExpr | ((source: TItem) => TKey) | undefined; + keyOf: (item: TItem) => TKey; + load: (loadOptions: LoadOptions) => DeferredObj>; + totalCount: () => number; + isSelectableItem: (item: TItem) => boolean; + isItemSelected: (arg: TItem | TKey, options?: PendingOptions) => boolean; + getItemData: (item: TItem) => TItem; + dataFields: () => SelectDescriptor | undefined; + filter: Filter; + allowNullValue: boolean; + deferred: TDeferred; + equalByReference: boolean; + mode: string; + selectedItems: TItem[]; + selectionFilter: SelectionFilter; + maxFilterLengthInRequest: number; +} + +export type SelectionOptions< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey = any, + TDeferred extends boolean = boolean, +> = DefaultOptions & { + selectedKeys: TKey[]; + selectedItemKeys: TKey[]; + plainItems: (cached?: boolean) => TItem[]; + isVirtualPaging?: boolean; + sensitivity?: Sensitivity; + allowLoadByRange?: () => boolean | undefined; + alwaysSelectByShift?: boolean; + getLoadOptions?: ( + loadItemIndex: number, + focusedItemIndex: number, + shiftItemIndex?: number + ) => LoadOptions; + addedItemKeys: TKey[]; + removedItemKeys: TKey[]; + addedItems: TItem[]; + removedItems: TItem[]; + onSelectionChanging: (event: SelectionChangeEvent & Cancelable) => void; + keyHashIndices: { + [keyHash: KeyHash]: number[]; + }; + ignoreDisabledItems?: boolean; + disabledItemKeys: TKey[]; +}; + +interface SelectionStrategyOptions< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey = any, +> { + disabledItemKeys: TKey[]; + addedItemKeys: TKey[]; + removedItemKeys: TKey[]; + addedItems: TItem[]; + removedItems: TItem[]; + onSelectionChanging?: (event: SelectionChangeEvent & Cancelable) => void; + keyHashIndices: { + [keyHash: KeyHash]: number[]; + } | null; +} + +export type StrategyOptions< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey = any, + TDeferred extends boolean = boolean, +> = SelectionStrategyOptions & SelectionOptions; + +export type SelectionStrategy< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TItem extends SelectionItem = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TKey extends string | number = any, + TDeferred extends boolean = boolean, +> = TDeferred extends true + ? DeferredStrategy + : StandardStrategy; + +export type ClearedFilterItem = object; + +export type RemoteFilterItem = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + template: any; +} & ClearedFilterItem; + +export type RemoteFilter = SelectionFilter | RemoteFilterItem | RemoteFilterItem[]; + +export interface QueryParams { + langParams: { + collatorOptions: { + sensitivity: Sensitivity; + }; + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface RequestItems { + added: (TItem | TKey)[]; + removed: (TItem | TKey)[]; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface RequestData { + addedItems: (TItem | TKey)[]; + removedItems: (TItem | TKey)[]; + keys: TKey[]; +} diff --git a/packages/devextreme/js/__internal/ui/slider/m_slider.ts b/packages/devextreme/js/__internal/ui/slider/m_slider.ts index fe96a05faa30..11577fd0b21e 100644 --- a/packages/devextreme/js/__internal/ui/slider/m_slider.ts +++ b/packages/devextreme/js/__internal/ui/slider/m_slider.ts @@ -77,6 +77,11 @@ class Slider< _startOffset?: number; + // eslint-disable-next-line class-methods-use-this + protected _activeStateUnit(): string { + return SLIDER_HANDLE_SELECTOR; + } + _supportedKeys(): SupportedKeys { const { rtlEnabled } = this.option(); @@ -188,12 +193,6 @@ class Slider< }; } - _init(): void { - super._init(); - - this._activeStateUnit = SLIDER_HANDLE_SELECTOR; - } - _toggleValidationMessage(visible: boolean): void { if (!this.option('isValid')) { this.$element() diff --git a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip.ts b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip.ts index 680de861a0fa..065a652dbd91 100644 --- a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip.ts +++ b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip.ts @@ -5,8 +5,7 @@ import type { Format } from '@js/localization'; import type { OptionChanged } from '@ts/core/widget/types'; import type { TooltipProperties } from '@ts/ui/m_tooltip'; import Tooltip from '@ts/ui/m_tooltip'; - -import { SliderTooltipPositionController } from './m_slider_tooltip_position_controller'; +import { SliderTooltipPositionController } from '@ts/ui/slider/slider_tooltip_position_controller'; // NOTE: Visibility is contolled by the 'visible' option // and 'dx-slider-tooltip-visible-on-hover' class diff --git a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts b/packages/devextreme/js/__internal/ui/slider/slider_tooltip_position_controller.ts similarity index 94% rename from packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts rename to packages/devextreme/js/__internal/ui/slider/slider_tooltip_position_controller.ts index dccf7f4929eb..a0e0a21ba818 100644 --- a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/slider/slider_tooltip_position_controller.ts @@ -9,11 +9,11 @@ import type { PopoverControllerElements, PopoverControllerProperties, PopoverPosition, -} from '@ts/ui/popover/m_popover_position_controller'; +} from '@ts/ui/popover/popover_position_controller'; import { isCommonPosition, PopoverPositionController, -} from '@ts/ui/popover/m_popover_position_controller'; +} from '@ts/ui/popover/popover_position_controller'; const SLIDER_CLASS = 'dx-slider'; @@ -92,7 +92,6 @@ export class SliderTooltipPositionController< this._updateVisualPositionValue(); } - // eslint-disable-next-line class-methods-use-this _positionToObject(position: TPosition): PopoverPosition { if (isCommonPosition(position)) { const configuration: PopoverPosition = { diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts index ba28e08c7929..db190fcc6b08 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_action.ts @@ -3,7 +3,7 @@ import Guid from '@js/core/guid'; import { extend } from '@js/core/utils/extend'; import readyCallbacks from '@js/core/utils/ready_callbacks'; import type { Properties } from '@js/ui/speed_dial_action'; -import swatchContainer from '@ts/core/utils/m_swatch_container'; +import swatchContainer from '@ts/core/utils/swatch_container'; import Widget from '@ts/core/widget/widget'; import { disposeAction, initAction } from './m_speed_dial_main_item'; diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts index 6da4d66d31ef..5a928c32ad17 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts @@ -7,7 +7,7 @@ import { extend } from '@js/core/utils/extend'; import { getHeight } from '@js/core/utils/size'; import { isCompact, isFluent, isMaterial } from '@js/ui/themes'; import errors from '@js/ui/widget/ui.errors'; -import swatchContainer from '@ts/core/utils/m_swatch_container'; +import swatchContainer from '@ts/core/utils/swatch_container'; import type { SpeedDialItemProperties } from './m_speed_dial_item'; import SpeedDialItem from './m_speed_dial_item'; diff --git a/packages/devextreme/js/__internal/ui/splitter/resize_handle.ts b/packages/devextreme/js/__internal/ui/splitter/resize_handle.ts index 31fb844f2c02..49f475d77722 100644 --- a/packages/devextreme/js/__internal/ui/splitter/resize_handle.ts +++ b/packages/devextreme/js/__internal/ui/splitter/resize_handle.ts @@ -319,31 +319,31 @@ class ResizeHandle extends Widget { this._attachEventHandlers(); } - _resizeStartHandler(e: DxEvent): void { + _resizeStartHandler(e: InteractionEvent): void { this._getAction(RESIZE_EVENT.onResizeStart)({ event: e, }); } - _resizeHandler(e: DxEvent): void { + _resizeHandler(e: InteractionEvent): void { this._getAction(RESIZE_EVENT.onResize)({ event: e, }); } - _resizeEndHandler(e: DxEvent): void { + _resizeEndHandler(e: InteractionEvent): void { this._getAction(RESIZE_EVENT.onResizeEnd)({ event: e, }); } - _collapsePrevHandler(e: DxEvent): void { + _collapsePrevHandler(e: InteractionEvent): void { this._getAction(COLLAPSE_EVENT.onCollapsePrev)({ event: e, }); } - _collapseNextHandler(e: DxEvent): void { + _collapseNextHandler(e: InteractionEvent): void { this._getAction(COLLAPSE_EVENT.onCollapseNext)({ event: e, }); diff --git a/packages/devextreme/js/__internal/ui/switch.ts b/packages/devextreme/js/__internal/ui/switch.ts index 6d59ee96fb63..e07809e22209 100644 --- a/packages/devextreme/js/__internal/ui/switch.ts +++ b/packages/devextreme/js/__internal/ui/switch.ts @@ -55,6 +55,10 @@ class Switch extends Editor { _clickAction?: (event?: Record) => void; + protected _feedbackHideTimeout(): number { + return 0; + } + _supportedKeys(): SupportedKeys { const { rtlEnabled } = this.option(); @@ -111,7 +115,6 @@ class Switch extends Editor { _init(): void { super._init(); - this._feedbackHideTimeout = 0; this._animating = false; } diff --git a/packages/devextreme/js/__internal/ui/tabs/tabs.ts b/packages/devextreme/js/__internal/ui/tabs/tabs.ts index 1ef3e52affd8..d2b4a521cebd 100644 --- a/packages/devextreme/js/__internal/ui/tabs/tabs.ts +++ b/packages/devextreme/js/__internal/ui/tabs/tabs.ts @@ -153,6 +153,14 @@ class Tabs extends CollectionWidgetLiveUpdate { _$wrapper!: dxElementWrapper; + protected _activeStateUnit(): string { + return `.${TABS_ITEM_CLASS}`; + } + + protected _feedbackHideTimeout(): number { + return FEEDBACK_HIDE_TIMEOUT; + } + _getDefaultOptions(): TabsProperties { return { ...super._getDefaultOptions(), @@ -231,7 +239,6 @@ class Tabs extends CollectionWidgetLiveUpdate { super._init(); - this._activeStateUnit = `.${TABS_ITEM_CLASS}`; this.setAria('role', 'tablist'); this.$element().addClass(TABS_CLASS); this._toggleScrollingEnabledClass(scrollingEnabled); @@ -241,8 +248,6 @@ class Tabs extends CollectionWidgetLiveUpdate { this._toggleStylingModeClass(stylingMode); this._renderWrapper(); this._renderMultiple(); - - this._feedbackHideTimeout = FEEDBACK_HIDE_TIMEOUT; } _prepareDefaultItemTemplate(data: Item, $container: dxElementWrapper): void { diff --git a/packages/devextreme/js/__internal/ui/tile_view.ts b/packages/devextreme/js/__internal/ui/tile_view.ts index cf57ab405cf9..78a37a4efcc1 100644 --- a/packages/devextreme/js/__internal/ui/tile_view.ts +++ b/packages/devextreme/js/__internal/ui/tile_view.ts @@ -84,6 +84,10 @@ class TileView extends CollectionWidget { _cells!: number[][]; + protected _activeStateUnit(): string { + return TILEVIEW_ITEM_SELECTOR; + } + _getDefaultOptions(): TileViewProperties { return { ...super._getDefaultOptions(), @@ -136,8 +140,6 @@ class TileView extends CollectionWidget { _init(): void { super._init(); - this._activeStateUnit = TILEVIEW_ITEM_SELECTOR; - this.$element().addClass(TILEVIEW_CLASS); this._initScrollView(); } diff --git a/packages/devextreme/js/__internal/ui/toast/hide_toasts.ts b/packages/devextreme/js/__internal/ui/toast/hide_toasts.ts new file mode 100644 index 000000000000..4ab007d740a5 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/toast/hide_toasts.ts @@ -0,0 +1,42 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import Toast, { TOAST_CLASS } from '@ts/ui/toast/toast'; + +function hideToasts(container?: Element | dxElementWrapper): void { + const toasts = $(`.${TOAST_CLASS}`).toArray(); + + if (arguments.length === 0) { + toasts.forEach((toast) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Toast.getInstance(toast).hide(); + }); + + return; + } + + if (!container) { + return; + } + + const containerElement = $(container).get(0); + + toasts + .map((toast): Toast => { + const instance = Toast.getInstance(toast); + + return instance; + }) + .filter((instance) => { + const { container: toastContainer } = instance.option(); + + const toastContainerElement = $(toastContainer).get(0); + + return containerElement === toastContainerElement && containerElement; + }) + .forEach((instance) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + instance.hide(); + }); +} + +export default hideToasts; diff --git a/packages/devextreme/js/__internal/ui/toast/m_hide_toasts.ts b/packages/devextreme/js/__internal/ui/toast/m_hide_toasts.ts deleted file mode 100644 index 338a6112d5bd..000000000000 --- a/packages/devextreme/js/__internal/ui/toast/m_hide_toasts.ts +++ /dev/null @@ -1,27 +0,0 @@ -import $ from '@js/core/renderer'; - -const TOAST_CLASS = 'dx-toast'; - -function hideAllToasts(container): void { - const toasts = $(`.${TOAST_CLASS}`).toArray(); - if (!arguments.length) { - // @ts-expect-error - toasts.forEach((toast) => { $(toast).dxToast('hide'); }); - return; - } - - const containerElement = $(container).get(0); - - toasts - // @ts-expect-error - .map((toast) => $(toast).dxToast('instance')) - .filter((instance) => { - const toastContainerElement = $(instance.option('container')).get(0); - return containerElement === toastContainerElement && containerElement; - }) - .forEach((instance) => { - instance.hide(); - }); -} - -export default hideAllToasts; diff --git a/packages/devextreme/js/__internal/ui/toast/m_toast.ts b/packages/devextreme/js/__internal/ui/toast/toast.ts similarity index 63% rename from packages/devextreme/js/__internal/ui/toast/m_toast.ts rename to packages/devextreme/js/__internal/ui/toast/toast.ts index c4756176bf36..58da41491adc 100644 --- a/packages/devextreme/js/__internal/ui/toast/m_toast.ts +++ b/packages/devextreme/js/__internal/ui/toast/toast.ts @@ -1,32 +1,33 @@ +import type { PositionAlignment } from '@js/common'; import eventsEngine from '@js/common/core/events/core/events_engine'; import pointerEvents from '@js/common/core/events/pointer'; +import type { DeepPartial } from '@js/core'; import registerComponent from '@js/core/component_registrator'; import domAdapter from '@js/core/dom_adapter'; import type { DefaultOptionsRule } from '@js/core/options/utils'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import type { DeferredObj } from '@js/core/utils/deferred'; -import { extend } from '@js/core/utils/extend'; import readyCallbacks from '@js/core/utils/ready_callbacks'; import { isString } from '@js/core/utils/type'; import Overlay from '@js/ui/overlay/ui.overlay'; -import { isMaterialBased } from '@js/ui/themes'; +import { current, isMaterialBased } from '@js/ui/themes'; import type { Properties } from '@js/ui/toast'; import type { OptionChanged } from '@ts/core/widget/types'; +import type { OverlayProperties, PointerLikeEvent } from '@ts/ui/overlay/overlay'; const ready = readyCallbacks.add; -const TOAST_CLASS = 'dx-toast'; -const TOAST_CLASS_PREFIX = `${TOAST_CLASS}-`; -const TOAST_WRAPPER_CLASS = `${TOAST_CLASS_PREFIX}wrapper`; -const TOAST_CONTENT_CLASS = `${TOAST_CLASS_PREFIX}content`; -const TOAST_MESSAGE_CLASS = `${TOAST_CLASS_PREFIX}message`; -const TOAST_ICON_CLASS = `${TOAST_CLASS_PREFIX}icon`; +export const TOAST_CLASS = 'dx-toast'; +const TOAST_WRAPPER_CLASS = 'dx-toast-wrapper'; +const TOAST_CONTENT_CLASS = 'dx-toast-content'; +const TOAST_MESSAGE_CLASS = 'dx-toast-message'; +const TOAST_ICON_CLASS = 'dx-toast-icon'; const WIDGET_NAME = 'dxToast'; const toastTypes = ['info', 'warning', 'error', 'success']; -const TOAST_STACK = []; +const TOAST_STACK: Toast[] = []; const FIRST_Z_INDEX_OFFSET = 8000; const POSITION_ALIASES = { @@ -51,24 +52,34 @@ const DEFAULT_BOUNDARY_OFFSET = { h: 0, v: 0 }; const DEFAULT_MARGIN = 20; ready(() => { - // @ts-expect-error ts-error - eventsEngine.subscribeGlobal(domAdapter.getDocument(), pointerEvents.down, (e) => { - for (let i = TOAST_STACK.length - 1; i >= 0; i--) { - // @ts-expect-error ts-error - if (!TOAST_STACK[i]._proxiedDocumentDownHandler(e)) { + const element = domAdapter.getDocument(); + + const callback = (e: PointerLikeEvent): void => { + for (let i = TOAST_STACK.length - 1; i >= 0; i -= 1) { + if (!TOAST_STACK[i]._proxiedDocumentDownHandler?.(e)) { return; } } - }); + }; + + // @ts-expect-error subscribeGlobal should be described in .d.ts + eventsEngine.subscribeGlobal( + element, + pointerEvents.down, + callback, + ); }); -interface ToastProperties extends Properties {} +interface ToastProperties extends Properties { + container: OverlayProperties['container']; +} class Toast< -TProperties extends ToastProperties = ToastProperties, + TProperties extends ToastProperties = ToastProperties, > extends Overlay { _message?: dxElementWrapper; + // eslint-disable-next-line no-restricted-globals _hideTimeout?: ReturnType; _getDefaultOptions(): TProperties { @@ -118,13 +129,12 @@ TProperties extends ToastProperties = ToastProperties, }; const tabletAndMobileCommonOptions = { - // @ts-expect-error ts-error - displayTime: isMaterialBased() ? 4000 : 2000, + displayTime: isMaterialBased(current()) ? 4000 : 2000, hideOnOutsideClick: true, animation: tabletAndMobileAnimation, }; - // @ts-expect-error ts-error - return super._defaultOptionsRules().concat([ + + const toastRules: DefaultOptionsRule[] = [ { device(device): boolean { return device.deviceType === 'phone'; @@ -132,7 +142,7 @@ TProperties extends ToastProperties = ToastProperties, options: { width: `calc(100vw - ${DEFAULT_MARGIN * 2}px)`, ...tabletAndMobileCommonOptions, - }, + } as DeepPartial, }, { device(device): boolean { @@ -142,20 +152,26 @@ TProperties extends ToastProperties = ToastProperties, width: 'auto', maxWidth: '80vw', ...tabletAndMobileCommonOptions, - }, + } as DeepPartial, }, { device(device): boolean { - // @ts-expect-error ts-error - return isMaterialBased() && device.deviceType === 'desktop'; + return isMaterialBased(current()) && device.deviceType === 'desktop'; }, options: { minWidth: 344, maxWidth: 568, displayTime: 4000, - }, + } as DeepPartial, }, - ]); + ]; + + const rules = [ + ...super._defaultOptionsRules(), + ...toastRules, + ]; + + return rules; } _init(): void { @@ -164,23 +180,21 @@ TProperties extends ToastProperties = ToastProperties, this._posStringToObject(); } - // @ts-expect-error ts-error - _renderContentImpl() { + _renderContentImpl(): Promise { const { message, type } = this.option(); this._message = $('
') .addClass(TOAST_MESSAGE_CLASS) - // @ts-expect-error ts-error - .text(message) + .text(message ?? '') .appendTo(this.$content()); this.setAria('role', 'alert', this._message); - // @ts-expect-error ts-error - if (toastTypes.includes(type.toLowerCase())) { + + if (type && toastTypes.includes(type.toLowerCase())) { this.$content().prepend($('
').addClass(TOAST_ICON_CLASS)); } - super._renderContentImpl(); + return super._renderContentImpl(); } _render(): void { @@ -190,56 +204,83 @@ TProperties extends ToastProperties = ToastProperties, this.$wrapper().addClass(TOAST_WRAPPER_CLASS); const { type } = this.option(); - this.$content().addClass(TOAST_CLASS_PREFIX + String(type).toLowerCase()); + + if (type) { + this.$content().addClass(`${TOAST_CLASS}-${type.toLowerCase()}`); + } + this.$content().addClass(TOAST_CONTENT_CLASS); this._toggleCloseEvents('Swipe'); this._toggleCloseEvents('Click'); } - _toggleCloseEvents(event): void { + _toggleCloseEvents(event: 'Swipe' | 'Click'): void { const dxEvent = `dx${event.toLowerCase()}`; eventsEngine.off(this.$content(), dxEvent); - this.option(`closeOn${event}`) && eventsEngine.on(this.$content(), dxEvent, this.hide.bind(this)); + + const optionName = `closeOn${event}`; + const optionValue = this.option(optionName); + + if (optionValue) { + eventsEngine.on(this.$content(), dxEvent, this.hide.bind(this)); + } } _posStringToObject(): void { const { position } = this.option(); - if (!isString(position)) return; + if (!isString(position)) { + return; + } const verticalPosition = position.split(' ')[0]; const horizontalPosition = position.split(' ')[1]; - this.option('position', extend({ boundaryOffset: DEFAULT_BOUNDARY_OFFSET }, POSITION_ALIASES[verticalPosition])); + const newPosition = { + boundaryOffset: DEFAULT_BOUNDARY_OFFSET, + ...POSITION_ALIASES[verticalPosition], + }; + + this.option('position', newPosition); - // eslint-disable-next-line default-case switch (horizontalPosition) { case 'center': case 'left': - case 'right': - // @ts-expect-error ts-error - this.option('position').at += ` ${horizontalPosition}`; - // @ts-expect-error ts-error - this.option('position').my += ` ${horizontalPosition}`; + case 'right': { + if (newPosition && typeof newPosition === 'object') { + const at = `${newPosition.at as PositionAlignment} ${horizontalPosition}`; + const my = `${newPosition.my as PositionAlignment} ${horizontalPosition}`; + + this.option('position.at', at); + this.option('position.my', my); + } + break; + } + default: break; } } - _show(): DeferredObj { - // @ts-expect-error ts-error - return super._show.apply(this, arguments).always(() => { + _show(): DeferredObj | Promise { + const callback = (): void => { clearTimeout(this._hideTimeout); const { displayTime } = this.option(); - // eslint-disable-next-line @typescript-eslint/no-misused-promises + // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-restricted-globals this._hideTimeout = setTimeout(this.hide.bind(this), displayTime); - }); + }; + + const promise = super._show() as DeferredObj; + + promise.always(callback); + + return promise; } - // @ts-expect-error ts-error + // @ts-expect-error Violation of the Principle of Liskov Substitutability // eslint-disable-next-line class-methods-use-this _overlayStack(): Toast[] { return TOAST_STACK; @@ -259,8 +300,11 @@ TProperties extends ToastProperties = ToastProperties, switch (name) { case 'type': - this.$content().removeClass(TOAST_CLASS_PREFIX + previousValue); - this.$content().addClass(TOAST_CLASS_PREFIX + String(value).toLowerCase()); + this.$content().removeClass(`${TOAST_CLASS}-${previousValue}`); + + if (value) { + this.$content().addClass(`${TOAST_CLASS}-${String(value).toLowerCase()}`); + } break; case 'message': if (this._message) { diff --git a/packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.list.ts b/packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.list.ts index 3756d70ce9fb..a1b1b29f8fde 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.list.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.list.ts @@ -9,7 +9,7 @@ import type { ActionConfig } from '@ts/core/widget/component'; import type { ItemRenderInfo, ItemTemplate } from '@ts/ui/collection/collection_widget.base'; import { ListBase } from '@ts/ui/list/list.base'; -const TOOLBAR_MENU_ACTION_CLASS = 'dx-toolbar-menu-action'; +export const TOOLBAR_MENU_ACTION_CLASS = 'dx-toolbar-menu-action'; const TOOLBAR_HIDDEN_BUTTON_CLASS = 'dx-toolbar-hidden-button'; const TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS = 'dx-toolbar-hidden-button-group'; const TOOLBAR_MENU_SECTION_CLASS = 'dx-toolbar-menu-section'; @@ -19,12 +19,8 @@ const SCROLLVIEW_CONTENT_CLASS = 'dx-scrollview-content'; type ActionableComponents = Extract; export default class ToolbarMenuList extends ListBase { - _activeStateUnit!: string; - - _init(): void { - super._init(); - - this._activeStateUnit = `.${TOOLBAR_MENU_ACTION_CLASS}:not(.${TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS})`; + protected _activeStateUnit(): string { + return `.${TOOLBAR_MENU_ACTION_CLASS}:not(.${TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS})`; } _initMarkup(): void { diff --git a/packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.ts b/packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.ts index ea612a503e44..9668323e01af 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.ts @@ -18,7 +18,7 @@ import Widget from '@ts/core/widget/widget'; import Button from '@ts/ui/button/wrapper'; import type { ListBase } from '@ts/ui/list/list.base'; import Popup from '@ts/ui/popup/m_popup'; -import ToolbarMenuList from '@ts/ui/toolbar/internal/toolbar.menu.list'; +import ToolbarMenuList, { TOOLBAR_MENU_ACTION_CLASS } from '@ts/ui/toolbar/internal/toolbar.menu.list'; import { toggleItemFocusableElementTabIndex } from '@ts/ui/toolbar/toolbar.utils'; const DROP_DOWN_MENU_CLASS = 'dx-dropdownmenu'; @@ -277,8 +277,26 @@ export default class DropDownMenu extends Widget { dragEnabled: false, showTitle: false, fullScreen: false, + ignoreChildEvents: false, _fixWrapperPosition: true, }); + this._popup.registerKeyHandler('space', ( + e: DxEvent, + ) => { + this._popupKeyHandler(e); + }); + this._popup.registerKeyHandler('enter', ( + e: DxEvent, + ) => { + this._popupKeyHandler(e); + }); + this._popup.registerKeyHandler('escape', ( + e: DxEvent, + ): void => { + if (this._popup?.$overlayContent().is($(e.target))) { + this.option('opened', false); + } + }); } _getMaxHeight(): number { @@ -317,12 +335,10 @@ export default class DropDownMenu extends Widget { indicateLoading: false, noDataText: '', itemTemplate, - onItemClick: (e: ItemClickEvent): void => { - const { closeOnClick } = this.option(); - if (closeOnClick) { - this.option('opened', false); - } - this._itemClickAction?.(e); + onItemClick: ( + e: ItemClickEvent, + ) => { + this._itemClickHandler(e); }, tabIndex: -1, focusStateEnabled: false, @@ -332,6 +348,24 @@ export default class DropDownMenu extends Widget { }); } + _popupKeyHandler(e: DxEvent): void { + if ($(e.target).closest(`.${TOOLBAR_MENU_ACTION_CLASS}`).length) { + this._closePopup(); + } + } + + _closePopup(): void { + const { closeOnClick } = this.option(); + if (closeOnClick) { + this.option('opened', false); + } + } + + _itemClickHandler(e: ItemClickEvent): void { + this._closePopup(); + this._itemClickAction?.(e); + } + _itemOptionChanged( item: Item, property: 'disabled', diff --git a/packages/devextreme/js/__internal/ui/tree_view/tree_view.base.ts b/packages/devextreme/js/__internal/ui/tree_view/tree_view.base.ts index 76073d8c0f30..b523d9224097 100644 --- a/packages/devextreme/js/__internal/ui/tree_view/tree_view.base.ts +++ b/packages/devextreme/js/__internal/ui/tree_view/tree_view.base.ts @@ -121,6 +121,10 @@ class TreeViewBase extends HierarchicalCollectionWidget) => void; + protected _activeStateUnit(): string { + return `.${ITEM_CLASS}`; + } + _supportedKeys(): SupportedKeys { const click = (e: DxEvent): void => { const { focusedElement } = this.option(); @@ -507,8 +511,6 @@ class TreeViewBase extends HierarchicalCollectionWidget { @@ -1911,7 +1913,8 @@ class TreeViewBase extends HierarchicalCollectionWidget; + /** * @namespace DevExpress.aiIntegration */ @@ -211,4 +236,8 @@ export class AIIntegration { * @publicName translate(params, callbacks) */ translate(params: TranslateCommandParams, callbacks: RequestCallbacks): () => void; + /** + * @publicName smartPaste(params, callbacks) + */ + smartPaste(params: SmartPasteCommandParams, callbacks: RequestCallbacks): () => void; } diff --git a/packages/devextreme/js/core/dom_component.d.ts b/packages/devextreme/js/core/dom_component.d.ts index 8a9896e26434..7cfd3f609d89 100644 --- a/packages/devextreme/js/core/dom_component.d.ts +++ b/packages/devextreme/js/core/dom_component.d.ts @@ -33,12 +33,6 @@ export interface DOMComponentOptions extends ComponentOptions< InitializedEventInfo, OptionChangedEventInfo > { - /** - * @docid - * @default {} - * @public - */ - bindingOptions?: { [key: string]: any }; /** * @docid * @default {} diff --git a/packages/devextreme/js/events/events.types.d.ts b/packages/devextreme/js/events/events.types.d.ts index 89437ca85e1e..94973741edb6 100644 --- a/packages/devextreme/js/events/events.types.d.ts +++ b/packages/devextreme/js/events/events.types.d.ts @@ -1,7 +1,7 @@ /** * @namespace DevExpress.events */ -export type EventObject = { +export type EventObject = { /** * @docid * @public @@ -20,6 +20,13 @@ export type EventObject = { */ delegateTarget: Element; + /** + * @docid + * @public + * @type event + */ + originalEvent: TNativeEvent; + /** * @docid * @public @@ -79,7 +86,16 @@ export interface EventType { } * @type EventObject|jQuery.Event * */ -export type DxEvent = {} extends EventType ? (EventObject & TNativeEvent) : EventType; +export type DxEvent = {} extends EventType + ? (EventObject & TNativeEvent) + : (Omit & { + /** + * @docid + * @public + * @type event + */ + originalEvent: TNativeEvent; + }); /** @deprecated EventObject */ export type dxEvent = EventObject; diff --git a/packages/devextreme/js/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json index 678720f3f5fd..af9df666b948 100644 --- a/packages/devextreme/js/localization/messages/ar.json +++ b/packages/devextreme/js/localization/messages/ar.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "اختياري", "dxForm-requiredMessage": "{0} مطلوب", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "إعادة تعيين", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "يجب أن تكون القيمة رقمًا", "dxNumberBox-noDataText": "لايوجد بيانات", diff --git a/packages/devextreme/js/localization/messages/bg.json b/packages/devextreme/js/localization/messages/bg.json index 466235aabed9..1062875880cf 100644 --- a/packages/devextreme/js/localization/messages/bg.json +++ b/packages/devextreme/js/localization/messages/bg.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "Незадължително", "dxForm-requiredMessage": "{0} is required", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Нулиране", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Value must be a number", "dxNumberBox-noDataText": "Няма данни", diff --git a/packages/devextreme/js/localization/messages/ca.json b/packages/devextreme/js/localization/messages/ca.json index ee7d22d176d1..6dc4ad105c83 100644 --- a/packages/devextreme/js/localization/messages/ca.json +++ b/packages/devextreme/js/localization/messages/ca.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "opcional", "dxForm-requiredMessage": "{0} és obligatori", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Restablir", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "El valor ha de ser un número", "dxNumberBox-noDataText": "No hi ha dades", diff --git a/packages/devextreme/js/localization/messages/cs.json b/packages/devextreme/js/localization/messages/cs.json index 2cdb40068ae8..18eb5fac5c51 100644 --- a/packages/devextreme/js/localization/messages/cs.json +++ b/packages/devextreme/js/localization/messages/cs.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "volitelný", "dxForm-requiredMessage": "{0} je vyžadováno", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Reset", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Hodnota musí být číslo", "dxNumberBox-noDataText": "Žádná data", diff --git a/packages/devextreme/js/localization/messages/da.json b/packages/devextreme/js/localization/messages/da.json index 323ab6330e28..9c88db83cc46 100644 --- a/packages/devextreme/js/localization/messages/da.json +++ b/packages/devextreme/js/localization/messages/da.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "valgfri", "dxForm-requiredMessage": "{0} er påkrævet", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Nulstil", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Værdien skal være et tal", "dxNumberBox-noDataText": "Ingen data", diff --git a/packages/devextreme/js/localization/messages/de.json b/packages/devextreme/js/localization/messages/de.json index 63a2b38f4bbe..496b5c4ed825 100644 --- a/packages/devextreme/js/localization/messages/de.json +++ b/packages/devextreme/js/localization/messages/de.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "optional", "dxForm-requiredMessage": "{0} ist ein Pflichtfeld", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Zurücksetzen", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Der Wert muss eine Zahl sein", "dxNumberBox-noDataText": "Keine Daten", @@ -380,10 +383,10 @@ "dxAvatar-defaultImageAlt": "Avatar", "dxChat-elementAriaLabel": "Chat", - "dxChat-cancelEditingButtonAriaLabel": "Abbrechen", - "dxChat-editingMessageCaption": "Nachricht bearbeiten", "dxChat-textareaPlaceholder": "Geben Sie eine Nachricht ein", "dxChat-sendButtonAriaLabel": "Senden", + "dxChat-cancelEditingButtonAriaLabel": "Abbrechen", + "dxChat-editingMessageCaption": "Nachricht bearbeiten", "dxChat-defaultUserName": "Unbekannter Benutzer", "dxChat-messageListAriaLabel": "Nachrichtenliste", "dxChat-alertListAriaLabel": "Fehlerliste", diff --git a/packages/devextreme/js/localization/messages/el.json b/packages/devextreme/js/localization/messages/el.json index 3f39976a9396..69858f7abe86 100644 --- a/packages/devextreme/js/localization/messages/el.json +++ b/packages/devextreme/js/localization/messages/el.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "Προαιρετικό", "dxForm-requiredMessage": "Το πεδίο {0} είναι απαιτούμενο", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Επαναφορά", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Η τιμή πρέπει να είναι αριθμητική", "dxNumberBox-noDataText": "Δεν υπάρχουν δεδομένα", diff --git a/packages/devextreme/js/localization/messages/en.json b/packages/devextreme/js/localization/messages/en.json index 0903e09ee934..ca8e93449cca 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "optional", "dxForm-requiredMessage": "{0} is required", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Reset", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Value must be a number", "dxNumberBox-noDataText": "No data", diff --git a/packages/devextreme/js/localization/messages/es.json b/packages/devextreme/js/localization/messages/es.json index 0936bf34c5d6..e8b044885508 100644 --- a/packages/devextreme/js/localization/messages/es.json +++ b/packages/devextreme/js/localization/messages/es.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "opcional", "dxForm-requiredMessage": "{0} es obligatorio", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Reestablecer", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Valor debe ser un número", "dxNumberBox-noDataText": "Sin datos", diff --git a/packages/devextreme/js/localization/messages/fa.json b/packages/devextreme/js/localization/messages/fa.json index e2b7f11c4be9..dd84ada67b4c 100644 --- a/packages/devextreme/js/localization/messages/fa.json +++ b/packages/devextreme/js/localization/messages/fa.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "optional", "dxForm-requiredMessage": "{0} اجباری می باشد", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "شروع مجدد", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "مقدار وارد شده می بایست عدد باشد", "dxNumberBox-noDataText": "(بدون داده)", diff --git a/packages/devextreme/js/localization/messages/fi.json b/packages/devextreme/js/localization/messages/fi.json index acd12a301a69..a36b11aa7c64 100644 --- a/packages/devextreme/js/localization/messages/fi.json +++ b/packages/devextreme/js/localization/messages/fi.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "valinnainen", "dxForm-requiredMessage": "{0} on pakollinen", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Palauta", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Arvon on oltava numero", "dxNumberBox-noDataText": "Ei dataa", diff --git a/packages/devextreme/js/localization/messages/fr.json b/packages/devextreme/js/localization/messages/fr.json index 2455928ac03b..e7fcf886b762 100644 --- a/packages/devextreme/js/localization/messages/fr.json +++ b/packages/devextreme/js/localization/messages/fr.json @@ -14,8 +14,11 @@ "OK": "OK", "Today": "Aujourd'hui", "Yesterday": "Hier", + "dxCollectionWidget-noDataText": "Pas de données", + "dxDropDownEditor-selectLabel": "Sélection", + "validation-required": "Obligatoire", "validation-required-formatted": "{0} est obligatoire", "validation-numeric": "La valeur doit être un nombre", @@ -35,7 +38,9 @@ "validation-email": "L'adresse email est invalide", "validation-email-formatted": "{0} est invalide", "validation-mask": "La valeur est invalide", + "dxLookup-searchPlaceholder": "Nombre minimum de caractères: {0}", + "dxList-pullingDownText": "Tirez vers le bas pour actualiser...", "dxList-pulledDownText": "Relacher pour actualiser...", "dxList-refreshingText": "Actualisation...", @@ -51,20 +56,24 @@ "dxList-selectAll-notChecked": "Non vérifié", "dxList-ariaRoleDescription": "Liste", "dxList-listAriaLabel-itemContent": "Contenu de la liste", + "dxScrollView-pullingDownText": "Tirez vers le bas pour actualiser...", "dxScrollView-pulledDownText": "Relacher pour actualiser...", "dxScrollView-refreshingText": "Mise à jour...", "dxScrollView-reachBottomText": "Chargement...", + "dxDateBox-simulatedDataPickerTitleTime": "Choisissez l'heure", "dxDateBox-simulatedDataPickerTitleDate": "Choisissez la date", "dxDateBox-simulatedDataPickerTitleDateTime": "Choisissez la date et l'heure", "dxDateBox-validation-datetime": "La valeur doit être une date ou une heure.", + "dxDateRangeBox-invalidStartDateMessage": "La valeur de départ doit être une date", "dxDateRangeBox-invalidEndDateMessage": "La valeur de fin doit être une date", "dxDateRangeBox-startDateOutOfRangeMessage": "Start date is out of range", "dxDateRangeBox-endDateOutOfRangeMessage": "La date de fin est hors limite", "dxDateRangeBox-startDateLabel": "Date de début", "dxDateRangeBox-endDateLabel": "Date de fin", + "dxFileUploader-selectFile": "Choisissez un fichier", "dxFileUploader-dropFile": "Déposez un fichier", "dxFileUploader-bytes": "Bytes", @@ -79,14 +88,21 @@ "dxFileUploader-invalidFileExtension": "Type de fichier non autorisé", "dxFileUploader-invalidMaxFileSize": "Fichier trop volumineux", "dxFileUploader-invalidMinFileSize": "Fichier trop petit", + "dxRangeSlider-ariaFrom": "De {0}", "dxRangeSlider-ariaTill": "à {0}", "dxSwitch-switchedOnText": "ON", "dxSwitch-switchedOffText": "OFF", + "dxForm-optionalMark": "optionnel", "dxForm-requiredMessage": "{0} est obligatoire", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Réinitialiser", + "dxForm-submitButtonText": "Submit", + "dxNumberBox-invalidValueMessage": "La valeur doit être un nombre", "dxNumberBox-noDataText": "Pas de données", + "dxDataGrid-emptyHeaderWithColumnChooserText": "Utilisez {0} pour voir les colonnes", "dxDataGrid-emptyHeaderWithGroupPanelText": "Faites glisser une colonne depuis le panneau de groupe", "dxDataGrid-emptyHeaderWithColumnChooserAndGroupPanelText": "Utilisez {0} ou glisser une colonne depuis le panneau de groupe", @@ -191,11 +207,13 @@ "dxDataGrid-masterDetail": "Cellule avec détails", "dxDataGrid-moveColumnToTheRight": "Déplacer vers la droite", "dxDataGrid-moveColumnToTheLeft": "Déplacer vers la gauche", + "dxTreeList-ariaTreeList": "Liste arborescente avec {0} lignes et {1} colonnes", "dxTreeList-ariaExpandableInstruction": "Appuyez sur Ctrl + flèche droite pour développer le noeud ciblé et sur Ctrl + flèche gauche pour le réduire", "dxTreeList-ariaSearchInGrid": "Rechercher dans l'arborescence", "dxTreeList-ariaToolbar": "Barre d'outils de la liste arborescente", "dxTreeList-editingAddRowToNode": "Ajouter", + "dxPager-infoText": "Page {0} sur {1} ({2} élements)", "dxPager-pagesCountText": "sur", "dxPager-pageSize": "Elements par page : {0}", @@ -206,6 +224,7 @@ "dxPager-ariaLabel": "Navigation de la page", "dxPager-ariaPageSize": "Taille de la page", "dxPager-ariaPageNumber": "N° de page", + "dxPagination-infoText": "Page {0} sur {1} ({2} élements)", "dxPagination-pagesCountText": "sur", "dxPagination-pageSize": "Elements par page : {0}", @@ -216,6 +235,7 @@ "dxPagination-ariaLabel": "Navigation de la page", "dxPagination-ariaPageSize": "Taille de la page", "dxPagination-ariaPageNumber": "N° de page", + "dxPivotGrid-grandTotal": "Total général", "dxPivotGrid-total": "Total {0}", "dxPivotGrid-fieldChooserTitle": "Liste des champs", @@ -235,22 +255,30 @@ "dxPivotGrid-dataFieldArea": "Déposer les champs de données ici", "dxPivotGrid-rowFieldArea": "Déposer les champs de ligne ici", "dxPivotGrid-filterFieldArea": "Déposer les champs de filtre ici", + "dxScheduler-dateRange": "de {0} à {1}", "dxScheduler-ariaLabel": "Agenda. {0} vue : {1} avec {2} rendez-vous", "dxScheduler-ariaLabel-currentIndicator-present": "L'indicateur de temps actuel est visible dans la vue", "dxScheduler-ariaLabel-currentIndicator-not-present": "L'indicateur de l'heure actuelle n'est pas visible sur l'écran", + "dxScheduler-appointmentAriaLabel-group": "Groupe : {0}", "dxScheduler-appointmentAriaLabel-recurring": "Rendez-vous récurrent", + "dxScheduler-appointmentListAriaLabel": "Liste des rendez-vous ", + + "dxScheduler-editorLabelTitle": "Titre", "dxScheduler-editorLabelStartDate": "Date de début", "dxScheduler-editorLabelEndDate": "Date de fin", "dxScheduler-editorLabelDescription": "Description", "dxScheduler-editorLabelRecurrence": "Récurrence", + "dxScheduler-navigationToday": "Aujourd'hui", "dxScheduler-navigationPrevious": "Page précédente", "dxScheduler-navigationNext": "Page suivante", + "dxScheduler-openAppointment": "Définir un évenement", + "dxScheduler-recurrenceNever": "Jamais", "dxScheduler-recurrenceMinutely": "Par minute", "dxScheduler-recurrenceHourly": "Par heure", @@ -258,42 +286,55 @@ "dxScheduler-recurrenceWeekly": "Hebdomadaire", "dxScheduler-recurrenceMonthly": "Mensuel", "dxScheduler-recurrenceYearly": "Annuel", + "dxScheduler-recurrenceRepeatEvery": "Chaque", "dxScheduler-recurrenceRepeatOn": "Répète tous les", "dxScheduler-recurrenceEnd": "Jusqu'à", "dxScheduler-recurrenceAfter": "Après", "dxScheduler-recurrenceOn": "Le", + "dxScheduler-recurrenceUntilDateLabel": "Date de fin de la répétition", "dxScheduler-recurrenceOccurrenceLabel": "Nombre d'occurrences", + "dxScheduler-recurrenceRepeatMinutely": "minute(s)", "dxScheduler-recurrenceRepeatHourly": "hour(s)", "dxScheduler-recurrenceRepeatDaily": "Jour(s)", "dxScheduler-recurrenceRepeatWeekly": "Semaine(s)", "dxScheduler-recurrenceRepeatMonthly": "Mois(s)", "dxScheduler-recurrenceRepeatYearly": "Année(s)", + "dxScheduler-switcherDay": "Jour", "dxScheduler-switcherWeek": "Semaine", "dxScheduler-switcherWorkWeek": "Semaine de travail", "dxScheduler-switcherMonth": "Mois", + "dxScheduler-switcherAgenda": "Agenda", + "dxScheduler-switcherTimelineDay": "Timeline Jour", "dxScheduler-switcherTimelineWeek": "Timeline Semaine", "dxScheduler-switcherTimelineWorkWeek": "Timeline Semaine de travail", "dxScheduler-switcherTimelineMonth": "Timeline Mois", + "dxScheduler-recurrenceRepeatOnDate": "le", "dxScheduler-recurrenceRepeatCount": "occurence(s)", "dxScheduler-allDay": "Temps plein", + "dxScheduler-ariaEditForm": "Modifier le formulaire", + "dxScheduler-confirmRecurrenceEditTitle": "Modifier le rendez-vous récurrent", "dxScheduler-confirmRecurrenceDeleteTitle": "Supprimer un rendez-vous récurrent", + "dxScheduler-confirmRecurrenceEditMessage": "Voulez-vous éditer cet évenement ou la série entière ?", "dxScheduler-confirmRecurrenceDeleteMessage": "Voulez-vous supprimer cet évenement ou la série entière ?", + "dxScheduler-confirmRecurrenceEditSeries": "Editer serie", "dxScheduler-confirmRecurrenceDeleteSeries": "Supprimer serie", "dxScheduler-confirmRecurrenceEditOccurrence": "Editer évenement", "dxScheduler-confirmRecurrenceDeleteOccurrence": "Supprimer évenement", + "dxScheduler-noTimezoneTitle": "Pas de fuseau horaire", "dxScheduler-moreAppointments": "{0} en plus", + "dxCalendar-currentDay": "Aujourd'hui", "dxCalendar-currentMonth": "Mois courant", "dxCalendar-currentYear": "Année courante", @@ -318,6 +359,7 @@ "dxCalendar-selectedMultipleDateRange": "de {0} à {1}", "dxCalendar-selectedDateRangeCount": "Il y a {0} plages de dates sélectionnées", "dxCalendar-readOnlyLabel": "Calendrier en lecture seule", + "dxCardView-ariaSearchInGrid": "Rechercher dans la vue carte", "dxCardView-ariaHeaderItemLabel": "Nom du champ {0}", "dxCardView-ariaHeaderItemSortingAscendingLabel": "Tri par ordre croissant", @@ -337,7 +379,9 @@ "dxCardView-headerItemDropZoneText": "Déposez l'élément d'en-tête ici", "dxCardView-emptyHeaderPanelText": "Utilisez {0} pour afficher les colonnes", "dxCardView-emptyHeaderPanelColumnChooserText": "sélecteur de colonnes", + "dxAvatar-defaultImageAlt": "Avatar", + "dxChat-elementAriaLabel": "Chat", "dxChat-textareaPlaceholder": "Tapez un message", "dxChat-sendButtonAriaLabel": "Envoyer", @@ -358,19 +402,23 @@ "dxChat-editingDeleteConfirmText": "Êtes-vous sûr de vouloir supprimer ce message ?", "dxChat-deletedMessageText": "Le message a été supprimé", "dxChat-defaultImageAlt": "Image partagée dans le chat", + "dxColorView-ariaRed": "Rouge", "dxColorView-ariaGreen": "Vert", "dxColorView-ariaBlue": "Bleu", "dxColorView-ariaAlpha": "Transparence", "dxColorView-ariaHex": "Code couleur", + "dxTagBox-selected": "{0} selectionnés", "dxTagBox-allSelected": "Tous sélectionnés ({0})", "dxTagBox-moreSelected": "{0} en plus", "dxTagBox-tagRoleDescription": "Balise. Appuyez sur le bouton Supprimer pour supprimer cette balise.", "dxTagBox-ariaRoleDescription": "Boîte à balise", + "vizExport-printingButtonText": "Imprimer", "vizExport-titleMenuText": "Exporter/Imprimer", "vizExport-exportButtonText": "{0} fichier", + "dxFilterBuilder-and": "Et", "dxFilterBuilder-or": "Ou", "dxFilterBuilder-notAnd": "Non Et", @@ -402,6 +450,7 @@ "dxFilterBuilder-filterAriaItemField": "champs", "dxFilterBuilder-filterAriaItemOperation": "opération", "dxFilterBuilder-filterAriaItemValue": "valeur", + "dxHtmlEditor-dialogColorCaption": "Changer couleur police", "dxHtmlEditor-dialogBackgroundCaption": "Changer couleur fond", "dxHtmlEditor-dialogLinkCaption": "Ajouter un hyperlien", @@ -511,6 +560,7 @@ "dxHtmlEditor-aiToolbarItemAriaLabel": "Élément de la barre d'outils de l'assistant AI", "dxHtmlEditor-aiResultTextAreaAriaLabel": "AI Assistant result", "dxHtmlEditor-aiAskPlaceholder": "Demandez à l'IA de modifier le texte", + "dxFileManager-newDirectoryName": "Répertoire sans titre", "dxFileManager-rootDirectoryName": "Fichiers", "dxFileManager-errorNoAccess": "Accès interdit. L'opération ne peut se terminer.", @@ -523,6 +573,7 @@ "dxFileManager-errorInvalidSymbols": "Ce nom contient des caractères invalides.", "dxFileManager-errorDefault": "Erreur non spécifiée.", "dxFileManager-errorDirectoryOpenFailed": "Le répertoire ne peut s'ouvrir", + "dxFileManager-commandCreate": "Nouveau répertoire", "dxFileManager-commandRename": "Renommer", "dxFileManager-commandMove": "Déplacer", @@ -535,6 +586,7 @@ "dxFileManager-commandDetails": "Mode détails", "dxFileManager-commandClearSelection": "Vider sélection", "dxFileManager-commandShowNavPane": "Basculer le volet de navigation", + "dxFileManager-dialogDirectoryChooserMoveTitle": "Déplacer vers", "dxFileManager-dialogDirectoryChooserMoveButtonText": "Déplacer", "dxFileManager-dialogDirectoryChooserCopyTitle": "Copier vers", @@ -548,14 +600,17 @@ "dxFileManager-dialogDeleteItemSingleItemConfirmation": "Voulez-vous vraiment supprimer {0}?", "dxFileManager-dialogDeleteItemMultipleItemsConfirmation": "Voulez-vous vraiment supprimer {0}?", "dxFileManager-dialogButtonCancel": "Annuler", + "dxFileManager-editingCreateSingleItemProcessingMessage": "Créer un répertoire dans {0}", "dxFileManager-editingCreateSingleItemSuccessMessage": "Répertoire créé dans {0}", "dxFileManager-editingCreateSingleItemErrorMessage": "Répertoire n'est pas créé", "dxFileManager-editingCreateCommonErrorMessage": "Répertoire n'est pas créé", + "dxFileManager-editingRenameSingleItemProcessingMessage": "Renommer un item dans {0}", "dxFileManager-editingRenameSingleItemSuccessMessage": "Element renommé dans {0}", "dxFileManager-editingRenameSingleItemErrorMessage": "Element non renommé", "dxFileManager-editingRenameCommonErrorMessage": "Element non renommé", + "dxFileManager-editingDeleteSingleItemProcessingMessage": "Supprimer un élement de {0}", "dxFileManager-editingDeleteMultipleItemsProcessingMessage": "Supprimer {0} élements de {1}", "dxFileManager-editingDeleteSingleItemSuccessMessage": "Element supprimé de {0}", @@ -563,6 +618,7 @@ "dxFileManager-editingDeleteSingleItemErrorMessage": "Element non suprimé", "dxFileManager-editingDeleteMultipleItemsErrorMessage": "{0} élements non supprimés", "dxFileManager-editingDeleteCommonErrorMessage": "Des élements ne sont pas supprimés", + "dxFileManager-editingMoveSingleItemProcessingMessage": "En train de déplacer un élement vers {0}", "dxFileManager-editingMoveMultipleItemsProcessingMessage": "En train de déplacer {0} élements vers {1}", "dxFileManager-editingMoveSingleItemSuccessMessage": "Element déplacé vers {0}", @@ -570,6 +626,7 @@ "dxFileManager-editingMoveSingleItemErrorMessage": "Element non déplacé", "dxFileManager-editingMoveMultipleItemsErrorMessage": "{0} élements non déplacés", "dxFileManager-editingMoveCommonErrorMessage": "Des élements ne sont pas déplacés", + "dxFileManager-editingCopySingleItemProcessingMessage": "En train de copier un élement vers {0}", "dxFileManager-editingCopyMultipleItemsProcessingMessage": "En train de copier {0} élementss vers {1}", "dxFileManager-editingCopySingleItemSuccessMessage": "Element copié vers {0}", @@ -577,6 +634,7 @@ "dxFileManager-editingCopySingleItemErrorMessage": "Element non copié", "dxFileManager-editingCopyMultipleItemsErrorMessage": "{0} élements non copiés", "dxFileManager-editingCopyCommonErrorMessage": "Des élements ne sont pas copiés", + "dxFileManager-editingUploadSingleItemProcessingMessage": "En train de téléverser un élement vers {0}", "dxFileManager-editingUploadMultipleItemsProcessingMessage": "En train de téléverser {0} élements vers {1}", "dxFileManager-editingUploadSingleItemSuccessMessage": "Element téléversé vers {0}", @@ -584,21 +642,27 @@ "dxFileManager-editingUploadSingleItemErrorMessage": "Element non téléversé", "dxFileManager-editingUploadMultipleItemsErrorMessage": "{0} élements non téléversés", "dxFileManager-editingUploadCanceledMessage": "Annulé", + "dxFileManager-editingDownloadSingleItemErrorMessage": "L'élément n'a pas été téléchargé", "dxFileManager-editingDownloadMultipleItemsErrorMessage": "{0} éléments n'ont pas été téléchargés", + "dxFileManager-listDetailsColumnCaptionName": "Nom", "dxFileManager-listDetailsColumnCaptionDateModified": "Date modifié", "dxFileManager-listDetailsColumnCaptionFileSize": "Taille de fichier", + "dxFileManager-listThumbnailsTooltipTextSize": "Taille", "dxFileManager-listThumbnailsTooltipTextDateModified": "Date modifié", + "dxFileManager-notificationProgressPanelTitle": "En cours", "dxFileManager-notificationProgressPanelEmptyListText": "Aucune opération", "dxFileManager-notificationProgressPanelOperationCanceled": "Annulé", + "dxDiagram-categoryGeneral": "Général", "dxDiagram-categoryFlowchart": "Organigramme", "dxDiagram-categoryOrgChart": "Structure organisationnelle", "dxDiagram-categoryContainers": "Conteneurs", "dxDiagram-categoryCustom": "Personnalisé", + "dxDiagram-commandExportToSvg": "Exporter en SVG", "dxDiagram-commandExportToPng": "Exporter en PNG", "dxDiagram-commandExportToJpg": "Exporter en JPEG", @@ -660,15 +724,18 @@ "dxDiagram-commandLayoutRightToLeft": "Droite à gauche", "dxDiagram-commandLayoutTopToBottom": "Haut en bas", "dxDiagram-commandLayoutBottomToTop": "Bas en haut", + "dxDiagram-unitIn": "po", "dxDiagram-unitCm": "cm", "dxDiagram-unitPx": "px", + "dxDiagram-dialogButtonOK": "OK", "dxDiagram-dialogButtonCancel": "Annuler", "dxDiagram-dialogInsertShapeImageTitle": "Insérer une image", "dxDiagram-dialogEditShapeImageTitle": "Changer image", "dxDiagram-dialogEditShapeImageSelectButton": "Sélectionner une image", "dxDiagram-dialogEditShapeImageLabelText": "ou déposer le fichier ici", + "dxDiagram-uiExport": "Exporter", "dxDiagram-uiProperties": "Propriétés", "dxDiagram-uiSettings": "Paramètres", @@ -683,6 +750,7 @@ "dxDiagram-uiObject": "Objet", "dxDiagram-uiConnector": "Connecteur", "dxDiagram-uiPage": "Page", + "dxDiagram-shapeText": "Texte", "dxDiagram-shapeRectangle": "Rectangle", "dxDiagram-shapeEllipse": "Ellipse", @@ -728,6 +796,7 @@ "dxDiagram-shapeCardWithImageOnLeft": "Carte avec image à gauche", "dxDiagram-shapeCardWithImageOnTop": "Carte avec image sur le dessus", "dxDiagram-shapeCardWithImageOnRight": "Carte avec image à droite", + "dxGantt-dialogTitle": "Titre", "dxGantt-dialogStartTitle": "Départ", "dxGantt-dialogEndTitle": "Fin", @@ -771,13 +840,17 @@ "dxGantt-showDependencies": "Afficher les dépendances", "dxGantt-dialogStartDateValidation": "La date de début doit être postérieure au {0}", "dxGantt-dialogEndDateValidation": "La date de fin doit être postérieure au {0}", + "dxGallery-itemName": "Élément de la galerie", + "dxMultiView-elementAriaRoleDescription": "MultiView", "dxMultiView-elementAriaLabel": "Utilisez les touches fléchées ou faites glisser pour naviguer entre les vues", "dxMultiView-itemAriaRoleDescription": "Vue", "dxMultiView-itemAriaLabel": "{0} de {1}", + "dxSplitter-resizeHandleAriaLabel": "Barre de séparation", "dxSplitter-resizeHandleAriaRoleDescription": "Séparateur", + "dxStepper-optionalMark": "(Optionel)" } } diff --git a/packages/devextreme/js/localization/messages/hu.json b/packages/devextreme/js/localization/messages/hu.json index d4f66f707402..ce684dfcd692 100644 --- a/packages/devextreme/js/localization/messages/hu.json +++ b/packages/devextreme/js/localization/messages/hu.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "választható", "dxForm-requiredMessage": "{0} kötelező", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Visszaállítás", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Az érték szám kell legyen", "dxNumberBox-noDataText": "Nincs adat", diff --git a/packages/devextreme/js/localization/messages/it.json b/packages/devextreme/js/localization/messages/it.json index fc722e68866c..737486afa4a7 100644 --- a/packages/devextreme/js/localization/messages/it.json +++ b/packages/devextreme/js/localization/messages/it.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "opzionale", "dxForm-requiredMessage": "{0} è richiesto", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Annulla", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Il valore deve essere numerico", "dxNumberBox-noDataText": "Nessun dato", diff --git a/packages/devextreme/js/localization/messages/ja.json b/packages/devextreme/js/localization/messages/ja.json index 5f8863be0c1e..3bb159a1c3c3 100644 --- a/packages/devextreme/js/localization/messages/ja.json +++ b/packages/devextreme/js/localization/messages/ja.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "任意", "dxForm-requiredMessage": "{0} は必須フィールドです", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "リセット", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "数値を指定してください。", "dxNumberBox-noDataText": "データがありません", diff --git a/packages/devextreme/js/localization/messages/lt.json b/packages/devextreme/js/localization/messages/lt.json index 77b620552d83..63fb483024fd 100644 --- a/packages/devextreme/js/localization/messages/lt.json +++ b/packages/devextreme/js/localization/messages/lt.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "neprivaloma", "dxForm-requiredMessage": "{0} privalomas", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Reset", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Reikšmė turi būti skaičius", "dxNumberBox-noDataText": "Nėra duomenų", diff --git a/packages/devextreme/js/localization/messages/lv.json b/packages/devextreme/js/localization/messages/lv.json index 373fca31b983..db10d24ba227 100644 --- a/packages/devextreme/js/localization/messages/lv.json +++ b/packages/devextreme/js/localization/messages/lv.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "neobligāts", "dxForm-requiredMessage": "{0} ir obligāts", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Atiestatīt", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Vērtībai jābūt skaitlim", "dxNumberBox-noDataText": "Nav datu", diff --git a/packages/devextreme/js/localization/messages/nb.json b/packages/devextreme/js/localization/messages/nb.json index 39781f9e387a..65463cbb84b8 100644 --- a/packages/devextreme/js/localization/messages/nb.json +++ b/packages/devextreme/js/localization/messages/nb.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "Valgfri", "dxForm-requiredMessage": "{0} er påkrevd", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Tilbakestill", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Verdien må være et tall", "dxNumberBox-noDataText": "Ingen data", diff --git a/packages/devextreme/js/localization/messages/nl.json b/packages/devextreme/js/localization/messages/nl.json index d36d95de566c..5865f08523e9 100644 --- a/packages/devextreme/js/localization/messages/nl.json +++ b/packages/devextreme/js/localization/messages/nl.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "optioneel", "dxForm-requiredMessage": "{0} is verplicht", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Reset", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Waarde moet een nummer zijn", "dxNumberBox-noDataText": "Geen gegevens", diff --git a/packages/devextreme/js/localization/messages/pl.json b/packages/devextreme/js/localization/messages/pl.json index 2e67b2628264..a7f7ae61be8c 100644 --- a/packages/devextreme/js/localization/messages/pl.json +++ b/packages/devextreme/js/localization/messages/pl.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "opcjonalnie", "dxForm-requiredMessage": "{0} jest polem obowiązkowym", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Resetuj", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Wartość musi być liczbą", "dxNumberBox-noDataText": "Brak danych", diff --git a/packages/devextreme/js/localization/messages/pt.json b/packages/devextreme/js/localization/messages/pt.json index 47e4b9615963..26e06bc1cf97 100644 --- a/packages/devextreme/js/localization/messages/pt.json +++ b/packages/devextreme/js/localization/messages/pt.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "opcional", "dxForm-requiredMessage": "{0} é de preenchimento obrigatório", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Limpar", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Valor deve ser um número", "dxNumberBox-noDataText": "Sem dados", diff --git a/packages/devextreme/js/localization/messages/ro.json b/packages/devextreme/js/localization/messages/ro.json index 9bcda6132d01..d7164c4a6253 100644 --- a/packages/devextreme/js/localization/messages/ro.json +++ b/packages/devextreme/js/localization/messages/ro.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "optional", "dxForm-requiredMessage": "{0} este obligatoriu", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Resetare", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Valoarea trebuie sa fie un numră", "dxNumberBox-noDataText": "Nu există date", diff --git a/packages/devextreme/js/localization/messages/ru.json b/packages/devextreme/js/localization/messages/ru.json index cf84df52165a..496ade9c20a2 100644 --- a/packages/devextreme/js/localization/messages/ru.json +++ b/packages/devextreme/js/localization/messages/ru.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "необязательный", "dxForm-requiredMessage": " Поле {0} должно быть заполнено", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Сбросить", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Значение должно быть числом", "dxNumberBox-noDataText": "Нет данных", diff --git a/packages/devextreme/js/localization/messages/sl.json b/packages/devextreme/js/localization/messages/sl.json index 2020434ac668..c518f89f711b 100644 --- a/packages/devextreme/js/localization/messages/sl.json +++ b/packages/devextreme/js/localization/messages/sl.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "opcijsko", "dxForm-requiredMessage": "Podatek {0} je obvezen", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Ponastavi", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Vrednost mora biti število", "dxNumberBox-noDataText": "Ni podatkov", diff --git a/packages/devextreme/js/localization/messages/sv.json b/packages/devextreme/js/localization/messages/sv.json index 860c85313271..24bddfc003fe 100644 --- a/packages/devextreme/js/localization/messages/sv.json +++ b/packages/devextreme/js/localization/messages/sv.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "valfri", "dxForm-requiredMessage": "{0} är nödvändigt", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Återställ", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Värdet måste vara ett nummer", "dxNumberBox-noDataText": "Inget data", diff --git a/packages/devextreme/js/localization/messages/tr.json b/packages/devextreme/js/localization/messages/tr.json index 9cc0552c929a..2781e7c7dfa5 100644 --- a/packages/devextreme/js/localization/messages/tr.json +++ b/packages/devextreme/js/localization/messages/tr.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "isteğe bağlı", "dxForm-requiredMessage": "{0} gerekli", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Sıfırla", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Değer bir sayı olmalı", "dxNumberBox-noDataText": "Veri yok", diff --git a/packages/devextreme/js/localization/messages/uk.json b/packages/devextreme/js/localization/messages/uk.json index e05012b155ef..0eadfedffba8 100644 --- a/packages/devextreme/js/localization/messages/uk.json +++ b/packages/devextreme/js/localization/messages/uk.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "необов'язково", "dxForm-requiredMessage": "{0} — обов'язкове поле", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Скинути", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Значення має бути числом", "dxNumberBox-noDataText": "Немає даних", @@ -263,6 +266,7 @@ "dxScheduler-appointmentListAriaLabel": "Список подій", + "dxScheduler-editorLabelTitle": "Тема", "dxScheduler-editorLabelStartDate": "Дата початку", "dxScheduler-editorLabelEndDate": "Дата завершення", diff --git a/packages/devextreme/js/localization/messages/vi.json b/packages/devextreme/js/localization/messages/vi.json index 630ba1c646c1..f790da48a85c 100644 --- a/packages/devextreme/js/localization/messages/vi.json +++ b/packages/devextreme/js/localization/messages/vi.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "tùy chọn", "dxForm-requiredMessage": "{0} là bắt buộc", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "Làm lại", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "Giá trị phải là một số", "dxNumberBox-noDataText": "Không có dữ liệu", diff --git a/packages/devextreme/js/localization/messages/zh-tw.json b/packages/devextreme/js/localization/messages/zh-tw.json index 56883bc965d5..c123b98912c7 100644 --- a/packages/devextreme/js/localization/messages/zh-tw.json +++ b/packages/devextreme/js/localization/messages/zh-tw.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "可選", "dxForm-requiredMessage": "{0} 是必須的", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "重置", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "值必須是一個數字", "dxNumberBox-noDataText": "無資料", diff --git a/packages/devextreme/js/localization/messages/zh.json b/packages/devextreme/js/localization/messages/zh.json index 10cc097e454e..2ff0087107ac 100644 --- a/packages/devextreme/js/localization/messages/zh.json +++ b/packages/devextreme/js/localization/messages/zh.json @@ -96,6 +96,9 @@ "dxForm-optionalMark": "可选", "dxForm-requiredMessage": "{0} 是必须的", + "dxForm-smartPasteButtonText": "Smart Paste", + "dxForm-resetButtonText": "重置", + "dxForm-submitButtonText": "Submit", "dxNumberBox-invalidValueMessage": "值必须是一个数字", "dxNumberBox-noDataText": "无数据", diff --git a/packages/devextreme/js/ui/calendar.d.ts b/packages/devextreme/js/ui/calendar.d.ts index 4d710412e16a..068c55287281 100644 --- a/packages/devextreme/js/ui/calendar.d.ts +++ b/packages/devextreme/js/ui/calendar.d.ts @@ -78,7 +78,7 @@ export type ValueChangedEvent = NativeEventInfo { */ allowDeleting?: boolean | ((options: { component?: dxChat; message?: Message }) => boolean); }; + /** + * @docid + * @default null + * @type_function_return string|Element|jQuery + * @public + */ + emptyViewTemplate?: template | null | ((data: EmptyViewTemplateData, itemElement: DxElement) => string | UserDefinedElement); /** * @docid * @type string | Array | Store | DataSource | DataSourceOptions | null diff --git a/packages/devextreme/js/ui/chat_types.d.ts b/packages/devextreme/js/ui/chat_types.d.ts index 2f45a3579e02..d0aa33d1040c 100644 --- a/packages/devextreme/js/ui/chat_types.d.ts +++ b/packages/devextreme/js/ui/chat_types.d.ts @@ -17,6 +17,7 @@ export { ImageMessage, Message, MessageTemplateData, + EmptyViewTemplateData, dxChatOptions, Properties, } from './chat'; diff --git a/packages/devextreme/js/ui/color_box.d.ts b/packages/devextreme/js/ui/color_box.d.ts index 41551725a010..db4af74e31d4 100644 --- a/packages/devextreme/js/ui/color_box.d.ts +++ b/packages/devextreme/js/ui/color_box.d.ts @@ -197,6 +197,7 @@ export interface dxColorBoxOptions extends dxDropDownEditorOptions { * @default null * @type_function_return string|Element|jQuery * @public + * @deprecated dxDropDownEditorOptions.fieldAddons */ fieldTemplate?: template | ((value: string, fieldElement: DxElement) => string | UserDefinedElement); /** diff --git a/packages/devextreme/js/ui/date_box.d.ts b/packages/devextreme/js/ui/date_box.d.ts index 8670e765003d..cf861571af1c 100644 --- a/packages/devextreme/js/ui/date_box.d.ts +++ b/packages/devextreme/js/ui/date_box.d.ts @@ -294,7 +294,10 @@ export interface dxDateBoxOptions extends DateBoxBaseOptions { * @docid * @namespace DevExpress.ui */ -export interface DateBoxBaseOptions extends dxDropDownEditorOptions { +export interface DateBoxBaseOptions extends Omit< + dxDropDownEditorOptions, + 'fieldAddons' +> { /** * @docid * @default "OK" diff --git a/packages/devextreme/js/ui/date_box.js b/packages/devextreme/js/ui/date_box.js index f3b9f7c8aff4..325842a65de7 100644 --- a/packages/devextreme/js/ui/date_box.js +++ b/packages/devextreme/js/ui/date_box.js @@ -2,3 +2,8 @@ import DateBox from '../__internal/ui/date_box/m_date_box'; export default DateBox; // STYLE dateBox + +/** + * @name dxDateBoxOptions.fieldAddons + * @hidden + */ diff --git a/packages/devextreme/js/ui/date_range_box.js b/packages/devextreme/js/ui/date_range_box.js index 4231fd3251e6..8d4a39093ef4 100644 --- a/packages/devextreme/js/ui/date_range_box.js +++ b/packages/devextreme/js/ui/date_range_box.js @@ -3,6 +3,10 @@ export default DateRangeBox; // STYLE dateRangeBox +/** + * @name dxDateRangeBoxOptions.fieldAddons + * @hidden + */ /** * @name dxDateRangeBoxOptions.inputAttr * @hidden diff --git a/packages/devextreme/js/ui/dialog.js b/packages/devextreme/js/ui/dialog.js index 6599629338aa..a74489f9dd0b 100644 --- a/packages/devextreme/js/ui/dialog.js +++ b/packages/devextreme/js/ui/dialog.js @@ -2,7 +2,7 @@ export { confirm, alert, custom -} from '../__internal/ui/m_dialog'; +} from '../__internal/ui/dialog'; /** * @name ui.dialog diff --git a/packages/devextreme/js/ui/drop_down_box.d.ts b/packages/devextreme/js/ui/drop_down_box.d.ts index 913dc80298a1..4e82376db99f 100644 --- a/packages/devextreme/js/ui/drop_down_box.d.ts +++ b/packages/devextreme/js/ui/drop_down_box.d.ts @@ -211,6 +211,7 @@ export interface dxDropDownBoxOptions extends DataExpressionMixinOptions string | UserDefinedElement); /** diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.d.ts b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.d.ts index 698bec39036e..b642b05b9f86 100644 --- a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.d.ts +++ b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.d.ts @@ -35,6 +35,25 @@ export interface DropDownButtonTemplateDataModel { readonly icon?: string; } +/** + * @namespace DevExpress.ui + * @docid + */ +export type FieldAddons = { + /** + * @docid + * @type_function_param1 data:object + * @type_function_return string|Element|jQuery + */ + beforeTemplate?: template | ((data: any, element: DxElement) => string | UserDefinedElement); + /** + * @docid + * @type_function_param1 data:object + * @type_function_return string|Element|jQuery + */ + afterTemplate?: template | ((data: any, element: DxElement) => string | UserDefinedElement); +}; + /** * @namespace DevExpress.ui * @docid @@ -86,6 +105,12 @@ export interface dxDropDownEditorOptions extends Omit string | UserDefinedElement); + /** + * @docid + * @default null + * @public + */ + fieldAddons?: FieldAddons; /** * @docid * @default null diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.d.ts b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.d.ts index 194d825e4613..751cb611fcc1 100644 --- a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.d.ts +++ b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.d.ts @@ -45,7 +45,10 @@ export interface SelectionChangedInfo { * @docid * @hidden */ -export interface dxDropDownListOptions extends DataExpressionMixinOptions, dxDropDownEditorOptions { +export interface dxDropDownListOptions extends DataExpressionMixinOptions, Omit< + dxDropDownEditorOptions, + 'fieldAddons' +> { /** * @docid * @readonly diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js index 80d5eef1dc20..796e99ab15e5 100644 --- a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js +++ b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_list.js @@ -7,6 +7,11 @@ export default DropDownList; * @hidden */ +/** + * @name dxDropDownListOptions.fieldAddons + * @hidden + */ + /** * @name dxDropDownListOptions.applyValueMode * @hidden diff --git a/packages/devextreme/js/ui/file_manager/ui.file_manager.items_list.thumbnails.list_box.js b/packages/devextreme/js/ui/file_manager/ui.file_manager.items_list.thumbnails.list_box.js index fc1d3ed5c127..de85c6958d30 100644 --- a/packages/devextreme/js/ui/file_manager/ui.file_manager.items_list.thumbnails.list_box.js +++ b/packages/devextreme/js/ui/file_manager/ui.file_manager.items_list.thumbnails.list_box.js @@ -12,7 +12,7 @@ import { BindableTemplate } from '../../core/templates/bindable_template'; import ScrollView from '../scroll_view'; import CollectionWidget from '../collection/ui.collection_widget.edit'; -import Selection from '../../__internal/ui/selection/m_selection'; +import Selection from '../../__internal/ui/selection/selection'; const FILE_MANAGER_THUMBNAILS_VIEW_PORT_CLASS = 'dx-filemanager-thumbnails-view-port'; const FILE_MANAGER_THUMBNAILS_ITEM_LIST_CONTAINER_CLASS = 'dx-filemanager-thumbnails-container'; diff --git a/packages/devextreme/js/ui/file_uploader.js b/packages/devextreme/js/ui/file_uploader.js index 29f76ccce87c..28b6c53e40ca 100644 --- a/packages/devextreme/js/ui/file_uploader.js +++ b/packages/devextreme/js/ui/file_uploader.js @@ -1,4 +1,4 @@ -import FileUploader from '../__internal/ui/m_file_uploader'; +import FileUploader from '../__internal/ui/file_uploader'; export default FileUploader; diff --git a/packages/devextreme/js/ui/form.d.ts b/packages/devextreme/js/ui/form.d.ts index fbd92a6981e6..e512bb3815b4 100644 --- a/packages/devextreme/js/ui/form.d.ts +++ b/packages/devextreme/js/ui/form.d.ts @@ -1,3 +1,4 @@ +import { AIIntegration } from '../common/ai-integration'; import { UserDefinedElement, DxElement, @@ -24,9 +25,10 @@ import { } from '../common'; import { - EventInfo, - InitializedEventInfo, - ChangedOptionInfo, + AsyncCancelable, + EventInfo, + InitializedEventInfo, + ChangedOptionInfo, } from '../common/core/events'; import dxButton, { @@ -61,6 +63,23 @@ export type FormItemType = 'empty' | 'group' | 'simple' | 'tabbed' | 'button'; export type LabelLocation = 'left' | 'right' | 'top'; /** @public */ export type FormLabelMode = 'static' | 'floating' | 'hidden' | 'outside'; +/** @public */ +export type FormPredefinedButtonItem = 'reset' | 'submit' | 'smartPaste'; + +/** @public */ +export type AIResult = Record; + +/** + * @docid + * @hidden + */ +export type SmartPasteInfo = { + /** + * @docid + * @type object + */ + readonly aiResult: AIResult; +}; /** * @docid _ui_form_ContentReadyEvent @@ -121,6 +140,22 @@ export type InitializedEvent = InitializedEventInfo; */ export type OptionChangedEvent = EventInfo & ChangedOptionInfo; +/** + * @docid _ui_form_SmartPastingEvent + * @public + * @type object + * @inherits EventInfo,AsyncCancelable,SmartPasteInfo + */ +export type SmartPastingEvent = EventInfo & AsyncCancelable & SmartPasteInfo; + +/** + * @docid _ui_form_SmartPastedEvent + * @public + * @type object + * @inherits EventInfo,SmartPasteInfo + */ +export type SmartPastedEvent = EventInfo & SmartPasteInfo; + /** @public */ export type GroupItemTemplateData = { readonly component: dxForm; @@ -152,6 +187,12 @@ export type SimpleItemLabelTemplateData = SimpleItemTemplateData & { text: strin * @docid */ export interface dxFormOptions extends WidgetOptions { + /** + * @docid + * @default undefined + * @public + */ + aiIntegration?: AIIntegration | undefined; /** * @docid * @default true @@ -217,7 +258,7 @@ export interface dxFormOptions extends WidgetOptions { * @default "outside" * @public */ - labelMode?: FormLabelMode; + labelMode?: FormLabelMode; /** * @docid * @default 200 @@ -240,6 +281,22 @@ export interface dxFormOptions extends WidgetOptions { * @public */ onFieldDataChanged?: ((e: FieldDataChangedEvent) => void); + /** + * @docid + * @default null + * @type_function_param1 e:{ui/form:SmartPastingEvent} + * @action + * @public + */ + onSmartPasting?: ((e: SmartPastingEvent) => void); + /** + * @docid + * @default null + * @type_function_param1 e:{ui/form:SmartPastedEvent} + * @action + * @public + */ + onSmartPasted?: ((e: SmartPastedEvent) => void); /** * @docid * @default "optional" @@ -365,6 +422,13 @@ export default class dxForm extends Widget { * @public */ reset(editorsData?: Record): void; + /** + * @docid + * @publicName smartPaste(text) + * @param1 text:string|undefined + * @public + */ + smartPaste(text?: string): void; /** * @docid * @publicName updateData(data) @@ -447,7 +511,7 @@ export interface dxFormButtonItem { * @default undefined * @public */ - name?: string | undefined; + name?: FormPredefinedButtonItem | string | undefined; /** * @docid * @default "top" @@ -624,6 +688,24 @@ export type SimpleItem = dxFormSimpleItem; * @namespace DevExpress.ui */ export interface dxFormSimpleItem { + /** + * @docid + * @public + */ + aiOptions?: { + /** + * @docid + * @default undefined + * @public + */ + instruction?: string | undefined; + /** + * @docid + * @default false + * @public + */ + disabled?: boolean; + }; /** * @docid * @default undefined @@ -874,7 +956,8 @@ export type Options = dxFormOptions; // type FilterOutHidden = Omit; -// type EventsIntegrityCheckingHelper = CheckedEvents, Required, 'onEditorEnterKey' | 'onFieldDataChanged'>; +// type EventsIntegrityCheckingHelper = CheckedEvents, Required, 'onEditorEnterKey' +// | 'onFieldDataChanged' | 'onSmartPasting' | 'onSmartPasted'>; /** * @hidden diff --git a/packages/devextreme/js/ui/form_types.d.ts b/packages/devextreme/js/ui/form_types.d.ts index bc1b62c46abb..455e1ff59e21 100644 --- a/packages/devextreme/js/ui/form_types.d.ts +++ b/packages/devextreme/js/ui/form_types.d.ts @@ -6,12 +6,16 @@ export { FormItemType, LabelLocation, FormLabelMode, + FormPredefinedButtonItem, + AIResult, ContentReadyEvent, DisposingEvent, EditorEnterKeyEvent, FieldDataChangedEvent, InitializedEvent, OptionChangedEvent, + SmartPastingEvent, + SmartPastedEvent, GroupItemTemplateData, GroupCaptionTemplateData, SimpleItemTemplateData, diff --git a/packages/devextreme/js/ui/gantt/ui.gantt.dialogs.js b/packages/devextreme/js/ui/gantt/ui.gantt.dialogs.js index 911312d17c85..cf73a79ece52 100644 --- a/packages/devextreme/js/ui/gantt/ui.gantt.dialogs.js +++ b/packages/devextreme/js/ui/gantt/ui.gantt.dialogs.js @@ -256,7 +256,7 @@ class TaskEditDialogInfo extends DialogInfoBase { this._parameters.title = formData.title; this._parameters.start = formData.start; this._parameters.end = formData.end; - this._parameters.progress = formData.progress * 100; + this._parameters.progress = Math.round(formData.progress * 100); this._parameters.assigned = formData.assigned; } isValidated() { diff --git a/packages/devextreme/js/ui/load_panel.js b/packages/devextreme/js/ui/load_panel.js index c8b9e12219d5..92e3fd542f2c 100644 --- a/packages/devextreme/js/ui/load_panel.js +++ b/packages/devextreme/js/ui/load_panel.js @@ -1,4 +1,4 @@ -import LoadPanel from '../__internal/ui/m_load_panel'; +import LoadPanel from '../__internal/ui/load_panel'; export default LoadPanel; diff --git a/packages/devextreme/js/ui/notify.js b/packages/devextreme/js/ui/notify.js index c70b923b4799..103b1a5dfbab 100644 --- a/packages/devextreme/js/ui/notify.js +++ b/packages/devextreme/js/ui/notify.js @@ -1,3 +1,3 @@ -import notify from '../__internal/ui/m_notify'; +import notify from '../__internal/ui/notify'; export default notify; diff --git a/packages/devextreme/js/ui/scheduler.d.ts b/packages/devextreme/js/ui/scheduler.d.ts index 0dfecd43cfb2..3a0d09938f77 100644 --- a/packages/devextreme/js/ui/scheduler.d.ts +++ b/packages/devextreme/js/ui/scheduler.d.ts @@ -446,6 +446,7 @@ export interface dxSchedulerOptions extends WidgetOptions { /** * @docid * @default "appointmentCollector" + * @type_function_param1 data:{ui/scheduler:AppointmentCollectorTemplateData} * @public */ appointmentCollectorTemplate?: template | ((data: AppointmentCollectorTemplateData, collectorElement: DxElement) => string | UserDefinedElement); @@ -514,8 +515,6 @@ export interface dxSchedulerOptions extends WidgetOptions { * @docid * @default "item" * @type_function_param1 model:{ui/scheduler:AppointmentTemplateData} - * @type_function_param1_field appointmentData:object - * @type_function_param1_field targetedAppointmentData:object * @public */ appointmentTemplate?: template | ((model: AppointmentTemplateData, itemIndex: number, contentElement: DxElement) => string | UserDefinedElement); @@ -523,8 +522,6 @@ export interface dxSchedulerOptions extends WidgetOptions { * @docid * @default "appointmentTooltip" * @type_function_param1 model:{ui/scheduler:AppointmentTooltipTemplateData} - * @type_function_param1_field appointmentData:object - * @type_function_param1_field targetedAppointmentData:object * @public */ appointmentTooltipTemplate?: template | ((model: AppointmentTooltipTemplateData, itemIndex: number, contentElement: DxElement) => string | UserDefinedElement); diff --git a/packages/devextreme/js/ui/select_box.d.ts b/packages/devextreme/js/ui/select_box.d.ts index 4ecb21a512ce..454619effee6 100644 --- a/packages/devextreme/js/ui/select_box.d.ts +++ b/packages/devextreme/js/ui/select_box.d.ts @@ -17,6 +17,7 @@ import { import { DropDownButtonTemplateDataModel, + FieldAddons, } from './drop_down_editor/ui.drop_down_editor'; import dxDropDownList, { @@ -230,8 +231,15 @@ export interface dxSelectBoxOptions extends dxDropDownListOptions string | UserDefinedElement); + /** + * @docid + * @default null + * @public + */ + fieldAddons?: FieldAddons; /** * @section Utils * @type function diff --git a/packages/devextreme/js/ui/toast.js b/packages/devextreme/js/ui/toast.js index a9656bfcca4f..4997c07cf1d0 100644 --- a/packages/devextreme/js/ui/toast.js +++ b/packages/devextreme/js/ui/toast.js @@ -1,4 +1,4 @@ -import Toast from '../__internal/ui/toast/m_toast'; +import Toast from '../__internal/ui/toast/toast'; export default Toast; diff --git a/packages/devextreme/js/ui/toast/hide_toasts.js b/packages/devextreme/js/ui/toast/hide_toasts.js index 8f938da87113..b42ca7327a4a 100644 --- a/packages/devextreme/js/ui/toast/hide_toasts.js +++ b/packages/devextreme/js/ui/toast/hide_toasts.js @@ -1,3 +1,3 @@ -import hideToasts from '../../__internal/ui/toast/m_hide_toasts'; +import hideToasts from '../../__internal/ui/toast/hide_toasts'; export default hideToasts; diff --git a/packages/devextreme/js/ui/widget/ui.errors.js b/packages/devextreme/js/ui/widget/ui.errors.js index 0c2be83fe87a..5e5970fd4f46 100644 --- a/packages/devextreme/js/ui/widget/ui.errors.js +++ b/packages/devextreme/js/ui/widget/ui.errors.js @@ -251,6 +251,16 @@ export default errorUtils(errors.ERROR_MESSAGES, { */ E1062: 'The "cellDuration" must be a positive integer, evenly dividing the ("endDayHour" - "startDayHour") interval into minutes.', + /** + * @name ErrorsUIWidgets.E1063 + */ + E1063: 'The \'smartPaste(text)\' method was called, but \'aiIntegration\' is not configured.', + + /** + * @name ErrorsUIWidgets.E1064 + */ + E1064: 'AI returned {1} for the {0} field, but this field only accepts {2} values. Update the \'instruction\' for this field.', + /** * @name ErrorsUIWidgets.W1001 */ @@ -368,4 +378,8 @@ export default errorUtils(errors.ERROR_MESSAGES, { * @name ErrorsUIWidgets.W1027 */ W1027: 'A prompt should be specified for a custom command.', + /** + * @name ErrorsUIWidgets.W1028 + */ + W1028: 'Nested/banded columns do not support the following properties: {0}.', }); diff --git a/packages/devextreme/js/viz/chart.d.ts b/packages/devextreme/js/viz/chart.d.ts index 840d0becb56d..5dc2f25c994e 100644 --- a/packages/devextreme/js/viz/chart.d.ts +++ b/packages/devextreme/js/viz/chart.d.ts @@ -237,7 +237,7 @@ export type OptionChangedEvent = EventInfo & ChangedOptionInfo; * @type object * @inherits Cancelable,NativeEventInfo,PointInteractionInfo */ -export type PointClickEvent = Cancelable & NativeEventInfo & PointInteractionInfo; +export type PointClickEvent = Cancelable & NativeEventInfo & PointInteractionInfo; /** * @docid _viz_chart_PointHoverChangedEvent @@ -245,7 +245,7 @@ export type PointClickEvent = Cancelable & NativeEventInfo & PointInteractionInfo; +export type PointHoverChangedEvent = EventInfo & PointInteractionInfo; /** * @docid _viz_chart_PointSelectionChangedEvent @@ -253,7 +253,7 @@ export type PointHoverChangedEvent = EventInfo & PointInteractionInfo; * @type object * @inherits EventInfo,PointInteractionInfo */ -export type PointSelectionChangedEvent = EventInfo & PointInteractionInfo; +export type PointSelectionChangedEvent = EventInfo & PointInteractionInfo; /** * @docid _viz_chart_SeriesClickEvent @@ -288,7 +288,7 @@ export type SeriesSelectionChangedEvent = EventInfo & SeriesInteraction * @type object * @inherits EventInfo,_viz_chart_components_base_chart_TooltipInfo */ -export type TooltipHiddenEvent = EventInfo & TooltipInfo; +export type TooltipHiddenEvent = EventInfo & TooltipInfo; /** * @docid _viz_chart_TooltipShownEvent @@ -296,7 +296,7 @@ export type TooltipHiddenEvent = EventInfo & TooltipInfo; * @type object * @inherits EventInfo,_viz_chart_components_base_chart_TooltipInfo */ -export type TooltipShownEvent = EventInfo & TooltipInfo; +export type TooltipShownEvent = EventInfo & TooltipInfo; /** * @docid _viz_chart_ZoomEndEvent @@ -768,7 +768,7 @@ export interface chartSeriesObject extends baseSeriesObject { * @namespace DevExpress.viz * @docid */ -export interface dxChartOptions extends BaseChartOptions { +export interface dxChartOptions extends BaseChartOptions { /** * @docid * @default true diff --git a/packages/devextreme/js/viz/chart_components/base_chart.d.ts b/packages/devextreme/js/viz/chart_components/base_chart.d.ts index d1221eb46190..a7a35c0d3284 100644 --- a/packages/devextreme/js/viz/chart_components/base_chart.d.ts +++ b/packages/devextreme/js/viz/chart_components/base_chart.d.ts @@ -49,18 +49,24 @@ import { * @docid * @hidden */ -export interface PointInteractionInfo { - /** @docid */ - readonly target: basePointObject; +export interface PointInteractionInfo { + /** + * @docid + * @type object + */ + readonly target: TPoint; } /** * @docid _viz_chart_components_base_chart_TooltipInfo * @hidden */ -export interface TooltipInfo { - /** @docid _viz_chart_components_base_chart_TooltipInfo.target */ - target?: basePointObject | dxChartAnnotationConfig | any; +export interface TooltipInfo { + /** + * @docid _viz_chart_components_base_chart_TooltipInfo.target + * @type object + */ + target?: TPoint | dxChartAnnotationConfig | any; } /** @@ -68,7 +74,10 @@ export interface TooltipInfo { * @docid * @type object */ -export interface BaseChartOptions extends BaseWidgetOptions { +export interface BaseChartOptions< + TComponent, + TPoint extends basePointObject = basePointObject, +> extends BaseWidgetOptions { /** * @docid * @type object @@ -144,51 +153,56 @@ export interface BaseChartOptions extends BaseWidgetOptions & PointInteractionInfo) => void) | string; + onPointClick?: ((e: NativeEventInfo & PointInteractionInfo) => void) | string; /** * @docid * @type_function_param1 e:object * @type_function_param1_field component:object * @type_function_param1_field element:object + * @type_function_param1_field target:object * @notUsedInTheme * @action * @public */ - onPointHoverChanged?: ((e: EventInfo & PointInteractionInfo) => void); + onPointHoverChanged?: ((e: EventInfo & PointInteractionInfo) => void); /** * @docid * @type_function_param1 e:object * @type_function_param1_field component:object * @type_function_param1_field element:object + * @type_function_param1_field target:object * @notUsedInTheme * @action * @public */ - onPointSelectionChanged?: ((e: EventInfo & PointInteractionInfo) => void); + onPointSelectionChanged?: ((e: EventInfo & PointInteractionInfo) => void); /** * @docid * @default null * @type_function_param1 e:object * @type_function_param1_field component:this + * @type_function_param1_field target:object * @notUsedInTheme * @action * @public */ - onTooltipHidden?: ((e: EventInfo & TooltipInfo) => void); + onTooltipHidden?: ((e: EventInfo & TooltipInfo) => void); /** * @docid * @default null * @type_function_param1 e:object * @type_function_param1_field component:this + * @type_function_param1_field target:object * @notUsedInTheme * @action * @public */ - onTooltipShown?: ((e: EventInfo & TooltipInfo) => void); + onTooltipShown?: ((e: EventInfo & TooltipInfo) => void); /** * @docid * @default "Material" diff --git a/packages/devextreme/js/viz/chart_components/scroll_bar.js b/packages/devextreme/js/viz/chart_components/scroll_bar.js index cb0a054dfe4b..9f0599541501 100644 --- a/packages/devextreme/js/viz/chart_components/scroll_bar.js +++ b/packages/devextreme/js/viz/chart_components/scroll_bar.js @@ -8,7 +8,7 @@ import { start as dragEventStart, move as dragEventMove, end as dragEventEnd } f const _min = Math.min; const _max = Math.max; -const MIN_SCROLL_BAR_SIZE = 2; +const MIN_SCROLL_BAR_SIZE = 10; export const ScrollBar = function(renderer, group) { this._translator = new Translator2D({}, {}, {}); @@ -221,16 +221,48 @@ ScrollBar.prototype = { const that = this; const visibleArea = that._translator.getCanvasVisibleArea(); - x1 = _max(x1, visibleArea.min); - x1 = _min(x1, visibleArea.max); + const min = visibleArea.min; + const max = visibleArea.max; - x2 = _min(x2, visibleArea.max); - x2 = _max(x2, visibleArea.min); + if(max <= min) { + return; + } + + if(x1 > x2) { + [x1, x2] = [x2, x1]; + } + + x1 = Math.max(x1, min); + x2 = Math.min(x2, max); + + if(x2 - x1 < MIN_SCROLL_BAR_SIZE) { + if(max - min < MIN_SCROLL_BAR_SIZE) { + x1 = min; + x2 = max; + } else { + const center = (x1 + x2) / 2; + + x1 = center - MIN_SCROLL_BAR_SIZE / 2; + x2 = center + MIN_SCROLL_BAR_SIZE / 2; + + if(x1 < min) { + x1 = min; + x2 = min + MIN_SCROLL_BAR_SIZE; + } else if(x2 > max) { + x2 = max; + x1 = max - MIN_SCROLL_BAR_SIZE; + } + } + } + + x1 = Math.max(x1, min); + x2 = Math.min(x2, max); + + const height = Math.max(x2 - x1, 0); - const height = Math.abs(x2 - x1); that._scroll.attr({ y: x1, - height: height < MIN_SCROLL_BAR_SIZE ? MIN_SCROLL_BAR_SIZE : height + height, }); } }; diff --git a/packages/devextreme/js/viz/pie_chart.d.ts b/packages/devextreme/js/viz/pie_chart.d.ts index 713d03b4f10b..802000cb5d13 100644 --- a/packages/devextreme/js/viz/pie_chart.d.ts +++ b/packages/devextreme/js/viz/pie_chart.d.ts @@ -178,7 +178,7 @@ export type OptionChangedEvent = EventInfo & ChangedOptionInfo; * @type object * @inherits NativeEventInfo,PointInteractionInfo */ -export type PointClickEvent = NativeEventInfo & PointInteractionInfo; +export type PointClickEvent = NativeEventInfo & PointInteractionInfo; /** * @docid _viz_pie_chart_PointHoverChangedEvent @@ -186,7 +186,7 @@ export type PointClickEvent = NativeEventInfo & PointInteractionInfo; +export type PointHoverChangedEvent = EventInfo & PointInteractionInfo; /** * @docid _viz_pie_chart_PointSelectionChangedEvent @@ -194,7 +194,7 @@ export type PointHoverChangedEvent = EventInfo & PointInteractionInf * @type object * @inherits EventInfo,PointInteractionInfo */ -export type PointSelectionChangedEvent = EventInfo & PointInteractionInfo; +export type PointSelectionChangedEvent = EventInfo & PointInteractionInfo; /** * @docid _viz_pie_chart_TooltipHiddenEvent @@ -202,7 +202,7 @@ export type PointSelectionChangedEvent = EventInfo & PointInteractio * @type object * @inherits EventInfo,_viz_chart_components_base_chart_TooltipInfo */ -export type TooltipHiddenEvent = EventInfo & TooltipInfo; +export type TooltipHiddenEvent = EventInfo & TooltipInfo; /** * @docid _viz_pie_chart_TooltipShownEvent @@ -210,7 +210,7 @@ export type TooltipHiddenEvent = EventInfo & TooltipInfo; * @type object * @inherits EventInfo,_viz_chart_components_base_chart_TooltipInfo */ -export type TooltipShownEvent = EventInfo & TooltipInfo; +export type TooltipShownEvent = EventInfo & TooltipInfo; /** * @public @@ -274,7 +274,7 @@ export interface PieChartSeries extends dxPieChartSeriesTypesCommonPieChartSerie * @namespace DevExpress.viz * @docid */ -export interface dxPieChartOptions extends BaseChartOptions { +export interface dxPieChartOptions extends BaseChartOptions { /** * @docid * @type object diff --git a/packages/devextreme/js/viz/polar_chart.d.ts b/packages/devextreme/js/viz/polar_chart.d.ts index ac7f2b885eaf..d40eb36b9a35 100644 --- a/packages/devextreme/js/viz/polar_chart.d.ts +++ b/packages/devextreme/js/viz/polar_chart.d.ts @@ -199,7 +199,7 @@ export type OptionChangedEvent = EventInfo & ChangedOptionInfo; * @type object * @inherits Cancelable,NativeEventInfo,PointInteractionInfo */ -export type PointClickEvent = Cancelable & NativeEventInfo & PointInteractionInfo; +export type PointClickEvent = Cancelable & NativeEventInfo & PointInteractionInfo; /** * @docid _viz_polar_chart_PointHoverChangedEvent @@ -207,7 +207,7 @@ export type PointClickEvent = Cancelable & NativeEventInfo & PointInteractionInfo; +export type PointHoverChangedEvent = EventInfo & PointInteractionInfo; /** * @docid _viz_polar_chart_PointSelectionChangedEvent @@ -215,7 +215,7 @@ export type PointHoverChangedEvent = EventInfo & PointInteractionI * @type object * @inherits EventInfo,PointInteractionInfo */ -export type PointSelectionChangedEvent = EventInfo & PointInteractionInfo; +export type PointSelectionChangedEvent = EventInfo & PointInteractionInfo; /** * @docid _viz_polar_chart_SeriesClickEvent @@ -250,7 +250,7 @@ export type SeriesSelectionChangedEvent = EventInfo & SeriesIntera * @type object * @inherits EventInfo,_viz_chart_components_base_chart_TooltipInfo */ -export type TooltipHiddenEvent = EventInfo & TooltipInfo; +export type TooltipHiddenEvent = EventInfo & TooltipInfo; /** * @docid _viz_polar_chart_TooltipShownEvent @@ -258,7 +258,7 @@ export type TooltipHiddenEvent = EventInfo & TooltipInfo; * @type object * @inherits EventInfo,_viz_chart_components_base_chart_TooltipInfo */ -export type TooltipShownEvent = EventInfo & TooltipInfo; +export type TooltipShownEvent = EventInfo & TooltipInfo; /** * @docid _viz_polar_chart_ZoomEndEvent @@ -333,7 +333,7 @@ export interface PolarChartSeries extends dxPolarChartSeriesTypesCommonPolarChar * @namespace DevExpress.viz * @docid */ -export interface dxPolarChartOptions extends BaseChartOptions { +export interface dxPolarChartOptions extends BaseChartOptions { /** * @docid * @type object diff --git a/packages/devextreme/js/viz/series/points/candlestick_point.js b/packages/devextreme/js/viz/series/points/candlestick_point.js index 9d09c6cf8337..ff7cc7341ec9 100644 --- a/packages/devextreme/js/viz/series/points/candlestick_point.js +++ b/packages/devextreme/js/viz/series/points/candlestick_point.js @@ -11,8 +11,6 @@ const _round = _math.round; const DEFAULT_FINANCIAL_TRACKER_MARGIN = 2; export default _extend({}, barPoint, { - _calculateVisibility: symbolPoint._calculateVisibility, - _getContinuousPoints: function(openCoord, closeCoord) { const that = this; const x = that.x; @@ -265,19 +263,23 @@ export default _extend({}, barPoint, { }, _translate: function() { - const that = this; - const rotated = that._options.rotated; - const valTranslator = that._getValTranslator(); - const x = that._getArgTranslator().translate(that.argument); + const valTranslator = this._getValTranslator(); + const x = this._getArgTranslator().translate(this.argument); - that.vx = that.vy = that.x = x === null ? x : x + (that.xCorrection || 0); - that.openY = that.openValue !== null ? valTranslator.translate(that.openValue) : null; - that.highY = valTranslator.translate(that.highValue); - that.lowY = valTranslator.translate(that.lowValue); - that.closeY = that.closeValue !== null ? valTranslator.translate(that.closeValue) : null; + this.vx = this.vy = this.x = x === null ? x : x + (this.xCorrection || 0); + this.openY = this.openValue !== null ? valTranslator.translate(this.openValue) : null; + this.highY = valTranslator.translate(this.highValue); + this.lowY = valTranslator.translate(this.lowValue); + this.closeY = this.closeValue !== null ? valTranslator.translate(this.closeValue) : null; - const centerValue = _min(that.lowY, that.highY) + _abs(that.lowY - that.highY) / 2; - that._calculateVisibility(!rotated ? that.x : centerValue, !rotated ? centerValue : that.x); + const minValue = Math.min(this.lowY, this.highY); + const height = Math.abs(this.lowY - this.highY); + + if(this._options.rotated) { + this._calculateVisibility(minValue, this.x, height, 0); + } else { + this._calculateVisibility(this.x, minValue, 0, height); + } }, getCrosshairData: function(x, y) { diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 8cc398c84614..ec6ded19f0ce 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -85,7 +85,7 @@ "@stylistic/eslint-plugin": "catalog:", "@testcafe-community/axe": "3.5.0", "@types/enzyme": "3.10.18", - "@types/jquery": "3.5.29", + "@types/jquery": "catalog:", "@types/react": "16.14.34", "@typescript-eslint/eslint-plugin": "catalog:", "@typescript-eslint/experimental-utils": "5.62.0", @@ -127,6 +127,7 @@ "eslint-plugin-simple-import-sort": "10.0.0", "eslint-plugin-spellcheck": "0.0.20", "eslint-plugin-testcafe": "0.2.1", + "eslint-plugin-unicorn": "^60.0.0", "fancy-log": "2.0.0", "file-saver": "2.0.5", "glob": "10.4.5", @@ -167,9 +168,9 @@ "jest-each": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-environment-node": "29.7.0", - "jquery": "3.7.1", + "jquery": "catalog:", "jquery.tmpl": "0.0.2", - "jspdf": "3.0.1", + "jspdf": "3.0.2", "jspdf-autotable": "3.8.3", "knockout": "3.5.1", "lazypipe": "1.0.2", @@ -246,8 +247,10 @@ "validate-ts": "gulp validate-ts", "validate-declarations": "dx-tools validate-declarations --sources ./js --exclude \"js/(renovation|__internal|.eslintrc.js)\" --compiler-options \"{ \\\"typeRoots\\\": [] }\"", "testcafe-in-docker": "docker build -f ./testing/testcafe/docker/Dockerfile -t testcafe-testing . && docker run -it testcafe-testing", - "test-jest": "jest --no-coverage --runInBand", + "test-jest": "cross-env NODE_OPTIONS='--expose-gc' jest --no-coverage --runInBand --selectProjects jsdom-tests", "test-jest:watch": "jest --watch", + "test-jest:node": "jest --no-coverage --runInBand --selectProjects node-tests", + "test-jest:all": "pnpm test-jest && pnpm test-jest:node", "qunit-in-docker": "gulp qunit-in-docker --constel", "tcd-update": "tcd-update" }, diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index ba19c7f587a6..af01278ebc6c 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -152,10 +152,11 @@ "test-jest": { "executor": "nx:run-script", "options": { - "script": "test-jest" + "script": "test-jest:all" }, "inputs": [ "{projectRoot}/js/**/*", + "{projectRoot}/build/**/*", "{projectRoot}/jest.*" ], "cache": true diff --git a/packages/devextreme/testing/helpers/calendarFixtures.js b/packages/devextreme/testing/helpers/calendarFixtures.js index 0f7d9261ea2e..905ee592c731 100644 --- a/packages/devextreme/testing/helpers/calendarFixtures.js +++ b/packages/devextreme/testing/helpers/calendarFixtures.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import { noop } from 'core/utils/common'; import Class from 'core/class'; -import Views from '__internal/ui/calendar/m_calendar.views'; +import Views from '__internal/ui/calendar/calendar.views'; const TEXTEDITOR_INPUT_SELECTOR = '.dx-texteditor-input'; diff --git a/packages/devextreme/testing/runner/Views/Main/RunSuite.cshtml b/packages/devextreme/testing/runner/Views/Main/RunSuite.cshtml index c8947f1dc607..16433e6c4677 100644 --- a/packages/devextreme/testing/runner/Views/Main/RunSuite.cshtml +++ b/packages/devextreme/testing/runner/Views/Main/RunSuite.cshtml @@ -169,6 +169,10 @@