Skip to content

Commit a2ecbbb

Browse files
authored
Chat: Implement onTyping events
1 parent 48ed0b5 commit a2ecbbb

File tree

4 files changed

+478
-13
lines changed

4 files changed

+478
-13
lines changed

packages/devextreme/js/__internal/ui/chat/chat.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import $ from '@js/core/renderer';
55
import { isDefined } from '@js/core/utils/type';
66
import type { Options as DataSourceOptions } from '@js/data/data_source';
77
import DataHelperMixin from '@js/data_helper';
8+
import type { NativeEventInfo } from '@js/events';
89
import messageLocalization from '@js/localization/message';
910
import type {
1011
Message,
1112
MessageSendEvent,
1213
Properties as ChatProperties,
14+
User,
1315
} from '@js/ui/chat';
1416
import type { OptionChanged } from '@ts/core/widget/types';
1517
import Widget from '@ts/core/widget/widget';
@@ -19,16 +21,22 @@ import ChatHeader from './header';
1921
import type {
2022
MessageSendEvent as MessageBoxMessageSendEvent,
2123
Properties as MessageBoxProperties,
24+
TypingStartEvent as MessageBoxTypingStartEvent,
2225
} from './messagebox';
2326
import MessageBox from './messagebox';
2427
import MessageList from './messagelist';
2528

2629
const CHAT_CLASS = 'dx-chat';
2730
const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';
2831

32+
type TypingStartEvent = NativeEventInfo<Chat> & { user?: User };
33+
type TypingEndEvent = NativeEventInfo<Chat> & { user?: User };
34+
2935
type Properties = ChatProperties & {
3036
title: string;
3137
showDayHeaders: boolean;
38+
onTypingStart?: ((e: TypingStartEvent) => void);
39+
onTypingEnd?: ((e: TypingEndEvent) => void);
3240
};
3341

3442
class Chat extends Widget<Properties> {
@@ -42,19 +50,25 @@ class Chat extends Widget<Properties> {
4250

4351
_messageSendAction?: (e: Partial<MessageSendEvent>) => void;
4452

53+
_typingStartAction?: (e: Partial<TypingStartEvent>) => void;
54+
55+
_typingEndAction?: (e: Partial<TypingEndEvent>) => void;
56+
4557
_getDefaultOptions(): Properties {
4658
return {
4759
...super._getDefaultOptions(),
60+
title: '',
61+
showDayHeaders: true,
4862
activeStateEnabled: true,
4963
focusStateEnabled: true,
5064
hoverStateEnabled: true,
51-
title: '',
5265
items: [],
5366
dataSource: null,
5467
user: { id: new Guid().toString() },
55-
onMessageSend: undefined,
56-
showDayHeaders: true,
5768
errors: [],
69+
onMessageSend: undefined,
70+
onTypingStart: undefined,
71+
onTypingEnd: undefined,
5872
};
5973
}
6074

@@ -63,11 +77,12 @@ class Chat extends Widget<Properties> {
6377

6478
// @ts-expect-error
6579
this._initDataController();
66-
6780
// @ts-expect-error
6881
this._refreshDataSource();
6982

7083
this._createMessageSendAction();
84+
this._createTypingStartAction();
85+
this._createTypingEndAction();
7186
}
7287

7388
_dataSourceLoadErrorHandler(): void {
@@ -161,6 +176,12 @@ class Chat extends Widget<Properties> {
161176
onMessageSend: (e) => {
162177
this._messageSendHandler(e);
163178
},
179+
onTypingStart: (e) => {
180+
this._typingStartHandler(e);
181+
},
182+
onTypingEnd: () => {
183+
this._typingEndHandler();
184+
},
164185
};
165186

166187
this._messageBox = this._createComponent($messageBox, MessageBox, configuration);
@@ -184,7 +205,21 @@ class Chat extends Widget<Properties> {
184205
_createMessageSendAction(): void {
185206
this._messageSendAction = this._createActionByOption(
186207
'onMessageSend',
187-
{ excludeValidators: ['disabled', 'readOnly'] },
208+
{ excludeValidators: ['disabled'] },
209+
);
210+
}
211+
212+
_createTypingStartAction(): void {
213+
this._typingStartAction = this._createActionByOption(
214+
'onTypingStart',
215+
{ excludeValidators: ['disabled'] },
216+
);
217+
}
218+
219+
_createTypingEndAction(): void {
220+
this._typingEndAction = this._createActionByOption(
221+
'onTypingEnd',
222+
{ excludeValidators: ['disabled'] },
188223
);
189224
}
190225

@@ -201,6 +236,19 @@ class Chat extends Widget<Properties> {
201236
this._messageSendAction?.({ message, event });
202237
}
203238

239+
_typingStartHandler(e: MessageBoxTypingStartEvent): void {
240+
const { event } = e;
241+
const { user } = this.option();
242+
243+
this._typingStartAction?.({ user, event });
244+
}
245+
246+
_typingEndHandler(): void {
247+
const { user } = this.option();
248+
249+
this._typingEndAction?.({ user });
250+
}
251+
204252
_focusTarget(): dxElementWrapper {
205253
const $input = $(this.element()).find(`.${TEXTEDITOR_INPUT_CLASS}`);
206254

@@ -249,6 +297,12 @@ class Chat extends Widget<Properties> {
249297
case 'onMessageSend':
250298
this._createMessageSendAction();
251299
break;
300+
case 'onTypingStart':
301+
this._createTypingStartAction();
302+
break;
303+
case 'onTypingEnd':
304+
this._createTypingEndAction();
305+
break;
252306
case 'showDayHeaders':
253307
this._messageList.option(name, value);
254308
break;

packages/devextreme/js/__internal/ui/chat/messagebox.ts

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,34 @@ import type { Properties as DOMComponentProperties } from '@ts/core/widget/dom_c
77
import DOMComponent from '@ts/core/widget/dom_component';
88
import type { OptionChanged } from '@ts/core/widget/types';
99

10-
import type { EnterKeyEvent } from '../../../ui/text_area';
10+
import type { EnterKeyEvent, InputEvent } from '../../../ui/text_area';
1111
import type dxTextArea from '../../../ui/text_area';
1212
import TextArea from '../m_text_area';
1313

1414
const CHAT_MESSAGEBOX_CLASS = 'dx-chat-messagebox';
1515
const CHAT_MESSAGEBOX_TEXTAREA_CLASS = 'dx-chat-messagebox-textarea';
1616
const CHAT_MESSAGEBOX_BUTTON_CLASS = 'dx-chat-messagebox-button';
1717

18+
export const TYPING_END_DELAY = 2000;
19+
1820
export type MessageSendEvent =
1921
NativeEventInfo<MessageBox, KeyboardEvent | PointerEvent | MouseEvent | TouchEvent> &
2022
{ text?: string };
2123

22-
export interface Properties extends DOMComponentProperties<MessageBox> {
23-
onMessageSend?: (e: MessageSendEvent) => void;
24+
export type TypingStartEvent = NativeEventInfo<MessageBox, UIEvent & { target: HTMLInputElement }>;
2425

26+
export interface Properties extends DOMComponentProperties<MessageBox> {
2527
activeStateEnabled?: boolean;
2628

2729
focusStateEnabled?: boolean;
2830

2931
hoverStateEnabled?: boolean;
32+
33+
onMessageSend?: (e: MessageSendEvent) => void;
34+
35+
onTypingStart?: (e: TypingStartEvent) => void;
36+
37+
onTypingEnd?: (e: NativeEventInfo<MessageBox>) => void;
3038
}
3139

3240
class MessageBox extends DOMComponent<MessageBox, Properties> {
@@ -36,20 +44,30 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
3644

3745
_messageSendAction?: (e: Partial<MessageSendEvent>) => void;
3846

47+
_typingStartAction?: (e: Partial<TypingStartEvent>) => void;
48+
49+
_typingEndAction?: () => void;
50+
51+
_typingEndTimeoutId?: ReturnType<typeof setTimeout> | undefined;
52+
3953
_getDefaultOptions(): Properties {
4054
return {
4155
...super._getDefaultOptions(),
42-
onMessageSend: undefined,
4356
activeStateEnabled: true,
4457
focusStateEnabled: true,
4558
hoverStateEnabled: true,
59+
onMessageSend: undefined,
60+
onTypingStart: undefined,
61+
onTypingEnd: undefined,
4662
};
4763
}
4864

4965
_init(): void {
5066
super._init();
5167

5268
this._createMessageSendAction();
69+
this._createTypingStartAction();
70+
this._createTypingEndAction();
5371
}
5472

5573
_initMarkup(): void {
@@ -81,10 +99,13 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
8199
autoResizeEnabled: true,
82100
valueChangeEvent: 'input',
83101
maxHeight: '8em',
84-
onInput: (): void => {
102+
onInput: (e: InputEvent): void => {
85103
const shouldButtonBeDisabled = !this._isValuableTextEntered();
86104

87105
this._toggleButtonDisableState(shouldButtonBeDisabled);
106+
107+
this._triggerTypingStartAction(e);
108+
this._updateTypingEndTimeout();
88109
},
89110
onEnterKey: (e: EnterKeyEvent): void => {
90111
if (!e.event?.shiftKey) {
@@ -129,15 +150,54 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
129150
_createMessageSendAction(): void {
130151
this._messageSendAction = this._createActionByOption(
131152
'onMessageSend',
132-
{ excludeValidators: ['disabled', 'readOnly'] },
153+
{ excludeValidators: ['disabled'] },
154+
);
155+
}
156+
157+
_createTypingStartAction(): void {
158+
this._typingStartAction = this._createActionByOption(
159+
'onTypingStart',
160+
{ excludeValidators: ['disabled'] },
161+
);
162+
}
163+
164+
_createTypingEndAction(): void {
165+
this._typingEndAction = this._createActionByOption(
166+
'onTypingEnd',
167+
{ excludeValidators: ['disabled'] },
133168
);
134169
}
135170

171+
_triggerTypingStartAction(e: InputEvent): void {
172+
if (!this._typingEndTimeoutId) {
173+
this._typingStartAction?.({ event: e.event });
174+
}
175+
}
176+
177+
_updateTypingEndTimeout(): void {
178+
clearTimeout(this._typingEndTimeoutId);
179+
180+
this._typingEndTimeoutId = setTimeout(() => {
181+
this._typingEndAction?.();
182+
183+
this._clearTypingEndTimeout();
184+
}, TYPING_END_DELAY);
185+
}
186+
187+
_clearTypingEndTimeout(): void {
188+
clearTimeout(this._typingEndTimeoutId);
189+
190+
this._typingEndTimeoutId = undefined;
191+
}
192+
136193
_sendHandler(e: ClickEvent | EnterKeyEvent): void {
137194
if (!this._isValuableTextEntered()) {
138195
return;
139196
}
140197

198+
this._clearTypingEndTimeout();
199+
this._typingEndAction?.();
200+
141201
const { text } = this._textArea.option();
142202

143203
this._textArea.reset();
@@ -170,12 +230,27 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
170230
}
171231
case 'onMessageSend':
172232
this._createMessageSendAction();
233+
234+
break;
235+
case 'onTypingStart':
236+
this._createTypingStartAction();
237+
238+
break;
239+
case 'onTypingEnd':
240+
this._createTypingEndAction();
241+
173242
break;
174243
default:
175244
super._optionChanged(args);
176245
}
177246
}
178247

248+
_clean(): void {
249+
this._clearTypingEndTimeout();
250+
251+
super._clean();
252+
}
253+
179254
updateInputAria(emptyViewId: string | null): void {
180255
this._textArea.option({
181256
inputAttr: {

0 commit comments

Comments
 (0)