@hellotext/hellotext 2.4.2 → 2.4.4

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.2",
3
+ "version": "2.4.4",
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
  }
@@ -60,7 +64,10 @@ export default class extends Controller {
60
64
  moveToLeft() {
61
65
  if (!this.hasCarouselContainerTarget) return
62
66
 
63
- const scrollAmount = this.getScrollAmount()
67
+ const nextScrollLeft = this.getPreviousPageScrollLeft()
68
+ const scrollAmount = this.carouselContainerTarget.scrollLeft - nextScrollLeft
69
+
70
+ if (scrollAmount < 1) return
64
71
 
65
72
  this.carouselContainerTarget.scrollBy({
66
73
  left: -scrollAmount,
@@ -71,7 +78,10 @@ export default class extends Controller {
71
78
  moveToRight() {
72
79
  if (!this.hasCarouselContainerTarget) return
73
80
 
74
- const scrollAmount = this.getScrollAmount()
81
+ const nextScrollLeft = this.getNextPageScrollLeft()
82
+ const scrollAmount = nextScrollLeft - this.carouselContainerTarget.scrollLeft
83
+
84
+ if (scrollAmount < 1) return
75
85
 
76
86
  this.carouselContainerTarget.scrollBy({
77
87
  left: scrollAmount,
@@ -80,7 +90,10 @@ export default class extends Controller {
80
90
  }
81
91
 
82
92
  getScrollAmount() {
83
- // Get the actual card width from DOM
93
+ return this.getCardScrollAmount()
94
+ }
95
+
96
+ getCardScrollAmount() {
84
97
  const firstCard = this.carouselContainerTarget.querySelector('.message__carousel_card')
85
98
 
86
99
  if (!firstCard) {
@@ -88,31 +101,141 @@ export default class extends Controller {
88
101
  }
89
102
 
90
103
  const cardWidth = firstCard.offsetWidth
91
- const gap = 16 // gap-x-4 = 1rem = 16px
92
104
 
93
- 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
94
196
  }
95
197
 
96
198
  updateFades() {
97
199
  if (!this.hasCarouselContainerTarget) return
98
200
 
99
- const scrollLeft = this.carouselContainerTarget.scrollLeft
100
- const maxScroll =
101
- this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth
201
+ const maxScroll = Math.max(this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth, 0)
102
202
 
103
- // Show left fade if scrolled past start
104
- if (scrollLeft > 0) {
105
- this.leftFadeTarget.classList.remove('hidden')
106
- } else {
107
- this.leftFadeTarget.classList.add('hidden')
203
+ if (maxScroll <= 1) {
204
+ this.hideFade(this.leftFadeTarget)
205
+ this.hideFade(this.rightFadeTarget)
206
+ return
108
207
  }
109
208
 
110
- // Show right fade if not at end
111
- if (scrollLeft < maxScroll - 1) {
112
- // -1 for rounding errors
113
- this.rightFadeTarget.classList.remove('hidden')
114
- } else {
115
- 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
116
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')
117
240
  }
118
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() {