UNPKG

@progress/kendo-angular-conversational-ui

Version:

Kendo UI for Angular Conversational UI components

366 lines (359 loc) 16.9 kB
/**----------------------------------------------------------------------------------------- * 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'] }] } });