@progress/kendo-ui
Version:
This package is part of the [Kendo UI for jQuery](http://www.telerik.com/kendo-ui) suite.
1,427 lines (1,417 loc) • 125 kB
JavaScript
import { a as htmlService, c as Widget, d as utilsService, i as dateUtilsService, n as widgetRegistryService, o as dateParserService, r as fileUtilsService, s as formatterService, u as domUtilsService } from "./kendo.core-Dic9NZAL.js";
import { r as PromptBox } from "./kendo.promptbox-BTu6cTMO.js";
//#region ../src/chat/collaborators/data-manager.collaborator.ts
/**
* DataManager handles all data operations for the Chat component.
* Manages message storage, retrieval, updates, and data source configuration.
*
* This is an internal collaborator - not a shared service.
*/
var DataManager = class DataManager {
static {
this.DEFAULT_OPTIONS = {
autoSync: true,
schema: { model: {
id: "id",
fields: {
id: { type: "string" },
text: { type: "string" },
authorId: { type: "string" },
authorName: { type: "string" },
authorImageUrl: { type: "string" },
authorImageAltText: { type: "string" },
replyToId: {
type: "string",
defaultValue: null
},
isDeleted: {
type: "boolean",
defaultValue: false
},
isPinned: {
type: "boolean",
defaultValue: false
},
isTyping: {
type: "boolean",
defaultValue: false
},
timestamp: { type: "date" },
files: { parse: (value) => value || [] },
suggestedActions: { parse: (value) => value || [] },
status: {
type: "string",
defaultValue: null
},
failed: {
type: "boolean",
defaultValue: false
},
attachments: { parse: (value) => value || null },
attachmentLayout: {
type: "string",
defaultValue: null
}
}
} }
};
}
static {
this.FIELD_MAP = {
text: "textField",
authorId: "authorIdField",
authorName: "authorNameField",
authorImageUrl: "authorImageUrlField",
authorImageAltText: "authorImageAltTextField",
id: "idField",
timestamp: "timestampField",
files: "filesField",
suggestedActions: "suggestedActionsField",
replyToId: "replyToIdField",
isDeleted: "isDeletedField",
isPinned: "isPinnedField",
isTyping: "isTypingField",
status: "statusField",
failed: "failedField",
attachments: "attachmentsField",
attachmentLayout: "attachmentLayoutField"
};
}
/**
* Constructs a new DataManager instance.
* @param context - Context with chat options and services
*/
constructor(context) {
this.context = context;
this.options = this.buildOptions(context.chatOptions);
this.dataSource = this.createDataSource(context.chatOptions);
}
/**
* Builds internal options from chat options.
*/
buildOptions(chatOptions) {
let options;
if (chatOptions.dataSource instanceof kendo.data.DataSource) {
options = $.extend(true, {}, DataManager.DEFAULT_OPTIONS, chatOptions.dataSource.options);
} else if (Array.isArray(chatOptions.dataSource)) {
options = $.extend(true, {}, DataManager.DEFAULT_OPTIONS, { data: chatOptions.dataSource });
} else {
options = $.extend(true, {}, DataManager.DEFAULT_OPTIONS, chatOptions.dataSource);
}
options.autoAssignId = chatOptions.autoAssignId;
this.mapFields(options.schema.model.fields, chatOptions);
return options;
}
/**
* Creates and configures a data source for the chat component.
*/
createDataSource(chatOptions) {
if (chatOptions.dataSource instanceof kendo.data.DataSource) {
return chatOptions.dataSource;
}
return kendo.data.DataSource.create(this.options);
}
/**
* Maps field configuration from chat options to data source field definitions.
*/
mapFields(fields, chatOptions) {
for (const key in fields) {
if (Object.prototype.hasOwnProperty.call(fields, key) && DataManager.FIELD_MAP[key]) {
const optionKey = DataManager.FIELD_MAP[key];
fields[key].from = chatOptions[optionKey];
}
}
}
/**
* Gets the data source instance.
*/
getDataSource() {
return this.dataSource;
}
/**
* Gets the current data source view.
*/
getView() {
return this.dataSource.view();
}
/**
* Gets the last message in the data source view.
*/
getLastMessage() {
const view = this.getView();
return view.length ? view[view.length - 1] : null;
}
/**
* Gets a message by ID from the data source.
*/
getMessageById(id) {
if (!id) {
return null;
}
return this.dataSource.get(id);
}
/**
* Gets a message by UID from the data source.
*/
getMessageByUid(uid) {
return this.dataSource.getByUid(uid);
}
/**
* Gets a file by UID from a message's files array.
*/
getFileByUid(message, uid) {
if (!message || !uid) {
return null;
}
return message.files.find((file) => file.uid === uid) || null;
}
/**
* Gets the currently pinned message.
*/
getPinnedMessage() {
return this.dataSource.data().find((m) => m.isPinned) || null;
}
/**
* Adds a new message to the data source.
*/
postMessage(message, currentUserId) {
const messageInput = typeof message === "string" ? { text: message } : message;
const messageData = {
id: this.options.autoAssignId ? utilsService.guid() : undefined,
timestamp: new Date(),
files: [],
...messageInput,
authorId: messageInput.authorId?.toString() || currentUserId
};
return this.dataSource.add(messageData);
}
/**
* Updates a message in the data source.
*/
updateMessage(message, newData) {
if (!message) {
return null;
}
let targetMessage = message;
if (!(message instanceof kendo.data.ObservableObject)) {
targetMessage = this.getMessageById(message.id);
}
if (!targetMessage) {
return null;
}
for (const key in newData) {
if (Object.prototype.hasOwnProperty.call(newData, key)) {
targetMessage.set(key, newData[key]);
}
}
return targetMessage;
}
/**
* Marks a message as deleted.
*/
removeMessage(message) {
if (!message) {
return false;
}
message.set("isDeleted", true);
return true;
}
/**
* Pins a message.
*/
pinMessage(message) {
if (!message) {
return false;
}
const pinnedMessage = this.getPinnedMessage();
if (pinnedMessage) {
pinnedMessage.set("isPinned", false);
}
message.set("isPinned", true);
return message;
}
/**
* Clears the currently pinned message.
*/
clearPinnedMessage() {
const pinnedMessage = this.getPinnedMessage();
if (pinnedMessage) {
pinnedMessage.set("isPinned", false);
}
}
};
//#endregion
//#region ../src/chat/constants.ts
/**
* Chat component constants
*
* This module contains all constants used throughout the Chat widget
* and its collaborators.
*/
/** Namespace for event binding */
const NS = ".kendoChat";
/** Message width display modes */
const MESSAGE_WIDTH_MODE = {
STANDARD: "standard",
FULL: "full"
};
/** Message status values */
const MESSAGE_STATUS = {
SENT: "sent",
DELIVERED: "delivered",
SEEN: "seen",
FAILED: "failed"
};
/** Files layout modes */
const FILES_LAYOUT_MODE = {
HORIZONTAL: "horizontal",
VERTICAL: "vertical",
WRAP: "wrap"
};
/** Suggested actions layout modes */
const SUGGESTED_ACTIONS_LAYOUT_MODE = {
SCROLL: "scroll",
WRAP: "wrap",
SCROLLBUTTONS: "scrollbuttons"
};
/** Icon identifiers used in the Chat component */
const ICONS = {
attachment: "paperclip-outline-alt-right",
attachmentMenu: "more-vertical",
checkCircle: "check-circle",
chevronDown: "chevron-down",
chevronLeft: "chevron-left",
chevronRight: "chevron-right",
chevronUp: "chevron-up",
download: "download",
fileError: "file-error",
filePdf: "file-pdf",
microphoneOutline: "microphone-outline",
pin: "pin",
undo: "undo",
copy: "copy",
trash: "trash",
send: "paper-plane",
stop: "stop-sm",
upload: "upload",
warningTriangle: "warning-triangle",
x: "x",
arrowDown: "arrow-down",
retry: "arrow-rotate-cw-outline"
};
/** Command identifiers for menu actions */
const COMMANDS = {
reply: "reply",
copy: "copy",
pin: "pin",
delete: "delete",
download: "download"
};
/** CSS class names used in the Chat component */
const STYLES = {
active: "k-active",
selected: "k-selected",
focus: "k-focus",
expanded: "k-expanded",
disabled: "k-disabled",
hidden: "k-hidden",
generating: "k-generating",
noAvatar: "k-no-avatar",
file: "k-file-box",
fileInfo: "k-file-info",
fileName: "k-file-name",
fileSize: "k-file-size",
fileWrapper: "k-file-box-wrapper",
fileWrapperScrollableStart: "k-file-box-wrapper-scrollable-start",
filesScroll: "k-files-scroll",
filesVertical: "k-files-vertical",
filesWrap: "k-files-wrap",
avatar: "k-avatar",
bubble: "k-bubble",
bubbleContent: "k-bubble-content",
bubbleExpandable: "k-bubble-expandable",
bubbleExpandableIndicator: "k-bubble-expandable-indicator",
chatBubble: "k-chat-bubble",
chatBubbleText: "k-chat-bubble-text",
button: "k-button",
buttonDefaults: "k-button-md k-rounded-md k-button-solid k-button-solid-base",
buttonIcon: "k-button-icon",
buttonPrimary: "k-button-md k-rounded-md k-button-flat k-button-flat-primary",
iconButton: "k-icon-button",
menuButton: "k-menu-button",
chatSend: "k-chat-send",
chatUpload: "k-chat-upload",
downloadButton: "k-chat-download-button",
downloadButtonWrapper: "k-chat-download-button-wrapper",
canvas: "k-chat-canvas",
card: "k-card",
cardAction: "k-card-action",
cardActions: "k-card-actions",
cardActionsVertical: "k-actions-vertical",
cardBody: "k-card-body",
cardDeck: "k-card-deck",
cardDeckScrollWrap: "k-card-deck-scrollwrap",
cardList: "k-card-list",
cardMedia: "k-card-media",
cardRich: "k-card-type-rich",
cardSubtitle: "k-card-subtitle",
cardTitle: "k-card-title",
cardWrapper: "k-card-container",
dropzoneHint: "k-dropzone-hint",
dropzoneIcon: "k-dropzone-icon",
dropzoneInner: "k-dropzone-inner",
externalDropzone: "k-external-dropzone",
header: "k-chat-header",
message: "k-message",
messageAuthor: "k-message-author",
messageGroup: "k-message-group",
messageGroupContent: "k-message-group-content",
messageGroupFullWidth: "k-message-group-full-width",
messageGroupReceiver: "k-message-group-receiver",
messageGroupSender: "k-message-group-sender",
messageListContent: "k-message-list-content",
messageListContentEmpty: "k-message-list-content-empty",
messageRemoved: "k-message-removed",
messageStatus: "k-message-status",
messageTime: "k-message-time",
messageInfo: "k-message-info",
messageFailed: "k-message-failed",
messageFailedContent: "k-message-failed-content",
messageFailedText: "k-message-failed-text",
messageToolbar: "k-chat-message-toolbar",
messageRetryButton: "k-resend-button",
messagePinned: "k-message-pinned",
messageReference: "k-message-reference",
messageReferenceContent: "k-message-reference-content",
messageReferenceReceiver: "k-message-reference-receiver",
messageReferenceSender: "k-message-reference-sender",
scrollButtonIconLeft: "chevron-left",
scrollButtonIconRight: "chevron-right",
suggestion: "k-suggestion",
suggestionGroup: "k-suggestion-group",
suggestionGroupScrollable: "k-suggestion-group-scrollable",
suggestionGroupWrap: "k-suggestion-group-wrap",
suggestionsScroll: "k-suggestions-scroll",
suggestionScrollWrap: "k-suggestion-scrollwrap",
suggestionScrollWrapGradient: "k-suggestion-scrollwrap-gradient",
timestamp: "k-timestamp",
typingIndicator: "k-typing-indicator",
viewWrapper: "k-message-list",
wrapper: "k-chat",
messageBox: "k-message-box",
messageBoxWrapper: "k-message-box-wrapper",
messageBoxSeparator: "k-separator",
spacer: "k-spacer",
scrollFab: "k-chat-scroll-fab",
fab: "k-fab"
};
/** Data attribute references for element selection */
const REFERENCES = {
fileButton: "ref-chat-file-button",
fileMenuButton: "ref-chat-file-menu-button",
fileWrapper: "ref-chat-file-wrapper",
fileCloseButton: "ref-chat-file-close-button",
attachmentActionButton: "ref-chat-attachment-action-button",
userStatusWrapper: "ref-chat-user-status-wrapper",
bubbleExpandableIndicator: "ref-chat-bubble-expandable-indicator",
messageReferencePinWrapper: "ref-chat-message-reference-pin-wrapper",
messageReferenceReplyWrapper: "ref-chat-message-reference-reply-wrapper",
pinnedMessageCloseButton: "ref-chat-pinned-message-close-button",
replyMessageCloseButton: "ref-chat-reply-message-close-button",
leftScrollButton: "ref-chat-left-scroll-button",
rightScrollButton: "ref-chat-right-scroll-button",
sendButton: "ref-chat-message-box-send-button",
speechToTextButton: "ref-chat-message-box-speech-to-text-button",
suggestionGroup: "ref-chat-suggestion-group",
fileUploadInput: "ref-chat-file-upload-input",
viewWrapper: "ref-chat-view-wrapper",
scrollToBottomButton: "ref-chat-scroll-to-bottom-button",
retryButton: "ref-chat-message-retry-button"
};
/** Event names triggered by the Chat component */
const EVENTS = {
sendMessage: "sendMessage",
suggestionClick: "suggestionClick",
unpin: "unpin",
input: "input",
toolbarAction: "toolbarAction",
fileMenuAction: "fileMenuAction",
contextMenuAction: "contextMenuAction",
download: "download",
fileSelect: "fileSelect",
fileRemove: "fileRemove",
executeAction: "executeAction",
resendMessage: "resendMessage",
suggestedActionClick: "suggestedActionClick",
messageToolbarExecute: "messageToolbarExecute",
fileMenuExecute: "fileMenuExecute",
downloadAllFiles: "downloadAllFiles",
expandableToggle: "expandableToggle",
replyMessageCloseButtonClick: "replyMessageCloseButtonClick"
};
/** Common DOM event names */
const CLICK = "click";
const INPUT = "input";
const KEYDOWN = "keydown";
const FOCUS = "focus";
const BLUR = "blur";
const CHANGE = "change";
const SCROLL = "scroll";
/** Text direction constants */
const LTR = "ltr";
const RTL = "rtl";
const K_RTL = "k-rtl";
/** CSS selector prefix */
const DOT = ".";
/** Scrolling delta for suggestion navigation */
const SCROLLING_DELTA = 200;
/** Scroll to bottom threshold in pixels */
const SCROLL_TO_BOTTOM_THRESHOLD = 100;
//#endregion
//#region ../src/chat/collaborators/accessibility-manager.collaborator.ts
const NAVIGATION_NS = `${NS}navigation`;
const ON_FOCUS_MESSAGE_TIME_SELECTOR = `.${STYLES.messageTime}[data-timestamp-visibility='onFocus']`;
function toggleOnFocusMessageTime($bubble, visible) {
$bubble.closest(`.${STYLES.message}`).find(ON_FOCUS_MESSAGE_TIME_SELECTOR).toggleClass(STYLES.hidden, !visible);
}
let isKeyEvent = null;
let isShiftKey = false;
/**
* AccesibilityManager handles ARIA attributes and keyboard navigation
* for the Chat component according to WCAG 2.1 AA compliance.
*
* This is an internal collaborator - not a shared service.
*/
var AccesibilityManager = class {
/**
* Constructs a new AccesibilityManager instance.
* @param context - Context with wrapper, options, and services
*/
constructor(context) {
this.context = context;
}
/**
* Sets up ARIA attributes for the Chat component.
*/
setupAriaAttributes() {
const options = this.context.getOptions();
const messages = options.messages;
const messageList = this.context.wrapper.find(`.${STYLES.viewWrapper}`);
if (messageList.length) {
messageList.attr("role", "log").attr("aria-live", "polite").attr("aria-label", messages.messageListLabel);
}
this.setupBubbleTabNavigation();
this.setupSuggestionAccessibility();
this.setupButtonAccessibility(messages);
this.setupExpandableIndicators();
this.setupScrollButtonsAria();
this.setupMessageBoxAria(messages);
}
/**
* Sets up tabindex navigation for chat bubbles.
*/
setupBubbleTabNavigation() {
const allBubbles = this.context.wrapper.find(`.${STYLES.bubble}`);
const interactableBubbles = allBubbles.filter(function() {
return $(this).find(`.${STYLES.typingIndicator}`).length === 0;
});
const typingIndicatorBubbles = allBubbles.filter(function() {
return $(this).find(`.${STYLES.typingIndicator}`).length > 0;
});
interactableBubbles.attr("tabindex", "0");
typingIndicatorBubbles.attr("tabindex", "-1");
interactableBubbles.off(`${FOCUS}${NAVIGATION_NS} ${BLUR}${NAVIGATION_NS} ${CLICK}${NAVIGATION_NS}`).on(`${FOCUS}${NAVIGATION_NS}`, function() {
const $this = $(this);
allBubbles.each(function() {
toggleOnFocusMessageTime($(this), false);
});
if (isKeyEvent) {
allBubbles.removeClass(STYLES.selected);
}
allBubbles.removeClass(STYLES.focus);
if (isKeyEvent) {
$this.addClass(STYLES.selected);
}
$this.addClass(STYLES.focus);
toggleOnFocusMessageTime($this, true);
isKeyEvent = false;
}).on(`${BLUR}${NAVIGATION_NS}`, function() {
if (isKeyEvent && !isShiftKey) {
$(this).removeClass(STYLES.selected);
}
toggleOnFocusMessageTime($(this), false);
$(this).removeClass(STYLES.focus);
}).on(`${CLICK}${NAVIGATION_NS}`, function(e) {
if ($(e.target).closest(REFERENCES.fileMenuButton)) {
return;
}
$(this).trigger(FOCUS);
});
}
/**
* Sets up accessibility for suggestion groups.
*/
setupSuggestionAccessibility() {
const suggestionGroups = this.context.wrapper.find(`.${STYLES.suggestionGroup}`);
suggestionGroups.each((_index, element) => {
const $group = $(element);
$group.attr("role", "group");
const suggestions = $group.find(`.${STYLES.suggestion}`);
suggestions.each((_suggestionIndex, suggestionElement) => {
const $suggestion = $(suggestionElement);
$suggestion.attr("role", "button").attr("tabindex", "0");
});
});
}
/**
* Sets up accessibility for various buttons.
*/
setupButtonAccessibility(messages) {
const suffixButtons = this.context.wrapper.find(".k-input-suffix > .k-button");
suffixButtons.each((_index, element) => {
const $button = $(element);
if (!$button.attr("role") && element.nodeName.toLowerCase() !== "button") {
$button.attr("role", "button");
}
if (!$button.attr("aria-label") && !$button.attr("title")) {
if ($button.hasClass(STYLES.chatSend)) {
$button.attr("aria-label", messages.actionButton || messages.sendButton);
} else if ($button.hasClass("k-chat-upload")) {
$button.attr("aria-label", messages.fileButton);
}
}
if ($button.hasClass(STYLES.chatSend) && $button.hasClass("k-disabled")) {
$button.attr("aria-disabled", "true");
}
});
const downloadButtons = this.context.wrapper.find(`.${STYLES.downloadButton}`);
downloadButtons.each((_index, element) => {
const $button = $(element);
if (!$button.attr("role") && element.nodeName.toLowerCase() !== "button") {
$button.attr("role", "button");
}
if (!$button.attr("aria-label") && !$button.attr("title")) {
$button.attr("aria-label", messages.downloadAll);
}
});
this.setupCloseButtonAria(REFERENCES.pinnedMessageCloseButton, messages.pinnedMessageCloseButton);
this.setupCloseButtonAria(REFERENCES.replyMessageCloseButton, messages.replyMessageCloseButton);
const fileMenuButtons = this.context.wrapper.find(`[${REFERENCES.fileMenuButton}]`);
fileMenuButtons.each((_index, element) => {
const $button = $(element);
$button.attr("aria-label", messages.fileMenuButton);
$button.attr("title", messages.fileMenuButton);
});
}
/**
* Sets up ARIA attributes for close buttons.
*/
setupCloseButtonAria(reference, label) {
const buttons = this.context.wrapper.find(`[${reference}]`);
buttons.each((_index, element) => {
const $button = $(element);
if (!$button.attr("role") && element.nodeName.toLowerCase() !== "button") {
$button.attr("role", "button");
}
if (!$button.attr("aria-label") && !$button.attr("title")) {
$button.attr("aria-label", label);
$button.attr("title", label);
}
});
}
/**
* Sets up ARIA attributes for expandable indicators.
*/
setupExpandableIndicators() {
const expandableIndicators = this.context.wrapper.find(`.${STYLES.bubbleExpandableIndicator}`);
expandableIndicators.each((_index, element) => {
const $indicator = $(element);
$indicator.attr("role", "button").attr("tabindex", "0");
const bubble = $indicator.closest(`.${STYLES.bubble}`);
const isExpanded = bubble.hasClass(STYLES.expanded);
const label = isExpanded ? "Collapse message" : "Expand message";
$indicator.attr("aria-label", label);
});
}
/**
* Sets up ARIA attributes for scroll buttons.
*/
setupScrollButtonsAria() {
const dir = this.context.getOptions().dir;
const leftScrollButton = this.context.wrapper.find(`[${REFERENCES.leftScrollButton}]`);
const rightScrollButton = this.context.wrapper.find(`[${REFERENCES.rightScrollButton}]`);
const leftText = dir === "rtl" ? "Scroll Right" : "Scroll Left";
const rightText = dir === "rtl" ? "Scroll Left" : "Scroll Right";
leftScrollButton.attr("aria-label", leftText);
leftScrollButton.attr("title", leftText);
rightScrollButton.attr("aria-label", rightText);
rightScrollButton.attr("title", rightText);
}
/**
* Sets up ARIA attributes for the message box.
*/
setupMessageBoxAria(messages) {
const messageBox = this.context.wrapper.find(`.${STYLES.messageBox}`);
messageBox.find("textarea, input:not([type='hidden']):not([type='file']):not([type='button']):not([type='submit']):not([type='reset']):not([type='checkbox']):not([type='radio'])").first().attr("aria-label", messages.messageBoxLabel);
}
/**
* Sets up ARIA attributes for file close buttons.
*/
setFileCloseButtonAria(container) {
const fileCloseButton = container.find(`[${REFERENCES.fileCloseButton}]`);
fileCloseButton.each((_index, element) => {
const $button = $(element);
if (!$button.attr("role") && element.nodeName.toLowerCase() !== "button") {
$button.attr("role", "button");
}
if (!$button.attr("aria-label") && !$button.attr("title")) {
$button.attr("aria-label", "Remove selected file");
$button.attr("title", "Remove selected file");
}
});
}
/**
* Updates ARIA attributes for expandable indicators when their state changes.
*/
updateExpandableIndicatorAria(indicator, isExpanded) {
const label = isExpanded ? "Collapse message" : "Expand message";
indicator.attr("aria-label", label);
}
/**
* Handles keyboard navigation for the Chat component.
*/
handleKeyDown(e, messageBox) {
if (!$(e.target).closest(this.context.wrapper).length) {
return null;
}
const target = $(e.target);
const key = e.keyCode || e.which;
isKeyEvent = true;
isShiftKey = e.shiftKey;
if (target.hasClass(STYLES.bubble)) {
this.handleBubbleKeyDown(e, key);
}
if (target.hasClass(STYLES.suggestion)) {
this.handleClickableElementKeyDown(e, key);
}
if (target.hasClass(STYLES.bubbleExpandableIndicator)) {
this.handleExpandableIndicatorKeyDown(e, key, target);
}
if (target.hasClass("k-input-inner")) {
this.handleMessageInputKeyDown(e, key, messageBox);
}
if (target.closest(".k-input-suffix .k-button").length) {
this.handleClickableElementKeyDown(e, key);
}
return null;
}
/**
* Handles keyboard navigation within bubbles.
*/
handleBubbleKeyDown(e, key) {
const currentBubble = $(e.target);
if (currentBubble.find(`.${STYLES.typingIndicator}`).length > 0) {
return;
}
const allBubbles = this.context.wrapper.find(`.${STYLES.bubble}`);
const bubbles = allBubbles.filter(function() {
return $(this).find(`.${STYLES.typingIndicator}`).length === 0;
});
const currentIndex = bubbles.index(currentBubble);
const keys = utilsService.keys;
switch (key) {
case keys.UP:
e.preventDefault();
this.focusBubbleAtIndex(bubbles, currentIndex - 1);
break;
case keys.DOWN:
e.preventDefault();
this.focusBubbleAtIndex(bubbles, currentIndex + 1);
break;
case keys.HOME:
e.preventDefault();
this.focusBubbleAtIndex(bubbles, 0);
break;
case keys.END:
e.preventDefault();
this.focusBubbleAtIndex(bubbles, bubbles.length - 1);
break;
}
}
/**
* Focuses a bubble at the specified index.
*/
focusBubbleAtIndex(bubbles, index) {
if (bubbles.length === 0 || index < 0 || index >= bubbles.length) {
return;
}
const targetBubble = bubbles.eq(index);
targetBubble.attr("data-keyboard-focus", "true");
targetBubble.focus();
targetBubble.removeAttr("data-keyboard-focus");
}
/**
* Handles keyboard events for expandable indicators.
*/
handleExpandableIndicatorKeyDown(e, key, target) {
const keys = utilsService.keys;
if (key === keys.ENTER || key === keys.SPACEBAR) {
e.preventDefault();
target.trigger(CLICK);
const bubble = target.closest(`.${STYLES.bubble}`);
const isExpanded = bubble.hasClass(STYLES.expanded);
this.updateExpandableIndicatorAria(target, !isExpanded);
}
}
/**
* Handles keyboard events for the message input.
* Note: Enter key handling is delegated to PromptBox component.
*/
handleMessageInputKeyDown(e, key, messageBox) {}
/**
* Handles keyboard events for clickable elements (suggestions, buttons).
*/
handleClickableElementKeyDown(e, key) {
const keys = utilsService.keys;
if (key === keys.ENTER || key === keys.SPACEBAR) {
e.preventDefault();
$(e.target).trigger(CLICK);
}
}
};
//#endregion
//#region ../src/chat/collaborators/message-toolbar.collaborator.ts
/**
* MessageToolbar provides toolbar functionality for chat messages.
* Handles quick actions that can be performed on messages through toolbar buttons.
*
* This is an internal collaborator that extends Widget for event handling.
*/
var MessageToolbar = class MessageToolbar extends Widget {
static {
this.options = { name: "MessageToolbar" };
}
/**
* Constructs a new MessageToolbar instance.
*/
constructor(element, options) {
super(element, $.extend(true, {}, MessageToolbar.options, options));
this.extendItemsConfig();
this.create();
this.attachEvents();
}
/**
* Creates the underlying Kendo UI ToolBar.
*/
create() {
const existingToolBar = this.element.data("kendoToolBar");
if (existingToolBar) {
existingToolBar.destroy();
}
const toolbarOptions = $.extend({}, this.options, { fillMode: "flat" });
delete toolbarOptions.name;
this.toolbar = new kendo.ui.ToolBar(this.element, toolbarOptions);
}
/**
* Extends items configuration with default properties.
*/
extendItemsConfig() {
const items = this.options.items;
if (items) {
items.forEach((item) => {
item.attributes = item.attributes || {};
const ariaLabel = item.attributes["aria-label"];
item.attributes["data-command"] = item.name.toLowerCase();
item.attributes["aria-label"] = ariaLabel ?? item.name;
item.type = "button";
item.fillMode = "flat";
item.overflow = "never";
});
}
}
/**
* Attaches event handlers to the toolbar.
*/
attachEvents() {
this.toolbar.bind(CLICK, this.onClick.bind(this));
}
/**
* Handles toolbar button clicks.
*/
onClick(e) {
const message = e.target.closest(DOT + STYLES.message);
const command = e.target.data("command");
if (command) {
this.executeCommand(command, e.target, message);
}
}
/**
* Executes a command from the toolbar.
*/
executeCommand(command, item, message) {
this.trigger("execute", {
type: command,
item,
message
});
}
destroy() {
if (this.toolbar) {
this.toolbar.destroy();
this.toolbar = null;
}
this.element.empty();
}
};
//#endregion
//#region ../src/chat/collaborators/file-menu.collaborator.ts
/**
* FileMenu provides dropdown button functionality for file attachments.
* Handles actions like download, preview, and delete for individual files.
*
* This is an internal collaborator that extends Widget for event handling.
*/
var FileMenu = class FileMenu extends Widget {
static {
this.options = {
name: "ChatFileMenu",
items: []
};
}
/**
* Constructs a new FileMenu instance.
*/
constructor(element, options) {
super(element, $.extend(true, {}, FileMenu.options, options));
this.setCommandAttributes();
this.createDropdownButton();
}
/**
* Creates a dropdown button on the element.
*/
createDropdownButton() {
this.dropdownButton = new kendo.ui.DropDownButton(this.element, {
items: this.options.items,
fillMode: "flat",
icon: ICONS.attachmentMenu
});
this.dropdownButton.bind("click", this.onClick.bind(this)).bind("open", this.onOpen.bind(this)).bind("close", this.onClose.bind(this));
}
/**
* Sets command attributes on menu items for identification.
*/
setCommandAttributes() {
this.options.items.forEach((item) => {
item.attributes = item.attributes || {};
item.attributes["data-command"] = item.name.toLowerCase();
item.id = item.name.toLowerCase();
});
}
/**
* Handles dropdown button item click.
*/
onClick(e) {
const command = e.id;
const file = $(e.sender.element).closest(DOT + STYLES.file);
const message = $(e.sender.element).closest(DOT + STYLES.message);
if (command) {
this.executeCommand(command, $(e.sender.element), file, message);
}
}
/**
* Handles dropdown open event.
*/
onOpen(e) {
let setActive = true;
const target = $(e.sender.element);
if (target.closest(DOT + STYLES.messageRemoved).length || target.find(DOT + STYLES.typingIndicator).length) {
e.preventDefault();
setActive = false;
}
this.setActive(target, setActive);
}
/**
* Handles dropdown close event.
*/
onClose(e) {
const target = $(e.sender.element);
const message = target.closest(DOT + STYLES.message);
this.setActive(target, false);
this.trigger("close", { message });
}
/**
* Executes a command from the file menu.
*/
executeCommand(command, item, file, message) {
this.trigger("execute", {
type: command,
item,
file,
message
});
}
/**
* Sets the active state for a target element.
*/
setActive(target, active) {
const bubble = target.closest(DOT + STYLES.bubble);
if (active) {
bubble.addClass(STYLES.active);
} else {
bubble.removeClass(STYLES.active);
}
}
destroy() {
if (this.dropdownButton) {
this.dropdownButton.destroy();
this.dropdownButton = null;
}
super.destroy();
}
};
//#endregion
//#region ../src/chat/templates/common.template.ts
/**
* Renders an avatar element for a message author.
*/
const renderAvatar = (url, altText) => {
return new kendo.ui.Avatar("<div>", {
type: "image",
image: url,
alt: htmlService.encode(altText ?? "")
}).wrapper[0].outerHTML;
};
/**
* Renders a typing indicator animation.
*/
const renderTypingIndicator = () => {
return `<div class="${STYLES.typingIndicator}">
<span></span>
<span></span>
<span></span>
</div>`;
};
/**
* Renders a single file attachment item.
* Uses small-sized buttons for menu/download actions per Chat v3 spec.
*/
const renderFile = (file, closeButton, fileMenuButton = true) => {
const closeButtonHtml = closeButton ? new kendo.ui.Button(`<button class="" ${REFERENCES.fileCloseButton}>`, {
icon: ICONS.x,
fillMode: "flat",
size: "small"
}).wrapper[0].outerHTML : "";
const fileMenuButtonHtml = fileMenuButton && !closeButton ? new kendo.ui.Button(`<button class="${STYLES.menuButton}" ${REFERENCES.fileMenuButton}>`, {
icon: ICONS.attachmentMenu,
fillMode: "flat",
size: "small"
}).wrapper[0].outerHTML : "";
return `<li class="${STYLES.file}" data-uid="${htmlService.encode(file.uid)}">
${kendo.ui.icon({
icon: fileUtilsService.getFileGroup(file.extension, true),
size: "xlarge"
})}
<div class="${STYLES.fileInfo}">
<span class="${STYLES.fileName}">${htmlService.encode(file.name)}</span>
<span class="${STYLES.fileSize}">${htmlService.encode(fileUtilsService.getFileSizeMessage(file.size))}</span>
</div>
${closeButtonHtml}
${fileMenuButtonHtml}
</li>`;
};
//#endregion
//#region ../src/chat/templates/file.template.ts
/**
* Renders a list of file attachments with layout mode support.
* Uses small-sized buttons per Chat v3 spec.
*/
const renderFiles = (files, downloadAll, messages, closeButton, layoutMode = FILES_LAYOUT_MODE.VERTICAL) => {
if (!files?.length) {
return "";
}
let fileItems = "";
files.forEach((file) => {
fileItems += renderFile(file, closeButton ?? false, true);
});
let layoutClass = "";
if (layoutMode === FILES_LAYOUT_MODE.VERTICAL) {
layoutClass = ` ${STYLES.filesVertical}`;
} else if (layoutMode === FILES_LAYOUT_MODE.WRAP) {
layoutClass = ` ${STYLES.filesWrap}`;
}
let html = `<ul class="${STYLES.fileWrapper}${layoutClass}" ${REFERENCES.fileWrapper}>`;
if (layoutMode === FILES_LAYOUT_MODE.HORIZONTAL) {
html += `<div class="${STYLES.filesScroll}">${fileItems}</div>`;
} else {
html += fileItems;
}
html += "</ul>";
if (downloadAll && files.length > 1 && messages?.downloadAll) {
html += `<div class="${STYLES.downloadButtonWrapper}">
${new kendo.ui.Button(`<button class="${STYLES.downloadButton}">${messages.downloadAll}</button>`, {
icon: ICONS.download,
fillMode: "flat",
size: "small"
}).wrapper[0].outerHTML}
</div>`;
}
return html;
};
/**
* Renders a list of file attachments with the specified layout mode.
* Uses small-sized buttons per Chat v3 spec.
* @param files - Array of files to render
* @param downloadAll - Whether to show a "download all" button
* @param messages - Localization messages
* @param closeButton - Whether to show close buttons on files
* @param layoutMode - Layout mode: "horizontal" | "vertical" | "wrap"
*/
const renderFilesWithLayout = (files, downloadAll, messages, closeButton, layoutMode = FILES_LAYOUT_MODE.HORIZONTAL) => {
if (!files?.length) {
return "";
}
let fileItems = "";
files.forEach((file) => {
fileItems += renderFile(file, closeButton ?? false, true);
});
let layoutClass = "";
if (layoutMode === FILES_LAYOUT_MODE.VERTICAL) {
layoutClass = ` ${STYLES.filesVertical}`;
} else if (layoutMode === FILES_LAYOUT_MODE.WRAP) {
layoutClass = ` ${STYLES.filesWrap}`;
}
let html = `<ul class="${STYLES.fileWrapper}${layoutClass}" ${REFERENCES.fileWrapper}>`;
if (layoutMode === FILES_LAYOUT_MODE.HORIZONTAL) {
html += `<div class="${STYLES.filesScroll}">${fileItems}</div>`;
} else {
html += fileItems;
}
html += "</ul>";
if (downloadAll && files.length > 1) {
html += `<div class="${STYLES.downloadButtonWrapper}">
${new kendo.ui.Button(`<button class="${STYLES.downloadButton}">${messages.downloadAll}</button>`, {
icon: ICONS.download,
fillMode: "flat",
size: "small"
}).wrapper[0].outerHTML}
</div>`;
}
return html;
};
/**
* Renders a message reference (for reply or pinned messages).
*/
const renderMessageReference = (context) => {
const { text, files, isOwnMessage, isPinMessage, isDeleted, renderCloseButton, renderFileMenuButton, messages } = context;
const messageReferenceSenderStyle = isOwnMessage ? STYLES.messageReferenceSender : STYLES.messageReferenceReceiver;
const pinMessageReferenceStyle = isPinMessage ? STYLES.messagePinned : "";
const closeButtonReference = isPinMessage ? REFERENCES.pinnedMessageCloseButton : REFERENCES.replyMessageCloseButton;
const wrapperReference = isPinMessage ? REFERENCES.messageReferencePinWrapper : REFERENCES.messageReferenceReplyWrapper;
let content = htmlService.convertTextUrlToLink(text || "");
if (!content) {
content = files?.length ? renderFile(files[0], false, renderFileMenuButton ?? false) : "";
}
if (isDeleted && messages) {
content = isOwnMessage ? htmlService.encode(messages.selfMessageDeleted) : htmlService.encode(messages.otherMessageDeleted);
}
return `<div class="${STYLES.messageReference} ${messageReferenceSenderStyle} ${pinMessageReferenceStyle}" ${wrapperReference}>
${isPinMessage ? kendo.ui.icon({
icon: ICONS.pin,
size: "xlarge"
}) : ""}
<div class="${STYLES.messageReferenceContent}">${content}</div>
<span class="${STYLES.spacer}"></span>
${renderCloseButton ? new kendo.ui.Button(`<button ${closeButtonReference}>`, {
icon: ICONS.x,
fillMode: "flat"
}).wrapper[0].outerHTML : ""}
</div>`;
};
//#endregion
//#region ../src/chat/templates/message.template.ts
/**
* Renders the retry button for failed messages.
*/
const renderRetryButton = (message, retryText) => {
if (!message.failed || !message.isOwnMessage) {
return "";
}
return kendo.html.renderButton(`<button class="${STYLES.messageRetryButton}" ${REFERENCES.retryButton} title="${retryText}" aria-label="${retryText}" data-uid="${message.uid || ""}"></button>`, {
icon: ICONS.retry,
size: "small",
fillMode: "clear"
});
};
const renderFailedContent = (failedText) => {
return `<span class="${STYLES.messageFailedContent}">
${kendo.ui.icon({
icon: ICONS.warningTriangle,
size: "xsmall"
})}
<span class="${STYLES.messageFailedText}">${htmlService.encode(failedText)}</span>
</span>`;
};
/**
* Renders the message status indicator.
* Supports custom status settings with icon, text, and cssClass for enhanced display.
*/
const renderMessageStatus = (status, message, statusTemplate, statusSettings) => {
if (!status) {
return "";
}
if (statusTemplate) {
return statusTemplate({
status,
message
});
}
const settings = statusSettings?.[status];
const statusClass = `${STYLES.messageStatus}${settings?.cssClass ? " " + settings.cssClass : ""}`;
let iconHtml = "";
if (settings?.icon) {
iconHtml = kendo.ui.icon({ icon: settings.icon });
}
const statusText = settings?.text !== undefined ? settings.text : status.charAt(0).toUpperCase() + status.slice(1);
return `<span class="${statusClass}">${iconHtml}${htmlService.encode(statusText)}</span>`;
};
/**
* Renders a single attachment card.
*/
const renderAttachment = (attachment, message, attachmentIndex, attachmentTemplate) => {
if (attachmentTemplate) {
return attachmentTemplate({
attachment,
message
});
}
const title = attachment.title ? `<div class="${STYLES.cardTitle}">${htmlService.encode(attachment.title)}</div>` : "";
const subtitle = attachment.subtitle ? `<div class="${STYLES.cardSubtitle}">${htmlService.encode(attachment.subtitle)}</div>` : "";
const image = attachment.thumbnailUrl ? `<div class="${STYLES.cardMedia}"><img src="${htmlService.encode(attachment.thumbnailUrl)}" alt="" /></div>` : "";
const actions = attachment.actions?.length ? `<div class="${STYLES.cardActions}">${attachment.actions.map((action, actionIndex) => {
const label = action.title || action.text;
return `<button class="${STYLES.cardAction} ${STYLES.button}" ${REFERENCES.attachmentActionButton} data-attachment-index="${attachmentIndex}" data-action-index="${actionIndex}">${htmlService.encode(label)}</button>`;
}).join("")}</div>` : "";
return `<div class="${STYLES.card}">
${image}
<div class="${STYLES.cardBody}">
${title}
${subtitle}
</div>
${actions}
</div>`;
};
/**
* Renders attachments with the specified layout.
*/
const renderAttachments = (attachments, message, layout = "list", attachmentTemplate) => {
if (!attachments?.length) {
return "";
}
const attachmentsHtml = attachments.map((attachment, attachmentIndex) => renderAttachment(attachment, message, attachmentIndex, attachmentTemplate)).join("");
if (layout === "carousel") {
return `<div class="${STYLES.cardDeckScrollWrap}">
${new kendo.ui.Button(`<button ${REFERENCES.leftScrollButton}>`, { icon: ICONS.chevronLeft }).wrapper[0].outerHTML}
<div class="${STYLES.cardDeck}">${attachmentsHtml}</div>
${new kendo.ui.Button(`<button ${REFERENCES.rightScrollButton}>`, { icon: ICONS.chevronRight }).wrapper[0].outerHTML}
</div>`;
}
return `<div class="${STYLES.cardList}">${attachmentsHtml}</div>`;
};
/**
* Renders a single chat message.
*/
const renderMessage = (message, replyMessage, downloadAll, messages, expandable, messageTimeFormat, skipSanitization, statusTemplate, filesLayoutMode = FILES_LAYOUT_MODE.VERTICAL, contentTemplate, attachmentTemplate, attachmentLayout, timestampVisibility = "onFocus", statusSettings, showMessageTime = true) => {
const isDeleted = message.isDeleted;
const isFailedStatus = message.status === MESSAGE_STATUS.FAILED;
const isFailed = message.isOwnMessage && (message.failed || isFailedStatus);
const messageStatus = !message.isOwnMessage && isFailedStatus ? undefined : message.status;
const expandableClasses = expandable && !message.isTyping ? [STYLES.bubbleExpandable, STYLES.expanded] : [];
const replyMessageHtml = replyMessage ? renderMessageReference({
text: replyMessage.text,
files: replyMessage.files,
isOwnMessage: replyMessage.isOwnMessage,
isPinMessage: false,
renderCloseButton: false,
renderFileMenuButton: false
}) : "";
let messageContent = "";
if (message.isTyping && !message.isOwnMessage) {
messageContent = renderTypingIndicator();
} else if (contentTemplate && !isDeleted) {
messageContent = contentTemplate(message);
} else if (isDeleted) {
messageContent = message.isOwnMessage ? htmlService.encode(messages.selfMessageDeleted) : htmlService.encode(messages.otherMessageDeleted);
} else {
messageContent = htmlService.convertTextUrlToLink(message.text || "", skipSanitization);
}
const timeAttributes = timestampVisibility === "onFocus" ? ` data-timestamp-visibility="${timestampVisibility}"` : "";
const timeClasses = [STYLES.messageTime, timestampVisibility === "onFocus" ? STYLES.hidden : ""].filter(Boolean).join(" ");
const timeHtml = showMessageTime ? `<time class="${timeClasses}"${timeAttributes}>${formatterService.toString(dateParserService.parseDate(message.timestamp), messageTimeFormat)}</time>` : "";
const statusHtml = renderMessageStatus(messageStatus, message, statusTemplate, statusSettings || null);
const retryHtml = isFailed ? renderRetryButton(message, messages.retryMessage || "Retry") : "";
const failedContentHtml = isFailed ? renderFailedContent(statusSettings?.[MESSAGE_STATUS.FAILED]?.text || "Failed to send") : "";
const messageInfoHtml = `<div class="${STYLES.messageInfo}">${statusHtml}${timeHtml}${failedContentHtml}</div>`;
return `<div class="${STYLES.message}${isDeleted ? " " + STYLES.messageRemoved : ""}${isFailed ? " " + STYLES.messageFailed : ""}" data-uid="${htmlService.encode(message.uid ?? "")}">
${retryHtml}
<div class="${STYLES.chatBubble} ${STYLES.bubble} ${expandableClasses.join(" ")}">
<div class="${STYLES.bubbleContent}">
${replyMessageHtml}
<span class="${STYLES.chatBubbleText}">${messageContent}</span>
${!isDeleted ? renderFiles(message.files, downloadAll, messages, false, filesLayoutMode) : ""}
</div>
${expandable && !message.isTyping ? `<span class="${STYLES.bubbleExpandableIndicator}" ${REFERENCES.bubbleExpandableIndicator}>${kendo.ui.icon({ icon: ICONS.chevronUp })}</span>` : ""}
</div>
${messageInfoHtml}
${!isDeleted ? `<div class="${STYLES.messageToolbar}"></div>` : ""}
</div>`;
};
/**
* Renders a message group with author avatar and multiple messages.
*/
const renderMessageGroup = (context) => {
const { message, author, isOwnMessage, replyMessage, downloadAll = true, messages, expandable = false, fullWidth = false, messageTimeFormat = "ddd MMM dd yyyy", timestampTemplate, statusTemplate, showTimestamp = false, messageTemplate, skipSanitization = false, messageSettings, filesLayoutMode = FILES_LAYOUT_MODE.VERTICAL, contentTemplate, attachmentTemplate, attachmentLayout, userStatusTemplate, timestampVisibility = "onFocus", statusSettings, showMessageTime = true } = context;
const effectiveShowAvatar = messageSettings?.showAvatar !== undefined ? messageSettings.showAvatar && author && author.imageUrl : author && author.imageUrl;
const effectiveShowUsername = messageSettings?.showUsername !== undefined ? messageSettings.showUsername : true;
const effectiveExpandable = messageSettings?.allowMessageCollapse !== undefined ? messageSettings.allowMessageCollapse : expandable;
const effectiveFullWidth = messageSettings?.messageWidthMode === "full" || fullWidth;
const groupClasses = [
STYLES.messageGroup,
isOwnMessage ? STYLES.messageGroupSender : STYLES.messageGroupReceiver,
effectiveShowAvatar ? "" : STYLES.noAvatar,
effectiveFullWidth ? STYLES.messageGroupFullWidth : ""
].filter(Boolean).join(" ");
let timestampContent = "";
if (showTimestamp && message.timestamp) {
const messageDate = dateParserService.parseDate(message.timestamp);
if (utilsService.isFunction(timestampTemplate)) {
timestampContent = timestampTemplate({
date: messageDate,
message
});
} else {
const relativeDateText = dateUtilsService.getRelativeDateString(messageDate);
timestampContent = `<div class="${STYLES.timestamp}">${relativeDateText}</div>`;
}
}
const userStatusHtml = effectiveShowAvatar && userStatusTemplate ? userStatusTemplate({ message: {
...message,
author
} }) : "";
const effectiveAttachmentLayout = message.attachmentLayout || attachmentLayout || "list";
const attachmentsHtml = !message.isDeleted ? renderAttachments(message.attachments, message, effectiveAttachmentLayout, attachmentTemplate) : "";
let messageHtml;
if (messageTemplate) {
messageHtml = messageTemplate({
...message,
isOwnMessage,
author
}, replyMessage ?? null, downloadAll, messages, effectiveExpandable, messageTimeFormat, skipSanitization, statusTemplate);
} else {
messageHtml = renderMessage({
...message,
isOwnMessage,
author
}, replyMessage ?? null, downloadAll, messages, effectiveExpandable, messageTimeFormat, skipSanitization, statusTemplate, filesLayoutMode, contentTemplate, attachmentTemplate, attachmentLayout, timestampVisibility, statusSettings, showMessageTime);
}
return `${showTimestamp && timestampContent ? timestampContent : ""}
<div class="${groupClasses}">${userStatusHtml ? `<div ${REFERENCES.userStatusWrapper}>${userStatusHtml}</div>` : ""}
${effectiveShowAvatar ? renderAvatar(author.imageUrl, author.imageAltText) : ""}
<div class="${STYLES.messageGroupContent}">
${effectiveShowUsername ? `<span class="${STYLES.messageAuthor}">${htmlService.encode(author.name ?? "")}</span>` : ""}
${messageHtml}
</div>
</div>
${attachmentsHtml}`;
};
//#endregion
//#region ../src/chat/templates/suggestions.template.ts
/**
* Renders a list of suggestions/suggested actions with the specified layout mode.
* @param suggestions - Array of suggestions to render
* @param layoutMode - Layout mode: "scroll" | "wrap" | "scrollbuttons"
* @param isRtl - Whether RTL direction is used
*/
const renderSuggestions = (suggestions) => {
if (!suggestions?.length) {
return "";
}
const suggestionItems = suggestions.map((suggestion) => `<span class="${STYLES.suggestion}">${htmlService.encode(suggestion.text)}</span>`).join("");
return `<div class="${STYLES.suggestionGroup}" ${REFERENCES.suggestionGroup}>${suggestionItems}</div>`;
};
/**
* Renders suggestions with the specified layout mode from an array of suggestions.
* @param suggestions - Array of suggestions to render
* @param layoutMode - Layout mode: "scroll" | "wrap" | "scrollbuttons"
* @param isRtl - Whether RTL direction is used
*/
const renderSuggestionsWithLayout = (suggestions, layoutMode = SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLL, isRtl = false) => {
if (!suggestions?.length) {
return "";
}
const suggestionItems = suggestions.map((suggestion) => `<span class="${STYLES.suggestion}">${htmlService.encode(suggestion.text)}</span>`).join("");
return wrapSuggestionsWithLayout(suggestionItems, layoutMode, isRtl);
};
/**
* Wraps pre-rendered suggestion content with the specified layout mode.
* Use this when you have custom-rendered content from a template.
* @param content - Pre-rendered HTML content (suggestion items)
* @param layoutMode - Layout mode: "scroll" | "wrap" | "scrollbuttons"
* @param isRtl - Whether RTL direction is used
*/
const wrapSuggestionsWithLayout = (content, layoutMode = SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLL, isRtl = false) => {
if (!content) {
return "";
}
if (layoutMode === SUGGESTED_ACTIONS_LAYOUT_MODE.WRAP) {
return `<div class="${STYLES.suggestionGroup}" ${REFERENCES.suggestionGroup}>${content}</div>`;
}
const layoutClass = layoutMode === SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLL ? STYLES.suggestionGroupScrollable : "";
const groupClass = layoutClass ? `${STYLES.suggestionGroup} ${layoutClass}` : STYLES.suggestionGroup;
const scrollContent = `<div class="${STYLES.suggestionsScroll}">${content}</div>`;
const suggestionsElement = `<div class="${groupClass}" ${REFERENCES.suggestionGroup}>${scrollContent}</div>`;
if (layoutMode === SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLLBUTTONS) {
return renderScrollableSuggestions(suggestionsElement, isRtl);
}
return suggestionsElement;
};
/**
* Renders scrollable suggestions with navigation buttons and gradient styling.
*/
const renderScrollableSuggestions = (suggestionsElement, isRtl) => {
const leftIcon = isRtl ? ICONS.chevronRight : ICONS.chevronLeft;
const rightIcon = isRtl ? ICONS.chevronLeft : ICONS.chevronRight;
return `<div class="${STYLES.suggestionScrollWrap} ${STYLES.suggestionScrollWrapGradient}">
${new kendo.ui.Button(`<button ${REFERENCES.leftScrollButton}>`, { icon: leftIcon }).wrapper[0].outerHTML}
${suggestionsElement}
${new kendo.ui.Button(`<button ${REFERENCES.rightScrollButton}>`, { icon: rightIcon }).wrapper[0].outerHTML}
</div>`;
};
/**
* Renders the chat header using Toolbar.
* @param items - Array of toolbar items configuration
* @param headerTemplate - Optional custom header template that overrides items
*/
const renderHeader = (items, headerTemplate) => {
if (headerTemplate) {
return `<div class="${STYLES.header}">${headerTemplate()}</div>`;
}
if (!items || items.length === 0) {
return "";
}
const toolbarElement = $(`<div class="${STYLES.header}">`);
new kendo.ui.ToolBar(toolbarElement, { items });
return toolbarElement[0].outerHTML;
};
//#endregion
//#region ../src/chat/collaborators/chat-view/message-renderer.ts
var MessageRenderer = class {
constructor(context) {
this.context = context;
}
renderMessage(message) {
const options = this.context.options;
const componentMessages = options.messages;
const author = {
id: message.authorId,
name: message.authorName,
imageUrl: me