@xapp/chat-widget 1.96.3 → 1.97.0
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/dist/components/ActionBar/ActionBar.d.ts +0 -0
- package/dist/components/ActionBar/index.d.ts +0 -0
- package/dist/index.css +1 -1
- package/dist/index.es.js +391 -29
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +391 -29
- package/dist/index.js.map +1 -1
- package/dist/utils/formPage.d.ts +0 -0
- package/dist/xapp-chat-widget.css +1 -1
- package/dist/xapp-chat-widget.js +5 -5
- package/dist/xapp-chat-widget.js.map +1 -1
- package/package.json +2 -2
package/dist/index.es.js
CHANGED
|
@@ -78,7 +78,9 @@ color.invertColorHEX = invertColorHEX;
|
|
|
78
78
|
color.invertColorRgb = invertColorRgb;
|
|
79
79
|
color.invertColor = invertColor;
|
|
80
80
|
color.isSupportedColor = isSupportedColor;
|
|
81
|
+
color.usableColorOr = usableColorOr;
|
|
81
82
|
color.readableTextOn = readableTextOn;
|
|
83
|
+
color.darkenColor = darkenColor;
|
|
82
84
|
/**
|
|
83
85
|
* Colour maths for building a widget theme from a business's own branding.
|
|
84
86
|
*
|
|
@@ -187,6 +189,19 @@ function isSupportedColor(color) {
|
|
|
187
189
|
}
|
|
188
190
|
return parseRgb(color) !== null;
|
|
189
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* The colour to theme with: the one given, or the fallback when we cannot work
|
|
194
|
+
* out what text reads on it.
|
|
195
|
+
*
|
|
196
|
+
* Every builder needs this and each was doing it by hand, which is how the
|
|
197
|
+
* chat widget ended up guarded and the other two not. A background whose
|
|
198
|
+
* contrast cannot be computed is one we must not use — the foreground would be
|
|
199
|
+
* a guess paired with an unknown, and on a dark unparseable colour that is
|
|
200
|
+
* invisible text.
|
|
201
|
+
*/
|
|
202
|
+
function usableColorOr(color, fallback) {
|
|
203
|
+
return isSupportedColor(color) ? color : fallback;
|
|
204
|
+
}
|
|
190
205
|
/**
|
|
191
206
|
* The text colour that will read on this background: black or white, never
|
|
192
207
|
* anything else.
|
|
@@ -226,6 +241,77 @@ function hexToRgb(hex) {
|
|
|
226
241
|
b: parseInt(value.slice(4, 6), 16)
|
|
227
242
|
};
|
|
228
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* A shade of the input colour, darkened by `amount` percent (negative
|
|
246
|
+
* lightens). Ported alongside the rest so the form widget's theme does not
|
|
247
|
+
* need its own copy of it.
|
|
248
|
+
*/
|
|
249
|
+
function darkenColor(colorIn, amount) {
|
|
250
|
+
if (!colorIn) {
|
|
251
|
+
return colorIn;
|
|
252
|
+
}
|
|
253
|
+
let r, g, b, a;
|
|
254
|
+
let colorOut;
|
|
255
|
+
try {
|
|
256
|
+
// Parse the color format
|
|
257
|
+
if (colorIn.startsWith('#')) {
|
|
258
|
+
// Hex format
|
|
259
|
+
const hexMatch = colorIn.match(/\w\w/g);
|
|
260
|
+
if (hexMatch) {
|
|
261
|
+
[r, g, b] = hexMatch.map(x => parseInt(x, 16));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (colorIn.startsWith('rgb')) {
|
|
265
|
+
// RGB or RGBA format
|
|
266
|
+
const rgba = parseRgb(colorIn);
|
|
267
|
+
if (rgba) {
|
|
268
|
+
({ r, g, b, a } = rgba);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (r === undefined || g === undefined || b === undefined) {
|
|
272
|
+
console.error(`Unsupported color format: ${colorIn}`);
|
|
273
|
+
return colorIn;
|
|
274
|
+
}
|
|
275
|
+
if (a !== undefined) {
|
|
276
|
+
// Adjust luminance for RGBA
|
|
277
|
+
// Luminance is on the 0-255 scale the channels are, so the clamp
|
|
278
|
+
// has to be too. Bounding it to 0-1 collapsed it to 1 for every
|
|
279
|
+
// real colour, which made the ratio tiny and crushed the result to
|
|
280
|
+
// near-black instead of darkening it: rgba(20, 52, 204, 0.8) at 20%
|
|
281
|
+
// came back rgba(0, 1, 3, 1).
|
|
282
|
+
const luminance = 0.3 * r + 0.59 * g + 0.11 * b;
|
|
283
|
+
let newLuminance = luminance * (1 - amount / 100);
|
|
284
|
+
newLuminance = Math.max(0, Math.min(255, newLuminance));
|
|
285
|
+
const ratio = newLuminance / (luminance || 1); // no division by zero
|
|
286
|
+
r = Math.round(r * ratio);
|
|
287
|
+
g = Math.round(g * ratio);
|
|
288
|
+
b = Math.round(b * ratio);
|
|
289
|
+
// Adjust alpha
|
|
290
|
+
a = a + amount / 100;
|
|
291
|
+
a = Math.max(0, Math.min(1, a)); // within 0 and 1
|
|
292
|
+
colorOut = `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Adjust the color for RGB or Hex
|
|
296
|
+
if (amount > 0) {
|
|
297
|
+
r = Math.max(0, r - amount);
|
|
298
|
+
g = Math.max(0, g - amount);
|
|
299
|
+
b = Math.max(0, b - amount);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
amount = Math.abs(amount); // positive for lightening
|
|
303
|
+
r = Math.min(255, r + amount);
|
|
304
|
+
g = Math.min(255, g + amount);
|
|
305
|
+
b = Math.min(255, b + amount);
|
|
306
|
+
}
|
|
307
|
+
colorOut = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
console.error(`Unsupported color format: ${colorIn} - Error: ${e}`);
|
|
312
|
+
}
|
|
313
|
+
return colorOut;
|
|
314
|
+
}
|
|
229
315
|
|
|
230
316
|
Object.defineProperty(buildChatWidgetChannel$1, "__esModule", { value: true });
|
|
231
317
|
buildChatWidgetChannel$1.buildChatWidgetChannel = buildChatWidgetChannel;
|
|
@@ -238,7 +324,7 @@ function buildChatWidgetChannel(props) {
|
|
|
238
324
|
// Only a colour whose contrast can actually be computed, so the background
|
|
239
325
|
// and the text on it are always a matched pair rather than one scraped
|
|
240
326
|
// value and one guess.
|
|
241
|
-
const primaryColor = (0, color_1.
|
|
327
|
+
const primaryColor = (0, color_1.usableColorOr)(props.primaryColor, DEFAULT_PRIMARY);
|
|
242
328
|
return {
|
|
243
329
|
type: "chat-widget",
|
|
244
330
|
// MCP by default. The widget runs on a public page, so homeowner mode
|
|
@@ -252,7 +338,7 @@ function buildChatWidgetChannel(props) {
|
|
|
252
338
|
mode: "homeowner"
|
|
253
339
|
}
|
|
254
340
|
},
|
|
255
|
-
theme: buildChatTheme(primaryColor),
|
|
341
|
+
theme: buildChatTheme(primaryColor, props.secondaryColor),
|
|
256
342
|
actionBar: buildActionBar({ apiUrl, formWidgetKey, phoneNumber })
|
|
257
343
|
};
|
|
258
344
|
}
|
|
@@ -303,19 +389,49 @@ function buildActionBar(props) {
|
|
|
303
389
|
};
|
|
304
390
|
}
|
|
305
391
|
/**
|
|
306
|
-
*
|
|
392
|
+
* The theme a new chat widget starts with, from the business's own colour.
|
|
393
|
+
*
|
|
394
|
+
* A faithful port of stentor-client's buildDefaultChatTheme, deliberately: the
|
|
395
|
+
* point of moving it is that onboarding can call this instead of its own copy,
|
|
396
|
+
* and that migration should not restyle every widget created afterwards. A
|
|
397
|
+
* thinner theme would have quietly dropped the CTA, message, input, footer and
|
|
398
|
+
* button styling that new widgets have today.
|
|
307
399
|
*
|
|
308
|
-
*
|
|
309
|
-
* reads on a given background is arithmetic, and getting it wrong ships a
|
|
310
|
-
* widget nobody can read.
|
|
400
|
+
* The one change is how the contrast colour is derived — see below.
|
|
311
401
|
*/
|
|
312
|
-
function buildChatTheme(primaryColor) {
|
|
313
|
-
const
|
|
402
|
+
function buildChatTheme(primaryColor, secondaryColor) {
|
|
403
|
+
const fontFamily = "RobotoRegular";
|
|
404
|
+
// secondaryColor is the colour of text sitting ON primaryColor — header
|
|
405
|
+
// title, subtitle, footer branding. Left unset it is computed, which is
|
|
406
|
+
// what makes the theme readable against whatever colour was scraped off
|
|
407
|
+
// the business's own site.
|
|
408
|
+
if (!secondaryColor) {
|
|
409
|
+
secondaryColor = (0, color_1.readableTextOn)(primaryColor);
|
|
410
|
+
}
|
|
314
411
|
return {
|
|
315
412
|
zIndex: "10000",
|
|
316
413
|
border: {
|
|
317
414
|
color: primaryColor,
|
|
318
|
-
radius: "10px"
|
|
415
|
+
radius: "10px",
|
|
416
|
+
},
|
|
417
|
+
carousel: {
|
|
418
|
+
button: {
|
|
419
|
+
color: primaryColor,
|
|
420
|
+
},
|
|
421
|
+
subtitle: {
|
|
422
|
+
color: "#979797",
|
|
423
|
+
fontFamily,
|
|
424
|
+
fontSize: "12px",
|
|
425
|
+
fontStyle: "normal",
|
|
426
|
+
fontWeight: "normal",
|
|
427
|
+
},
|
|
428
|
+
title: {
|
|
429
|
+
color: "#000",
|
|
430
|
+
fontFamily,
|
|
431
|
+
fontSize: "14px",
|
|
432
|
+
fontStyle: "normal",
|
|
433
|
+
fontWeight: "bold",
|
|
434
|
+
},
|
|
319
435
|
},
|
|
320
436
|
chatButton: {
|
|
321
437
|
background: primaryColor,
|
|
@@ -323,18 +439,147 @@ function buildChatTheme(primaryColor) {
|
|
|
323
439
|
bottom: "10px",
|
|
324
440
|
left: "10px",
|
|
325
441
|
right: "10px",
|
|
326
|
-
top: "10px"
|
|
327
|
-
}
|
|
442
|
+
top: "10px",
|
|
443
|
+
},
|
|
328
444
|
},
|
|
329
445
|
content: {
|
|
330
|
-
background: "#ffffff"
|
|
446
|
+
background: "#ffffff",
|
|
447
|
+
},
|
|
448
|
+
cta: {
|
|
449
|
+
background: "#e1e2e1",
|
|
450
|
+
text: {
|
|
451
|
+
// Use a dark color that contrasts well with the light grey background
|
|
452
|
+
// instead of primaryColor which may not have sufficient contrast
|
|
453
|
+
color: "#333333",
|
|
454
|
+
fontFamily,
|
|
455
|
+
fontSize: "14px",
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
footer: {
|
|
459
|
+
background: primaryColor,
|
|
460
|
+
border: {
|
|
461
|
+
color: primaryColor,
|
|
462
|
+
radius: "0px",
|
|
463
|
+
width: "0px",
|
|
464
|
+
},
|
|
465
|
+
branding: {
|
|
466
|
+
text: {
|
|
467
|
+
color: "#ffffff",
|
|
468
|
+
fontFamily,
|
|
469
|
+
fontSize: "14px",
|
|
470
|
+
fontWeight: "normal",
|
|
471
|
+
fontStyle: "normal",
|
|
472
|
+
},
|
|
473
|
+
},
|
|
331
474
|
},
|
|
332
475
|
header: {
|
|
333
476
|
background: primaryColor,
|
|
477
|
+
avatarBackground: "transparent",
|
|
478
|
+
border: {
|
|
479
|
+
color: primaryColor,
|
|
480
|
+
radius: "0px",
|
|
481
|
+
width: "0px",
|
|
482
|
+
},
|
|
334
483
|
text: {
|
|
335
|
-
color:
|
|
336
|
-
|
|
337
|
-
|
|
484
|
+
color: secondaryColor,
|
|
485
|
+
fontFamily,
|
|
486
|
+
fontSize: "16px",
|
|
487
|
+
fontWeight: "normal",
|
|
488
|
+
fontStyle: "normal",
|
|
489
|
+
},
|
|
490
|
+
subtitle: {
|
|
491
|
+
color: secondaryColor,
|
|
492
|
+
fontFamily,
|
|
493
|
+
fontSize: "10px",
|
|
494
|
+
fontWeight: "normal",
|
|
495
|
+
fontStyle: "normal",
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
input: {
|
|
499
|
+
background: "#ffffff",
|
|
500
|
+
border: {
|
|
501
|
+
radius: "20px",
|
|
502
|
+
},
|
|
503
|
+
text: {
|
|
504
|
+
color: "#000000",
|
|
505
|
+
fontFamily,
|
|
506
|
+
fontSize: "14px",
|
|
507
|
+
},
|
|
508
|
+
placeholder: {
|
|
509
|
+
color: "#e1e2e1",
|
|
510
|
+
fontFamily,
|
|
511
|
+
fontSize: "14px",
|
|
512
|
+
fontWeight: "normal",
|
|
513
|
+
fontStyle: "normal",
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
textTypingStatus: {
|
|
517
|
+
text: {
|
|
518
|
+
color: "#bfbfbf",
|
|
519
|
+
fontFamily,
|
|
520
|
+
fontSize: "14px",
|
|
521
|
+
fontWeight: "normal",
|
|
522
|
+
fontStyle: "normal",
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
menu: {
|
|
526
|
+
item: {
|
|
527
|
+
height: "24px",
|
|
528
|
+
text: {
|
|
529
|
+
fontSize: "14px",
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
menuButton: {
|
|
534
|
+
color: "#ffffff",
|
|
535
|
+
},
|
|
536
|
+
messages: {
|
|
537
|
+
maxWidth: "400px",
|
|
538
|
+
mine: {
|
|
539
|
+
bubbleColor: "#f2f2f2",
|
|
540
|
+
text: {
|
|
541
|
+
color: "#454545",
|
|
542
|
+
fontFamily,
|
|
543
|
+
fontSize: "18px",
|
|
544
|
+
fontStyle: "normal",
|
|
545
|
+
fontWeight: "normal",
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
others: {
|
|
549
|
+
bubbleColor: "#e1e2e1",
|
|
550
|
+
text: {
|
|
551
|
+
color: "#262726",
|
|
552
|
+
fontFamily,
|
|
553
|
+
fontSize: "18px",
|
|
554
|
+
fontStyle: "normal",
|
|
555
|
+
fontWeight: "normal",
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
padding: {
|
|
559
|
+
bottom: "0px",
|
|
560
|
+
left: "20px",
|
|
561
|
+
right: "16px",
|
|
562
|
+
top: "16px",
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
refreshButton: {
|
|
566
|
+
color: secondaryColor,
|
|
567
|
+
},
|
|
568
|
+
minimizeButton: {
|
|
569
|
+
color: secondaryColor,
|
|
570
|
+
},
|
|
571
|
+
cancelButton: {
|
|
572
|
+
color: secondaryColor,
|
|
573
|
+
},
|
|
574
|
+
primaryColor: primaryColor,
|
|
575
|
+
sendButton: {
|
|
576
|
+
color: "#505050",
|
|
577
|
+
width: "20px",
|
|
578
|
+
},
|
|
579
|
+
size: {
|
|
580
|
+
width: "500px",
|
|
581
|
+
height: "700px",
|
|
582
|
+
},
|
|
338
583
|
};
|
|
339
584
|
}
|
|
340
585
|
|
|
@@ -687,9 +932,39 @@ var MOBILE_MEDIA_QUERY = "(max-width: 640px), (max-height: 480px) and (pointer:
|
|
|
687
932
|
* Two concurrent bars would fight over it, and either one unmounting would clear it.
|
|
688
933
|
*/
|
|
689
934
|
var ACTION_BAR_HEIGHT_VAR = "--xapp-action-bar-height";
|
|
935
|
+
/**
|
|
936
|
+
* Whether pressing this button ends up in the form widget.
|
|
937
|
+
*
|
|
938
|
+
* `schedule` is the same form-widget infrastructure with a calendar icon, so it is the
|
|
939
|
+
* same button as far as this question goes. XappMedia/chat-widget#1626.
|
|
940
|
+
*/
|
|
941
|
+
function opensFormWidget(button) {
|
|
942
|
+
return lib.isActionBarFormButton(button) || lib.isActionBarScheduleButton(button);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Which of a bar's configured buttons would actually render.
|
|
946
|
+
*
|
|
947
|
+
* Exported because the decision is needed in two places: here, to render, and in
|
|
948
|
+
* `ChatWidget`, to know whether the bar has anything left to show and whether anything on
|
|
949
|
+
* the page can still open the form widget. Two copies of this predicate would drift.
|
|
950
|
+
*/
|
|
951
|
+
function visibleActionBarButtons(buttons, options) {
|
|
952
|
+
if (buttons === void 0) { buttons = []; }
|
|
953
|
+
if (options === void 0) { options = {}; }
|
|
954
|
+
var chatDisabled = options.chatDisabled, formDisabled = options.formDisabled;
|
|
955
|
+
return buttons.filter(function (button) {
|
|
956
|
+
if (chatDisabled && lib.isActionBarChatButton(button)) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
if (formDisabled && opensFormWidget(button)) {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
return true;
|
|
963
|
+
});
|
|
964
|
+
}
|
|
690
965
|
var ActionBar = function (_a) {
|
|
691
966
|
var _b, _c;
|
|
692
|
-
var config = _a.config, visible = _a.visible, chatDisabled = _a.chatDisabled, _d = _a.hasUserInteracted, hasUserInteracted = _d === void 0 ? false : _d, onChatClick = _a.onChatClick, onChatMinimize = _a.onChatMinimize, onFormClick = _a.onFormClick, onCtaDismiss = _a.onCtaDismiss;
|
|
967
|
+
var config = _a.config, visible = _a.visible, chatDisabled = _a.chatDisabled, formDisabled = _a.formDisabled, _d = _a.hasUserInteracted, hasUserInteracted = _d === void 0 ? false : _d, onChatClick = _a.onChatClick, onChatMinimize = _a.onChatMinimize, onFormClick = _a.onFormClick, onCtaDismiss = _a.onCtaDismiss;
|
|
693
968
|
var _e = useState(false), isMobile = _e[0], setIsMobile = _e[1];
|
|
694
969
|
// An explicit override can only be a width; without one we ask the same question the
|
|
695
970
|
// stylesheets ask, so the two cannot disagree. A survey of production on 2026-08-19
|
|
@@ -760,10 +1035,9 @@ var ActionBar = function (_a) {
|
|
|
760
1035
|
document.documentElement.style.removeProperty(ACTION_BAR_HEIGHT_VAR);
|
|
761
1036
|
};
|
|
762
1037
|
}, [isMobile]);
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
: config.buttons;
|
|
1038
|
+
// Drop the buttons this page has no use for - chat when chat is disabled, form/schedule
|
|
1039
|
+
// when the form already is the page.
|
|
1040
|
+
var filteredButtons = visibleActionBarButtons(config.buttons, { chatDisabled: chatDisabled, formDisabled: formDisabled });
|
|
767
1041
|
// Build class names - ActionBar stays visible even when chat is open
|
|
768
1042
|
var classNames = [
|
|
769
1043
|
"xapp-action-bar",
|
|
@@ -776,7 +1050,7 @@ var ActionBar = function (_a) {
|
|
|
776
1050
|
// - Must have CTA config with a message
|
|
777
1051
|
// - Chat must not be visible (user hasn't opened it)
|
|
778
1052
|
// - Chat must not be disabled
|
|
779
|
-
var hasChatButton =
|
|
1053
|
+
var hasChatButton = filteredButtons.some(lib.isActionBarChatButton);
|
|
780
1054
|
var showCta = ((_c = config.cta) === null || _c === void 0 ? void 0 : _c.message) && !visible && !chatDisabled && hasChatButton;
|
|
781
1055
|
var handleCtaClick = function () {
|
|
782
1056
|
onChatClick();
|
|
@@ -784,6 +1058,13 @@ var ActionBar = function (_a) {
|
|
|
784
1058
|
var handleCtaDismiss = function () {
|
|
785
1059
|
onCtaDismiss === null || onCtaDismiss === void 0 ? void 0 : onCtaDismiss();
|
|
786
1060
|
};
|
|
1061
|
+
// A bar with nothing in it is a bare strip of chrome the visitor cannot use. The bar
|
|
1062
|
+
// renders on `actionBar.enabled` alone, so an App configured with only Form + Schedule
|
|
1063
|
+
// filters down to zero on a form page - render nothing rather than an empty shell.
|
|
1064
|
+
// This is after the hooks on purpose; bailing earlier would change the hook order.
|
|
1065
|
+
if (filteredButtons.length === 0) {
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
787
1068
|
return (jsxs("div", { ref: barRef, className: classNames, role: "toolbar", "aria-label": "Chat actions", children: [showCta && (jsx("div", { className: "xapp-action-bar-cta-wrapper", children: jsx(ActionBarCta, { config: config.cta, isMobile: isMobile, onClick: handleCtaClick, onDismiss: handleCtaDismiss, hasUserInteracted: hasUserInteracted }) })), jsx("div", { className: "xapp-action-bar__buttons", children: filteredButtons.map(function (button) { return (jsx(ActionBarButton, { button: button, chatActive: visible, onChatClick: onChatClick, onChatMinimize: onChatMinimize, onFormClick: onFormClick, isMobile: isMobile }, button.id)); }) })] }));
|
|
788
1069
|
};
|
|
789
1070
|
|
|
@@ -12552,6 +12833,64 @@ function getSrcId(url) {
|
|
|
12552
12833
|
return res;
|
|
12553
12834
|
}
|
|
12554
12835
|
|
|
12836
|
+
/*! Copyright (c) 2026, XAPPmedia */
|
|
12837
|
+
/**
|
|
12838
|
+
* Body attribute a host page sets to declare "a form widget already *is* this page".
|
|
12839
|
+
*
|
|
12840
|
+
* The standalone contact/booking page (contact.xapp.ai, book.xapp.ai, sche.dule.pro)
|
|
12841
|
+
* renders a docked form-widget as its whole content and then injects the chat widget on
|
|
12842
|
+
* top of it. Without a signal, the injected widget cheerfully offers two more ways into
|
|
12843
|
+
* the same form - an ActionBar Contact/Book button and the form-widget's own side button.
|
|
12844
|
+
*
|
|
12845
|
+
* This is a declaration, not a detection: the page knows what it renders, the widget does
|
|
12846
|
+
* not. Sniffing the DOM for a docked form would be guesswork, and it would also fire on
|
|
12847
|
+
* pages that legitimately embed a form *and* want a separate one from the bar.
|
|
12848
|
+
*
|
|
12849
|
+
* The writer is `@xapp/form-widget`'s `useFormPageMarker` (used by `@xapp/contact-app`).
|
|
12850
|
+
* The two packages ship independently, so this string is the contract between them -
|
|
12851
|
+
* the same arrangement `[data-xapp-form-side-button]` already uses. Do not rename it on
|
|
12852
|
+
* one side only. XappMedia/chat-widget#1626.
|
|
12853
|
+
*/
|
|
12854
|
+
var FORM_PAGE_ATTRIBUTE = "data-xapp-form-page";
|
|
12855
|
+
/**
|
|
12856
|
+
* Whether a form widget is already the page, per the marker above.
|
|
12857
|
+
*/
|
|
12858
|
+
function isFormPage() {
|
|
12859
|
+
if (typeof document === "undefined" || !document.body) {
|
|
12860
|
+
return false;
|
|
12861
|
+
}
|
|
12862
|
+
return document.body.getAttribute(FORM_PAGE_ATTRIBUTE) === "true";
|
|
12863
|
+
}
|
|
12864
|
+
/**
|
|
12865
|
+
* Reactive form of {@link isFormPage}.
|
|
12866
|
+
*
|
|
12867
|
+
* The marker and the chat widget mount independently - the widget arrives from a
|
|
12868
|
+
* `<script>` tag, so it can win or lose the race against the page's own React tree.
|
|
12869
|
+
* Observing the attribute rather than reading it once means the filter settles correctly
|
|
12870
|
+
* whichever order they land in.
|
|
12871
|
+
*/
|
|
12872
|
+
function useIsFormPage() {
|
|
12873
|
+
var _a = useState(isFormPage), formPage = _a[0], setFormPage = _a[1];
|
|
12874
|
+
useEffect(function () {
|
|
12875
|
+
if (typeof document === "undefined" || !document.body) {
|
|
12876
|
+
return undefined;
|
|
12877
|
+
}
|
|
12878
|
+
// Re-read on mount: the marker may have appeared between the initial state and here.
|
|
12879
|
+
setFormPage(isFormPage());
|
|
12880
|
+
// jsdom has MutationObserver, but a customer's browser might not - an old browser
|
|
12881
|
+
// that reads the marker once is still better than one that throws on mount.
|
|
12882
|
+
if (typeof MutationObserver === "undefined") {
|
|
12883
|
+
return undefined;
|
|
12884
|
+
}
|
|
12885
|
+
var observer = new MutationObserver(function () {
|
|
12886
|
+
setFormPage(isFormPage());
|
|
12887
|
+
});
|
|
12888
|
+
observer.observe(document.body, { attributes: true, attributeFilter: [FORM_PAGE_ATTRIBUTE] });
|
|
12889
|
+
return function () { return observer.disconnect(); };
|
|
12890
|
+
}, []);
|
|
12891
|
+
return formPage;
|
|
12892
|
+
}
|
|
12893
|
+
|
|
12555
12894
|
function useChatServerVisitorId() {
|
|
12556
12895
|
var dispatch = useChatServerDispatch();
|
|
12557
12896
|
var clientVisitorId = useSelector(function (state) {
|
|
@@ -36796,6 +37135,21 @@ var ChatWidgetUI = function (props) {
|
|
|
36796
37135
|
log("Rendering - accountStatus: \"".concat(chatState.accountStatus, "\", connectionStatus: \"").concat(chatState.connection.connectionStatus, "\""));
|
|
36797
37136
|
var config = props.config, onConnectionStatusChange = props.onConnectionStatusChange;
|
|
36798
37137
|
var actionBarEnabled = ((_s = config === null || config === void 0 ? void 0 : config.actionBar) === null || _s === void 0 ? void 0 : _s.enabled) === true;
|
|
37138
|
+
// The host page declares this; the widget cannot work it out. On the standalone
|
|
37139
|
+
// contact/booking page the form already *is* the page, which makes every button that
|
|
37140
|
+
// only opens one redundant. XappMedia/chat-widget#1626.
|
|
37141
|
+
var isFormPage = useIsFormPage();
|
|
37142
|
+
var actionBarButtons = useMemo(function () {
|
|
37143
|
+
return actionBarEnabled && (config === null || config === void 0 ? void 0 : config.actionBar)
|
|
37144
|
+
? visibleActionBarButtons(config.actionBar.buttons, {
|
|
37145
|
+
chatDisabled: config.disabled,
|
|
37146
|
+
formDisabled: isFormPage,
|
|
37147
|
+
})
|
|
37148
|
+
: [];
|
|
37149
|
+
}, [actionBarEnabled, config === null || config === void 0 ? void 0 : config.actionBar, config === null || config === void 0 ? void 0 : config.disabled, isFormPage]);
|
|
37150
|
+
// A bar that filters down to nothing is a bare strip of chrome. Treat it as if the bar
|
|
37151
|
+
// were off, so the visitor still gets the plain chat button rather than an empty shell.
|
|
37152
|
+
var actionBarVisible = actionBarEnabled && actionBarButtons.length > 0;
|
|
36799
37153
|
// Disable greeting in preview mode
|
|
36800
37154
|
useGreeting(!isOffline && !props.preChatFormEnabled && visible && !previewMode);
|
|
36801
37155
|
var connectionStatus = chatState.connection.connectionStatus;
|
|
@@ -36805,23 +37159,31 @@ var ChatWidgetUI = function (props) {
|
|
|
36805
37159
|
}
|
|
36806
37160
|
}, [connectionStatus, onConnectionStatusChange]);
|
|
36807
37161
|
useExternalScript((_t = props.config) === null || _t === void 0 ? void 0 : _t.middlewareUrl);
|
|
36808
|
-
//
|
|
37162
|
+
// Only reach for form.xapp.ai when something on this page can still open the form.
|
|
37163
|
+
//
|
|
37164
|
+
// Loading it is not free: the script mounts a second form-widget instance whose wrapper
|
|
37165
|
+
// renders its own sticky "Contact Us" side button. On a page that already is the form
|
|
37166
|
+
// that button is a third way into the same thing, so not loading the script at all is
|
|
37167
|
+
// what actually removes it - hiding it in CSS only papers over the second instance.
|
|
37168
|
+
//
|
|
37169
|
+
// With no bar configured the side button IS the entry point, so that case still loads.
|
|
37170
|
+
var formWidgetNeeded = !isFormPage && (!actionBarEnabled || actionBarButtons.some(opensFormWidget));
|
|
36809
37171
|
useEffect(function () {
|
|
36810
|
-
if (config === null || config === void 0 ? void 0 : config.formWidgetKey) {
|
|
37172
|
+
if ((config === null || config === void 0 ? void 0 : config.formWidgetKey) && formWidgetNeeded) {
|
|
36811
37173
|
preloadFormWidgetScript(config.formWidgetKey);
|
|
36812
37174
|
}
|
|
36813
|
-
}, [config === null || config === void 0 ? void 0 : config.formWidgetKey]);
|
|
37175
|
+
}, [config === null || config === void 0 ? void 0 : config.formWidgetKey, formWidgetNeeded]);
|
|
36814
37176
|
// Set body attribute when actionBar is enabled (used by CSS to hide form-widget side button on mobile)
|
|
36815
37177
|
// Uses reference counting to handle multiple widget instances
|
|
36816
37178
|
useEffect(function () {
|
|
36817
|
-
if (
|
|
37179
|
+
if (actionBarVisible) {
|
|
36818
37180
|
// Increment reference count
|
|
36819
37181
|
var currentCount = parseInt(document.body.getAttribute("data-xapp-action-bar-count") || "0", 10);
|
|
36820
37182
|
document.body.setAttribute("data-xapp-action-bar-count", String(currentCount + 1));
|
|
36821
37183
|
document.body.setAttribute("data-xapp-action-bar-enabled", "true");
|
|
36822
37184
|
}
|
|
36823
37185
|
return function () {
|
|
36824
|
-
if (
|
|
37186
|
+
if (actionBarVisible) {
|
|
36825
37187
|
// Decrement reference count
|
|
36826
37188
|
var currentCount = parseInt(document.body.getAttribute("data-xapp-action-bar-count") || "0", 10);
|
|
36827
37189
|
var newCount = Math.max(0, currentCount - 1);
|
|
@@ -36834,7 +37196,7 @@ var ChatWidgetUI = function (props) {
|
|
|
36834
37196
|
}
|
|
36835
37197
|
}
|
|
36836
37198
|
};
|
|
36837
|
-
}, [
|
|
37199
|
+
}, [actionBarVisible]);
|
|
36838
37200
|
// This is a pseudo agent. It represent's the widget (shown in the header avatar for instance)
|
|
36839
37201
|
var widgetAgent = ((_u = chatState.agents["agent:robot"]) === null || _u === void 0 ? void 0 : _u.user) ||
|
|
36840
37202
|
(config === null || config === void 0 ? void 0 : config.agent) || {
|
|
@@ -36842,9 +37204,9 @@ var ChatWidgetUI = function (props) {
|
|
|
36842
37204
|
avatarPath: config.avatarUrl,
|
|
36843
37205
|
display_name: "Agent",
|
|
36844
37206
|
};
|
|
36845
|
-
return (jsxs(Fragment, { children: [jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(
|
|
37207
|
+
return (jsxs(Fragment, { children: [jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_x = (_w = (_v = props.config) === null || _v === void 0 ? void 0 : _v.connection) === null || _w === void 0 ? void 0 : _w.mcp) === null || _x === void 0 ? void 0 : _x.debugMode, onSubmit: handleOnSubmit }), jsx(MessageList, { visible: visible, queuePosition: chatState.queuePosition, isChatting: chatState.isChatting, isOffline: isOffline, messages: messages, agents: chatState.agents, agent: config === null || config === void 0 ? void 0 : config.agent, lastRatingRequestTimestamp: chatState.lastRatingRequestTimestamp, hasRating: chatState.hasRating, visitorId: chatState.visitorId, hasWsButton: !!chatState.wsButton, messageMiddleware: props.messageMiddleware, textTypingStatusEnabled: (_z = (_y = props.config) === null || _y === void 0 ? void 0 : _y.typingStatus) === null || _z === void 0 ? void 0 : _z.textTypingStatusEnabled, disableAutoScroll: props.disableAutoScroll, onSend: handleSendMessage, onWrite: handleWriteMessage, onOpenUrl: handleOpenUrl, minimizeOnClick: handleMinimizeClick }), jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && jsx(ServerOffline, {}), chatState.wsButton && visible && (jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), jsx(ChatFooter, { isAdmin: config === null || config === void 0 ? void 0 : config.isAdmin, isChatting: chatState.isChatting, placeholder: (_0 = config === null || config === void 0 ? void 0 : config.input) === null || _0 === void 0 ? void 0 : _0.placeholder, sendButtonIcon: (_2 = (_1 = config === null || config === void 0 ? void 0 : config.footer) === null || _1 === void 0 ? void 0 : _1.sendButton) === null || _2 === void 0 ? void 0 : _2.icon, sendButtonIconHover: (_4 = (_3 = config === null || config === void 0 ? void 0 : config.footer) === null || _3 === void 0 ? void 0 : _3.sendButton) === null || _4 === void 0 ? void 0 : _4.iconHover, sendButtonIconDisabled: (_6 = (_5 = config === null || config === void 0 ? void 0 : config.footer) === null || _5 === void 0 ? void 0 : _5.sendButton) === null || _6 === void 0 ? void 0 : _6.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: (_7 = props.config) === null || _7 === void 0 ? void 0 : _7.menu, footerConfig: (_8 = props.config) === null || _8 === void 0 ? void 0 : _8.footer, inputConfig: (_9 = props.config) === null || _9 === void 0 ? void 0 : _9.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarVisible && config.actionBar ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign(__assign({}, config.actionBar), {
|
|
36846
37208
|
// Use actionBar.cta if set, otherwise fall back to widget-level cta
|
|
36847
|
-
cta: (_10 = config.actionBar.cta) !== null && _10 !== void 0 ? _10 : config.cta }), visible: visible, chatDisabled: config.disabled, hasUserInteracted: (_11 = chatState.visuals) === null || _11 === void 0 ? void 0 : _11.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: config === null || config === void 0 ? void 0 : config.cta, imageUrl: (_12 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _12 === void 0 ? void 0 : _12.imageUrl, visible: visible, hasInteracted: (_13 = chatState.visuals) === null || _13 === void 0 ? void 0 : _13.hasInteracted, onCtaDismiss: handleCtaDismiss })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
|
|
37209
|
+
cta: (_10 = config.actionBar.cta) !== null && _10 !== void 0 ? _10 : config.cta }), visible: visible, chatDisabled: config.disabled, formDisabled: isFormPage, hasUserInteracted: (_11 = chatState.visuals) === null || _11 === void 0 ? void 0 : _11.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: config === null || config === void 0 ? void 0 : config.cta, imageUrl: (_12 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _12 === void 0 ? void 0 : _12.imageUrl, visible: visible, hasInteracted: (_13 = chatState.visuals) === null || _13 === void 0 ? void 0 : _13.hasInteracted, onCtaDismiss: handleCtaDismiss })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
|
|
36848
37210
|
};
|
|
36849
37211
|
/**
|
|
36850
37212
|
* Top-level wrapper that dispatches between preview mode and connected mode.
|