diff --git a/packages/devextreme/eslint.config.mjs b/packages/devextreme/eslint.config.mjs index 03ba17a3e76b..b971ed40dfaa 100644 --- a/packages/devextreme/eslint.config.mjs +++ b/packages/devextreme/eslint.config.mjs @@ -579,6 +579,25 @@ export default [ 'devextreme-custom/no-deferred': 'off', }, }, + // Strict TypeScript rules for scheduler/header + { + files: ['js/__internal/scheduler/header/**/*.ts?(x)'], + languageOptions: { + parser: tsParser, + ecmaVersion: 5, + sourceType: 'script', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: `${__dirname}/js/__internal`, + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'error', + }, + }, // Rules for grid controls { files: [ diff --git a/packages/devextreme/js/__internal/scheduler/header/m_calendar.ts b/packages/devextreme/js/__internal/scheduler/header/m_calendar.ts index eaf76198bbe8..4aed71418c43 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_calendar.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_calendar.ts @@ -1,44 +1,49 @@ import registerComponent from '@js/core/component_registrator'; import devices from '@js/core/devices'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import Calendar from '@js/ui/calendar'; +import type { DateLike } from '@js/ui/calendar'; import Popover from '@js/ui/popover/ui.popover'; import Popup from '@js/ui/popup/ui.popup'; -import type { dxSchedulerOptions } from '@js/ui/scheduler'; -import Scrollable from '@js/ui/scroll_view/ui.scrollable'; -import Widget from '@js/ui/widget/ui.widget'; +import Widget from '@ts/core/widget/widget'; +import type { KeyboardKeyDownEvent } from '@ts/events/core/m_keyboard_processor'; +import type { CalendarProperties } from '@ts/ui/calendar/calendar'; +import Calendar from '@ts/ui/calendar/calendar'; +import Scrollable from '@ts/ui/scroll_view/scrollable'; + +import type { HeaderCalendarOptions } from './types'; const CALENDAR_CLASS = 'dx-scheduler-navigator-calendar'; const CALENDAR_POPOVER_CLASS = 'dx-scheduler-navigator-calendar-popover'; -export default class SchedulerCalendar extends Widget { - _overlay: any; +export default class SchedulerCalendar extends Widget { + _overlay?: Popup | Popover; - _calendar: any; + _calendar?: Calendar; - show(target) { - if (!this._isMobileLayout()) { - this._overlay.option('target', target); + async show(target: HTMLElement): Promise { + if (!SchedulerCalendar._isMobileLayout()) { + this._overlay?.option('target', target); } - this._overlay.show(); + + await this._overlay?.show(); } - hide() { - this._overlay.hide(); + async hide(): Promise { + await this._overlay?.hide(); } - _keyboardHandler(opts): void { + _keyboardHandler(opts: KeyboardKeyDownEvent): boolean { this._calendar?._keyboardHandler(opts); + return true; } _init(): void { - // @ts-expect-error super._init(); this.$element(); } _render(): void { - // @ts-expect-error super._render(); this._renderOverlay(); } @@ -46,17 +51,16 @@ export default class SchedulerCalendar extends Widget { _renderOverlay(): void { this.$element().addClass(CALENDAR_POPOVER_CLASS); - const isMobileLayout = this._isMobileLayout(); + const isMobileLayout = SchedulerCalendar._isMobileLayout(); - const overlayType = isMobileLayout ? Popup : Popover; - - // @ts-expect-error - this._overlay = this._createComponent(this.$element(), overlayType, { - contentTemplate: () => this._createOverlayContent(), - onShown: () => this._calendar.focus(), + const overlayConfig = { + contentTemplate: (): dxElementWrapper => this._createOverlayContent(), + onShown: (): void => { + this._calendar?.focus(); + }, defaultOptionsRules: [ { - device: () => isMobileLayout, + device: (): boolean => isMobileLayout, options: { fullScreen: true, showCloseButton: false, @@ -67,15 +71,20 @@ export default class SchedulerCalendar extends Widget { }, }, ], - }); + }; + + if (isMobileLayout) { + this._overlay = this._createComponent(this.$element(), Popup, overlayConfig); + } else { + this._overlay = this._createComponent(this.$element(), Popover, overlayConfig); + } } - _createOverlayContent() { + _createOverlayContent(): dxElementWrapper { const result = $('
').addClass(CALENDAR_CLASS); - // @ts-expect-error this._calendar = this._createComponent(result, Calendar, this._getCalendarOptions()); - if (this._isMobileLayout()) { + if (SchedulerCalendar._isMobileLayout()) { const scrollable = this._createScrollable(result); return scrollable.$element(); } @@ -83,8 +92,7 @@ export default class SchedulerCalendar extends Widget { return result; } - _createScrollable(content) { - // @ts-expect-error + _createScrollable(content: dxElementWrapper): Scrollable { const result = this._createComponent('
', Scrollable, { height: 'auto', direction: 'both', @@ -94,7 +102,9 @@ export default class SchedulerCalendar extends Widget { return result; } - _optionChanged({ name, value }) { + _optionChanged( + { name, value } : { name: string; value: DateLike | DateLike[] }, + ): void { switch (name) { case 'value': this._calendar?.option('value', value); @@ -104,23 +114,26 @@ export default class SchedulerCalendar extends Widget { } } - _getCalendarOptions() { + _getCalendarOptions(): CalendarProperties { + const { + value, min, max, firstDayOfWeek, focusStateEnabled, tabIndex, onValueChanged, + } = this.option(); return { - value: this.option('value'), - min: this.option('min'), - max: this.option('max'), - firstDayOfWeek: this.option('firstDayOfWeek'), - focusStateEnabled: this.option('focusStateEnabled'), - onValueChanged: this.option('onValueChanged'), + value, + min, + max, + firstDayOfWeek, + focusStateEnabled, + tabIndex, + onValueChanged, + // @ts-expect-error skipFocusCheck is an internal Calendar property skipFocusCheck: true, - tabIndex: this.option('tabIndex'), }; } - _isMobileLayout() { + static _isMobileLayout(): boolean { return !devices.current().generic; } } -// @ts-expect-error registerComponent('dxSchedulerCalendarPopup', SchedulerCalendar); diff --git a/packages/devextreme/js/__internal/scheduler/header/m_date_navigator.test.ts b/packages/devextreme/js/__internal/scheduler/header/m_date_navigator.test.ts index 12748972021a..b966e2ed7b57 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_date_navigator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_date_navigator.test.ts @@ -1,14 +1,16 @@ import { describe, expect, it, jest, } from '@jest/globals'; +import type { ToolbarItem } from '@js/ui/scheduler'; import { CLASS, DEFAULT_ITEMS, getDateNavigator, ITEMS_NAME, } from './m_date_navigator'; +import type { SchedulerHeader } from './m_header'; describe('getDateNavigator', () => { it('should return default options in case of item is empty', () => { - expect(getDateNavigator({} as any, {})).toEqual({ + expect(getDateNavigator({} as SchedulerHeader, {})).toEqual({ location: 'before', name: 'dateNavigator', widget: 'dxButtonGroup', @@ -26,13 +28,13 @@ describe('getDateNavigator', () => { }); }); it('should return replace items in correct order with custom options', () => { - expect(getDateNavigator({} as any, { + expect(getDateNavigator({} as SchedulerHeader, { customField: 'customField', options: { customOption: 'customOption', items: ['dateInterval', 'next', { key: 'customButton' }], }, - } as any)).toEqual({ + } as ToolbarItem)).toEqual({ location: 'before', name: 'dateNavigator', widget: 'dxButtonGroup', @@ -54,7 +56,7 @@ describe('getDateNavigator', () => { it('should handle default and custom click callback', () => { const customClick = jest.fn(); const event = { itemData: { clickHandler: jest.fn() } }; - const config = getDateNavigator({} as any, { + const config = getDateNavigator({} as SchedulerHeader, { options: { onItemClick: customClick }, }); diff --git a/packages/devextreme/js/__internal/scheduler/header/m_date_navigator.ts b/packages/devextreme/js/__internal/scheduler/header/m_date_navigator.ts index 7e9405f55a1f..7894ae76b3ed 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_date_navigator.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_date_navigator.ts @@ -2,7 +2,7 @@ import messageLocalization from '@js/common/core/localization/message'; import dateUtils from '@js/core/utils/date'; import type { ContentReadyEvent } from '@js/ui/button'; import type { Item as ButtonGroupItem, ItemClickEvent, Properties as ButtonGroupOptions } from '@js/ui/button_group'; -import { isMaterialBased } from '@js/ui/themes'; +import { current, isMaterialBased } from '@js/ui/themes'; import type { Item as ToolbarItem } from '@js/ui/toolbar'; import { dateUtilsTs } from '@ts/core/utils/date'; import { extend } from '@ts/core/utils/m_extend'; @@ -31,7 +31,7 @@ const { trimTime } = dateUtils; interface DateNavigatorItem extends ButtonGroupItem { key: string; - clickHandler: (event: ItemClickEvent) => void; + clickHandler: (event: ItemClickEvent) => Promise | void; onContentReady: (event: ContentReadyEvent) => void; } @@ -154,9 +154,28 @@ const getNextButtonOptions = (header: SchedulerHeader): DateNavigatorItem => { }; }; +export const getTodayButtonOptions = ( + header: SchedulerHeader, + item: ToolbarItem, +): ToolbarItem => extend(true, {}, { + location: 'before', + locateInMenu: 'auto', + widget: 'dxButton', + cssClass: 'dx-scheduler-today', + options: { + text: messageLocalization.format('dxScheduler-navigationToday'), + icon: 'today', + stylingMode: 'outlined', + type: 'normal', + onClick() { + const { indicatorTime } = header.option(); + header._updateCurrentDate(indicatorTime ?? new Date()); + }, + }, +}, item) as ToolbarItem; + export const getDateNavigator = (header: SchedulerHeader, item: ToolbarItem): ToolbarItem => { - // @ts-expect-error current theme used - const stylingMode = isMaterialBased() ? 'text' : 'contained'; + const stylingMode = isMaterialBased(current()) ? 'text' : 'contained'; const config: ToolbarItem = extend(true, {}, { location: 'before', name: 'dateNavigator', @@ -170,7 +189,8 @@ export const getDateNavigator = (header: SchedulerHeader, item: ToolbarItem): To const options = config.options as ButtonGroupOptions; const { onItemClick } = options; - options.items = (options.items ?? DEFAULT_ITEMS).map((groupItem) => { + const items = (options.items ?? DEFAULT_ITEMS); + options.items = items.map((groupItem: ButtonGroupItem | string) => { switch (groupItem) { case ITEMS_NAME.previousButton: return getPreviousButtonOptions(header); @@ -179,7 +199,7 @@ export const getDateNavigator = (header: SchedulerHeader, item: ToolbarItem): To case ITEMS_NAME.calendarButton: return getCalendarButtonOptions(header); default: - return groupItem; + return groupItem as ButtonGroupItem; } }); options.onItemClick = (event): void => { diff --git a/packages/devextreme/js/__internal/scheduler/header/m_header.ts b/packages/devextreme/js/__internal/scheduler/header/m_header.ts index 049eb0e857f2..cc0f549dc9e7 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_header.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_header.ts @@ -1,4 +1,3 @@ -import '@js/ui/button_group'; import '@js/ui/drop_down_button'; import registerComponent from '@js/core/component_registrator'; @@ -8,14 +7,18 @@ import $ from '@js/core/renderer'; import { getPathParts } from '@js/core/utils/data'; import dateUtils from '@js/core/utils/date'; import { extend } from '@js/core/utils/extend'; +import type { ItemClickEvent } from '@js/ui/button_group'; +import type { DateNavigatorTextInfo, ToolbarItem } from '@js/ui/scheduler'; import Toolbar from '@js/ui/toolbar'; -import Widget from '@js/ui/widget/ui.widget'; -import type { NormalizedView } from '@ts/scheduler/utils/options/types'; +import type { OptionChanged } from '@ts/core/widget/types'; +import Widget from '@ts/core/widget/widget'; +import type { NormalizedView, SafeSchedulerOptions } from '@ts/scheduler/utils/options/types'; import type { Direction } from './constants'; import SchedulerCalendar from './m_calendar'; import { getDateNavigator, + getTodayButtonOptions, } from './m_date_navigator'; import { getCaption, @@ -27,8 +30,10 @@ import { getDropDownViewSwitcher, getTabViewSwitcher, } from './m_view_switcher'; -import { getTodayButtonOptions } from './today'; -import type { HeaderOptions, IntervalOptions } from './types'; +import type { + EventMapHandler, + HeaderOptions, IntervalOptions, +} from './types'; const CLASSES = { component: 'dx-scheduler-header', @@ -41,20 +46,19 @@ const ITEM_NAMES = { }; export class SchedulerHeader extends Widget { - eventMap: any; + eventMap!: Map; - _toolbar!: Toolbar; + _toolbar?: Toolbar; - _calendar: any; + _calendar?: SchedulerCalendar; - get captionText() { + get captionText(): string { return this._getCaption().text; } getIntervalOptions(date: Date): IntervalOptions { - const currentView = this.option('currentView'); + const { currentView, firstDayOfWeek } = this.option(); const step = getStep(currentView.type); - const firstDayOfWeek = this.option('firstDayOfWeek'); return { date, @@ -65,44 +69,43 @@ export class SchedulerHeader extends Widget { }; } - _getDefaultOptions() { - // @ts-expect-error + _getDefaultOptions(): HeaderOptions & { _useShortDateFormat: boolean } { return extend(super._getDefaultOptions(), { _useShortDateFormat: !devices.real().generic || devices.isSimulator(), - }); + }) as HeaderOptions & { _useShortDateFormat: boolean }; } - _createEventMap() { - this.eventMap = new Map( - [ - ['currentView', []], - ['views', []], - ['currentDate', [this._getCalendarOptionUpdater('value')]], - ['min', [this._getCalendarOptionUpdater('min')]], - ['max', [this._getCalendarOptionUpdater('max')]], - ['tabIndex', [this.repaint.bind(this)]], - ['focusStateEnabled', [this.repaint.bind(this)]], - ['useDropDownViewSwitcher', [this.repaint.bind(this)]], - ['indicatorTime', []], - ], - ); + _createEventMap(): void { + this.eventMap = new Map([ + ['currentView', []], + ['views', []], + ['currentDate', [this._getCalendarOptionUpdater('value')]], + ['min', [this._getCalendarOptionUpdater('min')]], + ['max', [this._getCalendarOptionUpdater('max')]], + ['tabIndex', [this.repaint.bind(this)]], + ['focusStateEnabled', [this.repaint.bind(this)]], + ['useDropDownViewSwitcher', [this.repaint.bind(this)]], + ['indicatorTime', []], + ] as [string, EventMapHandler[]][]); } - _addEvent(name, event) { + _addEvent(name: string, event: EventMapHandler): void { if (!this.eventMap.has(name)) { this.eventMap.set(name, []); } const events = this.eventMap.get(name); - this.eventMap.set(name, [...events, event]); + if (events) { + this.eventMap.set(name, [...events, event]); + } } - _optionChanged(args) { + _optionChanged(args: OptionChanged): void { const { name, value } = args; if (this.eventMap.has(name)) { const events = this.eventMap.get(name); - events.forEach((event) => { + events?.forEach((event) => { event(value); }); } @@ -119,32 +122,27 @@ export class SchedulerHeader extends Widget { this.repaint(); break; case fullName === 'toolbar.items': - this._toolbar.option( + this._toolbar?.option( 'items', - (value as []).map((item) => this._parseItem(item)), + (value as []).map((item: ToolbarItem) => this._parseItem(item)), ); break; case parts[1] === 'items' && parts.length === 3: - // `toolbar.items[i]` case - this._toolbar.option(optionName, this._parseItem(value)); + this._toolbar?.option(optionName, this._parseItem(value as ToolbarItem)); break; default: - // `toolbar.prop` case - // `toolbar.items[i].prop` case - this._toolbar.option(optionName, value); + this._toolbar?.option(optionName, value); } } - _init() { - // @ts-expect-error + _init(): void { super._init(); this._createEventMap(); this.$element().addClass(CLASSES.component); } - _render() { - // @ts-expect-error + _render(): void { super._render(); this._createEventMap(); @@ -152,20 +150,19 @@ export class SchedulerHeader extends Widget { this._toggleVisibility(); } - _renderToolbar() { + _renderToolbar(): void { const config = this._createToolbarConfig(); const toolbarElement = $('
'); toolbarElement.appendTo(this.$element()); - // @ts-expect-error this._toolbar = this._createComponent(toolbarElement, Toolbar, config); } _toggleVisibility(): void { - const toolbarOptions = this.option('toolbar') as any; - const isHeaderShown = toolbarOptions.visible - || (toolbarOptions.visible === undefined && toolbarOptions.items.length); + const { toolbar } = this.option(); + const isHeaderShown = toolbar.visible + ?? (toolbar.visible === undefined && toolbar.items.length); if (isHeaderShown) { this.$element().removeClass(CLASSES.invisible); @@ -174,17 +171,17 @@ export class SchedulerHeader extends Widget { } } - _createToolbarConfig() { - const options = this.option('toolbar'); - const parsedItems = options.items.map((element) => this._parseItem(element)); + _createToolbarConfig(): SafeSchedulerOptions['toolbar'] { + const { toolbar } = this.option(); + const parsedItems = toolbar.items.map((element) => this._parseItem(element)); return { - ...options, + ...toolbar, items: parsedItems, }; } - _parseItem(item) { + _parseItem(item: ToolbarItem | string): ToolbarItem { const itemName = typeof item === 'string' ? item : item.name; const itemOptions = typeof item === 'string' ? {} : item; @@ -205,74 +202,82 @@ export class SchedulerHeader extends Widget { } } - return extend(true, {}, item); + return extend(true, {}, item) as ToolbarItem; } - _callEvent(event, arg) { + _callEvent(event: string, arg: unknown): void { if (this.eventMap.has(event)) { const events = this.eventMap.get(event); - events.forEach((event) => event(arg)); + if (events) { + events.forEach((eventMapHandler) => eventMapHandler(arg)); + } } } - _updateCurrentView(view: Required) { - this.option('onCurrentViewChange')(view.name); + _updateCurrentView(view: Required): void { + const { onCurrentViewChange } = this.option(); + onCurrentViewChange(view.name); } - _updateCalendarValueAndCurrentDate(date) { + _updateCalendarValueAndCurrentDate(date: Date): void { this._updateCurrentDate(date); - this._calendar.option('value', date); + this._calendar?.option('value', date); } - _updateCurrentDate(date) { - this.option('onCurrentDateChange')(date); + _updateCurrentDate(date: Date): void { + const { onCurrentDateChange } = this.option(); + onCurrentDateChange(date); this._callEvent('currentDate', date); } - _renderCalendar() { - // @ts-expect-error + _renderCalendar(): void { + const { + currentDate, min, max, firstDayOfWeek, focusStateEnabled, tabIndex, + } = this.option(); this._calendar = this._createComponent('
', SchedulerCalendar, { - value: this.option('currentDate'), - min: this.option('min'), - max: this.option('max'), - firstDayOfWeek: this.option('firstDayOfWeek'), - focusStateEnabled: this.option('focusStateEnabled'), - tabIndex: this.option('tabIndex'), - onValueChanged: (e) => { + value: currentDate, + min, + max, + firstDayOfWeek, + focusStateEnabled, + tabIndex, + onValueChanged: async (e) => { this._updateCurrentDate(e.value); - this._calendar.hide(); + await this._calendar?.hide(); }, }); this._calendar.$element().appendTo(this.$element()); } - _getCalendarOptionUpdater(name) { - return (value) => { + _getCalendarOptionUpdater(name: string) { + return (value: unknown): void => { if (this._calendar) { this._calendar.option(name, value); } }; } - _getNextDate(direction: Direction, initialDate?: Date) { - const date = initialDate ?? this.option('currentDate'); + _getNextDate(direction: Direction, initialDate?: Date): Date { + const { currentDate } = this.option(); + const date = initialDate ?? currentDate; const options = this.getIntervalOptions(date); return getNextIntervalDate(options, direction); } - _getDisplayedDate() { - const startViewDate = new Date(this.option('startViewDate')); - const isMonth = this.option('currentView')?.type === 'month'; + _getDisplayedDate(): Date { + const { startViewDate, currentView } = this.option(); + const isMonth = currentView.type === 'month'; return isMonth ? nextWeek(startViewDate) : startViewDate; } - _getCaptionOptions() { - let date = this.option('currentDate'); + _getCaptionOptions(): IntervalOptions { + const { currentDate, startViewDate } = this.option(); + let date = currentDate; - if (this.option('startViewDate')) { + if (startViewDate) { date = this._getDisplayedDate(); } @@ -281,28 +286,27 @@ export class SchedulerHeader extends Widget { return this.getIntervalOptions(date); } - _getCaption() { + _getCaption(): DateNavigatorTextInfo { + const { customizeDateNavigatorText } = this.option(); const options = this._getCaptionOptions(); - const customizationFunction = this.option('customizeDateNavigatorText'); const useShortDateFormat = this.option('_useShortDateFormat'); - return getCaption(options, Boolean(useShortDateFormat), customizationFunction); + return getCaption(options, Boolean(useShortDateFormat), customizeDateNavigatorText); } - _updateDateByDirection(direction: Direction) { + _updateDateByDirection(direction: Direction): void { const date = this._getNextDate(direction); this._updateCalendarValueAndCurrentDate(date); } - _showCalendar(e) { - this._calendar.show(e.element); + async _showCalendar(e: ItemClickEvent): Promise { + await this._calendar?.show(e.element); } - _hideCalendar() { - this._calendar.hide(); + async _hideCalendar(): Promise { + await this._calendar?.hide(); } } -// @ts-expect-error registerComponent('dxSchedulerHeader', SchedulerHeader); diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 7c8770d56997..2b581c1dc585 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -3,6 +3,7 @@ import dateUtils from '@js/core/utils/date'; import { isFunction, isObject } from '@js/core/utils/type'; import messageLocalization from '@js/localization/message'; import type { DateNavigatorTextInfo, Properties } from '@js/ui/scheduler'; +import type { BaseFormat } from '@ts/core/localization/date'; import { camelize } from '@ts/core/utils/m_inflector'; import type { IntervalOptions, Step } from '@ts/scheduler/header/types'; import type { NormalizedView, RawViewType, ViewType } from '@ts/scheduler/utils/options/types'; @@ -62,7 +63,7 @@ const getDateAfterWorkWeek = (workWeekStart: Date): Date => { let workDaysCount = 0; while (workDaysCount < DAYS_IN_WORK_WEEK) { if (!isWeekend(date)) { - workDaysCount++; + workDaysCount += 1; } date = nextDay(date); @@ -71,56 +72,34 @@ const getDateAfterWorkWeek = (workWeekStart: Date): Date => { return date; }; -const nextAgendaStart = (date: Date, agendaDuration: number): Date => addDateInterval(date, { days: agendaDuration }, 1); +const nextAgendaStart = ( + date: Date, + agendaDuration: number, +): Date => addDateInterval(date, { days: agendaDuration }, 1); -const getIntervalStartDate = (options: IntervalOptions) => { +const getIntervalStartDate = (options: IntervalOptions): Date => { const { date, step, firstDayOfWeek } = options; - // eslint-disable-next-line default-case switch (step) { case 'day': case 'week': case 'month': - return getPeriodStart(date, step, false, firstDayOfWeek); + return getPeriodStart(date, step, false, firstDayOfWeek) as Date; case 'workWeek': - // eslint-disable-next-line no-case-declarations - const firstWeekDay = getWeekStart(date, firstDayOfWeek); - return getWorkWeekStart(firstWeekDay); + return getWorkWeekStart(getWeekStart(date, firstDayOfWeek)); case 'agenda': return new Date(date); + default: + return new Date(date); } }; -const getIntervalEndDate = (startDate: Date, options: IntervalOptions) => { - const { intervalCount, step, agendaDuration } = options; - - let periodStartDate; - let periodEndDate; - let nextPeriodStartDate = new Date(startDate); - - for (let i = 0; i < intervalCount; i++) { - periodStartDate = nextPeriodStartDate; - - periodEndDate = getPeriodEndDate(periodStartDate, step, agendaDuration!); - - nextPeriodStartDate = getNextPeriodStartDate(periodEndDate, step); - } - - return periodEndDate; -}; - -export const getCaptionInterval = (options: IntervalOptions): { - startDate: Date; - endDate: Date; -} => { - const startDate = getIntervalStartDate(options); - const endDate = getIntervalEndDate(startDate, options); - - return { startDate, endDate }; -}; - -const getPeriodEndDate = (currentPeriodStartDate: Date, step: Step, agendaDuration: number): Date => { - let date; +const getPeriodEndDate = ( + currentPeriodStartDate: Date, + step: Step, + agendaDuration: number, +): Date => { + let date: Date = new Date(); // eslint-disable-next-line default-case switch (step) { @@ -156,29 +135,22 @@ const getNextPeriodStartDate = (currentPeriodEndDate: Date, step): Date => { return date; }; -export const getNextIntervalDate = (options, direction: Direction): Date => { - const { - date, step, intervalCount, agendaDuration, - } = options; +const getIntervalEndDate = (startDate: Date, options: IntervalOptions): Date => { + const { intervalCount, step, agendaDuration } = options; - let dayDuration; - // eslint-disable-next-line default-case - switch (step) { - case 'day': - dayDuration = Number(intervalCount); - break; - case 'week': - case 'workWeek': - dayDuration = 7 * intervalCount; - break; - case 'agenda': - dayDuration = agendaDuration; - break; - case 'month': - return getNextMonthDate(date, intervalCount, direction); + let periodStartDate = new Date(startDate); + let periodEndDate = new Date(startDate); + let nextPeriodStartDate = new Date(startDate); + + for (let i = 0; i < intervalCount; i += 1) { + periodStartDate = nextPeriodStartDate; + + periodEndDate = getPeriodEndDate(periodStartDate, step, agendaDuration ?? 0); + + nextPeriodStartDate = getNextPeriodStartDate(periodEndDate, step); } - return addDateInterval(date, { days: dayDuration }, direction); + return periodEndDate; }; const getNextMonthDate = (date: Date, intervalCount: number, direction: Direction): Date => { @@ -202,9 +174,34 @@ const getNextMonthDate = (date: Date, intervalCount: number, direction: Directio return thatMonthMinDate; }; +export const getNextIntervalDate = (options: IntervalOptions, direction: Direction): Date => { + const { + date, step, intervalCount, agendaDuration, + } = options; + + let dayDuration = 0; + // eslint-disable-next-line default-case + switch (step) { + case 'day': + dayDuration = Number(intervalCount); + break; + case 'week': + case 'workWeek': + dayDuration = 7 * intervalCount; + break; + case 'agenda': + dayDuration = agendaDuration ?? 0; + break; + case 'month': + return getNextMonthDate(date, intervalCount, direction); + } + + return addDateInterval(date, { days: dayDuration }, direction); +}; + const getDateMonthFormatter = (isShort: boolean) => { const monthType = isShort ? 'abbreviated' : 'wide'; - const months = dateLocalization.getMonthNames(monthType as any); + const months = dateLocalization.getMonthNames(monthType as BaseFormat); return (date: Date): string => { const day = formatDate(date, 'day'); @@ -240,6 +237,16 @@ const getDifferentYearCaption = (startDate: Date, endDate: Date): string => { return `${firstDateText}-${lastDateDateText}`; }; +export const getCaptionInterval = (options: IntervalOptions): { + startDate: Date; + endDate: Date; +} => { + const startDate = getIntervalStartDate(options); + const endDate = getIntervalEndDate(startDate, options); + + return { startDate, endDate }; +}; + const getSameYearCaption = (startDate: Date, endDate: Date, isShort: boolean): string => { const isDifferentMonthDates = startDate.getMonth() !== endDate.getMonth(); const useShortFormat = isDifferentMonthDates || isShort; diff --git a/packages/devextreme/js/__internal/scheduler/header/m_view_switcher.ts b/packages/devextreme/js/__internal/scheduler/header/m_view_switcher.ts index 822499f11681..8b98db201586 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_view_switcher.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_view_switcher.ts @@ -1,4 +1,4 @@ -import { isFluent } from '@js/ui/themes'; +import { current, isFluent } from '@js/ui/themes'; import type { Item as ToolbarItem } from '@js/ui/toolbar'; import type { NormalizedView } from '../utils/options/types'; @@ -14,25 +14,32 @@ const ClASS = { dropDownButtonContent: 'dx-scheduler-view-switcher-dropdown-button-content', }; -const getViewsAndSelectedView = (header: SchedulerHeader) => { - const views = formatViews(header.option('views')); - const selectedView = getViewName(header.option('currentView')); - const isSelectedViewInViews = views.some((view) => view.name === selectedView); +const getViewsAndSelectedView = (header: SchedulerHeader): +{ + selectedView: string | undefined; + views: NormalizedView[]; +} => { + const { views, currentView } = header.option(); + const formattedViews = formatViews(views); + const selectedView = getViewName(currentView); + const isSelectedViewInViews = formattedViews.some((view) => view.name === selectedView); return { selectedView: isSelectedViewInViews ? selectedView : undefined, - views, + views: formattedViews, }; }; const isViewSwitcherVisible = (views: NormalizedView[]): boolean => views.length > 1; -export const getTabViewSwitcher = (header: SchedulerHeader, item): ToolbarItem => { +export const getTabViewSwitcher = ( + header: SchedulerHeader, + item: ToolbarItem, +): ToolbarItem => { const { selectedView, views } = getViewsAndSelectedView(header); const isVisible = isViewSwitcherVisible(views); - // @ts-expect-error - const stylingMode = isFluent() ? 'outlined' : 'contained'; + const stylingMode = isFluent(current()) ? 'outlined' : 'contained'; return { widget: 'dxButtonGroup', @@ -53,7 +60,7 @@ export const getTabViewSwitcher = (header: SchedulerHeader, item): ToolbarItem = const viewSwitcher = e.component; header._addEvent('currentView', (view) => { - viewSwitcher.option('selectedItemKeys', [getViewName(view)]); + viewSwitcher.option('selectedItemKeys', [getViewName(view as NormalizedView)]); }); }, }, @@ -61,7 +68,10 @@ export const getTabViewSwitcher = (header: SchedulerHeader, item): ToolbarItem = } as ToolbarItem; }; -export const getDropDownViewSwitcher = (header: SchedulerHeader, item): ToolbarItem => { +export const getDropDownViewSwitcher = ( + header: SchedulerHeader, + item: ToolbarItem, +): ToolbarItem => { const { selectedView, views } = getViewsAndSelectedView(header); const isVisible = isViewSwitcherVisible(views); @@ -88,8 +98,8 @@ export const getDropDownViewSwitcher = (header: SchedulerHeader, item): ToolbarI onContentReady: (e) => { const viewSwitcher = e.component; - header._addEvent('currentView', (view: NormalizedView) => { - viewSwitcher.option('selectedItemKey', getViewName(view)); + header._addEvent('currentView', (view) => { + viewSwitcher.option('selectedItemKey', getViewName(view as NormalizedView)); }); }, dropDownOptions: { diff --git a/packages/devextreme/js/__internal/scheduler/header/today.ts b/packages/devextreme/js/__internal/scheduler/header/today.ts deleted file mode 100644 index b4b94339aa75..000000000000 --- a/packages/devextreme/js/__internal/scheduler/header/today.ts +++ /dev/null @@ -1,24 +0,0 @@ -import messageLocalization from '@js/common/core/localization/message'; -import type { Item as ToolbarItem } from '@js/ui/toolbar'; -import { extend } from '@ts/core/utils/m_extend'; - -import type { SchedulerHeader } from './m_header'; - -export const getTodayButtonOptions = ( - header: SchedulerHeader, - item: ToolbarItem, -): ToolbarItem => extend(true, {}, { - location: 'before', - locateInMenu: 'auto', - widget: 'dxButton', - cssClass: 'dx-scheduler-today', - options: { - text: messageLocalization.format('dxScheduler-navigationToday'), - icon: 'today', - stylingMode: 'outlined', - type: 'normal', - onClick() { - header._updateCurrentDate(header.option('indicatorTime') ?? new Date()); - }, - }, -}, item) as ToolbarItem; diff --git a/packages/devextreme/js/__internal/scheduler/header/types.ts b/packages/devextreme/js/__internal/scheduler/header/types.ts index 98c2ca07eab7..0481ff50a903 100644 --- a/packages/devextreme/js/__internal/scheduler/header/types.ts +++ b/packages/devextreme/js/__internal/scheduler/header/types.ts @@ -1,3 +1,6 @@ +import type { FirstDayOfWeek } from '@js/common'; +import type { ValueChangedEvent } from '@js/ui/calendar'; + import type { NormalizedView, SafeSchedulerOptions } from '../utils/options/types'; export interface HeaderOptions { @@ -11,7 +14,7 @@ export interface HeaderOptions { tabIndex?: number; focusStateEnabled?: boolean; useDropDownViewSwitcher: boolean; - firstDayOfWeek?: number; + firstDayOfWeek?: FirstDayOfWeek; toolbar: SafeSchedulerOptions['toolbar']; onCurrentViewChange: (name: string) => void; onCurrentDateChange: (date: Date) => void; @@ -27,3 +30,15 @@ export interface IntervalOptions { intervalCount: number; agendaDuration?: number; } + +export interface HeaderCalendarOptions { + value: Date; + min?: Date; + max?: Date; + firstDayOfWeek?: FirstDayOfWeek; + focusStateEnabled?: boolean; + tabIndex?: number; + onValueChanged?: (e: ValueChangedEvent) => void; +} + +export type EventMapHandler = (value: unknown) => void; diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 4f170cd2253c..0c635929bded 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -28,7 +28,7 @@ import { import { hasWindow } from '@js/core/utils/window'; import DataHelperMixin from '@js/data_helper'; import { custom as customDialog } from '@js/ui/dialog'; -import type { Appointment, AppointmentTooltipShowingEvent } from '@js/ui/scheduler'; +import type { Appointment, AppointmentTooltipShowingEvent, FirstDayOfWeek } from '@js/ui/scheduler'; import errors from '@js/ui/widget/ui.errors'; import { dateUtilsTs } from '@ts/core/utils/date'; @@ -2160,10 +2160,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - getFirstDayOfWeek() { + getFirstDayOfWeek(): FirstDayOfWeek { return isDefined(this.getViewOption('firstDayOfWeek')) - ? this.getViewOption('firstDayOfWeek') - : dateLocalization.firstDayOfWeekIndex(); + ? this.getViewOption('firstDayOfWeek') as FirstDayOfWeek + : dateLocalization.firstDayOfWeekIndex() as FirstDayOfWeek; } _validateKeyFieldIfAgendaExist() {