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 5948d999ba90..88e14a398747 100644 --- a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts +++ b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts @@ -58,7 +58,9 @@ export class CompactAppointmentsHelper { private getExtraOptionsForTooltip(options: CompactAppointmentOptions, $appointmentCollector) { return { clickEvent: this.clickEvent(options.onAppointmentClick).bind(this), - dragBehavior: options.allowDrag && this.createTooltipDragBehavior($appointmentCollector).bind(this), + dragBehavior: options.allowDrag + ? this.createTooltipDragBehavior($appointmentCollector).bind(this) + : undefined, isButtonClick: true, tabFocusLoopEnabled: true, }; diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 66fa3dcb5f61..cfa046ca5852 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -62,8 +62,8 @@ import { } from './r1/utils/index'; import { validateRRule } from './recurrence/validate_rule'; import { SchedulerOptionsBaseWidget } from './scheduler_options_base_widget'; -import { DesktopTooltipStrategy } from './tooltip_strategies/m_desktop_tooltip_strategy'; -import { MobileTooltipStrategy } from './tooltip_strategies/m_mobile_tooltip_strategy'; +import { DesktopTooltipStrategy } from './tooltip_strategies/desktop_tooltip_strategy'; +import { MobileTooltipStrategy } from './tooltip_strategies/mobile_tooltip_strategy'; import type { AppointmentTooltipItem, SafeAppointment, diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_desktop_tooltip_strategy.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/desktop_tooltip_strategy.ts similarity index 53% rename from packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_desktop_tooltip_strategy.ts rename to packages/devextreme/js/__internal/scheduler/tooltip_strategies/desktop_tooltip_strategy.ts index 39de4b390f81..f0c87f33febf 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_desktop_tooltip_strategy.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/desktop_tooltip_strategy.ts @@ -1,57 +1,64 @@ import messageLocalization from '@js/common/core/localization/message'; +import type { dxElementWrapper } from '@js/core/renderer'; +import type { ContentReadyEvent, ItemContextMenuEvent, Properties as ListProperties } from '@js/ui/list'; import supportUtils from '@ts/core/utils/m_support'; import Tooltip from '@ts/ui/m_tooltip'; -import { TooltipStrategyBase } from './m_tooltip_strategy_base'; +import type { AppointmentTooltipItem } from '../types'; +import { TooltipStrategyBase } from './tooltip_strategy_base'; const APPOINTMENT_TOOLTIP_WRAPPER_CLASS = 'dx-scheduler-appointment-tooltip-wrapper'; const MAX_TOOLTIP_HEIGHT = 200; export class DesktopTooltipStrategy extends TooltipStrategyBase { - protected override prepareBeforeVisibleChanged(dataList) { - this.tooltip.option('position', { + protected override prepareBeforeVisibleChanged(dataList: AppointmentTooltipItem[]): void { + this.tooltip?.option('position', { my: 'bottom', at: 'top', boundary: this.getBoundary(dataList), - offset: this.extraOptions.offset, + offset: this.extraOptions?.offset, collision: 'fit flipfit', }); } - private getBoundary(dataList) { - return this._options.isAppointmentInAllDayPanel(dataList[0].appointment) ? this._options.container : this._options.getScrollableContainer(); + private getBoundary(dataList: AppointmentTooltipItem[]): dxElementWrapper { + return this._options.isAppointmentInAllDayPanel(dataList[0].appointment) + ? this._options.container + : this._options.getScrollableContainer(); } - protected override onShown() { + protected override onShown(): void { super.onShown(); - if (this.extraOptions.isButtonClick) { + if (this.extraOptions?.isButtonClick) { this.list.focus(); this.list.option('focusedElement', null); } } - // @ts-expect-error - protected override createListOption(target, dataList) { - // @ts-expect-error - const result: any = super.createListOption(target, dataList); + protected override createListOption( + dataList: AppointmentTooltipItem[], + ): ListProperties { + const result = super.createListOption(dataList); // T724287 this condition is not covered by tests, because touch variable cannot be overridden. // In the future, it is necessary to cover the tests result.showScrollbar = supportUtils.touch ? 'always' : 'onHover'; return result; } - protected override createTooltip(dataList) { + protected override createTooltip( + dataList: AppointmentTooltipItem[], + ): Tooltip { const tooltipElement = this.createTooltipElement(APPOINTMENT_TOOLTIP_WRAPPER_CLASS); const tooltip = this._options.createComponent(tooltipElement, Tooltip, { target: this.$target, maxHeight: MAX_TOOLTIP_HEIGHT, - rtlEnabled: this.extraOptions.rtlEnabled, + rtlEnabled: this.extraOptions?.rtlEnabled, onShown: this.onShown.bind(this), contentTemplate: this.getContentTemplate(dataList), wrapperAttr: { class: APPOINTMENT_TOOLTIP_WRAPPER_CLASS }, - tabFocusLoopEnabled: this.extraOptions.tabFocusLoopEnabled, - }); + tabFocusLoopEnabled: this.extraOptions?.tabFocusLoopEnabled, + }) as Tooltip; tooltip.setAria({ role: 'dialog', @@ -61,11 +68,17 @@ export class DesktopTooltipStrategy extends TooltipStrategyBase { return tooltip; } - protected override onListRender(e) { - return this.extraOptions.dragBehavior && this.extraOptions.dragBehavior(e); + protected override onListRender( + e: ContentReadyEvent, + ): void { + if (this.extraOptions?.dragBehavior) { + this.extraOptions.dragBehavior(e); + } } - protected override onListItemContextMenu(e) { + protected override onListItemContextMenu( + e: ItemContextMenuEvent, + ): void { const contextMenuEventArgs = this._options.createEventArgs(e); this._options.onItemContextMenu(contextMenuEventArgs); } diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts deleted file mode 100644 index 454890a61e11..000000000000 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts +++ /dev/null @@ -1,284 +0,0 @@ -import type { dxElementWrapper } from '@js/core/renderer'; -import $ from '@js/core/renderer'; -import { FunctionTemplate } from '@js/core/templates/function_template'; -import { isRenderer } from '@js/core/utils/type'; -import Button from '@js/ui/button'; -import { createPromise } from '@ts/core/utils/promise'; -import List from '@ts/ui/list/list.edit'; - -import type { CompactAppointmentOptions } from '../types'; - -const TOOLTIP_APPOINTMENT_ITEM = 'dx-tooltip-appointment-item'; -const TOOLTIP_APPOINTMENT_ITEM_CONTENT = `${TOOLTIP_APPOINTMENT_ITEM}-content`; -const TOOLTIP_APPOINTMENT_ITEM_CONTENT_SUBJECT = `${TOOLTIP_APPOINTMENT_ITEM}-content-subject`; -const TOOLTIP_APPOINTMENT_ITEM_CONTENT_DATE = `${TOOLTIP_APPOINTMENT_ITEM}-content-date`; -const TOOLTIP_APPOINTMENT_ITEM_MARKER = `${TOOLTIP_APPOINTMENT_ITEM}-marker`; -const TOOLTIP_APPOINTMENT_ITEM_MARKER_BODY = `${TOOLTIP_APPOINTMENT_ITEM}-marker-body`; - -const TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON_CONTAINER = `${TOOLTIP_APPOINTMENT_ITEM}-delete-button-container`; -const TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON = `${TOOLTIP_APPOINTMENT_ITEM}-delete-button`; - -const APPOINTMENT_TOOLTIP_TEMPLATE = 'appointmentTooltipTemplate'; - -export class TooltipStrategyBase { - protected asyncTemplatePromises = new Set>(); - - protected tooltip: any; - - // TODO: make private once external usages in m_scheduler.ts are removed - _options: any; - - protected extraOptions: any; - - protected list: any; - - protected $target: dxElementWrapper | null = null; - - constructor(options) { - this.tooltip = null; - this._options = options; - this.extraOptions = null; - } - - show(target: dxElementWrapper, dataList, extraOptions) { - if (dataList.length) { - this.hide(); - this.$target = target; - this.extraOptions = extraOptions; - this.showCore(dataList); - } - } - - public setTarget($target: dxElementWrapper): void { - this.$target = $target; - - if (this.isDesktop()) { - const originalAnimationValue = this.tooltip.option('animation'); - - this.tooltip.option('animation', null); - this.tooltip.option('target', $target); - this.tooltip.option('animation', originalAnimationValue); - } - } - - public getTarget(): dxElementWrapper | null { - return this.$target; - } - - public setListItems(dataList: CompactAppointmentOptions['items']): void { - if (dataList.length === 0) { - this.hide(); - } - - this.list.option('dataSource', dataList); - } - - private showCore(dataList) { - const describedByValue = isRenderer(this.$target) && this.$target?.attr('aria-describedby') as string; - - if (!this.tooltip) { - this.tooltip = this.createTooltip(dataList); - } else { - if (this.isDesktop()) { - this.tooltip.option('target', this.$target); - } - - this.list.option('dataSource', dataList); - } - - this.prepareBeforeVisibleChanged(dataList); - this.tooltip.option('visible', true); - - describedByValue && this.$target?.attr('aria-describedby', describedByValue); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected prepareBeforeVisibleChanged(dataList) { - - } - - private isDeletingAllowed(appointment) { - const { editing } = this.extraOptions; - const disabled = this._options.getAppointmentDisabled(appointment); - const isDeletingAllowed = editing === true || editing?.allowDeleting === true; - return !disabled && isDeletingAllowed; - } - - protected getContentTemplate(dataList) { - return (container) => { - const listElement = $('
'); - $(container).append(listElement); - this.list = this.createList(listElement, dataList); - this.list.registerKeyHandler?.('escape', () => { - this.hide(); - this.tooltip.option('target').focus(); - }); - this.list.registerKeyHandler?.('del', () => { - const { focusedElement } = this.list.option(); - if (!focusedElement) { - return; - } - - const { appointment, targetedAppointment } = this.list._getItemData(focusedElement); - if (!appointment) { - return; - } - - if (this.isDeletingAllowed(appointment)) { - this._options.checkAndDeleteAppointment(appointment, targetedAppointment); - } - }); - }; - } - - isShownForTarget($target: dxElementWrapper): boolean { - if (!this.tooltip?.option('visible')) { - return false; - } - - return $target.get(0) === this.$target?.get(0); - } - - protected onShown() { - this.list.option('focusStateEnabled', this.extraOptions.focusStateEnabled); - } - - dispose() { - } - - hide() { - if (this.tooltip) { - this.tooltip.option('visible', false); - } - } - - protected isDesktop(): boolean { - return true; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected createTooltip(dataList) { - } - - protected createListOption(dataList) { - return { - dataSource: dataList, - onContentReady: this.onListRender.bind(this), - onItemClick: (e) => this.onListItemClick(e), - onItemContextMenu: this.onListItemContextMenu.bind(this), - itemTemplate: (item, index) => this.renderTemplate(item.appointment, item.targetedAppointment, index, item.color), - pageLoadMode: 'scrollBottom', - }; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected onListRender(e) { } - - protected createTooltipElement(wrapperClass) { - return $('
').appendTo(this._options.container).addClass(wrapperClass); - } - - private createList(listElement, dataList) { - return this._options.createComponent(listElement, List, this.createListOption(dataList)); - } - - private renderTemplate(appointment, targetedAppointment, index, color) { - const itemListContent = this.createItemListContent(appointment, targetedAppointment, color); - this._options.addDefaultTemplates({ - // @ts-expect-error - appointmentTooltip: new FunctionTemplate((options) => { - const $container = $(options.container); - $container.append(itemListContent); - return $container; - }), - }); - - const template = this._options.getAppointmentTemplate(APPOINTMENT_TOOLTIP_TEMPLATE); - return this.createFunctionTemplate(template, appointment, targetedAppointment, index); - } - - private createFunctionTemplate(template, appointmentData, targetedAppointmentData, index) { - const isButtonClicked = Boolean(this.extraOptions.isButtonClick); - - // @ts-expect-error - return new FunctionTemplate((options) => { - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - const { promise, resolve } = createPromise(); - this.asyncTemplatePromises.add(promise); - return template.render({ - model: { - appointmentData, - targetedAppointmentData, - isButtonClicked, - }, - container: options.container, - index, - onRendered: () => { - this.asyncTemplatePromises.delete(promise); - resolve(); - }, - }); - }); - } - - private onListItemClick(e) { - this.hide(); - this.extraOptions.clickEvent && this.extraOptions.clickEvent(e); - this._options.showAppointmentPopup(e.itemData.appointment, false, e.itemData.targetedAppointment); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected onListItemContextMenu(e) { } - - private createItemListContent(appointment, targetedAppointment, color) { - const $itemElement = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM); - $itemElement.append(this.createItemListMarker(color)); - $itemElement.append(this.createItemListInfo(this._options.createFormattedDateText(appointment, targetedAppointment))); - - if (this.isDeletingAllowed(appointment)) { - $itemElement.append(this.createDeleteButton(appointment, targetedAppointment)); - } - - return $itemElement; - } - - private createItemListMarker(color) { - const $marker = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_MARKER); - const $markerBody = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_MARKER_BODY); - - $marker.append($markerBody); - color.then((value) => { - if (value) { - $markerBody.css('background', value); - } - }); - - return $marker; - } - - private createItemListInfo(object) { - const result = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT); - const $title = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT_SUBJECT).text(object.text); - const $date = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT_DATE).text(object.formatDate); - - return result.append($title).append($date); - } - - private createDeleteButton(appointment, targetedAppointment) { - const $container = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON_CONTAINER); - const $deleteButton = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON); - - $container.append($deleteButton); - this._options.createComponent($deleteButton, Button, { - icon: 'trash', - stylingMode: 'text', - tabIndex: -1, - onClick: (e) => { - e.event.stopPropagation(); - this._options.checkAndDeleteAppointment(appointment, targetedAppointment); - }, - }); - - return $container; - } -} diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_mobile_tooltip_strategy.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/mobile_tooltip_strategy.ts similarity index 77% rename from packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_mobile_tooltip_strategy.ts rename to packages/devextreme/js/__internal/scheduler/tooltip_strategies/mobile_tooltip_strategy.ts index cd6199c15388..565e5d974e17 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_mobile_tooltip_strategy.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/mobile_tooltip_strategy.ts @@ -1,8 +1,11 @@ import { getHeight, getOuterHeight, getWidth } from '@js/core/utils/size'; import { getWindow } from '@js/core/utils/window'; +import type dxOverlay from '@js/ui/overlay'; +import type { Properties as OverlayProperties } from '@js/ui/overlay'; import Overlay from '@js/ui/overlay/ui.overlay'; -import { TooltipStrategyBase } from './m_tooltip_strategy_base'; +import type { AppointmentTooltipItem } from '../types'; +import { TooltipStrategyBase } from './tooltip_strategy_base'; const CLASS = { slidePanel: 'dx-scheduler-overlay-panel', @@ -22,7 +25,7 @@ const MAX_WIDTH = { TABLET: '80%', }; -const animationConfig = { +const animationConfig: OverlayProperties['animation'] = { show: { type: 'slide', duration: 300, @@ -37,7 +40,7 @@ const animationConfig = { }, }; -const createPhoneDeviceConfig = (listHeight) => ({ +const createPhoneDeviceConfig = (listHeight: number): OverlayProperties => ({ shading: false, width: MAX_WIDTH.PHONE, height: listHeight > MAX_HEIGHT.PHONE ? MAX_HEIGHT.PHONE : MAX_HEIGHT.DEFAULT, @@ -48,7 +51,7 @@ const createPhoneDeviceConfig = (listHeight) => ({ }, }); -const createTabletDeviceConfig = (listHeight) => { +const createTabletDeviceConfig = (listHeight: number): OverlayProperties => { const currentMaxHeight = getHeight(getWindow()) * MAX_TABLET_OVERLAY_HEIGHT_FACTOR; return { @@ -64,7 +67,7 @@ const createTabletDeviceConfig = (listHeight) => { }; export class MobileTooltipStrategy extends TooltipStrategyBase { - protected override isDesktop() { + protected override isDesktop(): boolean { return false; } @@ -72,7 +75,7 @@ export class MobileTooltipStrategy extends TooltipStrategyBase { const isTabletWidth = getWidth(getWindow()) > 700; const listHeight = getOuterHeight(this.list.$element().find(CLASS.scrollableContent)); - this.tooltip.option( + this.tooltip?.option( isTabletWidth ? createTabletDeviceConfig(listHeight) : createPhoneDeviceConfig(listHeight), @@ -80,7 +83,7 @@ export class MobileTooltipStrategy extends TooltipStrategyBase { } private async onShowing(): Promise { - this.tooltip.option('height', MAX_HEIGHT.DEFAULT); + this.tooltip?.option('height', MAX_HEIGHT.DEFAULT); /* NOTE: there are two setTooltipConfig calls to reduce blinking of overlay. The first one sets initial sizes, the second updates them after rendering async templates @@ -91,7 +94,9 @@ export class MobileTooltipStrategy extends TooltipStrategyBase { this.setTooltipConfig(); } - protected override createTooltip(dataList) { + protected override createTooltip( + dataList: AppointmentTooltipItem[], + ): dxOverlay { const element = this.createTooltipElement(CLASS.slidePanel); return this._options.createComponent(element, Overlay, { @@ -103,6 +108,6 @@ export class MobileTooltipStrategy extends TooltipStrategyBase { onShown: this.onShown.bind(this), contentTemplate: this.getContentTemplate(dataList), wrapperAttr: { class: CLASS.slidePanel }, - }); + }) as dxOverlay; } } 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 new file mode 100644 index 000000000000..9e52fb916cfc --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts @@ -0,0 +1,398 @@ +import type { DxElement } from '@js/core/element'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { FunctionTemplate } from '@js/core/templates/function_template'; +import { isRenderer } from '@js/core/utils/type'; +import type { ClickEvent as ButtonClickEvent } from '@js/ui/button'; +import Button from '@js/ui/button'; +import type { + ContentReadyEvent, + ItemClickEvent, + ItemContextMenuEvent, + Properties as ListProperties, +} from '@js/ui/list'; +import type dxOverlay from '@js/ui/overlay'; +import type { Properties as OverlayProperties } from '@js/ui/overlay'; +import type { Appointment, Properties as SchedulerProperties } from '@js/ui/scheduler'; +import { createPromise } from '@ts/core/utils/promise'; +import List from '@ts/ui/list/list.edit'; +import type Tooltip from '@ts/ui/m_tooltip'; + +import type { AppointmentTooltipItem, TargetedAppointment } from '../types'; + +const TOOLTIP_APPOINTMENT_ITEM = 'dx-tooltip-appointment-item'; +const TOOLTIP_APPOINTMENT_ITEM_CONTENT = `${TOOLTIP_APPOINTMENT_ITEM}-content`; +const TOOLTIP_APPOINTMENT_ITEM_CONTENT_SUBJECT = `${TOOLTIP_APPOINTMENT_ITEM}-content-subject`; +const TOOLTIP_APPOINTMENT_ITEM_CONTENT_DATE = `${TOOLTIP_APPOINTMENT_ITEM}-content-date`; +const TOOLTIP_APPOINTMENT_ITEM_MARKER = `${TOOLTIP_APPOINTMENT_ITEM}-marker`; +const TOOLTIP_APPOINTMENT_ITEM_MARKER_BODY = `${TOOLTIP_APPOINTMENT_ITEM}-marker-body`; + +const TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON_CONTAINER = `${TOOLTIP_APPOINTMENT_ITEM}-delete-button-container`; +const TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON = `${TOOLTIP_APPOINTMENT_ITEM}-delete-button`; + +const APPOINTMENT_TOOLTIP_TEMPLATE = 'appointmentTooltipTemplate'; + +interface AppointmentTooltipOptions { + createComponent: ( + element: dxElementWrapper, + component: unknown, + options: unknown, + ) => unknown; + container: dxElementWrapper; + getScrollableContainer: () => dxElementWrapper; + addDefaultTemplates: (templates: Record) => void; + getAppointmentTemplate: (optionName: string) => FunctionTemplate; + showAppointmentPopup: ( + appointment: Appointment, + createNewAppointment: boolean, + targetedAppointment?: Appointment | TargetedAppointment, + ) => void; + checkAndDeleteAppointment: ( + appointment: Appointment, + targetedAppointment?: Appointment | TargetedAppointment, + ) => void; + isAppointmentInAllDayPanel: (appointment: Appointment) => boolean; + createFormattedDateText: ( + appointment: Appointment, + targetedAppointment?: Appointment | TargetedAppointment, + format?: string, + ) => { + text: string; + formatDate: string; + }; + getAppointmentDisabled: (appointment: Appointment) => boolean | undefined; + onItemContextMenu: (eventArgs: unknown) => void; + createEventArgs: (e: ItemContextMenuEvent) => unknown; +} + +interface AppointmentTooltipExtraOptions { + clickEvent?: (e: ItemClickEvent) => void; + dragBehavior?: (e: ContentReadyEvent) => void; + editing?: SchedulerProperties['editing']; + focusStateEnabled?: boolean; + isButtonClick?: boolean; + offset?: unknown; + rtlEnabled?: boolean; + tabFocusLoopEnabled?: boolean; +} + +export abstract class TooltipStrategyBase { + protected asyncTemplatePromises = new Set>(); + + protected tooltip: Tooltip | dxOverlay | null = null; + + // TODO: make private once external usages in m_scheduler.ts are removed + _options: AppointmentTooltipOptions; + + protected extraOptions: AppointmentTooltipExtraOptions | null = null; + + protected list!: List; + + protected $target: dxElementWrapper | null = null; + + constructor(options: AppointmentTooltipOptions) { + this._options = options; + } + + show( + target: dxElementWrapper, + dataList: AppointmentTooltipItem[], + extraOptions: AppointmentTooltipExtraOptions, + ): void { + if (dataList.length) { + this.hide(); + this.$target = target; + this.extraOptions = extraOptions; + this.showCore(dataList); + } + } + + public setTarget($target: dxElementWrapper): void { + this.$target = $target; + + if (this.isDesktop() && this.tooltip) { + const originalAnimationValue = this.tooltip.option('animation'); + + this.tooltip.option('animation', null); + this.tooltip.option('target', $target); + this.tooltip.option('animation', originalAnimationValue); + } + } + + public getTarget(): dxElementWrapper | null { + return this.$target; + } + + public setListItems(dataList: AppointmentTooltipItem[]): void { + if (dataList.length === 0) { + this.hide(); + } + + this.list.option('dataSource', dataList); + } + + private showCore(dataList: AppointmentTooltipItem[]): void { + const { $target } = this; + const describedByValue = $target && isRenderer($target) + ? $target.attr('aria-describedby') + : undefined; + + if (!this.tooltip) { + this.tooltip = this.createTooltip(dataList); + } else { + if (this.isDesktop()) { + this.tooltip.option('target', this.$target); + } + + this.list.option('dataSource', dataList); + } + + this.prepareBeforeVisibleChanged(dataList); + this.tooltip.option('visible', true); + + if (describedByValue) { + this.$target?.attr('aria-describedby', describedByValue); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected prepareBeforeVisibleChanged(dataList: AppointmentTooltipItem[]): void { + + } + + private isDeletingAllowed(appointment: Appointment): boolean { + const { editing } = this.extraOptions ?? {}; + const disabled = this._options.getAppointmentDisabled(appointment); + const isDeletingAllowed = editing === true + || (typeof editing === 'object' && editing.allowDeleting === true); + return !disabled && isDeletingAllowed; + } + + protected getContentTemplate(dataList: AppointmentTooltipItem[]) { + return (container: DxElement): void => { + const listElement = $('
'); + $(container).append(listElement); + this.list = this.createList(listElement, dataList); + this.list.registerKeyHandler?.('escape', (): void => { + this.hide(); + (this.getTarget()?.get(0) as HTMLElement | undefined)?.focus(); + }); + this.list.registerKeyHandler?.('del', (): void => { + const { focusedElement } = this.list.option(); + if (!focusedElement) { + return; + } + + // @ts-expect-error + const { appointment, targetedAppointment } = this.list._getItemData(focusedElement); + if (!appointment) { + return; + } + + if (this.isDeletingAllowed(appointment)) { + this._options.checkAndDeleteAppointment(appointment, targetedAppointment); + } + }); + }; + } + + isShownForTarget($target: dxElementWrapper): boolean { + if (!this.tooltip?.option('visible')) { + return false; + } + + return $target.get(0) === this.$target?.get(0); + } + + protected onShown(): void { + this.list.option('focusStateEnabled', this.extraOptions?.focusStateEnabled); + } + + dispose(): void { + } + + hide(): void { + if (this.tooltip) { + this.tooltip.option('visible', false); + } + } + + protected isDesktop(): boolean { + return true; + } + + protected abstract createTooltip( + dataList: AppointmentTooltipItem[], + ): Tooltip | dxOverlay; + + protected createListOption( + dataList: AppointmentTooltipItem[], + ): ListProperties { + return { + dataSource: dataList, + onContentReady: this.onListRender.bind(this), + onItemClick: ( + e: ItemClickEvent, + ): void => this.onListItemClick(e), + onItemContextMenu: this.onListItemContextMenu.bind(this), + itemTemplate: ( + item: AppointmentTooltipItem, + index: number, + ): FunctionTemplate => this.renderTemplate( + item.appointment, + item.targetedAppointment, + index, + item.color, + ), + pageLoadMode: 'scrollBottom', + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected onListRender(e: ContentReadyEvent): void { } + + protected createTooltipElement(wrapperClass: string): dxElementWrapper { + return $('
').appendTo(this._options.container).addClass(wrapperClass); + } + + private createList( + listElement: dxElementWrapper, + dataList: AppointmentTooltipItem[], + ): List { + return this._options.createComponent( + listElement, + List, + this.createListOption(dataList), + ) as List; + } + + private renderTemplate( + appointment: Appointment, + targetedAppointment: Appointment | TargetedAppointment | undefined, + index: number, + color: Promise, + ): FunctionTemplate { + const itemListContent = this.createItemListContent(appointment, targetedAppointment, color); + this._options.addDefaultTemplates({ + appointmentTooltip: new FunctionTemplate( + // @ts-expect-error + (options: { container: DxElement }): dxElementWrapper => { + const $container = $(options.container); + $container.append(itemListContent); + return $container; + }, + ), + }); + + const template = this._options.getAppointmentTemplate(APPOINTMENT_TOOLTIP_TEMPLATE); + return this.createFunctionTemplate(template, appointment, targetedAppointment, index); + } + + private createFunctionTemplate( + template: FunctionTemplate, + appointmentData: Appointment, + targetedAppointmentData: Appointment | TargetedAppointment | undefined, + index: number, + ): FunctionTemplate { + const isButtonClicked = Boolean(this.extraOptions?.isButtonClick); + + // @ts-expect-error + return new FunctionTemplate((options: { container: DxElement }): DxElement => { + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + const { promise, resolve } = createPromise(); + this.asyncTemplatePromises.add(promise); + return template.render({ + model: { + appointmentData, + targetedAppointmentData, + isButtonClicked, + }, + container: options.container, + // @ts-expect-error + index, + onRendered: () => { + this.asyncTemplatePromises.delete(promise); + resolve(); + }, + }); + }); + } + + private onListItemClick(e: ItemClickEvent): void { + if (!e.itemData) { + return; + } + + this.hide(); + this.extraOptions?.clickEvent?.(e); + this._options.showAppointmentPopup( + e.itemData.appointment, + false, + e.itemData.targetedAppointment, + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected onListItemContextMenu(e: ItemContextMenuEvent): void { } + + private createItemListContent( + appointment: Appointment, + targetedAppointment: Appointment | TargetedAppointment | undefined, + color: Promise, + ): dxElementWrapper { + const $itemElement = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM); + $itemElement.append(this.createItemListMarker(color)); + $itemElement.append(this.createItemListInfo( + this._options.createFormattedDateText(appointment, targetedAppointment), + )); + + if (this.isDeletingAllowed(appointment)) { + $itemElement.append(this.createDeleteButton(appointment, targetedAppointment)); + } + + return $itemElement; + } + + private createItemListMarker(color: Promise): dxElementWrapper { + const $marker = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_MARKER); + const $markerBody = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_MARKER_BODY); + + $marker.append($markerBody); + color.then((value: string | undefined): void => { + if (value) { + $markerBody.css('background', value); + } + }, () => {}); + + return $marker; + } + + private createItemListInfo(object: { text: string; formatDate: string }): dxElementWrapper { + const result = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT); + const $title = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT_SUBJECT).text(object.text); + const $date = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_CONTENT_DATE).text(object.formatDate); + + return result.append($title).append($date); + } + + private createDeleteButton( + appointment: Appointment, + targetedAppointment: Appointment | TargetedAppointment | undefined, + ): dxElementWrapper { + const $container = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON_CONTAINER); + const $deleteButton = $('
').addClass(TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON); + + $container.append($deleteButton); + this._options.createComponent($deleteButton, Button, { + icon: 'trash', + stylingMode: 'text', + tabIndex: -1, + onClick: (e: ButtonClickEvent): void => { + e.event?.stopPropagation(); + this._options.checkAndDeleteAppointment(appointment, targetedAppointment); + }, + }); + + return $container; + } +} diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js index 152a78021cac..5326f4b556ab 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js @@ -1,4 +1,4 @@ -import { DesktopTooltipStrategy } from '__internal/scheduler/tooltip_strategies/m_desktop_tooltip_strategy'; +import { DesktopTooltipStrategy } from '__internal/scheduler/tooltip_strategies/desktop_tooltip_strategy'; import { FunctionTemplate } from 'core/templates/function_template'; import { extend } from 'core/utils/extend'; import Tooltip from 'ui/tooltip';