@progress/kendo-angular-conversational-ui
Version:
Kendo UI for Angular Conversational UI components
366 lines (359 loc) • 16.9 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component, ElementRef, EventEmitter, HostBinding, Input, Output, QueryList, Renderer2, ViewChildren } from '@angular/core';
import { NgFor, NgSwitch, NgSwitchCase, NgIf } from '@angular/common';
import { ExecuteActionEvent } from './api';
import { Keys, ResizeSensorComponent } from '@progress/kendo-angular-common';
import { IntlService } from '@progress/kendo-angular-intl';
import { LocalizationService } from '@progress/kendo-angular-l10n';
import { closest } from './common/utils';
import { ChatItem } from './chat-item';
import { chatView, isAuthor } from './chat-view';
import { AttachmentTemplateDirective } from './attachment-template.directive';
import { MessageTemplateDirective } from './message-template.directive';
import { Subscription } from 'rxjs';
import { SuggestedActionsComponent } from './suggested-actions.component';
import { MessageComponent } from './message.component';
import { MessageAttachmentsComponent } from './message-attachments.component';
import { AttachmentComponent } from './attachment.component';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-intl";
/**
* @hidden
*/
export 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)"
[class.k-only]="group.messages.length === 1"
[class.k-first]="group.messages.length > 1 && firstMessage"
[class.k-last]="group.messages.length > 1 && lastMessage"
>
</kendo-chat-message>
<kendo-chat-attachment
*ngIf="msg.attachments && msg.attachments.length === 1"
[attachment]="msg.attachments[0]"
[template]="attachmentTemplate"
>
</kendo-chat-attachment>
</ng-container>
</div>
<kendo-chat-message-attachments #attachments
*ngSwitchCase="'attachment-group'"
[attachments]="group.attachments"
[layout]="group.attachmentLayout"
[localization]="localization"
[tabbable]="lastGroup"
[template]="attachmentTemplate"
(click)="select(attachments)"
(focus)="select(attachments)"
>
</kendo-chat-message-attachments>
<kendo-chat-suggested-actions #actions
*ngSwitchCase="'action-group'"
[actions]="group.actions"
[tabbable]="lastGroup"
(dispatch)="dispatchAction($event, last(group.messages))"
(click)="select(actions)"
(focus)="select(actions)"
>
</kendo-chat-suggested-actions>
</ng-container>
</ng-container>
<kendo-resize-sensor (resize)="onResize()">
</kendo-resize-sensor>
`, isInline: true, dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MessageComponent, selector: "kendo-chat-message", inputs: ["message", "tabbable", "template"] }, { kind: "component", type: AttachmentComponent, selector: "kendo-chat-attachment", inputs: ["attachment", "template"] }, { kind: "component", type: MessageAttachmentsComponent, selector: "kendo-chat-message-attachments", inputs: ["attachments", "layout", "tabbable", "template", "localization"] }, { kind: "component", type: SuggestedActionsComponent, selector: "kendo-chat-suggested-actions", inputs: ["actions", "tabbable"], outputs: ["dispatch"] }, { kind: "component", type: ResizeSensorComponent, selector: "kendo-resize-sensor", inputs: ["rateLimit"], outputs: ["resize"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MessageListComponent, decorators: [{
type: Component,
args: [{
selector: 'kendo-chat-message-list',
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)"
[class.k-only]="group.messages.length === 1"
[class.k-first]="group.messages.length > 1 && firstMessage"
[class.k-last]="group.messages.length > 1 && lastMessage"
>
</kendo-chat-message>
<kendo-chat-attachment
*ngIf="msg.attachments && msg.attachments.length === 1"
[attachment]="msg.attachments[0]"
[template]="attachmentTemplate"
>
</kendo-chat-attachment>
</ng-container>
</div>
<kendo-chat-message-attachments #attachments
*ngSwitchCase="'attachment-group'"
[attachments]="group.attachments"
[layout]="group.attachmentLayout"
[localization]="localization"
[tabbable]="lastGroup"
[template]="attachmentTemplate"
(click)="select(attachments)"
(focus)="select(attachments)"
>
</kendo-chat-message-attachments>
<kendo-chat-suggested-actions #actions
*ngSwitchCase="'action-group'"
[actions]="group.actions"
[tabbable]="lastGroup"
(dispatch)="dispatchAction($event, last(group.messages))"
(click)="select(actions)"
(focus)="select(actions)"
>
</kendo-chat-suggested-actions>
</ng-container>
</ng-container>
<kendo-resize-sensor (resize)="onResize()">
</kendo-resize-sensor>
`,
standalone: true,
imports: [NgFor, NgSwitch, NgSwitchCase, NgIf, MessageComponent, AttachmentComponent, MessageAttachmentsComponent, SuggestedActionsComponent, ResizeSensorComponent]
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.IntlService }, { type: i0.Renderer2 }]; }, propDecorators: { messages: [{
type: Input
}], attachmentTemplate: [{
type: Input
}], messageTemplate: [{
type: Input
}], localization: [{
type: Input
}], user: [{
type: Input
}], executeAction: [{
type: Output
}], navigate: [{
type: Output
}], resize: [{
type: Output
}], items: [{
type: ViewChildren,
args: [ChatItem]
}], cssClass: [{
type: HostBinding,
args: ['class.k-message-list-content']
}] } });