@hellotext/hellotext 2.4.2 → 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.
@@ -30,6 +30,11 @@ function _possibleConstructorReturn(self, call) { if (call && (typeof call === "
30
30
  function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
31
31
  function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
32
32
  function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
33
+ const POPOVER_ANIMATION_DURATION = 120;
34
+ const MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
35
+ hour: 'numeric',
36
+ minute: '2-digit'
37
+ };
33
38
  let _default = /*#__PURE__*/function (_Controller) {
34
39
  _inherits(_default, _Controller);
35
40
  var _super = _createSuper(_default);
@@ -49,6 +54,7 @@ let _default = /*#__PURE__*/function (_Controller) {
49
54
  this.onTypingStart = this.onTypingStart.bind(this);
50
55
  this.onScroll = this.onScroll.bind(this);
51
56
  this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this);
57
+ this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this);
52
58
  this.broadcastChannel = new BroadcastChannel(`hellotext--webchat--${this.idValue}`);
53
59
  _get(_getPrototypeOf(_default.prototype), "initialize", this).call(this);
54
60
  }
@@ -72,6 +78,7 @@ let _default = /*#__PURE__*/function (_Controller) {
72
78
  }
73
79
  this.setupTeaser();
74
80
  this.setupOpeningSequence();
81
+ this.localizeMessageTimestamps();
75
82
  this.webChatChannel.onMessage(this.onMessageReceived);
76
83
  this.webChatChannel.onTypingStart(this.onTypingStart);
77
84
  this.webChatChannel.onReaction(this.onMessageReaction);
@@ -81,6 +88,7 @@ let _default = /*#__PURE__*/function (_Controller) {
81
88
  }
82
89
  _hellotext.default.eventEmitter.dispatch('webchat:mounted');
83
90
  this.broadcastChannel.addEventListener('message', this.onOutboundMessageSent);
91
+ window.addEventListener('keydown', this.closePopoverOnEscape, true);
84
92
  this.scheduleBehaviourOpen();
85
93
  _get(_getPrototypeOf(_default.prototype), "connect", this).call(this);
86
94
  }
@@ -88,10 +96,12 @@ let _default = /*#__PURE__*/function (_Controller) {
88
96
  key: "disconnect",
89
97
  value: function disconnect() {
90
98
  this.cancelBehaviourOpen();
99
+ this.clearPopoverOpenAnimation();
91
100
  this.teardownTeaser();
92
101
  this.teardownOpeningSequence();
93
102
  this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent);
94
103
  this.messagesContainerTarget.removeEventListener('scroll', this.onScroll);
104
+ window.removeEventListener('keydown', this.closePopoverOnEscape, true);
95
105
 
96
106
  // Clean up typing indicator timeouts
97
107
  this.clearTypingIndicator();
@@ -180,6 +190,7 @@ let _default = /*#__PURE__*/function (_Controller) {
180
190
  const callbacks = {
181
191
  'message:sent': data => {
182
192
  const element = new DOMParser().parseFromString(data.element, 'text/html').body.firstElementChild;
193
+ this.localizeMessageTimestamps(element);
183
194
 
184
195
  // Insert message before typing indicator if one exists
185
196
  if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
@@ -192,8 +203,8 @@ let _default = /*#__PURE__*/function (_Controller) {
192
203
  });
193
204
  },
194
205
  'message:failed': data => {
195
- var _this$messagesContain;
196
- (_this$messagesContain = this.messagesContainerTarget.querySelector(`#${data.id}`)) === null || _this$messagesContain === void 0 ? void 0 : _this$messagesContain.classList.add('failed');
206
+ const element = this.messagesContainerTarget.querySelector(`#${data.id}`);
207
+ this.markMessageFailed(element, data.reason);
197
208
  }
198
209
  };
199
210
  if (callbacks[data.type]) {
@@ -222,6 +233,7 @@ let _default = /*#__PURE__*/function (_Controller) {
222
233
  body,
223
234
  attachments
224
235
  } = message;
236
+ const createdAt = message.created_at || message.createdAt;
225
237
  const div = document.createElement('div');
226
238
  div.innerHTML = body;
227
239
  const element = this.messageTemplateTarget.cloneNode(true);
@@ -244,6 +256,7 @@ let _default = /*#__PURE__*/function (_Controller) {
244
256
  });
245
257
  }
246
258
  element.setAttribute('data-body', body);
259
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
247
260
  this.messagesContainerTarget.prepend(element);
248
261
  });
249
262
  this.messagesContainerTarget.scroll({
@@ -262,10 +275,30 @@ let _default = /*#__PURE__*/function (_Controller) {
262
275
  }, {
263
276
  key: "closePopover",
264
277
  value: function closePopover() {
265
- this.popoverTarget.classList.add(...this.fadeOutClasses);
266
- setTimeout(() => {
267
- this.openValue = false;
268
- }, 250);
278
+ this.clearPopoverOpenAnimation();
279
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
280
+ this.openValue = false;
281
+ }
282
+ }, {
283
+ key: "preparePopoverOpenAnimation",
284
+ value: function preparePopoverOpenAnimation() {
285
+ this.clearPopoverOpenAnimation();
286
+ this.popoverTarget.classList.remove(...this.fadeOutClasses);
287
+ this.popoverTarget.classList.add('hellotext--webchat-popover-opening');
288
+ this.popoverOpenAnimationTimeout = setTimeout(() => {
289
+ this.popoverTarget.classList.remove('hellotext--webchat-popover-opening');
290
+ this.popoverOpenAnimationTimeout = null;
291
+ }, POPOVER_ANIMATION_DURATION);
292
+ }
293
+ }, {
294
+ key: "clearPopoverOpenAnimation",
295
+ value: function clearPopoverOpenAnimation() {
296
+ var _this$popoverTarget;
297
+ if (this.popoverOpenAnimationTimeout) {
298
+ clearTimeout(this.popoverOpenAnimationTimeout);
299
+ this.popoverOpenAnimationTimeout = null;
300
+ }
301
+ (_this$popoverTarget = this.popoverTarget) === null || _this$popoverTarget === void 0 ? void 0 : _this$popoverTarget.classList.remove('hellotext--webchat-popover-opening');
269
302
  }
270
303
  }, {
271
304
  key: "onPopoverOpened",
@@ -299,6 +332,7 @@ let _default = /*#__PURE__*/function (_Controller) {
299
332
  }, {
300
333
  key: "onPopoverClosed",
301
334
  value: function onPopoverClosed() {
335
+ this.clearPopoverOpenAnimation();
302
336
  _hellotext.default.eventEmitter.dispatch('webchat:closed');
303
337
  localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'closed');
304
338
  }
@@ -336,6 +370,7 @@ let _default = /*#__PURE__*/function (_Controller) {
336
370
  attachments,
337
371
  teaser
338
372
  } = message;
373
+ const createdAt = message.created_at || message.createdAt;
339
374
  if (!this.claimMessageId(id)) return;
340
375
  (_this$hideTeaser = this.hideTeaser) === null || _this$hideTeaser === void 0 ? void 0 : _this$hideTeaser.call(this);
341
376
  if (message.carousel) {
@@ -348,6 +383,7 @@ let _default = /*#__PURE__*/function (_Controller) {
348
383
  element.querySelector('[data-body]').innerHTML = div.innerHTML;
349
384
  element.setAttribute('data-id', id);
350
385
  element.setAttribute('data-hellotext--webchat-target', 'message');
386
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt);
351
387
  if (attachments) {
352
388
  attachments.forEach(attachmentUrl => {
353
389
  var _this$messageAttachme2;
@@ -405,6 +441,7 @@ let _default = /*#__PURE__*/function (_Controller) {
405
441
  const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild;
406
442
  element.setAttribute('data-id', message.id);
407
443
  element.setAttribute('data-hellotext--webchat-target', 'message');
444
+ this.localizeMessageTimestamps(element);
408
445
  this.clearTypingIndicator();
409
446
  this.messagesContainerTarget.appendChild(element);
410
447
  element.scrollIntoView({
@@ -449,14 +486,14 @@ let _default = /*#__PURE__*/function (_Controller) {
449
486
  (_this$dismissTeaserFo2 = this.dismissTeaserForSession) === null || _this$dismissTeaserFo2 === void 0 ? void 0 : _this$dismissTeaserFo2.call(this);
450
487
  const formData = new FormData();
451
488
  formData.append('message[body]', body);
452
- formData.append('message[replied_to]', id);
453
- formData.append('message[product]', product);
454
- formData.append('message[button]', buttonId);
489
+ if (id) formData.append('message[replied_to]', id);
490
+ if (product) formData.append('message[product]', product);
491
+ if (buttonId) formData.append('message[button]', buttonId);
455
492
  formData.append('session', _hellotext.default.session);
456
493
  formData.append('locale', _core.Locale.toString());
457
494
  this.appendOpeningSequenceMessageIds(formData);
458
495
  const element = this.buildMessageElement();
459
- const attachment = (_cardElement$querySel = cardElement.querySelector('img')) === null || _cardElement$querySel === void 0 ? void 0 : _cardElement$querySel.cloneNode(true);
496
+ const attachment = cardElement === null || cardElement === void 0 ? void 0 : (_cardElement$querySel = cardElement.querySelector('img')) === null || _cardElement$querySel === void 0 ? void 0 : _cardElement$querySel.cloneNode(true);
460
497
  element.querySelector('[data-body]').innerText = body;
461
498
  if (attachment) {
462
499
  var _this$messageAttachme3;
@@ -480,17 +517,14 @@ let _default = /*#__PURE__*/function (_Controller) {
480
517
  if (response.failed) {
481
518
  // Clear the optimistic typing indicator on failure
482
519
  clearTimeout(this.optimisticTypingTimeout);
483
- this.broadcastChannel.postMessage({
484
- type: 'message:failed',
485
- id: element.id
486
- });
487
- return element.classList.add('failed');
520
+ return this.markMessageFailedFromResponse(response, element);
488
521
  }
489
522
  const data = await response.json();
490
523
  this.dispatch('set:id', {
491
524
  target: element,
492
525
  detail: data.id
493
526
  });
527
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
494
528
  this.clearRevealedOpeningSequenceMessageIds();
495
529
  const message = {
496
530
  id: data.id,
@@ -545,14 +579,11 @@ let _default = /*#__PURE__*/function (_Controller) {
545
579
  const response = await this.messagesAPI.create(formData);
546
580
  if (response.failed) {
547
581
  clearTimeout(this.optimisticTypingTimeout);
548
- this.broadcastChannel.postMessage({
549
- type: 'message:failed',
550
- id: element.id
551
- });
552
- return element.classList.add('failed');
582
+ return this.markMessageFailedFromResponse(response, element);
553
583
  }
554
584
  const data = await response.json();
555
585
  element.setAttribute('data-id', data.id);
586
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
556
587
  this.clearRevealedOpeningSequenceMessageIds();
557
588
  _hellotext.default.eventEmitter.dispatch('webchat:message:sent', {
558
589
  id: data.id,
@@ -648,15 +679,12 @@ let _default = /*#__PURE__*/function (_Controller) {
648
679
  if (response.failed) {
649
680
  // Clear the optimistic typing indicator on failure
650
681
  clearTimeout(this.optimisticTypingTimeout);
651
- this.broadcastChannel.postMessage({
652
- type: 'message:failed',
653
- id: element.id
654
- });
655
- return element.classList.add('failed');
682
+ return this.markMessageFailedFromResponse(response, element);
656
683
  }
657
684
  const data = await response.json();
658
685
  element.setAttribute('data-id', data.id);
659
686
  message.id = data.id;
687
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt);
660
688
  this.clearRevealedOpeningSequenceMessageIds();
661
689
  _hellotext.default.eventEmitter.dispatch('webchat:message:sent', message);
662
690
  if (data.conversation !== this.conversationIdValue) {
@@ -679,8 +707,131 @@ let _default = /*#__PURE__*/function (_Controller) {
679
707
  element.style.removeProperty('display');
680
708
  element.setAttribute('data-controller', 'hellotext--message');
681
709
  element.setAttribute('data-hellotext--webchat-target', 'message');
710
+ this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), new Date());
682
711
  return element;
683
712
  }
713
+ }, {
714
+ key: "focusCompose",
715
+ value: function focusCompose(event) {
716
+ const {
717
+ target
718
+ } = event;
719
+ const ignoredSelector = ['button', 'a', 'input', 'textarea', 'select', 'label', '[role="button"]', 'em-emoji-picker', '[data-hellotext--webchat--emoji-target~="popover"]', '[data-controller~="hellotext--webchat--emoji"]'].join(', ');
720
+ if (!this.hasInputTarget || target.closest(ignoredSelector)) return;
721
+ event.preventDefault();
722
+ this.inputTarget.focus();
723
+ if (typeof this.inputTarget.selectionStart === 'number') {
724
+ const position = this.inputTarget.value.length;
725
+ this.inputTarget.setSelectionRange(position, position);
726
+ }
727
+ }
728
+ }, {
729
+ key: "closePopoverFromHeader",
730
+ value: function closePopoverFromHeader(event) {
731
+ const {
732
+ target
733
+ } = event;
734
+ if (target.closest('.hellotext--webchat-header-channel-button, .hellotext--webchat-close-button')) return;
735
+ event.preventDefault();
736
+ this.closePopover();
737
+ }
738
+ }, {
739
+ key: "closePopoverOnEscape",
740
+ value: function closePopoverOnEscape(event) {
741
+ var _this$triggerTarget, _this$triggerTarget$f;
742
+ if (event.key !== 'Escape' || !this.openValue) return;
743
+ event.preventDefault();
744
+ event.stopPropagation();
745
+ this.closePopover();
746
+ (_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);
747
+ }
748
+ }, {
749
+ key: "markMessageFailedFromResponse",
750
+ value: async function markMessageFailedFromResponse(response, element) {
751
+ const reason = await this.messageFailureReason(response);
752
+ this.markMessageFailed(element, reason);
753
+ this.broadcastChannel.postMessage({
754
+ type: 'message:failed',
755
+ id: element.id,
756
+ reason
757
+ });
758
+ }
759
+ }, {
760
+ key: "markMessageFailed",
761
+ value: function markMessageFailed(element, reason) {
762
+ if (!element) return;
763
+ element.classList.add('failed');
764
+ if (!reason) return;
765
+ const timestamp = element.querySelector('[data-message-timestamp]');
766
+ if (timestamp) {
767
+ timestamp.textContent = reason;
768
+ }
769
+ }
770
+ }, {
771
+ key: "localizeMessageTimestamps",
772
+ value: function localizeMessageTimestamps(root = this.element) {
773
+ var _root$matches, _root$querySelectorAl;
774
+ if (!root) return;
775
+ const 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]')) || []);
776
+ timestamps.forEach(timestamp => this.localizeMessageTimestamp(timestamp));
777
+ }
778
+ }, {
779
+ key: "localizeMessageTimestamp",
780
+ value: function localizeMessageTimestamp(timestamp, value = timestamp === null || timestamp === void 0 ? void 0 : timestamp.getAttribute('datetime')) {
781
+ if (!timestamp || !value) return;
782
+ const date = value instanceof Date ? value : new Date(value);
783
+ if (Number.isNaN(date.getTime())) return;
784
+ timestamp.setAttribute('datetime', date.toISOString());
785
+ timestamp.textContent = this.formatMessageTimestamp(date);
786
+ }
787
+ }, {
788
+ key: "formatMessageTimestamp",
789
+ value: function formatMessageTimestamp(date) {
790
+ return this.constructor.messageTimestampFormatterFor(_core.Locale.toString()).format(date);
791
+ }
792
+ }, {
793
+ key: "messageFailureReason",
794
+ value: async function messageFailureReason(response) {
795
+ const nativeResponse = (response === null || response === void 0 ? void 0 : response.data) || (response === null || response === void 0 ? void 0 : response.response);
796
+ const fallback = (nativeResponse === null || nativeResponse === void 0 ? void 0 : nativeResponse.statusText) || 'Message failed';
797
+ try {
798
+ var _jsonResponse$json;
799
+ const jsonResponse = nativeResponse !== null && nativeResponse !== void 0 && nativeResponse.clone ? nativeResponse.clone() : nativeResponse;
800
+ const payload = await (jsonResponse === null || jsonResponse === void 0 ? void 0 : (_jsonResponse$json = jsonResponse.json) === null || _jsonResponse$json === void 0 ? void 0 : _jsonResponse$json.call(jsonResponse));
801
+ const reason = this.messageFailureReasonFromPayload(payload);
802
+ if (reason) return reason;
803
+ } catch (_) {
804
+ // Fall through to text parsing/fallback. Some stores strip content-type or
805
+ // return non-JSON failures, so the timestamp still needs a useful reason.
806
+ }
807
+ try {
808
+ var _textResponse$text;
809
+ const textResponse = nativeResponse !== null && nativeResponse !== void 0 && nativeResponse.clone ? nativeResponse.clone() : nativeResponse;
810
+ const text = await (textResponse === null || textResponse === void 0 ? void 0 : (_textResponse$text = textResponse.text) === null || _textResponse$text === void 0 ? void 0 : _textResponse$text.call(textResponse));
811
+ return this.messageFailureReasonFromText(text) || fallback;
812
+ } catch (_) {
813
+ return fallback;
814
+ }
815
+ }
816
+ }, {
817
+ key: "messageFailureReasonFromText",
818
+ value: function messageFailureReasonFromText(text) {
819
+ if (typeof text !== 'string') return null;
820
+ const value = text.trim();
821
+ if (!value || value.startsWith('<')) return null;
822
+ try {
823
+ return this.messageFailureReasonFromPayload(JSON.parse(value)) || value;
824
+ } catch (_) {
825
+ return value;
826
+ }
827
+ }
828
+ }, {
829
+ key: "messageFailureReasonFromPayload",
830
+ value: function messageFailureReasonFromPayload(payload) {
831
+ var _payload$error, _payload$errors, _payload$errors2, _payload$errors2$, _payload$errors3, _payload$errors3$;
832
+ if (!payload) return null;
833
+ 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);
834
+ }
684
835
  }, {
685
836
  key: "messageAttachmentsContainer",
686
837
  value: function messageAttachmentsContainer(element) {
@@ -809,10 +960,29 @@ let _default = /*#__PURE__*/function (_Controller) {
809
960
  get: function () {
810
961
  return window.matchMedia(`(max-width: ${this.fullScreenThresholdValue}px)`).matches;
811
962
  }
963
+ }], [{
964
+ key: "messageTimestampFormatterFor",
965
+ value: function messageTimestampFormatterFor(locale) {
966
+ const key = locale || 'default';
967
+ if (!this.messageTimestampFormatters[key]) {
968
+ this.messageTimestampFormatters[key] = this.buildMessageTimestampFormatter(locale);
969
+ }
970
+ return this.messageTimestampFormatters[key];
971
+ }
972
+ }, {
973
+ key: "buildMessageTimestampFormatter",
974
+ value: function buildMessageTimestampFormatter(locale) {
975
+ try {
976
+ return new Intl.DateTimeFormat(locale || undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
977
+ } catch (_) {
978
+ return new Intl.DateTimeFormat(undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS);
979
+ }
980
+ }
812
981
  }]);
813
982
  return _default;
814
983
  }(_stimulus.Controller);
815
984
  exports.default = _default;
985
+ _default.messageTimestampFormatters = {};
816
986
  _default.values = {
817
987
  id: String,
818
988
  conversationId: String,