UNPKG

@progress/kendo-angular-conversational-ui

Version:

Kendo UI for Angular Conversational UI components

1,277 lines (1,246 loc) 148 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import * as i0 from '@angular/core'; import { Directive, Optional, EventEmitter, Component, HostBinding, ViewChild, Input, Output, isDevMode, forwardRef, ViewChildren, ElementRef, ContentChild, InjectionToken, Inject, Injectable, ContentChildren, NgModule } from '@angular/core'; import * as i1$1 from '@progress/kendo-angular-l10n'; import { ComponentMessages, LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { validatePackage } from '@progress/kendo-licensing'; import { NgIf, NgTemplateOutlet, NgFor, NgSwitch, NgSwitchCase } from '@angular/common'; import { Keys, ResizeSensorComponent, isPresent, focusableSelector, guid, ResizeBatchService } from '@progress/kendo-angular-common'; import { paperPlaneIcon, chevronLeftIcon, chevronRightIcon, sparklesIcon, commentIcon, moreHorizontalIcon, thumbUpIcon, thumbDownOutlineIcon, thumbDownIcon, thumbUpOutlineIcon, copyIcon, arrowRotateCwIcon, chevronUpIcon, chevronDownIcon } from '@progress/kendo-svg-icons'; import { ButtonComponent } from '@progress/kendo-angular-buttons'; import { TextBoxComponent, TextAreaComponent, InputSeparatorComponent, TextBoxSuffixTemplateDirective, TextAreaSuffixComponent } from '@progress/kendo-angular-inputs'; import * as i1 from '@progress/kendo-angular-intl'; import { fromEvent, Subscription, Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { IconsService } from '@progress/kendo-angular-icons'; import { PopupService } from '@progress/kendo-angular-popup'; import { DialogContainerService, DialogService, WindowService, WindowContainerService } from '@progress/kendo-angular-dialog'; import { PanelBarComponent } from '@progress/kendo-angular-layout'; /** * Defines a template for displaying message attachments. * * To define an attachment template, nest an `<ng-template>` tag with the `kendoChatAttachmentTemplate` attribute inside the `<kendo-chat>` component. * The template context is set to the attachment instance. * For more information, refer to the article on [message attachments](slug:attachments_chat). * * @example * ```html * <kendo-chat> * <ng-template kendoChatAttachmentTemplate let-attachment> * <div>Attachment: {{ attachment.content }}</div> * </ng-template> * </kendo-chat> * ``` */ class AttachmentTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AttachmentTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: AttachmentTemplateDirective, isStandalone: true, selector: "[kendoChatAttachmentTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AttachmentTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoChatAttachmentTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }]; } }); /** * Describes the arguments for the `sendMessage` event. */ class SendMessageEvent { /** * Represents the message that contains the metadata and user input. * * > The Chat does not automatically add the message to its data. * > For more information, see [Data Binding](slug:databinding_chat). */ message; /** * @hidden */ constructor(message) { this.message = message; } } const noop = () => { }; const handlers = { 'reply': (action, sender) => { sender.sendMessage.emit(new SendMessageEvent({ author: sender.user, text: action.value, timestamp: new Date() })); }, 'call': (action) => { window.open('tel:' + action.value); }, 'openUrl': (action) => { window.open(action.value); } }; /** * @hidden */ const makeHandler = (action) => handlers[action.type] || noop; /** * Defines a template for displaying Chat messages. * * To define a message template, nest an `<ng-template>` tag with the `kendoChatMessageTemplate` directive inside the `<kendo-chat>` component. * The template context is set to the `message` instance. * For more information, refer to the article on [message templates](slug:message_templates_chat). * * @example * ```html * <kendo-chat> * <ng-template kendoChatMessageTemplate let-message> * <div>Message: {{ message.text }}</div> * </ng-template> * </kendo-chat> * ``` */ class MessageTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: MessageTemplateDirective, isStandalone: true, selector: "[kendoChatMessageTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoChatMessageTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }]; } }); /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-conversational-ui', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1751463370, version: '19.2.0', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/' }; /** * Creates a message box area that overrides the default message box of the Chat component. * * To define a message box template, nest an `<ng-template>` tag with the `kendoChatMessageBoxTemplate` directive inside the `<kendo-chat>` tag. * For more information, see [Message Box Template](slug:message_box#toc-message-box-template). * * @example * ```html * <kendo-chat> * <ng-template kendoChatMessageBoxTemplate> * <input type="text" placeholder="Custom message box..." /> * </ng-template> * </kendo-chat> * ``` */ class ChatMessageBoxTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ChatMessageBoxTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ChatMessageBoxTemplateDirective, isStandalone: true, selector: "[kendoChatMessageBoxTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ChatMessageBoxTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoChatMessageBoxTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef }]; } }); /** * @hidden */ class MessageBoxComponent { borderColor = 'inherit'; messageBoxInput; user; autoScroll; type; localization; messageBoxTemplate; sendMessage = new EventEmitter(); /** * @hidden */ sendIcon = paperPlaneIcon; /** * @hidden */ sendClick() { const input = this.messageBoxInput; const value = input.value; if (!value) { return; } const message = { author: this.user, text: value, timestamp: new Date() }; this.sendMessage.emit(new SendMessageEvent(message)); input.value = null; input.focus(); this.autoScroll = true; } /** * @hidden */ inputKeydown(e) { if (e.keyCode === Keys.Enter) { this.sendClick(); } } /** * @hidden */ textAreaKeydown(e) { const isEnter = e.keyCode === Keys.Enter; if (!isEnter) { return; } const newLine = (e.metaKey || e.ctrlKey); const enterOnly = !(e.shiftKey || e.metaKey || e.ctrlKey); if (enterOnly) { e.preventDefault(); this.sendClick(); } if (newLine) { this.messageBoxInput.value += `\r\n`; } } /** * @hidden */ textFor(key) { return this.localization.get(key); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MessageBoxComponent, isStandalone: true, selector: "kendo-message-box", inputs: { user: "user", autoScroll: "autoScroll", type: "type", localization: "localization", messageBoxTemplate: "messageBoxTemplate" }, outputs: { sendMessage: "sendMessage" }, host: { properties: { "style.border-color": "this.borderColor" } }, viewQueries: [{ propertyName: "messageBoxInput", first: true, predicate: ["messageBoxInput"], descendants: true }], ngImport: i0, template: ` <ng-container *ngIf="!messageBoxTemplate"> <kendo-textbox *ngIf="type === 'textbox'" #messageBoxInput class="k-message-box" [inputAttributes]="{ 'aria-label': textFor('messageBoxInputLabel') }" [placeholder]="textFor('messagePlaceholder')" (keydown)="inputKeydown($event)" > <ng-template kendoTextBoxSuffixTemplate> <kendo-textbox-separator></kendo-textbox-separator> <button kendoButton fillMode="flat" class="k-chat-send" icon="paper-plane" [svgIcon]="sendIcon" [tabindex]="0" [attr.title]="textFor('send')" (click)="sendClick()" > </button> </ng-template> </kendo-textbox> <kendo-textarea *ngIf="type === 'textarea'" #messageBoxInput class="k-message-box !k-align-items-end" resizable="none" [rows]="3" [inputAttributes]="{ 'aria-label': textFor('messageBoxInputLabel') }" [placeholder]="textFor('messagePlaceholder')" [showSuffixSeparator]="true" (keydown)="textAreaKeydown($event)" > <kendo-textarea-suffix> <button kendoButton fillMode="flat" class="k-chat-send" icon="paper-plane" [svgIcon]="sendIcon" [tabindex]="0" [attr.title]="textFor('send')" (click)="sendClick()" > </button> </kendo-textarea-suffix> </kendo-textarea> </ng-container> <ng-template *ngIf="messageBoxTemplate" [ngTemplateOutlet]="messageBoxTemplate?.templateRef"></ng-template> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: TextAreaComponent, selector: "kendo-textarea", inputs: ["focusableId", "flow", "inputAttributes", "adornmentsOrientation", "rows", "cols", "maxlength", "tabindex", "tabIndex", "resizable", "size", "rounded", "fillMode", "showPrefixSeparator", "showSuffixSeparator"], outputs: ["focus", "blur", "valueChange"], exportAs: ["kendoTextArea"] }, { kind: "component", type: InputSeparatorComponent, selector: "kendo-input-separator, kendo-textbox-separator", inputs: ["orientation"] }, { kind: "directive", type: TextBoxSuffixTemplateDirective, selector: "[kendoTextBoxSuffixTemplate]", inputs: ["showSeparator"] }, { kind: "component", type: TextAreaSuffixComponent, selector: "kendo-textarea-suffix", inputs: ["flow", "orientation"], exportAs: ["kendoTextAreaSuffix"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageBoxComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-message-box', template: ` <ng-container *ngIf="!messageBoxTemplate"> <kendo-textbox *ngIf="type === 'textbox'" #messageBoxInput class="k-message-box" [inputAttributes]="{ 'aria-label': textFor('messageBoxInputLabel') }" [placeholder]="textFor('messagePlaceholder')" (keydown)="inputKeydown($event)" > <ng-template kendoTextBoxSuffixTemplate> <kendo-textbox-separator></kendo-textbox-separator> <button kendoButton fillMode="flat" class="k-chat-send" icon="paper-plane" [svgIcon]="sendIcon" [tabindex]="0" [attr.title]="textFor('send')" (click)="sendClick()" > </button> </ng-template> </kendo-textbox> <kendo-textarea *ngIf="type === 'textarea'" #messageBoxInput class="k-message-box !k-align-items-end" resizable="none" [rows]="3" [inputAttributes]="{ 'aria-label': textFor('messageBoxInputLabel') }" [placeholder]="textFor('messagePlaceholder')" [showSuffixSeparator]="true" (keydown)="textAreaKeydown($event)" > <kendo-textarea-suffix> <button kendoButton fillMode="flat" class="k-chat-send" icon="paper-plane" [svgIcon]="sendIcon" [tabindex]="0" [attr.title]="textFor('send')" (click)="sendClick()" > </button> </kendo-textarea-suffix> </kendo-textarea> </ng-container> <ng-template *ngIf="messageBoxTemplate" [ngTemplateOutlet]="messageBoxTemplate?.templateRef"></ng-template> `, standalone: true, imports: [NgIf, ButtonComponent, NgTemplateOutlet, TextBoxComponent, TextAreaComponent, InputSeparatorComponent, TextBoxSuffixTemplateDirective, TextAreaSuffixComponent] }] }], propDecorators: { borderColor: [{ type: HostBinding, args: ['style.border-color'] }], messageBoxInput: [{ type: ViewChild, args: ['messageBoxInput', { static: false }] }], user: [{ type: Input }], autoScroll: [{ type: Input }], type: [{ type: Input }], localization: [{ type: Input }], messageBoxTemplate: [{ type: Input }], sendMessage: [{ type: Output }] } }); /** * @hidden */ class PreventableEvent { prevented = false; /** * Prevents the default action for a specified event. * In this way, the source component suppresses * the built-in behavior that follows the event. */ preventDefault() { this.prevented = true; } /** * Returns `true` if the event was prevented * by any of its subscribers. * * @returns `true` if the default action was prevented. * Otherwise, returns `false`. */ isDefaultPrevented() { return this.prevented; } } /** * Represents the arguments for the `executeAction` event. * * The `executeAction` event fires when the user clicks a quick action button. * Call `preventDefault()` to suppress the built-in action handler. */ class ExecuteActionEvent extends PreventableEvent { /** * Represents the action to execute. */ action; /** * Represents the message that contains the action. */ message; /** * @hidden */ constructor(action, message) { super(); this.action = action; this.message = message; } } /** * @hidden */ const closest = (node, predicate) => { while (node && !predicate(node)) { node = node.parentNode; } return node; }; /** * @hidden */ class ChatItem { selected; } /** * @hidden */ const isAuthor = (user, msg) => user && msg.author && user.id === msg.author.id; const last = (arr) => arr[arr.length - 1]; const dateChanged = (curr, prev) => (curr && prev) && (prev.getDate() !== curr.getDate() || prev.getMonth() !== curr.getMonth() || prev.getFullYear() !== curr.getFullYear()); const addDateMarker = (acc, msg) => { const timestamp = msg.timestamp; const lastItem = last(acc); if (!timestamp) { return; } if (!lastItem || dateChanged(timestamp, lastItem.timestamp)) { const dateMarker = { type: 'date-marker', timestamp: timestamp, trackBy: timestamp.getTime() }; acc.push(dateMarker); } }; const groupMessages = (acc, msg, isLastMessage) => { const lastItem = last(acc); let messages; if (isDevMode() && !msg.author) { throw new Error('Author must be set for message: ' + JSON.stringify(msg)); } if (msg.typing && !isLastMessage) { return; } if (lastItem && lastItem.type === 'message-group') { messages = lastItem.messages; } if (messages && isAuthor(msg.author, last(messages))) { messages.push(msg); } else { acc.push({ type: 'message-group', messages: [msg], author: msg.author, timestamp: msg.timestamp, trackBy: msg }); } }; const groupItems = (total) => (acc, msg, index) => { const isLastMessage = index === total - 1; addDateMarker(acc, msg); groupMessages(acc, msg, isLastMessage); if (msg.attachments && msg.attachments.length > 1) { acc.push({ type: 'attachment-group', attachments: msg.attachments, attachmentLayout: msg.attachmentLayout, timestamp: msg.timestamp, trackBy: msg }); } if (msg.suggestedActions && isLastMessage) { acc.push({ type: 'action-group', actions: msg.suggestedActions, timestamp: msg.timestamp, trackBy: msg }); } return acc; }; /** * @hidden */ const chatView = (messages) => messages.reduce(groupItems(messages.length), []); /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-empty-function */ /** * @hidden */ class SuggestedActionsComponent extends ChatItem { actions; tabbable; dispatch = new EventEmitter(); defaultClass = true; items; selectedIndex = 0; keyHandlers = { [Keys.Tab]: (e) => this.changeSelectedIndex(e), [Keys.Enter]: (_, action) => this.actionClick(action), [Keys.Space]: (_, action) => this.actionClick(action), }; isSelected(index) { return this.selected && this.selectedIndex === index; } actionClick(action) { this.dispatch.next(action); } actionKeydown(e, action) { const handler = this.keyHandlers[e.keyCode]; if (handler) { handler(e, action); } } focus() { } changeSelectedIndex(e) { const offset = e.shiftKey ? -1 : 1; const prevIndex = this.selectedIndex; this.selectedIndex = Math.max(0, Math.min(prevIndex + offset, this.items.length - 1)); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SuggestedActionsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: SuggestedActionsComponent, isStandalone: true, selector: "kendo-chat-suggested-actions", inputs: { actions: "actions", tabbable: "tabbable" }, outputs: { dispatch: "dispatch" }, host: { properties: { "class.k-quick-replies": "this.defaultClass" } }, providers: [{ provide: ChatItem, useExisting: forwardRef(() => SuggestedActionsComponent) }], viewQueries: [{ propertyName: "items", predicate: ["item"], descendants: true }], usesInheritance: true, ngImport: i0, template: ` <span #item *ngFor="let action of actions; index as index; first as first; last as last" class="k-quick-reply" role="button" [class.k-selected]="isSelected(index)" [class.k-focus]="isSelected(index)" [class.k-first]="first" [class.k-last]="last" [attr.tabindex]="0" (click)="actionClick(action)" (keydown)="actionKeydown($event, action)" > {{ action.title || action.value }} </span> `, isInline: true, dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SuggestedActionsComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-chat-suggested-actions', providers: [{ provide: ChatItem, useExisting: forwardRef(() => SuggestedActionsComponent) }], template: ` <span #item *ngFor="let action of actions; index as index; first as first; last as last" class="k-quick-reply" role="button" [class.k-selected]="isSelected(index)" [class.k-focus]="isSelected(index)" [class.k-first]="first" [class.k-last]="last" [attr.tabindex]="0" (click)="actionClick(action)" (keydown)="actionKeydown($event, action)" > {{ action.title || action.value }} </span> `, standalone: true, imports: [NgFor] }] }], propDecorators: { actions: [{ type: Input }], tabbable: [{ type: Input }], dispatch: [{ type: Output }], defaultClass: [{ type: HostBinding, args: ['class.k-quick-replies'] }], items: [{ type: ViewChildren, args: ['item'] }] } }); // eslint-disable no-forward-ref /** * @hidden */ class MessageComponent extends ChatItem { element; intl; message; tabbable; template; cssClass = true; selected; get tabIndex() { return this.tabbable ? '0' : '-1'; } constructor(element, intl) { super(); this.element = element; this.intl = intl; } formatTimeStamp(date) { return this.intl.formatDate(date, { datetime: 'short' }); } focus() { this.element.nativeElement.focus(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageComponent, deps: [{ token: i0.ElementRef }, { token: i1.IntlService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MessageComponent, isStandalone: true, selector: "kendo-chat-message", inputs: { message: "message", tabbable: "tabbable", template: "template" }, host: { properties: { "class.k-message": "this.cssClass", "class.k-selected": "this.selected", "class.k-focus": "this.selected", "attr.tabIndex": "this.tabIndex" } }, providers: [{ provide: ChatItem, useExisting: forwardRef(() => MessageComponent) }], usesInheritance: true, ngImport: i0, template: ` <time [attr.aria-hidden]="!selected" class="k-message-time" *ngIf="message.timestamp" > {{ formatTimeStamp(message.timestamp) }} </time> <ng-container *ngIf="!message.typing; else typing"> <div class="k-chat-bubble" *ngIf="template"> <ng-container *ngTemplateOutlet="template.templateRef; context: { $implicit: message };" > </ng-container> </div> <div class="k-chat-bubble" *ngIf="!template && message.text"> {{message.text}} </div> </ng-container> <span class="k-message-status" *ngIf="message.status" > {{ message.status }} </span> <ng-template #typing> <div class="k-chat-bubble"> <div class="k-typing-indicator"> <span></span> <span></span> <span></span> </div> </div> </ng-template> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-chat-message', providers: [{ provide: ChatItem, useExisting: forwardRef(() => MessageComponent) }], template: ` <time [attr.aria-hidden]="!selected" class="k-message-time" *ngIf="message.timestamp" > {{ formatTimeStamp(message.timestamp) }} </time> <ng-container *ngIf="!message.typing; else typing"> <div class="k-chat-bubble" *ngIf="template"> <ng-container *ngTemplateOutlet="template.templateRef; context: { $implicit: message };" > </ng-container> </div> <div class="k-chat-bubble" *ngIf="!template && message.text"> {{message.text}} </div> </ng-container> <span class="k-message-status" *ngIf="message.status" > {{ message.status }} </span> <ng-template #typing> <div class="k-chat-bubble"> <div class="k-typing-indicator"> <span></span> <span></span> <span></span> </div> </div> </ng-template> `, standalone: true, imports: [NgIf, NgTemplateOutlet] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.IntlService }]; }, propDecorators: { message: [{ type: Input }], tabbable: [{ type: Input }], template: [{ type: Input }], cssClass: [{ type: HostBinding, args: ['class.k-message'] }], selected: [{ type: HostBinding, args: ['class.k-selected'] }, { type: HostBinding, args: ['class.k-focus'] }], tabIndex: [{ type: HostBinding, args: ['attr.tabIndex'] }] } }); /** * @hidden */ class AttachmentComponent { set attachment(value) { this._attachment = value; this.context = { $implicit: this.attachment }; } get attachment() { return this._attachment; } template; get image() { return this.contentType.startsWith('image/'); } get unknown() { return !this.image; } context; get contentType() { return this.attachment.contentType || ''; } _attachment; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AttachmentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: AttachmentComponent, isStandalone: true, selector: "kendo-chat-attachment", inputs: { attachment: "attachment", template: "template" }, ngImport: i0, template: ` <ng-container *ngIf="template"> <ng-container *ngTemplateOutlet="template.templateRef; context: context;"> </ng-container> </ng-container> <div *ngIf="!template" class="k-card"> <div class="k-card-body"> <h5 class="k-card-title" *ngIf="attachment.title"> {{ attachment.title }} </h5> <h6 class="k-card-subtitle" *ngIf="attachment.subtitle"> {{ attachment.subtitle }} </h6> <img *ngIf="image" [attr.src]="attachment.content" /> <ng-container *ngIf="unknown"> {{ attachment.content }} </ng-container> </div> </div> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AttachmentComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-chat-attachment', template: ` <ng-container *ngIf="template"> <ng-container *ngTemplateOutlet="template.templateRef; context: context;"> </ng-container> </ng-container> <div *ngIf="!template" class="k-card"> <div class="k-card-body"> <h5 class="k-card-title" *ngIf="attachment.title"> {{ attachment.title }} </h5> <h6 class="k-card-subtitle" *ngIf="attachment.subtitle"> {{ attachment.subtitle }} </h6> <img *ngIf="image" [attr.src]="attachment.content" /> <ng-container *ngIf="unknown"> {{ attachment.content }} </ng-container> </div> </div> `, standalone: true, imports: [NgIf, NgTemplateOutlet] }] }], propDecorators: { attachment: [{ type: Input }], template: [{ type: Input }] } }); /* eslint-disable @typescript-eslint/no-explicit-any */ // eslint-disable no-forward-ref /** * @hidden */ class MessageAttachmentsComponent extends ChatItem { zone; localizationService; /** * @hidden */ chevronLeftIcon = chevronLeftIcon; /** * @hidden */ chevronRightIcon = chevronRightIcon; attachments; layout; tabbable; template; localization; get carousel() { return this.layout !== 'list'; } deck; items; scrollPosition = 0; selectedIndex = 0; scrollSubscription; direction; get showLeftArrow() { return this.carousel && this.direction === 'rtl' ? this.scrollPosition > -1 : this.scrollPosition > 0; } get showRightArrow() { return this.carousel && this.direction === 'rtl' ? this.scrollPosition < 0 : this.scrollPosition < 1; } carouselKeyHandlers = { [Keys.ArrowLeft]: (e) => this.navigateTo(e, this.direction === 'rtl' ? 1 : -1), [Keys.ArrowRight]: (e) => this.navigateTo(e, this.direction === 'rtl' ? -1 : 1) }; listKeyHandlers = { [Keys.ArrowUp]: (e) => this.navigateTo(e, -1), [Keys.ArrowDown]: (e) => this.navigateTo(e, 1) }; constructor(zone, localizationService) { super(); this.zone = zone; this.localizationService = localizationService; this.direction = this.localizationService.rtl ? 'rtl' : 'ltr'; } ngAfterViewInit() { this.zone.runOutsideAngular(() => { const scrollDebounceTime = 100; this.scrollSubscription = fromEvent(this.deck.nativeElement, 'scroll') .pipe(debounceTime(scrollDebounceTime)) .subscribe(() => this.onScroll()); }); } ngOnDestroy() { this.scrollSubscription.unsubscribe(); } isSelected(index) { return this.selectedIndex === index; } itemKeydown(e, attachment) { const keyHandlers = this.layout === 'list' ? this.listKeyHandlers : this.carouselKeyHandlers; const handler = keyHandlers[e.keyCode]; if (handler) { handler(e, attachment); } } itemClick(index) { this.select(index); } focus() { this.select(this.selectedIndex); } scrollTo(dir) { const el = this.deck.nativeElement; const scrollStep = el.scrollWidth / this.items.length; const max = el.scrollWidth - el.offsetWidth; const pos = el.scrollLeft + scrollStep * dir; el.scrollLeft = this.direction === 'rtl' ? Math.min(0, max, pos) : Math.max(0, Math.min(max, pos)); } select(index) { this.selectedIndex = index; const item = this.items.toArray()[index]; if (item) { item.nativeElement.focus(); } } navigateTo(e, offset) { const prevIndex = this.selectedIndex; const nextIndex = Math.max(0, Math.min(prevIndex + offset, this.items.length - 1)); if (nextIndex !== prevIndex) { this.select(nextIndex); e.preventDefault(); } } onScroll() { const el = this.deck.nativeElement; if (el.scrollWidth === 0) { return; } const pos = el.scrollLeft / (el.scrollWidth - el.offsetWidth); if (pos !== this.scrollPosition) { this.zone.run(() => { this.scrollPosition = pos; }); } } /** * @hidden */ textFor(key) { return this.localization.get(key); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageAttachmentsComponent, deps: [{ token: i0.NgZone }, { token: i1$1.LocalizationService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MessageAttachmentsComponent, isStandalone: true, selector: "kendo-chat-message-attachments", inputs: { attachments: "attachments", layout: "layout", tabbable: "tabbable", template: "template", localization: "localization" }, host: { properties: { "class.k-card-deck-scrollwrap": "this.carousel" } }, providers: [{ provide: ChatItem, useExisting: forwardRef(() => MessageAttachmentsComponent) }], viewQueries: [{ propertyName: "deck", first: true, predicate: ["deck"], descendants: true, read: ElementRef, static: true }, { propertyName: "items", predicate: ["item"], descendants: true, read: ElementRef }], usesInheritance: true, ngImport: i0, template: ` <button *ngIf="showLeftArrow" (click)="scrollTo(-1)" kendoButton tabindex="-1" [attr.title]="textFor('messageAttachmentLeftArrow')" [svgIcon]="chevronLeftIcon" icon="chevron-left" > </button> <div #deck [class.k-card-deck]="carousel" [class.k-card-list]="!carousel" > <kendo-chat-attachment #item *ngFor="let att of attachments; index as index; first as first; last as last" [attachment]="att" [template]="template" [class.k-selected]="isSelected(index)" [class.k-focus]="isSelected(index)" [class.k-card-wrap]="true" [class.k-first]="first" [class.k-last]="last" [attr.tabindex]="tabbable && isSelected(index) ? '0' : '-1'" (click)="itemClick(index)" (keydown)="itemKeydown($event, att)" > </kendo-chat-attachment> </div> <button *ngIf="showRightArrow" (click)="scrollTo(1)" kendoButton tabindex="-1" [attr.title]="textFor('messageAttachmentRightArrow')" [svgIcon]="chevronRightIcon" icon="chevron-right" > </button> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: AttachmentComponent, selector: "kendo-chat-attachment", inputs: ["attachment", "template"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageAttachmentsComponent, decorators: [{ type: Component, args: [{ providers: [{ provide: ChatItem, useExisting: forwardRef(() => MessageAttachmentsComponent) }], selector: 'kendo-chat-message-attachments', template: ` <button *ngIf="showLeftArrow" (click)="scrollTo(-1)" kendoButton tabindex="-1" [attr.title]="textFor('messageAttachmentLeftArrow')" [svgIcon]="chevronLeftIcon" icon="chevron-left" > </button> <div #deck [class.k-card-deck]="carousel" [class.k-card-list]="!carousel" > <kendo-chat-attachment #item *ngFor="let att of attachments; index as index; first as first; last as last" [attachment]="att" [template]="template" [class.k-selected]="isSelected(index)" [class.k-focus]="isSelected(index)" [class.k-card-wrap]="true" [class.k-first]="first" [class.k-last]="last" [attr.tabindex]="tabbable && isSelected(index) ? '0' : '-1'" (click)="itemClick(index)" (keydown)="itemKeydown($event, att)" > </kendo-chat-attachment> </div> <button *ngIf="showRightArrow" (click)="scrollTo(1)" kendoButton tabindex="-1" [attr.title]="textFor('messageAttachmentRightArrow')" [svgIcon]="chevronRightIcon" icon="chevron-right" > </button> `, standalone: true, imports: [NgIf, ButtonComponent, NgFor, AttachmentComponent] }] }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i1$1.LocalizationService }]; }, propDecorators: { attachments: [{ type: Input }], layout: [{ type: Input }], tabbable: [{ type: Input }], template: [{ type: Input }], localization: [{ type: Input }], carousel: [{ type: HostBinding, args: ['class.k-card-deck-scrollwrap'] }], deck: [{ type: ViewChild, args: ['deck', { read: ElementRef, static: true }] }], items: [{ type: ViewChildren, args: ['item', { read: ElementRef }] }] } }); /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ /** * @hidden */ class MessageListComponent { element; intl; renderer; set messages(value) { const data = value || []; this.view = chatView(data); this._messages = data; } get messages() { return this._messages; } attachmentTemplate; messageTemplate; localization; user; executeAction = new EventEmitter(); navigate = new EventEmitter(); resize = new EventEmitter(); items; cssClass = true; view = []; _messages; subs = new Subscription(); selectedItem; keyActions = { [Keys.Home]: (_) => this.onHomeOrEndKeyDown('home'), [Keys.End]: (_) => this.onHomeOrEndKeyDown('end'), [Keys.ArrowUp]: (e) => this.navigateTo(e, -1), [Keys.ArrowDown]: (e) => this.navigateTo(e, 1), [Keys.Tab]: (e) => this.onTabKeyDown(e), }; constructor(element, intl, renderer) { this.element = element; this.intl = intl; this.renderer = renderer; } ngOnInit() { const elRef = this.element.nativeElement; this.subs.add(this.renderer.listen(elRef, 'keydown', event => this.onKeydown(event))); this.subs.add(this.renderer.listen(elRef, 'focusout', event => this.onBlur(event))); } ngAfterViewInit() { this.selectedItem = this.items.last; } ngOnDestroy() { this.subs.unsubscribe(); } onResize() { this.resize.emit(); } formatTimeStamp(date) { return this.intl.formatDate(date, { date: 'full' }); } onKeydown(e) { const action = this.keyActions[e.keyCode]; if (action) { action(e); } } onBlur(args) { const next = args.relatedTarget || document.activeElement; const outside = !closest(next, (node) => node === this.element.nativeElement); if (outside) { this.select(null); } } isOwnMessage(msg) { return isAuthor(this.user, msg); } dispatchAction(action, message) { const args = new ExecuteActionEvent(action, message); this.executeAction.emit(args); } trackGroup(_index, item) { return item.trackBy; } select(item) { const prevItem = this.selectedItem; if (prevItem) { prevItem.selected = false; } if (item) { item.selected = true; this.selectedItem = item; } } last(items) { if (!items || items.length === 0) { return; } return items[items.length - 1]; } onHomeOrEndKeyDown(key) { const items = this.items.toArray(); if (key === 'home') { items[0].focus(); } else { items[items.length - 1].focus(); } } onTabKeyDown(event) { const item = this.items.toArray()[this.items.length - 1]; const isLastItemQuickReply = item instanceof SuggestedActionsComponent; const isLastItemMessage = item instanceof MessageComponent; event.target.blur(); if (isLastItemQuickReply || isLastItemMessage) { this.select(item); item.focus(); this.navigate.emit(); } } navigateTo(e, offset) { const items = this.items.toArray(); const prevItem = this.selectedItem; const prevIndex = items.indexOf(prevItem); const nextIndex = Math.max(0, Math.min(prevIndex + offset, this.items.length - 1)); const nextItem = items[nextIndex]; const isNextItemQuickReply = nextItem instanceof SuggestedActionsComponent; if (nextItem !== prevItem) { if (isNextItemQuickReply) { nextItem.items.toArray()[0]?.nativeElement.focus(); } this.select(nextItem); nextItem.focus(); this.navigate.emit(); e.preventDefault(); } } /** * @hidden */ textFor(key) { return this.localization.get(key); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageListComponent, deps: [{ token: i0.ElementRef }, { token: i1.IntlService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MessageListComponent, isStandalone: true, selector: "kendo-chat-message-list", inputs: { messages: "messages", attachmentTemplate: "attachmentTemplate", messageTemplate: "messageTemplate", localization: "localization", user: "user" }, outputs: { executeAction: "executeAction", navigate: "navigate", resize: "resize" }, host: { properties: { "class.k-message-list-content": "this.cssClass" } }, viewQueries: [{ propertyName: "items", predicate: ChatItem, descendants: true }], ngImport: i0, template: ` <ng-container *ngFor="let group of view; last as lastGroup; trackBy: trackGroup"> <ng-container [ngSwitch]="group.type"> <div *ngSwitchCase="'date-marker'" class="k-timestamp" > {{ formatTimeStamp(group.timestamp) }} </div> <div *ngSwitchCase="'message-group'" class="k-message-group" [class.k-alt]="isOwnMessage(group.messages[0])" [class.k-no-avatar]="!group.author.avatarUrl" > <p *ngIf="group.author.name" class="k-author">{{ group.author.name }}</p> <div *ngIf="group.author.avatarUrl" class="k-avatar k-avatar-md k-avatar-solid k-avatar-solid-primary k-rounded-full" > <span class="k-avatar-image"> <img [attr.src]="group.author.avatarUrl" [alt]="textFor('messageAvatarAlt')" /> </span> </div> <ng-container *ngFor="let msg of group.messages; first as firstMessage; last as lastMessage" > <div *ngIf="msg.user?.avatarUrl" class="k-avatar k-avatar-md k-avatar-solid k-avatar-solid-primary k-rounded-full" > <span class="k-avatar-image"> <img [src]="msg.user?.avatarUrl"> </span> </div> <kendo-chat-message #message [message]="msg" [tabbable]="lastGroup && lastMessage" [template]="messageTemplate" (click)="select(message)" (focus)="select(message