UNPKG

devextreme

Version:

JavaScript/TypeScript Component Suite for Responsive Web Development

463 lines (462 loc) • 17.9 kB
/** * 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;