@hellotext/hellotext 2.4.1 → 2.4.3

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.
@@ -28,6 +28,11 @@ import { usePopover } from './mixins/usePopover';
28
28
  import { useBehaviour } from './webchat/useBehaviour';
29
29
  import { useOpeningSequence } from './webchat/useOpeningSequence';
30
30
  import { useTeaser } from './webchat/useTeaser';
31
+ var POPOVER_ANIMATION_DURATION = 120;
32
+ var MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
33
+ hour: 'numeric',
34
+ minute: '2-digit'
35
+ };
31
36
  var _default = /*#__PURE__*/function (_Controller) {
32
37
  _inherits(_default, _Controller);
33
38
  var _super = _createSuper(_default);
@@ -47,6 +52,7 @@ var _default = /*#__PURE__*/function (_Controller) {
47
52
  this.onTypingStart = this.onTypingStart.bind(this);
48
53
  this.onScroll = this.onScroll.bind(this);
49
54
  this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
55
+ this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this);
50
56
  this.broadcastChannel = new BroadcastChannel("hellotext--webchat--".concat(this.idValue));
51
57
  _get(_getPrototypeOf(_default.prototype), "initialize", this).call(this);
52
58
  }
@@ -70,6 +76,7 @@ var _default = /*#__PURE__*/function (_Controller) {
70
76
  }
71
77
  this.setupTeaser();
72
78
  this.setupOpeningSequence();
79
+ this.localizeMessageTimestamps();
73
80
  this.webChatChannel.onMessage(this.onMessageReceived);
74
81
  this.webChatChannel.onTypingStart(this.onTypingStart);
75
82
  this.webChatChannel.onReaction(this.onMessageReaction);
@@ -79,6 +86,7 @@ var _default = /*#__PURE__*/function (_Controller) {
79
86
  }
80
87
  Hellotext.eventEmitter.dispatch('webchat:mounted');
81
88
  this.broadcastChannel.addEventListener('message', this.onOutboundMessageSent);
89
+ window.addEventListener('keydown', this.closePopoverOnEscape, true);
82
90
  this.scheduleBehaviourOpen();
83
91
  _get(_getPrototypeOf(_default.prototype), "connect", this).call(this);
84
92
  }
@@ -86,10 +94,12 @@ var _default = /*#__PURE__*/function (_Controller) {
86
94
  key: "disconnect",
87
95
  value: function disconnect() {
88
96
  this.cancelBehaviourOpen();
97
+ this.clearPopoverOpenAnimation();
89
98
  this.teardownTeaser();
90
99
  this.teardownOpeningSequence();
91
100
  this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent);
92
101
  this.messagesContainerTarget.removeEventListener('scroll', this.onScroll);
102
+ window.removeEventListener('keydown', this.closePopoverOnEscape, true);
93
103
 
94
104
  // Clean up typing indicator timeouts
95
105
  this.clearTypingIndicator();
@@ -178,6 +188,7 @@ var _default = /*#__PURE__*/function (_Controller) {
178
188
  var callbacks = {
179
189
  'message:sent': data => {
180
190
  var element = new DOMParser().parseFromString(data.element, 'text/html').body.firstElementChild;
191
+ this.localizeMessageTimestamps(element);
181
192
 
182
193
  // Insert message before typing indicator if one exists
183
194
  if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
@@ -190,8 +201,8 @@ var _default = /*#__PURE__*/function (_Controller) {
190
201
  });
191
202
  },
192
203
  'message:failed': data => {
193
- var _this$messagesContain;
194
- (_this$messagesContain = this.messagesContainerTarget.querySelector("#".concat(data.id))) === null || _this$messagesContain === void 0 ? void 0 : _this$messagesContain.classList.add('failed');
204
+ var element = this.messagesContainerTarget.querySelector("#".concat(data.id));
205
+ this.markMessageFailed(element, data.reason);
195
206
  }
196
207
  };
197
208
  if (callbacks[data.type]) {
@@ -221,6 +232,7 @@ var _default = /*#__PURE__*/function (_Controller) {
221
232
  body,
222
233
  attachments
223
234
  } = message;
235
+ var createdAt = message.created_at || message.createdAt;
224
236
  var div = document.createElement('div');
225
237
  div.innerHTML = body;
226
238
  var element = this.messageTemplateTarget.cloneNode(true);
@@ -243,6 +255,7 @@ var _default = /*#__PURE__*/function (_Controller) {
243
255
  });
244
256
  }
245
257
  element.setAttribute('data-body', body);
258
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
246
259
  this.messagesContainerTarget.prepend(element);
247
260
  });
248
261
  this.messagesContainerTarget.scroll({
@@ -266,10 +279,30 @@ var _default = /*#__PURE__*/function (_Controller) {
266
279
  }, {
267
280
  key: "closePopover",
268
281
  value: function closePopover() {
269
- this.popoverTarget.classList.add(...this.fadeOutClasses);
270
- setTimeout(() => {
271
- this.openValue = false;
272
- }, 250);
282
+ this.clearPopoverOpenAnimation();
283
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
284
+ this.openValue = false;
285
+ }
286
+ }, {
287
+ key: "preparePopoverOpenAnimation",
288
+ value: function preparePopoverOpenAnimation() {
289
+ this.clearPopoverOpenAnimation();
290
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
291
+ this.popoverTarget.classList.add('hellotext--webchat-popover-opening');
292
+ this.popoverOpenAnimationTimeout = setTimeout(() => {
293
+ this.popoverTarget.classList.remove('hellotext--webchat-popover-opening');
294
+ this.popoverOpenAnimationTimeout = null;
295
+ }, POPOVER_ANIMATION_DURATION);
296
+ }
297
+ }, {
298
+ key: "clearPopoverOpenAnimation",
299
+ value: function clearPopoverOpenAnimation() {
300
+ var _this$popoverTarget;
301
+ if (this.popoverOpenAnimationTimeout) {
302
+ clearTimeout(this.popoverOpenAnimationTimeout);
303
+ this.popoverOpenAnimationTimeout = null;
304
+ }
305
+ (_this$popoverTarget = this.popoverTarget) === null || _this$popoverTarget === void 0 ? void 0 : _this$popoverTarget.classList.remove('hellotext--webchat-popover-opening');
273
306
  }
274
307
  }, {
275
308
  key: "onPopoverOpened",
@@ -303,6 +336,7 @@ var _default = /*#__PURE__*/function (_Controller) {
303
336
  }, {
304
337
  key: "onPopoverClosed",
305
338
  value: function onPopoverClosed() {
339
+ this.clearPopoverOpenAnimation();
306
340
  Hellotext.eventEmitter.dispatch('webchat:closed');
307
341
  localStorage.setItem("hellotext--webchat--".concat(this.idValue), 'closed');
308
342
  }
@@ -340,6 +374,7 @@ var _default = /*#__PURE__*/function (_Controller) {
340
374
  attachments,
341
375
  teaser
342
376
  } = message;
377
+ var createdAt = message.created_at || message.createdAt;
343
378
  if (!this.claimMessageId(id)) return;
344
379
  (_this$hideTeaser = this.hideTeaser) === null || _this$hideTeaser === void 0 ? void 0 : _this$hideTeaser.call(this);
345
380
  if (message.carousel) {
@@ -352,6 +387,7 @@ var _default = /*#__PURE__*/function (_Controller) {
352
387
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
353
388
  element.setAttribute('data-id', id);
354
389
  element.setAttribute('data-hellotext--webchat-target', 'message');
390
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
355
391
  if (attachments) {
356
392
  attachments.forEach(attachmentUrl => {
357
393
  var _this$messageAttachme2;
@@ -408,6 +444,7 @@ var _default = /*#__PURE__*/function (_Controller) {
408
444
  var element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild;
409
445
  element.setAttribute('data-id', message.id);
410
446
  element.setAttribute('data-hellotext--webchat-target', 'message');
447
+ this.localizeMessageTimestamps(element);
411
448
  this.clearTypingIndicator();
412
449
  this.messagesContainerTarget.appendChild(element);
413
450
  element.scrollIntoView({
@@ -453,14 +490,14 @@ var _default = /*#__PURE__*/function (_Controller) {
453
490
  (_this$dismissTeaserFo2 = this.dismissTeaserForSession) === null || _this$dismissTeaserFo2 === void 0 ? void 0 : _this$dismissTeaserFo2.call(this);
454
491
  var formData = new FormData();
455
492
  formData.append('message[body]', body);
456
- formData.append('message[replied_to]', id);
457
- formData.append('message[product]', product);
458
- formData.append('message[button]', buttonId);
493
+ if (id) formData.append('message[replied_to]', id);
494
+ if (product) formData.append('message[product]', product);
495
+ if (buttonId) formData.append('message[button]', buttonId);
459
496
  formData.append('session', Hellotext.session);
460
497
  formData.append('locale', Locale.toString());
461
498
  this.appendOpeningSequenceMessageIds(formData);
462
499
  var element = this.buildMessageElement();
463
- var attachment = (_cardElement$querySel = cardElement.querySelector('img')) === null || _cardElement$querySel === void 0 ? void 0 : _cardElement$querySel.cloneNode(true);
500
+ var attachment = cardElement === null || cardElement === void 0 ? void 0 : (_cardElement$querySel = cardElement.querySelector('img')) === null || _cardElement$querySel === void 0 ? void 0 : _cardElement$querySel.cloneNode(true);
464
501
  element.querySelector('[data-body]').innerText = body;
465
502
  if (attachment) {
466
503
  var _this$messageAttachme3;
@@ -484,17 +521,14 @@ var _default = /*#__PURE__*/function (_Controller) {
484
521
  if (response.failed) {
485
522
  // Clear the optimistic typing indicator on failure
486
523
  clearTimeout(this.optimisticTypingTimeout);
487
- this.broadcastChannel.postMessage({
488
- type: 'message:failed',
489
- id: element.id
490
- });
491
- return element.classList.add('failed');
524
+ return this.markMessageFailedFromResponse(response, element);
492
525
  }
493
526
  var data = yield response.json();
494
527
  this.dispatch('set:id', {
495
528
  target: element,
496
529
  detail: data.id
497
530
  });
531
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
498
532
  this.clearRevealedOpeningSequenceMessageIds();
499
533
  var message = {
500
534
  id: data.id,
@@ -555,14 +589,11 @@ var _default = /*#__PURE__*/function (_Controller) {
555
589
  var response = yield this.messagesAPI.create(formData);
556
590
  if (response.failed) {
557
591
  clearTimeout(this.optimisticTypingTimeout);
558
- this.broadcastChannel.postMessage({
559
- type: 'message:failed',
560
- id: element.id
561
- });
562
- return element.classList.add('failed');
592
+ return this.markMessageFailedFromResponse(response, element);
563
593
  }
564
594
  var data = yield response.json();
565
595
  element.setAttribute('data-id', data.id);
596
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
566
597
  this.clearRevealedOpeningSequenceMessageIds();
567
598
  Hellotext.eventEmitter.dispatch('webchat:message:sent', {
568
599
  id: data.id,
@@ -664,15 +695,12 @@ var _default = /*#__PURE__*/function (_Controller) {
664
695
  if (response.failed) {
665
696
  // Clear the optimistic typing indicator on failure
666
697
  clearTimeout(this.optimisticTypingTimeout);
667
- this.broadcastChannel.postMessage({
668
- type: 'message:failed',
669
- id: element.id
670
- });
671
- return element.classList.add('failed');
698
+ return this.markMessageFailedFromResponse(response, element);
672
699
  }
673
700
  var data = yield response.json();
674
701
  element.setAttribute('data-id', data.id);
675
702
  message.id = data.id;
703
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
676
704
  this.clearRevealedOpeningSequenceMessageIds();
677
705
  Hellotext.eventEmitter.dispatch('webchat:message:sent', message);
678
706
  if (data.conversation !== this.conversationIdValue) {
@@ -700,8 +728,145 @@ var _default = /*#__PURE__*/function (_Controller) {
700
728
  element.style.removeProperty('display');
701
729
  element.setAttribute('data-controller', 'hellotext--message');
702
730
  element.setAttribute('data-hellotext--webchat-target', 'message');
731
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), new Date());
703
732
  return element;
704
733
  }
734
+ }, {
735
+ key: "focusCompose",
736
+ value: function focusCompose(event) {
737
+ var {
738
+ target
739
+ } = event;
740
+ var ignoredSelector = ['button', 'a', 'input', 'textarea', 'select', 'label', '[role="button"]', 'em-emoji-picker', '[data-hellotext--webchat--emoji-target~="popover"]', '[data-controller~="hellotext--webchat--emoji"]'].join(', ');
741
+ if (!this.hasInputTarget || target.closest(ignoredSelector)) return;
742
+ event.preventDefault();
743
+ this.inputTarget.focus();
744
+ if (typeof this.inputTarget.selectionStart === 'number') {
745
+ var position = this.inputTarget.value.length;
746
+ this.inputTarget.setSelectionRange(position, position);
747
+ }
748
+ }
749
+ }, {
750
+ key: "closePopoverFromHeader",
751
+ value: function closePopoverFromHeader(event) {
752
+ var {
753
+ target
754
+ } = event;
755
+ if (target.closest('.hellotext--webchat-header-channel-button, .hellotext--webchat-close-button')) return;
756
+ event.preventDefault();
757
+ this.closePopover();
758
+ }
759
+ }, {
760
+ key: "closePopoverOnEscape",
761
+ value: function closePopoverOnEscape(event) {
762
+ var _this$triggerTarget, _this$triggerTarget$f;
763
+ if (event.key !== 'Escape' || !this.openValue) return;
764
+ event.preventDefault();
765
+ event.stopPropagation();
766
+ this.closePopover();
767
+ (_this$triggerTarget = this.triggerTarget) === null || _this$triggerTarget === void 0 ? void 0 : (_this$triggerTarget$f = _this$triggerTarget.focus) === null || _this$triggerTarget$f === void 0 ? void 0 : _this$triggerTarget$f.call(_this$triggerTarget);
768
+ }
769
+ }, {
770
+ key: "markMessageFailedFromResponse",
771
+ value: function () {
772
+ var _markMessageFailedFromResponse = _asyncToGenerator(function* (response, element) {
773
+ var reason = yield this.messageFailureReason(response);
774
+ this.markMessageFailed(element, reason);
775
+ this.broadcastChannel.postMessage({
776
+ type: 'message:failed',
777
+ id: element.id,
778
+ reason
779
+ });
780
+ });
781
+ function markMessageFailedFromResponse(_x4, _x5) {
782
+ return _markMessageFailedFromResponse.apply(this, arguments);
783
+ }
784
+ return markMessageFailedFromResponse;
785
+ }()
786
+ }, {
787
+ key: "markMessageFailed",
788
+ value: function markMessageFailed(element, reason) {
789
+ if (!element) return;
790
+ element.classList.add('failed');
791
+ if (!reason) return;
792
+ var timestamp = element.querySelector('[data-message-timestamp]');
793
+ if (timestamp) {
794
+ timestamp.textContent = reason;
795
+ }
796
+ }
797
+ }, {
798
+ key: "localizeMessageTimestamps",
799
+ value: function localizeMessageTimestamps() {
800
+ var _root$matches, _root$querySelectorAl;
801
+ var root = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.element;
802
+ if (!root) return;
803
+ var timestamps = (_root$matches = root.matches) !== null && _root$matches !== void 0 && _root$matches.call(root, 'time[datetime][data-message-timestamp]') ? [root] : Array.from(((_root$querySelectorAl = root.querySelectorAll) === null || _root$querySelectorAl === void 0 ? void 0 : _root$querySelectorAl.call(root, 'time[datetime][data-message-timestamp]')) || []);
804
+ timestamps.forEach(timestamp => this.localizeMessageTimestamp(timestamp));
805
+ }
806
+ }, {
807
+ key: "localizeMessageTimestamp",
808
+ value: function localizeMessageTimestamp(timestamp) {
809
+ var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : timestamp === null || timestamp === void 0 ? void 0 : timestamp.getAttribute('datetime');
810
+ if (!timestamp || !value) return;
811
+ var date = value instanceof Date ? value : new Date(value);
812
+ if (Number.isNaN(date.getTime())) return;
813
+ timestamp.setAttribute('datetime', date.toISOString());
814
+ timestamp.textContent = this.formatMessageTimestamp(date);
815
+ }
816
+ }, {
817
+ key: "formatMessageTimestamp",
818
+ value: function formatMessageTimestamp(date) {
819
+ return this.constructor.messageTimestampFormatterFor(Locale.toString()).format(date);
820
+ }
821
+ }, {
822
+ key: "messageFailureReason",
823
+ value: function () {
824
+ var _messageFailureReason = _asyncToGenerator(function* (response) {
825
+ var nativeResponse = (response === null || response === void 0 ? void 0 : response.data) || (response === null || response === void 0 ? void 0 : response.response);
826
+ var fallback = (nativeResponse === null || nativeResponse === void 0 ? void 0 : nativeResponse.statusText) || 'Message failed';
827
+ try {
828
+ var _jsonResponse$json;
829
+ var jsonResponse = nativeResponse !== null && nativeResponse !== void 0 && nativeResponse.clone ? nativeResponse.clone() : nativeResponse;
830
+ var payload = yield jsonResponse === null || jsonResponse === void 0 ? void 0 : (_jsonResponse$json = jsonResponse.json) === null || _jsonResponse$json === void 0 ? void 0 : _jsonResponse$json.call(jsonResponse);
831
+ var reason = this.messageFailureReasonFromPayload(payload);
832
+ if (reason) return reason;
833
+ } catch (_) {
834
+ // Fall through to text parsing/fallback. Some stores strip content-type or
835
+ // return non-JSON failures, so the timestamp still needs a useful reason.
836
+ }
837
+ try {
838
+ var _textResponse$text;
839
+ var textResponse = nativeResponse !== null && nativeResponse !== void 0 && nativeResponse.clone ? nativeResponse.clone() : nativeResponse;
840
+ var text = yield textResponse === null || textResponse === void 0 ? void 0 : (_textResponse$text = textResponse.text) === null || _textResponse$text === void 0 ? void 0 : _textResponse$text.call(textResponse);
841
+ return this.messageFailureReasonFromText(text) || fallback;
842
+ } catch (_) {
843
+ return fallback;
844
+ }
845
+ });
846
+ function messageFailureReason(_x6) {
847
+ return _messageFailureReason.apply(this, arguments);
848
+ }
849
+ return messageFailureReason;
850
+ }()
851
+ }, {
852
+ key: "messageFailureReasonFromText",
853
+ value: function messageFailureReasonFromText(text) {
854
+ if (typeof text !== 'string') return null;
855
+ var value = text.trim();
856
+ if (!value || value.startsWith('<')) return null;
857
+ try {
858
+ return this.messageFailureReasonFromPayload(JSON.parse(value)) || value;
859
+ } catch (_) {
860
+ return value;
861
+ }
862
+ }
863
+ }, {
864
+ key: "messageFailureReasonFromPayload",
865
+ value: function messageFailureReasonFromPayload(payload) {
866
+ var _payload$error, _payload$errors, _payload$errors2, _payload$errors2$, _payload$errors3, _payload$errors3$;
867
+ if (!payload) return null;
868
+ return [(_payload$error = payload.error) === null || _payload$error === void 0 ? void 0 : _payload$error.message, payload.message, (_payload$errors = payload.errors) === null || _payload$errors === void 0 ? void 0 : _payload$errors.message, (_payload$errors2 = payload.errors) === null || _payload$errors2 === void 0 ? void 0 : (_payload$errors2$ = _payload$errors2[0]) === null || _payload$errors2$ === void 0 ? void 0 : _payload$errors2$.message, (_payload$errors3 = payload.errors) === null || _payload$errors3 === void 0 ? void 0 : (_payload$errors3$ = _payload$errors3[0]) === null || _payload$errors3$ === void 0 ? void 0 : _payload$errors3$.description].find(reason => typeof reason === 'string' && reason.trim().length > 0);
869
+ }
705
870
  }, {
706
871
  key: "messageAttachmentsContainer",
707
872
  value: function messageAttachmentsContainer(element) {
@@ -832,9 +997,28 @@ var _default = /*#__PURE__*/function (_Controller) {
832
997
  get: function get() {
833
998
  return window.matchMedia("(max-width: ".concat(this.fullScreenThresholdValue, "px)")).matches;
834
999
  }
1000
+ }], [{
1001
+ key: "messageTimestampFormatterFor",
1002
+ value: function messageTimestampFormatterFor(locale) {
1003
+ var key = locale || 'default';
1004
+ if (!this.messageTimestampFormatters[key]) {
1005
+ this.messageTimestampFormatters[key] = this.buildMessageTimestampFormatter(locale);
1006
+ }
1007
+ return this.messageTimestampFormatters[key];
1008
+ }
1009
+ }, {
1010
+ key: "buildMessageTimestampFormatter",
1011
+ value: function buildMessageTimestampFormatter(locale) {
1012
+ try {
1013
+ return new Intl.DateTimeFormat(locale || undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
1014
+ } catch (_) {
1015
+ return new Intl.DateTimeFormat(undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
1016
+ }
1017
+ }
835
1018
  }]);
836
1019
  return _default;
837
1020
  }(Controller);
1021
+ _default.messageTimestampFormatters = {};
838
1022
  _default.values = {
839
1023
  id: String,
840
1024
  conversationId: String,
@@ -39,6 +39,7 @@ let Configuration = /*#__PURE__*/function () {
39
39
  */
40
40
  function assign(props) {
41
41
  if (props) {
42
+ const shouldInferActionCableUrl = Object.prototype.hasOwnProperty.call(props, 'apiRoot') && !Object.prototype.hasOwnProperty.call(props, 'actionCableUrl');
42
43
  Object.entries(props).forEach(([key, value]) => {
43
44
  if (key === 'forms') {
44
45
  this.forms = _forms.Forms.assign(value);
@@ -48,6 +49,9 @@ let Configuration = /*#__PURE__*/function () {
48
49
  this[key] = value;
49
50
  }
50
51
  });
52
+ if (shouldInferActionCableUrl) {
53
+ this.actionCableUrl = this.actionCableUrlForApiRoot(this.apiRoot);
54
+ }
51
55
  }
52
56
  return this;
53
57
  }
@@ -64,6 +68,21 @@ let Configuration = /*#__PURE__*/function () {
64
68
  value: function endpoint(path) {
65
69
  return `${this.apiRoot}/${path}`;
66
70
  }
71
+ }, {
72
+ key: "actionCableUrlForApiRoot",
73
+ value: function actionCableUrlForApiRoot(apiRoot) {
74
+ try {
75
+ const url = new URL(apiRoot);
76
+ const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
77
+ url.protocol = protocol;
78
+ url.pathname = '/cable';
79
+ url.search = '';
80
+ url.hash = '';
81
+ return url.toString();
82
+ } catch (_) {
83
+ return this.actionCableUrl;
84
+ }
85
+ }
67
86
  }]);
68
87
  return Configuration;
69
88
  }();
@@ -34,6 +34,7 @@ var Configuration = /*#__PURE__*/function () {
34
34
  */
35
35
  function assign(props) {
36
36
  if (props) {
37
+ var shouldInferActionCableUrl = Object.prototype.hasOwnProperty.call(props, 'apiRoot') && !Object.prototype.hasOwnProperty.call(props, 'actionCableUrl');
37
38
  Object.entries(props).forEach(_ref => {
38
39
  var [key, value] = _ref;
39
40
  if (key === 'forms') {
@@ -44,6 +45,9 @@ var Configuration = /*#__PURE__*/function () {
44
45
  this[key] = value;
45
46
  }
46
47
  });
48
+ if (shouldInferActionCableUrl) {
49
+ this.actionCableUrl = this.actionCableUrlForApiRoot(this.apiRoot);
50
+ }
47
51
  }
48
52
  return this;
49
53
  }
@@ -60,6 +64,21 @@ var Configuration = /*#__PURE__*/function () {
60
64
  value: function endpoint(path) {
61
65
  return "".concat(this.apiRoot, "/").concat(path);
62
66
  }
67
+ }, {
68
+ key: "actionCableUrlForApiRoot",
69
+ value: function actionCableUrlForApiRoot(apiRoot) {
70
+ try {
71
+ var url = new URL(apiRoot);
72
+ var protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
73
+ url.protocol = protocol;
74
+ url.pathname = '/cable';
75
+ url.search = '';
76
+ url.hash = '';
77
+ return url.toString();
78
+ } catch (_) {
79
+ return this.actionCableUrl;
80
+ }
81
+ }
63
82
  }]);
64
83
  return Configuration;
65
84
  }();
@@ -12,17 +12,22 @@ function _defineProperties(target, props) { for (var i = 0; i < props.length; i+
12
12
  function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
13
13
  function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
14
14
  function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
15
+ const stylesheetAttribute = 'data-hellotext-stylesheet';
16
+ const stylesheetLoadTimeout = 10000;
17
+
15
18
  /**
16
19
  * @typedef {Object} BusinessCountry
17
20
  * @property {String} [code] - ISO country code configured for the business.
18
21
  * @property {String} [prefix] - Phone country prefix configured for the business.
19
22
  */
23
+
20
24
  /**
21
25
  * @typedef {Object} BusinessWebchat
22
26
  * @property {String} [id] - Dashboard webchat id configured for the business.
23
27
  * @property {Object} [appearance] - Dashboard appearance defaults for the webchat.
24
28
  * @property {Object} [whatsapp] - Dashboard WhatsApp handoff defaults for the webchat.
25
29
  */
30
+
26
31
  /**
27
32
  * Public business metadata returned by `public/businesses/:id`.
28
33
  *
@@ -36,6 +41,7 @@ function _toPrimitive(input, hint) { if (typeof input !== "object" || input ===
36
41
  * @property {String|Array<String>} [whitelist] - Domain whitelist configuration.
37
42
  * @property {String} [subscription] - Current business subscription tier.
38
43
  */
44
+
39
45
  /**
40
46
  * Public business context used by the SDK for tracking, forms, and webchat defaults.
41
47
  */
@@ -47,6 +53,8 @@ let Business = /*#__PURE__*/function () {
47
53
  _classCallCheck(this, Business);
48
54
  this.id = id;
49
55
  this.data = null;
56
+ this.stylesheet = null;
57
+ this.stylesheetLoaded = Promise.resolve(false);
50
58
  }
51
59
 
52
60
  /**
@@ -87,11 +95,12 @@ let Business = /*#__PURE__*/function () {
87
95
  key: "setData",
88
96
  value: function setData(data) {
89
97
  this.data = data;
90
- if (typeof document !== 'undefined') {
91
- const linkTag = document.createElement('link');
92
- linkTag.rel = 'stylesheet';
93
- linkTag.href = data.style_url;
94
- document.head.append(linkTag);
98
+ if (typeof document !== 'undefined' && data.style_url) {
99
+ this.stylesheet = this.constructor.ensureStylesheet(data.style_url);
100
+ this.stylesheetLoaded = this.constructor.waitForStylesheet(this.stylesheet);
101
+ } else {
102
+ this.stylesheet = null;
103
+ this.stylesheetLoaded = Promise.resolve(false);
95
104
  }
96
105
  }
97
106
  }, {
@@ -135,6 +144,78 @@ let Business = /*#__PURE__*/function () {
135
144
  get: function () {
136
145
  return this.data.features;
137
146
  }
147
+ }], [{
148
+ key: "stylesheetSelector",
149
+ get: function () {
150
+ return `link[rel="stylesheet"][${stylesheetAttribute}]`;
151
+ }
152
+ }, {
153
+ key: "ensureStylesheet",
154
+ value: function ensureStylesheet(styleUrl) {
155
+ const href = this.normalizedStylesheetUrl(styleUrl);
156
+ const existingLink = this.stylesheetLinks.find(link => link.href === href);
157
+ if (existingLink) {
158
+ existingLink.setAttribute(stylesheetAttribute, 'true');
159
+ return existingLink;
160
+ }
161
+ const linkTag = document.createElement('link');
162
+ linkTag.rel = 'stylesheet';
163
+ linkTag.href = styleUrl;
164
+ linkTag.setAttribute(stylesheetAttribute, 'true');
165
+ this.waitForStylesheet(linkTag);
166
+ document.head.append(linkTag);
167
+ return linkTag;
168
+ }
169
+ }, {
170
+ key: "stylesheetLinks",
171
+ get: function () {
172
+ if (typeof document === 'undefined') return [];
173
+ return Array.from(document.querySelectorAll(this.stylesheetSelector));
174
+ }
175
+ }, {
176
+ key: "latestStylesheet",
177
+ get: function () {
178
+ return this.stylesheetLinks[this.stylesheetLinks.length - 1];
179
+ }
180
+ }, {
181
+ key: "normalizedStylesheetUrl",
182
+ value: function normalizedStylesheetUrl(styleUrl) {
183
+ try {
184
+ return new URL(styleUrl, document.baseURI).href;
185
+ } catch (_error) {
186
+ return styleUrl;
187
+ }
188
+ }
189
+ }, {
190
+ key: "waitForStylesheet",
191
+ value: function waitForStylesheet(linkTag) {
192
+ if (!linkTag) return Promise.resolve(false);
193
+ if (this.stylesheetIsLoaded(linkTag)) return Promise.resolve(true);
194
+ if (linkTag.dataset.hellotextStylesheetLoaded === 'false') return Promise.resolve(false);
195
+ if (linkTag._hellotextStylesheetLoaded) return linkTag._hellotextStylesheetLoaded;
196
+ linkTag._hellotextStylesheetLoaded = new Promise(resolve => {
197
+ let timeout;
198
+ const finish = loaded => {
199
+ clearTimeout(timeout);
200
+ linkTag.removeEventListener('load', handleLoad);
201
+ linkTag.removeEventListener('error', handleError);
202
+ linkTag.dataset.hellotextStylesheetLoaded = loaded ? 'true' : 'false';
203
+ resolve(loaded);
204
+ };
205
+ const handleLoad = () => finish(this.stylesheetIsLoaded(linkTag));
206
+ const handleError = () => finish(false);
207
+ linkTag.addEventListener('load', handleLoad);
208
+ linkTag.addEventListener('error', handleError);
209
+ timeout = setTimeout(() => finish(this.stylesheetIsLoaded(linkTag)), stylesheetLoadTimeout);
210
+ if (timeout.unref) timeout.unref();
211
+ });
212
+ return linkTag._hellotextStylesheetLoaded;
213
+ }
214
+ }, {
215
+ key: "stylesheetIsLoaded",
216
+ value: function stylesheetIsLoaded(linkTag) {
217
+ return linkTag.dataset.hellotextStylesheetLoaded === 'true' || !!linkTag.sheet;
218
+ }
138
219
  }]);
139
220
  return Business;
140
221
  }();