@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.
@@ -7,6 +7,8 @@ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typ
7
7
  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); }
8
8
  import locales from '../locales';
9
9
  import BusinessesAPI from '../api/businesses';
10
+ var stylesheetAttribute = 'data-hellotext-stylesheet';
11
+ var stylesheetLoadTimeout = 10000;
10
12
 
11
13
  /**
12
14
  * @typedef {Object} BusinessCountry
@@ -46,6 +48,8 @@ var Business = /*#__PURE__*/function () {
46
48
  _classCallCheck(this, Business);
47
49
  this.id = id;
48
50
  this.data = null;
51
+ this.stylesheet = null;
52
+ this.stylesheetLoaded = Promise.resolve(false);
49
53
  }
50
54
 
51
55
  /**
@@ -91,11 +95,12 @@ var Business = /*#__PURE__*/function () {
91
95
  key: "setData",
92
96
  value: function setData(data) {
93
97
  this.data = data;
94
- if (typeof document !== 'undefined') {
95
- var linkTag = document.createElement('link');
96
- linkTag.rel = 'stylesheet';
97
- linkTag.href = data.style_url;
98
- 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);
99
104
  }
100
105
  }
101
106
  }, {
@@ -139,6 +144,78 @@ var Business = /*#__PURE__*/function () {
139
144
  get: function get() {
140
145
  return this.data.features;
141
146
  }
147
+ }], [{
148
+ key: "stylesheetSelector",
149
+ get: function get() {
150
+ return "link[rel=\"stylesheet\"][".concat(stylesheetAttribute, "]");
151
+ }
152
+ }, {
153
+ key: "ensureStylesheet",
154
+ value: function ensureStylesheet(styleUrl) {
155
+ var href = this.normalizedStylesheetUrl(styleUrl);
156
+ var existingLink = this.stylesheetLinks.find(link => link.href === href);
157
+ if (existingLink) {
158
+ existingLink.setAttribute(stylesheetAttribute, 'true');
159
+ return existingLink;
160
+ }
161
+ var 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 get() {
172
+ if (typeof document === 'undefined') return [];
173
+ return Array.from(document.querySelectorAll(this.stylesheetSelector));
174
+ }
175
+ }, {
176
+ key: "latestStylesheet",
177
+ get: function get() {
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
+ var timeout;
198
+ var 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
+ var handleLoad = () => finish(this.stylesheetIsLoaded(linkTag));
206
+ var 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
+ }
142
219
  }]);
143
220
  return Business;
144
221
  }();
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.Webchat = void 0;
7
7
  var _core = require("../core");
8
8
  var _api = _interopRequireDefault(require("../api"));
9
+ var _business = require("./business");
9
10
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
11
  function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
11
12
  function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
@@ -16,13 +17,20 @@ let Webchat = /*#__PURE__*/function () {
16
17
  function Webchat(data) {
17
18
  _classCallCheck(this, Webchat);
18
19
  this.data = data;
19
- this.render();
20
+ this.mounted = false;
21
+ this.rendered = Promise.resolve(false);
20
22
  }
21
23
  _createClass(Webchat, [{
22
24
  key: "render",
23
- value: function render() {
25
+ value: async function render() {
24
26
  this.applyBehaviourOverride();
27
+ if (!(await this.stylesheetLoaded)) {
28
+ console.warn('Hellotext webchat was not mounted because its stylesheet failed to load.');
29
+ return false;
30
+ }
25
31
  this.containerToAppendTo.appendChild(this.data.html);
32
+ this.mounted = true;
33
+ return true;
26
34
  }
27
35
  }, {
28
36
  key: "applyBehaviourOverride",
@@ -53,13 +61,20 @@ let Webchat = /*#__PURE__*/function () {
53
61
  get: function () {
54
62
  return document.querySelector(_core.Configuration.webchat.container);
55
63
  }
64
+ }, {
65
+ key: "stylesheetLoaded",
66
+ get: function () {
67
+ return _business.Business.waitForStylesheet(_business.Business.latestStylesheet);
68
+ }
56
69
  }], [{
57
70
  key: "load",
58
71
  value: async function load(id) {
59
- return new Webchat({
72
+ const webchat = new Webchat({
60
73
  id,
61
74
  html: await _api.default.webchats.get(id)
62
75
  });
76
+ webchat.rendered = webchat.render();
77
+ return webchat;
63
78
  }
64
79
  }]);
65
80
  return Webchat;
@@ -7,18 +7,32 @@ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typ
7
7
  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); }
8
8
  import { Configuration } from '../core';
9
9
  import API from '../api';
10
+ import { Business } from './business';
10
11
  var Webchat = /*#__PURE__*/function () {
11
12
  function Webchat(data) {
12
13
  _classCallCheck(this, Webchat);
13
14
  this.data = data;
14
- this.render();
15
+ this.mounted = false;
16
+ this.rendered = Promise.resolve(false);
15
17
  }
16
18
  _createClass(Webchat, [{
17
19
  key: "render",
18
- value: function render() {
19
- this.applyBehaviourOverride();
20
- this.containerToAppendTo.appendChild(this.data.html);
21
- }
20
+ value: function () {
21
+ var _render = _asyncToGenerator(function* () {
22
+ this.applyBehaviourOverride();
23
+ if (!(yield this.stylesheetLoaded)) {
24
+ console.warn('Hellotext webchat was not mounted because its stylesheet failed to load.');
25
+ return false;
26
+ }
27
+ this.containerToAppendTo.appendChild(this.data.html);
28
+ this.mounted = true;
29
+ return true;
30
+ });
31
+ function render() {
32
+ return _render.apply(this, arguments);
33
+ }
34
+ return render;
35
+ }()
22
36
  }, {
23
37
  key: "applyBehaviourOverride",
24
38
  value: function applyBehaviourOverride() {
@@ -48,14 +62,21 @@ var Webchat = /*#__PURE__*/function () {
48
62
  get: function get() {
49
63
  return document.querySelector(Configuration.webchat.container);
50
64
  }
65
+ }, {
66
+ key: "stylesheetLoaded",
67
+ get: function get() {
68
+ return Business.waitForStylesheet(Business.latestStylesheet);
69
+ }
51
70
  }], [{
52
71
  key: "load",
53
72
  value: function () {
54
73
  var _load = _asyncToGenerator(function* (id) {
55
- return new Webchat({
74
+ var webchat = new Webchat({
56
75
  id,
57
76
  html: yield API.webchats.get(id)
58
77
  });
78
+ webchat.rendered = webchat.render();
79
+ return webchat;
59
80
  });
60
81
  function load(_x) {
61
82
  return _load.apply(this, arguments);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellotext/hellotext",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "description": "Hellotext JavaScript Client",
5
5
  "source": "src/index.js",
6
6
  "main": "lib/index.cjs",
@@ -4,7 +4,9 @@ import Hellotext from '../hellotext'
4
4
 
5
5
  export default class extends Controller {
6
6
  static values = {
7
+ fadeDistance: { type: Number, default: 64 },
7
8
  id: String,
9
+ pageStartOffset: { type: Number, default: 0 },
8
10
  }
9
11
 
10
12
  static targets = ['carouselContainer', 'leftFade', 'rightFade', 'carouselCard']
@@ -24,14 +26,16 @@ export default class extends Controller {
24
26
 
25
27
  quickReply({ currentTarget }) {
26
28
  const card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]')
29
+ const messageElement = currentTarget.closest('[data-controller~="hellotext--message"]')
30
+ const body = currentTarget.dataset.text || currentTarget.textContent.trim()
27
31
 
28
32
  this.dispatch('quickReply', {
29
33
  detail: {
30
34
  id: this.idValue,
31
- product: card.dataset.id,
35
+ product: card?.dataset.id,
32
36
  buttonId: currentTarget.dataset.id,
33
- body: currentTarget.dataset.text,
34
- cardElement: card,
37
+ body,
38
+ cardElement: card || messageElement || currentTarget,
35
39
  },
36
40
  })
37
41
  }
@@ -39,25 +43,31 @@ export default class extends Controller {
39
43
  addToCart({ currentTarget }) {
40
44
  const card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]')
41
45
 
42
- const payload = {
46
+ const { id, reference, source } = card.dataset
47
+ const item = { product: id, quantity: 1 }
48
+
49
+ Hellotext.track('cart.added', { object_parameters: { items: [item] } })
50
+
51
+ Hellotext.eventEmitter.dispatch('cart.added', {
43
52
  object_parameters: {
44
53
  items: [
45
54
  {
46
- product: card.dataset.id,
47
- quantity: 1,
55
+ ...item,
56
+ ...(reference && { reference }),
57
+ ...(source && { source }),
48
58
  },
49
59
  ],
50
60
  },
51
- }
52
-
53
- Hellotext.track('cart.added', payload)
54
- Hellotext.eventEmitter.dispatch('cart.added', payload)
61
+ })
55
62
  }
56
63
 
57
64
  moveToLeft() {
58
65
  if (!this.hasCarouselContainerTarget) return
59
66
 
60
- const scrollAmount = this.getScrollAmount()
67
+ const nextScrollLeft = this.getPreviousPageScrollLeft()
68
+ const scrollAmount = this.carouselContainerTarget.scrollLeft - nextScrollLeft
69
+
70
+ if (scrollAmount < 1) return
61
71
 
62
72
  this.carouselContainerTarget.scrollBy({
63
73
  left: -scrollAmount,
@@ -68,7 +78,10 @@ export default class extends Controller {
68
78
  moveToRight() {
69
79
  if (!this.hasCarouselContainerTarget) return
70
80
 
71
- const scrollAmount = this.getScrollAmount()
81
+ const nextScrollLeft = this.getNextPageScrollLeft()
82
+ const scrollAmount = nextScrollLeft - this.carouselContainerTarget.scrollLeft
83
+
84
+ if (scrollAmount < 1) return
72
85
 
73
86
  this.carouselContainerTarget.scrollBy({
74
87
  left: scrollAmount,
@@ -77,7 +90,10 @@ export default class extends Controller {
77
90
  }
78
91
 
79
92
  getScrollAmount() {
80
- // Get the actual card width from DOM
93
+ return this.getCardScrollAmount()
94
+ }
95
+
96
+ getCardScrollAmount() {
81
97
  const firstCard = this.carouselContainerTarget.querySelector('.message__carousel_card')
82
98
 
83
99
  if (!firstCard) {
@@ -85,31 +101,141 @@ export default class extends Controller {
85
101
  }
86
102
 
87
103
  const cardWidth = firstCard.offsetWidth
88
- const gap = 16 // gap-x-4 = 1rem = 16px
89
104
 
90
- return cardWidth + gap
105
+ return cardWidth + this.getGap()
106
+ }
107
+
108
+ getPageScrollAmount() {
109
+ const scrollAmount = this.carouselContainerTarget.clientWidth - this.getGap()
110
+
111
+ return scrollAmount > 0 ? scrollAmount : this.getCardScrollAmount()
112
+ }
113
+
114
+ getNextPageScrollLeft() {
115
+ const currentScrollLeft = this.getCurrentScrollLeft()
116
+ const viewportRight = currentScrollLeft + this.carouselContainerTarget.clientWidth
117
+ const cardMetrics = this.getCardMetrics()
118
+ const nextCard = cardMetrics.find(card => card.end > viewportRight + 1)
119
+ const targetScrollLeft = nextCard
120
+ ? this.getPageAlignedScrollLeft(nextCard.start)
121
+ : currentScrollLeft + this.getPageScrollAmount()
122
+ const fallbackScrollLeft = currentScrollLeft + this.getPageScrollAmount()
123
+
124
+ return this.clampScrollLeft(targetScrollLeft > currentScrollLeft + 1 ? targetScrollLeft : fallbackScrollLeft)
125
+ }
126
+
127
+ getPreviousPageScrollLeft() {
128
+ const currentScrollLeft = this.getCurrentScrollLeft()
129
+
130
+ if (currentScrollLeft <= 1) return 0
131
+
132
+ const targetThreshold = Math.max(currentScrollLeft - this.getPageScrollAmount(), 0)
133
+
134
+ if (targetThreshold <= 1) return 0
135
+
136
+ const cardMetrics = this.getCardMetrics()
137
+ const previousPageCard = cardMetrics.find(card => card.start >= targetThreshold - 1 && card.start < currentScrollLeft - 1)
138
+ const previousCard = [...cardMetrics].reverse().find(card => card.start < currentScrollLeft - 1)
139
+
140
+ return this.getPageAlignedScrollLeft(previousPageCard?.start ?? previousCard?.start ?? 0)
141
+ }
142
+
143
+ getPageAlignedScrollLeft(cardStart) {
144
+ const pageStartOffset = this.getPageStartOffset()
145
+
146
+ if (cardStart <= pageStartOffset + 1) return 0
147
+
148
+ return this.clampScrollLeft(cardStart - pageStartOffset)
149
+ }
150
+
151
+ getCardMetrics() {
152
+ return Array.from(this.carouselContainerTarget.querySelectorAll('.message__carousel_card')).map(card => {
153
+ const start = this.getCardScrollLeft(card)
154
+
155
+ return {
156
+ start,
157
+ end: start + card.offsetWidth,
158
+ }
159
+ })
160
+ }
161
+
162
+ getCardScrollLeft(card) {
163
+ const cardRect = card.getBoundingClientRect()
164
+ const containerRect = this.carouselContainerTarget.getBoundingClientRect()
165
+
166
+ if (cardRect.left || cardRect.width || containerRect.left || containerRect.width) {
167
+ return cardRect.left - containerRect.left + this.carouselContainerTarget.scrollLeft
168
+ }
169
+
170
+ return card.offsetLeft || 0
171
+ }
172
+
173
+ getCurrentScrollLeft() {
174
+ return this.clampScrollLeft(this.carouselContainerTarget.scrollLeft)
175
+ }
176
+
177
+ clampScrollLeft(scrollLeft) {
178
+ const maxScroll = Math.max(this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth, 0)
179
+
180
+ return Math.min(Math.max(scrollLeft, 0), maxScroll)
181
+ }
182
+
183
+ getGap() {
184
+ const styles = window.getComputedStyle(this.carouselContainerTarget)
185
+ const gap = Number.parseFloat(styles.columnGap || styles.gap)
186
+
187
+ return Number.isFinite(gap) ? gap : 16
188
+ }
189
+
190
+ getFadeDistance() {
191
+ return Number.isFinite(this.fadeDistanceValue) ? this.fadeDistanceValue : 64
192
+ }
193
+
194
+ getPageStartOffset() {
195
+ return Number.isFinite(this.pageStartOffsetValue) ? this.pageStartOffsetValue : 0
91
196
  }
92
197
 
93
198
  updateFades() {
94
199
  if (!this.hasCarouselContainerTarget) return
95
200
 
96
- const scrollLeft = this.carouselContainerTarget.scrollLeft
97
- const maxScroll =
98
- this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth
201
+ const maxScroll = Math.max(this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth, 0)
99
202
 
100
- // Show left fade if scrolled past start
101
- if (scrollLeft > 0) {
102
- this.leftFadeTarget.classList.remove('hidden')
103
- } else {
104
- this.leftFadeTarget.classList.add('hidden')
203
+ if (maxScroll <= 1) {
204
+ this.hideFade(this.leftFadeTarget)
205
+ this.hideFade(this.rightFadeTarget)
206
+ return
105
207
  }
106
208
 
107
- // Show right fade if not at end
108
- if (scrollLeft < maxScroll - 1) {
109
- // -1 for rounding errors
110
- this.rightFadeTarget.classList.remove('hidden')
111
- } else {
112
- this.rightFadeTarget.classList.add('hidden')
209
+ const scrollLeft = Math.min(Math.max(this.carouselContainerTarget.scrollLeft, 0), maxScroll)
210
+
211
+ const fadeDistance = this.getFadeDistance()
212
+
213
+ this.setFadeOpacity(this.leftFadeTarget, scrollLeft / fadeDistance)
214
+ this.setFadeOpacity(this.rightFadeTarget, (maxScroll - scrollLeft) / fadeDistance)
215
+ }
216
+
217
+ setFadeOpacity(fadeTarget, opacity) {
218
+ const clampedOpacity = Math.min(Math.max(opacity, 0), 1)
219
+
220
+ if (clampedOpacity <= 0.05) {
221
+ this.hideFade(fadeTarget)
222
+ return
113
223
  }
224
+
225
+ fadeTarget.classList.remove('hidden')
226
+ fadeTarget.removeAttribute('disabled')
227
+ fadeTarget.removeAttribute('tabindex')
228
+ fadeTarget.setAttribute('aria-hidden', 'false')
229
+ fadeTarget.style.opacity = clampedOpacity.toFixed(3)
230
+ fadeTarget.style.pointerEvents = 'auto'
231
+ }
232
+
233
+ hideFade(fadeTarget) {
234
+ fadeTarget.style.opacity = '0'
235
+ fadeTarget.style.pointerEvents = 'none'
236
+ fadeTarget.setAttribute('aria-hidden', 'true')
237
+ fadeTarget.setAttribute('tabindex', '-1')
238
+ if ('disabled' in fadeTarget) fadeTarget.disabled = true
239
+ fadeTarget.classList.add('hidden')
114
240
  }
115
241
  }
@@ -35,6 +35,7 @@ export const usePopover = controller => {
35
35
  if (this.disabledValue) return
36
36
 
37
37
  if (this.openValue) {
38
+ this.preparePopoverOpenAnimation?.()
38
39
  this.popoverTarget.showPopover()
39
40
  this.popoverTarget.setAttribute('aria-expanded', 'true')
40
41
 
@@ -141,7 +141,10 @@ export const useTeaser = controller => {
141
141
  },
142
142
 
143
143
  teaserSeenKey() {
144
- return `hellotext:webchat:${this.idValue || this.element.id}:teaser-seen`
144
+ const teaserVersion = this.hasTeaserTarget ? this.teaserTarget.dataset.teaserVersion : ''
145
+ const versionSegment = teaserVersion ? `:${teaserVersion}` : ''
146
+
147
+ return `hellotext:webchat:${this.idValue || this.element.id}:teaser-seen${versionSegment}`
145
148
  },
146
149
 
147
150
  teaserSeenForSession() {