From 74e0c49c44169c870a12d2303a9146c429f6bb16 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 5 Oct 2026 12:49:39 +0400 Subject: [PATCH 01/10] Scheduler: type the appointment parts ahead of the m_ rename --- .../appointments/appointment/m_appointment.ts | 99 ++++++++++--------- .../appointments/appointment/m_types.ts | 23 ++++- .../appointments/m_appointment_layout.ts | 57 +++++++---- .../appointments/m_appointments_kbn.ts | 3 +- .../scheduler/appointments/m_text_utils.ts | 44 ++++++--- .../js/__internal/ui/resizable/resizable.ts | 2 +- 6 files changed, 146 insertions(+), 82 deletions(-) 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 828f6172d282..1c5b2a432d1c 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts @@ -3,12 +3,13 @@ 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'; import registerComponent from '@js/core/component_registrator'; -import DOMComponent from '@js/core/dom_component'; import Guid from '@js/core/guid'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; -import Resizable from '@js/ui/resizable'; +import DOMComponent from '@ts/core/widget/dom_component'; +import type { OptionChanged } from '@ts/core/widget/types'; +import Resizable from '@ts/ui/resizable/resizable'; import { hide, show } from '@ts/ui/tooltip/tooltip'; import { @@ -25,8 +26,9 @@ import { } from '../../classes'; import type { SubscribeKey, SubscribeMethods } from '../../m_subscribes'; import { validateRRule } from '../../recurrence/validate_rule'; +import type { SafeAppointment } from '../../types'; import type { AppointmentDataAccessor } from '../../utils/data_accessor/appointment_data_accessor'; -import type { AppointmentProperties } from './m_types'; +import type { AppointmentProperties, AppointmentReducedPart } from './m_types'; import { getAriaDescription, getAriaLabel, @@ -39,12 +41,21 @@ const DEFAULT_VERTICAL_HANDLES = 'top bottom'; const REDUCED_APPOINTMENT_POINTERENTER_EVENT_NAME = addNamespace(pointerEvents.enter, 'dxSchedulerAppointment'); const REDUCED_APPOINTMENT_POINTERLEAVE_EVENT_NAME = addNamespace(pointerEvents.leave, 'dxSchedulerAppointment'); -export class Appointment extends DOMComponent { - get coloredElement(): any { +interface ResizingRule { + handles: string; + minWidth: number | undefined; + minHeight: number; + step: number; + roundStepValue: boolean; + stepPrecision?: string; +} + +export class Appointment extends DOMComponent { + get coloredElement(): dxElementWrapper { return this.$element(); } - get rawAppointment(): any { + get rawAppointment(): SafeAppointment { return this.option('data'); } @@ -52,8 +63,7 @@ export class Appointment extends DOMComponent { return this.option('dataAccessors'); } - _getDefaultOptions() { - // @ts-expect-error + _getDefaultOptions(): AppointmentProperties { return extend(super._getDefaultOptions(), { data: {}, groupIndex: -1, @@ -70,7 +80,7 @@ export class Appointment extends DOMComponent { cellHeight: 0, cellWidth: 0, isDragSource: false, - }); + } satisfies Partial); } notifyObserver( @@ -93,7 +103,7 @@ export class Appointment extends DOMComponent { return notifyScheduler.invoke(funcName, ...args); } - _optionChanged(args) { + _optionChanged(args: OptionChanged): void { switch (args.name) { case 'data': case 'groupIndex': @@ -114,22 +124,21 @@ export class Appointment extends DOMComponent { this._renderDragSourceClass(); break; default: - // @ts-expect-error super._optionChanged(args); } } - _getHorizontalResizingRule() { + _getHorizontalResizingRule(): ResizingRule { const reducedHandles = { head: this.option('rtlEnabled') ? 'right' : 'left', body: '', tail: this.option('rtlEnabled') ? 'left' : 'right', }; - const getResizableStep: any = this.option('getResizableStep'); + const getResizableStep = this.option('getResizableStep'); const step = getResizableStep ? getResizableStep() : 0; return { - handles: this.option('reduced') ? reducedHandles[this.option('reduced') as any] : DEFAULT_HORIZONTAL_HANDLES, + handles: this.option('reduced') ? reducedHandles[this.option('reduced') as AppointmentReducedPart] : DEFAULT_HORIZONTAL_HANDLES, minHeight: 0, minWidth: this.invoke('getCellWidth'), step, @@ -137,7 +146,7 @@ export class Appointment extends DOMComponent { }; } - _getVerticalResizingRule() { + _getVerticalResizingRule(): ResizingRule { const height = Math.round(this.invoke('getCellHeight')); return { @@ -149,8 +158,7 @@ export class Appointment extends DOMComponent { }; } - _render() { - // @ts-expect-error + _render(): void { super._render(); this._renderAppointmentGeometry(); @@ -170,14 +178,15 @@ export class Appointment extends DOMComponent { this._setResourceColor(); } - _setResourceColor() { + _setResourceColor(): void { const appointmentConfig = { itemData: this.rawAppointment, groupIndex: this.option('groupIndex') ?? 0, }; const resourceManager = this.option('getResourceManager')(); - resourceManager.getAppointmentColor(appointmentConfig) + // eslint-disable-next-line no-void + void resourceManager.getAppointmentColor(appointmentConfig) .then((color) => { if (color) { this.coloredElement.css('backgroundColor', color); @@ -206,81 +215,84 @@ export class Appointment extends DOMComponent { } _renderAppointmentGeometry(): void { - const geometry: any = this.option('geometry'); - const $element: any = this.$element(); + const geometry = this.option('geometry'); + const $element = this.$element(); move($element, { top: geometry.top, left: geometry.left, }); $element.css({ + // @ts-expect-error the agenda passes '100%', which compares as NaN and is kept width: geometry.width < 0 ? 0 : geometry.width, height: geometry.height < 0 ? 0 : geometry.height, }); } - _renderEmptyClass() { - const geometry: any = this.option('geometry'); + _renderEmptyClass(): void { + const geometry = this.option('geometry'); if (geometry.empty || this.option('isCompact')) { - (this.$element() as any).addClass(EMPTY_APPOINTMENT_CLASS); + this.$element().addClass(EMPTY_APPOINTMENT_CLASS); } } - _renderReducedAppointment() { - const reducedPart: any = this.option('reduced'); + _renderReducedAppointment(): void { + const reducedPart = this.option('reduced'); if (!reducedPart) { return; } - (this.$element() as any) + this.$element() .toggleClass(REDUCED_APPOINTMENT_CLASS, true) .toggleClass(REDUCED_APPOINTMENT_PARTS_CLASSES[reducedPart], true); this._renderAppointmentReducedIcon(); } - _renderAppointmentReducedIcon() { + _renderAppointmentReducedIcon(): void { const $icon = $('
') .addClass(REDUCED_APPOINTMENT_ICON) .appendTo(this.$element()); eventsEngine.off($icon, REDUCED_APPOINTMENT_POINTERENTER_EVENT_NAME); eventsEngine.on($icon, REDUCED_APPOINTMENT_POINTERENTER_EVENT_NAME, () => { - show({ + // eslint-disable-next-line no-void + void show({ target: $icon.get(0), content: getReducedIconTooltip(this.option()), }); }); eventsEngine.off($icon, REDUCED_APPOINTMENT_POINTERLEAVE_EVENT_NAME); eventsEngine.on($icon, REDUCED_APPOINTMENT_POINTERLEAVE_EVENT_NAME, () => { - hide(); + // eslint-disable-next-line no-void + void hide(); }); } - _renderAllDayClass() { - (this.$element() as any).toggleClass(ALL_DAY_APPOINTMENT_CLASS, Boolean(this.option('allDay'))); + _renderAllDayClass(): void { + this.$element().toggleClass(ALL_DAY_APPOINTMENT_CLASS, Boolean(this.option('allDay'))); } - _renderDragSourceClass() { - (this.$element() as any).toggleClass(APPOINTMENT_DRAG_SOURCE_CLASS, Boolean(this.option('isDragSource'))); + _renderDragSourceClass(): void { + this.$element().toggleClass(APPOINTMENT_DRAG_SOURCE_CLASS, Boolean(this.option('isDragSource'))); } - _renderRecurrenceClass() { + _renderRecurrenceClass(): void { const rule = this.dataAccessors.get('recurrenceRule', this.rawAppointment); if (validateRRule(rule)) { - (this.$element() as any).addClass(RECURRENCE_APPOINTMENT_CLASS); + this.$element().addClass(RECURRENCE_APPOINTMENT_CLASS); } } - _renderDirection() { - (this.$element() as any).addClass(DIRECTION_APPOINTMENT_CLASSES[this.option('direction') as any]); + _renderDirection(): void { + this.$element().addClass(DIRECTION_APPOINTMENT_CLASSES[this.option('direction')]); } - _createResizingConfig() { - const config: any = this.option('direction') === 'vertical' ? this._getVerticalResizingRule() : this._getHorizontalResizingRule(); + _createResizingConfig(): ResizingRule { + const config: ResizingRule = this.option('direction') === 'vertical' ? this._getVerticalResizingRule() : this._getHorizontalResizingRule(); if (!this.invoke('isGroupedByDate')) { config.stepPrecision = 'strict'; @@ -289,9 +301,8 @@ export class Appointment extends DOMComponent { return config; } - _renderResizable() { + _renderResizable(): void { if (this.option('allowResize')) { - // @ts-expect-error this._createComponent( this.$element(), Resizable, @@ -303,9 +314,9 @@ export class Appointment extends DOMComponent { } } - _useTemplates() { + _useTemplates(): boolean { return false; } } -registerComponent('dxSchedulerAppointment', Appointment as any); +registerComponent('dxSchedulerAppointment', Appointment); 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 890a589ca966..472b0085703e 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts @@ -1,30 +1,45 @@ import type { Orientation } from '@js/common'; +import type { DOMComponentProperties } from '@ts/core/widget/dom_component'; import type NotifyScheduler from '@ts/scheduler/base/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'; import type { ResourceManager } from '@ts/scheduler/utils/resource_manager/resource_manager'; -export interface AppointmentProperties extends Record { +import type { Appointment } from './m_appointment'; + +export type AppointmentReducedPart = 'head' | 'body' | 'tail'; + +export interface AppointmentGeometry { + top?: number; + left?: number; + width: number | string; + height: number; + empty?: boolean; +} + +export interface AppointmentProperties extends DOMComponentProperties { data: SafeAppointment; groupIndex?: number; groupTexts: string[]; notifyScheduler: NotifyScheduler | undefined; - geometry: any; + geometry: AppointmentGeometry; direction: Orientation; allowResize: boolean; allowDrag: boolean; allowDelete: boolean; allDay: boolean; - reduced: string; + reduced: AppointmentReducedPart | null | undefined; isCompact: boolean; startDate: Date; cellWidth: number; cellHeight: number; resizableConfig: Record; - groups: any[]; + groups: string[]; partIndex?: number; partTotalCount: number; + isDragSource: boolean; + sortedIndex?: number; dataAccessors: AppointmentDataAccessor; timeZoneCalculator: TimeZoneCalculator; 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 a4e24a5c8c63..f797876703be 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_layout.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_layout.ts @@ -1,5 +1,6 @@ import messageLocalization from '@js/common/core/localization/message'; import domAdapter from '@js/core/dom_adapter'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { APPOINTMENT_CONTENT_CLASSES } from '../classes'; @@ -7,8 +8,22 @@ import { APPOINTMENT_CONTENT_CLASSES } from '../classes'; const allDayText = ` ${messageLocalization.format('dxScheduler-allDay')}: `; const recurringText = messageLocalization.format('dxScheduler-appointmentAriaLabel-recurring'); -export const createAppointmentLayout = (formatText, config) => { - const result = $((domAdapter as any).createDocumentFragment()); +export interface AppointmentFormatText { + text: string; + formatDate: string; +} + +export interface AppointmentLayoutConfig { + isAllDay?: boolean; + isRecurrence?: string; + html?: string; +} + +export const createAppointmentLayout = ( + formatText: AppointmentFormatText, + config: AppointmentLayoutConfig, +): dxElementWrapper => { + const result = $(domAdapter.createDocumentFragment()); $('
') .text(formatText.text) @@ -23,24 +38,29 @@ export const createAppointmentLayout = (formatText, config) => { $('
').addClass(APPOINTMENT_CONTENT_CLASSES.APPOINTMENT_DATE).text(formatText.formatDate).appendTo($contentDetails); - config.isRecurrence - && $('') - .addClass(`${APPOINTMENT_CONTENT_CLASSES.RECURRING_ICON} dx-icon-repeat`) - .attr('aria-label', recurringText) - .attr('role', 'img') - .appendTo(result); + if (config.isRecurrence) { + $('') + .addClass(`${APPOINTMENT_CONTENT_CLASSES.RECURRING_ICON} dx-icon-repeat`) + .attr('aria-label', recurringText) + .attr('role', 'img') + .appendTo(result); + } - config.isAllDay - && $('
') - .text(allDayText) - .addClass(APPOINTMENT_CONTENT_CLASSES.ALL_DAY_CONTENT) - .prependTo($contentDetails); + if (config.isAllDay) { + $('
') + .text(allDayText) + .addClass(APPOINTMENT_CONTENT_CLASSES.ALL_DAY_CONTENT) + .prependTo($contentDetails); + } return result; }; -export const createAgendaAppointmentLayout = (formatText, config) => { - const result = $((domAdapter as any).createDocumentFragment()); +export const createAgendaAppointmentLayout = ( + formatText: AppointmentFormatText, + config: AppointmentLayoutConfig, +): dxElementWrapper => { + const result = $(domAdapter.createDocumentFragment()); const leftLayoutContainer = $('
') .addClass('dx-scheduler-agenda-appointment-left-layout') @@ -54,9 +74,10 @@ export const createAgendaAppointmentLayout = (formatText, config) => { .addClass(APPOINTMENT_CONTENT_CLASSES.AGENDA_MARKER) .appendTo(leftLayoutContainer); - config.isRecurrence - && $('').addClass(`${APPOINTMENT_CONTENT_CLASSES.RECURRING_ICON} dx-icon-repeat`).attr('aria-label', recurringText) - .appendTo(marker); + if (config.isRecurrence) { + $('').addClass(`${APPOINTMENT_CONTENT_CLASSES.RECURRING_ICON} dx-icon-repeat`).attr('aria-label', recurringText) + .appendTo(marker); + } // eslint-disable-next-line @typescript-eslint/no-unused-vars const text = $('
') diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts index 237da54b148a..bbecd16a1c1b 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts @@ -142,7 +142,8 @@ export class AppointmentsKeyboardNavigation { this._collection.moveAppointmentBack(); - const resizableInstance = (this.$focusTarget() as any).dxResizable('instance'); + // @ts-expect-error dxResizable is a jQuery plugin method, not declared on dxElementWrapper + const resizableInstance = this.$focusTarget().dxResizable('instance'); if (resizableInstance) { resizableInstance._detachEventHandlers(); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_text_utils.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_text_utils.ts index 7a0d2c7e03d4..450d2e72bf2d 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_text_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_text_utils.ts @@ -7,20 +7,19 @@ export enum DateFormatType { DATE = 'DATE', } -export const createFormattedDateText = (options) => { - const { - startDate, - endDate, - allDay, - format, - } = options; - - const formatType = format || getFormatType(startDate, endDate, allDay); - - return formatDates(startDate, endDate, formatType); -}; +interface FormattedDateTextOptions { + startDate: Date; + endDate: Date; + allDay?: boolean; + format?: DateFormatType; +} -export const getFormatType = (startDate, endDate, isAllDay, isDateAndTimeView?) => { +export const getFormatType = ( + startDate: Date, + endDate: Date, + isAllDay?: boolean, + isDateAndTimeView?: boolean, +): DateFormatType => { if (isAllDay) { return DateFormatType.DATE; } @@ -30,7 +29,11 @@ export const getFormatType = (startDate, endDate, isAllDay, isDateAndTimeView?) return DateFormatType.DATETIME; }; -export const formatDates = (startDate, endDate, formatType) => { +export const formatDates = ( + startDate: Date, + endDate: Date, + formatType: DateFormatType, +): string | undefined => { const dateFormat = 'monthandday'; const timeFormat = 'shorttime'; const isSameDate = startDate.getDate() === endDate.getDate(); @@ -53,3 +56,16 @@ export const formatDates = (startDate, endDate, formatType) => { return undefined; } }; + +export const createFormattedDateText = (options: FormattedDateTextOptions): string | undefined => { + const { + startDate, + endDate, + allDay, + format, + } = options; + + const formatType = format || getFormatType(startDate, endDate, allDay); + + return formatDates(startDate, endDate, formatType); +}; diff --git a/packages/devextreme/js/__internal/ui/resizable/resizable.ts b/packages/devextreme/js/__internal/ui/resizable/resizable.ts index f2b7ed0e1bef..447d0a485922 100644 --- a/packages/devextreme/js/__internal/ui/resizable/resizable.ts +++ b/packages/devextreme/js/__internal/ui/resizable/resizable.ts @@ -85,7 +85,7 @@ export interface ResizableProperties extends Properties { disabled?: boolean; - step?: string; + step?: number | string; roundStepValue?: boolean; From 467fd1309e0b5f7f61ec11e9450f60067ea09231 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 5 Oct 2026 12:50:14 +0400 Subject: [PATCH 02/10] Scheduler: type the subscribes and the appointment data source ahead of the m_ rename --- .../appointments/appointment/m_appointment.ts | 1 + .../js/__internal/scheduler/m_subscribes.ts | 134 +++++++++++------- .../js/__internal/scheduler/scheduler.ts | 2 - .../view_model/m_appointment_data_source.ts | 4 +- .../scheduler/workspaces/work_space.ts | 1 - 5 files changed, 90 insertions(+), 52 deletions(-) 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 1c5b2a432d1c..4895066ea800 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts @@ -147,6 +147,7 @@ export class Appointment extends DOMComponent { - this.updateAppointmentCore(options.target, options.data, function () { + // eslint-disable-next-line no-void + void this.updateAppointmentCore(options.target, options.data, function () { this._appointments.moveAppointmentBack(); }); }); }, - getUpdatedData(rawAppointment) { + getUpdatedData(this: Scheduler, rawAppointment) { return this.getUpdatedData(rawAppointment); }, - updateAppointmentAfterDrag({ + updateAppointmentAfterDrag(this: Scheduler, { event, element, rawAppointment, isDropToTheSameCell, isDropToSelfScheduler, }) { const { info } = this._appointments.getAppointmentSettings(element) as AppointmentItemViewModel; @@ -100,27 +107,41 @@ const subscribes = { return; } - if (isDropToSelfScheduler && (!isDropToTheSameCell || isDragAndDropBetweenComponents || isDropBetweenAllDay)) { - this.checkRecurringAppointment(rawAppointment, targetedRawAppointment, info.sourceAppointment.startDate, () => { - this.updateAppointmentCore(rawAppointment, targetedRawAppointment, onCancel, event); - }, undefined, undefined, event); + if ( + isDropToSelfScheduler + && (!isDropToTheSameCell || isDragAndDropBetweenComponents || isDropBetweenAllDay) + ) { + this.checkRecurringAppointment( + rawAppointment, + targetedRawAppointment, + info.sourceAppointment.startDate, + () => { + // eslint-disable-next-line no-void + void this.updateAppointmentCore(rawAppointment, targetedRawAppointment, onCancel, event); + }, + // @ts-expect-error isDeleted: undefined is treated as false + undefined, + undefined, + event, + ); } else { onCancel(); } }, - onDeleteButtonPress(options) { + onDeleteButtonPress(this: Scheduler, options) { const targetedData = this.getTargetedAppointment(options.data, $(options.target)); this.checkAndDeleteAppointment(options.data, targetedData); this.hideAppointmentTooltip(); }, - focusFallbackAfterDelete() { + focusFallbackAfterDelete(this: Scheduler) { this.focusFallbackAfterDelete(); }, createFormattedDateText( + this: Scheduler, appointment: AppointmentTooltipItem['appointment'], targetedAppointmentRaw: TargetedAppointment, format?: string, @@ -134,15 +155,16 @@ const subscribes = { // 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 ?? getDateFormatType(startDate, endDate, adapter.allDay, this.currentView.type); + const formatType = format + ?? getDateFormatType(startDate, endDate, adapter.allDay, this.currentView.type); return { text: adapter.text || messageLocalization.format('dxScheduler-noSubject'), - formatDate: getDateText(startDate, endDate, formatType as any), + formatDate: getDateText(startDate, endDate, formatType as DateFormatType), }; }, - getResizableAppointmentArea(options) { + getResizableAppointmentArea(this: Scheduler, options) { const { allDay } = options; const groups = this.getViewOption('groups'); @@ -150,19 +172,27 @@ const subscribes = { if (allDay || this.currentView.type === 'month') { const horizontalGroupBounds = this._workSpace.getGroupBounds(options.coordinates); return { + // @ts-expect-error getGroupBounds returns bounds whenever the view is grouped left: horizontalGroupBounds.left, + // @ts-expect-error getGroupBounds returns bounds whenever the view is grouped right: horizontalGroupBounds.right, top: 0, bottom: 0, }; } - if (!allDay && VERTICAL_VIEW_TYPES.includes(this.currentView.type) && 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, right: 0, + // @ts-expect-error getGroupBounds returns bounds whenever the view is grouped top: verticalGroupBounds.top, + // @ts-expect-error getGroupBounds returns bounds whenever the view is grouped bottom: verticalGroupBounds.bottom, }; } @@ -171,44 +201,45 @@ const subscribes = { return undefined; }, - needRecalculateResizableArea() { + needRecalculateResizableArea(this: Scheduler) { return this.getWorkSpace().needRecalculateResizableArea(); }, - isAllDay(appointmentData): boolean { + isAllDay(this: Scheduler, appointmentData): boolean { return isAllDay(this, appointmentData); }, - getDeltaTime(e, initialSize, itemData) { + getDeltaTime(this: Scheduler, e, initialSize, itemData) { return getDeltaTime(e, initialSize, { viewType: this.currentView.type, cellSize: { width: this.getWorkSpace().getCellWidth(), height: this.getWorkSpace().getCellHeight(), }, + // @ts-expect-error SchedulerWorkSpaceLike.option() is untyped cellDurationInMinutes: this.getWorkSpace().option('cellDuration'), resizableStep: this.getWorkSpace().positionHelper.getResizableStep(), isAllDayPanel: isAllDay(this, itemData), }); }, - getCellWidth() { + getCellWidth(this: Scheduler) { return this.getWorkSpace().getCellWidth(); }, - getCellHeight() { + getCellHeight(this: Scheduler) { return this.getWorkSpace().getCellHeight(); }, - needCorrectAppointmentDates() { + needCorrectAppointmentDates(this: Scheduler) { return !['month', 'timelineMonth'].includes(this.currentView.type); }, - getRenderingStrategyDirection() { + getRenderingStrategyDirection(this: Scheduler) { return VERTICAL_VIEW_TYPES.includes(this.currentView.type) ? 'vertical' : 'horizontal'; }, - updateAppointmentEndDate(options) { + updateAppointmentEndDate(this: Scheduler, options: { endDate: Date; isSameDate?: boolean }) { const { endDate } = options; const endDayHour = this.getViewOption('endDayHour'); const startDayHour = this.getViewOption('startDayHour'); @@ -217,26 +248,30 @@ const subscribes = { if (endDate.getHours() >= endDayHour) { updatedEndDate.setHours(endDayHour, 0, 0, 0); - } else if (!options.isSameDate && startDayHour > 0 && (endDate.getHours() * 60 + endDate.getMinutes() < (startDayHour * 60))) { + } else if ( + !options.isSameDate + && startDayHour > 0 + && (endDate.getHours() * 60 + endDate.getMinutes() < (startDayHour * 60)) + ) { updatedEndDate = new Date(updatedEndDate.getTime() - toMs('day')); updatedEndDate.setHours(endDayHour, 0, 0, 0); } return updatedEndDate; }, - renderCompactAppointments(options: CompactAppointmentOptions): dxElementWrapper { + renderCompactAppointments(this: Scheduler, options: CompactAppointmentOptions): dxElementWrapper { return this._compactAppointmentsHelper.render(options); }, - clearCompactAppointments() { + clearCompactAppointments(this: Scheduler) { this._compactAppointmentsHelper.clear(); }, - getGroupCount() { + getGroupCount(this: Scheduler) { return this._workSpace.getGroupCount(); }, - mapAppointmentFields(config) { + mapAppointmentFields(this: Scheduler, config) { const { itemData, itemElement, targetedAppointment } = config; const targetedData = targetedAppointment || this.getTargetedAppointment(itemData, itemElement); @@ -247,55 +282,58 @@ const subscribes = { }; }, - dayHasAppointment(day, appointment, trimTime) { + dayHasAppointment(this: Scheduler, day, appointment, trimTime) { return this.dayHasAppointment(day, appointment, trimTime); }, - getLayoutManager() { + getLayoutManager(this: Scheduler) { return this._layoutManager; }, - getAgendaVerticalStepHeight() { + getAgendaVerticalStepHeight(this: Scheduler) { return this.getWorkSpace().getAgendaVerticalStepHeight(); }, - getAgendaDuration() { + getAgendaDuration(this: Scheduler) { + // @ts-expect-error agendaDuration is a view option, not a scheduler option return this.getViewOption('agendaDuration'); }, - getStartViewDate() { + getStartViewDate(this: Scheduler) { return this.getStartViewDate(); }, - getEndViewDate() { + getEndViewDate(this: Scheduler) { return this.getEndViewDate(); }, - forceMaxAppointmentPerCell() { + forceMaxAppointmentPerCell(this: Scheduler): unknown { + // @ts-expect-error Scheduler has no forceMaxAppointmentPerCell method + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.forceMaxAppointmentPerCell(); }, - getTargetedAppointmentData(appointment, element) { + getTargetedAppointmentData(this: Scheduler, appointment, element) { return this.getTargetedAppointment(appointment, element); }, - getEndDayHour() { + getEndDayHour(this: Scheduler) { return this._workSpace.option('endDayHour') || this.option('endDayHour'); }, - getStartDayHour() { + getStartDayHour(this: Scheduler) { return this._workSpace.option('startDayHour') || this.option('startDayHour'); }, - getViewOffsetMs() { + getViewOffsetMs(this: Scheduler) { return this.getViewOffsetMs(); }, - isAdaptive() { + isAdaptive(this: Scheduler) { return this.option('adaptivityEnabled'); }, - removeDroppableCellClass() { + removeDroppableCellClass(this: Scheduler) { this._workSpace.removeDroppableCellClass(); }, } as const; diff --git a/packages/devextreme/js/__internal/scheduler/scheduler.ts b/packages/devextreme/js/__internal/scheduler/scheduler.ts index 1abd9e9e0d35..d5a5250fd8a1 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler.ts @@ -2156,8 +2156,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { throw errors.Error('E1031', subject); } - // subscribes callbacks are not fully typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return (callback as ( this: Scheduler, ...callbackArgs: Parameters diff --git a/packages/devextreme/js/__internal/scheduler/view_model/m_appointment_data_source.ts b/packages/devextreme/js/__internal/scheduler/view_model/m_appointment_data_source.ts index 456fcfb4fb36..b45b23e1452c 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/m_appointment_data_source.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/m_appointment_data_source.ts @@ -1,3 +1,4 @@ +/* eslint-disable devextreme-custom/no-deferred */ import type { DataSource } from '@js/common/data'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; @@ -105,8 +106,8 @@ export class AppointmentDataSource { this.updatedAppointmentKeys = []; } + /* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-return */ add(rawAppointment: SafeAppointment): DeferredObj { - // @eslint-disable-next-line return this.dataSource!.store().insert(rawAppointment) // @ts-expect-error .done(() => this.dataSource.load()); @@ -134,6 +135,7 @@ export class AppointmentDataSource { // @ts-expect-error .done(() => this.dataSource.load()); } + /* eslint-enable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-return */ destroy(): void { const store = this.dataSource?.store(); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts index 859ef74a9daa..061b1cd4bea4 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts @@ -557,7 +557,6 @@ class SchedulerWorkSpace extends Widget { return undefined; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return notifyScheduler.invoke(funcName, ...args); } From 9ae082a6a7796fe0b16fcb254eb99c14ca332528 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 5 Oct 2026 12:50:51 +0400 Subject: [PATCH 03/10] Scheduler: type the drag behavior and the compact appointments helper ahead of the m_ rename --- .../appointments/m_appointment_collection.ts | 2 +- .../scheduler/m_appointment_drag_behavior.ts | 145 ++++++++++++++---- .../m_compact_appointments_helper.ts | 78 +++++++--- .../js/__internal/scheduler/scheduler.ts | 5 +- .../scheduler/workspaces/work_space.ts | 25 +-- .../integration.appointmentCollector.tests.js | 27 ++-- 6 files changed, 205 insertions(+), 77 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 9b417ebedba8..5a2cc12ea5cf 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -162,7 +162,7 @@ class SchedulerAppointments extends CollectionWidget { return this.option('getResourceManager')(); } - notifyObserver(subject, args) { + notifyObserver(subject, args?) { const notifyScheduler: any = this.option('notifyScheduler'); if (notifyScheduler) { notifyScheduler.invoke(subject, args); 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..7eda9b8622c0 100644 --- a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts +++ b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts @@ -1,36 +1,93 @@ +/* eslint-disable devextreme-custom/no-deferred */ +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import Draggable from '@js/ui/draggable'; +import type { + AppointmentDraggingAddEvent, + AppointmentDraggingEndEvent, + AppointmentDraggingMoveEvent, + AppointmentDraggingRemoveEvent, + AppointmentDraggingStartEvent, + Properties as SchedulerProperties, +} from '@js/ui/scheduler'; import { APPOINTMENT_SETTINGS_KEY, LIST_ITEM_DATA_KEY } from './constants'; +import type Scheduler from './scheduler'; +import type { SafeAppointment } from './types'; import { isSchedulerComponent } from './utils/is_scheduler_component'; import type { AppointmentViewModelPlain } from './view_model/types'; +import type { DragBehaviorConfig } from './workspaces/work_space'; const APPOINTMENT_ITEM_CLASS = 'dx-scheduler-appointment'; +type AppointmentDragging = NonNullable; + +interface DragPosition { + left: number; + top: number; +} + +interface DragStartData { + itemData: SafeAppointment; + itemSettings: AppointmentViewModelPlain; + initialPosition: DragPosition; +} + +interface AppointmentInfo { + appointment: SafeAppointment; + settings: AppointmentViewModelPlain; +} + +interface ListItemData { + appointment?: SafeAppointment; + settings?: AppointmentViewModelPlain; +} + +type DragStartArgs = AppointmentDraggingStartEvent & { + itemSettings?: AppointmentViewModelPlain; +}; + +type DragEndArgs = Omit & { + itemData?: SafeAppointment; + toItemData?: SafeAppointment; +}; + +type DropArgs = Omit & { + itemData?: SafeAppointment; +}; + +interface DragBehaviorOptions { + onDragStart: (e: AppointmentDraggingStartEvent) => void; + onDragMove: (e: AppointmentDraggingMoveEvent) => void; + onDragEnd: (e: AppointmentDraggingEndEvent) => void; + onDragCancel: (e: AppointmentDraggingRemoveEvent) => void; +} + export default class AppointmentDragBehavior { workspace = this.scheduler._workSpace; appointments = this.scheduler._appointments; - initialPosition = { + initialPosition: DragPosition = { left: 0, top: 0, }; - appointmentInfo: any = null; + appointmentInfo: AppointmentInfo | null = null; - dragBetweenComponentsPromise: any = null; + dragBetweenComponentsPromise: DeferredObj | null = null; - constructor(public scheduler) { + constructor(public scheduler: Scheduler) { } - isAllDay(appointment) { - return appointment.data(APPOINTMENT_SETTINGS_KEY).allDay; + isAllDay(appointment: dxElementWrapper): boolean { + return appointment.data(APPOINTMENT_SETTINGS_KEY).allDay; } - onDragStart(e) { + onDragStart(e: DragStartData): void { const { itemSettings, itemData, initialPosition } = e; this.initialPosition = initialPosition; @@ -42,19 +99,20 @@ export default class AppointmentDragBehavior { this.appointments.notifyObserver('hideAppointmentTooltip'); } - onDragMove(e) { + onDragMove(e: AppointmentDraggingMoveEvent): void { if (e.fromComponent !== e.toComponent) { this.appointments.notifyObserver('removeDroppableCellClass'); } } - getAppointmentElement(e) { + getAppointmentElement(e: AppointmentDraggingEndEvent): dxElementWrapper { + // @ts-expect-error the event of a drag end always carries the original event const itemElement = e.event.data?.itemElement || e.itemElement; return $(itemElement); } - onDragEnd(event) { + onDragEnd(event: AppointmentDraggingEndEvent): void { const element = this.getAppointmentElement(event); const isAllDay = this.isAllDay(element); @@ -74,26 +132,32 @@ export default class AppointmentDragBehavior { }); } - onDragCancel() { + onDragCancel(): void { this.removeDroppableClasses(); } - getItemData(appointmentElement) { - const dataFromTooltip: any = $(appointmentElement).data(LIST_ITEM_DATA_KEY); + getItemData(appointmentElement: Element | dxElementWrapper): SafeAppointment { + const dataFromTooltip = $(appointmentElement) + .data(LIST_ITEM_DATA_KEY); const itemDataFromTooltip = dataFromTooltip?.appointment; - const itemDataFromGrid = this.appointments._getItemData(appointmentElement); + const itemDataFromGrid: SafeAppointment = this.appointments._getItemData(appointmentElement); return itemDataFromTooltip || itemDataFromGrid; } - getItemSettings(appointment): AppointmentViewModelPlain | undefined { - const itemData: any = $(appointment).data(LIST_ITEM_DATA_KEY); + getItemSettings(appointment: Element | dxElementWrapper): AppointmentViewModelPlain | undefined { + const itemData = $(appointment).data(LIST_ITEM_DATA_KEY); return itemData?.settings; } - createDragStartHandler(options, appointmentDragging) { + createDragStartHandler( + options: DragBehaviorOptions, + appointmentDragging: AppointmentDragging, + ): (e: DragStartArgs) => void { return (e) => { + // @ts-expect-error the event of a drag start always carries the item element e.itemData = this.getItemData(e.itemElement); + // @ts-expect-error the event of a drag start always carries the item element e.itemSettings = this.getItemSettings(e.itemElement); if (this.scheduler._isAppointmentBeingUpdated(e.itemData)) { @@ -109,7 +173,10 @@ export default class AppointmentDragBehavior { }; } - createDragMoveHandler(options, appointmentDragging) { + createDragMoveHandler( + options: DragBehaviorOptions, + appointmentDragging: AppointmentDragging, + ): (e: AppointmentDraggingMoveEvent) => void { return (e) => { if (!this.appointmentInfo) { e.cancel = true; @@ -129,7 +196,10 @@ export default class AppointmentDragBehavior { }; } - createDragEndHandler(options, appointmentDragging) { + createDragEndHandler( + options: DragBehaviorOptions, + appointmentDragging: AppointmentDragging, + ): (e: DragEndArgs) => void { return (e) => { if (!this.appointmentInfo) { e.cancel = true; @@ -156,14 +226,15 @@ export default class AppointmentDragBehavior { } if (e.cancel !== true && isSchedulerComponent(e.toComponent)) { + // @ts-expect-error toComponent is a Scheduler here, see isSchedulerComponent const targetDragBehavior = e.toComponent._getDragBehavior(); - // @ts-expect-error + // @ts-expect-error Deferred is declared as a function targetDragBehavior.dragBetweenComponentsPromise = new Deferred(); } }; } - createDropHandler(appointmentDragging) { + createDropHandler(appointmentDragging: AppointmentDragging): (e: DropArgs) => void { return (e) => { const updatedData = this.appointments.invoke('getUpdatedData', e.itemData); e.itemData = extend({}, e.itemData, updatedData); @@ -178,9 +249,9 @@ export default class AppointmentDragBehavior { }; } - addTo(container, config) { + addTo(container: dxElementWrapper, config: DragBehaviorConfig): void { const appointmentDragging = this.scheduler.option('appointmentDragging') || {}; - const options = extend({ + const options: DragBehaviorOptions = extend({ component: this.scheduler, contentTemplate: null, filter: `.${APPOINTMENT_ITEM_CLASS}`, @@ -191,26 +262,36 @@ export default class AppointmentDragBehavior { onDragCancel: this.onDragCancel.bind(this), }, config); - this.appointments._createComponent(container, Draggable, extend({}, options, appointmentDragging, { - onDragStart: this.createDragStartHandler(options, appointmentDragging), - onDragMove: this.createDragMoveHandler(options, appointmentDragging), - onDragEnd: this.createDragEndHandler(options, appointmentDragging), - onDrop: this.createDropHandler(appointmentDragging), - onCancelByEsc: true, - })); + this.appointments._createComponent(container, Draggable, extend( + {}, + options, + appointmentDragging, + { + onDragStart: this.createDragStartHandler(options, appointmentDragging), + onDragMove: this.createDragMoveHandler(options, appointmentDragging), + onDragEnd: this.createDragEndHandler(options, appointmentDragging), + onDrop: this.createDropHandler(appointmentDragging), + onCancelByEsc: true, + }, + )); } - updateDragSource(appointment, settings) { + updateDragSource( + appointment: unknown, + settings: AppointmentViewModelPlain | undefined, + ): void { const { appointmentInfo } = this; if (appointmentInfo || appointment) { + // @ts-expect-error appointmentInfo is set when appointment is not passed const currentAppointment = appointment || appointmentInfo.appointment; + // @ts-expect-error appointmentInfo is set when appointment is not passed const currentSettings = settings || appointmentInfo.settings; this.appointments._setDragSourceAppointment(currentAppointment, currentSettings); } } - removeDroppableClasses() { + removeDroppableClasses(): void { this.appointments._removeDragSourceClassFromDraggedAppointment(); this.workspace.removeDroppableCellClass(); } 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 e1b01fce5548..bda7a8da2543 100644 --- a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts +++ b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts @@ -1,21 +1,28 @@ import { locate, move } from '@js/common/core/animation/translator'; import messageLocalization from '@js/common/core/localization/message'; import $, { type dxElementWrapper } from '@js/core/renderer'; +import type { FunctionTemplate as PublicFunctionTemplate } from '@js/core/templates/function_template'; +import type { ClickEvent as ButtonClickEvent } from '@js/ui/button'; import Button from '@js/ui/button'; +import type { ContentReadyEvent, ItemClickEvent } from '@js/ui/list'; import type { Appointment } from '@js/ui/scheduler'; import { FunctionTemplate } from '@ts/core/templates/function_template'; import { APPOINTMENT_SETTINGS_KEY, LIST_ITEM_CLASS, LIST_ITEM_DATA_KEY } from './constants'; import type Scheduler from './scheduler'; -import type { AppointmentTooltipItem, CompactAppointmentOptions, TargetedAppointment } from './types'; +import type { AppointmentTooltipExtraOptions } from './tooltip_strategies/tooltip_strategy_base'; +import type { + AppointmentTooltipItem, CompactAppointmentOptions, SafeAppointment, TargetedAppointment, +} from './types'; import { formatImplicitSchedulerDate } from './utils/global_formats'; +import type { AppointmentViewModelPlain } from './view_model/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`; export class CompactAppointmentsHelper { - elements: any[] = []; + elements: dxElementWrapper[] = []; instance: Scheduler; @@ -36,7 +43,7 @@ export class CompactAppointmentsHelper { return $button; } - clear() { + clear(): void { this.elements.forEach((button) => { button.detach(); button.remove(); @@ -44,7 +51,7 @@ export class CompactAppointmentsHelper { this.elements = []; } - private onButtonClick(e, options: CompactAppointmentOptions) { + private onButtonClick(e: ButtonClickEvent, options: CompactAppointmentOptions): void { const $button = $(e.element); this.instance.showAppointmentTooltipCore( $button, @@ -53,7 +60,10 @@ export class CompactAppointmentsHelper { ); } - private getExtraOptionsForTooltip(options: CompactAppointmentOptions, $appointmentCollector) { + private getExtraOptionsForTooltip( + options: CompactAppointmentOptions, + $appointmentCollector: dxElementWrapper, + ): AppointmentTooltipExtraOptions { return { clickEvent: this.clickEvent(options.onAppointmentClick).bind(this), dragBehavior: options.allowDrag @@ -64,21 +74,34 @@ export class CompactAppointmentsHelper { }; } - private clickEvent(onAppointmentClick) { + private clickEvent( + onAppointmentClick: CompactAppointmentOptions['onAppointmentClick'], + ): (e: ItemClickEvent) => void { return (e) => { + // @ts-expect-error _createEventArgs is typed for the context menu event, same shape const clickEventArgs = this.instance._createEventArgs(e); + // @ts-expect-error the collection passes its onItemClick action, always a function onAppointmentClick(clickEventArgs); }; } - private createTooltipDragBehavior($appointmentCollector) { + private createTooltipDragBehavior( + $appointmentCollector: dxElementWrapper, + ): (e: ContentReadyEvent) => void { return (e) => { const $element = $(e.element); const $schedulerElement = $(this.instance.element()); const workSpace = this.instance.getWorkSpace(); - const getItemData = (itemElement) => ($(itemElement) as any).data(LIST_ITEM_DATA_KEY)?.appointment; - const getItemSettings = (_, event) => event.itemSettings; + const getItemData = ( + itemElement: Element | dxElementWrapper, + ): SafeAppointment | undefined => ($(itemElement) + .data<{ appointment?: SafeAppointment } | undefined>(LIST_ITEM_DATA_KEY)) + ?.appointment; + const getItemSettings = ( + _: dxElementWrapper, + event: Record, + ): AppointmentViewModelPlain => event.itemSettings as AppointmentViewModelPlain; const initialPosition = locate($appointmentCollector); const options = { @@ -93,14 +116,17 @@ export class CompactAppointmentsHelper { }; } - private setPosition(element, position) { + private setPosition(element: dxElementWrapper, position: { top: number; left: number }): void { move(element, { top: position.top, left: position.left, }); } - private createCompactButton(template, options: CompactAppointmentOptions) { + private createCompactButton( + template: PublicFunctionTemplate, + options: CompactAppointmentOptions, + ): Button { const $button = this.createCompactButtonElement(options); return this.instance._createComponent($button, Button, { @@ -112,10 +138,14 @@ export class CompactAppointmentsHelper { }); } - static measureCollectorDimensions($container, isCompact) { + static measureCollectorDimensions( + $container: dxElementWrapper | null, + isCompact: boolean, + ): Record<'width' | 'height' | 'marginLeft' | 'marginRight' | 'marginTop' | 'marginBottom', string> { const $collector = $('
') .addClass(APPOINTMENT_COLLECTOR_CLASS) .toggleClass(COMPACT_APPOINTMENT_COLLECTOR_CLASS, isCompact) + // @ts-expect-error the all-day container is null only when the all-day panel is not rendered .appendTo($container); const styles = getComputedStyle($collector.get(0)); const geometry = { @@ -133,7 +163,7 @@ export class CompactAppointmentsHelper { private createCompactButtonElement({ isCompact, $container, coordinates, sortedIndex, items, - }: CompactAppointmentOptions) { + }: CompactAppointmentOptions): dxElementWrapper { const appointmentDate = this.getDateText( items[0].appointment, items[0].targetedAppointment, @@ -151,7 +181,11 @@ export class CompactAppointmentsHelper { return result; } - private renderTemplate(template, items: AppointmentTooltipItem[], isCompact) { + private renderTemplate( + template: PublicFunctionTemplate, + items: AppointmentTooltipItem[], + isCompact: boolean, + ): FunctionTemplate { return new FunctionTemplate((options) => template.render({ model: { appointmentCount: items.length, @@ -162,18 +196,24 @@ export class CompactAppointmentsHelper { })); } - private createTemplate(count, isCompact) { + private createTemplate(count: number, isCompact: boolean): PublicFunctionTemplate { this.initButtonTemplate(count, isCompact); return this.instance.getAppointmentTemplate('appointmentCollectorTemplate'); } - private initButtonTemplate(count, isCompact) { + private initButtonTemplate(count: number, isCompact: boolean): void { this.instance._templateManager.addDefaultTemplates({ - appointmentCollector: new FunctionTemplate((options) => this.createButtonTemplate(count, $(options.container), isCompact)), + appointmentCollector: new FunctionTemplate( + (options) => this.createButtonTemplate(count, $(options.container), isCompact), + ), }); } - private createButtonTemplate(appointmentCount, element, isCompact) { + private createButtonTemplate( + appointmentCount: number, + element: dxElementWrapper, + isCompact: boolean, + ): dxElementWrapper { const text = isCompact ? appointmentCount : messageLocalization.getFormatter('dxScheduler-moreAppointments')(appointmentCount); @@ -183,7 +223,7 @@ export class CompactAppointmentsHelper { .addClass(APPOINTMENT_COLLECTOR_CONTENT_CLASS); } - private localizeDate(date) { + private localizeDate(date: Date): string { return formatImplicitSchedulerDate(date); } diff --git a/packages/devextreme/js/__internal/scheduler/scheduler.ts b/packages/devextreme/js/__internal/scheduler/scheduler.ts index d5a5250fd8a1..9b8b2dd286d4 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler.ts @@ -8,7 +8,7 @@ import registerComponent from '@js/core/component_registrator'; import config from '@js/core/config'; import { getPublicElement } from '@js/core/element'; import type { PostponedOperations } from '@js/core/postponed_operations'; -import type { dxElementWrapper } from '@js/core/renderer'; +import type { Coordinates, dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { BindableTemplate } from '@js/core/templates/bindable_template'; import { EmptyTemplate } from '@js/core/templates/empty_template'; @@ -262,7 +262,7 @@ interface SchedulerWorkSpaceLike { needRecalculateResizableArea: () => boolean; getHeaderDate: () => Date; updateHeaderEmptyCellWidth: () => void; - initDragBehavior: (scheduler: unknown) => void; + initDragBehavior: (scheduler: Scheduler) => void; attachTablesEvents: () => void; getWorkArea: () => dxElementWrapper; $element: () => dxElementWrapper; @@ -274,6 +274,7 @@ interface SchedulerWorkSpaceLike { getCellData: ($cell: dxElementWrapper) => DroppableCellData; getCellWidth: () => number; getCellHeight: () => number; + getCellByCoordinates: (coordinates: Coordinates, allDay: boolean) => dxElementWrapper; getGroupCount: () => number; getGroupBounds: (coordinates: WorkspaceCoordinates) => GroupBoundsOffset | undefined; getPanelDOMSize: (panelName: PanelName) => RealSize; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts index 061b1cd4bea4..da450f738cf1 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts @@ -86,6 +86,7 @@ import { Cache } from '../global_cache'; import AppointmentDragBehavior from '../m_appointment_drag_behavior'; import { CompactAppointmentsHelper } from '../m_compact_appointments_helper'; import type { SubscribeKey, SubscribeMethods } from '../m_subscribes'; +import type Scheduler from '../scheduler'; import type HorizontalCurrentTimeShader from '../shaders/current_time_shader_horizontal'; import VerticalShader from '../shaders/current_time_shader_vertical'; import tableCreatorModule, { type GroupRows } from '../table_creator'; @@ -2555,7 +2556,7 @@ class SchedulerWorkSpace extends Widget { // DnD should be removed from work-space // ------------ - initDragBehavior(scheduler: { element: () => Element }): void { + initDragBehavior(scheduler: Scheduler): void { if (!this.dragBehavior && scheduler) { this.dragBehavior = new AppointmentDragBehavior(scheduler); @@ -3448,6 +3449,17 @@ interface DragBehaviorOptions { filter?: string; } +export interface DragBehaviorConfig { + container: dxElementWrapper; + dragTemplate: () => dxElementWrapper | undefined; + onDragStart: (e: AppointmentDraggingStartEvent) => void; + onDragMove: () => void; + onDragEnd: (e: AppointmentDraggingEndEvent) => void; + onDragCancel: (e: AppointmentDraggingRemoveEvent) => void; + cursorOffset: (() => TranslateVector) | undefined; + filter: string | undefined; +} + const createDragBehaviorConfig = ( container: dxElementWrapper, rootElement: dxElementWrapper, @@ -3460,16 +3472,7 @@ const createDragBehaviorConfig = ( removeDroppableCellClass: () => void, getCellWidthCallback: () => number, options: DragBehaviorOptions, -): { - container: dxElementWrapper; - dragTemplate: () => dxElementWrapper | undefined; - onDragStart: (e: AppointmentDraggingStartEvent) => void; - onDragMove: () => void; - onDragEnd: (e: AppointmentDraggingEndEvent) => void; - onDragCancel: (e: AppointmentDraggingRemoveEvent) => void; - cursorOffset: (() => TranslateVector) | undefined; - filter: string | undefined; -} => { +): DragBehaviorConfig => { const state: { dragElement: dxElementWrapper | undefined; itemData: unknown; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentCollector.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentCollector.tests.js index 8cb9e8f6d602..82cce2621720 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentCollector.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentCollector.tests.js @@ -78,19 +78,22 @@ module('Integration: collector', baseConfig, () => { }); module('Integration: Appointments Collector Base Tests', baseConfig, () => { + class WidgetMock extends Widget { + option(options) { + if(options === 'appointmentCollectorTemplate') { + return 'appointmentCollector'; + } + return super.option(options); + } + + getAppointmentTemplate(template) { + return this._getTemplateByOption(template); + } + } + WidgetMock.prototype._dataAccessors = mockDataAccessor; + const createWidget = () => { - return new (Widget.inherit({ - option(options) { - if(options === 'appointmentCollectorTemplate') { - return 'appointmentCollector'; - } - return this.callBase(options); - }, - getAppointmentTemplate(template) { - return this._getTemplateByOption(template); - }, - _dataAccessors: mockDataAccessor, - }))($('
')); + return new WidgetMock($('
')); }; const renderAppointmentsCollectorContainer = ({ widgetMock, items, options, color }) => { From 7e461e592c4ed8d7dc6e5c35bf23dbbc04471a93 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 5 Oct 2026 13:12:51 +0400 Subject: [PATCH 04/10] Scheduler: let the drag behavior getItemData return undefined when no item data is found --- .../js/__internal/scheduler/m_appointment_drag_behavior.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7eda9b8622c0..408d5babb611 100644 --- a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts +++ b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts @@ -136,7 +136,7 @@ export default class AppointmentDragBehavior { this.removeDroppableClasses(); } - getItemData(appointmentElement: Element | dxElementWrapper): SafeAppointment { + getItemData(appointmentElement: Element | dxElementWrapper): SafeAppointment | undefined { const dataFromTooltip = $(appointmentElement) .data(LIST_ITEM_DATA_KEY); const itemDataFromTooltip = dataFromTooltip?.appointment; From 8ebe36f6d169b970fbf5546b2d380f130d5e86c3 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 5 Oct 2026 13:34:15 +0400 Subject: [PATCH 05/10] Scheduler: type the appointment collection ahead of the m_ rename --- .../appointments/appointment/m_types.ts | 2 +- .../appointments/m_appointment_collection.ts | 321 ++++++++++++------ .../appointments/m_appointments_kbn.ts | 1 + .../js/__internal/scheduler/m_subscribes.ts | 6 +- .../js/__internal/scheduler/scheduler.ts | 7 +- .../__internal/scheduler/utils_time_zone.ts | 16 +- .../__internal/scheduler/view_model/types.ts | 2 +- 7 files changed, 240 insertions(+), 115 deletions(-) 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 472b0085703e..9472b6a98c20 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts @@ -34,7 +34,7 @@ export interface AppointmentProperties extends DOMComponentProperties; + resizableConfig: object; groups: string[]; partIndex?: number; partTotalCount: number; 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 5a2cc12ea5cf..14ebe791025f 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -10,21 +10,29 @@ import $ from '@js/core/renderer'; // @ts-expect-error import { grep, normalizeKey } from '@js/core/utils/common'; import dateUtils from '@js/core/utils/date'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { isElementInDom } from '@js/core/utils/dom'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; import { getBoundingRect } from '@js/core/utils/position'; import { setOuterHeight, setOuterWidth } from '@js/core/utils/size'; import { isDeferred, isPlainObject } from '@js/core/utils/type'; +import type { DxEvent } from '@js/events/events.types'; import type { AppointmentClickEvent, AppointmentContextMenuEvent, AppointmentDblClickEvent, AppointmentRenderedEvent, } from '@js/ui/scheduler'; +import type { TemplateBase } from '@ts/core/templates/template_base'; import { addOffsets } from '@ts/core/utils/date'; +import type { OptionChanged } from '@ts/core/widget/types'; import type { SupportedKeys } from '@ts/core/widget/widget'; +import type { ItemRenderInfo, PostprocessRenderItemInfo } from '@ts/ui/collection/collection_widget.base'; +import type { CollectionWidgetEditProperties } from '@ts/ui/collection/collection_widget.edit'; import CollectionWidget from '@ts/ui/collection/collection_widget.edit'; +import type Resizable from '@ts/ui/resizable/resizable'; +import type { ResizeActionArgs } from '@ts/ui/resizable/types'; import type NotifyScheduler from '../base/widget_notify_scheduler'; import { @@ -34,10 +42,17 @@ import { APPOINTMENT_ITEM_CLASS, } from '../classes'; import { APPOINTMENT_SETTINGS_KEY } from '../constants'; +import type { SubscribeKey, SubscribeMethods } from '../m_subscribes'; import type { TimeZoneCalculator } from '../r1/timezone_calculator/index'; import type { DesktopTooltipStrategy } from '../tooltip_strategies/desktop_tooltip_strategy'; import type { MobileTooltipStrategy } from '../tooltip_strategies/mobile_tooltip_strategy'; -import type { CompactAppointmentOptions, DOMMetaData, ScrollToGroupValuesOrOptions } from '../types'; +import type { + CompactAppointmentOptions, + DOMMetaData, + SafeAppointment, + ScrollToGroupValuesOrOptions, + TargetedAppointment, +} from '../types'; import { AppointmentAdapter } from '../utils/appointment_adapter/appointment_adapter'; import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; import { @@ -59,10 +74,12 @@ import type { import type ViewDataProvider from '../workspaces/view_model/view_data_provider'; import { AgendaAppointment } from './appointment/agenda_appointment'; import { Appointment } from './appointment/m_appointment'; +import type { AppointmentProperties } from './appointment/m_types'; import { createAgendaAppointmentLayout, createAppointmentLayout } from './m_appointment_layout'; import { AppointmentsKeyboardNavigation } from './m_appointments_kbn'; import { DateFormatType } from './m_text_utils'; import { getAppointmentDateRange } from './resizing/m_core'; +import type { DateRange } from './resizing/types'; import { isNeedToAdd } from './utils/get_arrays_diff'; import { getViewModelDiff } from './utils/get_view_model_diff'; @@ -81,7 +98,7 @@ export interface AppointmentCollectionOptions { onItemRendered: (args: AppointmentRenderedEvent) => void; onItemClick: (args: AppointmentClickEvent) => void; onItemContextMenu: (args: AppointmentContextMenuEvent) => void; - onAppointmentDblClick: (args: AppointmentDblClickEvent) => void; + onAppointmentDblClick: ((args: AppointmentDblClickEvent) => void) | null; tabIndex: number; focusStateEnabled: boolean; allowDrag: boolean; @@ -100,12 +117,40 @@ export interface AppointmentCollectionOptions { onContentReady: () => void; } +type ItemActionOptionName = 'onItemRendered' | 'onItemClick' | 'onItemContextMenu'; + +export interface AppointmentCollectionProperties + extends Omit< + CollectionWidgetEditProperties, + Exclude | 'items' + >, + Omit { + items?: AppointmentViewModelPlain[]; + fixedContainer?: dxElementWrapper | null; + allDayContainer?: dxElementWrapper | null; + resources?: unknown[]; +} + const COMPONENT_CLASS = 'dx-scheduler-scrollable-appointments'; const DBLCLICK_EVENT_NAME = addNamespace(dblclickEvent, 'dxSchedulerAppointment'); const toMs = dateUtils.dateToMilliseconds; +type AppointmentResizeEvent = ResizeActionArgs & { + element: Element; + component: Resizable; +}; + +type ResizableArea = NonNullable> +| dxElementWrapper; + +interface AppointmentResizableConfig { + area: ResizableArea; + onResizeStart: (e: AppointmentResizeEvent) => void; + onResizeEnd: (e: AppointmentResizeEvent) => void; +} + interface ViewModelDiff { item: AppointmentViewModelPlain; element?: dxElementWrapper; @@ -114,19 +159,19 @@ interface ViewModelDiff { needToUpdateItems?: true; } -class SchedulerAppointments extends CollectionWidget { +class SchedulerAppointments extends CollectionWidget { // NOTE: The key of this array is `sortedIndex` of appointment rendered in Element $itemBySortedIndex!: dxElementWrapper[]; - _appointmentClickTimeout: any; + _appointmentClickTimeout?: ReturnType; _currentAppointmentSettings?: AppointmentViewModelPlain; - _preventSingleAppointmentClick: any; + _preventSingleAppointmentClick!: boolean; - _initialSize: any; + _initialSize?: { width: number; height: number }; - _initialCoordinates: any; + _initialCoordinates?: { left: number; top: number }; private _kbn!: AppointmentsKeyboardNavigation; @@ -146,38 +191,52 @@ class SchedulerAppointments extends CollectionWidget { return this.invoke('isVirtualScrolling'); } - get appointmentDataSource() { + get appointmentDataSource(): AppointmentDataSource { return this.option('getAppointmentDataSource')(); } get dataAccessors(): AppointmentDataAccessor { - return this.option('dataAccessors') as AppointmentDataAccessor; + return this.option('dataAccessors'); } get sortedItems(): SortedEntity[] { - return this.option('getSortedAppointments')() as SortedEntity[]; + return this.option('getSortedAppointments')(); } getResourceManager(): ResourceManager { return this.option('getResourceManager')(); } - notifyObserver(subject, args?) { - const notifyScheduler: any = this.option('notifyScheduler'); + notifyObserver( + subject: Subject, + args?: Parameters[0] | Record, + ): void { + const notifyScheduler = this.option('notifyScheduler'); if (notifyScheduler) { + // @ts-expect-error the subscribes that are notified take a single argument object notifyScheduler.invoke(subject, args); } } - invoke(funcName: string, ...args) { - const notifyScheduler: any = this.option('notifyScheduler'); + invoke( + funcName: Subject, + ...args: Parameters + ): ReturnType; + + invoke( + funcName: Subject, + ...args: Parameters + ): ReturnType | undefined { + const notifyScheduler = this.option('notifyScheduler'); if (notifyScheduler) { return notifyScheduler.invoke(funcName, ...args); } + + return undefined; } - _dispose() { + _dispose(): void { clearTimeout(this._appointmentClickTimeout); super._dispose(); @@ -198,13 +257,13 @@ class SchedulerAppointments extends CollectionWidget { return $($item).data(APPOINTMENT_SETTINGS_KEY); } - _moveFocus() {} + _moveFocus(): void {} - _focusTarget() { + _focusTarget(): dxElementWrapper { return this._kbn.getFocusableItems(); } - _renderFocusTarget() { + _renderFocusTarget(): void { if (this.$itemBySortedIndex?.length) { this._kbn.resetTabIndex(this._kbn.getFirstVisibleItem()); } @@ -231,21 +290,22 @@ class SchedulerAppointments extends CollectionWidget { } } - _focusInHandler(e) { + _focusInHandler(e: DxEvent): void { super._focusInHandler(e); this._kbn.focusInHandler(e); } - _focusOutHandler(e) { + _focusOutHandler(e: DxEvent): void { this._kbn.focusOutHandler(); super._focusOutHandler(e); } - _eventBindingTarget() { + _eventBindingTarget(): dxElementWrapper { return this._itemContainer(); } - _getDefaultOptions() { + _getDefaultOptions(): AppointmentCollectionProperties { + // @ts-expect-error noDataText is null to hide the no data message, the public type has no null return extend(super._getDefaultOptions(), { noDataText: null, activeStateEnabled: true, @@ -278,7 +338,7 @@ class SchedulerAppointments extends CollectionWidget { return diff; } - _optionChanged(args) { + _optionChanged(args: OptionChanged): void { switch (args.name) { case 'items': this._cleanFocusState(); @@ -438,8 +498,8 @@ class SchedulerAppointments extends CollectionWidget { $allDayFragment: dxElementWrapper, ) => void): void { if (this.isVirtualScrolling) { - const $commonFragment = $(domAdapter.createDocumentFragment() as any); - const $allDayFragment = $(domAdapter.createDocumentFragment() as any); + const $commonFragment = $(domAdapter.createDocumentFragment()); + const $allDayFragment = $(domAdapter.createDocumentFragment()); renderFunction($commonFragment, $allDayFragment); @@ -453,11 +513,11 @@ class SchedulerAppointments extends CollectionWidget { } } - _refreshActiveDescendant() { + _refreshActiveDescendant(): void { // override to do nothing } - _attachAppointmentsEvents() { + _attachAppointmentsEvents(): void { this._attachClickEvent(); this._attachHoldEvent(); this._attachContextMenuEvent(); @@ -468,15 +528,18 @@ class SchedulerAppointments extends CollectionWidget { this._attachHoverEvents(); } - _clearDropDownItemsElements() { + _clearDropDownItemsElements(): void { this.invoke('clearCompactAppointments'); } - _findItemElementByItem(item) { - const result: any = []; - const that: any = this; + // @ts-expect-error returns every element of the item, the base returns one element + _findItemElementByItem(item: unknown): dxElementWrapper[] { + const result: dxElementWrapper[] = []; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; - (this as any).itemElements().each(function () { + this.itemElements().each(function () { + // eslint-disable-next-line @typescript-eslint/no-invalid-this const $item = $(this); if ($item.data(that._itemDataKey()) === item) { result.push($item); @@ -486,11 +549,11 @@ class SchedulerAppointments extends CollectionWidget { return result; } - _itemClass() { + _itemClass(): string { return APPOINTMENT_ITEM_CLASS; } - _itemContainer() { + _itemContainer(): dxElementWrapper { const $container = super._itemContainer(); let $result = $container; const $allDayContainer = this.option('allDayContainer'); @@ -502,7 +565,7 @@ class SchedulerAppointments extends CollectionWidget { return $result; } - _cleanItemContainer() { + _cleanItemContainer(): void { super._cleanItemContainer(); const $allDayContainer = this.option('allDayContainer'); @@ -511,7 +574,7 @@ class SchedulerAppointments extends CollectionWidget { } } - _init() { + _init(): void { super._init(); this.$itemBySortedIndex = []; this._kbn = new AppointmentsKeyboardNavigation(this); @@ -521,7 +584,11 @@ class SchedulerAppointments extends CollectionWidget { } // TODO: used externally in scheduler.ts - _renderAppointmentTemplate($container, appointment, model) { + _renderAppointmentTemplate( + $container: dxElementWrapper, + appointment: SafeAppointment, + model: { targetedAppointmentData: TargetedAppointment }, + ): void { const config = { isAllDay: appointment.allDay, isRecurrence: appointment.recurrenceRule, @@ -560,30 +627,36 @@ class SchedulerAppointments extends CollectionWidget { } } - _executeItemRenderAction(index, itemData, itemElement) { - const action = (this as any)._getItemRenderAction(); + _executeItemRenderAction( + index: number, + itemData: SafeAppointment, + itemElement: HTMLElement, + ): void { + const action = this._getItemRenderAction(); if (action) { + // @ts-expect-error the scheduler maps the item info to the appointment event fields action(this.invoke('mapAppointmentFields', { itemData, itemElement })); } delete this._currentAppointmentSettings; } - _itemClickHandler(e) { + _itemClickHandler(e: DxEvent): void { super._itemClickHandler(e, {}, { - afterExecute: function (e) { - this._processItemClick(e.args[0].event); + afterExecute: function (actionInfo): void { + this._processItemClick(actionInfo.args[0].event); }.bind(this), }); } - _processItemClick(e) { + _processItemClick(e: DxEvent): void { const $target = $(e.currentTarget); - const data = (this as any)._getItemData($target); + const data = this._getItemData($target); if ($target.is('.dx-scheduler-appointment-collector')) { return; } + // @ts-expect-error a keydown event returns before isFakeClickEvent reads the mouse fields if (e.type === 'keydown' || isFakeClickEvent(e)) { this.notifyObserver('showEditAppointmentPopup', { data, target: $target }); return; @@ -598,35 +671,38 @@ class SchedulerAppointments extends CollectionWidget { }, 300); } - _extendActionArgs($itemElement) { + // @ts-expect-error the scheduler maps the item info to the appointment event fields + _extendActionArgs($itemElement: dxElementWrapper): ReturnType { const args = super._extendActionArgs($itemElement); return this.invoke('mapAppointmentFields', args); } - _render() { + _render(): void { super._render(); this._attachAppointmentDblClick(); } - _attachAppointmentDblClick() { - const that: any = this; + _attachAppointmentDblClick(): void { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; const itemSelector = that._itemSelector(); const itemContainer = this._itemContainer(); eventsEngine.off(itemContainer, DBLCLICK_EVENT_NAME, itemSelector); eventsEngine.on(itemContainer, DBLCLICK_EVENT_NAME, itemSelector, (e) => { that._itemDXEventHandler(e, 'onAppointmentDblClick', {}, { - afterExecute(e) { - that._dblClickHandler(e.args[0].event); + afterExecute(actionInfo): void { + // @ts-expect-error args holds the arguments of the dblclick action + that._dblClickHandler(actionInfo.args[0].event); }, }); }); } - _dblClickHandler(e) { + _dblClickHandler(e: DxEvent): void { const $targetAppointment = $(e.currentTarget); - const appointmentData = (this as any)._getItemData($targetAppointment); + const appointmentData = this._getItemData($targetAppointment); clearTimeout(this._appointmentClickTimeout); this._preventSingleAppointmentClick = true; @@ -654,13 +730,16 @@ class SchedulerAppointments extends CollectionWidget { return $item; } - _getItemContent($itemFrame) { + _getItemContent($itemFrame: dxElementWrapper): dxElementWrapper { $itemFrame.data(APPOINTMENT_SETTINGS_KEY, this._currentAppointmentSettings); const $itemContent = super._getItemContent($itemFrame); return $itemContent; } - _createItemByTemplate(itemTemplate, renderArgs) { + _createItemByTemplate( + itemTemplate: TemplateBase, + renderArgs: ItemRenderInfo, + ): dxElementWrapper { const { itemData, container, index } = renderArgs; const parent = $(container).parent(); @@ -682,12 +761,12 @@ class SchedulerAppointments extends CollectionWidget { _getAppointmentContainer(allDay: boolean): dxElementWrapper { const $allDayContainer = this.option('allDayContainer'); - const $container = (this as any).itemsContainer().not($allDayContainer); + const $container = this.itemsContainer().not($allDayContainer); return allDay && $allDayContainer ? $allDayContainer : $container; } - _postprocessRenderItem(args) { + _postprocessRenderItem(args: PostprocessRenderItemInfo): void { this.renderAppointment( args.itemElement, this._currentAppointmentSettings as AppointmentAgendaViewModel | AppointmentItemViewModel, @@ -718,7 +797,7 @@ class SchedulerAppointments extends CollectionWidget { } const { groups, groupsLeafs, resourceById } = this.getResourceManager(); - const config: any = { + const config: Partial = { data: settings.itemData, groupIndex: settings.groupIndex, groupTexts: getGroupTexts(groups, groupsLeafs, resourceById, settings.groupIndex), @@ -733,7 +812,7 @@ class SchedulerAppointments extends CollectionWidget { getResourceManager: this.option('getResourceManager'), }; - (this as any)._createComponent(element, AgendaAppointment, config); + this._createComponent(element, AgendaAppointment, config); } renderGeneralAppointment( @@ -746,7 +825,7 @@ class SchedulerAppointments extends CollectionWidget { const { allDay } = settings; const { groups, groupsLeafs, resourceById } = this.getResourceManager(); const isGroupByDate = this.option('groupByDate'); - const config: any = { + const config: Partial = { data: settings.itemData, groupIndex: settings.groupIndex, groupTexts: getGroupTexts(groups, groupsLeafs, resourceById, settings.groupIndex), @@ -773,12 +852,12 @@ class SchedulerAppointments extends CollectionWidget { getResourceManager: this.option('getResourceManager'), }; - (this as any)._createComponent(element, Appointment, config); + this._createComponent(element, Appointment, config); } - _applyResourceDataAttr($appointment) { + _applyResourceDataAttr($appointment: dxElementWrapper): void { const { resources } = this.getResourceManager(); - const rawAppointment = (this as any)._getItemData($appointment); + const rawAppointment = this._getItemData($appointment); const appointmentGroups = getAppointmentGroupValues(rawAppointment, resources); Object.entries(appointmentGroups).forEach(([resourceIndex, resourceIds]) => { @@ -790,10 +869,13 @@ class SchedulerAppointments extends CollectionWidget { }); } - _resizableConfig(appointmentData, itemSetting) { + _resizableConfig( + appointmentData: SafeAppointment, + itemSetting: AppointmentItemViewModel, + ): AppointmentResizableConfig { return { area: this._calculateResizableArea(itemSetting, appointmentData), - onResizeStart: (e) => { + onResizeStart: (e): void => { const $appointment = $(e.element); this._isResizing = true; @@ -806,22 +888,26 @@ class SchedulerAppointments extends CollectionWidget { ); e.component.option('area', updatedArea); + // @ts-expect-error the resize start action passes the original drag event e.component._renderDragOffsets(e.event); } this._initialSize = { width: e.width, height: e.height }; this._initialCoordinates = locate($appointment); }, - onResizeEnd: (e) => { + onResizeEnd: (e): void => { this._isResizing = false; this._resizeEndHandler(e); }, }; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _calculateResizableArea(itemSetting, appointmentData) { - const area = (this as any).$element().closest('.dx-scrollable-content'); + _calculateResizableArea( + itemSetting: { left?: number; groupIndex?: number; allDay?: boolean }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + appointmentData: unknown, + ): ResizableArea { + const area = this.$element().closest('.dx-scrollable-content'); return this.invoke('getResizableAppointmentArea', { coordinates: { @@ -833,18 +919,23 @@ class SchedulerAppointments extends CollectionWidget { }) || area; } - _resizeEndHandler(e) { + _resizeEndHandler(e: AppointmentResizeEvent): void { const $element = $(e.element); const { allDay, info } = $element.data(APPOINTMENT_SETTINGS_KEY); - const sourceAppointment = (this as any)._getItemData($element); + const sourceAppointment = this._getItemData($element); const viewOffset = this.invoke('getViewOffsetMs'); + // eslint-disable-next-line @typescript-eslint/init-declarations let dateRange: { startDate: Date; endDate: Date }; if (allDay) { dateRange = this.resizeAllDay(e); } else { - const startDate = this._getEndResizeAppointmentStartDate(e, sourceAppointment, info.appointment); + const startDate = this._getEndResizeAppointmentStartDate( + e, + sourceAppointment, + info.appointment, + ); const { endDate } = info.appointment; const shiftedStartDate = addOffsets(startDate, -viewOffset); const shiftedEndDate = addOffsets(endDate, -viewOffset); @@ -862,7 +953,7 @@ class SchedulerAppointments extends CollectionWidget { ); } - resizeAllDay(e) { + resizeAllDay(e: AppointmentResizeEvent): DateRange { const $element = $(e.element); const timeZoneCalculator = this.option('timeZoneCalculator'); @@ -872,6 +963,7 @@ class SchedulerAppointments extends CollectionWidget { isVerticalGroupedWorkSpace: this.option('isVerticalGroupedWorkSpace')(), appointmentRect: getBoundingRect($element[0]), parentAppointmentRect: getBoundingRect($element.parent()[0]), + // @ts-expect-error the work space exists while an appointment is resized viewDataProvider: this.option('getViewDataProvider')(), isDateAndTimeView: this.option('isDateAndTimeView')(), startDayHour: this.invoke('getStartDayHour'), @@ -879,18 +971,19 @@ class SchedulerAppointments extends CollectionWidget { timeZoneCalculator, dataAccessors: this.dataAccessors, rtlEnabled: this.option('rtlEnabled'), + // @ts-expect-error the work space exists while an appointment is resized DOMMetaData: this.option('getDOMElementsMetaData')(), viewOffset: this.invoke('getViewOffsetMs'), }); } updateResizedAppointment( - $element, + $element: dxElementWrapper, dateRange: { startDate: Date; endDate: Date }, dataAccessors: AppointmentDataAccessor, - timeZoneCalculator, - ) { - const sourceAppointment = (this as any)._getItemData($element); + timeZoneCalculator: TimeZoneCalculator, + ): void { + const sourceAppointment = this._getItemData($element); const gridAdapter = new AppointmentAdapter( sourceAppointment, dataAccessors, @@ -909,7 +1002,8 @@ class SchedulerAppointments extends CollectionWidget { .calculateDates(timeZoneCalculator, 'fromGrid') .calculateDates(timeZoneCalculator, 'toGrid'); - const startDateDelta = gridAdapter.startDate.getTime() - convertedBackAdapter.startDate.getTime(); + const startDateDelta = gridAdapter.startDate.getTime() + - convertedBackAdapter.startDate.getTime(); const endDateDelta = gridAdapter.endDate.getTime() - convertedBackAdapter.endDate.getTime(); gridAdapter.startDate = addOffsets(gridAdapter.startDate, startDateDelta); @@ -926,7 +1020,11 @@ class SchedulerAppointments extends CollectionWidget { }); } - _getEndResizeAppointmentStartDate(e, rawAppointment, appointmentInfo) { + _getEndResizeAppointmentStartDate( + e: AppointmentResizeEvent, + rawAppointment: SafeAppointment, + appointmentInfo: AppointmentItemViewModel['info']['appointment'], + ): Date { const timeZoneCalculator = this.option('timeZoneCalculator'); const appointmentAdapter = new AppointmentAdapter( rawAppointment, @@ -948,15 +1046,21 @@ class SchedulerAppointments extends CollectionWidget { return startDate; } - private getDateRange(e, startDate, endDate) { - const itemData = (this as any)._getItemData(e.element); + private getDateRange( + e: AppointmentResizeEvent, + startDate: Date, + endDate: Date, + ): { startDate: Date; endDate: Date } { + const itemData = this._getItemData(e.element); const deltaTime = this.invoke('getDeltaTime', e, this._initialSize, itemData); const renderingStrategyDirection = this.invoke('getRenderingStrategyDirection'); let isStartDateChanged = false; const isAllDay = this.invoke('isAllDay', itemData); const needCorrectDates = this.invoke('needCorrectAppointmentDates') && !isAllDay; - let startTime; - let endTime; + // eslint-disable-next-line @typescript-eslint/init-declarations + let startTime: number; + // eslint-disable-next-line @typescript-eslint/init-declarations + let endTime: number; if (renderingStrategyDirection !== 'vertical' || isAllDay) { isStartDateChanged = this.option('rtlEnabled') ? e.handles.right : e.handles.left; @@ -968,7 +1072,8 @@ class SchedulerAppointments extends CollectionWidget { startTime = needCorrectDates ? this._correctStartDateByDelta(startDate, deltaTime) : startDate.getTime() - deltaTime; - startTime += timeZoneUtils.getTimezoneOffsetChangeInMs(startDate, endDate, startTime, endDate); + startTime += timeZoneUtils + .getTimezoneOffsetChangeInMs(startDate, endDate, startTime, endDate); endTime = endDate.getTime(); } else { startTime = startDate.getTime(); @@ -984,7 +1089,7 @@ class SchedulerAppointments extends CollectionWidget { }; } - _correctEndDateByDelta(endDate, deltaTime) { + _correctEndDateByDelta(endDate: Date, deltaTime: number): number { const endDayHour = this.invoke('getEndDayHour'); const startDayHour = this.invoke('getStartDayHour'); @@ -1009,7 +1114,8 @@ class SchedulerAppointments extends CollectionWidget { if (result > maxDate.getTime() || result <= minDate.getTime()) { const tailOfCurrentDay = maxDate.getTime() - correctEndDate.getTime(); const tailOfPrevDays = deltaTime - tailOfCurrentDay; - const correctedEndDate = new Date(correctEndDate).setDate(correctEndDate.getDate() + daysCount); + const correctedEndDate = new Date(correctEndDate) + .setDate(correctEndDate.getDate() + daysCount); const lastDay = new Date(correctedEndDate); lastDay.setHours(startDayHour, 0, 0, 0); @@ -1018,7 +1124,7 @@ class SchedulerAppointments extends CollectionWidget { return result; } - _correctStartDateByDelta(startDate, deltaTime) { + _correctStartDateByDelta(startDate: Date, deltaTime: number): number { const endDayHour = this.invoke('getEndDayHour'); const startDayHour = this.invoke('getStartDayHour'); @@ -1082,6 +1188,7 @@ class SchedulerAppointments extends CollectionWidget { sortedIndex: appointment.sortedIndex, width: appointment.width, height: appointment.height, + // @ts-expect-error onItemClick holds the scheduler onAppointmentClick action onAppointmentClick: this.option('onItemClick'), allowDrag: this.option('allowDrag'), isCompact: appointment.isCompact, @@ -1118,7 +1225,9 @@ class SchedulerAppointments extends CollectionWidget { return result; } - moveAppointmentBack(dragEvent?) { + moveAppointmentBack( + dragEvent?: { cancel?: boolean | PromiseLike | DeferredObj } | null, + ): void { const $appointment = this._kbn.$focusTarget(); const size = this._initialSize; const coords = this._initialCoordinates; @@ -1129,6 +1238,7 @@ class SchedulerAppointments extends CollectionWidget { this._removeDragSourceClassFromDraggedAppointment(); if (isDeferred(dragEvent.cancel)) { + // @ts-expect-error isDeferred() is not declared as a type guard dragEvent.cancel.resolve(true); } else { dragEvent.cancel = true; @@ -1148,33 +1258,38 @@ class SchedulerAppointments extends CollectionWidget { } } - focus() { + focus(): void { this._kbn.focus(); } - _removeDragSourceClassFromDraggedAppointment() { - const $appointments = (this as any)._itemElements().filter(`.${APPOINTMENT_DRAG_SOURCE_CLASS}`); + _removeDragSourceClassFromDraggedAppointment(): void { + const $appointments = this._itemElements().filter(`.${APPOINTMENT_DRAG_SOURCE_CLASS}`); $appointments.each((_, element) => { - const appointmentInstance = ($(element) as any).dxSchedulerAppointment('instance'); + // @ts-expect-error dxSchedulerAppointment is a jQuery plugin method + const appointmentInstance = $(element).dxSchedulerAppointment('instance'); appointmentInstance.option('isDragSource', false); }); } - _setDragSourceAppointment(appointment, settings) { + _setDragSourceAppointment(appointment: unknown, settings: AppointmentViewModelPlain): void { const $appointments = this._findItemElementByItem(appointment); - const { startDate, endDate } = settings.info.sourceAppointment; + const { startDate, endDate } = (settings as AppointmentItemViewModel).info.sourceAppointment; const { groupIndex } = settings; $appointments.forEach(($item) => { - const { info: itemInfo, groupIndex: itemGroupIndex } = $item.data(APPOINTMENT_SETTINGS_KEY); + const { + info: itemInfo, + groupIndex: itemGroupIndex, + } = $item.data(APPOINTMENT_SETTINGS_KEY); const { startDate: itemStartDate, endDate: itemEndDate, } = itemInfo.sourceAppointment; + // @ts-expect-error dxSchedulerAppointment is a jQuery plugin method const appointmentInstance = $item.dxSchedulerAppointment('instance'); const isDragSource = startDate.getTime() === itemStartDate.getTime() && endDate.getTime() === itemEndDate.getTime() @@ -1184,31 +1299,33 @@ class SchedulerAppointments extends CollectionWidget { }); } - updateResizableArea() { - const $allResizableElements = (this as any).$element().find('.dx-scheduler-appointment.dx-resizable'); + updateResizableArea(): void { + const $allResizableElements = this.$element().find('.dx-scheduler-appointment.dx-resizable'); - const horizontalResizables = grep($allResizableElements, (el) => { - const $el: any = $(el); + const horizontalResizables = grep($allResizableElements, (el: Element): boolean => { + const $el = $(el); + // @ts-expect-error dxResizable is a jQuery plugin method const resizableInst = $el.dxResizable('instance'); const { area, handles } = resizableInst.option(); return (handles === 'right left' || handles === 'left right') && isPlainObject(area); }); - each(horizontalResizables, (_, el) => { - const $el: any = $(el); + each(horizontalResizables, (_, el: Element): void => { + const $el = $(el); const position = locate($el); - const appointmentData = (this as any)._getItemData($el); + const appointmentData = this._getItemData($el); const area = this._calculateResizableArea({ left: position.left, }, appointmentData); + // @ts-expect-error dxResizable is a jQuery plugin method $el.dxResizable('instance').option('area', area); }); } } -registerComponent('dxSchedulerAppointments', SchedulerAppointments as any); +registerComponent('dxSchedulerAppointments', SchedulerAppointments); export default SchedulerAppointments; diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts index bbecd16a1c1b..24a51cacf3e7 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts @@ -202,6 +202,7 @@ export class AppointmentsKeyboardNavigation { private scrollToByItemData(itemData: SortedEntity): void { const date = new Date(Math.max( + // @ts-expect-error the start view date is defined once the work space is rendered this._collection.invoke('getStartViewDate').getTime(), itemData.source.startDate, )); diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index eeec6ce084ff..8bc89b9d5173 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -317,11 +317,13 @@ const subscribes = { return this.getTargetedAppointment(appointment, element); }, - getEndDayHour(this: Scheduler) { + getEndDayHour(this: Scheduler): number { + // @ts-expect-error SchedulerWorkSpaceLike.option() is untyped return this._workSpace.option('endDayHour') || this.option('endDayHour'); }, - getStartDayHour(this: Scheduler) { + getStartDayHour(this: Scheduler): number { + // @ts-expect-error SchedulerWorkSpaceLike.option() is untyped return this._workSpace.option('startDayHour') || this.option('startDayHour'); }, diff --git a/packages/devextreme/js/__internal/scheduler/scheduler.ts b/packages/devextreme/js/__internal/scheduler/scheduler.ts index 9b8b2dd286d4..6f2aec183a91 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler.ts @@ -1286,7 +1286,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { ? MobileTooltipStrategy : DesktopTooltipStrategy)(this.getAppointmentTooltipOptions()); - this._appointments = this._createComponent('
', AppointmentCollection, this.appointmentsConfig()); + this._appointments = this._createComponent( + '
', + AppointmentCollection, + // @ts-expect-error item actions get the collection item event mapped by mapAppointmentFields + this.appointmentsConfig(), + ); this._appointments.option('itemTemplate', this.getAppointmentTemplate('appointmentTemplate')); this.createAppointmentPopupForm(); diff --git a/packages/devextreme/js/__internal/scheduler/utils_time_zone.ts b/packages/devextreme/js/__internal/scheduler/utils_time_zone.ts index ce3bf1ffb8c5..e7d0b53dd642 100644 --- a/packages/devextreme/js/__internal/scheduler/utils_time_zone.ts +++ b/packages/devextreme/js/__internal/scheduler/utils_time_zone.ts @@ -61,20 +61,20 @@ const getDaylightOffsetInMs = ( ): number => getDaylightOffset(startDate, endDate) * toMs('minute'); const getTimezoneOffsetChangeInMinutes = ( - startDate: Date, - endDate: Date, - updatedStartDate: Date, - updatedEndDate: Date, + startDate: Date | number, + endDate: Date | number, + updatedStartDate: Date | number, + updatedEndDate: Date | number, ): number => getDaylightOffset(updatedStartDate, updatedEndDate) - getDaylightOffset( startDate, endDate, ); const getTimezoneOffsetChangeInMs = ( - startDate: Date, - endDate: Date, - updatedStartDate: Date, - updatedEndDate: Date, + startDate: Date | number, + endDate: Date | number, + updatedStartDate: Date | number, + updatedEndDate: Date | number, ): number => { const minutes = getTimezoneOffsetChangeInMinutes( startDate, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/types.ts b/packages/devextreme/js/__internal/scheduler/view_model/types.ts index 591d2be66ccc..4924fcb86cf2 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/types.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/types.ts @@ -202,7 +202,7 @@ export interface AppointmentAgendaViewModel extends BaseAppointmentViewModel { } export interface AppointmentItemViewModel extends BaseAppointmentViewModel, AppointmentInfo { - direction: string; + direction: Orientation; skipResizing: boolean; level: number; maxLevel: number; From 74cd17763e9333a02efdfcdb6213dd08bf6ac745 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 5 Oct 2026 23:50:14 +0400 Subject: [PATCH 06/10] Scheduler: narrow the item action suppressions to the three collection options --- .../appointments/m_appointment_collection.ts | 24 +++++++++---------- .../js/__internal/scheduler/m_subscribes.ts | 1 - .../js/__internal/scheduler/scheduler.ts | 10 ++++---- 3 files changed, 15 insertions(+), 20 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 14ebe791025f..5b97f5b71949 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -18,12 +18,7 @@ import { getBoundingRect } from '@js/core/utils/position'; import { setOuterHeight, setOuterWidth } from '@js/core/utils/size'; import { isDeferred, isPlainObject } from '@js/core/utils/type'; import type { DxEvent } from '@js/events/events.types'; -import type { - AppointmentClickEvent, - AppointmentContextMenuEvent, - AppointmentDblClickEvent, - AppointmentRenderedEvent, -} from '@js/ui/scheduler'; +import type { AppointmentDblClickEvent } from '@js/ui/scheduler'; import type { TemplateBase } from '@ts/core/templates/template_base'; import { addOffsets } from '@ts/core/utils/date'; import type { OptionChanged } from '@ts/core/widget/types'; @@ -83,6 +78,11 @@ import type { DateRange } from './resizing/types'; import { isNeedToAdd } from './utils/get_arrays_diff'; import { getViewModelDiff } from './utils/get_view_model_diff'; +type CollectionItemAction = Exclude< + CollectionWidgetEditProperties[K], + string | null | undefined +>; + export interface AppointmentCollectionOptions { getResourceManager: () => ResourceManager; getAppointmentDataSource: () => AppointmentDataSource; @@ -95,9 +95,9 @@ export interface AppointmentCollectionOptions { appointmentTooltip: MobileTooltipStrategy | DesktopTooltipStrategy; dataAccessors: AppointmentDataAccessor; notifyScheduler: NotifyScheduler; - onItemRendered: (args: AppointmentRenderedEvent) => void; - onItemClick: (args: AppointmentClickEvent) => void; - onItemContextMenu: (args: AppointmentContextMenuEvent) => void; + onItemRendered: CollectionItemAction<'onItemRendered'>; + onItemClick: CollectionItemAction<'onItemClick'>; + onItemContextMenu: CollectionItemAction<'onItemContextMenu'>; onAppointmentDblClick: ((args: AppointmentDblClickEvent) => void) | null; tabIndex: number; focusStateEnabled: boolean; @@ -117,14 +117,12 @@ export interface AppointmentCollectionOptions { onContentReady: () => void; } -type ItemActionOptionName = 'onItemRendered' | 'onItemClick' | 'onItemContextMenu'; - export interface AppointmentCollectionProperties extends Omit< CollectionWidgetEditProperties, - Exclude | 'items' + keyof AppointmentCollectionOptions | 'items' >, - Omit { + AppointmentCollectionOptions { items?: AppointmentViewModelPlain[]; fixedContainer?: dxElementWrapper | null; allDayContainer?: dxElementWrapper | null; diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index 8bc89b9d5173..7cce15184790 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -309,7 +309,6 @@ const subscribes = { forceMaxAppointmentPerCell(this: Scheduler): unknown { // @ts-expect-error Scheduler has no forceMaxAppointmentPerCell method - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.forceMaxAppointmentPerCell(); }, diff --git a/packages/devextreme/js/__internal/scheduler/scheduler.ts b/packages/devextreme/js/__internal/scheduler/scheduler.ts index 6f2aec183a91..cfd9e330a48e 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler.ts @@ -1286,12 +1286,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { ? MobileTooltipStrategy : DesktopTooltipStrategy)(this.getAppointmentTooltipOptions()); - this._appointments = this._createComponent( - '
', - AppointmentCollection, - // @ts-expect-error item actions get the collection item event mapped by mapAppointmentFields - this.appointmentsConfig(), - ); + this._appointments = this._createComponent('
', AppointmentCollection, this.appointmentsConfig()); this._appointments.option('itemTemplate', this.getAppointmentTemplate('appointmentTemplate')); this.createAppointmentPopupForm(); @@ -1591,8 +1586,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { appointmentTooltip: this.appointmentTooltip, dataAccessors: this._dataAccessors, notifyScheduler: this.notifyScheduler, + // @ts-expect-error the collection passes its item event, mapped by mapAppointmentFields onItemRendered: this.getAppointmentRenderedAction(), + // @ts-expect-error the collection passes its item event, mapped by mapAppointmentFields onItemClick: this.createSchedulerAction('onAppointmentClick'), + // @ts-expect-error the collection passes its item event, mapped by mapAppointmentFields onItemContextMenu: this.createSchedulerAction('onAppointmentContextMenu'), onAppointmentDblClick: this.createSchedulerAction('onAppointmentDblClick'), tabIndex: this.option('tabIndex') ?? 0, From fd22caa40a3bedd5955e92f96670d5e1913f82ae Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 6 Oct 2026 00:49:11 +0400 Subject: [PATCH 07/10] Scheduler: close the typing gaps behind the suppressions of the m_ files --- .../appointments/appointment/m_appointment.ts | 8 ++++++-- .../scheduler/m_compact_appointments_helper.ts | 2 -- .../js/__internal/scheduler/m_subscribes.ts | 5 ----- .../js/__internal/scheduler/scheduler.ts | 15 +++++++++------ .../devextreme/js/__internal/scheduler/types.ts | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) 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 4895066ea800..8c98dff2024e 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts @@ -43,7 +43,7 @@ const REDUCED_APPOINTMENT_POINTERLEAVE_EVENT_NAME = addNamespace(pointerEvents.l interface ResizingRule { handles: string; - minWidth: number | undefined; + minWidth: number; minHeight: number; step: number; roundStepValue: boolean; @@ -90,6 +90,11 @@ export class Appointment extends DOMComponent( + funcName: Subject, + ...args: Parameters + ): ReturnType; + invoke( funcName: Subject, ...args: Parameters @@ -147,7 +152,6 @@ export class Appointment extends DOMComponent) => void { return (e) => { - // @ts-expect-error _createEventArgs is typed for the context menu event, same shape const clickEventArgs = this.instance._createEventArgs(e); - // @ts-expect-error the collection passes its onItemClick action, always a function onAppointmentClick(clickEventArgs); }; } diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index 7cce15184790..c7ceeaabdaac 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -72,7 +72,6 @@ const subscribes = { .getAppointmentSettings(options.$appointment) as AppointmentItemViewModel; const { startDate } = info.sourceAppointment; - // @ts-expect-error isDeleted: omitted, treated as false this.checkRecurringAppointment(options.target, options.data, startDate, () => { // eslint-disable-next-line no-void void this.updateAppointmentCore(options.target, options.data, function () { @@ -119,7 +118,6 @@ const subscribes = { // eslint-disable-next-line no-void void this.updateAppointmentCore(rawAppointment, targetedRawAppointment, onCancel, event); }, - // @ts-expect-error isDeleted: undefined is treated as false undefined, undefined, event, @@ -216,7 +214,6 @@ const subscribes = { width: this.getWorkSpace().getCellWidth(), height: this.getWorkSpace().getCellHeight(), }, - // @ts-expect-error SchedulerWorkSpaceLike.option() is untyped cellDurationInMinutes: this.getWorkSpace().option('cellDuration'), resizableStep: this.getWorkSpace().positionHelper.getResizableStep(), isAllDayPanel: isAllDay(this, itemData), @@ -317,12 +314,10 @@ const subscribes = { }, getEndDayHour(this: Scheduler): number { - // @ts-expect-error SchedulerWorkSpaceLike.option() is untyped return this._workSpace.option('endDayHour') || this.option('endDayHour'); }, getStartDayHour(this: Scheduler): number { - // @ts-expect-error SchedulerWorkSpaceLike.option() is untyped return this._workSpace.option('startDayHour') || this.option('startDayHour'); }, diff --git a/packages/devextreme/js/__internal/scheduler/scheduler.ts b/packages/devextreme/js/__internal/scheduler/scheduler.ts index cfd9e330a48e..b1a2135159de 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler.ts @@ -37,7 +37,7 @@ import type { DataSourceOptions } from '@js/data/data_source'; import DataHelperMixin from '@js/data_helper'; import type { CustomDialogOptions } from '@js/ui/dialog'; import { custom as customDialog } from '@js/ui/dialog'; -import type { ItemContextMenuEvent } from '@js/ui/list'; +import type { ItemClickEvent, ItemContextMenuEvent } from '@js/ui/list'; import type { Appointment, AppointmentAddingEvent, @@ -244,7 +244,10 @@ interface SchedulerWorkSpaceLike { positionHelper: PositionHelper; virtualScrollingDispatcher: VirtualScrollingDispatcherLike; viewDataProvider: ViewDataProvider; - option: (name: string | Record, value?: unknown) => unknown; + option: { + (name: K): WorkspaceOptionsInternal[K]; + (name: string | Record, value?: unknown): unknown; + }; getDateRange: () => Date[]; getCellFromDragTarget: ($dragTarget: dxElementWrapper) => dxElementWrapper | null; getCellFromPoint: (x: number, y: number) => dxElementWrapper | null; @@ -1430,7 +1433,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } _createEventArgs( - e: ItemContextMenuEvent, + e: ItemContextMenuEvent | ItemClickEvent, ): AppointmentTooltipContextMenuEventArgs { const itemData = e.itemData?.appointment; if (!itemData) { @@ -1876,7 +1879,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { singleAppointment: SafeAppointment, exceptionDate: Date, callback: () => void, - isDeleted: boolean, + isDeleted?: boolean, isPopupEditing?: boolean, dragEvent?: SchedulerDragEvent | null, recurrenceEditMode?: RecurrenceEditMode, @@ -1935,7 +1938,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { rawAppointment: SafeAppointment, newRawAppointment: SafeAppointment, exceptionDate: Date, - isDeleted: boolean, + isDeleted: boolean | undefined, isPopupEditing: boolean, dragEvent?: SchedulerDragEvent | null, ): void { @@ -2007,7 +2010,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return dateSerialization.serializeDate(date, UTC_FULL_DATE_FORMAT) as string; } - private showRecurrenceChangeConfirm(isDeleted: boolean): DeferredObj { + private showRecurrenceChangeConfirm(isDeleted?: boolean): DeferredObj { const title = messageLocalization.format(isDeleted ? 'dxScheduler-confirmRecurrenceDeleteTitle' : 'dxScheduler-confirmRecurrenceEditTitle'); const message = messageLocalization.format(isDeleted ? 'dxScheduler-confirmRecurrenceDeleteMessage' : 'dxScheduler-confirmRecurrenceEditMessage'); const seriesText = messageLocalization.format(isDeleted ? 'dxScheduler-confirmRecurrenceDeleteSeries' : 'dxScheduler-confirmRecurrenceEditSeries'); diff --git a/packages/devextreme/js/__internal/scheduler/types.ts b/packages/devextreme/js/__internal/scheduler/types.ts index e6743f815a0a..c5cfea234560 100644 --- a/packages/devextreme/js/__internal/scheduler/types.ts +++ b/packages/devextreme/js/__internal/scheduler/types.ts @@ -1,6 +1,6 @@ import type { dxElementWrapper } from '@js/core/renderer'; import type { ItemContextMenuEvent } from '@js/ui/list'; -import type { Appointment, Properties } from '@js/ui/scheduler'; +import type { Appointment } from '@js/ui/scheduler'; import type { IntegrationOptions } from '@ts/core/template_manager'; import type { ResourceLoader } from './utils/loader/resource_loader'; @@ -319,7 +319,7 @@ export interface CompactAppointmentOptions { sortedIndex: number; width: number; height: number; - onAppointmentClick: Properties['onAppointmentClick']; + onAppointmentClick: (e: AppointmentTooltipContextMenuEventArgs) => void; allowDrag: boolean; isCompact: boolean; } From dd4cd5dea93749214777e40b789dad6c90e84a46 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 6 Oct 2026 00:52:00 +0400 Subject: [PATCH 08/10] Scheduler: remove the dead m_text_utils module, the unused subscribe and the unused resources option --- .../appointments/m_appointment_collection.ts | 4 +- .../scheduler/appointments/m_text_utils.ts | 71 ------------------- .../js/__internal/scheduler/m_subscribes.ts | 9 +-- .../tooltip_strategy_base.ts | 3 +- 4 files changed, 5 insertions(+), 82 deletions(-) delete mode 100644 packages/devextreme/js/__internal/scheduler/appointments/m_text_utils.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 5b97f5b71949..394288b6d891 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -50,6 +50,7 @@ import type { } from '../types'; import { AppointmentAdapter } from '../utils/appointment_adapter/appointment_adapter'; import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; +import { DateFormatType } from '../utils/get_date_text'; import { getTargetedAppointment, getTargetedAppointmentFromInfo, @@ -72,7 +73,6 @@ import { Appointment } from './appointment/m_appointment'; import type { AppointmentProperties } from './appointment/m_types'; import { createAgendaAppointmentLayout, createAppointmentLayout } from './m_appointment_layout'; import { AppointmentsKeyboardNavigation } from './m_appointments_kbn'; -import { DateFormatType } from './m_text_utils'; import { getAppointmentDateRange } from './resizing/m_core'; import type { DateRange } from './resizing/types'; import { isNeedToAdd } from './utils/get_arrays_diff'; @@ -126,7 +126,6 @@ export interface AppointmentCollectionProperties items?: AppointmentViewModelPlain[]; fixedContainer?: dxElementWrapper | null; allDayContainer?: dxElementWrapper | null; - resources?: unknown[]; } const COMPONENT_CLASS = 'dx-scheduler-scrollable-appointments'; @@ -316,7 +315,6 @@ class SchedulerAppointments extends CollectionWidget { - if (isAllDay) { - return DateFormatType.DATE; - } - if (isDateAndTimeView && dateUtils.sameDate(startDate, endDate)) { - return DateFormatType.TIME; - } - return DateFormatType.DATETIME; -}; - -export const formatDates = ( - startDate: Date, - endDate: Date, - formatType: DateFormatType, -): string | undefined => { - const dateFormat = 'monthandday'; - const timeFormat = 'shorttime'; - const isSameDate = startDate.getDate() === endDate.getDate(); - - switch (formatType) { - case DateFormatType.DATETIME: - return [ - dateLocalization.format(startDate, dateFormat), - ' ', - dateLocalization.format(startDate, timeFormat), - ' - ', - isSameDate ? '' : `${dateLocalization.format(endDate, dateFormat)} `, - dateLocalization.format(endDate, timeFormat), - ].join(''); - case DateFormatType.TIME: - return `${dateLocalization.format(startDate, timeFormat)} - ${dateLocalization.format(endDate, timeFormat)}`; - case DateFormatType.DATE: - return `${dateLocalization.format(startDate, dateFormat)}${isSameDate ? '' : ` - ${dateLocalization.format(endDate, dateFormat)}`}`; - default: - return undefined; - } -}; - -export const createFormattedDateText = (options: FormattedDateTextOptions): string | undefined => { - const { - startDate, - endDate, - allDay, - format, - } = options; - - const formatType = format || getFormatType(startDate, endDate, allDay); - - return formatDates(startDate, endDate, formatType); -}; diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index c7ceeaabdaac..0cb4976c535d 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -142,7 +142,7 @@ const subscribes = { this: Scheduler, appointment: AppointmentTooltipItem['appointment'], targetedAppointmentRaw: TargetedAppointment, - format?: string, + format?: DateFormatType, ) { const targetedAppointment = { ...appointment, @@ -158,7 +158,7 @@ const subscribes = { return { text: adapter.text || messageLocalization.format('dxScheduler-noSubject'), - formatDate: getDateText(startDate, endDate, formatType as DateFormatType), + formatDate: getDateText(startDate, endDate, formatType), }; }, @@ -304,11 +304,6 @@ const subscribes = { return this.getEndViewDate(); }, - forceMaxAppointmentPerCell(this: Scheduler): unknown { - // @ts-expect-error Scheduler has no forceMaxAppointmentPerCell method - return this.forceMaxAppointmentPerCell(); - }, - getTargetedAppointmentData(this: Scheduler, appointment, element) { return this.getTargetedAppointment(appointment, element); }, diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts index 7ed0d8d4aabd..07232ec1f320 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts @@ -25,6 +25,7 @@ import type { AppointmentTooltipItem, TargetedAppointment, } from '../types'; +import type { DateFormatType } from '../utils/get_date_text'; const TOOLTIP_APPOINTMENT_ITEM = 'dx-tooltip-appointment-item'; const TOOLTIP_APPOINTMENT_ITEM_CONTENT = `${TOOLTIP_APPOINTMENT_ITEM}-content`; @@ -61,7 +62,7 @@ export interface AppointmentTooltipOptions { createFormattedDateText: ( appointment: Appointment, targetedAppointment?: Appointment | TargetedAppointment, - format?: string, + format?: DateFormatType, ) => { text: string; formatDate: string; From 6b19f3a49245d5a41183bd7dc66daf70dbc2ae00 Mon Sep 17 00:00:00 2001 From: Andrei Kharitonov Date: Fri, 23 Oct 2026 16:32:34 +0200 Subject: [PATCH 09/10] Scheduler: remove the drag behavior defaults that the work space config always overrides --- .../scheduler/m_appointment_drag_behavior.ts | 14 -------------- 1 file changed, 14 deletions(-) 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 408d5babb611..80469d502946 100644 --- a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts +++ b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts @@ -99,12 +99,6 @@ export default class AppointmentDragBehavior { this.appointments.notifyObserver('hideAppointmentTooltip'); } - onDragMove(e: AppointmentDraggingMoveEvent): void { - if (e.fromComponent !== e.toComponent) { - this.appointments.notifyObserver('removeDroppableCellClass'); - } - } - getAppointmentElement(e: AppointmentDraggingEndEvent): dxElementWrapper { // @ts-expect-error the event of a drag end always carries the original event const itemElement = e.event.data?.itemElement || e.itemElement; @@ -132,10 +126,6 @@ export default class AppointmentDragBehavior { }); } - onDragCancel(): void { - this.removeDroppableClasses(); - } - getItemData(appointmentElement: Element | dxElementWrapper): SafeAppointment | undefined { const dataFromTooltip = $(appointmentElement) .data(LIST_ITEM_DATA_KEY); @@ -256,10 +246,6 @@ export default class AppointmentDragBehavior { contentTemplate: null, filter: `.${APPOINTMENT_ITEM_CLASS}`, immediate: false, - onDragStart: this.onDragStart.bind(this), - onDragMove: this.onDragMove.bind(this), - onDragEnd: this.onDragEnd.bind(this), - onDragCancel: this.onDragCancel.bind(this), }, config); this.appointments._createComponent(container, Draggable, extend( From 0b3eb4f1a4a006e35de1068a38a42c82466706cd Mon Sep 17 00:00:00 2001 From: Andrei Kharitonov Date: Fri, 23 Oct 2026 16:44:46 +0200 Subject: [PATCH 10/10] Scheduler: get the appointment template through _getTemplate() instead of the undeclared _getTemplateByOption() --- packages/devextreme/js/__internal/scheduler/scheduler.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/scheduler.ts b/packages/devextreme/js/__internal/scheduler/scheduler.ts index b1a2135159de..8f555b3eb1c4 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler.ts @@ -1812,8 +1812,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._getTemplate(this.currentView[optionName]); } - // @ts-expect-error - return this._getTemplateByOption(optionName) as FunctionTemplate; + return this._getTemplate(this.option(optionName)); } private updateOption(viewName: 'workSpace' | 'header', optionName: string, value: T): void {