UNPKG

@progress/kendo-ui

Version:

This package is part of the [Kendo UI for jQuery](http://www.telerik.com/kendo-ui) suite.

1,502 lines (1,493 loc) 54.3 kB
const require_kendo_core = require('./kendo.core-CLgDzQPj.js'); //#region ../src/promptbox/constants.ts /** * PromptBox component constants * * This module contains all constants used throughout the PromptBox widget. */ /** Namespace for event binding */ const NS = ".kendoPromptBox"; /** Line mode options */ const LINE_MODE = { SINGLE: "single", MULTI: "multi", AUTO: "auto" }; /** Icon identifiers used in the PromptBox component */ const ICONS = { send: "paper-plane", stop: "stop", attachment: "paperclip" }; /** CSS class names used in the PromptBox component */ const STYLES = { promptBox: "k-prompt-box", promptBoxContent: "k-prompt-box-content", promptBoxHeader: "k-prompt-box-header", promptBoxAffix: "k-prompt-box-affix", promptBoxTextarea: "k-prompt-box-textarea", promptBoxInput: "k-prompt-box-input", promptBoxSingleline: "k-prompt-box-singleline", promptBoxMultiline: "k-prompt-box-multiline", input: "k-input", inputInner: "k-input-inner", disabled: "k-disabled", hidden: "k-hidden", focus: "k-focus", generating: "k-generating", active: "k-active", speechToTextButton: "k-speech-to-text-button", fileSelectButton: "k-file-select-button", fileBoxWrapper: "k-file-box-wrapper", fileBoxWrapperScrollableStart: "k-file-box-wrapper-scrollable-start", filesScroll: "k-files-scroll", fileBox: "k-file-box", fileInfo: "k-file-info", fileName: "k-file-name", fileSize: "k-file-size", spacer: "k-spacer" }; /** Attribute references for element selection (used as [ref-*] selectors) */ const REFERENCES = { sendButton: "ref-promptbox-send-button", fileSelectButton: "ref-promptbox-file-select-button", speechToTextButton: "ref-promptbox-speech-to-text-button", fileInput: "ref-promptbox-file-input", attachmentsHost: "ref-promptbox-attachments-host", attachmentRemoveButton: "ref-promptbox-attachment-remove-button", input: "ref-promptbox-input", header: "ref-promptbox-header", startAffix: "ref-promptbox-start-affix", endAffix: "ref-promptbox-end-affix", topAffix: "ref-promptbox-top-affix", affixSpacer: "ref-promptbox-affix-spacer" }; /** Event names triggered by the PromptBox component */ const EVENTS = { valueChange: "valueChange", input: "input", promptAction: "promptAction", fileSelect: "fileSelect", fileRemove: "fileRemove", speechToTextClick: "speechToTextClick", speechToTextStart: "speechToTextStart", speechToTextEnd: "speechToTextEnd", speechToTextError: "speechToTextError", speechToTextResult: "speechToTextResult", focus: "focus", blur: "blur", inputFocus: "inputFocus", inputBlur: "inputBlur", multilineStateChange: "multilineStateChange" }; /** Common DOM event names */ const CLICK = "click"; const INPUT = "input"; const KEYDOWN = "keydown"; const FOCUS = "focus"; const BLUR = "blur"; const CHANGE = "change"; const FOCUSIN = "focusin"; const FOCUSOUT = "focusout"; /** CSS selector prefix */ const DOT = "."; //#endregion //#region ../src/promptbox/templates/promptbox.template.ts const BUILT_IN_ATTR = " data-promptbox-built-in=\"true\""; /** * Renders the PromptBox header section. * Returns empty string when no headerTemplate (header is created dynamically when needed). */ const renderHeader = (headerTemplate, forceRender = false) => { let content = ""; if (headerTemplate) { content = headerTemplate(); } if (!content && !forceRender) { return ""; } const hiddenClass = content ? "" : ` ${STYLES.hidden}`; return `<div class="${STYLES.promptBoxHeader}${hiddenClass}" ${REFERENCES.header}><div ${REFERENCES.attachmentsHost}></div>${content}</div>`; }; /** * Renders a template function. */ const renderTemplate = (template) => { if (!template) { return ""; } return template(); }; /** * Renders the send button using kendo.html.renderButton. */ const renderSendButton = (messages, enable = true, settings) => { const disabledClass = !enable ? ` ${STYLES.disabled}` : ""; const ariaDisabled = !enable ? " aria-disabled=\"true\"" : ""; const buttonOptions = { icon: settings?.icon || "arrow-up", size: settings?.size || "small", rounded: settings?.rounded || "full" }; if (settings?.fillMode) { buttonOptions.fillMode = settings.fillMode; } if (settings?.themeColor) { buttonOptions.themeColor = settings.themeColor; } return kendo.html.renderButton(`<button title="${messages.actionButton}" aria-label="${messages.actionButton}" aria-live="polite" class="${disabledClass}"${ariaDisabled} ${REFERENCES.sendButton}${BUILT_IN_ATTR} type="button"></button>`, buttonOptions); }; /** * Renders the file select button and hidden file input using kendo.html.renderButton. */ const renderFileSelectButton = (fileInputId, accept, multiple, settings, enable = true, messages) => { const multipleAttr = multiple ? " multiple" : ""; const acceptAttr = accept ? ` accept="${accept}"` : ""; const disabledAttr = !enable ? " disabled" : ""; const ariaDisabledAttr = !enable ? " aria-disabled=\"true\"" : ""; const disabledClass = !enable ? ` ${STYLES.disabled}` : ""; const buttonTitle = settings?.text || messages?.fileSelectButton || "Attach file"; const buttonOptions = { icon: settings?.icon || ICONS.attachment, fillMode: settings?.fillMode || "clear", size: settings?.size || "small", rounded: settings?.rounded || "full" }; if (settings?.themeColor) { buttonOptions.themeColor = settings.themeColor; } const button = kendo.html.renderButton(`<button title="${buttonTitle}" aria-label="${buttonTitle}" class="${disabledClass}" ${REFERENCES.fileSelectButton}${BUILT_IN_ATTR} type="button"${disabledAttr}${ariaDisabledAttr}></button>`, buttonOptions); return `${button}<input type="file" id="${fileInputId}" class="${STYLES.hidden}" ${REFERENCES.fileInput}${multipleAttr}${acceptAttr} />`; }; /** * Renders the speech-to-text button placeholder. * The actual icon is rendered by the SpeechToTextButton component. */ const renderSpeechToTextButton = (settings) => { return `<button class="${STYLES.speechToTextButton}" ${REFERENCES.speechToTextButton}${BUILT_IN_ATTR} type="button"></button>`; }; const getFileSelectButtonLocation = (fileSelectButtonConfig) => { const settings = fileSelectButtonConfig?.settings; return settings?._renderAffixLocation === "start" ? "start" : "end"; }; const renderConfiguredFileSelectButton = (messages, fileSelectButtonConfig) => { if (!fileSelectButtonConfig) { return ""; } const fileSelectButtonEnabled = fileSelectButtonConfig.enable !== false; return renderFileSelectButton(fileSelectButtonConfig.fileInputId, fileSelectButtonConfig.accept, fileSelectButtonConfig.multiple, fileSelectButtonConfig.settings, fileSelectButtonEnabled, messages); }; /** * Renders the start affix container (before the input). * Uses k-prompt-box-affix class; position is determined by DOM order. */ const renderStartAffix = (startAffixTemplate, startContent = "") => { const content = `${startContent}${renderTemplate(startAffixTemplate)}`.trim(); if (!content) { return ""; } return `<div class="${STYLES.promptBoxAffix}" ${REFERENCES.startAffix}>${content}</div>`; }; /** * Renders the end affix container with buttons (after the input). * Returns empty string when no content. */ const renderEndAffix = (messages, showSendButton, showSpeechToText = true, endAffixTemplate, fileSelectButtonConfig, buttonSettingsConfig) => { const endAffix = renderTemplate(endAffixTemplate); const fileSelectButton = getFileSelectButtonLocation(fileSelectButtonConfig) === "end" ? renderConfiguredFileSelectButton(messages, fileSelectButtonConfig) : ""; const speechButton = showSpeechToText ? renderSpeechToTextButton(buttonSettingsConfig?.speechToTextButtonSettings) : ""; const sendButton = showSendButton ? renderSendButton(messages, true, buttonSettingsConfig?.actionButtonSettings) : ""; const content = `${endAffix}${fileSelectButton}${speechButton}${sendButton}`.trim(); if (!content) { return ""; } return `<div class="${STYLES.promptBoxAffix}" ${REFERENCES.endAffix}> ${endAffix} ${fileSelectButton} ${speechButton} ${sendButton} </div>`; }; /** * Renders the end affix container with start affix content (for multi mode). * In multi mode: fileSelectButton + startAffix content + spacer + endAffix content + buttons * The spacer is only rendered if there's content on both sides to separate. */ const renderEndAffixWithStartAffix = (messages, showSendButton, showSpeechToText = true, startAffixTemplate, endAffixTemplate, fileSelectButtonConfig, buttonSettingsConfig) => { const startAffix = renderTemplate(startAffixTemplate); const endAffix = renderTemplate(endAffixTemplate); const fileSelectButton = renderConfiguredFileSelectButton(messages, fileSelectButtonConfig); const speechButton = showSpeechToText ? renderSpeechToTextButton(buttonSettingsConfig?.speechToTextButtonSettings) : ""; const sendButton = showSendButton ? renderSendButton(messages, true, buttonSettingsConfig?.actionButtonSettings) : ""; const fileSelectButtonLocation = getFileSelectButtonLocation(fileSelectButtonConfig); const startFileSelectButton = fileSelectButtonLocation === "start" ? fileSelectButton : ""; const endFileSelectButton = fileSelectButtonLocation === "end" ? fileSelectButton : ""; const startAffixContent = `${startFileSelectButton}${startAffix}`.trim(); const needsSpacer = !!startAffixContent; return `<div class="${STYLES.promptBoxAffix}" ${REFERENCES.endAffix}> ${startAffixContent} ${needsSpacer ? `<div class="${STYLES.spacer}" ${REFERENCES.affixSpacer}></div>` : ""} ${endAffix} ${endFileSelectButton} ${speechButton} ${sendButton} </div>`; }; /** * Renders the top affix section (only for multi mode). * Note: This is placed at the beginning of content for multi-mode layout. */ const renderTopAffix = (topAffixTemplate) => { const content = renderTemplate(topAffixTemplate); if (!content) { return ""; } return `<div class="${STYLES.promptBoxAffix}" ${REFERENCES.topAffix}>${content}</div>`; }; /** * Renders the complete PromptBox structure. * For multi mode: startAffix content is rendered inside the end affix container with a spacer. * For auto mode: startAffix is rendered as a separate container (JS handles reorganization on expand). */ const renderPromptBox = (headerTemplate, messages, showSendButton, mode, showSpeechToText = true, renderConfig = {}) => { const { affixConfig = {}, fileSelectButtonConfig, buttonSettingsConfig } = renderConfig; const forceRenderHeader = !!fileSelectButtonConfig; const header = renderHeader(headerTemplate, forceRenderHeader); const topAffix = mode === LINE_MODE.MULTI ? renderTopAffix(affixConfig.topAffixTemplate) : ""; const isMultiMode = mode === LINE_MODE.MULTI; const startFileSelectButton = !isMultiMode && getFileSelectButtonLocation(fileSelectButtonConfig) === "start" ? renderConfiguredFileSelectButton(messages, fileSelectButtonConfig) : ""; const startAffix = isMultiMode ? "" : renderStartAffix(affixConfig.startAffixTemplate, startFileSelectButton); const endAffix = isMultiMode ? renderEndAffixWithStartAffix(messages, showSendButton, showSpeechToText, affixConfig.startAffixTemplate, affixConfig.endAffixTemplate, fileSelectButtonConfig, buttonSettingsConfig) : renderEndAffix(messages, showSendButton, showSpeechToText, affixConfig.endAffixTemplate, fileSelectButtonConfig, buttonSettingsConfig); return `${header} <div class="${STYLES.promptBoxContent}"> ${topAffix} ${startAffix} <span ${REFERENCES.input}></span> ${endAffix} </div>`; }; //#endregion //#region ../src/promptbox/collaborators/accessibility-manager.collaborator.ts var AccessibilityManager = class { constructor(context) { this.context = context; } applyInitial() { const options = this.context.getOptions(); const input = this.context.getInputElement(); input.attr("aria-label", options.messages?.messageBoxTitle || "Message"); if (!options.enable) { this.context.wrapper.attr("aria-disabled", "true"); } if (options.readonly) { this.updateInputAccessibility(); } if (options.loading) { this.context.wrapper.attr("aria-busy", "true"); } } updateInputAccessibility() { const options = this.context.getOptions(); const input = this.context.getInputElement(); if (!input) { return; } input.attr("aria-label", options.messages?.messageBoxTitle || "Message"); if (options.readonly) { input.attr("aria-readonly", "true"); } else { input.removeAttr("aria-readonly"); } if (!options.enable) { input.attr("aria-disabled", "true"); } else { input.removeAttr("aria-disabled"); } } updateWrapper(state) { if (state.disabled !== undefined) { if (state.disabled) { this.context.wrapper.attr("aria-disabled", "true"); } else { this.context.wrapper.removeAttr("aria-disabled"); } } if (state.loading !== undefined) { if (state.loading) { this.context.wrapper.attr("aria-busy", "true"); } else { this.context.wrapper.removeAttr("aria-busy"); } } } }; //#endregion //#region ../src/promptbox/collaborators/action-button-manager.collaborator.ts var ActionButtonManager = class { constructor(context) { this.context = context; } getSendButton() { return this.context.wrapper.find(`[${REFERENCES.sendButton}]`); } setLoading(loading) { const sendButton = this.getSendButton(); const messages = this.context.getMessages(); const settings = this.context.getSettings(); if (loading) { sendButton.addClass(`${STYLES.generating} ${STYLES.active}`); sendButton.attr("title", messages.actionButtonLoading); sendButton.attr("aria-label", messages.actionButtonLoading); const icon = sendButton.find(".k-icon, .k-svg-icon"); if (icon.length) { const loadingIcon = settings?.loadingIcon || "stop"; kendo.ui.icon(icon, { icon: loadingIcon }); } } else { sendButton.removeClass(`${STYLES.generating} ${STYLES.active}`); sendButton.attr("title", messages.actionButton); sendButton.attr("aria-label", messages.actionButton); const icon = sendButton.find(".k-icon, .k-svg-icon"); if (icon.length) { const sendIcon = settings?.icon || "arrow-up"; kendo.ui.icon(icon, { icon: sendIcon }); } } this.updateState(); } updateState() { const sendButton = this.getSendButton(); const isDisabled = this.context.isDisabled(); if (isDisabled) { sendButton.addClass(STYLES.disabled); sendButton.attr("aria-disabled", "true"); return; } sendButton.removeAttr("aria-disabled"); if (this.context.isLoading()) { sendButton.removeClass(STYLES.disabled); return; } if (this.context.hasContent()) { sendButton.removeClass(STYLES.disabled); } else { sendButton.addClass(STYLES.disabled); } } updateMessages() { const sendButton = this.getSendButton(); const messages = this.context.getMessages(); if (this.context.isLoading()) { sendButton.attr("title", messages.actionButtonLoading); sendButton.attr("aria-label", messages.actionButtonLoading); } else { sendButton.attr("title", messages.actionButton); sendButton.attr("aria-label", messages.actionButton); } } }; //#endregion //#region ../src/promptbox/collaborators/speech-handler.collaborator.ts var SpeechHandler = class { constructor(context) { this.instance = null; this.context = context; } init() { const option = this.context.getSpeechToTextButtonOption(); if (option === false) { return; } const speechToTextButton = this.context.wrapper.find(`[${REFERENCES.speechToTextButton}], ${DOT}${STYLES.speechToTextButton}`).first(); if (!speechToTextButton.length) { return; } const defaultSettings = { fillMode: "flat", size: "small", rounded: "full", enable: true }; const userSettings = typeof option === "object" && option !== null ? option : {}; const isEnabled = userSettings.enable !== false; const buttonOptions = { ...defaultSettings, ...userSettings, start: (e) => this.context.callbacks.onStart(e), end: (e) => this.context.callbacks.onEnd(e), result: (e) => this.context.callbacks.onResult(e), error: (e) => this.context.callbacks.onError(e) }; this.instance = new kendo.ui.SpeechToTextButton(speechToTextButton, buttonOptions); if (!isEnabled) { this.instance.enable(false); } } enable(enabled) { if (this.instance) { this.instance.enable(enabled); } } destroy() { if (this.instance) { this.instance.destroy(); this.instance = null; } } }; //#endregion //#region ../src/promptbox/collaborators/file-handler.collaborator.ts var FileHandler = class { constructor(context) { this.files = []; this._dragHandler = null; this.context = context; } initUpload() { const options = this.context.getOptions(); if (!this.showFileSelectButton(options)) { return; } const fileInput = this.context.wrapper.find(`[${REFERENCES.fileInput}]`); if (fileInput.length === 0) { return; } const settings = this.getFileSelectButtonSettings(options); const multiple = settings?.multiple !== false; const upload = new kendo.ui.Upload(fileInput, { multiple, async: false, uniqueFileUids: true, select: this.onUploadSelect.bind(this) }); upload?.wrapper?.addClass(STYLES.hidden); this.uploadInstance = upload; } getFiles() { return [...this.files]; } setFiles(newFiles) { this.files = [...newFiles]; this.renderAttachments(); this.context.callbacks.onFilesChanged(); } clearFiles() { this.files = []; this.renderAttachments(); this.context.callbacks.onFilesChanged(); } hasFiles() { return this.files.length > 0; } handleFileSelectClick() { const options = this.context.getOptions(); const settings = this.getFileSelectButtonSettings(options); if (!options.enable || options.readonly || settings?.enable === false) { return; } const uploadInput = this.uploadInstance?.element?.[0] || null; const fileInput = uploadInput || this.context.wrapper.find(`[${REFERENCES.fileInput}]`)[0]; if (fileInput && typeof fileInput.click === "function") { fileInput.click(); } } removeAttachmentByUid(uid) { const removedFile = this.files.find((file) => this.getFileUid(file) === uid); const newFiles = this.files.filter((file) => this.getFileUid(file) !== uid); if (newFiles.length === this.files.length) { return; } this.files = newFiles; this.renderAttachments(); this.context.callbacks.onFilesChanged(); if (removedFile) { this.context.callbacks.onFileRemove(removedFile, [...this.files]); } } renderAttachments() { this._destroyDragHandler(); const options = this.context.getOptions(); const header = this.context.wrapper.find(`[${REFERENCES.header}]`); const host = this.context.wrapper.find(`[${REFERENCES.attachmentsHost}]`); if (host.length === 0) { return; } if (!this.files.length) { host.empty(); if (!options.headerTemplate) { header.addClass(STYLES.hidden); } return; } header.removeClass(STYLES.hidden); if (options._filesTemplate) { host.html(options._filesTemplate(this.files)); this._attachDragToScroll(); return; } let html = `<ul class="${STYLES.fileBoxWrapper} ${STYLES.fileBoxWrapperScrollableStart}">`; html += `<div class="${STYLES.filesScroll}">`; for (let i = 0; i < this.files.length; i++) { const fileObj = this.files[i]; const rawFile = fileObj?.rawFile || fileObj; const uid = this.getFileUid(fileObj); const name = rawFile?.name || fileObj?.name || ""; const size = rawFile?.size || fileObj?.size || 0; const extension = name.lastIndexOf(".") > -1 ? name.substring(name.lastIndexOf(".")).toLowerCase() : ""; const iconName = require_kendo_core.fileUtilsService.getFileGroup(extension, true); const fileSizeMessage = require_kendo_core.fileUtilsService.getFileSizeMessage(size); const icon = kendo.ui.icon({ icon: iconName, size: "xlarge" }); const removeButton = kendo.html.renderButton(`<button type="button" ${REFERENCES.attachmentRemoveButton} data-uid="${require_kendo_core.htmlService.encode(uid)}" aria-label="Remove attachment" title="Remove attachment"></button>`, { icon: "x-circle", fillMode: "flat" }); html += `<li class="${STYLES.fileBox}">`; html += icon; html += `<div class="${STYLES.fileInfo}">`; html += `<span class="${STYLES.fileName}">${require_kendo_core.htmlService.encode(name)}</span>`; html += `<span class="${STYLES.fileSize}">${require_kendo_core.htmlService.encode(fileSizeMessage)}</span>`; html += `</div>`; html += removeButton; html += `</li>`; } html += "</div></ul>"; host.html(html); this._attachDragToScroll(); } _attachDragToScroll() { const host = this.context.wrapper.find(`[${REFERENCES.attachmentsHost}]`); const scrollContainer = host.find("." + STYLES.filesScroll); if (!scrollContainer.length) { return; } this._dragHandler = require_kendo_core.domUtilsService.createDragToScrollHandler(scrollContainer, { namespace: NS + ".promptBoxFileDrag", captureElement: this.context.wrapper }); this._dragHandler.attach(); } _destroyDragHandler() { if (this._dragHandler) { this._dragHandler.destroy(); this._dragHandler = null; } } destroy() { this._destroyDragHandler(); if (this.uploadInstance) { this.uploadInstance.destroy(); this.uploadInstance = null; } } onUploadSelect(e) { e.preventDefault(); const files = e.files; const options = this.context.getOptions(); const settings = this.getFileSelectButtonSettings(options); const validFiles = this.validateFiles(files, settings); if (validFiles.length > 0) { this.files = [...this.files, ...validFiles]; this.renderAttachments(); this.context.callbacks.onFilesChanged(); this.context.callbacks.onFileSelect(validFiles); } } validateFiles(files, settings) { const validFiles = []; const restrictions = settings?.restrictions; for (let i = 0; i < files.length; i++) { const fileObj = files[i]; const file = fileObj.rawFile || fileObj; let isValid = true; if (restrictions) { if (restrictions.allowedExtensions && restrictions.allowedExtensions.length > 0) { const ext = file.name.split(".").pop()?.toLowerCase() || ""; const allowedExts = restrictions.allowedExtensions.map((e) => e.toLowerCase().replace(".", "")); if (!allowedExts.includes(ext)) { isValid = false; } } if (restrictions.maxFileSize && file.size > restrictions.maxFileSize) { isValid = false; } if (restrictions.minFileSize && file.size < restrictions.minFileSize) { isValid = false; } } if (isValid) { validFiles.push(fileObj); } } return validFiles; } getFileUid(file) { if (!file) { return ""; } if (!file.uid) { file.uid = require_kendo_core.utilsService.guid(); } return file.uid; } showFileSelectButton(options) { return options.fileSelectButton === true || typeof options.fileSelectButton === "object" && options.fileSelectButton !== null; } getFileSelectButtonSettings(options) { if (typeof options.fileSelectButton === "object" && options.fileSelectButton !== null) { return options.fileSelectButton; } return undefined; } }; //#endregion //#region ../src/promptbox/collaborators/input-manager.collaborator.ts const DEFAULT_MESSAGES$1 = { placeholder: "Type a message...", actionButton: "Send", actionButtonLoading: "Stop", messageBoxTitle: "Message" }; var InputManager = class { constructor(context) { this.inputInstance = null; this.singleRowHeight = 0; this.singleLineWidth = 0; this.initialHeight = 0; this.minMultiRowHeight = 0; this.multiline = false; this.context = context; } init() { const inputContainer = this.context.wrapper.find(`[${REFERENCES.input}]`); const mode = this.context.getMode(); const inputElement = this.createInputElement(mode); inputContainer.replaceWith(inputElement); const value = this.context.getOptions().value || ""; if (value) { inputElement.val(value); } this.captureInitialDimensions(); this.attachInputEvents(); } getElement() { return this.inputInstance?.element || null; } getValue() { return this.inputInstance ? this.inputInstance.value() : ""; } setValue(value) { if (this.inputInstance) { this.inputInstance.value(value); } this.updateAutoResize(); this.updateMultiResize(); } enable(enabled) { if (this.inputInstance) { this.inputInstance.enable(enabled); } } setReadonly(readonly) { if (this.inputInstance?.element) { this.inputInstance.element.prop("readonly", readonly); } } focus() { if (this.inputInstance) { this.inputInstance.focus(); } } blur() { if (this.inputInstance?.element) { this.inputInstance.element.blur(); } } isMultiline() { return this.multiline; } updateAutoResize() { const mode = this.context.getMode(); if (mode !== LINE_MODE.AUTO || !this.inputInstance) { return; } const textarea = this.inputInstance.element; if (!textarea.is("textarea")) { return; } const el = textarea[0]; if (el.offsetHeight > 0 && this.initialHeight < el.offsetHeight) { this.captureInitialDimensions(); } if (!this.multiline) { const needsExpansion = el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight; if (needsExpansion) { this.updateMultilineState(true); this.updateHeight(); } } else { this.updateHeight(); if (this.shouldCollapse()) { this.updateMultilineState(false); textarea.css("height", ""); } } } updateMultiResize() { const mode = this.context.getMode(); if (mode !== LINE_MODE.MULTI || !this.inputInstance) { return; } const textarea = this.inputInstance.element; if (!textarea.is("textarea")) { return; } const el = textarea[0]; const options = this.context.getOptions(); if (el.offsetHeight > 0 && this.initialHeight < el.offsetHeight) { this.captureInitialDimensions(); } textarea.css("overflow", "hidden"); textarea.css("height", this.initialHeight + "px"); const scrollHeight = el.scrollHeight; const newHeight = this.calculateMultiHeight(scrollHeight, options.maxTextAreaHeight); if (newHeight === options.maxTextAreaHeight) { textarea.css("overflow", ""); } textarea.css("height", newHeight + "px"); } updatePlaceholder() { const options = this.context.getOptions(); const messages = $.extend({}, DEFAULT_MESSAGES$1, options.messages); const placeholder = options.placeholder || messages.placeholder; if (this.inputInstance?.element) { this.inputInstance.element.attr("placeholder", placeholder); } } destroy() { if (this.inputInstance) { this.inputInstance.element.off(NS); this.inputInstance.destroy(); this.inputInstance = null; } } createInputElement(mode) { const options = this.context.getOptions(); const messages = $.extend({}, DEFAULT_MESSAGES$1, options.messages); const title = options.title ? `title="${options.title}"` : ""; const readonly = options.readonly ? "readonly" : ""; let inputElement; if (mode === LINE_MODE.SINGLE) { inputElement = $(`<input type="text" class="${STYLES.promptBoxInput} ${STYLES.inputInner}" placeholder="${options.placeholder || messages.placeholder}" autocomplete="off" ${title} ${readonly} />`); this.inputInstance = { element: inputElement, value: (val) => { if (val === undefined) { return inputElement.val(); } inputElement.val(val); }, enable: (enabled) => { inputElement.prop("disabled", !enabled); }, focus: () => inputElement.trigger("focus"), destroy: () => {} }; } else { const rows = mode === LINE_MODE.AUTO ? 1 : options.rows || 1; inputElement = $(`<textarea class="${STYLES.promptBoxTextarea} ${STYLES.inputInner}" placeholder="${options.placeholder || messages.placeholder}" rows="${rows}" aria-multiline="true" ${title} ${readonly}></textarea>`); if (options.maxTextAreaHeight) { inputElement.css("max-height", options.maxTextAreaHeight + "px"); inputElement.css("overflow-y", "auto"); } this.inputInstance = { element: inputElement, value: (val) => { if (val === undefined) { return inputElement.val(); } inputElement.val(val); }, enable: (enabled) => { inputElement.prop("disabled", !enabled); }, focus: () => inputElement.trigger("focus"), destroy: () => {} }; } return inputElement; } captureInitialDimensions() { const mode = this.context.getMode(); if (mode === LINE_MODE.SINGLE || !this.inputInstance) { return; } const textarea = this.inputInstance.element; if (!textarea.is("textarea")) { return; } const el = textarea[0]; const computedStyle = window.getComputedStyle(el); const lineHeight = parseFloat(computedStyle.lineHeight) || 20; const paddingTop = parseFloat(computedStyle.paddingTop) || 0; const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0; const options = this.context.getOptions(); const rows = options.rows || 1; this.singleRowHeight = lineHeight + paddingTop + paddingBottom; this.initialHeight = el.offsetHeight || this.singleRowHeight; this.singleLineWidth = el.offsetWidth; this.minMultiRowHeight = lineHeight * rows + paddingTop + paddingBottom; } shouldCollapse() { if (!this.inputInstance || this.singleLineWidth <= 0) { return false; } const textarea = this.inputInstance.element; if (!textarea.is("textarea")) { return false; } const el = textarea[0]; const currentHeight = el.style.height; textarea.css({ overflow: "hidden", width: this.singleLineWidth + "px", whiteSpace: "nowrap", height: this.initialHeight + "px" }); const fitsSingleLine = el.scrollWidth <= this.singleLineWidth && el.scrollHeight <= this.initialHeight; textarea.css({ overflow: "", width: "", whiteSpace: "" }); if (currentHeight) { textarea.css("height", currentHeight); } else { textarea.css("height", ""); } return fitsSingleLine; } updateHeight() { const options = this.context.getOptions(); if (!this.inputInstance) { return; } const textarea = this.inputInstance.element; if (!textarea.is("textarea")) { return; } const el = textarea[0]; textarea.css("overflow", "hidden"); textarea.css("height", this.initialHeight + "px"); const scrollHeight = el.scrollHeight; const newHeight = options.maxTextAreaHeight ? Math.min(scrollHeight, options.maxTextAreaHeight) : scrollHeight; if (newHeight === options.maxTextAreaHeight) { textarea.css("overflow", ""); } textarea.css("height", newHeight + "px"); } calculateMultiHeight(scrollHeight, maxTextAreaHeight) { if (maxTextAreaHeight && scrollHeight > maxTextAreaHeight) { return maxTextAreaHeight; } return Math.max(this.minMultiRowHeight, scrollHeight); } updateMultilineState(isMultiline) { const mode = this.context.getMode(); if (mode !== LINE_MODE.AUTO) { return; } if (this.multiline === isMultiline) { return; } const wasMultiline = this.multiline; this.multiline = isMultiline; if (isMultiline) { this.context.wrapper.addClass(STYLES.promptBoxMultiline); } else { this.context.wrapper.removeClass(STYLES.promptBoxMultiline); } this.context.callbacks.onMultilineStateChange(isMultiline, wasMultiline); } attachInputEvents() { if (!this.inputInstance) { return; } const inputElement = this.inputInstance.element; inputElement.on(INPUT + NS, () => { this.updateAutoResize(); this.updateMultiResize(); this.context.callbacks.onInput(); }); inputElement.on(KEYDOWN + NS, (e) => { this.context.callbacks.onKeyDown(e); }); inputElement.on(FOCUS + NS, () => { this.context.callbacks.onFocus(); }); inputElement.on(BLUR + NS, () => { this.context.callbacks.onBlur(); }); } }; //#endregion //#region ../src/promptbox/promptbox.widget.ts const BUILT_IN_SELECTOR = "[data-promptbox-built-in=\"true\"]"; const BUILT_IN_FILE_SPACER_SELECTOR = ".k-spacer[data-promptbox-role=\"file-spacer\"]"; const DEFAULT_MESSAGES = { placeholder: "Type a message...", actionButton: "Send", actionButtonLoading: "Stop", messageBoxTitle: "Message", fileSelectButton: "Attach file" }; /** * PromptBox widget - A composite input component for chat-like interfaces. * * Supports three modes: * - "single": Single-line text input * - "multi": Multi-line textarea with fixed rows * - "auto": Auto-expanding textarea (starts from 1 row) */ var PromptBox = class PromptBox extends require_kendo_core.Widget { static { this.options = { name: "PromptBox", prefix: "", mode: LINE_MODE.AUTO, placeholder: "", value: "", enable: true, readonly: false, title: "", fillMode: undefined, maxTextAreaHeight: undefined, rows: 1, headerTemplate: null, loading: false, actionButton: null, fileSelectButton: false, speechToTextButton: true, startAffixTemplate: null, endAffixTemplate: null, topAffixTemplate: null, messages: DEFAULT_MESSAGES }; } /** * Constructs a new PromptBox instance. */ constructor(element, options) { super(element, $.extend(true, {}, PromptBox.options, options)); this.events = [ EVENTS.valueChange, EVENTS.input, EVENTS.promptAction, EVENTS.fileSelect, EVENTS.fileRemove, EVENTS.speechToTextClick, EVENTS.speechToTextStart, EVENTS.speechToTextEnd, EVENTS.speechToTextError, EVENTS.speechToTextResult, EVENTS.focus, EVENTS.blur, EVENTS.inputFocus, EVENTS.inputBlur, EVENTS.multilineStateChange ]; this._value = ""; this._loading = false; this._readonly = false; this._hasFocus = false; this._fileInputId = ""; this._value = this.options.value || ""; this._loading = this.options.loading || false; this._fileInputId = "promptbox_file_" + require_kendo_core.utilsService.guid(); this._initWrapper(); this._initInputManager(); this._initSpeechHandler(); this._initFileHandler(); this._attachEvents(); this.fileHandler.renderAttachments(); this._initAccessibilityManager(); this._initActionButtonManager(); this.bind(this.events, this.options); if (!this.options.enable) { this.enable(false); } if (this.options.readonly) { this.readonly(true); } if (this._value && this._getMode() === LINE_MODE.AUTO) { this.inputManager.updateAutoResize(); } this.actionButtonManager.updateState(); } /** * Gets or sets the input value. */ value(newValue) { if (newValue === undefined) { return this._value; } this._value = newValue; this.inputManager.setValue(newValue); this.actionButtonManager.updateState(); } /** * Enables or disables the PromptBox. * When enabling, this also clears the readonly state (similar to TextBox behavior). */ enable(enabled = true) { const options = this.options; options.enable = enabled; if (enabled) { this._readonly = false; options.readonly = false; this.inputManager.setReadonly(false); this.wrapper.removeClass(STYLES.disabled); } else { this.wrapper.addClass(STYLES.disabled); } this.inputManager.enable(enabled); this.speechHandler.enable(enabled); this._updateFileSelectButtonState(); this.actionButtonManager.updateState(); this.accessibilityManager.updateWrapper({ disabled: !enabled }); this.accessibilityManager.updateInputAccessibility(); } /** * Gets or sets the read-only state of the PromptBox. */ readonly(value) { if (value === undefined) { return this._readonly; } this._readonly = value; const options = this.options; options.readonly = value; this.inputManager.setReadonly(value); this.accessibilityManager.updateInputAccessibility(); this.actionButtonManager.updateState(); this.speechHandler.enable(!value); this._updateFileSelectButtonState(); } /** * Gets or sets the loading state (shows stop button instead of send). */ loading(value) { if (value === undefined) { return this._loading; } this._loading = value; this.accessibilityManager.updateWrapper({ loading: value }); this.actionButtonManager.setLoading(value); } /** * Focuses the input element. */ focus() { this.inputManager.focus(); } /** * Blurs the input element. */ blur() { this.inputManager.blur(); } /** * Gets or sets the attached files. * @param newFiles - Optional array of files to set. If not provided, returns current files. * @returns Array of attached files when getting, void when setting. */ files(newFiles) { if (newFiles === undefined) { return this.fileHandler.getFiles(); } this.fileHandler.setFiles(newFiles); } /** * Clears all attached files. */ clearFiles() { this.fileHandler.clearFiles(); } /** * Destroys the widget. */ destroy() { this._detachEvents(); this._destroyCollaborators(); super.destroy(); } _destroyCollaborators() { this.fileHandler.destroy(); this.speechHandler.destroy(); this.inputManager.destroy(); } /** * Updates widget options dynamically. * For complex changes (templates, buttons, mode), the widget is rebuilt. */ setOptions(options) { const requiresRebuild = options.headerTemplate !== undefined || options.startAffixTemplate !== undefined || options.endAffixTemplate !== undefined || options.topAffixTemplate !== undefined || options.actionButton !== undefined || options.actionButtonSettings !== undefined || options.fileSelectButton !== undefined || options.speechToTextButton !== undefined || options.mode !== undefined; if (requiresRebuild) { const currentValue = this._value; const wasLoading = this._loading; const attachments = this.options.attachments; const currentFiles = this.fileHandler.getFiles(); this._detachEvents(); this._destroyCollaborators(); super.setOptions(options); this.wrapper.remove(); this._initWrapper(); this._initInputManager(); this._initSpeechHandler(); this._initFileHandler(); this._attachEvents(); if (currentFiles.length > 0) { this.fileHandler.setFiles(currentFiles); } this._initAccessibilityManager(); this._initActionButtonManager(); if (currentValue) { this.value(currentValue); } if (wasLoading) { this.loading(true); } if (attachments) { this.options.attachments = attachments; this.fileHandler.renderAttachments(); } return; } super.setOptions(options); if (options.placeholder !== undefined || options.messages?.placeholder !== undefined) { this.inputManager.updatePlaceholder(); } if (options.messages?.actionButton !== undefined || options.messages?.actionButtonLoading !== undefined) { this.actionButtonManager.updateMessages(); } if (options.enable !== undefined) { this.enable(options.enable); } if (options.readonly !== undefined) { this.readonly(options.readonly); } if (options.loading !== undefined) { this.loading(options.loading); } } _initInputManager() { this.inputManager = new InputManager({ wrapper: this.wrapper, getOptions: () => this.options, getMode: () => this._getMode(), callbacks: { onInput: () => this._handleInput(), onKeyDown: (e) => this._handleKeyDown(e), onFocus: () => this.trigger(EVENTS.inputFocus), onBlur: () => this.trigger(EVENTS.inputBlur), onMultilineStateChange: (isMultiline, wasMultiline) => { this._handleAffixReorganization(isMultiline); this.trigger(EVENTS.multilineStateChange, { isMultiline, wasMultiline }); } } }); this.inputManager.init(); } /** * Reorganizes affixes when auto mode switches between single-line and multiline. * In multiline: startAffix content moves to endAffix (left side) with a spacer. * In single-line: startAffix content returns to its original container. */ _handleAffixReorganization(isMultiline) { const startAffix = this.wrapper.find(`[${REFERENCES.startAffix}]`).first(); const endAffix = this.wrapper.find(`[${REFERENCES.endAffix}]`).first(); if (!startAffix.length || !endAffix.length) { return; } if (isMultiline) { if (startAffix.hasClass(STYLES.hidden)) { return; } const startAffixContent = startAffix.contents().detach(); if (!startAffixContent.length) { return; } startAffix.addClass(STYLES.hidden); endAffix.prepend($(`<div class="${STYLES.spacer}" ${REFERENCES.affixSpacer}></div>`)); endAffix.prepend(startAffixContent); } else { const spacer = endAffix.children(`[${REFERENCES.affixSpacer}]`).first(); if (!spacer.length) { return; } const startAffixContent = spacer.prevAll().detach(); if (startAffixContent.length) { startAffix.empty().append(startAffixContent); } startAffix.removeClass(STYLES.hidden); spacer.remove(); } this._syncFileSelectSpacer(); } _initSpeechHandler() { this.speechHandler = new SpeechHandler({ wrapper: this.wrapper, getSpeechToTextButtonOption: () => this.options.speechToTextButton, callbacks: { onStart: (e) => this.trigger(EVENTS.speechToTextStart, e), onEnd: (e) => this.trigger(EVENTS.speechToTextEnd, e), onResult: (e) => { const transcript = e.alternatives?.[0]?.transcript || ""; if (transcript) { const currentValue = this._value || ""; const newValue = currentValue + (currentValue ? " " : "") + transcript; this.value(newValue); } this.trigger(EVENTS.speechToTextResult, { ...e, transcript }); }, onError: (e) => this.trigger(EVENTS.speechToTextError, e) } }); this.speechHandler.init(); } _initFileHandler() { this.fileHandler = new FileHandler({ wrapper: this.wrapper, getOptions: () => this.options, callbacks: { onFilesChanged: () => this.actionButtonManager.updateState(), onFileSelect: (files) => this.trigger(EVENTS.fileSelect, { files }), onFileRemove: (file, remainingFiles) => this.trigger(EVENTS.fileRemove, { file, files: remainingFiles }) } }); this.fileHandler.initUpload(); } _initAccessibilityManager() { this.accessibilityManager = new AccessibilityManager({ wrapper: this.wrapper, getOptions: () => this.options, getInputElement: () => this.inputManager.getElement() }); this.accessibilityManager.applyInitial(); } _initActionButtonManager() { this.actionButtonManager = new ActionButtonManager({ wrapper: this.wrapper, getMessages: () => this._getMessages(), getSettings: () => this.options.actionButton, isDisabled: () => { const options = this.options; const widgetDisabled = !options.enable; const widgetReadonly = options.readonly; const actionButtonDisabled = options.actionButton?.enable === false; return widgetDisabled || widgetReadonly || actionButtonDisabled; }, isLoading: () => this._loading, hasContent: () => !!this._value?.trim() || this.fileHandler.hasFiles() }); } /** * Gets the input mode (single, multi, or auto). */ _getMode() { const options = this.options; return options.mode || LINE_MODE.AUTO; } /** * Gets the merged messages, including overrides from button settings. */ _getMessages() { const options = this.options; const actionButtonSettings = options.actionButton; const fileSelectButtonSettings = typeof options.fileSelectButton === "object" ? options.fileSelectButton : null; const baseMessages = $.extend({}, DEFAULT_MESSAGES, options.messages); if (actionButtonSettings?.text) { baseMessages.actionButton = actionButtonSettings.text; } if (actionButtonSettings?.loadingText) { baseMessages.actionButtonLoading = actionButtonSettings.loadingText; } if (fileSelectButtonSettings?.text) { baseMessages.fileSelectButton = fileSelectButtonSettings.text; } return baseMessages; } /** * Gets whether the action button should be shown. Always returns true. */ _showActionButton() { return true; } /** * Gets whether the speech-to-text button should be shown. */ _showSpeechToTextButton() { const options = this.options; return options.speechToTextButton === true || options.speechToTextButton === null || options.speechToTextButton === undefined || typeof options.speechToTextButton === "object" && options.speechToTextButton !== null; } /** * Gets whether the file select button should be shown. */ _showFileSelectButton() { const options = this.options; return options.fileSelectButton === true || typeof options.fileSelectButton === "object" && options.fileSelectButton !== null; } /** * Updates the file select button state based on enable/readonly options. */ _updateFileSelectButtonState() { const options = this.options; const fileSelectButton = this.wrapper.find(`[${REFERENCES.fileSelectButton}]`); if (!fileSelectButton.length) { return; } const fileSelectSettings = this._getFileSelectButtonSettings(); const isDisabled = !options.enable || options.readonly || fileSelectSettings?.enable === false; if (isDisabled) { fileSelectButton.addClass(STYLES.disabled); fileSelectButton.attr("aria-disabled", "true"); } else { fileSelectButton.removeClass(STYLES.disabled); fileSelectButton.removeAttr("aria-disabled"); } } /** * Gets the file select button settings. */ _getFileSelectButtonSettings() { const options = this.options; if (typeof options.fileSelectButton === "object" && options.fileSelectButton !== null) { return options.fileSelectButton; } return undefined; } /** * Creates the wrapper element using wrap instead of replaceWith. * This keeps the original element in the DOM so jQuery data binding works. */ _initWrapper() { const options = this.options; const messages = this._getMessages(); const mode = this._getMode(); let modeClass = ""; if (mode === LINE_MODE.SINGLE) { modeClass = ` ${STYLES.promptBoxSingleline}`; } else if (mode === LINE_MODE.MULTI) { modeClass = ` ${STYLES.promptBoxMultiline}`; } const stylingClasses = this._getAppearanceClasses(); const affixConfig = { startAffixTemplate: options.startAffixTemplate, endAffixTemplate: options.endAffixTemplate, topAffixTemplate: options.topAffixTemplate }; const fileSelectButtonConfig = this._getFileSelectButtonConfig(); const buttonSettingsConfig = this._getButtonSettingsConfig(); const renderConfig = { affixConfig, fileSelectButtonConfig, buttonSettingsConfig }; const wrapperContent = renderPromptBox(options.headerTemplate, messages, this._showActionButton(), mode, this._showSpeechToTextButton(), renderConfig); this.wrapper = this.element.wrap(`<div class="${STYLES.input}${stylingClasses} ${STYLES.promptBox}${modeClass}" role="group"></div>`).parent(); this.wrapper.prepend(wrapperContent); this._adoptCustomAffixRefs(); this._syncFileSelectSpacer(); this.element.addClass(STYLES.hidden); this.wrapper.after(this.element); } _adoptCustomAffixRefs() { this._removeBuiltInDuplicate(this.wrapper.find(`[${REFERENCES.startAffix}]`).first(), REFERENCES.fileSelectButton); this._removeBuiltInDuplicate(this.wrapper.find(`[${REFERENCES.startAffix}]`).first(), REFERENCES.sendButton); this._removeBuiltInDuplicate(this.wrapper.find(`[${REFERENCES.startAffix}]`).first(), REFERENCES.speechToTextButton); this._removeBuiltInDuplicate(this.wrapper.find(`[${REFERENCES.endAffix}]`).first(), REFERENCES.fileSelectButton); this._removeBuiltInDuplicate(this.wrapper.find(`[${REFERENCES.endAffix}]`).first(), REFERENCES.sendButton); this._removeBuiltInDuplicate(this.wrapper.find(`[${REFERENCES.endAffix}]`).first(), REFERENCES.speechToTextButton); } _removeBuiltInDuplicate(container, reference) { if (!container?.length) { return; } const matches = container.find(`[${reference}]`); const customMatches = matches.not(BUILT_IN_SELECTOR); if (!customMatches.length) { return; } matches.filter(BUILT_IN_SELECTOR).remove(); } _syncFileSelectSpacer() { const settings = this._getFileSelectButtonSettings(); const endAffix = this.wrapper.find(`[${REFERENCES.endAffix}]`).first(); if (!endAffix.length) { return; } endAffix.children(BUILT_IN_FILE_SPACER_SELECTOR).remove(); if (!settings?.showSpacer || settings._renderAffixLocation === "start") { return; } const fileButtons = endAffix.find(`[${REFERENCES.fileSelectButton}]`); const preferredFileButton = fileButtons.not(BUILT_IN_SELECTOR).first(); const fileButton = preferredFileButton.length ? preferredFileButton : fileButtons.first(); const hasTrailingControls = endAffix.find(`[${REFERENCES.speechToTextButton}], [${REFERENCES.sendButton}]`).length > 0; if (!fileButton.length || !fileButton.parent().is(endAffix) || !hasTrailingControls) { return; } fileButton.after(`<div class="${STYLES.spacer}" data-promptbox-role="file-spacer"></div>`); } /** * Gets appearance CSS classes for fillMode. * PromptBox only supports fillMode at the root level per kendo-themes spec. */ _getAppearanceClasses() { const options = this.options; if (options.fillMode) { return ` k-input-${options.fillMode}`; } return ""; } /** * Gets the file select button configuration for template rendering. */ _getFileSelectButtonConfig() { if (!this._showFileSelectButton()) { return undefined; } const settings = this._getFileSelectButtonSettings(); let accept = settings?.accept; if (!accept && settings?.restrictions?.allowedExtensions?.length) { accept = settings.restrictions.allowedExtensions.map((ext) => ext.startsWith(".") ? ext : `.${ext}`).join(","); } return { enable: settings?.enable, fileInputId: this._fileInputId, accept, multiple: settings?.multiple, showSpacer: settings?.showSpacer, settings }; } /** * Gets the button settings configuration for template rendering. */ _getButtonSettingsConfig() { const options = this.options; const speechOption = options.speechToTextButton; const speechSettings = typeof speechOption === "object" && speechOption !== null ? speechOption : undefined; return { actionButtonSettings: options.actionButton || undefined,