devextreme
Version:
JavaScript/TypeScript Component Suite for Responsive Web Development
463 lines (462 loc) • 17.9 kB
JavaScript
/**
* DevExtreme (esm/__internal/ui/chat/message_box/chat_text_area.js)
* Version: 25.2.5
* Build date: Fri Feb 20 2026
*
* Copyright (c) 2012 - 2026 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import {
normalizeKeyName
} from "../../../../common/core/events/utils/index";
import messageLocalization from "../../../../common/core/localization/message";
import devices from "../../../../core/devices";
import $ from "../../../../core/renderer";
import {
current,
isMaterial
} from "../../../../ui/themes";
import Toolbar from "../../../../ui/toolbar";
import Widget from "../../../core/widget/widget";
import FileUploader from "../../../ui/file_uploader/file_uploader";
import Informer from "../../../ui/informer/informer";
import TextArea from "../../../ui/m_text_area";
const CHAT_TEXT_AREA_ATTACHMENTS = "dx-chat-textarea-attachments";
export const CHAT_TEXT_AREA_ATTACH_BUTTON = "dx-chat-textarea-attach-button";
export const CHAT_TEXTAREA_CLASS = "dx-chat-textarea";
export const CHAT_TEXT_AREA_TOOLBAR = "dx-chat-textarea-toolbar";
const MAX_ATTACHMENTS_COUNT = 10;
const INFORMER_DELAY = 1e4;
const ERRORS = {
fileLimit: messageLocalization.format("dxChat-fileLimitReachedWarning", 10)
};
const isMobile = () => "desktop" !== devices.current().deviceType;
export const DEFAULT_ALLOWED_FILE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".rtf", ".csv", ".md"];
class ChatTextArea extends TextArea {
constructor() {
super(...arguments);
this._fileUploaderOnCancelButtonClick = e => {
const {
file: file
} = e;
if (file) {
var _this$_filesToSend;
null === (_this$_filesToSend = this._filesToSend) || void 0 === _this$_filesToSend || _this$_filesToSend.delete(file)
}
this._toggleButtonDisableState()
}
}
getAttachments() {
var _this$_filesToSend2;
if (!(null !== (_this$_filesToSend2 = this._filesToSend) && void 0 !== _this$_filesToSend2 && _this$_filesToSend2.size)) {
return
}
return Array.from(this._filesToSend.values()).map((_ref => {
let {
name: name,
size: size
} = _ref;
return {
name: name,
size: size
}
}))
}
_getDefaultOptions() {
return Object.assign({}, super._getDefaultOptions(), {
stylingMode: "outlined",
placeholder: messageLocalization.format("dxChat-textareaPlaceholder"),
autoResizeEnabled: true,
valueChangeEvent: "input",
maxHeight: "53.86em"
})
}
_defaultOptionsRules() {
const rules = [...super._defaultOptionsRules(), {
device: () => isMaterial(current()),
options: {
stylingMode: "outlined"
}
}];
return rules
}
_supportedKeys() {
return Object.assign({}, super._supportedKeys(), {
enter: e => {
if (this._shouldSendMessageOnEnter(e)) {
e.preventDefault()
}
}
})
}
_enterKeyHandlerUp(e) {
super._enterKeyHandlerUp(e);
if ("enter" !== normalizeKeyName(e)) {
return
}
if (this._shouldSendMessageOnEnter(e)) {
this._processSendButtonActivation({
event: e
})
}
}
_init() {
super._init();
this._createSendAction()
}
_createSendAction() {
this._sendAction = this._createActionByOption("onSend", {
excludeValidators: ["disabled"]
})
}
_initMarkup() {
this.$element().addClass("dx-chat-textarea");
super._initMarkup();
this._renderToolbar();
this._initFileUploader()
}
_showInformer(text) {
if (this._informer) {
this._informer.option({
text: text
})
} else {
this._renderInformer(text)
}
this._updateInformerTimeout()
}
_renderInformer(text) {
const $informer = $("<div>").prependTo(this.$element());
this._informer = this._createComponent($informer, Informer, {
text: text,
contentAlignment: "start",
icon: "errorcircle"
})
}
_updateInformerTimeout() {
clearTimeout(this._informerTimeoutId);
this._informerTimeoutId = setTimeout((() => {
this._processInformerCleaning()
}), 1e4)
}
_renderToolbar() {
const toolbarItems = this._getToolbarItems();
const toolbarOptions = {
items: toolbarItems
};
this._$toolbar = $("<div>").addClass(CHAT_TEXT_AREA_TOOLBAR).appendTo(this.$element());
this._toolbar = this._createComponent(this._$toolbar, Toolbar, toolbarOptions)
}
_getToolbarItems() {
const {
fileUploaderOptions: fileUploaderOptions
} = this.option();
const items = [this._getSendButtonConfig()];
if (fileUploaderOptions) {
items.push(this._getAttachButtonConfig())
}
return items
}
_getAttachButtonConfig() {
const {
activeStateEnabled: activeStateEnabled,
focusStateEnabled: focusStateEnabled,
hoverStateEnabled: hoverStateEnabled
} = this.option();
const configuration = {
widget: "dxButton",
location: "before",
options: {
activeStateEnabled: activeStateEnabled,
focusStateEnabled: focusStateEnabled,
hoverStateEnabled: hoverStateEnabled,
elementAttr: {
class: CHAT_TEXT_AREA_ATTACH_BUTTON
},
icon: "attach",
onInitialized: e => {
this._attachButton = e.component
},
onClick: () => this._processInformerCleaning()
}
};
return configuration
}
_getSendButtonConfig() {
const {
activeStateEnabled: activeStateEnabled,
focusStateEnabled: focusStateEnabled,
hoverStateEnabled: hoverStateEnabled
} = this.option();
const configuration = {
widget: "dxButton",
location: "after",
options: {
activeStateEnabled: activeStateEnabled,
focusStateEnabled: focusStateEnabled,
hoverStateEnabled: hoverStateEnabled,
icon: "arrowright",
type: "default",
stylingMode: "contained",
disabled: true,
elementAttr: {
"aria-label": messageLocalization.format("dxChat-sendButtonAriaLabel")
},
onClick: e => {
this._processSendButtonActivation(e)
},
onInitialized: e => {
this._sendButton = e.component
}
}
};
return configuration
}
_initFileUploader() {
const {
fileUploaderOptions: fileUploaderOptions
} = this.option();
if (!fileUploaderOptions) {
return
}
this._renderFileUploader();
this._filesToSend = new Map
}
_renderFileUploader() {
this._$fileUploader = $("<div>").addClass(CHAT_TEXT_AREA_ATTACHMENTS).insertBefore(this._$textEditorContainer);
this._fileUploader = this._createComponent(this._$fileUploader, FileUploader, this._getFileUploaderOptions())
}
_shouldHideFileUploader() {
let value = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : [];
return 0 !== value.length
}
_getFileUploaderOptions() {
const {
fileUploaderOptions: fileUploaderOptions = {}
} = this.option();
const visible = this._shouldHideFileUploader(fileUploaderOptions.value);
const defaultFileUploaderOptions = {
multiple: true,
allowedFileExtensions: DEFAULT_ALLOWED_FILE_EXTENSIONS
};
return Object.assign({}, defaultFileUploaderOptions, fileUploaderOptions, {
visible: visible,
uploadMode: "instantly",
dialogTrigger: this.$element().find(`.${CHAT_TEXT_AREA_ATTACH_BUTTON}`).get(0),
_hideCancelButtonOnUpload: false,
_showFileIcon: true,
_cancelButtonPosition: "end",
_maxFileCount: 10,
onValueChanged: e => this._fileUploaderOnValueChanged(e),
onUploadStarted: e => this._fileUploaderOnUploadStarted(e),
onUploaded: e => this._fileUploaderOnUploaded(e),
onCancelButtonClick: e => this._fileUploaderOnCancelButtonClick(e),
onFileLimitReached: () => this._fileUploaderFileLimitReached(),
onFileValidationError: e => this._fileUploaderFileValidationError(e)
})
}
_fileUploaderOnValueChanged(e) {
var _fileUploaderOptions$;
const {
value: value,
component: component
} = e;
const {
fileUploaderOptions: fileUploaderOptions = {}
} = this.option();
component.option("visible", this._shouldHideFileUploader(value));
this._updateInputHeight();
null === (_fileUploaderOptions$ = fileUploaderOptions.onValueChanged) || void 0 === _fileUploaderOptions$ || _fileUploaderOptions$.call(fileUploaderOptions, e)
}
_addFileToMap(file) {
var _this$_filesToSend3;
null === (_this$_filesToSend3 = this._filesToSend) || void 0 === _this$_filesToSend3 || _this$_filesToSend3.set(file, {
readyToSend: false,
name: file.name,
size: file.size
});
this._toggleButtonDisableState()
}
_fileUploaderOnUploadStarted(e) {
var _fileUploaderOptions$2;
const {
file: file
} = e;
this._addFileToMap(file);
const {
fileUploaderOptions: fileUploaderOptions = {}
} = this.option();
null === (_fileUploaderOptions$2 = fileUploaderOptions.onUploadStarted) || void 0 === _fileUploaderOptions$2 || _fileUploaderOptions$2.call(fileUploaderOptions, e)
}
_fileUploaderOnUploaded(e) {
var _this$_filesToSend4, _fileUploaderOptions$3;
const {
file: file
} = e;
const {
fileUploaderOptions: fileUploaderOptions = {}
} = this.option();
const fileInfo = null === (_this$_filesToSend4 = this._filesToSend) || void 0 === _this$_filesToSend4 ? void 0 : _this$_filesToSend4.get(file);
if (this._filesToSend && fileInfo) {
this._filesToSend.set(file, Object.assign({}, fileInfo, {
readyToSend: true
}))
}
this._toggleButtonDisableState();
null === (_fileUploaderOptions$3 = fileUploaderOptions.onUploaded) || void 0 === _fileUploaderOptions$3 || _fileUploaderOptions$3.call(fileUploaderOptions, e)
}
_fileUploaderFileLimitReached() {
this._showInformer(ERRORS.fileLimit);
this._updateInputHeight()
}
_fileUploaderFileValidationError(e) {
const {
file: file
} = e;
this._addFileToMap(file)
}
_toggleButtonDisableState(state) {
var _this$_sendButton;
const shouldDisable = state ?? !this._isMessageCanBeSent();
null === (_this$_sendButton = this._sendButton) || void 0 === _this$_sendButton || _this$_sendButton.option("disabled", shouldDisable)
}
_renderButtonContainers() {}
_getAdjustedMaxHeight(maxHeight) {
return maxHeight
}
_getMaxHeight() {
const cssValue = this._input().css("maxHeight");
if (!cssValue || "none" === cssValue) {
return
}
const maxHeight = parseFloat(cssValue);
return maxHeight
}
_keyPressHandler(e) {
super._keyPressHandler(e);
this._toggleButtonDisableState()
}
_processSendButtonActivation(e) {
var _this$_sendAction;
null === (_this$_sendAction = this._sendAction) || void 0 === _this$_sendAction || _this$_sendAction.call(this, e);
this.reset();
this.resetFileUploader();
this._toggleButtonDisableState(true)
}
_shouldSendMessageOnEnter(e) {
return !(null !== e && void 0 !== e && e.shiftKey) && this._isMessageCanBeSent() && !isMobile()
}
_optionChanged(args) {
var _this$_sendButton2;
const {
name: name,
value: value
} = args;
switch (name) {
case "activeStateEnabled":
case "focusStateEnabled":
case "hoverStateEnabled":
null === (_this$_sendButton2 = this._sendButton) || void 0 === _this$_sendButton2 || _this$_sendButton2.option(name, value);
break;
case "text":
this._processInformerCleaning();
this._toggleButtonDisableState();
break;
case "onSend":
this._createSendAction();
break;
case "fileUploaderOptions":
this._handleFileUploaderOptionsChange(args);
break;
default:
super._optionChanged(args)
}
}
_handleFileUploaderOptionsChange(args) {
var _this$_fileUploader;
const {
fullName: fullName,
value: value,
previousValue: previousValue
} = args;
if ("fileUploaderOptions" === fullName && (!value || !previousValue)) {
this._cleanToolbar();
this._renderToolbar();
this._cleanFileUploader();
this._initFileUploader();
return
}
const options = Widget.getOptionsFromContainer(args);
null === (_this$_fileUploader = this._fileUploader) || void 0 === _this$_fileUploader || _this$_fileUploader.option(options)
}
_isValuableTextEntered() {
const {
text: text
} = this.option();
return Boolean(null === text || void 0 === text ? void 0 : text.trim())
}
_getFilesArray() {
return this._filesToSend ? Array.from(this._filesToSend.values()) : []
}
_areFilesReadyToSend() {
var _this$_filesToSend5;
if (!(null !== (_this$_filesToSend5 = this._filesToSend) && void 0 !== _this$_filesToSend5 && _this$_filesToSend5.size)) {
return false
}
return this._getFilesArray().every((file => file.readyToSend))
}
_isMessageCanBeSent() {
const hasText = this._isValuableTextEntered();
const hasReadyFiles = this._areFilesReadyToSend();
const hasUnreadyFiles = this._filesToSend && this._getFilesArray().some((file => !file.readyToSend));
return !hasUnreadyFiles && (hasText || hasReadyFiles)
}
_cleanFileUploader() {
var _this$_fileUploader2, _this$_$fileUploader;
null === (_this$_fileUploader2 = this._fileUploader) || void 0 === _this$_fileUploader2 || _this$_fileUploader2.dispose();
null === (_this$_$fileUploader = this._$fileUploader) || void 0 === _this$_$fileUploader || _this$_$fileUploader.remove();
this._fileUploader = null;
this._$fileUploader = null
}
_processInformerCleaning() {
this._cleanInformer();
this._updateInputHeight()
}
_cleanInformer() {
this._clearInformerTimeout();
this._removeInformer()
}
_removeInformer() {
var _this$_informer, _this$_informer2;
null === (_this$_informer = this._informer) || void 0 === _this$_informer || _this$_informer.dispose();
null === (_this$_informer2 = this._informer) || void 0 === _this$_informer2 || _this$_informer2.$element().remove();
this._informer = null
}
_clearInformerTimeout() {
clearTimeout(this._informerTimeoutId);
this._informerTimeoutId = void 0
}
_cleanToolbar() {
var _this$_toolbar, _this$_$toolbar;
null === (_this$_toolbar = this._toolbar) || void 0 === _this$_toolbar || _this$_toolbar.dispose();
null === (_this$_$toolbar = this._$toolbar) || void 0 === _this$_$toolbar || _this$_$toolbar.remove();
this._toolbar = null;
this._$toolbar = null
}
_dispose() {
this._cleanFileUploader();
this._cleanToolbar();
this._cleanInformer();
super._dispose()
}
resetFileUploader() {
var _this$_fileUploader3, _this$_filesToSend6;
null === (_this$_fileUploader3 = this._fileUploader) || void 0 === _this$_fileUploader3 || _this$_fileUploader3.reset();
null === (_this$_filesToSend6 = this._filesToSend) || void 0 === _this$_filesToSend6 || _this$_filesToSend6.clear()
}
toggleAttachButtonVisibleState(state) {
var _this$_attachButton;
null === (_this$_attachButton = this._attachButton) || void 0 === _this$_attachButton || _this$_attachButton.option("visible", state)
}
}
export default ChatTextArea;