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