Skip to content

Commit 8fe6482

Browse files
authored
Chat: implement FileUploader integration (#31509)
1 parent 7c36901 commit 8fe6482

File tree

14 files changed

+546
-82
lines changed

14 files changed

+546
-82
lines changed
-3.55 KB
Loading
-3.55 KB
Loading
-6.59 KB
Loading
Loading
864 Bytes
Loading

packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebox/_index.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525
}
2626
}
2727

28+
.dx-textarea-attachments {
29+
.dx-fileuploader-input-wrapper {
30+
display: none;
31+
}
32+
33+
.dx-fileuploader-wrapper {
34+
padding: 0;
35+
}
36+
}
37+
2838
.dx-textarea-toolbar.dx-toolbar {
2939
.dx-toolbar-items-container {
3040
height: auto;

packages/devextreme-themebuilder/tests/data/dependencies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const dependencies: FlatStylesDependencies = {
1717
dropdownbutton: ['validation', 'button', 'buttongroup', 'popup', 'loadindicator', 'loadpanel', 'scrollview', 'list'],
1818
calendar: ['validation', 'button'],
1919
cardview: ['box', 'button', 'calendar', 'checkbox', 'contextmenu', 'datebox', 'filterbuilder', 'form', 'list', 'loadindicator', 'loadpanel', 'multiview', 'numberbox', 'popup', 'responsivebox', 'scrollview', 'selectbox', 'sortable', 'tabpanel', 'tabs', 'textbox', 'toast', 'toolbar', 'treeview', 'validation'],
20-
chat: ['button', 'loadindicator', 'loadpanel', 'popup', 'scrollview', 'textbox', 'toolbar', 'validation'],
20+
chat: ['button', 'loadindicator', 'loadpanel', 'popup', 'progressbar', 'scrollview', 'textbox', 'toolbar', 'validation'],
2121
checkbox: ['validation'],
2222
numberbox: ['validation', 'button', 'loadindicator'],
2323
colorbox: ['validation', 'button', 'loadindicator', 'numberbox', 'textbox', 'popup'],

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ class Chat extends Widget<Properties> {
536536
}
537537

538538
_messageEnteredHandler(e: MessageBoxMessageEnteredEvent): void {
539-
const { text, event } = e;
539+
const { text, event, attachments } = e;
540540
const { user } = this.option();
541541

542542
const message: Message = {
@@ -545,6 +545,10 @@ class Chat extends Widget<Properties> {
545545
text,
546546
};
547547

548+
if (attachments) {
549+
message.attachments = attachments;
550+
}
551+
548552
// @ts-expect-error
549553
const dataSource = this.getDataSource();
550554

@@ -581,15 +585,17 @@ class Chat extends Widget<Properties> {
581585
}
582586

583587
_optionChanged(args: OptionChanged<Properties>): void {
584-
const { name, value } = args;
588+
const { name, fullName, value } = args;
585589

586590
switch (name) {
587591
case 'activeStateEnabled':
588592
case 'focusStateEnabled':
589593
case 'hoverStateEnabled':
590-
case 'fileUploaderOptions':
591594
this._messageBox.option(name, value);
592595
break;
596+
case 'fileUploaderOptions':
597+
this._messageBox.option(fullName, value);
598+
break;
593599
case 'user': {
594600
const author = value as Properties[typeof name];
595601

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

Lines changed: 190 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,33 @@ import type {
1111
InitializedEvent,
1212
} from '@js/ui/button';
1313
import type Button from '@js/ui/button';
14-
import type { Properties as FileUploaderProperties } from '@js/ui/file_uploader';
14+
import type { Attachment } from '@js/ui/chat';
15+
import type { UploadedEvent, UploadStartedEvent, ValueChangedEvent } from '@js/ui/file_uploader';
1516
import { current, isMaterial } from '@js/ui/themes';
1617
import type { Item as ToolbarItem } from '@js/ui/toolbar';
1718
import Toolbar from '@js/ui/toolbar';
1819
import type { OptionChanged } from '@ts/core/widget/types';
1920
import type { SupportedKeys } from '@ts/core/widget/widget';
21+
import Widget from '@ts/core/widget/widget';
22+
import FileUploader from '@ts/ui/file_uploader/file_uploader';
23+
import type { CancelButtonClickEvent, Properties as FileUploaderProperties } from '@ts/ui/file_uploader/file_uploader.types';
2024
import type { TextAreaProperties } from '@ts/ui/m_text_area';
2125
import TextArea from '@ts/ui/m_text_area';
2226

2327
export const TEXT_AREA_TOOLBAR = 'dx-textarea-toolbar';
28+
const TEXT_AREA_ATTACHMENTS = 'dx-textarea-attachments';
29+
const TEXT_AREA_ATTACH_BUTTON = 'dx-textarea-attach-button';
2430

2531
const isMobile = (): boolean => devices.current().deviceType !== 'desktop';
2632

2733
type EnterKeyEvent = NativeEventInfo<ChatTextArea, KeyboardEvent>;
2834

2935
export type SendEvent = ClickEvent | EnterKeyEvent;
3036

37+
type FileToSend = Attachment & {
38+
readyToSend: boolean;
39+
};
40+
3141
export type Properties = TextAreaProperties & {
3242
fileUploaderOptions?: FileUploaderProperties;
3343

@@ -39,18 +49,34 @@ class ChatTextArea extends TextArea<Properties> {
3949

4050
_toolbar?: Toolbar | null;
4151

52+
_$fileUploader?: dxElementWrapper | null;
53+
54+
_fileUploader?: FileUploader | null;
55+
56+
_filesToSend?: Map<File, FileToSend>;
57+
4258
_sendButton?: Button;
4359

4460
_sendAction?: (e: SendEvent) => void;
4561

62+
getAttachments(): Attachment[] | undefined {
63+
if (!this._filesToSend?.size) {
64+
return undefined;
65+
}
66+
67+
return Array
68+
.from(this._filesToSend.values())
69+
.map(({ name, size }) => ({ name, size }));
70+
}
71+
4672
_getDefaultOptions(): Properties {
4773
return {
4874
...super._getDefaultOptions(),
4975
stylingMode: 'outlined',
5076
placeholder: messageLocalization.format('dxChat-textareaPlaceholder'),
5177
autoResizeEnabled: true,
52-
valueChangeEvent: 'input',
5378
maxHeight: '8em',
79+
valueChangeEvent: 'input',
5480
fileUploaderOptions: undefined,
5581
};
5682
}
@@ -112,6 +138,7 @@ class ChatTextArea extends TextArea<Properties> {
112138
_initMarkup(): void {
113139
super._initMarkup();
114140
this._renderToolbar();
141+
this._initFileUploader();
115142
}
116143

117144
_renderToolbar(): void {
@@ -160,6 +187,7 @@ class ChatTextArea extends TextArea<Properties> {
160187
activeStateEnabled,
161188
focusStateEnabled,
162189
hoverStateEnabled,
190+
elementAttr: { class: TEXT_AREA_ATTACH_BUTTON },
163191
icon: 'attach',
164192
},
165193
} as ToolbarItem;
@@ -200,35 +228,136 @@ class ChatTextArea extends TextArea<Properties> {
200228
return configuration;
201229
}
202230

203-
_toggleButtonDisableState(state: boolean): void {
204-
this._sendButton?.option('disabled', state);
231+
_initFileUploader(): void {
232+
const { fileUploaderOptions } = this.option();
233+
234+
if (!fileUploaderOptions) {
235+
return;
236+
}
237+
238+
this._renderFileUploader();
239+
this._filesToSend = new Map<File, FileToSend>();
240+
}
241+
242+
_renderFileUploader(): void {
243+
this._$fileUploader = $('<div>')
244+
.addClass(TEXT_AREA_ATTACHMENTS)
245+
.insertBefore(this._$textEditorContainer);
246+
247+
this._fileUploader = this._createComponent(
248+
this._$fileUploader,
249+
FileUploader,
250+
this._getFileUploaderOptions(),
251+
);
252+
}
253+
254+
_shouldHideFileUploader(value: File[] = []): boolean {
255+
return value.length !== 0;
256+
}
257+
258+
_getFileUploaderOptions(): FileUploaderProperties {
259+
const { fileUploaderOptions = {} } = this.option();
260+
const multiple = fileUploaderOptions.multiple ?? true;
261+
const visible = this._shouldHideFileUploader(fileUploaderOptions.value);
262+
263+
return {
264+
...fileUploaderOptions,
265+
multiple,
266+
visible,
267+
uploadMode: 'instantly',
268+
dialogTrigger: this.$element().find(`.${TEXT_AREA_ATTACH_BUTTON}`).get(0),
269+
_hideCancelButtonOnUpload: false,
270+
_showFileIcon: true,
271+
_cancelButtonPosition: 'end',
272+
onValueChanged: (e) => this._fileUploaderOnValueChanged(e),
273+
onUploadStarted: (e) => this._fileUploaderOnUploadStarted(e),
274+
onUploaded: (e) => this._fileUploaderOnUploaded(e),
275+
onCancelButtonClick: (e) => this._fileUploaderOnCancelButtonClick(e),
276+
};
277+
}
278+
279+
_fileUploaderOnValueChanged(e: ValueChangedEvent): void {
280+
const { value, component } = e;
281+
const { fileUploaderOptions = {} } = this.option();
282+
283+
component.option('visible', this._shouldHideFileUploader(value));
284+
this._updateInputHeight();
285+
fileUploaderOptions.onValueChanged?.(e);
286+
}
287+
288+
_fileUploaderOnUploadStarted(e: UploadStartedEvent): void {
289+
const { file } = e;
290+
const { fileUploaderOptions = {} } = this.option();
291+
292+
this._filesToSend?.set(file, {
293+
readyToSend: false,
294+
name: file.name,
295+
size: file.size,
296+
});
297+
this._toggleButtonDisableState();
298+
299+
fileUploaderOptions.onUploadStarted?.(e);
300+
}
301+
302+
_fileUploaderOnUploaded(e: UploadedEvent): void {
303+
const { file } = e;
304+
const { fileUploaderOptions = {} } = this.option();
305+
const fileInfo = this._filesToSend?.get(file);
306+
307+
if (this._filesToSend && fileInfo) {
308+
this._filesToSend.set(file, {
309+
...fileInfo,
310+
readyToSend: true,
311+
});
312+
}
313+
314+
this._toggleButtonDisableState();
315+
316+
fileUploaderOptions.onUploaded?.(e);
317+
}
318+
319+
_fileUploaderOnCancelButtonClick = (e: CancelButtonClickEvent): void => {
320+
const { file } = e;
321+
322+
if (file) {
323+
this._filesToSend?.delete(file);
324+
}
325+
326+
this._toggleButtonDisableState();
327+
};
328+
329+
_toggleButtonDisableState(state?: boolean): void {
330+
const shouldDisable = state ?? !this._isMessageCanBeSent();
331+
this._sendButton?.option('disabled', shouldDisable);
205332
}
206333

207334
_renderButtonContainers(): void {}
208335

209336
_getHeightDifference($input: dxElementWrapper): number {
210337
const superResult = super._getHeightDifference($input);
211338
const toolbarHeight = getOuterHeight(this._$toolbar);
212-
const sum: number = superResult + toolbarHeight;
339+
const fileUploaderHeight = getOuterHeight(this._$fileUploader);
340+
const sum: number = superResult + toolbarHeight + fileUploaderHeight;
213341

214342
return sum;
215343
}
216344

217345
_keyPressHandler(e: InputEvent): void {
218346
super._keyPressHandler(e);
219347

220-
const shouldButtonBeDisabled = !this._isValuableTextEntered();
221-
this._toggleButtonDisableState(shouldButtonBeDisabled);
348+
this._toggleButtonDisableState();
222349
}
223350

224351
_processSendButtonActivation(e: SendEvent): void {
225352
this._sendAction?.(e);
226353
this.reset();
354+
this._fileUploader?.reset();
355+
this._filesToSend?.clear();
227356
this._toggleButtonDisableState(true);
228357
}
229358

230359
_shouldSendMessageOnEnter(e: DxEvent<KeyboardEvent>): boolean {
231-
return !e?.shiftKey && this._isValuableTextEntered() && !isMobile();
360+
return !e?.shiftKey && this._isMessageCanBeSent() && !isMobile();
232361
}
233362

234363
_optionChanged(args: OptionChanged<Properties>): void {
@@ -242,8 +371,7 @@ class ChatTextArea extends TextArea<Properties> {
242371
break;
243372

244373
case 'text': {
245-
const shouldButtonBeDisabled = !this._isValuableTextEntered();
246-
this._toggleButtonDisableState(shouldButtonBeDisabled);
374+
this._toggleButtonDisableState();
247375
break;
248376
}
249377

@@ -252,22 +380,73 @@ class ChatTextArea extends TextArea<Properties> {
252380
break;
253381

254382
case 'fileUploaderOptions':
383+
this._handleFileUploaderOptionsChange(args);
384+
break;
255385
default:
256386
super._optionChanged(args);
257387
}
258388
}
259389

390+
_handleFileUploaderOptionsChange(args: OptionChanged<Properties>): void {
391+
const { fullName, value, previousValue } = args;
392+
393+
if (fullName === 'fileUploaderOptions' && (!value || !previousValue)) {
394+
this._cleanToolbar();
395+
this._renderToolbar();
396+
this._cleanFileUploader();
397+
this._initFileUploader();
398+
399+
return;
400+
}
401+
402+
const options = Widget.getOptionsFromContainer(args);
403+
this._fileUploader?.option(options);
404+
}
405+
260406
_isValuableTextEntered(): boolean {
261407
const { text } = this.option();
262408

263409
return Boolean(text?.trim());
264410
}
265411

266-
_dispose(): void {
412+
_getFilesArray(): FileToSend[] {
413+
return this._filesToSend ? Array.from(this._filesToSend.values()) : [];
414+
}
415+
416+
_areFilesReadyToSend(): boolean {
417+
if (!this._filesToSend?.size) {
418+
return false;
419+
}
420+
421+
return this._getFilesArray().every((file) => file.readyToSend);
422+
}
423+
424+
_isMessageCanBeSent(): boolean {
425+
const hasText = this._isValuableTextEntered();
426+
const hasReadyFiles = this._areFilesReadyToSend();
427+
const hasUnreadyFiles = this._filesToSend && this._getFilesArray()
428+
.some((file) => !file.readyToSend);
429+
430+
return !hasUnreadyFiles && (hasText || hasReadyFiles);
431+
}
432+
433+
_cleanFileUploader(): void {
434+
this._fileUploader?.dispose();
435+
this._$fileUploader?.remove();
436+
this._fileUploader = null;
437+
this._$fileUploader = null;
438+
}
439+
440+
_cleanToolbar(): void {
267441
this._toolbar?.dispose();
268442
this._$toolbar?.remove();
269443
this._toolbar = null;
270444
this._$toolbar = null;
445+
}
446+
447+
_dispose(): void {
448+
this._cleanFileUploader();
449+
this._cleanToolbar();
271450
super._dispose();
272451
}
273452
}

0 commit comments

Comments
 (0)