From fe119a843938af733f92b4b08eb106b06cd46a88 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 22:16:38 -0300 Subject: [PATCH 1/7] Scheduler - Extract Scale facade from Workspace for Appointments --- .../scheduler/entieties/scale.test.ts | 145 ++++++++++++++++++ .../__internal/scheduler/entieties/scale.ts | 57 +++++++ .../js/__internal/scheduler/m_scheduler.ts | 17 +- 3 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts create mode 100644 packages/devextreme/js/__internal/scheduler/entieties/scale.ts diff --git a/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts b/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts new file mode 100644 index 000000000000..49ece89cd5ee --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts @@ -0,0 +1,145 @@ +import { WorkspaceScale } from './scale'; + +const createMockWorkspace = (overrides: Record = {}) => ({ + positionHelper: { + getResizableStep: jest.fn(() => 42), + }, + getDOMElementsMetaData: jest.fn(() => ({ + dateTableCellsMeta: [[{ + left: 0, top: 0, width: 100, height: 50, + }]], + allDayPanelCellsMeta: [{ + left: 0, top: 0, width: 100, height: 30, + }], + })), + viewDataProvider: { getCellData: jest.fn() }, + isVerticalGroupedWorkSpace: jest.fn(() => false), + type: 'day' as const, + ...overrides, +}); + +describe('WorkspaceScale', () => { + describe('getResizableStep', () => { + it('should delegate to workspace positionHelper', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.getResizableStep()).toBe(42); + expect(workspace.positionHelper.getResizableStep).toHaveBeenCalled(); + }); + + it('should return 0 when workspace is undefined', () => { + const scale = new WorkspaceScale(() => undefined); + + expect(scale.getResizableStep()).toBe(0); + }); + }); + + describe('getDOMElementsMetaData', () => { + it('should delegate to workspace', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + const meta = scale.getDOMElementsMetaData(); + + expect(meta).toEqual({ + dateTableCellsMeta: [[{ + left: 0, top: 0, width: 100, height: 50, + }]], + allDayPanelCellsMeta: [{ + left: 0, top: 0, width: 100, height: 30, + }], + }); + expect(workspace.getDOMElementsMetaData).toHaveBeenCalled(); + }); + + it('should return undefined when workspace is undefined', () => { + const scale = new WorkspaceScale(() => undefined); + + expect(scale.getDOMElementsMetaData()).toBeUndefined(); + }); + }); + + describe('viewDataProvider', () => { + it('should return workspace viewDataProvider', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.viewDataProvider).toBe(workspace.viewDataProvider); + }); + + it('should return undefined when workspace is undefined', () => { + const scale = new WorkspaceScale(() => undefined); + + expect(scale.viewDataProvider).toBeUndefined(); + }); + }); + + describe('isVerticalGroupedWorkSpace', () => { + it('should delegate to workspace', () => { + const workspace = createMockWorkspace({ + isVerticalGroupedWorkSpace: jest.fn(() => true), + }); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.isVerticalGroupedWorkSpace()).toBe(true); + }); + + it('should return false for non-vertical grouping', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.isVerticalGroupedWorkSpace()).toBe(false); + }); + }); + + describe('isDateAndTimeView', () => { + it('should return true for day view', () => { + const workspace = createMockWorkspace({ type: 'day' }); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.isDateAndTimeView()).toBe(true); + }); + + it('should return false for month view', () => { + const workspace = createMockWorkspace({ type: 'month' }); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.isDateAndTimeView()).toBe(false); + }); + + it('should return false for timelineMonth view', () => { + const workspace = createMockWorkspace({ type: 'timelineMonth' }); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.isDateAndTimeView()).toBe(false); + }); + + it('should return true for week view', () => { + const workspace = createMockWorkspace({ type: 'week' }); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.isDateAndTimeView()).toBe(true); + }); + }); + + describe('workspace replacement', () => { + it('should follow workspace getter to current workspace', () => { + const workspaceA = createMockWorkspace({ + positionHelper: { getResizableStep: jest.fn(() => 10) }, + }); + const workspaceB = createMockWorkspace({ + positionHelper: { getResizableStep: jest.fn(() => 20) }, + }); + + let current: ReturnType = workspaceA; + const scale = new WorkspaceScale(() => current); + + expect(scale.getResizableStep()).toBe(10); + + current = workspaceB; + + expect(scale.getResizableStep()).toBe(20); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts new file mode 100644 index 000000000000..94907d27df59 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts @@ -0,0 +1,57 @@ +import type { Rect } from '../appointments/resizing/types'; +import { isDateAndTimeView } from '../r1/utils/index'; +import type { ViewDataProviderType } from '../types'; + +export interface DOMElementsMetaData { + dateTableCellsMeta: Rect[][]; + allDayPanelCellsMeta: Rect[]; +} + +interface Workspace { + positionHelper: { + getResizableStep: () => number; + }; + getDOMElementsMetaData: () => DOMElementsMetaData; + viewDataProvider: ViewDataProviderType; + isVerticalGroupedWorkSpace: () => boolean; + type: string; +} + +export interface Scale { + readonly viewDataProvider: ViewDataProviderType | undefined; + getResizableStep: () => number; + getDOMElementsMetaData: () => DOMElementsMetaData | undefined; + isVerticalGroupedWorkSpace: () => boolean; + isDateAndTimeView: () => boolean; +} + +export class WorkspaceScale implements Scale { + private readonly getWorkspace: () => Workspace | undefined; + + constructor(getWorkspace: () => Workspace | undefined) { + this.getWorkspace = getWorkspace; + } + + get viewDataProvider(): ViewDataProviderType | undefined { + return this.getWorkspace()?.viewDataProvider; + } + + getResizableStep(): number { + const workspace = this.getWorkspace(); + return workspace ? workspace.positionHelper.getResizableStep() : 0; + } + + getDOMElementsMetaData(): DOMElementsMetaData | undefined { + return this.getWorkspace()?.getDOMElementsMetaData(); + } + + isVerticalGroupedWorkSpace(): boolean { + return this.getWorkspace()?.isVerticalGroupedWorkSpace() ?? false; + } + + isDateAndTimeView(): boolean { + const workspace = this.getWorkspace(); + if (!workspace) return false; + return isDateAndTimeView(workspace.type as Parameters[0]); + } +} diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index de3f4a0fe049..6dc768b2b65b 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -42,6 +42,8 @@ import { ACTION_TO_APPOINTMENT, AppointmentPopup as AppointmentLegacyPopup } fro import { AppointmentPopup } from './appointment_popup/m_popup'; import AppointmentCollection from './appointments/m_appointment_collection'; import NotifyScheduler from './base/m_widget_notify_scheduler'; +import type { Scale } from './entieties/scale'; +import { WorkspaceScale } from './entieties/scale'; import { SchedulerHeader } from './header/m_header'; import type { HeaderOptions } from './header/types'; import { CompactAppointmentsHelper } from './m_compact_appointments_helper'; @@ -56,7 +58,6 @@ import { excludeFromRecurrence, getToday, isAppointmentTakesAllDay, - isDateAndTimeView, isTimelineView, } from './r1/utils/index'; import { validateRRule } from './recurrence/validate_rule'; @@ -171,6 +172,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { _workSpace: any; + private _scale: Scale; + private header?: SchedulerHeader; _appointments: any; @@ -735,6 +738,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { super._init(); + this._scale = new WorkspaceScale(() => this._workSpace); + this.initAllDayPanel(); // @ts-expect-error @@ -1255,11 +1260,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { groups: this.getViewOption('groups'), groupByDate: this.getViewOption('groupByDate'), timeZoneCalculator: this.timeZoneCalculator, - getResizableStep: () => (this._workSpace ? this._workSpace.positionHelper.getResizableStep() : 0), - getDOMElementsMetaData: () => this._workSpace?.getDOMElementsMetaData(), - getViewDataProvider: () => this._workSpace?.viewDataProvider, - isVerticalGroupedWorkSpace: () => this._workSpace.isVerticalGroupedWorkSpace(), - isDateAndTimeView: () => isDateAndTimeView(this._workSpace.type), + getResizableStep: () => this._scale.getResizableStep(), + getDOMElementsMetaData: () => this._scale.getDOMElementsMetaData(), + getViewDataProvider: () => this._scale.viewDataProvider, + isVerticalGroupedWorkSpace: () => this._scale.isVerticalGroupedWorkSpace(), + isDateAndTimeView: () => this._scale.isDateAndTimeView(), onContentReady: () => { this._workSpace?.option('allDayExpanded', this.isAllDayExpanded()); }, From 01761d66e7b70d39c4cc8e85a15bb2b49ee819b1 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 22:27:20 -0300 Subject: [PATCH 2/7] Scheduler - Fix definite assignment for _scale property --- packages/devextreme/js/__internal/scheduler/m_scheduler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 6dc768b2b65b..ef41e5f00c84 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -172,7 +172,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { _workSpace: any; - private _scale: Scale; + private _scale!: Scale; private header?: SchedulerHeader; From d534fceba6e12432402fa76f43a80ab7eb40ed72 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:01:29 -0300 Subject: [PATCH 3/7] Scheduler - Add getCellDateInfo and getCellGeometry to Scale facade --- .../appointments/m_appointment_collection.ts | 2 + .../scheduler/appointments/resizing/m_core.ts | 41 +++--- .../scheduler/appointments/resizing/types.ts | 12 ++ .../scheduler/entieties/scale.test.ts | 117 +++++++++++++++++- .../__internal/scheduler/entieties/scale.ts | 61 ++++++++- .../js/__internal/scheduler/m_scheduler.ts | 2 + 6 files changed, 206 insertions(+), 29 deletions(-) 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 278cdc41434c..be091123db4b 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -783,6 +783,8 @@ class SchedulerAppointments extends CollectionWidget { appointmentRect: getBoundingRect($element[0]), parentAppointmentRect: getBoundingRect($element.parent()[0]), viewDataProvider: this.option('getViewDataProvider')(), + getCellDateInfo: this.option('getCellDateInfo'), + getCellGeometry: this.option('getCellGeometry'), isDateAndTimeView: this.option('isDateAndTimeView')(), startDayHour: this.invoke('getStartDayHour'), endDayHour: this.invoke('getEndDayHour'), 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 a235bc0515b4..a64599240b7c 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts @@ -1,7 +1,7 @@ import { dateUtilsTs } from '@ts/core/utils/date'; import { dateUtils } from '@ts/core/utils/m_date'; -import type { ViewCellData } from '../../types'; +import type { CellDateInfo } from '../../entieties/scale'; import type { CellsInfo, DateRange, @@ -12,25 +12,21 @@ import type { const toMs = dateUtils.dateToMilliseconds; -// NOTE: View data generator shifts all day cell dates by offset -// and return equal start and end dates. const getCellData = ( - { viewDataProvider }: GetAppointmentDateRangeOptionsExtended, + { getCellDateInfo }: GetAppointmentDateRangeOptionsExtended, cellRowIndex: number, cellColumnIndex: number, isOccupiedAllDay: boolean, isAllDay = false, rtlEnabled = false, -): ViewCellData => { - const cellData = viewDataProvider.getCellData( +): CellDateInfo => { + const cellData = getCellDateInfo( cellRowIndex, cellColumnIndex, isOccupiedAllDay, rtlEnabled, - ); - // NOTE: All day appointments occupy day if they start at the beginning of the day, - // 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')); } @@ -38,7 +34,7 @@ const getCellData = ( return cellData; }; -const getAppointmentLeftCell = (options: GetAppointmentDateRangeOptionsExtended): ViewCellData => { +const getAppointmentLeftCell = (options: GetAppointmentDateRangeOptionsExtended): CellDateInfo => { const { cellHeight, cellWidth, @@ -165,25 +161,16 @@ const getRelativeAppointmentRect = (appointmentRect: Rect, parentAppointmentRect const getAppointmentCellsInfo = (options: GetAppointmentDateRangeOptions): CellsInfo => { const { appointmentSettings, - isVerticalGroupedWorkSpace, - DOMMetaData, + getCellGeometry, } = options; - const DOMMetaTable = appointmentSettings.allDay && !isVerticalGroupedWorkSpace - ? [DOMMetaData.allDayPanelCellsMeta] - : DOMMetaData.dateTableCellsMeta; - - const { - height: cellHeight, - width: cellWidth, - } = DOMMetaTable[appointmentSettings.rowIndex][appointmentSettings.columnIndex]; - const cellCountInRow = DOMMetaTable[appointmentSettings.rowIndex].length; + const geometry = getCellGeometry( + appointmentSettings.rowIndex, + appointmentSettings.columnIndex, + Boolean(appointmentSettings.allDay), + ); - return { - cellWidth, - cellHeight, - cellCountInRow, - }; + return geometry ?? { cellWidth: 0, cellHeight: 0, cellCountInRow: 0 }; }; export const getAppointmentDateRange = (options: GetAppointmentDateRangeOptions): DateRange => { diff --git a/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts b/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts index cc5b123d35a5..f5e878484267 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts @@ -1,3 +1,4 @@ +import type { CellDateInfo } from '../../entieties/scale'; import type { TimeZoneCalculator } from '../../r1/timezone_calculator'; import type { ViewDataProviderType } from '../../types'; import type { AppointmentDataAccessor } from '../../utils/data_accessor/appointment_data_accessor'; @@ -15,6 +16,17 @@ export interface GetAppointmentDateRangeOptions { appointmentRect: Rect; parentAppointmentRect: Rect; viewDataProvider: ViewDataProviderType; + getCellDateInfo: ( + rowIndex: number, + columnIndex: number, + isAllDay: boolean, + rtlEnabled: boolean, + ) => CellDateInfo | undefined; + getCellGeometry: ( + rowIndex: number, + columnIndex: number, + isAllDay: boolean, + ) => CellsInfo | undefined; isDateAndTimeView: boolean; startDayHour: number; endDayHour: number; diff --git a/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts b/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts index 49ece89cd5ee..8a2987c26e91 100644 --- a/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts +++ b/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts @@ -12,7 +12,13 @@ const createMockWorkspace = (overrides: Record = {}) => ({ left: 0, top: 0, width: 100, height: 30, }], })), - viewDataProvider: { getCellData: jest.fn() }, + viewDataProvider: { + getCellData: jest.fn((rowIndex: number, columnIndex: number) => ({ + startDate: new Date(2024, 0, 1 + columnIndex), + endDate: new Date(2024, 0, 2 + columnIndex), + index: rowIndex * 7 + columnIndex, + })), + }, isVerticalGroupedWorkSpace: jest.fn(() => false), type: 'day' as const, ...overrides, @@ -75,6 +81,115 @@ describe('WorkspaceScale', () => { }); }); + describe('getCellDateInfo', () => { + it('should return startDate, endDate, and index from viewDataProvider', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + const info = scale.getCellDateInfo(0, 2, false, false); + + expect(info).toEqual({ + startDate: new Date(2024, 0, 3), + endDate: new Date(2024, 0, 4), + index: 2, + }); + expect(workspace.viewDataProvider.getCellData).toHaveBeenCalledWith(0, 2, false, false); + }); + + it('should pass isAllDay and rtlEnabled to viewDataProvider', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + scale.getCellDateInfo(1, 3, true, true); + + expect(workspace.viewDataProvider.getCellData).toHaveBeenCalledWith(1, 3, true, true); + }); + + it('should return undefined when workspace is undefined', () => { + const scale = new WorkspaceScale(() => undefined); + + expect(scale.getCellDateInfo(0, 0, false, false)).toBeUndefined(); + }); + }); + + describe('getCellGeometry', () => { + it('should return cellWidth, cellHeight, cellCountInRow from dateTable', () => { + const workspace = createMockWorkspace({ + getDOMElementsMetaData: jest.fn(() => ({ + dateTableCellsMeta: [[ + { + left: 0, top: 0, width: 120, height: 60, + }, + { + left: 120, top: 0, width: 120, height: 60, + }, + ]], + allDayPanelCellsMeta: [{ + left: 0, top: 0, width: 120, height: 30, + }], + })), + }); + const scale = new WorkspaceScale(() => workspace); + + const geometry = scale.getCellGeometry(0, 0, false); + + expect(geometry).toEqual({ + cellWidth: 120, + cellHeight: 60, + cellCountInRow: 2, + }); + }); + + it('should use allDayPanelCellsMeta when isAllDay and not vertical grouped', () => { + const workspace = createMockWorkspace({ + getDOMElementsMetaData: jest.fn(() => ({ + dateTableCellsMeta: [[{ + left: 0, top: 0, width: 100, height: 50, + }]], + allDayPanelCellsMeta: [ + { + left: 0, top: 0, width: 200, height: 40, + }, + { + left: 200, top: 0, width: 200, height: 40, + }, + ], + })), + isVerticalGroupedWorkSpace: jest.fn(() => false), + }); + const scale = new WorkspaceScale(() => workspace); + + const geometry = scale.getCellGeometry(0, 1, true); + + expect(geometry).toEqual({ + cellWidth: 200, + cellHeight: 40, + cellCountInRow: 2, + }); + }); + + it('should use dateTableCellsMeta when isAllDay but vertical grouped', () => { + const workspace = createMockWorkspace({ + isVerticalGroupedWorkSpace: jest.fn(() => true), + }); + const scale = new WorkspaceScale(() => workspace); + + const geometry = scale.getCellGeometry(0, 0, true); + + expect(geometry).toEqual({ + cellWidth: 100, + cellHeight: 50, + cellCountInRow: 1, + }); + }); + + it('should return undefined when workspace is undefined', () => { + const scale = new WorkspaceScale(() => undefined); + + expect(scale.getCellGeometry(0, 0, false)).toBeUndefined(); + }); + }); + describe('isVerticalGroupedWorkSpace', () => { it('should delegate to workspace', () => { const workspace = createMockWorkspace({ diff --git a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts index 94907d27df59..a2da27a1d698 100644 --- a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts +++ b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts @@ -1,4 +1,4 @@ -import type { Rect } from '../appointments/resizing/types'; +import type { CellsInfo, Rect } from '../appointments/resizing/types'; import { isDateAndTimeView } from '../r1/utils/index'; import type { ViewDataProviderType } from '../types'; @@ -7,6 +7,12 @@ export interface DOMElementsMetaData { allDayPanelCellsMeta: Rect[]; } +export interface CellDateInfo { + startDate: Date; + endDate: Date; + index: number; +} + interface Workspace { positionHelper: { getResizableStep: () => number; @@ -21,6 +27,17 @@ export interface Scale { readonly viewDataProvider: ViewDataProviderType | undefined; getResizableStep: () => number; getDOMElementsMetaData: () => DOMElementsMetaData | undefined; + getCellDateInfo: ( + rowIndex: number, + columnIndex: number, + isAllDay: boolean, + rtlEnabled: boolean, + ) => CellDateInfo | undefined; + getCellGeometry: ( + rowIndex: number, + columnIndex: number, + isAllDay: boolean, + ) => CellsInfo | undefined; isVerticalGroupedWorkSpace: () => boolean; isDateAndTimeView: () => boolean; } @@ -45,6 +62,48 @@ export class WorkspaceScale implements Scale { return this.getWorkspace()?.getDOMElementsMetaData(); } + getCellDateInfo( + rowIndex: number, + columnIndex: number, + isAllDay: boolean, + rtlEnabled: boolean, + ): CellDateInfo | undefined { + const workspace = this.getWorkspace(); + if (!workspace) return undefined; + const cellData = workspace.viewDataProvider.getCellData( + rowIndex, + columnIndex, + isAllDay, + rtlEnabled, + ); + return { + startDate: cellData.startDate, + endDate: cellData.endDate, + index: cellData.index, + }; + } + + getCellGeometry( + rowIndex: number, + columnIndex: number, + isAllDay: boolean, + ): CellsInfo | undefined { + const workspace = this.getWorkspace(); + if (!workspace) return undefined; + const meta = workspace.getDOMElementsMetaData(); + const isVertical = workspace.isVerticalGroupedWorkSpace(); + const metaTable = isAllDay && !isVertical + ? [meta.allDayPanelCellsMeta] + : meta.dateTableCellsMeta; + const row = metaTable[rowIndex]; + if (!row?.[columnIndex]) return undefined; + return { + cellWidth: row[columnIndex].width, + cellHeight: row[columnIndex].height, + cellCountInRow: row.length, + }; + } + isVerticalGroupedWorkSpace(): boolean { return this.getWorkspace()?.isVerticalGroupedWorkSpace() ?? false; } diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index ef41e5f00c84..9162df93b99f 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1263,6 +1263,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { getResizableStep: () => this._scale.getResizableStep(), getDOMElementsMetaData: () => this._scale.getDOMElementsMetaData(), getViewDataProvider: () => this._scale.viewDataProvider, + getCellDateInfo: this._scale.getCellDateInfo.bind(this._scale), + getCellGeometry: this._scale.getCellGeometry.bind(this._scale), isVerticalGroupedWorkSpace: () => this._scale.isVerticalGroupedWorkSpace(), isDateAndTimeView: () => this._scale.isDateAndTimeView(), onContentReady: () => { From de3a38e580d59bb7975b42d725a551531a690c61 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 21 Mar 2026 23:25:43 -0300 Subject: [PATCH 4/7] Scheduler - Pass Scale as single object to AppointmentCollection --- .../appointments/m_appointment_collection.ts | 14 +- .../js/__internal/scheduler/m_scheduler.ts | 1 + .../playground/tests/scheduler-resize.spec.ts | 163 ++++++++++++++++++ 3 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 packages/devextreme/playground/tests/scheduler-resize.spec.ts 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 be091123db4b..fc553aa14dc7 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -776,22 +776,24 @@ class SchedulerAppointments extends CollectionWidget { const $element = $(e.element); const timeZoneCalculator = this.option('timeZoneCalculator'); + const scale = this.option('scale'); + return getAppointmentDateRange({ handles: e.handles, appointmentSettings: $element.data(APPOINTMENT_SETTINGS_KEY) as any, - isVerticalGroupedWorkSpace: this.option('isVerticalGroupedWorkSpace')(), + isVerticalGroupedWorkSpace: scale.isVerticalGroupedWorkSpace(), appointmentRect: getBoundingRect($element[0]), parentAppointmentRect: getBoundingRect($element.parent()[0]), - viewDataProvider: this.option('getViewDataProvider')(), - getCellDateInfo: this.option('getCellDateInfo'), - getCellGeometry: this.option('getCellGeometry'), - isDateAndTimeView: this.option('isDateAndTimeView')(), + viewDataProvider: scale.viewDataProvider, + getCellDateInfo: scale.getCellDateInfo.bind(scale), + getCellGeometry: scale.getCellGeometry.bind(scale), + isDateAndTimeView: scale.isDateAndTimeView(), startDayHour: this.invoke('getStartDayHour'), endDayHour: this.invoke('getEndDayHour'), timeZoneCalculator, dataAccessors: this.dataAccessors, rtlEnabled: this.option('rtlEnabled'), - DOMMetaData: this.option('getDOMElementsMetaData')(), + DOMMetaData: scale.getDOMElementsMetaData(), viewOffset: this.invoke('getViewOffsetMs'), }); } diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 9162df93b99f..e422e14c31d6 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1260,6 +1260,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { groups: this.getViewOption('groups'), groupByDate: this.getViewOption('groupByDate'), timeZoneCalculator: this.timeZoneCalculator, + scale: this._scale, getResizableStep: () => this._scale.getResizableStep(), getDOMElementsMetaData: () => this._scale.getDOMElementsMetaData(), getViewDataProvider: () => this._scale.viewDataProvider, diff --git a/packages/devextreme/playground/tests/scheduler-resize.spec.ts b/packages/devextreme/playground/tests/scheduler-resize.spec.ts new file mode 100644 index 000000000000..b9b9de270c2c --- /dev/null +++ b/packages/devextreme/playground/tests/scheduler-resize.spec.ts @@ -0,0 +1,163 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Scheduler appointment resize', () => { + test.beforeEach(async ({ page }) => { + const errors: string[] = []; + page.on('pageerror', (err) => errors.push(err.message)); + + await page.goto('/demos/Scheduler/Editing/'); + await page.waitForSelector('.dx-scheduler-appointment', { timeout: 10000 }); + }); + + test('appointments render with correct geometry', async ({ page }) => { + const appointments = page.locator('.dx-scheduler-appointment'); + + const count = await appointments.count(); + expect(count).toBeGreaterThan(0); + + for (let i = 0; i < Math.min(count, 5); i++) { + const box = await appointments.nth(i).boundingBox(); + expect(box).not.toBeNull(); + expect(box!.width).toBeGreaterThan(0); + expect(box!.height).toBeGreaterThan(0); + } + }); + + test('appointment has resize handles', async ({ page }) => { + const appointment = page.locator('.dx-scheduler-appointment').first(); + await appointment.hover(); + + const topHandle = appointment.locator('.dx-resizable-handle-top'); + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + + await expect(topHandle).toBeVisible(); + await expect(bottomHandle).toBeVisible(); + }); + + test('resize appointment by dragging bottom handle changes height', async ({ page }) => { + const appointment = page.locator('.dx-scheduler-appointment').first(); + await appointment.hover(); + + const boxBefore = await appointment.boundingBox(); + expect(boxBefore).not.toBeNull(); + + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + const handleBox = await bottomHandle.boundingBox(); + expect(handleBox).not.toBeNull(); + + const cellHeight = await page.locator('.dx-scheduler-date-table-cell').first().evaluate( + (el) => el.getBoundingClientRect().height, + ); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move( + handleBox!.x + handleBox!.width / 2, + handleBox!.y + cellHeight, + { steps: 5 }, + ); + await page.mouse.up(); + + const boxAfter = await appointment.boundingBox(); + expect(boxAfter).not.toBeNull(); + expect(boxAfter!.height).toBeGreaterThan(boxBefore!.height); + }); + + test('resize snaps to cell boundaries', async ({ page }) => { + const appointment = page.locator('.dx-scheduler-appointment').first(); + await appointment.hover(); + + const cellHeight = await page.locator('.dx-scheduler-date-table-cell').first().evaluate( + (el) => el.getBoundingClientRect().height, + ); + + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + const handleBox = await bottomHandle.boundingBox(); + expect(handleBox).not.toBeNull(); + + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move( + handleBox!.x + handleBox!.width / 2, + handleBox!.y + cellHeight * 2, + { steps: 5 }, + ); + await page.mouse.up(); + + const boxAfter = await appointment.boundingBox(); + expect(boxAfter).not.toBeNull(); + const heightRatio = boxAfter!.height / cellHeight; + expect(Math.abs(heightRatio - Math.round(heightRatio))).toBeLessThan(0.15); + }); + + test('appointment position aligns with cell grid after resize', async ({ page }) => { + const appointment = page.locator('.dx-scheduler-appointment').first(); + await appointment.hover(); + + const boxBefore = await appointment.boundingBox(); + expect(boxBefore).not.toBeNull(); + + const cells = page.locator('.dx-scheduler-date-table-cell'); + const firstCellBox = await cells.first().boundingBox(); + expect(firstCellBox).not.toBeNull(); + + expect(boxBefore!.left).toBeGreaterThanOrEqual(firstCellBox!.left - 1); + + const bottomHandle = appointment.locator('.dx-resizable-handle-bottom'); + const handleBox = await bottomHandle.boundingBox(); + + const cellHeight = firstCellBox!.height; + await bottomHandle.hover(); + await page.mouse.down(); + await page.mouse.move( + handleBox!.x + handleBox!.width / 2, + handleBox!.y + cellHeight, + { steps: 5 }, + ); + await page.mouse.up(); + + const boxAfter = await appointment.boundingBox(); + expect(boxAfter).not.toBeNull(); + expect(boxAfter!.left).toBeCloseTo(boxBefore!.left, 0); + }); + + test('container resize triggers appointment repositioning', async ({ page }) => { + const appointment = page.locator('.dx-scheduler-appointment').first(); + const boxBefore = await appointment.boundingBox(); + expect(boxBefore).not.toBeNull(); + + await page.setViewportSize({ width: 800, height: 600 }); + await page.waitForTimeout(500); + + const boxAfter = await appointment.boundingBox(); + expect(boxAfter).not.toBeNull(); + expect(boxAfter!.width).not.toBe(boxBefore!.width); + expect(boxAfter!.height).toBeGreaterThan(0); + expect(boxAfter!.width).toBeGreaterThan(0); + }); + + test('drag appointment to different cell updates position', async ({ page }) => { + const appointment = page.locator('.dx-scheduler-appointment').first(); + const boxBefore = await appointment.boundingBox(); + expect(boxBefore).not.toBeNull(); + + const cellHeight = await page.locator('.dx-scheduler-date-table-cell').first().evaluate( + (el) => el.getBoundingClientRect().height, + ); + + await appointment.hover(); + await page.mouse.down(); + await page.mouse.move( + boxBefore!.x + boxBefore!.width / 2, + boxBefore!.y + cellHeight * 2, + { steps: 10 }, + ); + await page.mouse.up(); + + await page.waitForTimeout(300); + + const boxAfter = await appointment.boundingBox(); + expect(boxAfter).not.toBeNull(); + expect(Math.abs(boxAfter!.y - boxBefore!.y)).toBeGreaterThan(cellHeight * 0.5); + }); +}); From ec61ecdb04ec7754798d229e016b130a2260054a Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 22 Mar 2026 04:56:31 -0300 Subject: [PATCH 5/7] Scheduler - Wave 2: Migrate subscribes to Scale facade --- .../scheduler/entieties/scale.test.ts | 98 +++++++++++++++++++ .../__internal/scheduler/entieties/scale.ts | 71 ++++++++++++++ .../js/__internal/scheduler/m_subscribes.ts | 64 ++++++------ 3 files changed, 201 insertions(+), 32 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts b/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts index 8a2987c26e91..cf0f6a05eb5d 100644 --- a/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts +++ b/packages/devextreme/js/__internal/scheduler/entieties/scale.test.ts @@ -21,6 +21,20 @@ const createMockWorkspace = (overrides: Record = {}) => ({ }, isVerticalGroupedWorkSpace: jest.fn(() => false), type: 'day' as const, + getCellWidth: jest.fn(() => 100), + getCellHeight: jest.fn(() => 50), + option: jest.fn((name: string) => { + const options = { cellDuration: 30, startDayHour: 9, endDayHour: 18 }; + return options[name]; + }), + _getGroupCount: jest.fn(() => 2), + needRecalculateResizableArea: jest.fn(() => true), + getGroupBounds: jest.fn(() => ({ + left: 10, right: 200, top: 5, bottom: 300, + })), + getAgendaVerticalStepHeight: jest.fn(() => 80), + supportAllDayRow: jest.fn(() => true), + isGroupedByDate: jest.fn(() => false), ...overrides, }); @@ -257,4 +271,88 @@ describe('WorkspaceScale', () => { expect(scale.getResizableStep()).toBe(20); }); }); + + describe('getCellWidth / getCellHeight', () => { + it('should delegate to workspace', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.getCellWidth()).toBe(100); + expect(scale.getCellHeight()).toBe(50); + }); + + it('should return 0 when workspace is undefined', () => { + const scale = new WorkspaceScale(() => undefined); + + expect(scale.getCellWidth()).toBe(0); + expect(scale.getCellHeight()).toBe(0); + }); + }); + + describe('cellDuration / startDayHour / endDayHour', () => { + it('should read from workspace options', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.cellDuration).toBe(30); + expect(scale.startDayHour).toBe(9); + expect(scale.endDayHour).toBe(18); + }); + }); + + describe('groupCount', () => { + it('should delegate to workspace _getGroupCount', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.groupCount).toBe(2); + }); + }); + + describe('needRecalculateResizableArea', () => { + it('should delegate to workspace', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.needRecalculateResizableArea()).toBe(true); + }); + }); + + describe('getGroupBounds', () => { + it('should delegate to workspace', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + const bounds = scale.getGroupBounds({ groupIndex: 0 }); + + expect(bounds).toEqual({ + left: 10, right: 200, top: 5, bottom: 300, + }); + }); + + it('should return undefined when workspace is undefined', () => { + const scale = new WorkspaceScale(() => undefined); + + expect(scale.getGroupBounds({})).toBeUndefined(); + }); + }); + + describe('agendaVerticalStepHeight', () => { + it('should delegate to workspace', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.agendaVerticalStepHeight).toBe(80); + }); + }); + + describe('supportAllDayRow / isGroupedByDate', () => { + it('should delegate to workspace', () => { + const workspace = createMockWorkspace(); + const scale = new WorkspaceScale(() => workspace); + + expect(scale.supportAllDayRow).toBe(true); + expect(scale.isGroupedByDate).toBe(false); + }); + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts index a2da27a1d698..e78b7d06bb25 100644 --- a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts +++ b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts @@ -13,6 +13,13 @@ export interface CellDateInfo { index: number; } +export interface GroupBounds { + left: number; + right: number; + top: number; + bottom: number; +} + interface Workspace { positionHelper: { getResizableStep: () => number; @@ -21,6 +28,15 @@ interface Workspace { viewDataProvider: ViewDataProviderType; isVerticalGroupedWorkSpace: () => boolean; type: string; + getCellWidth: () => number; + getCellHeight: () => number; + option: (name: string) => unknown; + _getGroupCount: () => number; + needRecalculateResizableArea: () => boolean; + getGroupBounds: (coordinates: unknown) => GroupBounds; + getAgendaVerticalStepHeight: () => number; + supportAllDayRow: () => boolean; + isGroupedByDate: () => boolean; } export interface Scale { @@ -38,6 +54,17 @@ export interface Scale { columnIndex: number, isAllDay: boolean, ) => CellsInfo | undefined; + getCellWidth: () => number; + getCellHeight: () => number; + cellDuration: number; + startDayHour: number; + endDayHour: number; + groupCount: number; + needRecalculateResizableArea: () => boolean; + getGroupBounds: (coordinates: unknown) => GroupBounds | undefined; + agendaVerticalStepHeight: number; + supportAllDayRow: boolean; + isGroupedByDate: boolean; isVerticalGroupedWorkSpace: () => boolean; isDateAndTimeView: () => boolean; } @@ -104,6 +131,50 @@ export class WorkspaceScale implements Scale { }; } + getCellWidth(): number { + return this.getWorkspace()?.getCellWidth() ?? 0; + } + + getCellHeight(): number { + return this.getWorkspace()?.getCellHeight() ?? 0; + } + + get cellDuration(): number { + return (this.getWorkspace()?.option('cellDuration') as number) ?? 30; + } + + get startDayHour(): number { + return (this.getWorkspace()?.option('startDayHour') as number) ?? 0; + } + + get endDayHour(): number { + return (this.getWorkspace()?.option('endDayHour') as number) ?? 24; + } + + get groupCount(): number { + return this.getWorkspace()?._getGroupCount() ?? 0; + } + + needRecalculateResizableArea(): boolean { + return this.getWorkspace()?.needRecalculateResizableArea() ?? false; + } + + getGroupBounds(coordinates: unknown): GroupBounds | undefined { + return this.getWorkspace()?.getGroupBounds(coordinates); + } + + get agendaVerticalStepHeight(): number { + return this.getWorkspace()?.getAgendaVerticalStepHeight() ?? 0; + } + + get supportAllDayRow(): boolean { + return this.getWorkspace()?.supportAllDayRow() ?? false; + } + + get isGroupedByDate(): boolean { + return this.getWorkspace()?.isGroupedByDate() ?? false; + } + isVerticalGroupedWorkSpace(): boolean { return this.getWorkspace()?.isVerticalGroupedWorkSpace() ?? false; } diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index ceb9b6ae1979..0493ddd10cc5 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -47,7 +47,7 @@ const subscribes = { }, isGroupedByDate() { - return this.getWorkSpace().isGroupedByDate(); + return this._scale.isGroupedByDate; }, showAppointmentTooltip(options: { data: SafeAppointment; target: dxElementWrapper }) { @@ -83,13 +83,11 @@ const subscribes = { event, element, rawAppointment, isDropToTheSameCell, isDropToSelfScheduler, }) { 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)); const fromAllDay = Boolean(rawAppointment.allDay); const toAllDay = Boolean(targetedRawAppointment.allDay); - const isDropBetweenAllDay = this._workSpace.supportAllDayRow() && fromAllDay !== toAllDay; + const isDropBetweenAllDay = this._scale.supportAllDayRow && fromAllDay !== toAllDay; const isDragAndDropBetweenComponents = event.fromComponent !== event.toComponent; @@ -97,7 +95,6 @@ const subscribes = { this._appointments.moveAppointmentBack(event); }; if (!isDropToSelfScheduler && isDragAndDropBetweenComponents) { - // drop between schedulers return; } @@ -127,7 +124,6 @@ const subscribes = { ...targetedAppointmentRaw, } as TargetedAppointment; const adapter = new AppointmentAdapter(targetedAppointment, this._dataAccessors); - // 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'); @@ -144,23 +140,27 @@ const subscribes = { if (groups?.length) { if (allDay || this.currentView.type === 'month') { - const horizontalGroupBounds = this._workSpace.getGroupBounds(options.coordinates); - return { - left: horizontalGroupBounds.left, - right: horizontalGroupBounds.right, - top: 0, - bottom: 0, - }; + const horizontalGroupBounds = this._scale.getGroupBounds(options.coordinates); + if (horizontalGroupBounds) { + return { + left: horizontalGroupBounds.left, + right: horizontalGroupBounds.right, + top: 0, + bottom: 0, + }; + } } - if (!allDay && VERTICAL_VIEW_TYPES.includes(this.currentView.type) && this._workSpace.isVerticalGroupedWorkSpace()) { - const verticalGroupBounds = this._workSpace.getGroupBounds(options.coordinates); - return { - left: 0, - right: 0, - top: verticalGroupBounds.top, - bottom: verticalGroupBounds.bottom, - }; + if (!allDay && VERTICAL_VIEW_TYPES.includes(this.currentView.type) && this._scale.isVerticalGroupedWorkSpace()) { + const verticalGroupBounds = this._scale.getGroupBounds(options.coordinates); + if (verticalGroupBounds) { + return { + left: 0, + right: 0, + top: verticalGroupBounds.top, + bottom: verticalGroupBounds.bottom, + }; + } } } @@ -168,7 +168,7 @@ const subscribes = { }, needRecalculateResizableArea() { - return this.getWorkSpace().needRecalculateResizableArea(); + return this._scale.needRecalculateResizableArea(); }, isAllDay(appointmentData): boolean { @@ -179,21 +179,21 @@ const subscribes = { return getDeltaTime(e, initialSize, { viewType: this.currentView.type, cellSize: { - width: this.getWorkSpace().getCellWidth(), - height: this.getWorkSpace().getCellHeight(), + width: this._scale.getCellWidth(), + height: this._scale.getCellHeight(), }, - cellDurationInMinutes: this.getWorkSpace().option('cellDuration'), - resizableStep: this.getWorkSpace().positionHelper.getResizableStep(), + cellDurationInMinutes: this._scale.cellDuration, + resizableStep: this._scale.getResizableStep(), isAllDayPanel: isAllDay(this, itemData), }); }, getCellWidth() { - return this.getWorkSpace().getCellWidth(); + return this._scale.getCellWidth(); }, getCellHeight() { - return this.getWorkSpace().getCellHeight(); + return this._scale.getCellHeight(); }, needCorrectAppointmentDates() { @@ -229,7 +229,7 @@ const subscribes = { }, getGroupCount() { - return this._workSpace._getGroupCount(); + return this._scale.groupCount; }, mapAppointmentFields(config) { @@ -252,7 +252,7 @@ const subscribes = { }, getAgendaVerticalStepHeight() { - return this.getWorkSpace().getAgendaVerticalStepHeight(); + return this._scale.agendaVerticalStepHeight; }, getAgendaDuration() { @@ -276,11 +276,11 @@ const subscribes = { }, getEndDayHour() { - return this._workSpace.option('endDayHour') || this.option('endDayHour'); + return this._scale.endDayHour || this.option('endDayHour'); }, getStartDayHour() { - return this._workSpace.option('startDayHour') || this.option('startDayHour'); + return this._scale.startDayHour || this.option('startDayHour'); }, getViewOffsetMs() { From ea64312c79f3a7b06a67de901ad5a31c8b8b1ac6 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 22 Mar 2026 05:32:11 -0300 Subject: [PATCH 6/7] Scheduler - Wave 3: Migrate drag behavior and removeDroppableCellClass to Scale --- .../js/__internal/scheduler/entieties/scale.ts | 18 ++++++++++++++++++ .../scheduler/m_appointment_drag_behavior.ts | 9 ++++----- .../js/__internal/scheduler/m_subscribes.ts | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts index e78b7d06bb25..e8d60ae583f2 100644 --- a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts +++ b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts @@ -37,6 +37,9 @@ interface Workspace { getAgendaVerticalStepHeight: () => number; supportAllDayRow: () => boolean; isGroupedByDate: () => boolean; + getDroppableCell: () => unknown; + getCellByCoordinates: (coordinates: unknown, isAllDay: boolean) => unknown; + removeDroppableCellClass: () => void; } export interface Scale { @@ -65,6 +68,9 @@ export interface Scale { agendaVerticalStepHeight: number; supportAllDayRow: boolean; isGroupedByDate: boolean; + getDroppableCell: () => unknown; + getCellByCoordinates: (coordinates: unknown, isAllDay: boolean) => unknown; + removeDroppableCellClass: () => void; isVerticalGroupedWorkSpace: () => boolean; isDateAndTimeView: () => boolean; } @@ -175,6 +181,18 @@ export class WorkspaceScale implements Scale { return this.getWorkspace()?.isGroupedByDate() ?? false; } + getDroppableCell(): unknown { + return this.getWorkspace()?.getDroppableCell(); + } + + getCellByCoordinates(coordinates: unknown, isAllDay: boolean): unknown { + return this.getWorkspace()?.getCellByCoordinates(coordinates, isAllDay); + } + + removeDroppableCellClass(): void { + this.getWorkspace()?.removeDroppableCellClass(); + } + isVerticalGroupedWorkSpace(): boolean { return this.getWorkspace()?.isVerticalGroupedWorkSpace() ?? false; } 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 7d7b4a787f75..fdc72716ab04 100644 --- a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts +++ b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts @@ -10,7 +10,7 @@ import type { AppointmentViewModelPlain } from './view_model/types'; const APPOINTMENT_ITEM_CLASS = 'dx-scheduler-appointment'; export default class AppointmentDragBehavior { - workspace = this.scheduler._workSpace; + scale = this.scheduler._scale; appointments = this.scheduler._appointments; @@ -62,8 +62,8 @@ export default class AppointmentDragBehavior { const container = this.appointments._getAppointmentContainer(isAllDay); container.append(element); - const $targetCell = this.workspace.getDroppableCell(); - const $dragCell = this.workspace.getCellByCoordinates(this.initialPosition, isAllDay); + const $targetCell: any = this.scale.getDroppableCell(); + const $dragCell = this.scale.getCellByCoordinates(this.initialPosition, isAllDay); this.appointments.notifyObserver('updateAppointmentAfterDrag', { event, @@ -150,7 +150,6 @@ export default class AppointmentDragBehavior { } } - // NOTE: event.cancel may be promise or different type, so we need strict check here. if (e.cancel === true) { options.onDragCancel(e); } @@ -212,6 +211,6 @@ export default class AppointmentDragBehavior { removeDroppableClasses() { this.appointments._removeDragSourceClassFromDraggedAppointment(); - this.workspace.removeDroppableCellClass(); + this.scale.removeDroppableCellClass(); } } diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index 0493ddd10cc5..55451c9a9880 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -292,7 +292,7 @@ const subscribes = { }, removeDroppableCellClass() { - this._workSpace.removeDroppableCellClass(); + this._scale.removeDroppableCellClass(); }, } as const; From 0b2143c94fb34dbd8170c5b46030f1bb203ec6c3 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 22 Mar 2026 07:24:27 -0300 Subject: [PATCH 7/7] Scheduler - Wave 4: Type Scale, split into sub-interfaces, remove viewDataProvider and DOMMetaData from resizing --- .../appointments/m_appointment_collection.ts | 2 - .../scheduler/appointments/resizing/types.ts | 6 -- .../__internal/scheduler/entieties/scale.ts | 79 ++++++++++++------- .../scheduler/m_appointment_drag_behavior.ts | 6 +- .../js/__internal/scheduler/m_scheduler.ts | 6 -- 5 files changed, 53 insertions(+), 46 deletions(-) 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 fc553aa14dc7..0b98e69ecccb 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -784,7 +784,6 @@ class SchedulerAppointments extends CollectionWidget { isVerticalGroupedWorkSpace: scale.isVerticalGroupedWorkSpace(), appointmentRect: getBoundingRect($element[0]), parentAppointmentRect: getBoundingRect($element.parent()[0]), - viewDataProvider: scale.viewDataProvider, getCellDateInfo: scale.getCellDateInfo.bind(scale), getCellGeometry: scale.getCellGeometry.bind(scale), isDateAndTimeView: scale.isDateAndTimeView(), @@ -793,7 +792,6 @@ class SchedulerAppointments extends CollectionWidget { timeZoneCalculator, dataAccessors: this.dataAccessors, rtlEnabled: this.option('rtlEnabled'), - DOMMetaData: scale.getDOMElementsMetaData(), viewOffset: this.invoke('getViewOffsetMs'), }); } diff --git a/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts b/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts index f5e878484267..6316008d8540 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/types.ts @@ -1,6 +1,5 @@ import type { CellDateInfo } from '../../entieties/scale'; import type { TimeZoneCalculator } from '../../r1/timezone_calculator'; -import type { ViewDataProviderType } from '../../types'; import type { AppointmentDataAccessor } from '../../utils/data_accessor/appointment_data_accessor'; import type { AppointmentItemViewModel } from '../../view_model/types'; @@ -15,7 +14,6 @@ export interface GetAppointmentDateRangeOptions { isVerticalGroupedWorkSpace: boolean; appointmentRect: Rect; parentAppointmentRect: Rect; - viewDataProvider: ViewDataProviderType; getCellDateInfo: ( rowIndex: number, columnIndex: number, @@ -33,10 +31,6 @@ export interface GetAppointmentDateRangeOptions { timeZoneCalculator: TimeZoneCalculator; dataAccessors: AppointmentDataAccessor; rtlEnabled?: boolean; - DOMMetaData: { - allDayPanelCellsMeta: Rect[]; - dateTableCellsMeta: Rect[][]; - }; viewOffset: number; } diff --git a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts index e8d60ae583f2..87c22491609c 100644 --- a/packages/devextreme/js/__internal/scheduler/entieties/scale.ts +++ b/packages/devextreme/js/__internal/scheduler/entieties/scale.ts @@ -1,3 +1,5 @@ +import type { dxElementWrapper } from '@js/core/renderer'; + import type { CellsInfo, Rect } from '../appointments/resizing/types'; import { isDateAndTimeView } from '../r1/utils/index'; import type { ViewDataProviderType } from '../types'; @@ -20,10 +22,17 @@ export interface GroupBounds { bottom: number; } +export interface CellCoordinates { + left: number; + top: number; +} + +export interface GroupCoordinates { + groupIndex: number; +} + interface Workspace { - positionHelper: { - getResizableStep: () => number; - }; + positionHelper: { getResizableStep: () => number }; getDOMElementsMetaData: () => DOMElementsMetaData; viewDataProvider: ViewDataProviderType; isVerticalGroupedWorkSpace: () => boolean; @@ -33,48 +42,61 @@ interface Workspace { option: (name: string) => unknown; _getGroupCount: () => number; needRecalculateResizableArea: () => boolean; - getGroupBounds: (coordinates: unknown) => GroupBounds; + getGroupBounds: (coordinates: GroupCoordinates) => GroupBounds; getAgendaVerticalStepHeight: () => number; supportAllDayRow: () => boolean; isGroupedByDate: () => boolean; - getDroppableCell: () => unknown; - getCellByCoordinates: (coordinates: unknown, isAllDay: boolean) => unknown; + getDroppableCell: () => dxElementWrapper; + getCellByCoordinates: (coordinates: CellCoordinates, isAllDay: boolean) => dxElementWrapper; removeDroppableCellClass: () => void; } -export interface Scale { - readonly viewDataProvider: ViewDataProviderType | undefined; +export interface ScaleGeometry { + getCellWidth: () => number; + getCellHeight: () => number; getResizableStep: () => number; + getCellGeometry: ( + rowIndex: number, + columnIndex: number, + isAllDay: boolean, + ) => CellsInfo | undefined; getDOMElementsMetaData: () => DOMElementsMetaData | undefined; +} + +export interface ScaleData { + readonly viewDataProvider: ViewDataProviderType | undefined; getCellDateInfo: ( rowIndex: number, columnIndex: number, isAllDay: boolean, rtlEnabled: boolean, ) => CellDateInfo | undefined; - getCellGeometry: ( - rowIndex: number, - columnIndex: number, - isAllDay: boolean, - ) => CellsInfo | undefined; - getCellWidth: () => number; - getCellHeight: () => number; cellDuration: number; startDayHour: number; endDayHour: number; groupCount: number; - needRecalculateResizableArea: () => boolean; - getGroupBounds: (coordinates: unknown) => GroupBounds | undefined; agendaVerticalStepHeight: number; supportAllDayRow: boolean; isGroupedByDate: boolean; - getDroppableCell: () => unknown; - getCellByCoordinates: (coordinates: unknown, isAllDay: boolean) => unknown; - removeDroppableCellClass: () => void; isVerticalGroupedWorkSpace: () => boolean; isDateAndTimeView: () => boolean; } +export interface ScaleDragDrop { + getDroppableCell: () => dxElementWrapper | undefined; + getCellByCoordinates: ( + coordinates: CellCoordinates, isAllDay: boolean, + ) => dxElementWrapper | undefined; + removeDroppableCellClass: () => void; +} + +export interface ScaleInteraction { + needRecalculateResizableArea: () => boolean; + getGroupBounds: (coordinates: GroupCoordinates) => GroupBounds | undefined; +} + +export type Scale = ScaleGeometry & ScaleData & ScaleDragDrop & ScaleInteraction; + export class WorkspaceScale implements Scale { private readonly getWorkspace: () => Workspace | undefined; @@ -103,12 +125,8 @@ export class WorkspaceScale implements Scale { ): CellDateInfo | undefined { const workspace = this.getWorkspace(); if (!workspace) return undefined; - const cellData = workspace.viewDataProvider.getCellData( - rowIndex, - columnIndex, - isAllDay, - rtlEnabled, - ); + const { viewDataProvider } = workspace; + const cellData = viewDataProvider.getCellData(rowIndex, columnIndex, isAllDay, rtlEnabled); return { startDate: cellData.startDate, endDate: cellData.endDate, @@ -165,7 +183,7 @@ export class WorkspaceScale implements Scale { return this.getWorkspace()?.needRecalculateResizableArea() ?? false; } - getGroupBounds(coordinates: unknown): GroupBounds | undefined { + getGroupBounds(coordinates: GroupCoordinates): GroupBounds | undefined { return this.getWorkspace()?.getGroupBounds(coordinates); } @@ -181,11 +199,14 @@ export class WorkspaceScale implements Scale { return this.getWorkspace()?.isGroupedByDate() ?? false; } - getDroppableCell(): unknown { + getDroppableCell(): dxElementWrapper | undefined { return this.getWorkspace()?.getDroppableCell(); } - getCellByCoordinates(coordinates: unknown, isAllDay: boolean): unknown { + getCellByCoordinates( + coordinates: CellCoordinates, + isAllDay: boolean, + ): dxElementWrapper | undefined { return this.getWorkspace()?.getCellByCoordinates(coordinates, isAllDay); } 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 fdc72716ab04..513f2cc7204c 100644 --- a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts +++ b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts @@ -62,15 +62,15 @@ export default class AppointmentDragBehavior { const container = this.appointments._getAppointmentContainer(isAllDay); container.append(element); - const $targetCell: any = this.scale.getDroppableCell(); + const $targetCell = this.scale.getDroppableCell(); const $dragCell = this.scale.getCellByCoordinates(this.initialPosition, isAllDay); this.appointments.notifyObserver('updateAppointmentAfterDrag', { event, element, rawAppointment, - isDropToTheSameCell: $targetCell.is($dragCell), - isDropToSelfScheduler: $targetCell.length > 0, + isDropToTheSameCell: $targetCell?.is($dragCell), + isDropToSelfScheduler: ($targetCell?.length ?? 0) > 0, }); } diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index e422e14c31d6..7439f7e080ad 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1262,12 +1262,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { timeZoneCalculator: this.timeZoneCalculator, scale: this._scale, getResizableStep: () => this._scale.getResizableStep(), - getDOMElementsMetaData: () => this._scale.getDOMElementsMetaData(), - getViewDataProvider: () => this._scale.viewDataProvider, - getCellDateInfo: this._scale.getCellDateInfo.bind(this._scale), - getCellGeometry: this._scale.getCellGeometry.bind(this._scale), - isVerticalGroupedWorkSpace: () => this._scale.isVerticalGroupedWorkSpace(), - isDateAndTimeView: () => this._scale.isDateAndTimeView(), onContentReady: () => { this._workSpace?.option('allDayExpanded', this.isAllDayExpanded()); },