@cdevhub/ngx-chat 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, TemplateRef, Directive, input, ChangeDetectionStrategy, Component, output, signal, HostListener, viewChild, computed, DestroyRef, effect, viewChildren, linkedSignal, afterRenderEffect, contentChild, Injector, afterNextRender, PLATFORM_ID } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, TemplateRef, Directive, input, ChangeDetectionStrategy, Component, output, signal, HostListener, viewChild, computed, DestroyRef, effect, viewChildren, linkedSignal, afterRenderEffect, contentChild, Injector, afterNextRender, ElementRef, PLATFORM_ID } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
5
5
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
6
|
import { Subject } from 'rxjs';
|
|
7
7
|
import { NgScrollbar } from 'ngx-scrollbar';
|
|
8
|
-
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
8
|
+
import { LiveAnnouncer, FocusTrapFactory } from '@angular/cdk/a11y';
|
|
9
9
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
10
10
|
import { trigger, transition, style, animate, state, keyframes } from '@angular/animations';
|
|
11
11
|
|
|
@@ -73,6 +73,13 @@ const DEFAULT_MARKDOWN_CONFIG = {
|
|
|
73
73
|
'h4',
|
|
74
74
|
'h5',
|
|
75
75
|
'h6',
|
|
76
|
+
// Table elements (GFM tables)
|
|
77
|
+
'table',
|
|
78
|
+
'thead',
|
|
79
|
+
'tbody',
|
|
80
|
+
'tr',
|
|
81
|
+
'th',
|
|
82
|
+
'td',
|
|
76
83
|
],
|
|
77
84
|
allowedAttributes: {
|
|
78
85
|
a: ['href', 'title'],
|
|
@@ -3344,6 +3351,7 @@ class ChatSenderComponent {
|
|
|
3344
3351
|
name: a.file.name,
|
|
3345
3352
|
mimeType: a.file.type,
|
|
3346
3353
|
size: a.file.size,
|
|
3354
|
+
file: a.file,
|
|
3347
3355
|
}));
|
|
3348
3356
|
// Emit send event
|
|
3349
3357
|
const event = {
|
|
@@ -3589,6 +3597,8 @@ function groupMessages(messages, thresholdMs = DEFAULT_GROUPING_THRESHOLD_MS) {
|
|
|
3589
3597
|
function createGroup(messages) {
|
|
3590
3598
|
const firstMessage = messages[0];
|
|
3591
3599
|
return {
|
|
3600
|
+
// Use first message ID as stable group identifier for tracking
|
|
3601
|
+
id: `group-${firstMessage.id}`,
|
|
3592
3602
|
senderId: firstMessage.sender,
|
|
3593
3603
|
senderName: firstMessage.senderName,
|
|
3594
3604
|
avatar: firstMessage.avatar,
|
|
@@ -3936,6 +3946,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3936
3946
|
* }
|
|
3937
3947
|
* ```
|
|
3938
3948
|
*/
|
|
3949
|
+
/**
|
|
3950
|
+
* @Injectable without `providedIn` - must be provided at component level.
|
|
3951
|
+
*
|
|
3952
|
+
* This service maintains stateful data (scroll position, item heights, etc.)
|
|
3953
|
+
* that must be scoped to each ChatMessagesComponent instance. If provided
|
|
3954
|
+
* at root level, multiple chat instances would share state, causing bugs.
|
|
3955
|
+
*
|
|
3956
|
+
* @see ChatMessagesComponent which provides this service
|
|
3957
|
+
*/
|
|
3939
3958
|
class ChatVirtualScrollService {
|
|
3940
3959
|
configService = inject(ChatConfigService);
|
|
3941
3960
|
config = this.configService.getConfig().virtualScroll;
|
|
@@ -4219,11 +4238,10 @@ class ChatVirtualScrollService {
|
|
|
4219
4238
|
return low;
|
|
4220
4239
|
}
|
|
4221
4240
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChatVirtualScrollService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4222
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChatVirtualScrollService
|
|
4241
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChatVirtualScrollService });
|
|
4223
4242
|
}
|
|
4224
4243
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChatVirtualScrollService, decorators: [{
|
|
4225
|
-
type: Injectable
|
|
4226
|
-
args: [{ providedIn: 'root' }]
|
|
4244
|
+
type: Injectable
|
|
4227
4245
|
}] });
|
|
4228
4246
|
|
|
4229
4247
|
/**
|
|
@@ -7962,10 +7980,8 @@ class ChatMessagesComponent {
|
|
|
7962
7980
|
loadMoreDebounced = signal(false, ...(ngDevMode ? [{ debugName: "loadMoreDebounced" }] : []));
|
|
7963
7981
|
/** Timestamp formatter cache */
|
|
7964
7982
|
timestampFormatter = null;
|
|
7965
|
-
/** ResizeObserver for measuring message heights in virtual scroll mode */
|
|
7983
|
+
/** Native ResizeObserver for measuring message heights in virtual scroll mode */
|
|
7966
7984
|
resizeObserver = null;
|
|
7967
|
-
/** Map of message IDs to their indices for height measurement */
|
|
7968
|
-
messageIndexMap = new Map();
|
|
7969
7985
|
// ===========================================================================
|
|
7970
7986
|
// Computed Values
|
|
7971
7987
|
// ===========================================================================
|
|
@@ -8051,21 +8067,28 @@ class ChatMessagesComponent {
|
|
|
8051
8067
|
visibleStartIndex = computed(() => {
|
|
8052
8068
|
return this.virtualScrollService.visibleRange().startIndex;
|
|
8053
8069
|
}, ...(ngDevMode ? [{ debugName: "visibleStartIndex" }] : []));
|
|
8070
|
+
/**
|
|
8071
|
+
* Computed map of message IDs to their indices.
|
|
8072
|
+
* Used for height measurement in virtual scroll mode.
|
|
8073
|
+
* Uses computed() instead of effect() to avoid anti-pattern.
|
|
8074
|
+
*/
|
|
8075
|
+
messageIndexMap = computed(() => {
|
|
8076
|
+
const msgs = this.messages();
|
|
8077
|
+
const map = new Map();
|
|
8078
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
8079
|
+
map.set(msgs[i].id, i);
|
|
8080
|
+
}
|
|
8081
|
+
return map;
|
|
8082
|
+
}, ...(ngDevMode ? [{ debugName: "messageIndexMap" }] : []));
|
|
8083
|
+
/**
|
|
8084
|
+
* Tracks the previous message count to detect changes.
|
|
8085
|
+
* Used to sync virtual scroll service only when needed.
|
|
8086
|
+
*/
|
|
8087
|
+
lastSyncedMessageCount = 0;
|
|
8054
8088
|
// ===========================================================================
|
|
8055
8089
|
// Constructor & Lifecycle
|
|
8056
8090
|
// ===========================================================================
|
|
8057
8091
|
constructor() {
|
|
8058
|
-
// Effect to sync message count with virtual scroll service
|
|
8059
|
-
effect(() => {
|
|
8060
|
-
const count = this.messages().length;
|
|
8061
|
-
this.virtualScrollService.setItemCount(count);
|
|
8062
|
-
// Build message ID to index map for height measurement
|
|
8063
|
-
this.messageIndexMap.clear();
|
|
8064
|
-
const msgs = this.messages();
|
|
8065
|
-
for (let i = 0; i < msgs.length; i++) {
|
|
8066
|
-
this.messageIndexMap.set(msgs[i].id, i);
|
|
8067
|
-
}
|
|
8068
|
-
});
|
|
8069
8092
|
// Auto-scroll effect using afterRenderEffect with earlyRead/write phases
|
|
8070
8093
|
// Phase order: earlyRead → write → mixedReadWrite → read
|
|
8071
8094
|
afterRenderEffect({
|
|
@@ -8083,10 +8106,14 @@ class ChatMessagesComponent {
|
|
|
8083
8106
|
const lastProcessedId = this.lastProcessedMessageId();
|
|
8084
8107
|
const hasNewMessage = currentLastMessage &&
|
|
8085
8108
|
currentLastMessage.id !== lastProcessedId;
|
|
8086
|
-
|
|
8087
|
-
const
|
|
8088
|
-
|
|
8089
|
-
|
|
8109
|
+
const isFromSelf = currentLastMessage?.sender === 'self';
|
|
8110
|
+
const autoScrollEnabled = this.effectiveConfig().behavior.autoScroll;
|
|
8111
|
+
// Determine if we should scroll:
|
|
8112
|
+
// - Always scroll for self messages (user sent it)
|
|
8113
|
+
// - For other messages, only scroll if user is near bottom
|
|
8114
|
+
const shouldScroll = hasNewMessage &&
|
|
8115
|
+
autoScrollEnabled &&
|
|
8116
|
+
(isFromSelf || this.isNearBottom());
|
|
8090
8117
|
return {
|
|
8091
8118
|
shouldScroll,
|
|
8092
8119
|
newMessageId: currentLastMessage?.id ?? null,
|
|
@@ -8096,6 +8123,13 @@ class ChatMessagesComponent {
|
|
|
8096
8123
|
},
|
|
8097
8124
|
write: (earlyReadResultSignal) => {
|
|
8098
8125
|
const readResult = earlyReadResultSignal();
|
|
8126
|
+
// Sync message count with virtual scroll service (replaces effect() anti-pattern)
|
|
8127
|
+
// Only update when count changes to avoid unnecessary work
|
|
8128
|
+
const currentCount = this.messages().length;
|
|
8129
|
+
if (currentCount !== this.lastSyncedMessageCount) {
|
|
8130
|
+
this.virtualScrollService.setItemCount(currentCount);
|
|
8131
|
+
this.lastSyncedMessageCount = currentCount;
|
|
8132
|
+
}
|
|
8099
8133
|
if (!readResult || !readResult.newMessageId)
|
|
8100
8134
|
return;
|
|
8101
8135
|
const { shouldScroll, newMessageId, isFromOther, message } = readResult;
|
|
@@ -8114,11 +8148,10 @@ class ChatMessagesComponent {
|
|
|
8114
8148
|
},
|
|
8115
8149
|
});
|
|
8116
8150
|
// Height measurement effect for virtual scrolling
|
|
8117
|
-
// Uses
|
|
8151
|
+
// Uses native ResizeObserver for measuring message heights
|
|
8118
8152
|
afterRenderEffect(() => {
|
|
8119
8153
|
// Only measure heights when virtual scrolling is active
|
|
8120
8154
|
if (!this.shouldUseVirtualScroll()) {
|
|
8121
|
-
// Disconnect observer when not in virtual mode
|
|
8122
8155
|
if (this.resizeObserver) {
|
|
8123
8156
|
this.resizeObserver.disconnect();
|
|
8124
8157
|
}
|
|
@@ -8130,7 +8163,7 @@ class ChatMessagesComponent {
|
|
|
8130
8163
|
}
|
|
8131
8164
|
const viewport = scrollbarInstance.viewport.nativeElement;
|
|
8132
8165
|
const virtualItems = viewport.querySelectorAll('.ngx-chat-messages__virtual-item');
|
|
8133
|
-
// Create ResizeObserver
|
|
8166
|
+
// Create ResizeObserver lazily
|
|
8134
8167
|
if (!this.resizeObserver) {
|
|
8135
8168
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
8136
8169
|
for (const entry of entries) {
|
|
@@ -8188,14 +8221,18 @@ class ChatMessagesComponent {
|
|
|
8188
8221
|
// ===========================================================================
|
|
8189
8222
|
/**
|
|
8190
8223
|
* Programmatically scrolls to the bottom of the message list.
|
|
8224
|
+
* Uses requestAnimationFrame to ensure DOM is fully updated before scrolling.
|
|
8191
8225
|
*/
|
|
8192
8226
|
scrollToBottom() {
|
|
8193
8227
|
const scrollbarInstance = this.scrollbar();
|
|
8194
8228
|
if (!scrollbarInstance)
|
|
8195
8229
|
return;
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8230
|
+
// Use requestAnimationFrame to ensure DOM has updated before scrolling
|
|
8231
|
+
requestAnimationFrame(() => {
|
|
8232
|
+
scrollbarInstance.scrollTo({
|
|
8233
|
+
bottom: 0,
|
|
8234
|
+
duration: 200,
|
|
8235
|
+
});
|
|
8199
8236
|
});
|
|
8200
8237
|
}
|
|
8201
8238
|
/**
|
|
@@ -8252,7 +8289,7 @@ class ChatMessagesComponent {
|
|
|
8252
8289
|
* Reports measured heights to virtual scroll service.
|
|
8253
8290
|
*/
|
|
8254
8291
|
onMessageHeightChange(messageId, height) {
|
|
8255
|
-
const index = this.messageIndexMap.get(messageId);
|
|
8292
|
+
const index = this.messageIndexMap().get(messageId);
|
|
8256
8293
|
if (index !== undefined && height > 0) {
|
|
8257
8294
|
this.virtualScrollService.setItemHeightAt(index, height);
|
|
8258
8295
|
}
|
|
@@ -8275,10 +8312,9 @@ class ChatMessagesComponent {
|
|
|
8275
8312
|
this.resizeObserver.disconnect();
|
|
8276
8313
|
this.resizeObserver = null;
|
|
8277
8314
|
}
|
|
8278
|
-
this.messageIndexMap.clear();
|
|
8279
8315
|
}
|
|
8280
8316
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChatMessagesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8281
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ChatMessagesComponent, isStandalone: true, selector: "ngx-chat-messages", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, isTyping: { classPropertyName: "isTyping", publicName: "isTyping", isSignal: true, isRequired: false, transformFunction: null }, typingLabel: { classPropertyName: "typingLabel", publicName: "typingLabel", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null }, hasMore: { classPropertyName: "hasMore", publicName: "hasMore", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadMore: "loadMore" }, host: { attributes: { "role": "log", "aria-live": "polite" }, properties: { "attr.aria-busy": "loading() || loadingMore()" }, classAttribute: "ngx-chat-messages" }, viewQueries: [{ propertyName: "scrollbar", first: true, predicate: ["scrollbar"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
8317
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ChatMessagesComponent, isStandalone: true, selector: "ngx-chat-messages", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, isTyping: { classPropertyName: "isTyping", publicName: "isTyping", isSignal: true, isRequired: false, transformFunction: null }, typingLabel: { classPropertyName: "typingLabel", publicName: "typingLabel", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null }, hasMore: { classPropertyName: "hasMore", publicName: "hasMore", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadMore: "loadMore" }, host: { attributes: { "role": "log", "aria-live": "polite" }, properties: { "attr.aria-busy": "loading() || loadingMore()" }, classAttribute: "ngx-chat-messages" }, providers: [ChatVirtualScrollService], viewQueries: [{ propertyName: "scrollbar", first: true, predicate: ["scrollbar"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
8282
8318
|
<ng-scrollbar
|
|
8283
8319
|
#scrollbar
|
|
8284
8320
|
class="ngx-chat-messages__scrollbar"
|
|
@@ -8358,7 +8394,7 @@ class ChatMessagesComponent {
|
|
|
8358
8394
|
}
|
|
8359
8395
|
|
|
8360
8396
|
<!-- Message groups -->
|
|
8361
|
-
@for (group of messageGroups(); track group.
|
|
8397
|
+
@for (group of messageGroups(); track group.id) {
|
|
8362
8398
|
<div
|
|
8363
8399
|
class="ngx-chat-messages__group"
|
|
8364
8400
|
[class.ngx-chat-messages__group--self]="group.senderId === 'self'"
|
|
@@ -8393,7 +8429,7 @@ class ChatMessagesComponent {
|
|
|
8393
8429
|
}
|
|
8394
8430
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChatMessagesComponent, decorators: [{
|
|
8395
8431
|
type: Component,
|
|
8396
|
-
args: [{ selector: 'ngx-chat-messages', standalone: true, imports: [NgScrollbar, ChatTypingIndicatorComponent, ChatMessageBubbleComponent], template: `
|
|
8432
|
+
args: [{ selector: 'ngx-chat-messages', standalone: true, imports: [NgScrollbar, ChatTypingIndicatorComponent, ChatMessageBubbleComponent], providers: [ChatVirtualScrollService], template: `
|
|
8397
8433
|
<ng-scrollbar
|
|
8398
8434
|
#scrollbar
|
|
8399
8435
|
class="ngx-chat-messages__scrollbar"
|
|
@@ -8473,7 +8509,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
8473
8509
|
}
|
|
8474
8510
|
|
|
8475
8511
|
<!-- Message groups -->
|
|
8476
|
-
@for (group of messageGroups(); track group.
|
|
8512
|
+
@for (group of messageGroups(); track group.id) {
|
|
8477
8513
|
<div
|
|
8478
8514
|
class="ngx-chat-messages__group"
|
|
8479
8515
|
[class.ngx-chat-messages__group--self]="group.senderId === 'self'"
|
|
@@ -10707,13 +10743,148 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
10707
10743
|
*/
|
|
10708
10744
|
// Main chat component
|
|
10709
10745
|
|
|
10746
|
+
/**
|
|
10747
|
+
* @fileoverview Focus trap directive for modal and action components.
|
|
10748
|
+
* Traps keyboard focus within a container for accessibility compliance.
|
|
10749
|
+
* @module ngx-chat/directives
|
|
10750
|
+
*/
|
|
10751
|
+
/**
|
|
10752
|
+
* Directive that traps focus within its host element.
|
|
10753
|
+
*
|
|
10754
|
+
* This directive is used for accessibility compliance in modal dialogs,
|
|
10755
|
+
* action panels, and other overlay content where keyboard focus should
|
|
10756
|
+
* be constrained to the interactive elements within.
|
|
10757
|
+
*
|
|
10758
|
+
* Features:
|
|
10759
|
+
* - Traps Tab/Shift+Tab navigation within the element
|
|
10760
|
+
* - Auto-focuses the first focusable element when enabled
|
|
10761
|
+
* - Restores focus to the previously focused element on destroy
|
|
10762
|
+
* - Supports deferred focus for async content
|
|
10763
|
+
*
|
|
10764
|
+
* @example Basic usage
|
|
10765
|
+
* ```html
|
|
10766
|
+
* <div ngxChatFocusTrap>
|
|
10767
|
+
* <button>First</button>
|
|
10768
|
+
* <button>Second</button>
|
|
10769
|
+
* <button>Third</button>
|
|
10770
|
+
* </div>
|
|
10771
|
+
* ```
|
|
10772
|
+
*
|
|
10773
|
+
* @example Conditional focus trap
|
|
10774
|
+
* ```html
|
|
10775
|
+
* <div [ngxChatFocusTrap]="isOpen()">
|
|
10776
|
+
* <button>Close</button>
|
|
10777
|
+
* <div>Modal content</div>
|
|
10778
|
+
* </div>
|
|
10779
|
+
* ```
|
|
10780
|
+
*
|
|
10781
|
+
* @example With auto-focus disabled
|
|
10782
|
+
* ```html
|
|
10783
|
+
* <div ngxChatFocusTrap [ngxChatFocusTrapAutoFocus]="false">
|
|
10784
|
+
* <button>First</button>
|
|
10785
|
+
* </div>
|
|
10786
|
+
* ```
|
|
10787
|
+
*/
|
|
10788
|
+
class ChatFocusTrapDirective {
|
|
10789
|
+
elementRef = inject((ElementRef));
|
|
10790
|
+
focusTrapFactory = inject(FocusTrapFactory);
|
|
10791
|
+
injector = inject(Injector);
|
|
10792
|
+
/**
|
|
10793
|
+
* Whether the focus trap is enabled.
|
|
10794
|
+
* When false, focus can move freely in and out of the element.
|
|
10795
|
+
*/
|
|
10796
|
+
ngxChatFocusTrap = input(true, ...(ngDevMode ? [{ debugName: "ngxChatFocusTrap" }] : []));
|
|
10797
|
+
/**
|
|
10798
|
+
* Whether to auto-focus the first tabbable element when the trap is created.
|
|
10799
|
+
*/
|
|
10800
|
+
ngxChatFocusTrapAutoFocus = input(true, ...(ngDevMode ? [{ debugName: "ngxChatFocusTrapAutoFocus" }] : []));
|
|
10801
|
+
/**
|
|
10802
|
+
* The underlying CDK FocusTrap instance.
|
|
10803
|
+
*/
|
|
10804
|
+
focusTrap = null;
|
|
10805
|
+
/**
|
|
10806
|
+
* Element that had focus before the trap was activated.
|
|
10807
|
+
* Used to restore focus on destroy.
|
|
10808
|
+
*/
|
|
10809
|
+
previouslyFocusedElement = null;
|
|
10810
|
+
constructor() {
|
|
10811
|
+
afterNextRender(() => {
|
|
10812
|
+
this.initializeFocusTrap();
|
|
10813
|
+
}, { injector: this.injector });
|
|
10814
|
+
}
|
|
10815
|
+
/**
|
|
10816
|
+
* Initializes the focus trap after render.
|
|
10817
|
+
*/
|
|
10818
|
+
initializeFocusTrap() {
|
|
10819
|
+
if (!this.ngxChatFocusTrap()) {
|
|
10820
|
+
return;
|
|
10821
|
+
}
|
|
10822
|
+
// Store the currently focused element
|
|
10823
|
+
this.previouslyFocusedElement = document.activeElement;
|
|
10824
|
+
// Create the focus trap
|
|
10825
|
+
this.focusTrap = this.focusTrapFactory.create(this.elementRef.nativeElement);
|
|
10826
|
+
// Enable the trap
|
|
10827
|
+
this.focusTrap.enabled = true;
|
|
10828
|
+
// Auto-focus the first tabbable element
|
|
10829
|
+
if (this.ngxChatFocusTrapAutoFocus()) {
|
|
10830
|
+
this.focusTrap.focusInitialElementWhenReady();
|
|
10831
|
+
}
|
|
10832
|
+
}
|
|
10833
|
+
/**
|
|
10834
|
+
* Manually focuses the first tabbable element within the trap.
|
|
10835
|
+
*/
|
|
10836
|
+
focusFirstTabbableElement() {
|
|
10837
|
+
this.focusTrap?.focusFirstTabbableElement();
|
|
10838
|
+
}
|
|
10839
|
+
/**
|
|
10840
|
+
* Manually focuses the last tabbable element within the trap.
|
|
10841
|
+
*/
|
|
10842
|
+
focusLastTabbableElement() {
|
|
10843
|
+
this.focusTrap?.focusLastTabbableElement();
|
|
10844
|
+
}
|
|
10845
|
+
/**
|
|
10846
|
+
* Checks if an element is focusable within the trap.
|
|
10847
|
+
*/
|
|
10848
|
+
hasFocusableElements() {
|
|
10849
|
+
const element = this.elementRef.nativeElement;
|
|
10850
|
+
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
10851
|
+
return element.querySelectorAll(focusableSelector).length > 0;
|
|
10852
|
+
}
|
|
10853
|
+
/**
|
|
10854
|
+
* Cleanup on destroy.
|
|
10855
|
+
* Disables the trap and restores focus to the previous element.
|
|
10856
|
+
*/
|
|
10857
|
+
ngOnDestroy() {
|
|
10858
|
+
if (this.focusTrap) {
|
|
10859
|
+
this.focusTrap.destroy();
|
|
10860
|
+
this.focusTrap = null;
|
|
10861
|
+
}
|
|
10862
|
+
// Restore focus to the previously focused element
|
|
10863
|
+
if (this.previouslyFocusedElement &&
|
|
10864
|
+
typeof this.previouslyFocusedElement.focus === 'function') {
|
|
10865
|
+
// Use setTimeout to ensure focus is restored after any animations
|
|
10866
|
+
setTimeout(() => {
|
|
10867
|
+
this.previouslyFocusedElement?.focus();
|
|
10868
|
+
}, 0);
|
|
10869
|
+
}
|
|
10870
|
+
}
|
|
10871
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChatFocusTrapDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10872
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.6", type: ChatFocusTrapDirective, isStandalone: true, selector: "[ngxChatFocusTrap]", inputs: { ngxChatFocusTrap: { classPropertyName: "ngxChatFocusTrap", publicName: "ngxChatFocusTrap", isSignal: true, isRequired: false, transformFunction: null }, ngxChatFocusTrapAutoFocus: { classPropertyName: "ngxChatFocusTrapAutoFocus", publicName: "ngxChatFocusTrapAutoFocus", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["ngxChatFocusTrap"], ngImport: i0 });
|
|
10873
|
+
}
|
|
10874
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ChatFocusTrapDirective, decorators: [{
|
|
10875
|
+
type: Directive,
|
|
10876
|
+
args: [{
|
|
10877
|
+
selector: '[ngxChatFocusTrap]',
|
|
10878
|
+
standalone: true,
|
|
10879
|
+
exportAs: 'ngxChatFocusTrap',
|
|
10880
|
+
}]
|
|
10881
|
+
}], ctorParameters: () => [], propDecorators: { ngxChatFocusTrap: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngxChatFocusTrap", required: false }] }], ngxChatFocusTrapAutoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngxChatFocusTrapAutoFocus", required: false }] }] } });
|
|
10882
|
+
|
|
10710
10883
|
/**
|
|
10711
10884
|
* @fileoverview Directives barrel export.
|
|
10712
10885
|
* @module ngx-chat/directives
|
|
10713
10886
|
*/
|
|
10714
10887
|
// Header content projection directive
|
|
10715
|
-
// Additional directives will be added as they are created:
|
|
10716
|
-
// export { ChatFocusTrapDirective } from './chat-focus-trap.directive';
|
|
10717
10888
|
|
|
10718
10889
|
/**
|
|
10719
10890
|
* @fileoverview Error constants and factory functions for ngx-chat.
|
|
@@ -11598,6 +11769,386 @@ function getActionById(message, actionId) {
|
|
|
11598
11769
|
return message.actions.find((action) => action.id === actionId);
|
|
11599
11770
|
}
|
|
11600
11771
|
|
|
11772
|
+
/**
|
|
11773
|
+
* @fileoverview Attachment utility functions for ngx-chat library.
|
|
11774
|
+
* Pure functions for creating, validating, and transforming attachments.
|
|
11775
|
+
* @module ngx-chat/utils
|
|
11776
|
+
*/
|
|
11777
|
+
// ============================================================================
|
|
11778
|
+
// Constants
|
|
11779
|
+
// ============================================================================
|
|
11780
|
+
/**
|
|
11781
|
+
* MIME type to attachment type mapping.
|
|
11782
|
+
*/
|
|
11783
|
+
const MIME_TYPE_MAP = {
|
|
11784
|
+
// Images
|
|
11785
|
+
'image/jpeg': 'image',
|
|
11786
|
+
'image/jpg': 'image',
|
|
11787
|
+
'image/png': 'image',
|
|
11788
|
+
'image/gif': 'image',
|
|
11789
|
+
'image/webp': 'image',
|
|
11790
|
+
'image/svg+xml': 'image',
|
|
11791
|
+
'image/bmp': 'image',
|
|
11792
|
+
'image/tiff': 'image',
|
|
11793
|
+
'image/avif': 'image',
|
|
11794
|
+
'image/heic': 'image',
|
|
11795
|
+
'image/heif': 'image',
|
|
11796
|
+
// Videos
|
|
11797
|
+
'video/mp4': 'video',
|
|
11798
|
+
'video/webm': 'video',
|
|
11799
|
+
'video/ogg': 'video',
|
|
11800
|
+
'video/quicktime': 'video',
|
|
11801
|
+
'video/x-msvideo': 'video',
|
|
11802
|
+
'video/x-matroska': 'video',
|
|
11803
|
+
// Audio
|
|
11804
|
+
'audio/mpeg': 'audio',
|
|
11805
|
+
'audio/mp3': 'audio',
|
|
11806
|
+
'audio/wav': 'audio',
|
|
11807
|
+
'audio/ogg': 'audio',
|
|
11808
|
+
'audio/webm': 'audio',
|
|
11809
|
+
'audio/aac': 'audio',
|
|
11810
|
+
'audio/flac': 'audio',
|
|
11811
|
+
'audio/x-m4a': 'audio',
|
|
11812
|
+
};
|
|
11813
|
+
/**
|
|
11814
|
+
* File extensions that support preview generation.
|
|
11815
|
+
*/
|
|
11816
|
+
const PREVIEWABLE_EXTENSIONS = new Set([
|
|
11817
|
+
'jpg',
|
|
11818
|
+
'jpeg',
|
|
11819
|
+
'png',
|
|
11820
|
+
'gif',
|
|
11821
|
+
'webp',
|
|
11822
|
+
'bmp',
|
|
11823
|
+
'svg',
|
|
11824
|
+
'mp4',
|
|
11825
|
+
'webm',
|
|
11826
|
+
'ogg',
|
|
11827
|
+
]);
|
|
11828
|
+
// ============================================================================
|
|
11829
|
+
// Type Detection Functions
|
|
11830
|
+
// ============================================================================
|
|
11831
|
+
/**
|
|
11832
|
+
* Determines the attachment type from a MIME type string.
|
|
11833
|
+
*
|
|
11834
|
+
* @param mimeType - The MIME type to check (e.g., 'image/jpeg')
|
|
11835
|
+
* @returns The attachment type, defaults to 'file' for unknown types
|
|
11836
|
+
*
|
|
11837
|
+
* @example
|
|
11838
|
+
* ```typescript
|
|
11839
|
+
* getAttachmentType('image/jpeg'); // 'image'
|
|
11840
|
+
* getAttachmentType('video/mp4'); // 'video'
|
|
11841
|
+
* getAttachmentType('application/pdf'); // 'file'
|
|
11842
|
+
* ```
|
|
11843
|
+
*/
|
|
11844
|
+
function getAttachmentType(mimeType) {
|
|
11845
|
+
// Normalize the MIME type
|
|
11846
|
+
const normalized = mimeType.toLowerCase().trim();
|
|
11847
|
+
// Check exact match first
|
|
11848
|
+
if (MIME_TYPE_MAP[normalized]) {
|
|
11849
|
+
return MIME_TYPE_MAP[normalized];
|
|
11850
|
+
}
|
|
11851
|
+
// Check by prefix for generic types
|
|
11852
|
+
if (normalized.startsWith('image/')) {
|
|
11853
|
+
return 'image';
|
|
11854
|
+
}
|
|
11855
|
+
if (normalized.startsWith('video/')) {
|
|
11856
|
+
return 'video';
|
|
11857
|
+
}
|
|
11858
|
+
if (normalized.startsWith('audio/')) {
|
|
11859
|
+
return 'audio';
|
|
11860
|
+
}
|
|
11861
|
+
return 'file';
|
|
11862
|
+
}
|
|
11863
|
+
/**
|
|
11864
|
+
* Determines the attachment type from a File object.
|
|
11865
|
+
*
|
|
11866
|
+
* @param file - The File object to check
|
|
11867
|
+
* @returns The attachment type
|
|
11868
|
+
*
|
|
11869
|
+
* @example
|
|
11870
|
+
* ```typescript
|
|
11871
|
+
* const file = new File([''], 'photo.jpg', { type: 'image/jpeg' });
|
|
11872
|
+
* getAttachmentTypeFromFile(file); // 'image'
|
|
11873
|
+
* ```
|
|
11874
|
+
*/
|
|
11875
|
+
function getAttachmentTypeFromFile(file) {
|
|
11876
|
+
return getAttachmentType(file.type);
|
|
11877
|
+
}
|
|
11878
|
+
/**
|
|
11879
|
+
* Checks if an attachment type supports preview generation.
|
|
11880
|
+
*
|
|
11881
|
+
* @param type - The attachment type to check
|
|
11882
|
+
* @returns True if previews can be generated
|
|
11883
|
+
*/
|
|
11884
|
+
function isPreviewable(type) {
|
|
11885
|
+
return type === 'image' || type === 'video';
|
|
11886
|
+
}
|
|
11887
|
+
/**
|
|
11888
|
+
* Checks if a file extension supports preview generation.
|
|
11889
|
+
*
|
|
11890
|
+
* @param filename - The filename to check
|
|
11891
|
+
* @returns True if the file extension supports previews
|
|
11892
|
+
*/
|
|
11893
|
+
function isPreviewableByExtension(filename) {
|
|
11894
|
+
const ext = getFileExtension(filename);
|
|
11895
|
+
return PREVIEWABLE_EXTENSIONS.has(ext);
|
|
11896
|
+
}
|
|
11897
|
+
// ============================================================================
|
|
11898
|
+
// Factory Functions
|
|
11899
|
+
// ============================================================================
|
|
11900
|
+
/**
|
|
11901
|
+
* Creates a PendingAttachment from a File object.
|
|
11902
|
+
*
|
|
11903
|
+
* @param file - The File object to create an attachment from
|
|
11904
|
+
* @param options - Optional configuration
|
|
11905
|
+
* @returns A PendingAttachment ready for upload
|
|
11906
|
+
*
|
|
11907
|
+
* @example
|
|
11908
|
+
* ```typescript
|
|
11909
|
+
* const file = new File(['content'], 'document.pdf', { type: 'application/pdf' });
|
|
11910
|
+
* const pending = createPendingAttachment(file);
|
|
11911
|
+
* // { id: 'att-123', file, type: 'file', status: 'pending', progress: 0 }
|
|
11912
|
+
* ```
|
|
11913
|
+
*/
|
|
11914
|
+
function createPendingAttachment(file, options = {}) {
|
|
11915
|
+
const type = getAttachmentTypeFromFile(file);
|
|
11916
|
+
const id = options.id ?? generateId('att');
|
|
11917
|
+
const attachment = {
|
|
11918
|
+
id,
|
|
11919
|
+
file,
|
|
11920
|
+
type,
|
|
11921
|
+
status: 'pending',
|
|
11922
|
+
progress: 0,
|
|
11923
|
+
};
|
|
11924
|
+
// Create preview URL for images and videos
|
|
11925
|
+
if (options.createPreview !== false && isPreviewable(type)) {
|
|
11926
|
+
return {
|
|
11927
|
+
...attachment,
|
|
11928
|
+
previewUrl: URL.createObjectURL(file),
|
|
11929
|
+
};
|
|
11930
|
+
}
|
|
11931
|
+
return attachment;
|
|
11932
|
+
}
|
|
11933
|
+
/**
|
|
11934
|
+
* Creates a MessageAttachment from upload result data.
|
|
11935
|
+
*
|
|
11936
|
+
* @param data - The attachment data from the server
|
|
11937
|
+
* @returns A MessageAttachment object
|
|
11938
|
+
*/
|
|
11939
|
+
function createMessageAttachment(data) {
|
|
11940
|
+
return {
|
|
11941
|
+
id: data.id,
|
|
11942
|
+
type: getAttachmentType(data.mimeType),
|
|
11943
|
+
url: data.url,
|
|
11944
|
+
name: data.name,
|
|
11945
|
+
size: data.size,
|
|
11946
|
+
mimeType: data.mimeType,
|
|
11947
|
+
thumbnail: data.thumbnail,
|
|
11948
|
+
dimensions: data.dimensions
|
|
11949
|
+
? { width: data.dimensions.width, height: data.dimensions.height }
|
|
11950
|
+
: undefined,
|
|
11951
|
+
duration: data.duration,
|
|
11952
|
+
};
|
|
11953
|
+
}
|
|
11954
|
+
// ============================================================================
|
|
11955
|
+
// Validation Functions
|
|
11956
|
+
// ============================================================================
|
|
11957
|
+
/**
|
|
11958
|
+
* Validates a file against size and type constraints.
|
|
11959
|
+
*
|
|
11960
|
+
* @param file - The file to validate
|
|
11961
|
+
* @param options - Validation options
|
|
11962
|
+
* @returns Validation result with error message if invalid
|
|
11963
|
+
*
|
|
11964
|
+
* @example
|
|
11965
|
+
* ```typescript
|
|
11966
|
+
* const result = validateAttachment(file, {
|
|
11967
|
+
* maxSize: 5 * 1024 * 1024, // 5MB
|
|
11968
|
+
* allowedTypes: ['image/*', 'application/pdf']
|
|
11969
|
+
* });
|
|
11970
|
+
* if (!result.valid) {
|
|
11971
|
+
* console.error(result.error);
|
|
11972
|
+
* }
|
|
11973
|
+
* ```
|
|
11974
|
+
*/
|
|
11975
|
+
function validateAttachment(file, options = {}) {
|
|
11976
|
+
// Check file size
|
|
11977
|
+
if (options.maxSize && file.size > options.maxSize) {
|
|
11978
|
+
return {
|
|
11979
|
+
valid: false,
|
|
11980
|
+
error: `File exceeds maximum size of ${formatFileSize(options.maxSize)}`,
|
|
11981
|
+
};
|
|
11982
|
+
}
|
|
11983
|
+
// Check MIME type
|
|
11984
|
+
if (options.allowedTypes && options.allowedTypes.length > 0) {
|
|
11985
|
+
const isAllowed = options.allowedTypes.some((pattern) => {
|
|
11986
|
+
if (pattern.endsWith('/*')) {
|
|
11987
|
+
const prefix = pattern.slice(0, -1);
|
|
11988
|
+
return file.type.startsWith(prefix);
|
|
11989
|
+
}
|
|
11990
|
+
return file.type === pattern;
|
|
11991
|
+
});
|
|
11992
|
+
if (!isAllowed) {
|
|
11993
|
+
return {
|
|
11994
|
+
valid: false,
|
|
11995
|
+
error: `File type ${file.type} is not allowed`,
|
|
11996
|
+
};
|
|
11997
|
+
}
|
|
11998
|
+
}
|
|
11999
|
+
// Check extension
|
|
12000
|
+
if (options.allowedExtensions && options.allowedExtensions.length > 0) {
|
|
12001
|
+
const ext = getFileExtension(file.name);
|
|
12002
|
+
if (!options.allowedExtensions.includes(ext)) {
|
|
12003
|
+
return {
|
|
12004
|
+
valid: false,
|
|
12005
|
+
error: `File extension .${ext} is not allowed`,
|
|
12006
|
+
};
|
|
12007
|
+
}
|
|
12008
|
+
}
|
|
12009
|
+
return { valid: true };
|
|
12010
|
+
}
|
|
12011
|
+
// ============================================================================
|
|
12012
|
+
// Formatting Functions
|
|
12013
|
+
// ============================================================================
|
|
12014
|
+
/**
|
|
12015
|
+
* Formats a file size in bytes to a human-readable string.
|
|
12016
|
+
*
|
|
12017
|
+
* @param bytes - The size in bytes
|
|
12018
|
+
* @param decimals - Number of decimal places (default: 1)
|
|
12019
|
+
* @returns Formatted string like "1.5 MB"
|
|
12020
|
+
*
|
|
12021
|
+
* @example
|
|
12022
|
+
* ```typescript
|
|
12023
|
+
* formatFileSize(1024); // "1 KB"
|
|
12024
|
+
* formatFileSize(1536); // "1.5 KB"
|
|
12025
|
+
* formatFileSize(1048576); // "1 MB"
|
|
12026
|
+
* formatFileSize(1073741824); // "1 GB"
|
|
12027
|
+
* ```
|
|
12028
|
+
*/
|
|
12029
|
+
function formatFileSize(bytes, decimals = 1) {
|
|
12030
|
+
if (bytes === 0)
|
|
12031
|
+
return '0 B';
|
|
12032
|
+
if (bytes < 0)
|
|
12033
|
+
return '0 B';
|
|
12034
|
+
const k = 1024;
|
|
12035
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
12036
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
12037
|
+
const index = Math.min(i, sizes.length - 1);
|
|
12038
|
+
const value = bytes / Math.pow(k, index);
|
|
12039
|
+
// Use integer for small values
|
|
12040
|
+
if (value < 10 && index > 0) {
|
|
12041
|
+
return `${value.toFixed(decimals)} ${sizes[index]}`;
|
|
12042
|
+
}
|
|
12043
|
+
return `${Math.round(value)} ${sizes[index]}`;
|
|
12044
|
+
}
|
|
12045
|
+
/**
|
|
12046
|
+
* Formats a duration in seconds to a human-readable string.
|
|
12047
|
+
*
|
|
12048
|
+
* @param seconds - The duration in seconds
|
|
12049
|
+
* @returns Formatted string like "1:23" or "1:23:45"
|
|
12050
|
+
*
|
|
12051
|
+
* @example
|
|
12052
|
+
* ```typescript
|
|
12053
|
+
* formatDuration(65); // "1:05"
|
|
12054
|
+
* formatDuration(3665); // "1:01:05"
|
|
12055
|
+
* ```
|
|
12056
|
+
*/
|
|
12057
|
+
function formatDuration(seconds) {
|
|
12058
|
+
if (seconds < 0)
|
|
12059
|
+
return '0:00';
|
|
12060
|
+
const hours = Math.floor(seconds / 3600);
|
|
12061
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
12062
|
+
const secs = Math.floor(seconds % 60);
|
|
12063
|
+
if (hours > 0) {
|
|
12064
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
12065
|
+
}
|
|
12066
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
12067
|
+
}
|
|
12068
|
+
// ============================================================================
|
|
12069
|
+
// Helper Functions
|
|
12070
|
+
// ============================================================================
|
|
12071
|
+
/**
|
|
12072
|
+
* Extracts the file extension from a filename.
|
|
12073
|
+
*
|
|
12074
|
+
* @param filename - The filename to extract from
|
|
12075
|
+
* @returns Lowercase extension without the dot, or empty string
|
|
12076
|
+
*
|
|
12077
|
+
* @example
|
|
12078
|
+
* ```typescript
|
|
12079
|
+
* getFileExtension('photo.JPG'); // 'jpg'
|
|
12080
|
+
* getFileExtension('document.pdf'); // 'pdf'
|
|
12081
|
+
* getFileExtension('noextension'); // ''
|
|
12082
|
+
* ```
|
|
12083
|
+
*/
|
|
12084
|
+
function getFileExtension(filename) {
|
|
12085
|
+
const lastDot = filename.lastIndexOf('.');
|
|
12086
|
+
if (lastDot === -1 || lastDot === filename.length - 1) {
|
|
12087
|
+
return '';
|
|
12088
|
+
}
|
|
12089
|
+
return filename.slice(lastDot + 1).toLowerCase();
|
|
12090
|
+
}
|
|
12091
|
+
/**
|
|
12092
|
+
* Gets a display-friendly file name, truncating if too long.
|
|
12093
|
+
*
|
|
12094
|
+
* @param filename - The full filename
|
|
12095
|
+
* @param maxLength - Maximum length (default: 30)
|
|
12096
|
+
* @returns Truncated filename with extension preserved
|
|
12097
|
+
*
|
|
12098
|
+
* @example
|
|
12099
|
+
* ```typescript
|
|
12100
|
+
* truncateFileName('very-long-file-name.pdf', 15); // 'very-lo...e.pdf'
|
|
12101
|
+
* ```
|
|
12102
|
+
*/
|
|
12103
|
+
function truncateFileName(filename, maxLength = 30) {
|
|
12104
|
+
if (filename.length <= maxLength) {
|
|
12105
|
+
return filename;
|
|
12106
|
+
}
|
|
12107
|
+
const ext = getFileExtension(filename);
|
|
12108
|
+
const extWithDot = ext ? `.${ext}` : '';
|
|
12109
|
+
const nameWithoutExt = filename.slice(0, filename.length - extWithDot.length);
|
|
12110
|
+
const availableLength = maxLength - extWithDot.length - 3; // 3 for '...'
|
|
12111
|
+
if (availableLength <= 0) {
|
|
12112
|
+
return filename.slice(0, maxLength - 3) + '...';
|
|
12113
|
+
}
|
|
12114
|
+
const keepStart = Math.ceil(availableLength / 2);
|
|
12115
|
+
const keepEnd = Math.floor(availableLength / 2);
|
|
12116
|
+
return (nameWithoutExt.slice(0, keepStart) +
|
|
12117
|
+
'...' +
|
|
12118
|
+
nameWithoutExt.slice(-keepEnd) +
|
|
12119
|
+
extWithDot);
|
|
12120
|
+
}
|
|
12121
|
+
/**
|
|
12122
|
+
* Revokes a blob URL to free memory.
|
|
12123
|
+
* Safe to call with undefined/null.
|
|
12124
|
+
*
|
|
12125
|
+
* @param url - The blob URL to revoke
|
|
12126
|
+
*/
|
|
12127
|
+
function revokePreviewUrl(url) {
|
|
12128
|
+
if (url && url.startsWith('blob:')) {
|
|
12129
|
+
URL.revokeObjectURL(url);
|
|
12130
|
+
}
|
|
12131
|
+
}
|
|
12132
|
+
/**
|
|
12133
|
+
* Gets the appropriate icon name for an attachment type.
|
|
12134
|
+
*
|
|
12135
|
+
* @param type - The attachment type
|
|
12136
|
+
* @returns Icon identifier
|
|
12137
|
+
*/
|
|
12138
|
+
function getAttachmentIcon(type) {
|
|
12139
|
+
switch (type) {
|
|
12140
|
+
case 'image':
|
|
12141
|
+
return 'image';
|
|
12142
|
+
case 'video':
|
|
12143
|
+
return 'videocam';
|
|
12144
|
+
case 'audio':
|
|
12145
|
+
return 'audiotrack';
|
|
12146
|
+
case 'file':
|
|
12147
|
+
default:
|
|
12148
|
+
return 'insert_drive_file';
|
|
12149
|
+
}
|
|
12150
|
+
}
|
|
12151
|
+
|
|
11601
12152
|
/**
|
|
11602
12153
|
* Utility Functions
|
|
11603
12154
|
*
|
|
@@ -12074,5 +12625,5 @@ const chatAnimations = {
|
|
|
12074
12625
|
* Generated bundle index. Do not edit.
|
|
12075
12626
|
*/
|
|
12076
12627
|
|
|
12077
|
-
export { AttachmentPickerComponent, AttachmentPreviewComponent, AudioPreviewComponent, ButtonsActionComponent, CHAT_CONFIG, CHAT_CONFIG_OVERRIDES, CHAT_FEATURES, CHAT_I18N, CHAT_I18N_OVERRIDES, ChatA11yService, ChatAttachmentComponent, ChatAttachmentService, ChatComponent, ChatConfigService, ChatDropZoneDirective, ChatErrorBoundaryComponent, ChatErrorRecoveryService, ChatHeaderComponent, ChatHeaderContentDirective, ChatMarkdownComponent, ChatMarkdownService, ChatMessageActionsComponent, ChatMessageBubbleComponent, ChatMessagesComponent, ChatSenderComponent, ChatTypingIndicatorComponent, ChatVirtualScrollService, ConfirmActionComponent, DEFAULT_ATTACHMENT_CONFIG, DEFAULT_BEHAVIOR_CONFIG, DEFAULT_CHAT_CONFIG, DEFAULT_CHAT_I18N, DEFAULT_ERROR_RECOVERY_CONFIG, DEFAULT_GROUPING_THRESHOLD_MS, DEFAULT_KEYBOARD_CONFIG, DEFAULT_MARKDOWN_CONFIG, DEFAULT_VALIDATION_CONFIG, DEFAULT_VIRTUAL_SCROLL_CONFIG, ERROR_CODES, FilePreviewComponent, ImagePreviewComponent, MultiSelectActionComponent, SelectActionComponent, VideoPreviewComponent, allActionsResponded, appendMessageContent, breakLongWords, canRetry, chatAnimations, createButtonsAction, createConfirmAction, createError, createMultiSelectAction, createOtherMessage, createSelectAction, createSelfMessage, createSystemMessage, disableAllActions, errorShake, fadeInOut, findMessage, generateId, getActionById, getErrorMessages, getLastMessage, getMessagesByStatus, getPendingActions, getRetryDelay, getRetryableMessages, groupMessages, incrementRetryCount, isAuthError, isClientError, isNetworkError, isSafeUrl, isUngroupable, messageEnter, provideChat, removeMessage, resetIdCounter, sanitizeContent, shouldGroupMessages, stripInvisibleChars, typingPulse, updateActionResponse, updateMessageContent, updateMessageStatus, validateMessage };
|
|
12628
|
+
export { AttachmentPickerComponent, AttachmentPreviewComponent, AudioPreviewComponent, ButtonsActionComponent, CHAT_CONFIG, CHAT_CONFIG_OVERRIDES, CHAT_FEATURES, CHAT_I18N, CHAT_I18N_OVERRIDES, ChatA11yService, ChatAttachmentComponent, ChatAttachmentService, ChatComponent, ChatConfigService, ChatDropZoneDirective, ChatErrorBoundaryComponent, ChatErrorRecoveryService, ChatFocusTrapDirective, ChatHeaderComponent, ChatHeaderContentDirective, ChatMarkdownComponent, ChatMarkdownService, ChatMessageActionsComponent, ChatMessageBubbleComponent, ChatMessagesComponent, ChatSenderComponent, ChatTypingIndicatorComponent, ChatVirtualScrollService, ConfirmActionComponent, DEFAULT_ATTACHMENT_CONFIG, DEFAULT_BEHAVIOR_CONFIG, DEFAULT_CHAT_CONFIG, DEFAULT_CHAT_I18N, DEFAULT_ERROR_RECOVERY_CONFIG, DEFAULT_GROUPING_THRESHOLD_MS, DEFAULT_KEYBOARD_CONFIG, DEFAULT_MARKDOWN_CONFIG, DEFAULT_VALIDATION_CONFIG, DEFAULT_VIRTUAL_SCROLL_CONFIG, ERROR_CODES, FilePreviewComponent, ImagePreviewComponent, MultiSelectActionComponent, SelectActionComponent, VideoPreviewComponent, allActionsResponded, appendMessageContent, breakLongWords, canRetry, chatAnimations, createButtonsAction, createConfirmAction, createError, createMessageAttachment, createMultiSelectAction, createOtherMessage, createPendingAttachment, createSelectAction, createSelfMessage, createSystemMessage, disableAllActions, errorShake, fadeInOut, findMessage, formatDuration, formatFileSize, generateId, getActionById, getAttachmentIcon, getAttachmentType, getAttachmentTypeFromFile, getErrorMessages, getFileExtension, getLastMessage, getMessagesByStatus, getPendingActions, getRetryDelay, getRetryableMessages, groupMessages, incrementRetryCount, isAuthError, isClientError, isNetworkError, isPreviewable, isPreviewableByExtension, isSafeUrl, isUngroupable, messageEnter, provideChat, removeMessage, resetIdCounter, revokePreviewUrl, sanitizeContent, shouldGroupMessages, stripInvisibleChars, truncateFileName, typingPulse, updateActionResponse, updateMessageContent, updateMessageStatus, validateAttachment, validateMessage };
|
|
12078
12629
|
//# sourceMappingURL=cdevhub-ngx-chat.mjs.map
|