@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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chat21/chat21-web-widget",
3
3
  "author": "Tiledesk SRL",
4
- "version": "5.1.18",
4
+ "version": "5.1.20",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.tiledesk.com",
7
7
  "repository": {
@@ -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
- // box-shadow:rgba(0, 0, 0, 0.16) 0px 8px 36px 0px; //NEW GAB
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: 'min' | 'max' | 'top'){
1078
- var tiledeskDiv = this.g.windowContext.window.document.getElementById('tiledeskdiv');
1079
- this.g.size = mode
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(mode==='max'){
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(mode==='min'){
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(mode=== 'top'){
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){
@@ -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);
@@ -192,9 +192,10 @@ textarea:active{
192
192
  max-height: 110px;
193
193
  min-height: auto;
194
194
  height: 20px;
195
- padding: 0px 12px; //0px 40px 0px 70px; //NEW FOR EMOJII BUTTON
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;
@@ -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 = ''; // clear the textarea
451
+ this.textInputTextArea = '';
454
452
  if (textArea) {
455
- textArea.value = ''; // clear the textarea
456
- textArea.placeholder = this.translationMap.get('LABEL_PLACEHOLDER'); // restore the placholder
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
- <div id="htmlCode" #htmlCode [innerHTML]="htmlText | safeHtml"></div>
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>
@@ -74,4 +74,13 @@
74
74
  overflow: hidden;
75
75
  }
76
76
 
77
+ }
78
+
79
+ /* Render raw HTML source safely */
80
+ #htmlCode {
81
+ white-space: pre-wrap; /* preserve newlines, allow wrapping */
82
+ word-break: break-word;
83
+ margin: 0;
84
+ font-size: 1.4em;
85
+ line-height: 1.4em;
77
86
  }
@@ -65,6 +65,10 @@ p h1, p h2, p h3, p h4, p h5, p h6 {
65
65
  color: inherit; // Eredita il colore dal parent
66
66
  }
67
67
 
68
+ .message_innerhtml.marked h1 {
69
+ line-height: normal;
70
+ }
71
+
68
72
  p ul {
69
73
  margin-block-end: 0em;
70
74
  margin-block-start: 0em;
@@ -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 (value && value.length > 0) {
104
+ if (safeInput && safeInput.length > 0) {
78
105
  try {
79
- return marked.parse(value);
106
+ return marked.parse(safeInput);
80
107
  } catch (err) {
81
108
  console.error('Errore nel parsing markdown:', err);
82
- return value;
109
+ return safeInput;
83
110
  }
84
111
  }
85
- return value;
112
+ return safeInput;
86
113
  }
87
114
 
88
115
 
@@ -40,6 +40,7 @@
40
40
  h1 {
41
41
  font-size: 2em;
42
42
  margin: 0.67em 0;
43
+ line-height: normal;
43
44
  }
44
45
 
45
46
  /* Grouping content
@@ -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!important;
61
- right: 0!important;
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;
@@ -1,3 +0,0 @@
1
- {
2
- "git.ignoreLimitWarning": true
3
- }