@chat21/chat21-web-widget 5.1.18 → 5.1.20
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.
- package/CHANGELOG.md +12 -1
- package/package.json +1 -1
- package/src/app/app.component.scss +1 -4
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +66 -6
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +4 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +2 -1
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +4 -5
- package/src/app/component/message/html/html.component.html +5 -1
- package/src/app/component/message/html/html.component.scss +9 -0
- package/src/app/component/message/text/text.component.scss +4 -0
- package/src/app/pipe/marked.pipe.ts +31 -4
- package/src/app/sass/normalize.scss +1 -0
- package/src/iframe-style.css +31 -7
- package/.vscode/settings.json +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,12 +6,23 @@
|
|
|
6
6
|
### **Copyrigth**:
|
|
7
7
|
*Tiledesk SRL*
|
|
8
8
|
|
|
9
|
+
# 5.1.20
|
|
10
|
+
- **changed**: marked pipe do not render /n
|
|
11
|
+
|
|
12
|
+
# 5.1.19
|
|
13
|
+
- **bug fixed**: show bottom scroll button and unread message badge only when I'm not at the bottom of the page
|
|
14
|
+
- **changed**: allow HTML code to be inserted into messages, but do not parse the code. Ensure coexistence with Markdown.
|
|
15
|
+
- **bug fixed**: after sending a multi-line message, the text area remains open on multiple lines.
|
|
16
|
+
- **bug fixed**: fixed widget animation when opened
|
|
17
|
+
- **bug-fixed**: line-height in markdown
|
|
18
|
+
- **bug-fixed**: when i move to top mode and close the widget, the balloon moves to the right
|
|
19
|
+
- **changed**: saved the widget's size state to local storage. The parameter flow is (default → storage → settings → URL)
|
|
20
|
+
|
|
9
21
|
# 5.1.18
|
|
10
22
|
- **added**: Implemented Shadow DOM in the text component to isolate HTML and Markdown rendering in a safe and protected context
|
|
11
23
|
- **changed**: Adapted text component styles to support Shadow DOM (removed ::ng-deep, added styles for common markdown elements)
|
|
12
24
|
- **security**: HTML/Markdown content is now rendered in an isolated Shadow DOM, improving security and preventing interference with the rest of the application
|
|
13
25
|
|
|
14
|
-
|
|
15
26
|
# 5.1.17
|
|
16
27
|
- **bug-fixed**: set the maximum width on a message with iframe
|
|
17
28
|
|
package/package.json
CHANGED
|
@@ -488,14 +488,11 @@ chat-root {
|
|
|
488
488
|
right: 0px;
|
|
489
489
|
top: 0px;
|
|
490
490
|
bottom: 0px;
|
|
491
|
-
// border-radius: 16px;
|
|
492
491
|
overflow: hidden;
|
|
493
492
|
background-color: transparent;
|
|
494
|
-
// border: 1px solid #cccccc; //NEW GAB
|
|
495
493
|
margin: 0px;
|
|
496
494
|
padding: 0px;
|
|
497
|
-
|
|
498
|
-
|
|
495
|
+
opacity: 0;
|
|
499
496
|
&.full-screen-mode {
|
|
500
497
|
width: 100%;
|
|
501
498
|
height: 100%;
|
|
@@ -298,6 +298,19 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
298
298
|
if (this.afConversationComponent) {
|
|
299
299
|
this.afConversationComponent.nativeElement.focus();
|
|
300
300
|
}
|
|
301
|
+
// Sync initial "scroll to bottom" button/badge visibility.
|
|
302
|
+
// The state is normally driven by real scroll events, but on first render
|
|
303
|
+
// we might not get any scroll event -> stale UI.
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
try {
|
|
306
|
+
const isAtBottom = this.conversationContent?.checkContentScrollPosition();
|
|
307
|
+
if (typeof isAtBottom === 'boolean') {
|
|
308
|
+
this.onScrollContent(isAtBottom);
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {
|
|
311
|
+
this.logger.error('[CONV-COMP] initial scroll state sync error:', e);
|
|
312
|
+
}
|
|
313
|
+
}, 0);
|
|
301
314
|
this.isButtonsDisabled = false;
|
|
302
315
|
}, 300);
|
|
303
316
|
}
|
|
@@ -1074,29 +1087,76 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
1074
1087
|
// this.hideTextAreaContent = true
|
|
1075
1088
|
}
|
|
1076
1089
|
/** CALLED BY: conv-header component */
|
|
1077
|
-
onWidgetSizeChange(mode:
|
|
1078
|
-
|
|
1079
|
-
|
|
1090
|
+
onWidgetSizeChange(mode: any){
|
|
1091
|
+
const normalize = (val: any): 'min' | 'max' | 'top' => {
|
|
1092
|
+
const v = (typeof val === 'string') ? val.toLowerCase().trim() : '';
|
|
1093
|
+
return (v === 'min' || v === 'max' || v === 'top') ? (v as any) : 'min';
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
const normalizedMode = normalize(mode);
|
|
1097
|
+
const tiledeskDiv = this.g.windowContext?.window?.document?.getElementById('tiledeskdiv');
|
|
1098
|
+
if(!tiledeskDiv){
|
|
1099
|
+
this.g.size = normalizedMode;
|
|
1100
|
+
this.isMenuShow = false;
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
this.g.size = normalizedMode;
|
|
1080
1105
|
const parent = tiledeskDiv.parentElement as HTMLElement | null;
|
|
1081
|
-
if(
|
|
1106
|
+
if(normalizedMode==='max'){
|
|
1107
|
+
this.restoreInlinePositionStylesForPopup(tiledeskDiv);
|
|
1082
1108
|
tiledeskDiv.classList.add('max-size')
|
|
1083
1109
|
tiledeskDiv.classList.remove('min-size')
|
|
1084
1110
|
tiledeskDiv.classList.remove('top-size')
|
|
1085
1111
|
if(parent) parent.classList.remove('overlay--popup');
|
|
1086
|
-
} else if(
|
|
1112
|
+
} else if(normalizedMode==='min'){
|
|
1113
|
+
this.restoreInlinePositionStylesForPopup(tiledeskDiv);
|
|
1087
1114
|
tiledeskDiv.classList.add('min-size')
|
|
1088
1115
|
tiledeskDiv.classList.remove('max-size')
|
|
1089
1116
|
tiledeskDiv.classList.remove('top-size')
|
|
1090
1117
|
if(parent) parent.classList.remove('overlay--popup');
|
|
1091
|
-
} else if(
|
|
1118
|
+
} else if(normalizedMode=== 'top'){
|
|
1119
|
+
// Remove inline positioning so CSS can control centering without needing `!important`.
|
|
1120
|
+
// this.clearInlinePositionStylesForPopup(tiledeskDiv);
|
|
1092
1121
|
tiledeskDiv.classList.add('top-size')
|
|
1093
1122
|
tiledeskDiv.classList.remove('max-size')
|
|
1094
1123
|
tiledeskDiv.classList.remove('min-size')
|
|
1095
1124
|
if(parent) parent.classList.add('overlay--popup');
|
|
1096
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
// Persist user-driven size changes so, when `size` is not specified via URL/settings,
|
|
1128
|
+
// GlobalSettingsService can restore it from storage (it already loads `size` from storage).
|
|
1129
|
+
try{
|
|
1130
|
+
this.appStorageService.setItem('size', normalizedMode);
|
|
1131
|
+
}catch(e){
|
|
1132
|
+
this.logger.warn('[CONV-COMP] onWidgetSizeChange > cannot persist size', e);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1097
1135
|
this.isMenuShow = false;
|
|
1098
1136
|
}
|
|
1099
1137
|
|
|
1138
|
+
// private clearInlinePositionStylesForPopup(tiledeskDiv: HTMLElement) {
|
|
1139
|
+
// tiledeskDiv.style.removeProperty('left');
|
|
1140
|
+
// tiledeskDiv.style.removeProperty('right');
|
|
1141
|
+
// tiledeskDiv.style.removeProperty('top');
|
|
1142
|
+
// tiledeskDiv.style.removeProperty('bottom');
|
|
1143
|
+
// }
|
|
1144
|
+
|
|
1145
|
+
private restoreInlinePositionStylesForPopup(tiledeskDiv: HTMLElement) {
|
|
1146
|
+
const marginX = this.g.isMobile ? this.g.mobileMarginX : this.g.marginX;
|
|
1147
|
+
const marginY = this.g.isMobile ? this.g.mobileMarginY : this.g.marginY;
|
|
1148
|
+
|
|
1149
|
+
if (this.g.align === 'left') {
|
|
1150
|
+
tiledeskDiv.style.left = marginX;
|
|
1151
|
+
tiledeskDiv.style.removeProperty('right');
|
|
1152
|
+
} else {
|
|
1153
|
+
tiledeskDiv.style.right = marginX;
|
|
1154
|
+
tiledeskDiv.style.removeProperty('left');
|
|
1155
|
+
}
|
|
1156
|
+
tiledeskDiv.style.bottom = marginY;
|
|
1157
|
+
tiledeskDiv.style.removeProperty('top');
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1100
1160
|
|
|
1101
1161
|
/** CALLED BY: conv-header component */
|
|
1102
1162
|
onSignOutFN(event){
|
package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts
CHANGED
|
@@ -187,6 +187,10 @@ export class ConversationContentComponent implements OnInit {
|
|
|
187
187
|
objDiv.parentElement.scrollTop = objDiv.scrollHeight;
|
|
188
188
|
objDiv.style.opacity = '1';
|
|
189
189
|
that.firstScroll = false;
|
|
190
|
+
// Keep parent state in sync even when scroll is programmatic.
|
|
191
|
+
// Without this, the "scroll to bottom" button/badge can remain visible
|
|
192
|
+
// because (scroll) event might not fire reliably for programmatic scrollTop.
|
|
193
|
+
that.onScrollContent.emit(true);
|
|
190
194
|
}, 0);
|
|
191
195
|
} catch (err) {
|
|
192
196
|
this.logger.error('[CONV-CONTENT] scrollToBottom > Error :' + err);
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss
CHANGED
|
@@ -192,9 +192,10 @@ textarea:active{
|
|
|
192
192
|
max-height: 110px;
|
|
193
193
|
min-height: auto;
|
|
194
194
|
height: 20px;
|
|
195
|
-
padding: 0px 12px;
|
|
195
|
+
padding: 0px 12px;
|
|
196
196
|
margin: 10px 0px 10px;
|
|
197
197
|
border: none;
|
|
198
|
+
display: inline-block;
|
|
198
199
|
|
|
199
200
|
&::-webkit-scrollbar {
|
|
200
201
|
width: 6px;
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts
CHANGED
|
@@ -447,13 +447,11 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
private restoreTextArea() {
|
|
450
|
-
// that.logger.log('[CONV-FOOTER] AppComponent:restoreTextArea::restoreTextArea');
|
|
451
|
-
this.resizeInputField();
|
|
452
450
|
const textArea = (<HTMLInputElement>document.getElementById('chat21-main-message-context'));
|
|
453
|
-
this.textInputTextArea = '';
|
|
451
|
+
this.textInputTextArea = '';
|
|
454
452
|
if (textArea) {
|
|
455
|
-
textArea.value = '';
|
|
456
|
-
textArea.placeholder = this.translationMap.get('LABEL_PLACEHOLDER');
|
|
453
|
+
textArea.value = '';
|
|
454
|
+
textArea.placeholder = this.translationMap.get('LABEL_PLACEHOLDER');
|
|
457
455
|
if(textArea.style.height > this.HEIGHT_DEFAULT){
|
|
458
456
|
document.getElementById('chat21-button-send').style.removeProperty('right')
|
|
459
457
|
}
|
|
@@ -461,6 +459,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
461
459
|
}
|
|
462
460
|
this.setFocusOnId('chat21-main-message-context');
|
|
463
461
|
this.isStopRec= false;
|
|
462
|
+
this.resizeInputField();
|
|
464
463
|
}
|
|
465
464
|
|
|
466
465
|
/**
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
<!--
|
|
2
|
+
Security: render HTML messages as plain text (no DOM interpretation).
|
|
3
|
+
This preserves the exact input (e.g. "<h1>...</h1>") including line breaks.
|
|
4
|
+
-->
|
|
5
|
+
<pre id="htmlCode" #htmlCode [textContent]="htmlText"></pre>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Pipe, PipeTransform } from '@angular/core';
|
|
2
2
|
import { marked } from 'marked';
|
|
3
3
|
import { BLOCKED_DOMAINS } from '../utils/utils';
|
|
4
|
+
import { htmlEntities } from 'src/chat21-core/utils/utils';
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
@Pipe({
|
|
@@ -9,6 +10,32 @@ import { BLOCKED_DOMAINS } from '../utils/utils';
|
|
|
9
10
|
|
|
10
11
|
export class MarkedPipe implements PipeTransform {
|
|
11
12
|
transform(value: any): any {
|
|
13
|
+
// Security hardening:
|
|
14
|
+
// - Do not allow raw HTML from chat messages to be interpreted as DOM.
|
|
15
|
+
// - Keep Markdown working (marked will generate the needed HTML tags).
|
|
16
|
+
// This makes inputs like "<h1>Title</h1>" render exactly as typed.
|
|
17
|
+
const input =
|
|
18
|
+
typeof value === 'string'
|
|
19
|
+
? value
|
|
20
|
+
: (value === null || value === undefined) ? '' : String(value);
|
|
21
|
+
|
|
22
|
+
// Converti i \n letterali in newline reali prima di htmlEntities
|
|
23
|
+
// così il markdown con breaks: true li renderizzerà correttamente
|
|
24
|
+
const inputWithNewlines = input.replace(/\\n/g, '\n');
|
|
25
|
+
|
|
26
|
+
// Proteggi i > usati per i blockquote markdown (all'inizio di riga)
|
|
27
|
+
// sostituendoli temporaneamente con un placeholder
|
|
28
|
+
const BLOCKQUOTE_PLACEHOLDER = '___MARKDOWN_BLOCKQUOTE___';
|
|
29
|
+
const protectedInput = inputWithNewlines.replace(/^(\s*)>/gm, (match, spaces) => {
|
|
30
|
+
return spaces + BLOCKQUOTE_PLACEHOLDER;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Applica htmlEntities (che codificherà tutti gli altri >)
|
|
34
|
+
let safeInput = htmlEntities(protectedInput);
|
|
35
|
+
|
|
36
|
+
// Ripristina i > dei blockquote
|
|
37
|
+
safeInput = safeInput.replace(new RegExp(BLOCKQUOTE_PLACEHOLDER, 'g'), '>');
|
|
38
|
+
|
|
12
39
|
const renderer = new marked.Renderer();
|
|
13
40
|
renderer.link = function({ href, title, tokens }) {
|
|
14
41
|
// Normalizza l'href per evitare falsi negativi
|
|
@@ -74,15 +101,15 @@ export class MarkedPipe implements PipeTransform {
|
|
|
74
101
|
breaks: true
|
|
75
102
|
});
|
|
76
103
|
|
|
77
|
-
if (
|
|
104
|
+
if (safeInput && safeInput.length > 0) {
|
|
78
105
|
try {
|
|
79
|
-
return marked.parse(
|
|
106
|
+
return marked.parse(safeInput);
|
|
80
107
|
} catch (err) {
|
|
81
108
|
console.error('Errore nel parsing markdown:', err);
|
|
82
|
-
return
|
|
109
|
+
return safeInput;
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
|
-
return
|
|
112
|
+
return safeInput;
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
|
package/src/iframe-style.css
CHANGED
|
@@ -17,6 +17,24 @@
|
|
|
17
17
|
z-index: 3000000000; /*999999*/;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
#tiledesk-container.open {
|
|
21
|
+
/*
|
|
22
|
+
Fade-in the whole container when the "open" class is added.
|
|
23
|
+
This avoids the initial "flash" while the widget is still small.
|
|
24
|
+
*/
|
|
25
|
+
opacity: 0;
|
|
26
|
+
animation: tiledesk-container-fade-in 400ms ease-in both;
|
|
27
|
+
will-change: opacity;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@keyframes tiledesk-container-fade-in {
|
|
31
|
+
/* Stay invisible, then become visible very fast at the end */
|
|
32
|
+
0% { opacity: 0; }
|
|
33
|
+
80% { opacity: 0.15; }
|
|
34
|
+
92% { opacity: 0.30; }
|
|
35
|
+
100% { opacity: 1; }
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
#tiledesk-container.open.overlay--popup {
|
|
21
39
|
background-color: rgba(0, 0, 0, 0.4);
|
|
22
40
|
position: fixed;
|
|
@@ -57,8 +75,8 @@
|
|
|
57
75
|
margin: auto;
|
|
58
76
|
top: 0;
|
|
59
77
|
bottom: 0;
|
|
60
|
-
left: 0
|
|
61
|
-
right: 0
|
|
78
|
+
left: 0;
|
|
79
|
+
right: 0;
|
|
62
80
|
height: 100%; /*var(--iframeMaxHeight);*/
|
|
63
81
|
/* transform: translate(-50%, -50%); */
|
|
64
82
|
/* transform: translate(-50%, 0%); */
|
|
@@ -130,9 +148,20 @@
|
|
|
130
148
|
display: block;
|
|
131
149
|
/*width: 376px;*/
|
|
132
150
|
border-radius: 16px;
|
|
151
|
+
/*
|
|
152
|
+
Fade-in: keep content transparent while the widget grows (width/height transition
|
|
153
|
+
lives on #tiledeskdiv.{min-size|max-size|top-size}), then show it near full size.
|
|
154
|
+
*/
|
|
155
|
+
opacity: 0;
|
|
156
|
+
animation: tiledesk-iframe-fade-in 220ms ease-out 140ms both;
|
|
133
157
|
transition: box-shadow 0.8s ease-in;
|
|
134
158
|
box-shadow: rgba(0, 0, 0, 0.16) 0px 8px 36px 0px; /*NEW GAB*/
|
|
135
159
|
}
|
|
160
|
+
|
|
161
|
+
@keyframes tiledesk-iframe-fade-in {
|
|
162
|
+
from { opacity: 0; }
|
|
163
|
+
to { opacity: 1; }
|
|
164
|
+
}
|
|
136
165
|
/*
|
|
137
166
|
#tiledesk-container.open #tiledeskdiv.shadow {
|
|
138
167
|
transition: box-shadow 0.8s ease-in;
|
|
@@ -140,11 +169,6 @@
|
|
|
140
169
|
}
|
|
141
170
|
*/
|
|
142
171
|
|
|
143
|
-
#tiledesk-container.open #tiledeskdiv {
|
|
144
|
-
/* transition: box-shadow 0.8s ease-in;
|
|
145
|
-
box-shadow: rgba(0, 0, 0, 0.16) 0px 8px 36px 0px; */
|
|
146
|
-
}
|
|
147
|
-
|
|
148
172
|
#tiledesk-container.closed #tiledeskiframe {
|
|
149
173
|
display: block;
|
|
150
174
|
box-shadow: none;
|
package/.vscode/settings.json
DELETED