Skip to content

Commit 19c3224

Browse files
authored
Scheduler - Appointments Collection - Refactor KBN (DevExpress#32496)
1 parent 960718c commit 19c3224

File tree

2 files changed

+163
-106
lines changed

2 files changed

+163
-106
lines changed

packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts

Lines changed: 44 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import { name as dblclickEvent } from '@js/common/core/events/double_click';
55
import { addNamespace, isFakeClickEvent } from '@js/common/core/events/utils/index';
66
import registerComponent from '@js/core/component_registrator';
77
import domAdapter from '@js/core/dom_adapter';
8-
import { getPublicElement } from '@js/core/element';
9-
import { data as elementData } from '@js/core/element_data';
108
import type { dxElementWrapper } from '@js/core/renderer';
119
import $ from '@js/core/renderer';
1210
// @ts-expect-error
@@ -21,8 +19,9 @@ import { setOuterHeight, setOuterWidth } from '@js/core/utils/size';
2119
import {
2220
isDeferred, isDefined, isPlainObject, isString,
2321
} from '@js/core/utils/type';
24-
import CollectionWidget from '@js/ui/collection/ui.collection_widget.edit';
2522
import { dateUtilsTs } from '@ts/core/utils/date';
23+
import type { SupportedKeys } from '@ts/core/widget/widget';
24+
import CollectionWidget from '@ts/ui/collection/collection_widget.edit';
2625

2726
import { APPOINTMENT_SETTINGS_KEY } from '../constants';
2827
import { APPOINTMENT_CONTENT_CLASSES, APPOINTMENT_DRAG_SOURCE_CLASS, APPOINTMENT_ITEM_CLASS } from '../m_classes';
@@ -47,12 +46,12 @@ import type {
4746
import { AgendaAppointment } from './appointment/agenda_appointment';
4847
import { Appointment } from './appointment/m_appointment';
4948
import { createAgendaAppointmentLayout, createAppointmentLayout } from './m_appointment_layout';
49+
import { AppointmentsKeyboardNavigation } from './m_appointments_kbn';
5050
import { getAppointmentDateRange } from './resizing/m_core';
5151
import { countVisibleAppointments } from './utils/count_visible_appointments';
5252
import { isNeedToAdd } from './utils/get_arrays_diff';
5353
import { getViewModelDiff } from './utils/get_view_model_diff';
5454
import { getAppointmentTakesSeveralDays, sortAppointmentsByStartDate } from './utils/m_utils';
55-
import { getNextElement, getPrevElement } from './utils/sorted_index_utils';
5655

5756
const COMPONENT_CLASS = 'dx-scheduler-scrollable-appointments';
5857

@@ -67,15 +66,12 @@ interface ViewModelDiff {
6766
needToRemove?: true;
6867
}
6968

70-
// @ts-expect-error
71-
class SchedulerAppointments extends CollectionWidget {
69+
class SchedulerAppointments extends CollectionWidget<any> {
7270
// NOTE: The key of this array is `sortedIndex` of appointment rendered in Element
7371
renderedElementsBySortedIndex: dxElementWrapper[] = [];
7472

7573
_appointmentClickTimeout: any;
7674

77-
_$currentAppointment: any;
78-
7975
_currentAppointmentSettings?: AppointmentViewModelPlain;
8076

8177
_preventSingleAppointmentClick: any;
@@ -84,6 +80,14 @@ class SchedulerAppointments extends CollectionWidget {
8480

8581
_initialCoordinates: any;
8682

83+
private _kbn!: AppointmentsKeyboardNavigation;
84+
85+
private _isResizing = false;
86+
87+
public get isResizing(): boolean {
88+
return this._isResizing;
89+
}
90+
8791
get isAgendaView() {
8892
return this.invoke('isCurrentViewAgenda');
8993
}
@@ -134,97 +138,35 @@ class SchedulerAppointments extends CollectionWidget {
134138
super._dispose();
135139
}
136140

137-
_supportedKeys() {
138-
const parent = super._supportedKeys();
139-
140-
const tabHandler = function (e) {
141-
const navigatableItems = this._getNavigatableItems();
142-
const focusedItem = navigatableItems.filter('.dx-state-focused');
143-
let index = focusedItem.data(APPOINTMENT_SETTINGS_KEY).sortedIndex;
144-
let $nextAppointment = e.shiftKey
145-
? getPrevElement(index, this.renderedElementsBySortedIndex)
146-
: getNextElement(index, this.renderedElementsBySortedIndex);
147-
const lastIndex = navigatableItems.length - 1;
148-
149-
if ($nextAppointment || (index > 0 && e.shiftKey) || (index < lastIndex && !e.shiftKey)) {
150-
e.preventDefault();
151-
152-
if (!$nextAppointment) {
153-
e.shiftKey ? index-- : index++;
154-
$nextAppointment = this._getNavigatableItemByIndex(index);
155-
}
156-
157-
this._resetTabIndex($nextAppointment);
158-
// @ts-expect-error
159-
eventsEngine.trigger($nextAppointment, 'focus');
160-
}
161-
};
162-
163-
const currentAppointment = this._$currentAppointment;
164-
165-
return extend(parent, {
166-
escape: function () {
167-
if (this.resizeOccur) {
168-
this.moveAppointmentBack();
169-
this.resizeOccur = false;
170-
currentAppointment.dxResizable('instance')?._detachEventHandlers();
171-
currentAppointment.dxResizable('instance')?._attachEventHandlers();
172-
currentAppointment.dxResizable('instance')?._toggleResizingClass(false);
173-
}
174-
}.bind(this),
175-
del: function (e) {
176-
if (this.option('allowDelete')) {
177-
e.preventDefault();
178-
const data = this._getItemData(e.target);
179-
this.notifyObserver('onDeleteButtonPress', { data, target: e.target });
180-
}
181-
}.bind(this),
182-
tab: tabHandler,
183-
});
184-
}
185-
186-
private _getNavigatableItemByIndex(sortedIndex) {
187-
const appointments = this._getNavigatableItems();
188-
return appointments.filter(
189-
// @ts-expect-error
190-
(_, $item) => elementData($item, APPOINTMENT_SETTINGS_KEY).sortedIndex === sortedIndex,
191-
).eq(0);
192-
}
141+
_supportedKeys(): SupportedKeys {
142+
const parentValue = super._supportedKeys();
143+
const kbnValue = this._kbn.getSupportedKeys();
193144

194-
private _getNavigatableItems(): dxElementWrapper {
195-
// @ts-expect-error
196-
const appts = this._itemElements().not('.dx-state-disabled');
197-
// @ts-expect-error
198-
const apptCollectors = this.$element().find('.dx-scheduler-appointment-collector');
199-
return appts.add(apptCollectors);
145+
return extend(parentValue, kbnValue) as SupportedKeys;
200146
}
201147

202-
_resetTabIndex($appointment) {
203-
this._focusTarget().attr('tabIndex', -1);
204-
$appointment.attr('tabIndex', this.option('tabIndex'));
148+
public getAppointmentSettings($item: dxElementWrapper): AppointmentViewModelPlain {
149+
return $item.data(APPOINTMENT_SETTINGS_KEY) as unknown as AppointmentViewModelPlain;
205150
}
206151

207152
_moveFocus() {}
208153

209154
_focusTarget() {
210-
return this._getNavigatableItems();
155+
return this._kbn.getFocusableItems();
211156
}
212157

213158
_renderFocusTarget() {
214-
const $appointment = this._getNavigatableItemByIndex(0);
215-
216-
this._resetTabIndex($appointment);
159+
const $item = this._kbn.getFocusableItemBySortedIndex(0);
160+
this._kbn.resetTabIndex($item);
217161
}
218162

219163
_focusInHandler(e) {
220164
super._focusInHandler(e);
221-
this._$currentAppointment = $(e.target);
222-
this.option('focusedElement', getPublicElement($(e.target)));
165+
this._kbn.focusInHandler(e);
223166
}
224167

225168
_focusOutHandler(e) {
226-
const $appointment = this._getNavigatableItemByIndex(0);
227-
this.option('focusedElement', getPublicElement($appointment));
169+
this._kbn.focusOutHandler();
228170
super._focusOutHandler(e);
229171
}
230172

@@ -291,7 +233,7 @@ class SchedulerAppointments extends CollectionWidget {
291233
this._attachAppointmentsEvents();
292234
break;
293235
case 'focusedElement':
294-
this._resetTabIndex($(args.value));
236+
this._kbn.resetTabIndex($(args.value));
295237
super._optionChanged(args);
296238
break;
297239
case 'allowDelete':
@@ -450,15 +392,9 @@ class SchedulerAppointments extends CollectionWidget {
450392
}
451393
}
452394

453-
_clean() {
454-
super._clean();
455-
delete this._$currentAppointment;
456-
delete this._initialSize;
457-
delete this._initialCoordinates;
458-
}
459-
460395
_init() {
461396
super._init();
397+
this._kbn = new AppointmentsKeyboardNavigation(this);
462398
(this as any).$element().addClass(COMPONENT_CLASS);
463399
this._preventSingleAppointmentClick = false;
464400
}
@@ -730,24 +666,29 @@ class SchedulerAppointments extends CollectionWidget {
730666
_resizableConfig(appointmentData, itemSetting) {
731667
return {
732668
area: this._calculateResizableArea(itemSetting, appointmentData),
733-
onResizeStart: function (e) {
734-
this.resizeOccur = true;
735-
this._$currentAppointment = $(e.element);
669+
onResizeStart: (e) => {
670+
const $appointment = $(e.element);
671+
672+
this._isResizing = true;
673+
this._kbn.$focusedItem = $appointment;
736674

737675
if (this.invoke('needRecalculateResizableArea')) {
738-
const updatedArea = this._calculateResizableArea(this._$currentAppointment.data(APPOINTMENT_SETTINGS_KEY), this._$currentAppointment.data('dxItemData'));
676+
const updatedArea = this._calculateResizableArea(
677+
this.getAppointmentSettings($appointment),
678+
$appointment.data('dxItemData'),
679+
);
739680

740681
e.component.option('area', updatedArea);
741682
e.component._renderDragOffsets(e.event);
742683
}
743684

744685
this._initialSize = { width: e.width, height: e.height };
745-
this._initialCoordinates = locate(this._$currentAppointment);
746-
}.bind(this),
747-
onResizeEnd: function (e) {
748-
this.resizeOccur = false;
686+
this._initialCoordinates = locate($appointment);
687+
},
688+
onResizeEnd: (e) => {
689+
this._isResizing = false;
749690
this._resizeEndHandler(e);
750-
}.bind(this),
691+
},
751692
};
752693
}
753694

@@ -1160,11 +1101,13 @@ class SchedulerAppointments extends CollectionWidget {
11601101
return obj;
11611102
}
11621103

1163-
moveAppointmentBack(dragEvent) {
1164-
const $appointment = this._$currentAppointment;
1104+
moveAppointmentBack(dragEvent?) {
1105+
const $appointment = this._kbn.$focusedItem;
11651106
const size = this._initialSize;
11661107
const coords = this._initialCoordinates;
11671108

1109+
this._isResizing = false;
1110+
11681111
if (dragEvent) {
11691112
this._removeDragSourceClassFromDraggedAppointment();
11701113

@@ -1189,12 +1132,7 @@ class SchedulerAppointments extends CollectionWidget {
11891132
}
11901133

11911134
focus() {
1192-
if (this._$currentAppointment) {
1193-
const focusedElement = getPublicElement(this._$currentAppointment);
1194-
1195-
this.option('focusedElement', focusedElement);
1196-
(eventsEngine as any).trigger(focusedElement, 'focus');
1197-
}
1135+
this._kbn.focus();
11981136
}
11991137

12001138
splitAppointmentByDay(appointment) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { dxElementWrapper } from '@js/core/renderer';
2+
import $ from '@js/core/renderer';
3+
import type { DxEvent } from '@js/events/events.types';
4+
import { getPublicElement } from '@ts/core/m_element';
5+
import type { SupportedKeys } from '@ts/core/widget/widget';
6+
import eventsEngine from '@ts/events/core/m_events_engine';
7+
8+
import type SchedulerAppointments from './m_appointment_collection';
9+
import { getNextElement, getPrevElement } from './utils/sorted_index_utils';
10+
11+
export class AppointmentsKeyboardNavigation {
12+
private readonly _collection: SchedulerAppointments;
13+
14+
public $focusedItem: dxElementWrapper | null = null;
15+
16+
constructor(collection: SchedulerAppointments) {
17+
this._collection = collection;
18+
}
19+
20+
public getFocusableItems(): dxElementWrapper {
21+
const appts = this._collection._itemElements().not('.dx-state-disabled');
22+
const collectors = this._collection.$element().find('.dx-scheduler-appointment-collector');
23+
24+
// @ts-expect-error
25+
return appts.add(collectors);
26+
}
27+
28+
public getFocusableItemBySortedIndex(sortedIndex: number): dxElementWrapper {
29+
const $items = this.getFocusableItems();
30+
const itemElement = $items.toArray().filter((itemElement: Element) => {
31+
const $item = $(itemElement);
32+
const itemData = this._collection.getAppointmentSettings($item);
33+
return itemData.sortedIndex === sortedIndex;
34+
});
35+
36+
return $(itemElement);
37+
}
38+
39+
public focus(): void {
40+
if (this.$focusedItem) {
41+
const focusedElement = getPublicElement(this.$focusedItem);
42+
43+
this._collection.option('focusedElement', focusedElement);
44+
eventsEngine.trigger(focusedElement, 'focus');
45+
}
46+
}
47+
48+
public focusInHandler(e: DxEvent): void {
49+
this.$focusedItem = $(e.target);
50+
this._collection.option('focusedElement', getPublicElement(this.$focusedItem));
51+
}
52+
53+
public focusOutHandler(): void {
54+
const $item = this.getFocusableItemBySortedIndex(0);
55+
this._collection.option('focusedElement', getPublicElement($item));
56+
}
57+
58+
public getSupportedKeys(): SupportedKeys {
59+
return {
60+
escape: this.escHandler.bind(this),
61+
del: this.delHandler.bind(this),
62+
tab: this.tabHandler.bind(this),
63+
};
64+
}
65+
66+
public resetTabIndex($appointment: dxElementWrapper): void {
67+
this.getFocusableItems().attr('tabIndex', -1);
68+
$appointment.attr('tabIndex', this._collection.option('tabIndex'));
69+
}
70+
71+
private tabHandler(e): void {
72+
if (!this.$focusedItem) {
73+
return;
74+
}
75+
76+
const $focusableItems = this.getFocusableItems();
77+
let index = this._collection.getAppointmentSettings(this.$focusedItem).sortedIndex;
78+
let $nextAppointment = e.shiftKey
79+
? getPrevElement(index, this._collection.renderedElementsBySortedIndex)
80+
: getNextElement(index, this._collection.renderedElementsBySortedIndex);
81+
const lastIndex = $focusableItems.length - 1;
82+
83+
if ($nextAppointment || (index > 0 && e.shiftKey) || (index < lastIndex && !e.shiftKey)) {
84+
e.preventDefault();
85+
86+
if (!$nextAppointment) {
87+
e.shiftKey ? index-- : index++;
88+
$nextAppointment = this.getFocusableItemBySortedIndex(index);
89+
}
90+
91+
this.resetTabIndex($nextAppointment);
92+
eventsEngine.trigger($nextAppointment, 'focus');
93+
}
94+
}
95+
96+
private delHandler(e: DxEvent): void {
97+
if (this._collection.option('allowDelete')) {
98+
e.preventDefault();
99+
const data = this._collection.getAppointmentSettings($(e.target)).itemData;
100+
this._collection.notifyObserver('onDeleteButtonPress', { data, target: e.target });
101+
}
102+
}
103+
104+
private escHandler(): void {
105+
if (!this._collection.isResizing) {
106+
return;
107+
}
108+
109+
this._collection.moveAppointmentBack();
110+
111+
const resizableInstance = (this.$focusedItem as any).dxResizable('instance');
112+
113+
if (resizableInstance) {
114+
resizableInstance._detachEventHandlers();
115+
resizableInstance._attachEventHandlers();
116+
resizableInstance._toggleResizingClass(false);
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)