@progress/kendo-ui
Version:
This package is part of the [Kendo UI for jQuery](http://www.telerik.com/kendo-ui) suite.
4,702 lines • 179 kB
JavaScript
const require_kendo_core = require("./kendo.core-kmRaf_Lg.js");
require("./kendo.icons.js");
require("./kendo.html.button.js");
require("./kendo.button.js");
require("./kendo.data-T0mqWOrQ.js");
require("./kendo.dropdownbutton.js");
require("./kendo.menu.js");
require("./kendo.toolbar.js");
require("./kendo.avatar.js");
const require_kendo_promptbox = require("./kendo.promptbox-CrpKT-40.js");
//#region ../src/chat/collaborators/data-manager.collaborator.ts
var AuxiliaryReferenceStore = class {
constructor(pinnedMessages) {
this.resolvedReferenceMessages = [];
this.pinnedMessages = pinnedMessages;
}
getMessageById(id) {
if (!id) return null;
return this._getPinnedMessageById(id) || this._getResolvedReferenceMessageById(id);
}
getPinnedMessage() {
for (let index = this.pinnedMessages.length - 1; index >= 0; index--) if (this.pinnedMessages[index].isPinned) return this.pinnedMessages[index];
return this.pinnedMessages[this.pinnedMessages.length - 1] || null;
}
syncPinnedMessageState(message, isPinned, setPinnedState) {
const auxiliaryPinnedMessage = this._getPinnedMessageById(message?.id);
if (auxiliaryPinnedMessage && auxiliaryPinnedMessage !== message) setPinnedState(auxiliaryPinnedMessage, isPinned);
}
cacheResolvedReferenceMessages(messages) {
(messages || []).forEach((message) => {
if (!message?.id) return;
const existingMessage = this._getResolvedReferenceMessageById(message.id);
if (existingMessage) {
const existingIndex = this.resolvedReferenceMessages.indexOf(existingMessage);
this.resolvedReferenceMessages.splice(existingIndex, 1, message);
return;
}
this.resolvedReferenceMessages.push(message);
});
}
_getPinnedMessageById(id) {
return this._getMessageFromCollectionById(this.pinnedMessages, id);
}
_getResolvedReferenceMessageById(id) {
return this._getMessageFromCollectionById(this.resolvedReferenceMessages, id);
}
_getMessageFromCollectionById(collection, id) {
if (!id) return null;
for (let index = 0; index < collection.length; index++) if (String(collection[index].id) === String(id)) return collection[index];
return null;
}
};
var RemoteRangeBookkeeping = class {
constructor() {
this.startIndex = 0;
this.initialized = false;
}
reset() {
this.startIndex = 0;
this.initialized = false;
}
setStart(startIndex) {
this.startIndex = Math.max(0, startIndex);
this.initialized = true;
}
sync(totalCount, loadedCount) {
this.setStart(Math.max(0, totalCount - loadedCount));
return this.getRange(loadedCount);
}
getRange(loadedCount) {
const startIndex = this.initialized ? this.startIndex : 0;
return {
startIndex,
endIndex: startIndex + loadedCount
};
}
};
/**
* 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);
this.auxiliaryReferences = new AuxiliaryReferenceStore(context.chatOptions.pinnedMessages || []);
this.remoteRange = new RemoteRangeBookkeeping();
}
_isRemoteDataSource() {
return !!this.dataSource?.options?.serverPaging;
}
_setPinnedState(message, isPinned) {
if (!message) return;
if (typeof message.set === "function") {
message.set("isPinned", isPinned);
return;
}
message.isPinned = isPinned;
}
/**
* 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.getLoadedMessageById(id) || this.getAuxiliaryMessageById(id);
}
getAuxiliaryMessageById(id) {
if (!id) return null;
return this.auxiliaryReferences.getMessageById(id);
}
cacheResolvedReferenceMessages(messages) {
this.auxiliaryReferences.cacheResolvedReferenceMessages(messages);
}
getLoadedMessageById(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) || this.auxiliaryReferences.getPinnedMessage() || 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 ? require_kendo_core.utilsService.guid() : void 0,
timestamp: /* @__PURE__ */ 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.getLoadedMessageById(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 && String(pinnedMessage.id) !== String(message.id)) {
this._setPinnedState(pinnedMessage, false);
this.auxiliaryReferences.syncPinnedMessageState(pinnedMessage, false, this._setPinnedState.bind(this));
}
this._setPinnedState(message, true);
this.auxiliaryReferences.syncPinnedMessageState(message, true, this._setPinnedState.bind(this));
return message;
}
/**
* Clears the currently pinned message.
*/
clearPinnedMessage() {
const pinnedMessage = this.getPinnedMessage();
this._setPinnedState(pinnedMessage, false);
this.auxiliaryReferences.syncPinnedMessageState(pinnedMessage, false, this._setPinnedState.bind(this));
}
resetRemoteRange() {
this.remoteRange.reset();
}
setRemoteRangeStart(startIndex) {
this.remoteRange.setStart(startIndex);
}
syncRemoteRange(totalCount = this.getTotalCount()) {
if (!this._isRemoteDataSource()) {
this.resetRemoteRange();
return this.getLoadedRange();
}
return this.remoteRange.sync(totalCount, this.getLoadedCount());
}
getLoadedCount() {
return this.dataSource.data().length;
}
getLoadedRange() {
const loadedCount = this.getLoadedCount();
if (!this._isRemoteDataSource()) return {
startIndex: 0,
endIndex: loadedCount
};
return this.remoteRange.getRange(loadedCount);
}
isRangeLoaded(startIndex, endIndex) {
const loadedRange = this.getLoadedRange();
return startIndex >= loadedRange.startIndex && endIndex <= loadedRange.endIndex;
}
getMessagesInRange(startIndex, endIndex) {
const data = this.dataSource.data();
const loadedRange = this.getLoadedRange();
const safeStart = Math.max(startIndex, loadedRange.startIndex);
const safeEnd = Math.min(endIndex, loadedRange.endIndex);
const result = [];
if (safeEnd <= safeStart) return result;
const localStart = safeStart - loadedRange.startIndex;
const localEnd = safeEnd - loadedRange.startIndex;
for (let i = localStart; i < localEnd; i++) result.push(data[i]);
return result;
}
getMessageIndexById(id) {
if (!id) return -1;
const data = this.dataSource.data();
const loadedRange = this.getLoadedRange();
for (let i = 0; i < data.length; i++) if (String(data[i].id) === String(id)) return loadedRange.startIndex + i;
return -1;
}
getTotalCount() {
if (this._isRemoteDataSource()) return this.dataSource.total();
return this.dataSource.data().length;
}
};
//#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",
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",
pin: "pin",
undo: "undo",
copy: "copy",
trash: "trash",
send: "paper-plane",
stop: "stop",
upload: "upload",
warningTriangle: "warning-triangle",
x: "x",
arrowDown: "arrow-down",
retry: "arrow-rotate-cw"
};
/** 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",
referencedMessageClick: "referencedMessageClick",
suggestedActionClick: "suggestedActionClick",
messageToolbarExecute: "messageToolbarExecute",
fileMenuExecute: "fileMenuExecute",
downloadAllFiles: "downloadAllFiles",
expandableToggle: "expandableToggle",
replyMessageCloseButtonClick: "replyMessageCloseButtonClick"
};
/** Common DOM event names */
const CLICK = "click";
const FOCUS = "focus";
const BLUR = "blur";
const CHANGE = "change";
//#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 messages = this.context.getOptions().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() {
this.context.wrapper.find(`.${STYLES.suggestionGroup}`).each((_index, element) => {
const $group = $(element);
$group.attr("role", "group");
$group.find(`.${STYLES.suggestion}`).each((_suggestionIndex, suggestionElement) => {
$(suggestionElement).attr("role", "button").attr("tabindex", "0");
});
});
}
/**
* Sets up accessibility for various buttons.
*/
setupButtonAccessibility(messages) {
this.context.wrapper.find(".k-input-suffix > .k-button").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");
});
this.context.wrapper.find(`.${STYLES.downloadButton}`).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);
this.context.wrapper.find(`[${REFERENCES.fileMenuButton}]`).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) {
this.context.wrapper.find(`[${reference}]`).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() {
this.context.wrapper.find(`.${STYLES.bubbleExpandableIndicator}`).each((_index, element) => {
const $indicator = $(element);
$indicator.attr("role", "button").attr("tabindex", "0");
const label = $indicator.closest(`.${STYLES.bubble}`).hasClass(STYLES.expanded) ? "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) {
this.context.wrapper.find(`.${STYLES.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) {
container.find(`[${REFERENCES.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 bubbles = this.context.wrapper.find(`.${STYLES.bubble}`).filter(function() {
return $(this).find(`.${STYLES.typingIndicator}`).length === 0;
});
const currentIndex = bubbles.index(currentBubble);
const keys = require_kendo_core.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 = require_kendo_core.utilsService.keys;
if (key === keys.ENTER || key === keys.SPACEBAR) {
e.preventDefault();
target.trigger(CLICK);
const isExpanded = target.closest(`.${STYLES.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 = require_kendo_core.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 require_kendo_core.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",
size: "small"
});
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("." + 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 require_kendo_core.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,
size: "small"
});
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("." + STYLES.file);
const message = $(e.sender.element).closest("." + 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("." + STYLES.messageRemoved).length || target.find("." + 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("." + 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("." + 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: require_kendo_core.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: "xsmall"
}).wrapper[0].outerHTML : "";
const fileMenuButtonHtml = fileMenuButton && !closeButton ? new kendo.ui.Button(`<button class="${STYLES.menuButton}" ${REFERENCES.fileMenuButton}>`, {
icon: ICONS.attachmentMenu,
fillMode: "flat",
size: "xsmall"
}).wrapper[0].outerHTML : "";
return `<li class="${STYLES.file}" data-uid="${require_kendo_core.htmlService.encode(file.uid)}">
${kendo.ui.icon({
icon: require_kendo_core.fileUtilsService.getFileGroup(file.extension, true),
size: "xlarge"
})}
<div class="${STYLES.fileInfo}">
<span class="${STYLES.fileName}">${require_kendo_core.htmlService.encode(file.name)}</span>
<span class="${STYLES.fileSize}">${require_kendo_core.htmlService.encode(require_kendo_core.fileUtilsService.getFileSizeMessage(file.size))}</span>
</div>
${closeButtonHtml}
${fileMenuButtonHtml}
</li>`;
};
//#endregion
//#region ../src/chat/templates/file.template.ts
/**
* Renders a list of file attachments with the specified layout mode.
* @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 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 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 = require_kendo_core.htmlService.convertTextUrlToLink(text || "");
if (!content) content = files?.length ? renderFile(files[0], false, renderFileMenuButton ?? false) : "";
if (isDeleted && messages) content = isOwnMessage ? require_kendo_core.htmlService.encode(messages.selfMessageDeleted) : require_kendo_core.htmlService.encode(messages.otherMessageDeleted);
return `<div class="${STYLES.messageReference} ${messageReferenceSenderStyle} ${pinMessageReferenceStyle}" ${wrapperReference}>
${isPinMessage ? kendo.ui.icon({ icon: ICONS.pin }) : ""}
<div class="${STYLES.messageReferenceContent}">${content}</div>
<span class="${STYLES.spacer}"></span>
${renderCloseButton ? new kendo.ui.Button(`<button ${closeButtonReference}>`, {
icon: ICONS.x,
fillMode: "flat",
size: "xsmall"
}).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: "xsmall",
fillMode: "clear"
});
};
const renderFailedContent = (failedText) => {
return `<span class="${STYLES.messageFailedContent}">
${kendo.ui.icon({
icon: ICONS.warningTriangle,
size: "xsmall"
})}
<span class="${STYLES.messageFailedText}">${require_kendo_core.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 !== void 0 ? settings.text : status.charAt(0).toUpperCase() + status.slice(1);
return `<span class="${statusClass}">${iconHtml}${require_kendo_core.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}">${require_kendo_core.htmlService.encode(attachment.title)}</div>` : "";
const subtitle = attachment.subtitle ? `<div class="${STYLES.cardSubtitle}">${require_kendo_core.htmlService.encode(attachment.subtitle)}</div>` : "";
const image = attachment.thumbnailUrl ? `<div class="${STYLES.cardMedia}"><img src="${require_kendo_core.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}">${require_kendo_core.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, filesTemplate, 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 ? void 0 : 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 ? require_kendo_core.htmlService.encode(messages.selfMessageDeleted) : require_kendo_core.htmlService.encode(messages.otherMessageDeleted);
else messageContent = require_kendo_core.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}>${require_kendo_core.formatterService.toString(require_kendo_core.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>`;
let filesHtml = "";
if (message.files?.length && !isDeleted) filesHtml = filesTemplate ? filesTemplate(message.files, downloadAll, messages, false, filesLayoutMode) : renderFiles(message.files, downloadAll, messages, false, filesLayoutMode);
return `<div class="${STYLES.message}${isDeleted ? " " + STYLES.messageRemoved : ""}${isFailed ? " " + STYLES.messageFailed : ""}" data-uid="${require_kendo_core.htmlService.encode(message.uid ?? "")}">
${retryHtml}
<div class="${STYLES.chatBubble} ${STYLES.bubble} ${expandableClasses.join(" ")}">
<div class="${STYLES.bubbleContent}">
${replyMessageHtml}
<span class="${STYLES.chatBubbleText}">${messageContent}</span>
${filesHtml}
</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, filesTemplate, skipSanitization = false, messageSettings, filesLayoutMode = FILES_LAYOUT_MODE.VERTICAL, contentTemplate, attachmentTemplate, attachmentLayout, userStatusTemplate, timestampVisibility = "onFocus", statusSettings, showMessageTime = true } = context;
const effectiveShowAvatar = messageSettings?.showAvatar !== void 0 ? messageSettings.showAvatar && author && author.imageUrl : author && author.imageUrl;
const effectiveShowUsername = messageSettings?.showUsername !== void 0 ? messageSettings.showUsername : true;
const effectiveExpandable = messageSettings?.allowMessageCollapse !== void 0 ? 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 = require_kendo_core.dateParserService.parseDate(message.timestamp);
if (require_kendo_core.utilsService.isFunction(timestampTemplate)) timestampContent = timestampTemplate({
date: messageDate,
message
});
else {
const relativeDateText = require_kendo_core.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, filesTemplate, 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}">${require_kendo_core.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}">${require_kendo_core.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}">${require_kendo_core.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: message.authorImageUrl,
imageAltText: message.authorImageAltText
};
const isOwnMessage = String(author.id) === String(options.authorId);
const replyMessage = this.context.getReplyMessage(message);
const messageTimeFormat = options.messageTimeFormat;
const skipSanitization = options.skipSanitization;
const statusTemplate = options.messageStatusTemplate;
const userSettings = isOwnMessage ? options.authorMessageSettings : options.receiverMessageSettings;
const messageSettings = this._mergeMessageSettings(options, userSettings);
const expandable = messageSettings.allowMessageCollapse ?? options.allowMessageCollapse;
const fullWidth = messageSettings.messageWidthMode === MESSAGE_WIDTH_MODE.FULL;
if (this._handleExistingMessageUpdate({
message,
author,
isOwnMessage,
replyMessage,
componentMessages,
expandable,
messageTimeFormat,
skipSanitization,
statusTemplate,
messageSettings
})) return;
const targetGroupElement = this._findTargetMessageGroup();
if (this._canGroupWithLastMessage(targetGroupElement, author.id) && targetGroupElement.length) this._addMessageToExistingGroup({
message,
author,
isOwnMessage,
replyMessage,
componentMessages,
expandable,
targetGroupElement,
messageTimeFormat,
skipSanitization,
statusTemplate,
messageSettings
});
else {
const showTimestamp = this._shouldShowTimestamp(message);
this._createNewMessageGroup({
message,
author,
isOwnMessage,
replyMessage,
componentMessages,
expandable,
fullWidth,
messageTimeFormat,
showTimestamp,
skipSanitization,
statusTemplate,
messageSettings
});
}
}
_mergeMessageSettings(options, userSettings) {
const merged = {
showAvatar: options.showAvatar,
showUsername: options.showUsername,
allowMessageCollapse: options.allowMessageCollapse,
messageWidthMode: options.messageWidthMode,
messageToolbarActions: options.messageToolbarActions,
messageActions: options.messageActions,
messageTemplate: options.messageTemplate,
messageContentTemplate: options.messageContentTemplate
};
if (userSettings) {
if (userSettings.showAvatar !== void 0) merged.showAvatar = userSettings.showAvatar;
if (userSettings.showUsername !== void 0) merged.showUsername = userSettings.showUsername;
if (userSettings.allowMessageCollapse !== void 0) merged.allowMessageCollapse = userSettings.allowMessageCollapse;
if (userSettings.messageWidthMode !== void 0) merged.messageWidthMode = userSettings.messageWidthMode;
if (userSettings.enableFileActions !== void 0) merged.enableFileActions = userSettings.enableFileActions;
if (userSettings.enableContextMenuActions !== void 0) merged.enableContextMenuActions = userSettings.enableContextMenuActions;
if (userSettings.messageToolbarActions !== void 0) merged.messageToolbarActions = userSettings.messageToolbarActions;
if (userSettings.messageActions !== void 0) merged.messageActions = userSettings.messageActions;
if (userSettings.messageTemplate !== void 0) merged.messageTemplate = userSettings.messageTemplate;
if (userSettings.messageContentTemplate !== void 0) merged.messageContentTemplate = userSettings.messageContentTemplate;
}
return merged;
}
_getMessageTimeVisibility() {
return this.context.options.timestampVisibility;
}
_shouldShowTimestamp(message) {
if (!message.timestamp) return false;
const lastNonTypingMessage = this.context.list.find("." + STYLES.message).filter(function() {
return $(this).find("." + STYLES.typingIndicator).length === 0;
}).last();
if (!lastNonTypingMessage.length) return true;
const lastMessageData = this.context.dataItem(lastNonTypingMessage);
if (!lastMessageData || !lastMessageData.timestamp) return true;
const currentMessageDate = require_kendo_core.dateParserService.parseDate(message.timestamp);
const lastMessageDate = require_kendo_core.dateParserService.parseDate(lastMessageData.timestamp);
if (!currentMessageDate || !lastMessageDate) return false;
const currentDay = new Date(currentMessageDate);
currentDay.setHours(0, 0, 0, 0);
const lastDay = new Date(lastMessageDate);
lastDay.setHours(0, 0, 0, 0);
return currentDay.getTime() !== lastDay.getTime();
}
_shouldRenderUserStatus(author, messageSettings) {
return messageSettings?.showAvatar !== void 0 ? !!(messageSettings.showAvatar && author?.imageUrl) : !!author?.imageUrl;
}
_syncUserStatus(targetGroupElement, message, author, messageSettings) {
const userStatusWrapper = targetGroupElement.find(`[${REFERENCES.userStatusWrapper}]`);
const userStatusTemplate = this.context.options.userStatusTemplate;
if (!userStatusTemplate || !this._shouldRenderUserStatus(author, messageSettings)) {
userStatusWrapper.remove();
return;
}
const userStatusElement = $(`<div ${REFERENCES.userStatusWrapper}>${userStatusTemplate({ message: {
...message,
author
} })}</div>`);
userStatusWrapper.remove();
targetGroupElement.prepend(userStatusElement);
}
_isLastMessageInGroup(messageElement) {
return messageElement.closest("." + STYLES.messageGroup).find("." + STYLES.message).filter(function() {
return $(this).find("." + STYLES.typingIndicator).length === 0;
}).last().is(messageElement);
}
_handleExistingMessageUpdate(context) {
const { message, author, isOwnMessage, replyMessage, componentMessages, expandable, messageTimeFormat, skipSanitization, statusTemplate, messageSettings } = context;
const options = this.context.options;
const existingMessageElement = this.context.list.find("." + STYLES.message + `[data-uid="${require_kendo_core.htmlService.encode(message.uid ?? "")}"]`);
const messageTimeVisibility = this._getMessageTimeVisibility();
const messageTemplate = messageSettings?.messageTemplate || null;
const contentTemplate = messageSettings?.messageContentTemplate || null;
const filesTemplate = options.filesTemplate;
if (existingMessageElement.length) {
const isLastMessageInGroup = this._isLastMessageInGroup(existingMessageElement);
let messageHtml;
if (messageTemplate) messageHtml = messageTemplate({
...message,
isOwnMessage,
author
}, replyMessage, true, componentMessages, messageSettings?.allowMessageCollapse ?? expandable, messageTimeFormat, skipSanitization, statusTemplate);
else messageHtml = renderMessage({
...message,
isOwnMessage,
author
}, replyMessage, true, componentMessages, messageSettings?.allowMessageCollapse ?? expandable, messageTimeFormat, skipSanitization, statusTemplate, options.filesLayoutMode, filesTemplate, contentTemplate, options.attachmentTemplate, options.attachmentLayout, messageTimeVisibility, options.messageStatusSettings, messageTimeVisibility !== "hidden");
const updatedMessageElement = $(messageHtml);
updatedMessageElement.data("messageData", message);
existingMessageElement.replaceWith(updatedMessageElement);
this.context.initMessageToolbar(updatedMessageElement.find("." + STYLES.messageToolbar), messageSettings?.messageToolbarActions);
this.context.initFileMenus(updatedMessageElement, messageSettings?.enableFileActions);
if (isLastMessageInGroup) this._syncUserStatus(updatedMessageElement.closest("." + STYLES.messageGroup), message, author, messageSettings);
return true;
}
return false;
}
_findTargetMessageGroup() {
let lastGroupElement = this.context.list.children("." + STYLES.messageGroup).last();
let targetGroupElement = lastGroupElement;
const messagesInLastGroup = lastGroupElement.find("." + STYLES.message);
if (messagesInLastGroup.length === 1) {
if (messagesInLastGroup.first().find("." + STYLES.typingIndicator).length > 0) targetGroupElement = lastGroupElement.prev("." + STYLES.messageGroup);
}
return targetGroupElement;
}
_canGroupWithLastMessage(targetGroupElement, authorId) {
const lastMessageInTargetGroup = targetGroupElement.find("." + STYLES.message).filter(function() {
return $(this).find("." + STYLES.typingIndicator).length === 0;
}).last();
const lastMessageData = this.context.dataItem(lastMessageInTargetGroup);
return String(lastMessageData?.authorId) === String(authorId);
}
_addMessageToExistingGroup(context) {
const { message, author, isOwnMessage, replyMessage, componentMessages, expandable, targetGroupElement, messageTimeFormat, skipSanitization, statusTemplate, messageSettings } = context;
const options = this.context.options;
const messageGroupContent = targetGroupElement.find("." + STYLES.messageGroupContent);
const messageTimeVisibility = this._getMessageTimeVisibility();
const messageTemplate = messageSettings?.messageTemplate || null;
const contentTemplate = messageSettings?.messageContentTemplate || null;
const filesTemplate = options.filesTemplate;
let messageHtml;
if (messageTemplate) messageHtml = messageTemplate({
...message,
isOwnMessage,
author
}, replyMessage, true, componentMessages, messageSettings?.allowMessageCollapse ?? expandable, messageTimeFormat, skipSanitization, statusTemplate);
else messageHtml = renderMessage({
...message,
isOwnMessage,
author
}, replyMessage, true, componentMessages, messageSettings?.allowMessageCollapse ?? expandable, messageTimeFormat, skipSanitization, statusTemplate, options.filesLayoutMode, filesTemplate, contentTemplate, options.attachmentTemplate, options.attachmentLayout, messageTimeVisibility, options.messageStatusSettings, messageTimeVisibility !== "hidden");
const messageElement = $(messageHtml);
messageElement.data("messageData", message);
messageGroupContent.append(messageElement);
this.context.initMessageToolbar(messageElement.find("." + STYLES.messageToolbar), messageSettings?.messageToolbarActions);
this.context.initFileMenus(messageElement, messageSettings?.enableFileActions);
if (!message.isDeleted) {
const effectiveAttachmentLayout = message.attachmentLayout || options.attachmentLayout || "list";
const attachmentsHtml = renderAttachments(message.attachments, message, effectiveAttachmentLayout, options.attachmentTemplate);
if (attachmentsHtml) targetGroupElement.after($(attachmentsHtml));
}
this._syncUserStatus(targetGroupElement, message, author, messageSettings);
this._moveTypingIndicatorsToEnd();
}
_createNewMessageGroup(context) {
const { message, author, isOwnMessage, replyMessage, componentMessages, expandable, fullWidth, messageTimeFormat, showTimestamp, skipSanitization, statusTemplate, messageSettings } = context;
const options = this.context.options;
const messageGroupTemplate = options.messageGroupTemplate;
const timestampTemplate = options.timestampTemplate;
const messageTemplate = messageSettings?.messageTemplate || null;
const contentTemplate = messageSettings?.messageContentTemplate || null;
const groupElement = $(messageGroupTemplate({
message,
author,
isOwnMessage,
replyMessage,
downloadAll: true,
messages: componentMessages,
expandable,
fullWidth,
messageTimeFormat,
timestampTemplate,
statusTemplate,
showTimestamp,
messageTemplate,
contentTemplate,
filesTemplate: options.filesTemplate,
skipSanitization,
messageSettings,
filesLayoutMode: options.filesLayoutMode,
attachmentTemplate: options.attachmentTemplate,
attachmentLayout: options.attachmentLayout,
userStatusTemplate: options.userStatusTemplate,
timestampVisibility: this._getMessageTimeVisibility(),
statusSettings: options.messageStatusSettings,
showMessageTime: this._getMessageTimeVisibility() !== "hidden"
}));
groupElement.find("." + STYLES.message).first().data("messageData", message);
this.context.list.append(groupElement);
this.context.initMessageToolbar(groupElement.find("." + STYLES.messageToolbar), messageSettings?.messageToolbarActions);
this.context.initFileMenus(groupElement, messageSettings?.enableFileActions);
this._moveTypingIndicatorsToEnd();
return groupElement;
}
_moveTypingIndicatorsToEnd() {
const renderer = this;
const ctx = this.context;
const options = ctx.options;
const messageGroupTemplate = options.messageGroupTemplate;
ctx.list.find("." + STYLES.message).filter(function() {
return $(this).find("." + STYLES.typingIndicator).length > 0;
}).each(function() {
const typingMessage = $(this);
const messageGroup = typingMessage.closest("." + STYLES.messageGroup);
if (messageGroup.find("." + STYLES.message).length === 1) messageGroup.detach().appendTo(ctx.list);
else {
typingMessage.remove();
const message = ctx.dataItem(typingMessage);
if (!message) return;
const author = {
id: message.authorId,
name: message.authorName,
imageUrl: message.authorImageUrl,
imageAltText: message.authorImageAltText
};
const isOwnMessage = String(author.id) === String(options.authorId);
const userSettings = isOwnMessage ? options.authorMessageSettings : options.receiverMessageSettings;
const messageSettings = renderer._mergeMessageSettings(options, userSettings);
const typingGroupElement = $(messageGroupTemplate({
message,
author,
isOwnMessage,
downloadAll: true,
messages: options.messages,
expandable: messageSettings.allowMessageCollapse ?? options.allowMessageCollapse,
fullWidth: messageSettings.messageWidthMode === MESSAGE_WIDTH_MODE.FULL,
messageTimeFormat: options.messageTimeFormat,
statusTemplate: options.messageStatusTemplate,
showTimestamp: false,
timestampTemplate: options.timestampTemplate,
messageTemplate: messageSettings.messageTemplate || null,
contentTemplate: messageSettings.messageContentTemplate || null,
filesTemplate: options.filesTemplate,
skipSanitization: options.skipSanitization,
messageSettings,
filesLayoutMode: options.filesLayoutMode,
attachmentTemplate: options.attachmentTemplate,
attachmentLayout: options.attachmentLayout,
userStatusTemplate: options.userStatusTemplate,
timestampVisibility: renderer._getMessageTimeVisibility(),
statusSettings: options.messageStatusSettings,
showMessageTime: renderer._getMessageTimeVisibility() !== "hidden"
}));
ctx.list.append(typingGroupElement);
}
});
}
prependMessages(messages) {
if (!messages.length) return;
const options = this.context.options;
const componentMessages = options.messages;
const messageGroupTemplate = options.messageGroupTemplate;
const timestampTemplate = options.timestampTemplate;
let currentGroupElement = null;
let currentAuthorId = null;
const fragment = document.createDocumentFragment();
const $fragment = $(fragment);
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
const author = {
id: message.authorId,
name: message.authorName,
imageUrl: message.authorImageUrl,
imageAltText: message.authorImageAltText
};
const isOwnMessage = String(author.id) === String(options.authorId);
const replyMessage = this.context.getReplyMessage(message);
const userSettings = isOwnMessage ? options.authorMessageSettings : options.receiverMessageSettings;
const messageSettings = this._mergeMessageSettings(options, userSettings);
const statusTemplate = options.messageStatusTemplate;
if (String(author.id) === currentAuthorId && currentGroupElement) {
const messageGroupContent = currentGroupElement.find("." + STYLES.messageGroupContent);
const messageTemplate = messageSettings.messageTemplate || null;
const contentTemplate = messageSettings.messageContentTemplate || null;
let messageHtml;
if (messageTemplate) messageHtml = messageTemplate({
...message,
isOwnMessage,
author
}, replyMessage, true, componentMessages, messageSettings.allowMessageCollapse ?? options.allowMessageCollapse, options.messageTimeFormat, options.skipSanitization, statusTemplate);
else messageHtml = renderMessage({
...message,
isOwnMessage,
author
}, replyMessage, true, componentMessages, messageSettings.allowMessageCollapse ?? options.allowMessageCollapse, options.messageTimeFormat, options.skipSanitization, statusTemplate, options.filesLayoutMode, options.filesTemplate, contentTemplate, options.attachmentTemplate, options.attachmentLayout, options.timestampVisibility, options.messageStatusSettings, options.timestampVisibility !== "hidden");
const messageElement = $(messageHtml);
messageGroupContent.append(messageElement);
this.context.initMessageToolbar(messageElement.find("." + STYLES.messageToolbar), messageSettings?.messageToolbarActions);
this.context.initFileMenus(messageElement, messageSettings?.enableFileActions);
} else {
const showTimestamp = i === 0 && message.timestamp;
const messageTemplate = messageSettings.messageTemplate || null;
const contentTemplate = messageSettings.messageContentTemplate || null;
currentGroupElement = $(messageGroupTemplate({
message,
author,
isOwnMessage,
replyMessage,
downloadAll: true,
messages: componentMessages,
expandable: messageSettings.allowMessageCollapse ?? options.allowMessageCollapse,
fullWidth: messageSettings.messageWidthMode === MESSAGE_WIDTH_MODE.FULL,
messageTimeFormat: options.messageTimeFormat,
timestampTemplate,
statusTemplate,
showTimestamp: !!showTimestamp,
messageTemplate,
skipSanitization: options.skipSanitization,
messageSettings,
filesLayoutMode: options.filesLayoutMode,
contentTemplate,
attachmentTemplate: options.attachmentTemplate,
attachmentLayout: options.attachmentLayout,
userStatusTemplate: options.userStatusTemplate,
timestampVisibility: options.timestampVisibility,
statusSettings: options.messageStatusSettings,
showMessageTime: options.timestampVisibility !== "hidden"
}));
$fragment.append(currentGroupElement);
this.context.initMessageToolbar(currentGroupElement.find("." + STYLES.messageToolbar), messageSettings?.messageToolbarActions);
this.context.initFileMenus(currentGroupElement, messageSettings?.enableFileActions);
currentAuthorId = String(author.id);
}
}
const firstExistingGroup = this.context.list.children("." + STYLES.messageGroup).first();
if (currentGroupElement && firstExistingGroup.length) {
const lastPrependedAuthorId = currentAuthorId;
const firstExistingMessage = firstExistingGroup.find("." + STYLES.message).first();
const firstExistingData = this.context.dataItem(firstExistingMessage);
if (firstExistingData && String(firstExistingData.authorId) === lastPrependedAuthorId) {
const existingContent = firstExistingGroup.find("." + STYLES.messageGroupContent);
const messagesToMove = currentGroupElement.find("." + STYLES.messageGroupContent).children();
existingContent.prepend(messagesToMove);
currentGroupElement.remove();
}
}
this.context.list.prepend(fragment);
}
};
//#endregion
//#region ../src/chat/collaborators/chat-view/scroll-manager.ts
var ScrollManager = class {
constructor(context) {
this.scrollToBottomButton = null;
this.separator = null;
this._scrollHandler = null;
this._clickHandler = null;
this._olderLoadDebounceTimeout = null;
this._newerLoadDebounceTimeout = null;
this._isLoadingOlder = false;
this._allOlderLoaded = false;
this._isLoadingNewer = false;
this._allNewerLoaded = true;
this._scrolledAwayFromTop = false;
this._scrolledAwayFromBottom = false;
this._lastObservedClientHeight = 0;
this._resizeObserverService = null;
this.context = context;
this._initScrollToBottomButton();
this._initSeparator();
this._attachEvents();
}
_initScrollToBottomButton() {
if (this.context.options.scrollToBottomButton === false) {
this.scrollToBottomButton = null;
return;
}
const buttonHtml = kendo.html.renderButton(`<button class="${STYLES.scrollFab} ${STYLES.hidden}" ${REFERENCES.scrollToBottomButton} aria-label="Scroll to bottom"></button>`, {
icon: ICONS.arrowDown,
rounded: "full",
themeColor: "base"
});
this.scrollToBottomButton = $(buttonHtml).appendTo(this.context.element);
const cssStyles = {
position: "sticky",
bottom: "16px",
transform: "translateX(-50%)",
zIndex: 1
};
const isContextElementRtl = this.context.element.css("direction") === "rtl";
cssStyles[isContextElementRtl ? "right" : "left"] = "50%";
this.scrollToBottomButton.css(cssStyles);
}
_attachEvents() {
this._scrollHandler = this._onScroll.bind(this);
this._clickHandler = this._scrollToBottomButtonClick.bind(this);
this.context.element.on("scroll.kendoChat.scrollManager", this._scrollHandler).on("click.kendoChat.scrollManager", `[${REFERENCES.scrollToBottomButton}]`, this._clickHandler);
const containerElement = this.context.element[0];
if (containerElement) this._resizeObserverService = require_kendo_core.domUtilsService.createResizeObserver({
element: containerElement,
onResize: () => this._onContainerResize(),
debounceTime: 1
});
}
_onContainerResize() {
const el = this.context.element[0];
if (!el) return;
const previousClientHeight = this._lastObservedClientHeight;
const newClientHeight = el.clientHeight;
this._lastObservedClientHeight = newClientHeight;
if (previousClientHeight === 0 || newClientHeight === previousClientHeight) return;
if (el.scrollHeight - el.scrollTop - newClientHeight + (newClientHeight - previousClientHeight) <= 100) this.scrollToBottom();
}
_initSeparator() {
this.separator = $(`<div class="${STYLES.messageBoxSeparator} ${STYLES.hidden}"></div>`);
this.context.element.after(this.separator);
}
updateSeparator() {
if (!this.separator) return;
const el = this.context.element[0];
if (el.scrollHeight > el.clientHeight) this.separator.removeClass(STYLES.hidden);
else this.separator.addClass(STYLES.hidden);
}
_updateScrollToBottomButtonVisibility() {
if (!this.scrollToBottomButton) return;
const scrollTop = this.context.element.scrollTop() || 0;
const scrollHeight = this.context.element.prop("scrollHeight") || 0;
const clientHeight = this.context.element.prop("clientHeight") || 0;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
if (!(this.context.hasLatestMessageRendered ? this.context.hasLatestMessageRendered() : true) || distanceFromBottom > 100) this.scrollToBottomButton.removeClass(STYLES.hidden);
else this.scrollToBottomButton.addClass(STYLES.hidden);
}
_onScroll() {
this._updateScrollToBottomButtonVisibility();
this.updateSeparator();
const el = this.context.element[0];
const scrollTop = el.scrollTop;
const distanceFromBottom = el.scrollHeight - scrollTop - el.clientHeight;
if (!this._scrolledAwayFromTop && scrollTop > 200) {
this._clearOlderLoadDebounce();
this._scrolledAwayFromTop = true;
}
if (!this._scrolledAwayFromBottom && distanceFromBottom > 100) {
this._clearNewerLoadDebounce();
this._scrolledAwayFromBottom = true;
}
if (scrollTop <= 200 && this._scrolledAwayFromTop && this.context.onScrollNearTop) {
this._scrolledAwayFromTop = false;
if (!this._isLoadingOlder && !this._allOlderLoaded) this._debounceOlderLoad();
}
if (distanceFromBottom <= 100 && this._scrolledAwayFromBottom && this.context.onScrollNearBottom) {
this._scrolledAwayFromBottom = false;
if (!this._isLoadingNewer && !this._allNewerLoaded) this._debounceNewerLoad();
}
}
_getEndlessScrollDebounceDelay() {
return Math.max(0, this.context.options.endlessScrollDebounceDelay || 0);
}
_clearOlderLoadDebounce() {
if (this._olderLoadDebounceTimeout) {
clearTimeout(this._olderLoadDebounceTimeout);
this._olderLoadDebounceTimeout = null;
}
}
_clearNewerLoadDebounce() {
if (this._newerLoadDebounceTimeout) {
clearTimeout(this._newerLoadDebounceTimeout);
this._newerLoadDebounceTimeout = null;
}
}
_isNearTopEdge() {
return (this.context.element[0]?.scrollTop || 0) <= 200;
}
_isNearBottomEdge() {
const element = this.context.element[0];
if (!element) return false;
return element.scrollHeight - element.scrollTop - element.clientHeight <= 100;
}
_debounceOlderLoad() {
const delay = this._getEndlessScrollDebounceDelay();
this._clearOlderLoadDebounce();
if (!delay) {
this.context.onScrollNearTop?.();
return;
}
this._olderLoadDebounceTimeout = setTimeout(() => {
this._olderLoadDebounceTimeout = null;
if (this._isNearTopEdge() && !this._isLoadingOlder && !this._allOlderLoaded) this.context.onScrollNearTop?.();
}, delay);
}
_debounceNewerLoad() {
const delay = this._getEndlessScrollDebounceDelay();
this._clearNewerLoadDebounce();
if (!delay) {
this.context.onScrollNearBottom?.();
return;
}
this._newerLoadDebounceTimeout = setTimeout(() => {
this._newerLoadDebounceTimeout = null;
if (this._isNearBottomEdge() && !this._isLoadingNewer && !this._allNewerLoaded) this.context.onScrollNearBottom?.();
}, delay);
}
_scrollToBottomButtonClick(e) {
e.preventDefault();
if (this.context.onScrollToLatest && this.context.isLatestRangeActive && !this.context.isLatestRangeActive()) {
this.context.onScrollToLatest();
return;
}
this.scrollToBottom();
}
_getMessageElement(uid) {
return this.context.list.find("." + STYLES.message + `[data-uid="${require_kendo_core.htmlService.encode(uid)}"]`);
}
_getPreviousMessageElement(messageElement) {
return messageElement.prevAll("." + STYLES.message).first();
}
_getPinnedHeight() {
const pinnedElement = this.context.element.find("." + STYLES.messagePinned);
return pinnedElement.length ? pinnedElement.outerHeight() || 0 : 0;
}
_getAutoScrollThreshold(pinnedHeight) {
const availableHeight = Math.max((this.context.element.prop("clientHeight") || 0) - pinnedHeight, 0);
const threshold = this.context.options.autoScrollThreshold;
if (typeof threshold === "number") return Math.max(threshold, 0);
if (typeof threshold === "string") {
const trimmedThreshold = threshold.trim();
if (trimmedThreshold.endsWith("%")) {
const percentage = parseFloat(trimmedThreshold.slice(0, -1));
if (!isNaN(percentage)) return Math.max(availableHeight * percentage / 100, 0);
}
const pixelThreshold = parseFloat(trimmedThreshold);
if (!isNaN(pixelThreshold)) return Math.max(pixelThreshold, 0);
}
return 0;
}
preserveScrollPosition(callback) {
const container = this.context.element[0];
const scrollTop = container.scrollTop;
const previousScrollHeight = container.scrollHeight;
callback();
const newScrollHeight = container.scrollHeight;
container.scrollTo({
top: scrollTop + (newScrollHeight - previousScrollHeight),
behavior: "instant"
});
}
scrollToBottom() {
const container = this.context.element[0];
container.scrollTo({
top: container.scrollHeight,
behavior: "instant"
});
}
isNearBottom() {
const scrollTop = this.context.element.scrollTop() || 0;
const scrollHeight = this.context.element.prop("scrollHeight") || 0;
const clientHeight = this.context.element.prop("clientHeight") || 0;
return scrollHeight - scrollTop - clientHeight <= 100;
}
scrollToMessage(uid) {
if (!uid) return false;
const messageElement = this._getMessageElement(uid);
if (!messageElement.length) return false;
const pinnedHeight = this._getPinnedHeight();
return require_kendo_core.domUtilsService.scrollToElement(this.context.element, messageElement, {
position: "top",
offset: -pinnedHeight
});
}
scrollIncomingMessage(uid) {
if (!uid) return false;
const messageElement = this._getMessageElement(uid);
if (!messageElement.length) return false;
const container = this.context.element;
const containerElement = container[0];
const containerOffset = container.offset();
const messageOffset = messageElement.offset();
if (!containerElement || !containerOffset || !messageOffset) return false;
const scrollTop = container.scrollTop() || 0;
const containerHeight = containerElement.clientHeight || 0;
const pinnedHeight = this._getPinnedHeight();
const threshold = this._getAutoScrollThreshold(pinnedHeight);
const preferredScrollTop = messageOffset.top - containerOffset.top + scrollTop - pinnedHeight - threshold;
const previousMessageElement = this._getPreviousMessageElement(messageElement);
const maxScrollTop = containerElement.scrollHeight - containerHeight;
let targetScrollTop = preferredScrollTop;
if (previousMessageElement.length) {
const previousMessageOffset = previousMessageElement.offset();
if (previousMessageOffset) {
const previousMessageBottom = previousMessageOffset.top - containerOffset.top + scrollTop + (previousMessageElement.outerHeight() || 0);
const minimumPreviousContext = Math.min(threshold, 8);
const visibleTop = preferredScrollTop + pinnedHeight;
if (minimumPreviousContext > 0 && previousMessageBottom < visibleTop + minimumPreviousContext) targetScrollTop = previousMessageBottom - pinnedHeight - minimumPreviousContext;
}
}
targetScrollTop = Math.min(Math.max(targetScrollTop, 0), maxScrollTop);
container.finish().animate({ scrollTop: targetScrollTop }, 0, "linear");
return true;
}
resetEndlessState() {
this._clearOlderLoadDebounce();
this._clearNewerLoadDebounce();
this._isLoadingOlder = false;
this._allOlderLoaded = false;
this._isLoadingNewer = false;
this._allNewerLoaded = true;
this._scrolledAwayFromTop = false;
this._scrolledAwayFromBottom = false;
}
refreshState() {
this._updateScrollToBottomButtonVisibility();
this.updateSeparator();
}
destroy() {
this._clearOlderLoadDebounce();
this._clearNewerLoadDebounce();
if (this._resizeObserverService) {
this._resizeObserverService.destroy();
this._resizeObserverService = null;
}
if (this.scrollToBottomButton) {
this.scrollToBottomButton.remove();
this.scrollToBottomButton = null;
}
if (this.separator) {
this.separator.remove();
this.separator = null;
}
this.context.element.off(".kendoChat.scrollManager");
}
};
//#endregion
//#region ../src/chat/collaborators/chat-view/suggestion-renderer.ts
var SuggestionRenderer = class {
constructor(context) {
this.context = context;
}
renderSuggestedActions() {
const options = this.context.options;
const lastSuggestedActions = this.context.dataManager.getLastMessage()?.suggestedActions || [];
const suggestedActionsElement = options.suggestedActionsTemplate(lastSuggestedActions);
const layoutMode = this._getSuggestedActionsLayoutMode(options);
const suggestedActionsWrapper = wrapSuggestionsWithLayout(suggestedActionsElement, layoutMode, options.dir === "rtl");
this.removeSuggestedActions();
this.context.list.append(lastSuggestedActions.length ? suggestedActionsWrapper : "");
this.context.list.find(`[${REFERENCES.leftScrollButton}]`).addClass(STYLES.disabled);
}
removeSuggestedActions() {
this.context.list.find("." + STYLES.suggestionGroup).remove();
this.context.list.find("." + STYLES.suggestionScrollWrap).remove();
}
_getSuggestedActionsLayoutMode(options) {
if (options.suggestedActionsLayoutMode) return options.suggestedActionsLayoutMode;
if (options.suggestedActionsScrollable) return SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLLBUTTONS;
return SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLL;
}
};
//#endregion
//#region ../src/chat/collaborators/chat-view.collaborator.ts
var ChatView = class ChatView extends require_kendo_core.Widget {
static {
this.options = { name: "ChatView" };
}
_hideOnFocusMessageTimes() {
this.element.find(`${"." + STYLES.messageTime}[data-timestamp-visibility='onFocus']`).addClass(STYLES.hidden);
}
_toggleOnFocusMessageTime(messageElement, visible) {
messageElement.find(`${"." + STYLES.messageTime}[data-timestamp-visibility='onFocus']`).toggleClass(STYLES.hidden, !visible);
}
constructor(element, options, context) {
super(element, $.extend(true, {}, ChatView.options, options));
this._dragHandler = null;
this.events = [
EVENTS.suggestedActionClick,
EVENTS.executeAction,
EVENTS.messageToolbarExecute,
EVENTS.fileMenuExecute,
EVENTS.downloadAllFiles,
EVENTS.expandableToggle,
EVENTS.resendMessage
];
this.context = context;
this._initList();
this._initHelpers();
this._attachEvents();
}
_initList() {
const messages = this.options.messages;
this.element.addClass(STYLES.viewWrapper).attr("role", "log").attr("aria-label", messages.messageListLabel).css("overflow-anchor", "none");
this.list = $("<div>").addClass(STYLES.messageListContent).appendTo(this.element);
}
_initHelpers() {
const options = this.options;
this._messageRenderer = new MessageRenderer({
list: this.list,
options,
dataManager: this.context.dataManager,
dataItem: this.dataItem.bind(this),
getReplyMessage: this.getReplyMessage.bind(this),
initMessageToolbar: this._initMessageToolbar.bind(this),
initFileMenus: this._initFileMenus.bind(this)
});
this._scrollManager = new ScrollManager({
element: this.element,
options,
list: this.list,
onScrollNearTop: this.context.onScrollNearTop,
onScrollNearBottom: this.context.onScrollNearBottom,
onScrollToLatest: this.context.onScrollToLatest,
isLatestRangeActive: this.context.isLatestRangeActive,
hasLatestMessageRendered: this.context.hasLatestMessageRendered
});
this._suggestionRenderer = new SuggestionRenderer({
list: this.list,
options,
dataManager: this.context.dataManager
});
}
_attachEvents() {
this.element.on("click.kendoChat", this._listClick.bind(this)).on("click.kendoChat", "." + STYLES.message, this._messageClick.bind(this)).on("click.kendoChat", "." + STYLES.suggestion, this._suggestionClick.bind(this)).on("click.kendoChat", `[${REFERENCES.attachmentActionButton}]`, this._attachmentActionClick.bind(this)).on("click.kendoChat", `[${REFERENCES.leftScrollButton}]`, this._leftScrollButtonClick.bind(this)).on("click.kendoChat", `[${REFERENCES.rightScrollButton}]`, this._rightScrollButtonClick.bind(this)).on("click.kendoChat", "." + STYLES.downloadButton, this._downloadAllClick.bind(this)).on("click.kendoChat", `[${REFERENCES.retryButton}]`, this._retryButtonClick.bind(this));
this._attachDragToScrollEvents();
}
_attachDragToScrollEvents() {
this._dragHandler = require_kendo_core.domUtilsService.createDragToScrollHandler(this.element, {
namespace: ".kendoChat.chatViewDrag",
captureElement: this.element,
delegateSelector: `.${STYLES.suggestionsScroll}, .${STYLES.cardDeck}`
});
this._dragHandler.attach();
}
_getScrollableElementFromButton(button) {
const scrollWrap = button.closest(`.${STYLES.suggestionScrollWrap}, .${STYLES.cardDeckScrollWrap}`);
if (scrollWrap.hasClass(STYLES.cardDeckScrollWrap)) return scrollWrap.children("." + STYLES.cardDeck);
return scrollWrap.find("." + STYLES.suggestionsScroll);
}
_retryButtonClick(e) {
e.preventDefault();
e.stopPropagation();
const button = $(e.currentTarget);
const uid = button.attr("data-uid");
const messageElement = button.closest("." + STYLES.message);
const message = this._getMessageFromElement(messageElement);
this.trigger(EVENTS.resendMessage, {
message,
uid
});
}
_attachmentActionClick(e) {
e.preventDefault();
e.stopPropagation();
const button = $(e.currentTarget);
const attachmentIndex = Number(button.attr("data-attachment-index"));
const actionIndex = Number(button.attr("data-action-index"));
const messageElement = button.closest("." + STYLES.message);
const message = this._getMessageFromElement(messageElement);
const action = message?.attachments?.[attachmentIndex]?.actions?.[actionIndex];
if (!action) return;
this.trigger(EVENTS.executeAction, {
action,
message
});
}
_getMessageFromElement(element) {
const uid = element.attr("data-uid");
if (!uid) return null;
return this.context.dataManager.getMessageByUid(uid);
}
renderMessage(message) {
this._messageRenderer.renderMessage(message);
}
_initMessageToolbar(element, perMessageActions) {
const options = this.options;
const messageToolbarActions = perMessageActions ?? options.messageToolbarActions;
if (!messageToolbarActions || !messageToolbarActions.length || !element.length) {
element.remove();
return;
}
new MessageToolbar(element, {
items: messageToolbarActions,
dir: options.dir,
messages: options.messages,
resizable: false
}).bind("execute", (e) => this.trigger(EVENTS.messageToolbarExecute, e));
}
_initFileMenus(element, enableFileActions) {
const fileActions = this.options.fileActions;
const fileMenuButtons = element.find(`[${REFERENCES.fileMenuButton}]`);
if (enableFileActions === false || !fileActions?.length || !element.length) {
fileMenuButtons.remove();
return;
}
fileMenuButtons.each((_i, el) => {
const $el = $(el);
if (!$el.data("kendoDropDownButton")) new FileMenu($el, { items: fileActions }).bind("execute", (e) => this.trigger(EVENTS.fileMenuExecute, e));
});
}
dataItem(message) {
const uid = message && message.data("uid");
if (uid) return this.context.dataManager.getMessageByUid(uid) || message.data("messageData") || null;
return message?.data("messageData") || null;
}
fileDataItem(message, file) {
const uid = file && file.data("uid");
if (uid) return this.context.dataManager.getFileByUid(message, uid);
return null;
}
getReplyMessage(message) {
if (!message?.replyToId) return null;
const replyMessage = this.context.dataManager.getMessageById(message.replyToId);
if (replyMessage) replyMessage.isOwnMessage = String(replyMessage.authorId) === String(this.options.authorId);
return replyMessage;
}
renderSuggestedActions() {
this._suggestionRenderer.renderSuggestedActions();
}
clearMessages() {
this.list.find("." + STYLES.messageGroup).remove();
this.list.find("." + STYLES.timestamp).remove();
this._suggestionRenderer.removeSuggestedActions();
}
removeMessage(uid) {
const messageElement = this.list.find("." + STYLES.message + `[data-uid="${require_kendo_core.htmlService.encode(uid)}"]`);
if (!messageElement.length) return false;
const messageGroup = messageElement.closest("." + STYLES.messageGroup);
if (messageGroup.find("." + STYLES.message).length === 1) messageGroup.remove();
else messageElement.remove();
return true;
}
scrollToBottom() {
this._scrollManager.scrollToBottom();
}
isNearBottom() {
return this._scrollManager.isNearBottom();
}
scrollToMessage(uid) {
return this._scrollManager.scrollToMessage(uid);
}
scrollIncomingMessage(uid) {
return this._scrollManager.scrollIncomingMessage(uid);
}
hasMessage(uid) {
if (!uid) return false;
return this.list.find("." + STYLES.message + `[data-uid="${require_kendo_core.htmlService.encode(uid)}"]`).length > 0;
}
updateSeparator() {
this._scrollManager.updateSeparator();
}
refreshScrollState() {
this._scrollManager.refreshState();
}
refreshDataManager(dataManager) {
this.context.dataManager = dataManager;
}
showNoData(html) {
this.list.html(html);
this.list.addClass(STYLES.messageListContentEmpty);
}
hideNoData() {
this.list.empty();
this.list.removeClass(STYLES.messageListContentEmpty);
}
hasMessages() {
return this.list.children("." + STYLES.messageGroup).length > 0;
}
_listClick(e) {
const targetElement = $(e.target);
if (targetElement.hasClass(STYLES.message) || targetElement.parents("." + STYLES.message).length) return;
this._clearSelection(targetElement);
}
_suggestionClick(e) {
const suggestionElement = $(e.currentTarget);
if (suggestionElement.length) this.trigger(EVENTS.suggestedActionClick, { text: suggestionElement.text() });
}
_messageClick(e) {
const target = $(e.target);
const expandIcon = target.closest(`[${REFERENCES.bubbleExpandableIndicator}]`);
if (this._allowMessageClick(target)) return;
this._clearSelection(target);
if (expandIcon.length) {
const bubble = expandIcon.closest("." + STYLES.bubble);
const isExpanded = bubble.hasClass(STYLES.expanded);
bubble.toggleClass(STYLES.expanded, !isExpanded);
expandIcon.html(kendo.ui.icon({ icon: isExpanded ? ICONS.chevronDown : ICONS.chevronUp }));
this.trigger(EVENTS.expandableToggle, {
indicator: expandIcon,
isExpanded: !isExpanded
});
return;
}
const messageElement = $(e.currentTarget);
messageElement.find("." + STYLES.bubble).addClass(STYLES.selected);
this._toggleOnFocusMessageTime(messageElement, true);
}
_allowMessageClick(target) {
const disallowedSelectors = [
`[${REFERENCES.attachmentActionButton}]`,
`[${REFERENCES.fileMenuButton}]`,
"." + STYLES.messageToolbar,
"." + STYLES.typingIndicator,
"." + STYLES.downloadButton
].join(", ");
return target.closest(disallowedSelectors).length > 0 || target.children(disallowedSelectors).length > 0 || target.is(disallowedSelectors);
}
_clearSelection(target) {
const selectedMessages = this.element.find("." + STYLES.selected);
if (target.closest(`[${REFERENCES.bubbleExpandableIndicator}]`).length) return;
this._hideOnFocusMessageTimes();
selectedMessages.each((_index, element) => {
$(element).removeClass(STYLES.selected);
});
}
_leftScrollButtonClick(e) {
e.preventDefault();
const button = $(e.currentTarget);
const scrollableElement = this._getScrollableElementFromButton(button);
const isRtl = this.options.dir === "rtl";
if (!scrollableElement.length) return;
const position = require_kendo_core.domUtilsService.scrollByDelta(scrollableElement, isRtl ? 200 : -200);
button.siblings(`[${REFERENCES.rightScrollButton}]`).removeClass(STYLES.disabled);
if (position.atStart) button.addClass(STYLES.disabled);
}
_rightScrollButtonClick(e) {
e.preventDefault();
const button = $(e.currentTarget);
const scrollableElement = this._getScrollableElementFromButton(button);
const isRtl = this.options.dir === "rtl";
if (!scrollableElement.length) return;
const position = require_kendo_core.domUtilsService.scrollByDelta(scrollableElement, isRtl ? -200 : 200);
button.siblings(`[${REFERENCES.leftScrollButton}]`).removeClass(STYLES.disabled);
if (position.atEnd) button.addClass(STYLES.disabled);
}
_downloadAllClick(e) {
const message = $(e.currentTarget).closest("." + STYLES.message);
if (message.length) this.trigger(EVENTS.downloadAllFiles, { messageElement: message });
}
destroy() {
super.destroy();
this._scrollManager.destroy();
if (this._dragHandler) this._dragHandler.destroy();
this.element.find("[data-role='button']").each(function() {
const fileMenu = $(this).data("kendoChatFileMenu");
if (fileMenu) fileMenu.destroy();
});
this.element.empty();
this.element.off(NS);
this.list = null;
}
};
//#endregion
//#region ../src/chat/collaborators/endless-range.collaborator.ts
var EndlessRangeManager = class {
syncPageSize(isEndlessScrollEnabled, dataSource) {
if (!isEndlessScrollEnabled || !dataSource || typeof dataSource.pageSize !== "function") return 20;
let pageSize = Number(dataSource.pageSize());
if (!Number.isFinite(pageSize) || pageSize < 1) {
pageSize = 20;
dataSource.pageSize(pageSize);
}
return Math.max(1, pageSize);
}
getLatestRange(pageSize, totalCount) {
const endIndex = totalCount;
return {
startIndex: Math.max(0, endIndex - pageSize),
endIndex
};
}
getTargetRange(targetIndex, pageSize, totalCount) {
if (totalCount <= pageSize) return {
startIndex: 0,
endIndex: totalCount
};
let startIndex = Math.max(0, targetIndex - Math.floor(pageSize / 2));
let endIndex = Math.min(totalCount, startIndex + pageSize);
startIndex = Math.max(0, endIndex - pageSize);
return {
startIndex,
endIndex
};
}
isLatestRenderedRange(renderEndIndex, totalCount) {
return renderEndIndex === totalCount;
}
hasRenderedLatestMessage(renderEndIndex, totalCount) {
return this.isLatestRenderedRange(renderEndIndex, totalCount);
}
};
//#endregion
//#region ../src/chat/collaborators/endless-scroll-coordinator.collaborator.ts
var EndlessScrollCoordinator = class {
constructor(context) {
this.context = context;
}
requestLatestRemoteStartupRange() {
this.context.beginLatestRemoteStartup();
this._requestLatestRange(0, this.context.getPageSize(), "scroll-to-bottom", { scrollToBottom: true });
}
prepareLatestMessages() {
if (!this.context.isEndlessScrollEnabled()) return;
const totalCount = this.context.getTotalCount();
if (!totalCount) return;
const latestRange = this.context.getLatestRange(totalCount);
if (this.context.isRemoteEndlessScroll() && !this.context.isRangeLoaded(latestRange.startIndex, latestRange.endIndex)) {
this._requestLatestRange(latestRange.startIndex, latestRange.endIndex, "scroll-to-bottom", { scrollToBottom: true });
return;
}
if (!this.context.isLatestRenderedRange(totalCount)) {
this.context.renderRange(latestRange.startIndex, latestRange.endIndex, { scrollToBottom: true });
return;
}
this.context.scrollToBottom();
this.context.refreshScrollState();
}
navigateToReferencedMessage(id, message, loadedMessage) {
if (loadedMessage?.uid && this.context.scrollToMessage(loadedMessage.uid)) return;
if (!this.context.isEndlessScrollEnabled()) return;
const targetIndex = this.context.getMessageIndexById(id);
if (targetIndex < 0) {
if (!this.context.isRemoteEndlessScroll()) return;
this._requestLatestRange(0, this.context.getPageSize(), "reference-jump", { targetMessageId: id });
return;
}
const targetRange = this.context.getTargetRange(targetIndex, this.context.getTotalCount());
this.context.renderRange(targetRange.startIndex, targetRange.endIndex, { scrollToMessageUid: loadedMessage?.uid || message?.uid });
}
loadOlderMessages() {
const renderedRange = this.context.getRenderedRange();
if (this.context.isRemoteEndlessScroll()) {
const loadedRange = this.context.getLoadedRange();
if (loadedRange.startIndex <= 0) {
this.context.updateEndlessState();
return;
}
const pageSize = this.context.getPageSize();
this._requestAdjacentRange(Math.max(0, loadedRange.startIndex - pageSize), loadedRange.startIndex, "older", "prepend", "older");
return;
}
if (renderedRange.startIndex > 0) {
const pageSize = this.context.getPageSize();
this.context.prependLocalRange(Math.max(0, renderedRange.startIndex - pageSize), renderedRange.startIndex);
return;
}
this.context.updateEndlessState();
}
loadNewerMessages() {
const renderedRange = this.context.getRenderedRange();
const totalCount = this.context.getTotalCount();
if (this.context.isRemoteEndlessScroll()) {
const loadedRange = this.context.getLoadedRange();
if (loadedRange.endIndex >= totalCount) {
this.context.updateEndlessState();
return;
}
const pageSize = this.context.getPageSize();
this._requestAdjacentRange(loadedRange.endIndex, Math.min(totalCount, loadedRange.endIndex + pageSize), "newer", "append", "newer");
return;
}
if (renderedRange.endIndex >= totalCount) {
this.context.updateEndlessState();
return;
}
const pageSize = this.context.getPageSize();
this.context.appendLocalRange(renderedRange.endIndex, Math.min(totalCount, renderedRange.endIndex + pageSize));
}
_requestLatestRange(startIndex, endIndex, reason, extras = {}) {
this.context.requestRemoteRange(this._createRequest(startIndex, endIndex, "latest", reason, "replace", "all", extras));
}
_requestAdjacentRange(startIndex, endIndex, direction, mode, loadingState) {
this.context.requestRemoteRange(this._createRequest(startIndex, endIndex, direction, "edge-scroll", mode, loadingState));
}
_createRequest(startIndex, endIndex, direction, reason, mode, loadingState, extras = {}) {
return {
startIndex,
endIndex,
direction,
reason,
mode,
loadingState,
...extras
};
}
};
//#endregion
//#region ../src/chat/collaborators/message-box.collaborator.ts
/**
* MessageBox handles the input area of the chat component.
* Delegates text input and file attachments entirely to PromptBox.
*
* This is an internal collaborator that extends Widget for event handling.
*/
var MessageBox = class MessageBox extends require_kendo_core.Widget {
static {
this.options = { name: "MessageBox" };
}
get textAreaInstance() {
return this.promptBoxInstance || this.customInput;
}
/**
* Constructs a new MessageBox instance.
*/
constructor(element, options, context) {
super(element, $.extend(true, {}, MessageBox.options, options));
this.events = [
EVENTS.sendMessage,
EVENTS.input,
EVENTS.suggestionClick,
EVENTS.replyMessageCloseButtonClick,
EVENTS.fileSelect,
EVENTS.fileRemove
];
this._generating = false;
this._typing = false;
this.customFiles = [];
this._dragHandler = null;
this.context = context;
this.chatElement = context.chatElement;
this._generating = false;
this._initWrapper();
this._initPromptBox();
this._attachEvents();
this._typing = false;
}
/**
* Creates the wrapper element for the message box.
*/
_initWrapper() {
const options = this.options;
this.wrapper = $(`<div class="${STYLES.messageBoxWrapper}"></div>`);
if (options.suggestions.length) {
let suggestionsHtml;
if (options.suggestionsTemplate) suggestionsHtml = options.suggestionsTemplate(options.suggestions);
else {
const layoutMode = this._getSuggestionsLayoutMode(options);
suggestionsHtml = renderSuggestionsWithLayout(options.suggestions, layoutMode, options.dir === "rtl");
}
this.suggestions = this.wrapper.append(suggestionsHtml);
}
this.wrapper.find(`[${REFERENCES.leftScrollButton}]`).addClass(STYLES.disabled);
this.chatElement.append(this.wrapper);
}
/**
* Gets the effective suggestions layout mode, considering legacy options.
*/
_getSuggestionsLayoutMode(options) {
if (options.suggestionsLayoutMode) return options.suggestionsLayoutMode;
if (options.suggestionsScrollable) return SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLLBUTTONS;
return SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLL;
}
/**
* Initializes the PromptBox component for message input.
*/
_initPromptBox() {
const options = this.options;
if (options.messageBoxTemplate) {
this._initCustomMessageBox(options);
return;
}
const messages = options.messages || {};
const messageBox = options.messageBox || {};
const actionButtonOpts = options.actionButton;
const fileAttachmentEnabled = options.fileAttachment !== false;
const mode = messageBox.mode || "auto";
const maxTextAreaHeight = messageBox.maxTextAreaHeight;
const rows = messageBox.rows || 1;
const startAffixTemplate = messageBox.startAffixTemplate;
const endAffixTemplate = messageBox.endAffixTemplate;
const topAffixTemplate = messageBox.topAffixTemplate;
const actionButtonSettings = this._getActionButtonSettings(actionButtonOpts);
const speechToTextSettings = this._getSpeechToTextSettings(options);
const fileAttachmentSettings = this._getFileAttachmentSettings(options, fileAttachmentEnabled);
const promptBox = new require_kendo_promptbox.PromptBox(this.element, {
mode,
maxTextAreaHeight,
rows,
_filesTemplate: this._getInternalFilesTemplate(options),
startAffixTemplate,
endAffixTemplate,
topAffixTemplate,
placeholder: messages.placeholder,
speechToTextButton: speechToTextSettings,
actionButton: actionButtonSettings,
fileSelectButton: fileAttachmentSettings,
messages: {
placeholder: messages.placeholder,
actionButton: messages.sendButton || actionButtonOpts?.text || messages.actionButton,
actionButtonLoading: messages.sendButtonLoading || actionButtonOpts?.loadingText || actionButtonOpts?.stopText || messages.actionButtonLoading || messages.stopButton
},
input: this._input.bind(this),
promptAction: this._promptBoxAction.bind(this),
fileSelect: this._promptBoxFileSelect.bind(this),
fileRemove: this._promptBoxFileRemove.bind(this)
});
promptBox.wrapper.appendTo(this.wrapper);
this.promptBoxInstance = promptBox;
}
_initCustomMessageBox(options) {
const messages = options.messages || {};
const messageBox = $(`<div class="${STYLES.messageBox}"></div>`);
const templateHtml = options.messageBoxTemplate();
messageBox.append(templateHtml);
this.wrapper.append(messageBox);
this.customMessageBox = messageBox;
this.customInput = this._findCustomInput(messageBox);
this.customSendButton = this._findCustomSendButton(messageBox);
if (this.customInput?.length) {
this.customInput.attr("aria-label", messages.messageBoxLabel);
if (!this.customInput.attr("placeholder") && messages.placeholder) this.customInput.attr("placeholder", messages.placeholder);
}
if (this.customSendButton?.length) {
const sendLabel = messages.sendButton || "Send message";
this._customSendButtonHtml = this.customSendButton.html();
this._customSendButtonText = this.customSendButton.text();
if (!this.customSendButton.attr("aria-label")) this.customSendButton.attr("aria-label", sendLabel);
if (!this.customSendButton.attr("title")) this.customSendButton.attr("title", sendLabel);
}
}
/**
* Gets action button settings to pass to PromptBox.
*/
_getActionButtonSettings(actionButtonOpts) {
return {
enable: true,
icon: actionButtonOpts?.icon,
loadingIcon: actionButtonOpts?.loadingIcon || actionButtonOpts?.stopIcon,
fillMode: actionButtonOpts?.fillMode,
rounded: actionButtonOpts?.rounded,
size: actionButtonOpts?.size,
themeColor: actionButtonOpts?.themeColor,
text: actionButtonOpts?.text,
loadingText: actionButtonOpts?.loadingText || actionButtonOpts?.stopText
};
}
/**
* Gets speech-to-text settings to pass to PromptBox.
*/
_getSpeechToTextSettings(options) {
if (options.speechToText === false) return false;
if (require_kendo_core.utilsService.isObject(options.speechToText)) return options.speechToText;
return null;
}
/**
* Gets file attachment settings to pass to PromptBox.
*/
_getFileAttachmentSettings(options, enabled) {
if (!enabled) return false;
if (require_kendo_core.utilsService.isObject(options.fileAttachment)) return {
showSpacer: true,
_renderAffixLocation: "start",
...options.fileAttachment
};
return {
showSpacer: true,
_renderAffixLocation: "start"
};
}
_getInternalFilesTemplate(options) {
if (!options.filesTemplate || options.filesTemplate === renderFiles) return null;
return (files) => options.filesTemplate(files, false, options.messages, true);
}
/**
* Handles PromptBox promptAction event.
*/
_promptBoxAction(e) {
e.preventDefault();
const actionType = e.actionType;
const text = e.value || "";
const files = e.files || [];
if (actionType === "stop") {
this.trigger(EVENTS.sendMessage, { generating: true });
return;
}
if (!text.length && !files.length) return;
const eventData = { message: {
text,
files
} };
this.trigger(EVENTS.sendMessage, eventData);
}
/**
* Handles PromptBox fileSelect event.
*/
_promptBoxFileSelect(e) {
const files = this.promptBoxInstance?.files() || [];
this.trigger(EVENTS.fileSelect, { files });
}
/**
* Handles PromptBox fileRemove event.
*/
_promptBoxFileRemove(e) {
this.trigger(EVENTS.fileRemove, {
file: e.file,
files: e.files
});
}
/**
* Attaches event handlers to message box elements.
*/
_attachEvents() {
this.wrapper.on("click.kendoChat", `[${REFERENCES.replyMessageCloseButton}]`, this._replyMessageCloseButtonClick.bind(this)).on("click.kendoChat", "." + STYLES.suggestion, this._suggestionClick.bind(this)).on("click.kendoChat", `[${REFERENCES.leftScrollButton}]`, this._leftScrollButtonClick.bind(this)).on("click.kendoChat", `[${REFERENCES.rightScrollButton}]`, this._rightScrollButtonClick.bind(this)).on("click.kendoChat", this._getCustomSendButtonSelector(), this._customSendButtonClick.bind(this)).on("input.kendoChat", this._getCustomInputSelector(), this._customInputChange.bind(this)).on("keydown.kendoChat", this._getCustomInputSelector(), this._customInputKeyDown.bind(this)).on("change.kendoChat", `.${STYLES.messageBox} input[type='file']`, this._customFileInputChange.bind(this));
this._attachDragToScrollEvents();
}
/**
* Attaches drag-to-scroll events for suggestions.
*/
_attachDragToScrollEvents() {
const scrollContainer = this.wrapper.find("." + STYLES.suggestionsScroll);
if (!scrollContainer.length) return;
this._dragHandler = require_kendo_core.domUtilsService.createDragToScrollHandler(scrollContainer, {
namespace: ".kendoChat.chatMessageBoxDrag",
captureElement: this.chatElement
});
this._dragHandler.attach();
}
/**
* Gets the current files from the PromptBox.
*/
getFiles() {
if (this._usesCustomTemplate()) return [...this.customFiles];
return this.promptBoxInstance?.files() || [];
}
/**
* Sets files on the PromptBox.
*/
setFiles(files) {
if (this._usesCustomTemplate()) {
this.customFiles = [...files];
return;
}
this.promptBoxInstance?.files(files);
}
/**
* Clears all file attachments.
*/
clearFiles() {
if (this._usesCustomTemplate()) {
this.customFiles = [];
this.customMessageBox?.find("input[type='file']").val("");
return;
}
this.promptBoxInstance?.clearFiles();
}
_getPrefixContainer() {
const container = this._getMessageBoxContainer();
let header = container.find(".k-prompt-box-header");
if (header.length === 0) {
header = $("<div class=\"k-prompt-box-header\" ref-promptbox-header><div ref-promptbox-attachments-host></div></div>");
container.prepend(header);
}
return header;
}
/**
* Sets a reply message in the message box interface.
*/
setReplyMessage(message, isOwnMessage) {
const options = this.options;
const replyContainer = this.getReplyMessageContainer();
const replyTemplate = options.messageReferenceTemplate({
text: message.text,
files: message.files,
isOwnMessage,
renderCloseButton: true,
renderFileMenuButton: false
});
const prefixContainer = this._getPrefixContainer();
prefixContainer.removeClass("k-hidden");
if (replyContainer.length === 0) prefixContainer.prepend(replyTemplate);
else replyContainer.replaceWith(replyTemplate);
}
/**
* Removes the reply message from the message box interface.
*/
removeReplyMessage() {
const replyContainer = this.getReplyMessageContainer();
if (replyContainer.length > 0) {
replyContainer.remove();
this._updateHeaderVisibility();
}
}
_updateHeaderVisibility() {
const prefixContainer = this._getPrefixContainer();
const files = this.getFiles();
const hasFiles = files && files.length > 0;
const hasReply = this.getReplyMessageContainer().length > 0;
if (!hasFiles && !hasReply) prefixContainer.addClass("k-hidden");
}
/**
* Gets the reply message container element.
*/
getReplyMessageContainer() {
return this._getPrefixContainer().find(`[${REFERENCES.messageReferenceReplyWrapper}]`);
}
loading(value) {
if (value === void 0) return this._generating;
this._generating = value;
if (this._usesCustomTemplate()) {
this._setCustomLoadingState(value);
return;
}
this.promptBoxInstance.loading(value);
}
value(newValue) {
if (newValue === void 0) return this._getInputValue();
this._setInputValue(newValue);
}
_input(e) {
const currentValue = e?.value ?? this._getInputValue();
this.trigger(EVENTS.input, { value: currentValue });
}
_replyMessageCloseButtonClick(e) {
e.preventDefault();
this.trigger(EVENTS.replyMessageCloseButtonClick);
}
_suggestionClick(e) {
e.preventDefault();
const options = this.options;
const text = $(e.target).closest("." + STYLES.suggestion).text();
if (!text.length) return;
this.trigger(EVENTS.suggestionClick, {
text,
behavior: options.suggestionsBehavior || "send"
});
if (options.suggestionsBehavior === "insert") this._setInputValue(text);
}
_getInputValue() {
if (this._usesCustomTemplate()) return this.customInput?.val() || "";
return this.promptBoxInstance.value();
}
_setInputValue(value) {
if (this._usesCustomTemplate()) {
this.customInput?.val(value);
return;
}
this.promptBoxInstance.value(value);
}
_usesCustomTemplate() {
return !!this.customMessageBox?.length;
}
_getMessageBoxContainer() {
if (this._usesCustomTemplate()) return this.customMessageBox;
return this.promptBoxInstance.wrapper;
}
_findCustomInput(container) {
return container.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();
}
_findCustomSendButton(container) {
return container.find(`[${REFERENCES.sendButton}]`).first();
}
_getCustomInputSelector() {
return `.${STYLES.messageBox} textarea, .${STYLES.messageBox} input:not([type='hidden']):not([type='file']):not([type='button']):not([type='submit']):not([type='reset']):not([type='checkbox']):not([type='radio'])`;
}
_getCustomSendButtonSelector() {
return `.${STYLES.messageBox} [${REFERENCES.sendButton}]`;
}
_customSendButtonClick(e) {
if (!this._usesCustomTemplate()) return;
const button = $(e.currentTarget);
if (this.customSendButton?.length && !button.is(this.customSendButton)) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation?.();
this._sendCustomTemplateMessage();
}
_customInputChange(e) {
if (!this._usesCustomTemplate()) return;
this._input({ value: $(e.currentTarget).val() || "" });
}
_customInputKeyDown(e) {
if (!this._usesCustomTemplate()) return;
if (!(e.key === "Enter" || e.which === 13 || e.keyCode === 13) || this._generating || !this.customSendButton?.length) return;
if (e.shiftKey && $(e.currentTarget).is("textarea")) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation?.();
this._sendCustomTemplateMessage();
}
_customFileInputChange(e) {
if (!this._usesCustomTemplate()) return;
const input = e.currentTarget;
const selectedFiles = Array.from(input.files || []).map((file) => ({
uid: require_kendo_core.utilsService.guid(),
name: file.name,
size: file.size,
extension: file.name.lastIndexOf(".") > -1 ? file.name.substring(file.name.lastIndexOf(".")).toLowerCase() : "",
rawFile: file
}));
if (!selectedFiles.length) return;
if (input.multiple) this.customFiles = [...this.customFiles, ...selectedFiles];
else this.customFiles = selectedFiles;
this.trigger(EVENTS.fileSelect, { files: [...this.customFiles] });
}
_sendCustomTemplateMessage() {
if (this._generating) {
this.trigger(EVENTS.sendMessage, { generating: true });
return;
}
const text = this._getInputValue().trim();
const files = this.getFiles();
if (!text.length && !files.length) return;
this.trigger(EVENTS.sendMessage, { message: {
text,
files
} });
this._setInputValue("");
this.clearFiles();
this.customInput?.trigger("focus");
}
_setCustomLoadingState(value) {
const messages = this.options.messages || {};
if (!this.customSendButton?.length) return;
const label = value ? messages.sendButtonLoading || "Stop" : messages.sendButton || "Send message";
this.customSendButton.toggleClass(STYLES.generating, value);
this.customSendButton.attr("aria-label", label);
this.customSendButton.attr("title", label);
if (value) {
if (this._customSendButtonText?.trim()) this.customSendButton.text(label);
return;
}
if (this._customSendButtonText?.trim()) this.customSendButton.text(this._customSendButtonText);
else if (this._customSendButtonHtml !== void 0) this.customSendButton.html(this._customSendButtonHtml);
}
_leftScrollButtonClick(e) {
e.preventDefault();
const button = $(e.currentTarget);
const scrollableElement = this.wrapper.find(`.${STYLES.suggestionsScroll}`);
const isRtl = this.options.dir === "rtl";
const position = require_kendo_core.domUtilsService.scrollByDelta(scrollableElement, isRtl ? 200 : -200);
this.wrapper.find(`[${REFERENCES.rightScrollButton}]`).removeClass(STYLES.disabled);
if (position.atStart) button.addClass(STYLES.disabled);
}
_rightScrollButtonClick(e) {
e.preventDefault();
const button = $(e.currentTarget);
const scrollableElement = this.wrapper.find(`.${STYLES.suggestionsScroll}`);
const isRtl = this.options.dir === "rtl";
const position = require_kendo_core.domUtilsService.scrollByDelta(scrollableElement, isRtl ? -200 : 200);
this.wrapper.find(`[${REFERENCES.leftScrollButton}]`).removeClass(STYLES.disabled);
if (position.atEnd) button.addClass(STYLES.disabled);
}
destroy() {
if (this._dragHandler) this._dragHandler.destroy();
if (this.promptBoxInstance) this.promptBoxInstance.destroy();
this.customFiles = [];
this.wrapper.remove();
}
};
//#endregion
//#region ../src/chat/collaborators/message-menu.collaborator.ts
/**
* MessageMenu provides context menu functionality for chat messages.
* Handles actions like reply, copy, pin, and delete for individual messages.
*
* This is an internal collaborator that extends Widget for event handling.
*/
var MessageMenu = class MessageMenu extends require_kendo_core.Widget {
static {
this.options = {
name: "ChatMessageMenu",
filter: `.${STYLES.chatBubble}`,
dataSource: [],
keyboardAlignToAnchor: true
};
}
/**
* Constructs a new MessageMenu instance.
*/
constructor(element, options) {
options = $.extend(true, {}, MessageMenu.options, options);
options.dataSource = MessageMenu.normalizeActions(options.dataSource || []);
super(element, options);
this.create();
this.attachEvents();
}
/**
* Creates the underlying Kendo UI ContextMenu.
*/
create() {
this.menu = new kendo.ui.ContextMenu(this.element, this.options);
}
/**
* Attaches event handlers to the context menu.
*/
attachEvents() {
this.menu.bind("select", this.onSelect.bind(this)).bind("open", this.onOpen.bind(this)).bind("close", this.onClose.bind(this));
}
/**
* Sets command attributes on menu items for identification.
*/
static normalizeActions(actions) {
return (actions || []).map((item) => ({
...item,
attr: {
...item.attr || {},
"data-command": item.name.toLowerCase()
}
}));
}
setActions(actions) {
const dataSource = MessageMenu.normalizeActions(actions);
this.options.dataSource = dataSource;
this.menu.setOptions({ dataSource });
}
/**
* Handles menu item selection.
*/
onSelect(e) {
const item = $(e.item);
const message = $(e.target).closest("." + STYLES.message);
const command = item.data("command");
if (command) this.executeCommand(command, item, message);
}
/**
* Handles menu open event.
*/
onOpen(e) {
let setActive = true;
const target = $(e.target);
const originalTarget = $(e.event?.target).length ? $(e.event.target) : target;
const isMessageRemoved = target.closest("." + STYLES.messageRemoved).length > 0;
const hasTypingIndicator = target.find("." + STYLES.typingIndicator).length > 0;
const isAttachmentMenuButton = originalTarget.closest(`[${REFERENCES.fileMenuButton}]`).length > 0;
const message = target.closest("." + STYLES.message);
if (isMessageRemoved || hasTypingIndicator || isAttachmentMenuButton || !message.length) {
e.preventDefault();
setActive = false;
}
target.toggleClass(STYLES.active, setActive);
if (this.trigger("open", { message })) {
e.preventDefault();
target.removeClass(STYLES.active);
}
}
/**
* Handles menu close event.
*/
onClose(e) {
const target = $(e.target);
const message = target.closest("." + STYLES.message);
target.removeClass(STYLES.active);
this.trigger("close", { message });
}
/**
* Executes a command from the menu.
*/
executeCommand(command, item, message) {
this.trigger("execute", {
type: command,
item,
message
});
}
/**
* Toggles the visibility of the delete menu item.
*/
toggleDeleteVisibility(visible) {
const deleteItem = this.element.find("[data-command='delete']");
if (deleteItem.length) deleteItem.toggleClass(STYLES.hidden, !visible);
}
destroy() {
if (this.menu) {
this.menu.destroy();
this.menu = null;
this.element.remove();
}
}
};
//#endregion
//#region ../src/chat/collaborators/remote-range-loader.collaborator.ts
var RemoteRangeLoader = class {
constructor(context) {
this._pendingRemoteRangeRequests = {};
this._activeLatestRecoveryRequestKey = null;
this._queuedLatestRecoveryRequest = null;
this.context = context;
}
reset() {
this._pendingRemoteRangeRequests = {};
this._activeLatestRecoveryRequestKey = null;
this._queuedLatestRecoveryRequest = null;
}
requestRange(options) {
const preparedRequest = this._prepareRequest(options);
if (!preparedRequest) {
this.context.updateEndlessState();
return;
}
if (this._mergePendingRequest(preparedRequest) || this._queueLatestRecoveryRequestIfNeeded(preparedRequest)) return;
const requestState = this._activateRequest(preparedRequest);
this._dispatchRequest(requestState);
}
_prepareRequest(options) {
const requestOptions = this._normalizeRequestOptions(options);
if (!requestOptions) return null;
return {
key: this._getRemoteRangeRequestKey(requestOptions),
options: requestOptions
};
}
_normalizeRequestOptions(options) {
const normalizedRange = this._normalizeRequestedRange(options);
if (!normalizedRange) return null;
return {
...options,
startIndex: normalizedRange.startIndex,
endIndex: normalizedRange.endIndex
};
}
_normalizeRequestedRange(options) {
const totalCount = this.context.getTotalCount();
const isLatestScrollToBottomRequest = this._isLatestScrollToBottomRequest(options);
let safeStartIndex = Math.max(0, options.startIndex);
let safeEndIndex = Math.max(safeStartIndex, options.endIndex);
if (isLatestScrollToBottomRequest) {
const requestedWindow = Math.max(1, safeEndIndex - safeStartIndex);
return {
startIndex: safeStartIndex,
endIndex: safeStartIndex + requestedWindow
};
}
safeStartIndex = Math.max(0, Math.min(safeStartIndex, totalCount));
safeEndIndex = Math.max(safeStartIndex, Math.min(safeEndIndex, totalCount));
if (safeEndIndex <= safeStartIndex) return null;
return {
startIndex: safeStartIndex,
endIndex: safeEndIndex
};
}
_mergePendingRequest(preparedRequest) {
const pendingRequest = this._pendingRemoteRangeRequests[preparedRequest.key];
if (!pendingRequest) return false;
pendingRequest.options.scrollToBottom = pendingRequest.options.scrollToBottom || !!preparedRequest.options.scrollToBottom;
pendingRequest.options.loadingState = this._mergeRemoteLoadingState(pendingRequest.options.loadingState, preparedRequest.options.loadingState);
this.context.setRemoteLoadingState(pendingRequest.options.loadingState);
return true;
}
_queueLatestRecoveryRequestIfNeeded(preparedRequest) {
if (!this._isLatestScrollToBottomRequest(preparedRequest.options) || !this._activeLatestRecoveryRequestKey || this._activeLatestRecoveryRequestKey === preparedRequest.key) return false;
this._queueLatestRecoveryRequest(preparedRequest.options);
this.context.setRemoteLoadingState(this._queuedLatestRecoveryRequest.loadingState);
return true;
}
_activateRequest(preparedRequest) {
const requestState = {
key: preparedRequest.key,
options: preparedRequest.options
};
this._pendingRemoteRangeRequests[requestState.key] = requestState;
if (this._isLatestScrollToBottomRequest(requestState.options)) this._activeLatestRecoveryRequestKey = requestState.key;
this.context.setRemoteLoadingState(requestState.options.loadingState);
return requestState;
}
_dispatchRequest(requestState) {
const transportRead = this.context.getDataSource()?.transport?.read;
if (typeof transportRead !== "function") {
this._handleRequestError(requestState);
return;
}
transportRead({
data: this._createTransportRequestData(requestState.options),
success: (response) => {
this._handleRequestSuccess(requestState, response);
},
error: () => {
this._handleRequestError(requestState);
}
});
}
_createTransportRequestData(options) {
return this._buildRemoteRequestData(options.startIndex, options.endIndex, options.direction, options.reason, options.targetMessageId);
}
_handleRequestSuccess(requestState, response) {
const dataSource = this.context.getDataSource();
const lifecycle = this._createRequestLifecycle(requestState);
if (!dataSource) {
lifecycle.finalize();
return;
}
try {
const parsedResponse = this._parseResponse(dataSource, response, requestState.options);
this._resolveMissingReplyReferences(requestState.options, parsedResponse.models, () => {
this._applyResponse(requestState.options, parsedResponse);
lifecycle.finalize();
}, lifecycle.fail);
} catch (error) {
if (this._pendingRemoteRangeRequests[requestState.key]) lifecycle.finalize();
throw error;
}
}
_createRequestLifecycle(requestState) {
return {
finalize: () => {
this._completeRequest(requestState);
},
fail: (error) => {
this._completeRequest(requestState);
throw this._normalizeReferenceResolverError(error);
}
};
}
_parseResponse(dataSource, response, requestOptions) {
const parsedResponse = dataSource.reader.parse(response);
const items = dataSource.reader.data(parsedResponse) || [];
const responseTotal = Number(dataSource.reader.total(parsedResponse));
const totalCount = this._extractRemoteResponseTotal(responseTotal);
const models = this._createRemoteModels(items);
const range = this._extractRemoteResponseRange(parsedResponse, models.length, totalCount, requestOptions);
this._validateReferenceJumpResponse(requestOptions, models);
this._validateAdjacentRangeResponse(requestOptions, range, models);
return {
range,
models,
totalCount
};
}
_applyResponse(requestOptions, parsedResponse) {
this.context.applyResponse(requestOptions, parsedResponse);
}
_handleRequestError(requestState) {
this.context.updateEndlessState();
this._completeRequest(requestState);
}
_completeRequest(requestState) {
const queuedLatestRequest = this._clearRequestState(requestState.key);
this.context.completeEndlessLoad(requestState.options.loadingState, () => {
this._resumeQueuedLatestRecoveryRequest(queuedLatestRequest);
});
}
_clearRequestState(requestKey) {
const queuedLatestRequest = this._consumeQueuedLatestRecoveryRequest(requestKey);
delete this._pendingRemoteRangeRequests[requestKey];
if (this._activeLatestRecoveryRequestKey === requestKey) this._activeLatestRecoveryRequestKey = null;
return queuedLatestRequest;
}
_resumeQueuedLatestRecoveryRequest(requestOptions) {
if (requestOptions && this.context.canRequestRange()) this.requestRange(requestOptions);
}
_mergeRemoteLoadingState(current, next) {
if (current === "all" || next === "all" || current !== next) return "all";
return current;
}
_getRemoteRangeRequestKey(options) {
return [
options.startIndex,
options.endIndex,
options.direction,
options.reason,
options.mode,
options.targetMessageId == null ? "" : String(options.targetMessageId)
].join("|");
}
_isLatestScrollToBottomRequest(options) {
return options.direction === "latest" && options.reason === "scroll-to-bottom" && options.mode === "replace";
}
_isReferenceJumpRequest(options) {
return options.reason === "reference-jump" && options.mode === "replace" && options.targetMessageId != null;
}
_isLatestRecoveryRequest(options) {
if (!this._isLatestScrollToBottomRequest(options)) return false;
const renderedRange = this.context.getRenderedRange();
return renderedRange.endIndex > renderedRange.startIndex;
}
_queueLatestRecoveryRequest(options) {
if (!this._queuedLatestRecoveryRequest) {
this._queuedLatestRecoveryRequest = { ...options };
return;
}
this._queuedLatestRecoveryRequest = {
...options,
scrollToBottom: this._queuedLatestRecoveryRequest.scrollToBottom || !!options.scrollToBottom,
loadingState: this._mergeRemoteLoadingState(this._queuedLatestRecoveryRequest.loadingState, options.loadingState)
};
}
_consumeQueuedLatestRecoveryRequest(requestKey) {
if (this._activeLatestRecoveryRequestKey !== requestKey || !this._queuedLatestRecoveryRequest) return null;
const queuedRequest = this._queuedLatestRecoveryRequest;
this._queuedLatestRecoveryRequest = null;
return queuedRequest;
}
_buildRemoteRequestData(startIndex, endIndex, direction, reason, targetMessageId) {
const totalCount = this.context.getTotalCount();
const safeStartIndex = Math.max(0, startIndex);
const safeEndIndex = Math.max(safeStartIndex, Math.min(endIndex, totalCount || endIndex));
const take = Math.max(1, safeEndIndex - safeStartIndex);
const page = totalCount ? Math.max(1, Math.ceil(Math.max(totalCount - safeEndIndex, 0) / take) + 1) : 1;
if (direction === "latest" && reason === "scroll-to-bottom") return {
page,
pageSize: take,
skip: safeStartIndex,
take,
direction,
reason,
intent: "latest",
targetMessageId
};
if (reason === "reference-jump") return {
page: 1,
pageSize: take,
skip: 0,
take,
direction,
reason,
targetMessageId
};
return {
page,
pageSize: take,
skip: safeStartIndex,
take,
startIndex: safeStartIndex,
endIndex: safeEndIndex,
direction,
reason,
targetMessageId
};
}
_extractRemoteResponseTotal(totalCount) {
if (!Number.isInteger(totalCount) || totalCount < 0) throw new Error("The Chat endless response must include a valid total value.");
return totalCount;
}
_extractRemoteResponseRange(parsedResponse, itemCount, totalCount, requestOptions) {
const responseStartIndex = Number(parsedResponse?.startIndex);
const responseEndIndex = Number(parsedResponse?.endIndex);
if (!Number.isInteger(responseStartIndex) || !Number.isInteger(responseEndIndex)) throw new Error("The Chat endless response must include valid startIndex and endIndex values.");
const startIndex = responseStartIndex;
const endIndex = responseEndIndex;
if (startIndex < 0 || endIndex < startIndex || endIndex > totalCount) throw new Error("The Chat endless response returned an invalid range window.");
if (endIndex - startIndex !== itemCount) throw new Error("The Chat endless response count does not match the returned range.");
if (this._isLatestScrollToBottomRequest(requestOptions) && endIndex !== totalCount) {
if (!this._isLatestRecoveryRequest(requestOptions) || startIndex !== requestOptions.startIndex || endIndex !== requestOptions.endIndex) throw new Error("The Chat endless latest response must end at total.");
}
return {
startIndex,
endIndex
};
}
_validateReferenceJumpResponse(requestOptions, models) {
if (!this._isReferenceJumpRequest(requestOptions)) return;
const targetMessageId = String(requestOptions.targetMessageId);
if (!models.some((message) => String(message?.id) === targetMessageId)) throw new Error("The Chat endless jump response must contain the requested targetMessageId.");
}
_validateAdjacentRangeResponse(requestOptions, range, models) {
if (requestOptions.mode !== "prepend" && requestOptions.mode !== "append") return;
if (range.startIndex !== requestOptions.startIndex || range.endIndex !== requestOptions.endIndex) throw new Error("The Chat endless response must match the requested adjacent range.");
const loadedRange = this.context.getLoadedRange();
if (requestOptions.mode === "prepend" && range.endIndex !== loadedRange.startIndex) throw new Error("The Chat endless response must match the requested adjacent range.");
if (requestOptions.mode === "append" && range.startIndex !== loadedRange.endIndex) throw new Error("The Chat endless response must match the requested adjacent range.");
const loadedMessages = this.context.getLoadedMessagesInRange(loadedRange.startIndex, loadedRange.endIndex);
const loadedIds = new Set(loadedMessages.map((message) => String(message.id)));
if (models.some((message) => loadedIds.has(String(message.id)))) throw new Error("The Chat endless response must match the requested adjacent range.");
}
_createRemoteModels(items) {
const model = this.context.getDataSource()?.reader?.model;
return items.map((item) => {
return model ? new model(item) : item;
});
}
_collectMissingReplyTargetIds(requestOptions, models) {
const missingIds = [];
const seenIds = /* @__PURE__ */ new Set();
const availableIds = /* @__PURE__ */ new Set();
if (requestOptions.mode !== "replace") {
const loadedRange = this.context.getLoadedRange();
this.context.getLoadedMessagesInRange(loadedRange.startIndex, loadedRange.endIndex).forEach((message) => {
if (message?.id != null) availableIds.add(String(message.id));
});
}
models.forEach((message) => {
if (message?.id != null) availableIds.add(String(message.id));
});
models.forEach((message) => {
const replyToId = message?.replyToId;
if (!replyToId) return;
const key = String(replyToId);
if (availableIds.has(key) || this.context.getReferenceMessageById(replyToId) || seenIds.has(key)) return;
seenIds.add(key);
missingIds.push(replyToId);
});
return missingIds;
}
_resolveMissingReplyReferences(requestOptions, models, onSuccess, onError) {
const missingIds = this._collectMissingReplyTargetIds(requestOptions, models);
if (!missingIds.length) {
onSuccess();
return;
}
const referenceResolver = this.context.getChatOptions().referenceResolver;
if (typeof referenceResolver !== "function") {
onError(/* @__PURE__ */ new Error("The Chat referenceResolver option is required to render off-batch reply previews."));
return;
}
let completed = false;
referenceResolver({
value: missingIds,
success: (dataItems) => {
if (completed) return;
completed = true;
try {
const resolvedMessages = this._mapResolvedReferenceMessages(missingIds, dataItems);
this.context.cacheResolvedReferenceMessages(resolvedMessages);
onSuccess();
} catch (error) {
onError(error);
}
},
error: (error) => {
if (completed) return;
completed = true;
onError(error);
}
});
}
_mapResolvedReferenceMessages(requestedIds, dataItems) {
const resolvedById = {};
(Array.isArray(dataItems) ? dataItems : []).forEach((item) => {
this._validateResolvedReferenceMessage(item);
const key = String(item.id);
if (resolvedById[key]) return;
resolvedById[key] = item;
});
return requestedIds.map((id) => {
const resolvedMessage = resolvedById[String(id)];
if (!resolvedMessage) throw new Error("The Chat referenceResolver must resolve every requested id.");
return resolvedMessage;
});
}
_validateResolvedReferenceMessage(item) {
const hasId = item?.id !== void 0 && item?.id !== null && String(item.id) !== "";
const hasAuthorId = item?.authorId !== void 0 && item?.authorId !== null && String(item.authorId) !== "";
const hasText = typeof item?.text === "string" && item.text.length > 0;
const hasFiles = !!item?.files && typeof item.files.length === "number" && item.files.length > 0;
const isDeleted = item?.isDeleted === true;
if (!hasId || !hasAuthorId || !hasText && !hasFiles && !isDeleted) throw new Error("The Chat referenceResolver must return items with id, authorId, and enough data to render the reply preview.");
}
_normalizeReferenceResolverError(error) {
if (error instanceof Error) return error;
if (typeof error === "string" || typeof error === "number" || typeof error === "boolean") return /* @__PURE__ */ new Error(`${error}`);
if (typeof error === "symbol") return new Error(error.description || "The Chat referenceResolver failed.");
if (typeof error === "object" && error) {
if ("message" in error && typeof error.message === "string") return new Error(error.message);
try {
return new Error(JSON.stringify(error));
} catch {
return /* @__PURE__ */ new Error("The Chat referenceResolver failed.");
}
}
return /* @__PURE__ */ new Error("The Chat referenceResolver failed.");
}
};
//#endregion
//#region ../src/chat/helpers/command-handler.ts
var CommandHandler = class {
constructor(context) {
this.context = context;
}
commandExecute(e) {
const message = this.context.dataItem(e.message);
if (!message) return;
const file = this.context.fileDataItem(message, e.file);
const type = e.type;
if (!file) switch (type) {
case COMMANDS.reply:
this.messageReply(message);
break;
case COMMANDS.copy:
this.messageCopy(message);
break;
case COMMANDS.pin:
this.messagePin(message);
break;
case COMMANDS.delete:
this.messageDelete(message);
break;
}
else {
switch (type) {
case COMMANDS.download:
this.context.trigger(EVENTS.download, {
files: [file],
message
});
break;
}
this.context.trigger(EVENTS.fileMenuAction, {
type,
file,
message
});
}
}
contextMenuExecute(e) {
this.commandExecute(e);
const message = this.context.dataItem(e.message);
const type = e.type;
this.context.trigger(EVENTS.contextMenuAction, {
type,
message
});
}
messageReply(message) {
const isOwnMessage = String(message.authorId) === String(this.context.getUserId());
this.context.setCurrentMessageReplyId(message.id);
this.context.setReplyMessage(message, isOwnMessage);
this.context.setupAriaAttributes();
}
messageCopy(message) {
if (navigator && navigator.clipboard) navigator.clipboard.writeText(message.text);
}
messagePin(message) {
this.context.pinMessage(message);
}
messageDelete(message) {
this.context.removeMessage(message);
}
messageContextMenuOpen(e) {
const message = this.context.dataItem(e.message);
if (!message) return;
const isAuthor = String(message.authorId) === String(this.context.getUserId());
if (!this.context.isContextMenuAllowed(isAuthor)) {
e.preventDefault();
return;
}
this.context.setContextMenuActions(isAuthor);
this.context.toggleDeleteVisibility(isAuthor);
}
};
//#endregion
//#region ../src/chat/helpers/event-wiring.ts
var EventWiring = class {
constructor(context) {
this.context = context;
}
_triggerSendMessage(message) {
const ctx = this.context;
ctx.trigger(EVENTS.sendMessage, { message });
ctx.prepareLatestMessages();
ctx.postMessage(message);
}
_prepareLatestAndTrigger(eventName, args) {
const ctx = this.context;
ctx.prepareLatestMessages();
ctx.trigger(eventName, args);
}
attach() {
this._attachViewEvents();
this._attachMessageBoxEvents();
this._attachContextMenuEvents();
this._attachWrapperDomEvents();
}
_attachViewEvents() {
const ctx = this.context;
ctx.view.bind("suggestedActionClick", (args) => {
if (ctx.options.suggestionsBehavior === "insert") {
ctx.value(args.text);
ctx.trigger(EVENTS.suggestionClick, { text: args.text });
} else {
const message = {
text: args.text,
files: []
};
this._triggerSendMessage(message);
}
}).bind("messageToolbarExecute", (args) => {
const message = ctx.dataItem(args.message);
ctx.commandExecute({
type: args.type,
message: args.message
});
ctx.trigger(EVENTS.toolbarAction, {
type: args.type,
message
});
}).bind("fileMenuExecute", (args) => {
ctx.commandExecute(args);
}).bind("downloadAllFiles", (args) => {
const message = ctx.dataItem(args.messageElement);
ctx.trigger(EVENTS.download, {
files: message?.files,
message
});
}).bind("executeAction", (args) => {
ctx.trigger(EVENTS.executeAction, args);
}).bind("resendMessage", (args) => {
this._prepareLatestAndTrigger(EVENTS.resendMessage, args);
}).bind("expandableToggle", (args) => {
ctx.accessibility.updateExpandableIndicatorAria(args.indicator, args.isExpanded);
});
}
_attachMessageBoxEvents() {
const ctx = this.context;
ctx.messageBox.bind("input", (args) => {
ctx.trigger(EVENTS.input, { value: args.value });
}).bind("sendMessage", (args) => {
const generating = args.generating;
if (generating) ctx.trigger(EVENTS.sendMessage, { generating });
else {
const message = {
text: args.text || args.message?.text,
files: args.files || args.message?.files || []
};
this._triggerSendMessage(message);
ctx.clearReplyState();
}
}).bind("suggestionClick", (args) => {
ctx.trigger(EVENTS.suggestionClick, { text: args.text });
if (args.behavior !== "insert") {
const message = {
text: args.text,
files: []
};
this._triggerSendMessage(message);
}
}).bind("fileSelect", (args) => {
ctx.trigger(EVENTS.fileSelect, { files: args.files });
}).bind("fileRemove", (args) => {
ctx.trigger(EVENTS.fileRemove, {
file: args.file,
files: args.files
});
}).bind("replyMessageCloseButtonClick", () => {
ctx.clearReplyState();
});
}
_attachContextMenuEvents() {
const ctx = this.context;
ctx.messageContextMenu.bind("execute", ctx.contextMenuExecute.bind(ctx)).bind("open", ctx.messageContextMenuOpen.bind(ctx));
}
_attachWrapperDomEvents() {
const ctx = this.context;
ctx.wrapper.on("click.kendoChat", `[${REFERENCES.pinnedMessageCloseButton}]`, () => {
ctx.trigger(EVENTS.unpin, { message: ctx.getPinnedMessage() });
ctx.clearPinnedMessage();
});
ctx.wrapper.on("click.kendoChat", `[${REFERENCES.messageReferencePinWrapper}]`, (e) => {
if ($(e.target).closest(`[${REFERENCES.pinnedMessageCloseButton}]`).length) return;
if ($(e.target).is("a")) return;
else {
e.preventDefault();
e.stopPropagation();
}
const pinnedMessage = ctx.getPinnedMessage();
if (pinnedMessage) ctx.navigateToReferencedMessage(pinnedMessage.id);
});
ctx.wrapper.on("click.kendoChat", `[${REFERENCES.messageReferenceReplyWrapper}]`, (e) => {
if ($(e.target).is("a")) return;
else {
e.preventDefault();
e.stopPropagation();
}
const messageElement = $(e.currentTarget).closest("." + STYLES.message);
const currentMessage = ctx.dataItem(messageElement);
if (currentMessage && currentMessage.replyToId) ctx.navigateToReferencedMessage(currentMessage.replyToId);
});
ctx.wrapper.on("keydown.kendoChat", (e) => {
ctx.accessibility.handleKeyDown(e, ctx.messageBoxRef);
});
ctx.wrapper.on(`focus${NS}`, `.${STYLES.bubble}`, (e) => {
const bubbleElement = $(e.target);
if (bubbleElement.find(`.${STYLES.typingIndicator}`).length > 0) return;
if (bubbleElement.attr("data-keyboard-focus") === "true") {
const messageElement = bubbleElement.closest(`.${STYLES.message}`);
const message = ctx.dataItem(messageElement);
if (message && message.uid) ctx.scrollToMessage(message.uid);
}
});
}
};
//#endregion
//#region ../src/chat/chat.widget.ts
const DEFAULT_MESSAGE_ACTIONS = [
{
name: COMMANDS.reply,
text: "Reply",
icon: ICONS.undo
},
{
name: COMMANDS.copy,
text: "Copy",
icon: ICONS.copy
},
{
name: COMMANDS.pin,
text: "Pin",
icon: ICONS.pin
},
{
name: COMMANDS.delete,
text: "Delete",
icon: ICONS.trash
}
];
const DEFAULT_FILE_ACTIONS = [{
name: COMMANDS.download,
text: "Download",
icon: ICONS.download
}];
const DEFAULT_MESSAGES = {
messageListLabel: "Message list",
placeholder: "Type a message...",
sendButton: "Send message",
sendButtonLoading: "Stop",
actionButton: "Send message",
actionButtonLoading: "Stop",
stopButton: "Stop",
speechToTextButton: "Toggle speech to text",
fileButton: "Attach file",
downloadAll: "Download all",
selfMessageDeleted: "You removed this message.",
otherMessageDeleted: "This message was removed by its sender.",
stopGeneration: "Stop generation",
messageBoxLabel: "Type your message here",
pinnedMessageCloseButton: "Unpin message",
replyMessageCloseButton: "Remove reply",
fileMenuButton: "File menu",
retryMessage: "Retry"
};
var Chat = class Chat extends require_kendo_core.Widget {
static {
this.options = {
name: "Chat",
scrollMode: "scrollable",
pageSize: 20,
endlessScrollDebounceDelay: 150,
autoAssignId: true,
autoBind: true,
allowMessageCollapse: false,
showUsername: true,
showAvatar: true,
timestampVisibility: "onFocus",
suggestionsBehavior: "send",
authorId: null,
suggestionsScrollable: false,
suggestedActionsScrollable: false,
suggestionsLayoutMode: "scroll",
suggestedActionsLayoutMode: "scroll",
speechToText: true,
fileAttachment: true,
scrollToBottomButton: true,
autoScrollThreshold: "20%",
loading: false,
messageBox: {
mode: "multi",
maxTextAreaHeight: 110
},
messageToolbarActions: [],
messageActions: DEFAULT_MESSAGE_ACTIONS,
fileActions: DEFAULT_FILE_ACTIONS,
suggestions: [],
headerItems: [],
dir: "ltr",
messageTimeFormat: "ddd MMM dd yyyy",
width: null,
height: null,
messages: DEFAULT_MESSAGES,
messageWidthMode: "standard",
messageTemplate: null,
messageGroupTemplate: renderMessageGroup,
messageReferenceTemplate: renderMessageReference,
filesTemplate: renderFiles,
suggestionsTemplate: null,
suggestedActionsTemplate: renderSuggestions,
suggestedActionsField: "suggestedActions",
timestampTemplate: null,
messageStatusTemplate: null,
messageStatusSettings: null,
headerTemplate: null,
noDataTemplate: null,
messageContentTemplate: null,
userStatusTemplate: null,
attachmentTemplate: null,
messageBoxTemplate: null,
authorMessageSettings: null,
receiverMessageSettings: null,
actionButton: null,
attachmentLayout: "list",
filesLayoutMode: "vertical",
pinnedMessages: [],
referenceResolver: null,
textField: "text",
authorIdField: "authorId",
authorNameField: "authorName",
authorImageUrlField: "authorImageUrl",
authorImageAltTextField: "authorImageAltText",
idField: "id",
timestampField: "timestamp",
filesField: "files",
replyToIdField: "replyToId",
isDeletedField: "isDeleted",
isPinnedField: "isPinned",
isTypingField: "isTyping",
statusField: "status",
failedField: "failed",
attachmentsField: "attachments",
attachmentLayoutField: "attachmentLayout",
skipSanitization: false
};
}
/**
* Creates a new Chat widget instance.
*
* @param element - The DOM element to attach the widget to
* @param options - Configuration options
* @param events - Optional event handlers
*/
constructor(element, options, events) {
options = options || {};
Chat.mergeContextMenuActions(options, Chat.options, "messageActions");
Chat.mergeContextMenuActions(options, Chat.options, "fileActions");
super(element, $.extend(true, {}, Chat.options, options));
this.currentMessageReplyId = null;
this._hasHeader = false;
this._loading = false;
this._renderStartIndex = 0;
this._renderEndIndex = 0;
this._endlessPageSize = 20;
this._endlessRangeManager = new EndlessRangeManager();
this.events = [
EVENTS.sendMessage,
EVENTS.suggestionClick,
EVENTS.unpin,
EVENTS.input,
EVENTS.toolbarAction,
EVENTS.fileMenuAction,
EVENTS.contextMenuAction,
EVENTS.download,
EVENTS.fileSelect,
EVENTS.fileRemove,
EVENTS.executeAction,
EVENTS.resendMessage,
EVENTS.referencedMessageClick
];
this.options.messageActions = options.messageActions;
this.options.fileActions = options.fileActions;
if (events) this._events = events;
this._init(options);
}
static mergeContextMenuActions(options, defaultOptions, property) {
const userActions = options[property];
if (Array.isArray(userActions)) {
const defaultActions = defaultOptions[property] || [];
const defaultMap = {};
for (const action of defaultActions) defaultMap[action.name] = { ...action };
options[property] = userActions.map((userAction) => {
if (userAction.name in defaultMap) return {
...defaultMap[userAction.name],
...userAction
};
return { ...userAction };
});
} else options[property] = defaultOptions[property] || [];
}
_init(options) {
this.currentMessageReplyId = null;
this._hasHeader = !!options.headerItems?.length;
this._initUser();
this._initWrapper();
this._initDataSource();
this._validateRemoteEndlessStartup();
this._initView();
if (options.toolbar) require_kendo_core.utilsService.logToConsole("The 'toolbar' option has been deprecated.", "warn");
this._initMessageBox();
this._initMenus();
this._initAccessibility();
this._initRemoteRangeLoader();
this._initEndlessScrollCoordinator();
this._attachEvents();
if (this.options.loading) this.loading(true);
if (this._isEndlessScrollEnabled()) this.view._scrollManager._isLoadingOlder = true;
if (this.options.autoBind) if (this._isRemoteEndlessScroll()) this._requestLatestRemoteStartupRange();
else this.dataSource.fetch();
this.scrollToBottom();
}
_validateRemoteEndlessStartup() {
if (this._isRemoteEndlessScroll() && this.options.autoBind === false) throw new Error("The Chat remote endless mode does not support autoBind: false.");
}
_requestLatestRemoteStartupRange() {
this._endlessScrollCoordinator.requestLatestRemoteStartupRange();
}
_initUser() {
const options = this.options;
if (options.authorId) {
options.authorId = options.authorId.toString();
return;
}
options.authorId = require_kendo_core.utilsService.guid();
}
_initWrapper() {
const options = this.options;
const height = options.height;
const width = options.width;
const headerItems = options.headerItems;
const headerTemplate = options.headerTemplate;
const uiElements = `<div ${REFERENCES.viewWrapper}></div>`;
this.wrapper = this.element.addClass(STYLES.wrapper).attr("dir", options.dir).append(uiElements);
if (this._hasHeader || headerTemplate) this.wrapper.prepend(renderHeader(headerItems, headerTemplate));
if (height) this.wrapper.css({
height,
minHeight: height
});
if (width) this.wrapper.css({
width,
minWidth: width
});
}
_initDataSource() {
this._refreshHandler = this.refresh.bind(this);
this._dataManager = new DataManager({ chatOptions: this.options });
this.dataSource = this._dataManager.getDataSource();
this._syncEndlessPageSize();
this.dataSource.bind(CHANGE, this._refreshHandler);
}
_syncEndlessPageSize() {
const options = this.options;
this._endlessPageSize = this._endlessRangeManager.syncPageSize(this._isEndlessScrollEnabled(options), this.dataSource);
}
_getEndlessPageSize() {
return this._endlessPageSize;
}
_unbindDataSource() {
this.dataSource.unbind(CHANGE, this._refreshHandler);
this._dataManager = null;
}
_initView() {
const options = $.extend(true, {}, this.options);
delete options.name;
const element = this.wrapper.find(`[${REFERENCES.viewWrapper}]`);
const context = { dataManager: this._dataManager };
if (this._isEndlessScrollEnabled()) {
context.onScrollNearTop = this._loadOlderMessages.bind(this);
context.onScrollNearBottom = this._loadNewerMessages.bind(this);
context.onScrollToLatest = this._prepareLatestMessages.bind(this);
context.isLatestRangeActive = this._isLatestRenderedRange.bind(this);
context.hasLatestMessageRendered = this._hasRenderedLatestMessage.bind(this);
}
this.view = new ChatView(element, options, context);
}
_initMessageBox() {
const options = $.extend(true, {}, this.options);
delete options.name;
this.messageBox = new MessageBox($("<textarea></textarea>"), options, { chatElement: this.wrapper });
}
_initMenus() {
const messageActions = this.options.messageActions;
this.messageContextMenu = new MessageMenu($("<ul></ul>"), {
dataSource: messageActions,
target: this.wrapper
});
}
_initAccessibility() {
this.accessibility = new AccesibilityManager({
wrapper: this.wrapper,
getOptions: () => this.options
});
this.accessibility.setupAriaAttributes();
this.accessibility.setupBubbleTabNavigation();
}
_initRemoteRangeLoader() {
this._remoteRangeLoader = new RemoteRangeLoader({
getDataSource: () => this.dataSource,
getChatOptions: () => this.options,
getTotalCount: () => this._getEndlessDataCount(),
getLoadedRange: () => this._dataManager.getLoadedRange(),
getRenderedRange: () => ({
startIndex: this._renderStartIndex,
endIndex: this._renderEndIndex
}),
getLoadedMessagesInRange: this._dataManager.getMessagesInRange.bind(this._dataManager),
getLoadedMessageById: this._dataManager.getLoadedMessageById.bind(this._dataManager),
getReferenceMessageById: this._dataManager.getAuxiliaryMessageById.bind(this._dataManager),
cacheResolvedReferenceMessages: this._dataManager.cacheResolvedReferenceMessages.bind(this._dataManager),
applyResponse: this._applyRemoteRangeResponse.bind(this),
updateEndlessState: this._updateEndlessState.bind(this),
completeEndlessLoad: this._completeEndlessLoad.bind(this),
setRemoteLoadingState: this._setRemoteLoadingState.bind(this),
canRequestRange: () => !!(this.view && this.dataSource)
});
}
_initEndlessScrollCoordinator() {
this._endlessScrollCoordinator = new EndlessScrollCoordinator({
beginLatestRemoteStartup: this._beginLatestRemoteStartupRangeRequest.bind(this),
isEndlessScrollEnabled: this._isEndlessScrollEnabled.bind(this),
isRemoteEndlessScroll: this._isRemoteEndlessScroll.bind(this),
getPageSize: this._getEndlessPageSize.bind(this),
getTotalCount: this._getEndlessDataCount.bind(this),
getRenderedRange: () => ({
startIndex: this._renderStartIndex,
endIndex: this._renderEndIndex
}),
getLoadedRange: () => this._dataManager.getLoadedRange(),
getLatestRange: this._getLatestRange.bind(this),
getTargetRange: this._getTargetRange.bind(this),
isLatestRenderedRange: this._isLatestRenderedRange.bind(this),
isRangeLoaded: (startIndex, endIndex) => this._dataManager.isRangeLoaded(startIndex, endIndex),
getMessageIndexById: (id) => this._dataManager.getMessageIndexById(id),
requestRemoteRange: (options) => this._remoteRangeLoader.requestRange(options),
renderRange: (startIndex, endIndex, options = {}) => this._renderEndlessRange(startIndex, endIndex, options),
scrollToBottom: this.scrollToBottom.bind(this),
refreshScrollState: () => this.view.refreshScrollState(),
scrollToMessage: this.scrollToMessage.bind(this),
prependLocalRange: this._prependLocalEndlessRange.bind(this),
appendLocalRange: this._appendLocalEndlessRange.bind(this),
updateEndlessState: this._updateEndlessState.bind(this)
});
}
_beginLatestRemoteStartupRangeRequest() {
this.view._scrollManager._isLoadingOlder = true;
this.view._scrollManager._isLoadingNewer = false;
this.view.clearMessages();
}
_setRemoteLoadingState(direction) {
if (!this.view) return;
if (direction === "older" || direction === "all") this.view._scrollManager._isLoadingOlder = true;
if (direction === "newer" || direction === "all") this.view._scrollManager._isLoadingNewer = true;
this.view.refreshScrollState();
}
_applyRemoteRangeResponse(requestOptions, parsedResponse) {
const nextModels = this._updateRemoteRangeWindow(requestOptions.mode, parsedResponse.range.startIndex, parsedResponse.models, parsedResponse.totalCount);
if (requestOptions.mode === "prepend") this._applyPrependedRemoteRange(parsedResponse.range, nextModels);
else if (requestOptions.mode === "append") this._applyAppendedRemoteRange(parsedResponse.range, nextModels);
else this._applyReplacedRemoteRange(requestOptions, parsedResponse.range);
this._refreshPinnedState(this._dataManager.getPinnedMessage());
}
_updateRemoteRangeWindow(mode, startIndex, models, totalCount) {
const nextModels = models;
const wasAutoSync = this.dataSource.options.autoSync;
this.dataSource.options.autoSync = false;
this.dataSource.unbind(CHANGE, this._refreshHandler);
const data = this.dataSource.data();
data.omitChangeEvent = true;
try {
if (mode === "replace") {
data.splice(0, data.length, ...models);
this._dataManager.setRemoteRangeStart(startIndex);
} else if (mode === "prepend") {
data.splice(0, 0, ...nextModels);
this._dataManager.setRemoteRangeStart(startIndex);
} else data.splice(data.length, 0, ...nextModels);
} finally {
data.omitChangeEvent = false;
}
this.dataSource.bind(CHANGE, this._refreshHandler);
this.dataSource.options.autoSync = wasAutoSync;
this.dataSource._total = totalCount;
this.dataSource._pristineTotal = totalCount;
return nextModels;
}
_applyPrependedRemoteRange(range, nextModels) {
if (this.view && nextModels.length) this.view._scrollManager.preserveScrollPosition(() => {
this.view._messageRenderer.prependMessages(nextModels);
});
this._renderStartIndex = Math.min(range.startIndex, this._renderStartIndex);
this._renderEndIndex = Math.max(this._renderEndIndex, range.endIndex);
this._updateEndlessState();
this._finalizeIncrementalEndlessRender();
}
_applyAppendedRemoteRange(range, nextModels) {
nextModels.forEach((message) => {
this.renderMessage(message);
});
this._renderEndIndex = Math.max(this._renderEndIndex, range.endIndex);
this._updateEndlessState();
this._finalizeIncrementalEndlessRender();
}
_applyReplacedRemoteRange(requestOptions, range) {
const targetMessage = requestOptions.targetMessageId ? this._dataManager.getLoadedMessageById(requestOptions.targetMessageId) : null;
this._renderEndlessRange(range.startIndex, range.endIndex, {
scrollToBottom: requestOptions.scrollToBottom,
scrollToMessageUid: targetMessage?.uid
});
}
_finalizeIncrementalEndlessRender() {
if (!this.view) return;
this.view.renderSuggestedActions();
this.view.updateSeparator();
this.view.refreshScrollState();
if (!this.accessibility) return;
this.accessibility.setupAriaAttributes();
this.accessibility.setupBubbleTabNavigation();
}
_attachEvents() {
const options = this.options;
this._commandHandler = new CommandHandler({
getUserId: this.getUserId.bind(this),
dataItem: this.dataItem.bind(this),
fileDataItem: this.fileDataItem.bind(this),
removeMessage: this.removeMessage.bind(this),
pinMessage: (message) => this._dataManager.pinMessage(message),
setReplyMessage: (message, isOwnMessage) => this.messageBox.setReplyMessage(message, isOwnMessage),
setCurrentMessageReplyId: (id) => {
this.currentMessageReplyId = id;
},
setupAriaAttributes: () => this.accessibility.setupAriaAttributes(),
setContextMenuActions: (isOwnMessage) => {
const settings = isOwnMessage ? options.authorMessageSettings : options.receiverMessageSettings;
this.messageContextMenu.setActions(settings?.messageActions ?? options.messageActions);
},
toggleDeleteVisibility: (isAuthor) => this.messageContextMenu.toggleDeleteVisibility(isAuthor),
isContextMenuAllowed: (isOwnMessage) => {
return (isOwnMessage ? options.authorMessageSettings : options.receiverMessageSettings)?.enableContextMenuActions !== false;
},
trigger: this.trigger.bind(this)
});
this._eventWiring = new EventWiring({
options: this.options,
wrapper: this.wrapper,
view: this.view,
messageBox: this.messageBox,
messageContextMenu: this.messageContextMenu,
value: this.value.bind(this),
postMessage: this.postMessage.bind(this),
clearReplyState: this.clearReplyState.bind(this),
prepareLatestMessages: this._prepareLatestMessages.bind(this),
commandExecute: this._commandHandler.commandExecute.bind(this._commandHandler),
contextMenuExecute: this._commandHandler.contextMenuExecute.bind(this._commandHandler),
messageContextMenuOpen: this._commandHandler.messageContextMenuOpen.bind(this._commandHandler),
dataItem: this.dataItem.bind(this),
scrollToMessage: this.scrollToMessage.bind(this),
navigateToReferencedMessage: this._navigateToReferencedMessage.bind(this),
clearPinnedMessage: this.clearPinnedMessage.bind(this),
getPinnedMessage: () => this._dataManager.getPinnedMessage(),
getMessageById: (id) => this._dataManager.getMessageById(id),
trigger: this.trigger.bind(this),
accessibility: this.accessibility,
messageBoxRef: this.messageBox
});
this._eventWiring.attach();
this.bind(this.events, this.options);
}
getUserId() {
return this.options.authorId;
}
/**
* Gets or sets the input value of the message box.
* @param value - Optional value to set. If provided, sets the input value.
* @returns The current input value when called without arguments.
*/
value(newValue) {
if (newValue === void 0) return this.messageBox?.value() || "";
if (this.messageBox) {
this.messageBox.value(newValue);
this.trigger(EVENTS.input, { value: newValue });
}
}
/**
* Sets new options for the widget.
*/
setOptions(options) {
super.setOptions(options);
this.destroy();
const chat = new Chat(this.element, this.options);
Object.assign(this, chat);
}
/**
* Sets a new data source.
*/
setDataSource(dataSource) {
if (this.dataSource) this._unbindDataSource();
this._remoteRangeLoader.reset();
this.view.clearMessages();
this._renderStartIndex = 0;
this._renderEndIndex = 0;
if (this._isEndlessScrollEnabled()) this.view._scrollManager.resetEndlessState();
this.options.dataSource = dataSource;
this._initDataSource();
this._validateRemoteEndlessStartup();
if (this.options.autoBind) if (this._isRemoteEndlessScroll()) this._requestLatestRemoteStartupRange();
else this.dataSource.fetch();
if (this.view) this.view.refreshDataManager(this._dataManager);
}
/**
* Posts a new message to the chat.
*/
postMessage(message) {
if (this.currentMessageReplyId) {
if (typeof message === "string") message = { text: message };
message.replyToId = this.currentMessageReplyId;
}
return this._dataManager.postMessage(message, this.getUserId());
}
/**
* Removes a message (marks as deleted).
*/
removeMessage(message) {
return this._dataManager.removeMessage(message);
}
/**
* Updates an existing message.
*/
updateMessage(message, newData) {
return this._dataManager.updateMessage(message, newData);
}
/**
* Gets a message by its UID.
*/
getMessageByUid(uid) {
return this._dataManager.getMessageByUid(uid);
}
/**
* Gets the data item for a message element.
*/
dataItem(message) {
return this.view.dataItem(message);
}
/**
* Gets the file data item for a file element.
*/
fileDataItem(message, file) {
return this.view.fileDataItem(message, file);
}
/**
* Clears all messages from the view.
*/
clearMessages() {
this.view.clearMessages();
}
/**
* @internal
* Renders a message in the view.
*/
renderMessage(message) {
this.view.renderMessage(message);
this.accessibility.setupBubbleTabNavigation();
}
/**
* @internal
* Renders a pinned message.
* The pinned message is positioned sticky inside the message list per Chat v3 spec.
*/
renderPinnedMessage(message) {
const isOwnMessage = String(message.authorId) === String(this.getUserId());
const options = this.options;
const pinnedMessageElement = $(options.messageReferenceTemplate({
text: message.text,
files: message.files,
isDeleted: message.isDeleted,
isOwnMessage,
messages: options.messages,
isPinMessage: true,
renderCloseButton: true,
renderFileMenuButton: false
}));
this.wrapper.find(`[${REFERENCES.messageReferencePinWrapper}]`).remove();
const messageList = this.wrapper.find("." + STYLES.viewWrapper);
if (messageList.length) messageList.prepend(pinnedMessageElement);
else if (this._hasHeader) pinnedMessageElement.insertAfter(this.wrapper.find("." + STYLES.header));
else this.wrapper.prepend(pinnedMessageElement);
this.view._initFileMenus(pinnedMessageElement);
}
/**
* Clears the pinned message.
*/
clearPinnedMessage() {
this._dataManager.clearPinnedMessage();
this.wrapper.find("." + STYLES.messagePinned).remove();
}
/**
* Clears the reply state.
*/
clearReplyState() {
this.messageBox.removeReplyMessage();
this.currentMessageReplyId = null;
}
/**
* Scrolls the chat to the bottom.
*/
scrollToBottom() {
this.view.scrollToBottom();
}
/**
* Scrolls to a specific message.
*/
scrollToMessage(uid) {
return this.view.scrollToMessage(uid);
}
_getEndlessDataCount() {
return this._dataManager.getTotalCount();
}
_getLatestRange(totalCount = this._getEndlessDataCount()) {
return this._endlessRangeManager.getLatestRange(this._getEndlessPageSize(), totalCount);
}
_getTargetRange(targetIndex, totalCount = this._getEndlessDataCount()) {
return this._endlessRangeManager.getTargetRange(targetIndex, this._getEndlessPageSize(), totalCount);
}
_isLatestRenderedRange(totalCount = this._getEndlessDataCount()) {
return this._endlessRangeManager.isLatestRenderedRange(this._renderEndIndex, totalCount);
}
_hasRenderedLatestMessage(totalCount = this._getEndlessDataCount()) {
return this._endlessRangeManager.hasRenderedLatestMessage(this._renderEndIndex, totalCount);
}
_isEndlessScrollEnabled(options = this.options) {
return options.scrollMode === "endless";
}
_updateEndlessState() {
const options = this.options;
if (!this._isEndlessScrollEnabled(options) || !this.view) return;
const totalAvailable = this._getEndlessDataCount();
this.view._scrollManager._allOlderLoaded = this._renderStartIndex <= 0;
this.view._scrollManager._allNewerLoaded = this._renderEndIndex >= totalAvailable;
this.view.refreshScrollState();
}
_completeEndlessLoad(direction, callback) {
requestAnimationFrame(() => {
if (!this.view) {
callback?.();
return;
}
if (direction === "older" || direction === "all") this.view._scrollManager._isLoadingOlder = false;
if (direction === "newer" || direction === "all") this.view._scrollManager._isLoadingNewer = false;
this.view.refreshScrollState();
callback?.();
});
}
_renderEndlessRange(startIndex, endIndex, options = {}) {
const messages = this._dataManager.getMessagesInRange(startIndex, endIndex);
this.view.clearMessages();
messages.forEach((message) => {
this.renderMessage(message);
});
this._renderStartIndex = startIndex;
this._renderEndIndex = endIndex;
this._updateEndlessState();
this.view.renderSuggestedActions();
this.view.updateSeparator();
this.accessibility.setupAriaAttributes();
this.accessibility.setupBubbleTabNavigation();
requestAnimationFrame(() => {
if (!this.view) return;
if (options.scrollToBottom) this.scrollToBottom();
else if (options.scrollToMessageUid) this.scrollToMessage(options.scrollToMessageUid);
this.view.refreshScrollState();
});
}
_prepareLatestMessages() {
this._endlessScrollCoordinator.prepareLatestMessages();
}
_navigateToReferencedMessage(id) {
const message = this._dataManager.getMessageById(id);
const loadedMessage = this._dataManager.getLoadedMessageById(id);
this.trigger(EVENTS.referencedMessageClick, { id });
this._endlessScrollCoordinator.navigateToReferencedMessage(id, message, loadedMessage);
}
/**
* Gets or sets the loading state (transforms send button to stop button).
*/
loading(value) {
if (value === void 0) return this._loading;
this._loading = value;
if (this.wrapper) this.wrapper.toggleClass(STYLES.generating, value);
if (this.messageBox) this.messageBox.loading(value);
}
/**
* @deprecated Use `loading()` instead. Will be removed in future version.
*/
toggleSendButtonGenerating(value) {
this.loading(value);
}
/**
* @internal
* Refreshes the chat view when data changes.
*/
refresh(e) {
if (e?.action === "add") this._bumpRemoteEndlessTotalForLocalAdd(e.items);
const state = this._createRefreshState(e);
if (!e?.action) this._refreshFull(state);
this._refreshPinnedState(state.pinnedMessage);
if (e?.action === "sync" || e?.action === "add") this._refreshSync(state);
this._finalizeRefresh();
}
_bumpRemoteEndlessTotalForLocalAdd(items) {
if (!this._isEndlessScrollEnabled() || !this._isRemoteEndlessScroll() || !items?.length) return;
const newTotal = this._dataManager.getLoadedRange().startIndex + this._dataManager.getLoadedCount();
if (newTotal > this.dataSource.total()) {
this.dataSource._total = newTotal;
this.dataSource._pristineTotal = newTotal;
}
}
_createRefreshState(e) {
return {
data: this.dataSource.data ? this.dataSource.data() : this.dataSource.view(),
changedItems: e?.changedItems || [],
pinnedMessage: this._dataManager.getPinnedMessage(),
totalCount: this._getEndlessDataCount(),
addedCount: e?.items?.length ?? 0
};
}
_refreshFull(state) {
this._refreshNoDataTemplate(state.data);
if (this._isEndlessScrollEnabled()) {
this._refreshEndlessFull(state.totalCount);
return;
}
this._refreshStandardFull(state.data);
}
_refreshNoDataTemplate(data) {
const options = this.options;
if (data.length === 0 && options.noDataTemplate) this.view.showNoData(options.noDataTemplate());
else if (options.noDataTemplate) this.view.hideNoData();
}
_refreshEndlessFull(totalCount) {
this.view._scrollManager._isLoadingOlder = true;
this.view._scrollManager._isLoadingNewer = false;
this.view.clearMessages();
if (totalCount <= 0) {
this._resetEmptyEndlessRefresh();
return;
}
if (this._isRemoteEndlessScroll()) {
this._refreshRemoteEndlessFull(totalCount);
return;
}
this._refreshLocalEndlessFull(totalCount);
}
_refreshRemoteEndlessFull(totalCount) {
const latestRange = this._getLatestRange(totalCount);
if (!this._dataManager.isRangeLoaded(latestRange.startIndex, latestRange.endIndex)) {
this._remoteRangeLoader.requestRange({
startIndex: latestRange.startIndex,
endIndex: latestRange.endIndex,
direction: "latest",
reason: "scroll-to-bottom",
mode: "replace",
loadingState: "all",
scrollToBottom: true
});
return;
}
this._renderEndlessRange(latestRange.startIndex, latestRange.endIndex, { scrollToBottom: true });
this._completeEndlessLoad("all");
}
_refreshLocalEndlessFull(totalCount) {
const latestRange = this._getLatestRange(totalCount);
this._renderEndlessRange(latestRange.startIndex, latestRange.endIndex, { scrollToBottom: true });
this._completeEndlessLoad("older");
}
_resetEmptyEndlessRefresh() {
this._renderStartIndex = 0;
this._renderEndIndex = 0;
this._updateEndlessState();
this.scrollToBottom();
this._completeEndlessLoad("older");
}
_refreshStandardFull(data) {
this.view.clearMessages();
data.forEach((item) => {
this.renderMessage(item);
});
this.scrollToBottom();
}
_refreshPinnedState(pinnedMessage) {
if (pinnedMessage) {
this.renderPinnedMessage(pinnedMessage);
return;
}
this.wrapper.find(`[${REFERENCES.messageReferencePinWrapper}]`).remove();
}
_refreshSync(state) {
if (this.options.noDataTemplate && !this.view.hasMessages()) this.view.hideNoData();
const message = state.changedItems.length ? state.changedItems[0] : state.data[state.data.length - 1];
const isOwnMessage = !!(message && String(message.authorId) === String(this.getUserId()));
const wasNearBottom = this.view.isNearBottom();
const isRenderedMessage = !!(message?.uid && this.view.hasMessage(message.uid));
if (this._isEndlessScrollEnabled()) {
this._refreshEndlessSync(message, isOwnMessage, wasNearBottom, isRenderedMessage, state.totalCount, state.addedCount);
return;
}
this._refreshStandardSync(message, isOwnMessage, wasNearBottom);
}
_refreshEndlessSync(message, isOwnMessage, wasNearBottom, isRenderedMessage, totalCount, addedCount) {
if (!this.view.hasMessages() && totalCount > 0) {
this._prepareLatestMessages();
return;
}
if (isOwnMessage && totalCount > 0) {
this._refreshOwnEndlessSync(totalCount);
return;
}
const extendsLatestRange = addedCount > 0 && this._canAppendOwnEndlessSync(totalCount);
if (message && (isRenderedMessage || this._isLatestRenderedRange(totalCount) || extendsLatestRange)) {
this._refreshVisibleEndlessSyncMessage(message, isOwnMessage, wasNearBottom, isRenderedMessage, totalCount);
if (extendsLatestRange) this.scrollToBottom();
return;
}
this._updateEndlessState();
}
_refreshOwnEndlessSync(totalCount) {
if (this._isRemoteEndlessScroll() && this._canAppendOwnEndlessSync(totalCount)) {
this._dataManager.getMessagesInRange(this._renderEndIndex, totalCount).forEach((message) => {
this.renderMessage(message);
});
this._renderEndIndex = totalCount;
this._updateEndlessState();
this.scrollToBottom();
return;
}
const latestRange = this._getLatestRange(totalCount);
if (this._isRemoteEndlessScroll() && !this._dataManager.isRangeLoaded(latestRange.startIndex, latestRange.endIndex)) {
this._prepareLatestMessages();
return;
}
this._renderEndlessRange(latestRange.startIndex, latestRange.endIndex, { scrollToBottom: true });
}
_canAppendOwnEndlessSync(totalCount) {
const loadedRange = this._dataManager.getLoadedRange();
return this._renderEndIndex < totalCount && loadedRange.endIndex === totalCount && this._renderEndIndex >= loadedRange.startIndex;
}
_refreshVisibleEndlessSyncMessage(message, isOwnMessage, wasNearBottom, isRenderedMessage, totalCount) {
this.renderMessage(message);
if (!isRenderedMessage) this._renderEndIndex = totalCount;
this._updateEndlessState();
if (!isOwnMessage && wasNearBottom && message.uid) this.view.scrollIncomingMessage(message.uid);
}
_refreshStandardSync(message, isOwnMessage, wasNearBottom) {
if (!message) return;
this.renderMessage(message);
if (isOwnMessage) this.scrollToBottom();
else if (wasNearBottom && message.uid) this.view.scrollIncomingMessage(message.uid);
}
_finalizeRefresh() {
this.view.renderSuggestedActions();
this.view.updateSeparator();
this.view.refreshScrollState();
this.accessibility.setupAriaAttributes();
this.accessibility.setupBubbleTabNavigation();
}
_isRemoteEndlessScroll() {
const options = this.options;
return !!(this._isEndlessScrollEnabled(options) && this.dataSource.options?.serverPaging);
}
_prependLocalEndlessRange(startIndex, endIndex) {
const messages = this._dataManager.getMessagesInRange(startIndex, endIndex);
if (!messages.length) {
this._updateEndlessState();
return;
}
this.view._scrollManager._isLoadingOlder = true;
this.view._scrollManager.preserveScrollPosition(() => {
this.view._messageRenderer.prependMessages(messages);
});
this._renderStartIndex = startIndex;
this._updateEndlessState();
this._completeEndlessLoad("older");
this.accessibility.setupAriaAttributes();
this.accessibility.setupBubbleTabNavigation();
}
_appendLocalEndlessRange(startIndex, endIndex) {
const messages = this._dataManager.getMessagesInRange(startIndex, endIndex);
if (!messages.length) {
this._updateEndlessState();
return;
}
this.view._scrollManager._isLoadingNewer = true;
messages.forEach((message) => {
this.renderMessage(message);
});
this._renderEndIndex = endIndex;
this._updateEndlessState();
this._completeEndlessLoad("newer");
this.accessibility.setupAriaAttributes();
this.accessibility.setupBubbleTabNavigation();
}
_loadOlderMessages() {
this._endlessScrollCoordinator.loadOlderMessages();
}
_loadNewerMessages() {
this._endlessScrollCoordinator.loadNewerMessages();
}
/**
* @internal
*/
commandExecute(e) {
this._commandHandler.commandExecute(e);
}
/**
* @internal
*/
messageReply(message) {
this._commandHandler.messageReply(message);
}
/**
* @internal
* Copies message text to clipboard.
*/
messageCopy(message) {
this._commandHandler.messageCopy(message);
}
/**
* @internal
* Pins a message.
*/
messagePin(message) {
this._commandHandler.messagePin(message);
}
/**
* @internal
* Deletes a message.
*/
messageDelete(message) {
this._commandHandler.messageDelete(message);
}
/**
* @internal
*/
messageContextMenuOpen(e) {
this._commandHandler.messageContextMenuOpen(e);
}
/**
* Destroys the widget and cleans up resources.
*/
destroy() {
this._remoteRangeLoader.reset();
if (this.dataSource) {
this._unbindDataSource();
this.dataSource = null;
}
if (this.view) {
this.view.unbind();
this.view.destroy();
this.view = null;
}
if (this.messageBox) {
this.messageBox.unbind();
this.messageBox.destroy();
this.messageBox = null;
}
if (this.messageContextMenu) this.messageContextMenu.destroy();
if (this.wrapper) {
this.wrapper.off();
this.wrapper.empty();
this.wrapper = null;
}
super.destroy();
}
};
//#endregion
//#region ../src/kendo.chat.js
/**
* Kendo UI Chat Widget
*
* This file serves as the entry point for the Chat widget.
*/
const __meta__ = {
id: "chat",
name: "Chat",
category: "web",
description: "The Chat component.",
depends: [
"data",
"draganddrop",
"html.button",
"textarea",
"menu",
"avatar",
"toolbar",
"speechtotextbutton"
]
};
window.kendo.chat = {};
$.extend(window.kendo.chat, {
ChatView: {},
Component: {},
Components: {},
Templates: {},
getTemplate: () => require_kendo_core.utilsService.logToConsole("The getTemplate method is deprecated. Use one of the built-in templates or append elements manually.", "warn"),
getComponent: () => require_kendo_core.utilsService.logToConsole("The getComponent method is deprecated. Use one of the built-in templates or append elements manually.", "warn"),
registerTemplate: () => require_kendo_core.utilsService.logToConsole("The registerTemplate method is deprecated. Use one of the built-in templates or append elements manually.", "warn"),
registerComponent: () => require_kendo_core.utilsService.logToConsole("The registerComponent method is deprecated. Use one of the built-in templates or append elements manually.", "warn")
});
require_kendo_core.widgetRegistryService.register(Chat);
var kendo_chat_default = kendo;
//#endregion
Object.defineProperty(exports, "Chat", {
enumerable: true,
get: function() {
return Chat;
}
});
Object.defineProperty(exports, "__meta__", {
enumerable: true,
get: function() {
return __meta__;
}
});
Object.defineProperty(exports, "kendo_chat_default", {
enumerable: true,
get: function() {
return kendo_chat_default;
}
});