@hellotext/hellotext 2.3.9 → 2.4.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.
Files changed (39) hide show
  1. package/dist/hellotext.js +1 -1
  2. package/dist/webchat-emoji-en.js +1 -0
  3. package/dist/webchat-emoji-es.js +1 -0
  4. package/dist/webchat-emoji.js +1 -0
  5. package/index.d.ts +30 -4
  6. package/lib/api/webchats.cjs +20 -0
  7. package/lib/api/webchats.js +20 -0
  8. package/lib/controllers/mixins/usePopover.cjs +7 -2
  9. package/lib/controllers/mixins/usePopover.js +7 -2
  10. package/lib/controllers/webchat/emoji_picker_controller.cjs +49 -10
  11. package/lib/controllers/webchat/emoji_picker_controller.js +66 -8
  12. package/lib/controllers/webchat/useBehaviour.cjs +74 -0
  13. package/lib/controllers/webchat/useBehaviour.js +67 -0
  14. package/lib/controllers/webchat/useOpeningSequence.cjs +103 -0
  15. package/lib/controllers/webchat/useOpeningSequence.js +96 -0
  16. package/lib/controllers/webchat/useTeaser.cjs +163 -0
  17. package/lib/controllers/webchat/useTeaser.js +156 -0
  18. package/lib/controllers/webchat_controller.cjs +165 -22
  19. package/lib/controllers/webchat_controller.js +173 -24
  20. package/lib/core/configuration/webchat.cjs +117 -42
  21. package/lib/core/configuration/webchat.js +121 -43
  22. package/lib/hellotext.cjs +28 -4
  23. package/lib/hellotext.js +28 -1
  24. package/lib/models/business.cjs +2 -0
  25. package/lib/models/business.js +2 -0
  26. package/lib/models/webchat.cjs +25 -0
  27. package/lib/models/webchat.js +25 -0
  28. package/package.json +1 -1
  29. package/src/api/webchats.js +17 -0
  30. package/src/controllers/mixins/usePopover.js +4 -2
  31. package/src/controllers/webchat/emoji_picker_controller.js +51 -9
  32. package/src/controllers/webchat/useBehaviour.js +81 -0
  33. package/src/controllers/webchat/useOpeningSequence.js +127 -0
  34. package/src/controllers/webchat/useTeaser.js +192 -0
  35. package/src/controllers/webchat_controller.js +187 -23
  36. package/src/core/configuration/webchat.js +127 -43
  37. package/src/hellotext.js +32 -4
  38. package/src/models/business.js +2 -0
  39. package/src/models/webchat.js +28 -0
@@ -33,13 +33,13 @@ var strategies = {
33
33
  };
34
34
 
35
35
  /**
36
- * @typedef {'modal' | 'popover'} Behaviour
36
+ * @typedef {'modal' | 'popover'} Mode
37
37
  */
38
38
 
39
39
  /**
40
- * @enum {Behaviour}
40
+ * @enum {Mode}
41
41
  */
42
- var behaviors = {
42
+ var modes = {
43
43
  MODAL: 'modal',
44
44
  POPOVER: 'popover'
45
45
  };
@@ -51,6 +51,20 @@ var behaviors = {
51
51
  * @property {string} typography - The typography style (e.g., font family).
52
52
  */
53
53
 
54
+ /**
55
+ * @typedef {Object} Appearance
56
+ * @property {Object} [header] - Header appearance overrides.
57
+ * @property {string} [header.name] - Business name shown in the webchat header.
58
+ * @property {Object} [launcher] - Launcher appearance overrides.
59
+ * @property {string} [launcher.iconUrl] - Image URL used for the launcher icon.
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} WhatsApp
64
+ * @property {string} [number] - WhatsApp number used by the handoff configuration.
65
+ * @property {boolean} [restrictToChannel] - Whether handoff should be restricted to the configured channel.
66
+ */
67
+
54
68
  /**
55
69
  * @class Webchat
56
70
  * @classdesc
@@ -58,10 +72,11 @@ var behaviors = {
58
72
  * @property {String} id - the id of the webchat.
59
73
  * @property {String} container - the container to append the webchat to, defaults to 'body'
60
74
  * @property {Placement} placement - the placement of the webchat within the container, defaults to "bottom-right".
61
- * @property {String} classes - additional classes to apply to the webchat popup.
62
- * @property {String} triggerClasses - additional classes to apply to the webchat popup trigger.
63
- * @property {Behaviour} behaviour - the behaviour of the webchat, defaults to 'popover'.
75
+ * @property {Mode} mode - how the webchat behaves while open, defaults to 'popover'.
76
+ * @property {Object} behaviour - runtime opening behaviour for the webchat.
64
77
  * @property {Style} style - the style of the webchat.
78
+ * @property {Appearance} appearance - appearance overrides.
79
+ * @property {WhatsApp} whatsapp - WhatsApp handoff overrides.
65
80
  * @property {Strategy} strategy - the strategy used to position the webchat. Defaults to 'absolute'
66
81
  */
67
82
  var Webchat = /*#__PURE__*/function () {
@@ -87,36 +102,6 @@ var Webchat = /*#__PURE__*/function () {
87
102
  }
88
103
  this._placement = value;
89
104
  }
90
- }, {
91
- key: "classes",
92
- get: function get() {
93
- if (typeof this._classes === 'string') {
94
- return this._classes.split(',').map(c => c.trim());
95
- } else {
96
- return this._classes;
97
- }
98
- },
99
- set: function set(value) {
100
- if (!Array.isArray(value) && typeof value !== 'string') {
101
- throw new Error('classes must be an array or a string');
102
- }
103
- this._classes = value;
104
- }
105
- }, {
106
- key: "triggerClasses",
107
- get: function get() {
108
- if (typeof this._triggerClasses === 'string') {
109
- return this._triggerClasses.split(',').map(c => c.trim());
110
- } else {
111
- return this._triggerClasses;
112
- }
113
- },
114
- set: function set(value) {
115
- if (!Array.isArray(value) && typeof value !== 'string') {
116
- throw new Error('triggerClasses must be an array or a string');
117
- }
118
- this._triggerClasses = value;
119
- }
120
105
  }, {
121
106
  key: "id",
122
107
  get: function get() {
@@ -153,17 +138,103 @@ var Webchat = /*#__PURE__*/function () {
153
138
  });
154
139
  this._style = value;
155
140
  }
141
+ }, {
142
+ key: "appearance",
143
+ get: function get() {
144
+ return this._appearance;
145
+ },
146
+ set: function set(value) {
147
+ if (!this.isPlainObject(value)) {
148
+ throw new Error('Appearance must be an object');
149
+ }
150
+ Object.entries(value).forEach(_ref2 => {
151
+ var [key, nestedValue] = _ref2;
152
+ if (!['header', 'launcher'].includes(key)) {
153
+ throw new Error("Invalid appearance property: ".concat(key));
154
+ }
155
+ if (!this.isPlainObject(nestedValue)) {
156
+ throw new Error("Appearance ".concat(key, " must be an object"));
157
+ }
158
+ Object.entries(nestedValue).forEach(_ref3 => {
159
+ var [nestedKey, propertyValue] = _ref3;
160
+ if (key === 'header' && nestedKey !== 'name') {
161
+ throw new Error("Invalid appearance header property: ".concat(nestedKey));
162
+ }
163
+ if (key === 'launcher' && nestedKey !== 'iconUrl') {
164
+ throw new Error("Invalid appearance launcher property: ".concat(nestedKey));
165
+ }
166
+ if (propertyValue == null) {
167
+ return;
168
+ }
169
+ if (typeof propertyValue !== 'string') {
170
+ throw new Error("Invalid appearance ".concat(key, ".").concat(nestedKey, " value: ").concat(propertyValue));
171
+ }
172
+ });
173
+ });
174
+ this._appearance = value;
175
+ }
176
+ }, {
177
+ key: "whatsapp",
178
+ get: function get() {
179
+ return this._whatsapp;
180
+ },
181
+ set: function set(value) {
182
+ if (!this.isPlainObject(value)) {
183
+ throw new Error('WhatsApp must be an object');
184
+ }
185
+ Object.entries(value).forEach(_ref4 => {
186
+ var [key, nestedValue] = _ref4;
187
+ if (!['number', 'restrictToChannel'].includes(key)) {
188
+ throw new Error("Invalid WhatsApp property: ".concat(key));
189
+ }
190
+ if (nestedValue == null) {
191
+ return;
192
+ }
193
+ if (key === 'number' && typeof nestedValue !== 'string') {
194
+ throw new Error("Invalid WhatsApp number value: ".concat(nestedValue));
195
+ }
196
+ if (key === 'restrictToChannel' && typeof nestedValue !== 'boolean') {
197
+ throw new Error("Invalid WhatsApp restrictToChannel value: ".concat(nestedValue));
198
+ }
199
+ });
200
+ this._whatsapp = value;
201
+ }
202
+ }, {
203
+ key: "mode",
204
+ get: function get() {
205
+ return this._mode;
206
+ },
207
+ set: function set(value) {
208
+ if (!Object.values(modes).includes(value)) {
209
+ throw new Error("Invalid mode value: ".concat(value));
210
+ }
211
+ this._mode = value;
212
+ }
156
213
  }, {
157
214
  key: "behaviour",
158
215
  get: function get() {
159
216
  return this._behaviour;
160
217
  },
161
218
  set: function set(value) {
162
- if (!Object.values(behaviors).includes(value)) {
219
+ if (value == null) {
220
+ this._behaviour = value;
221
+ return;
222
+ }
223
+ if (typeof value !== 'object' || Array.isArray(value)) {
163
224
  throw new Error("Invalid behaviour value: ".concat(value));
164
225
  }
165
226
  this._behaviour = value;
166
227
  }
228
+ }, {
229
+ key: "hasBehaviourOverride",
230
+ get: function get() {
231
+ return this._hasBehaviourOverride;
232
+ }
233
+ }, {
234
+ key: "behaviourOverride",
235
+ set: function set(value) {
236
+ this._hasBehaviourOverride = !!value;
237
+ }
167
238
  }, {
168
239
  key: "strategy",
169
240
  get: function get() {
@@ -182,8 +253,8 @@ var Webchat = /*#__PURE__*/function () {
182
253
  key: "assign",
183
254
  value: function assign(props) {
184
255
  if (props) {
185
- Object.entries(props).forEach(_ref2 => {
186
- var [key, value] = _ref2;
256
+ Object.entries(props).forEach(_ref5 => {
257
+ var [key, value] = _ref5;
187
258
  this[key] = value;
188
259
  });
189
260
  }
@@ -194,15 +265,22 @@ var Webchat = /*#__PURE__*/function () {
194
265
  value: function isHexOrRgba(value) {
195
266
  return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value) || /^rgba?\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3},?\s*(0|1|0?\.\d+)?\s*\)$/.test(value);
196
267
  }
268
+ }, {
269
+ key: "isPlainObject",
270
+ value: function isPlainObject(value) {
271
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
272
+ }
197
273
  }]);
198
274
  return Webchat;
199
275
  }();
200
276
  Webchat._id = void 0;
201
277
  Webchat._container = 'body';
202
278
  Webchat._placement = 'bottom-right';
203
- Webchat._classes = [];
204
- Webchat._triggerClasses = [];
205
279
  Webchat._style = {};
206
- Webchat._behaviour = behaviors.POPOVER;
280
+ Webchat._appearance = {};
281
+ Webchat._whatsapp = {};
282
+ Webchat._mode = modes.POPOVER;
283
+ Webchat._behaviour = null;
284
+ Webchat._hasBehaviourOverride = false;
207
285
  Webchat._strategy = null;
208
- export { behaviors, Webchat };
286
+ export { modes, Webchat };
package/lib/hellotext.cjs CHANGED
@@ -35,10 +35,9 @@ let Hellotext = /*#__PURE__*/function () {
35
35
  this.forms = new _models.FormCollection();
36
36
  this.query = new _models.Query();
37
37
  const businessData = await this.business.hydrate();
38
- const webchatConfig = config.webchat === false ? false : {
39
- ...(businessData && businessData.webchat || {}),
40
- ...(config.webchat || {})
41
- };
38
+ const webchatConfig = config.webchat === false ? false : this.mergeWebchatConfig(businessData && businessData.webchat || {}, config.webchat || {});
39
+ const hasExplicitBehaviourOverride = config.webchat && config.webchat !== false && Object.prototype.hasOwnProperty.call(config.webchat, 'behaviour');
40
+ _core.Configuration.webchat.behaviourOverride = hasExplicitBehaviourOverride;
42
41
  if (webchatConfig && webchatConfig.id) {
43
42
  _core.Configuration.webchat.assign(webchatConfig);
44
43
  this.webchat = await _models.Webchat.load(webchatConfig.id);
@@ -47,6 +46,31 @@ let Hellotext = /*#__PURE__*/function () {
47
46
  this.forms.collectExistingFormsOnPage();
48
47
  }
49
48
  }
49
+ }, {
50
+ key: "mergeWebchatConfig",
51
+ value: function mergeWebchatConfig(dashboardConfig, localConfig) {
52
+ return this.deepMergePlainObjects(dashboardConfig, localConfig);
53
+ }
54
+ }, {
55
+ key: "deepMergePlainObjects",
56
+ value: function deepMergePlainObjects(base, override) {
57
+ const result = {
58
+ ...base
59
+ };
60
+ Object.entries(override).forEach(([key, value]) => {
61
+ if (this.isPlainObject(value) && this.isPlainObject(result[key])) {
62
+ result[key] = this.deepMergePlainObjects(result[key], value);
63
+ } else {
64
+ result[key] = value;
65
+ }
66
+ });
67
+ return result;
68
+ }
69
+ }, {
70
+ key: "isPlainObject",
71
+ value: function isPlainObject(value) {
72
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
73
+ }
50
74
 
51
75
  /**
52
76
  * Tracks an action that has happened on the page
package/lib/hellotext.js CHANGED
@@ -34,7 +34,9 @@ var Hellotext = /*#__PURE__*/function () {
34
34
  this.forms = new FormCollection();
35
35
  this.query = new Query();
36
36
  var businessData = yield this.business.hydrate();
37
- var webchatConfig = config.webchat === false ? false : _objectSpread(_objectSpread({}, businessData && businessData.webchat || {}), config.webchat || {});
37
+ var webchatConfig = config.webchat === false ? false : this.mergeWebchatConfig(businessData && businessData.webchat || {}, config.webchat || {});
38
+ var hasExplicitBehaviourOverride = config.webchat && config.webchat !== false && Object.prototype.hasOwnProperty.call(config.webchat, 'behaviour');
39
+ Configuration.webchat.behaviourOverride = hasExplicitBehaviourOverride;
38
40
  if (webchatConfig && webchatConfig.id) {
39
41
  Configuration.webchat.assign(webchatConfig);
40
42
  this.webchat = yield Webchat.load(webchatConfig.id);
@@ -48,6 +50,31 @@ var Hellotext = /*#__PURE__*/function () {
48
50
  }
49
51
  return initialize;
50
52
  }()
53
+ }, {
54
+ key: "mergeWebchatConfig",
55
+ value: function mergeWebchatConfig(dashboardConfig, localConfig) {
56
+ return this.deepMergePlainObjects(dashboardConfig, localConfig);
57
+ }
58
+ }, {
59
+ key: "deepMergePlainObjects",
60
+ value: function deepMergePlainObjects(base, override) {
61
+ var result = _objectSpread({}, base);
62
+ Object.entries(override).forEach(_ref => {
63
+ var [key, value] = _ref;
64
+ if (this.isPlainObject(value) && this.isPlainObject(result[key])) {
65
+ result[key] = this.deepMergePlainObjects(result[key], value);
66
+ } else {
67
+ result[key] = value;
68
+ }
69
+ });
70
+ return result;
71
+ }
72
+ }, {
73
+ key: "isPlainObject",
74
+ value: function isPlainObject(value) {
75
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
76
+ }
77
+
51
78
  /**
52
79
  * Tracks an action that has happened on the page
53
80
  *
@@ -20,6 +20,8 @@ function _toPrimitive(input, hint) { if (typeof input !== "object" || input ===
20
20
  /**
21
21
  * @typedef {Object} BusinessWebchat
22
22
  * @property {String} [id] - Dashboard webchat id configured for the business.
23
+ * @property {Object} [appearance] - Dashboard appearance defaults for the webchat.
24
+ * @property {Object} [whatsapp] - Dashboard WhatsApp handoff defaults for the webchat.
23
25
  */
24
26
  /**
25
27
  * Public business metadata returned by `public/businesses/:id`.
@@ -17,6 +17,8 @@ import BusinessesAPI from '../api/businesses';
17
17
  /**
18
18
  * @typedef {Object} BusinessWebchat
19
19
  * @property {String} [id] - Dashboard webchat id configured for the business.
20
+ * @property {Object} [appearance] - Dashboard appearance defaults for the webchat.
21
+ * @property {Object} [whatsapp] - Dashboard WhatsApp handoff defaults for the webchat.
20
22
  */
21
23
 
22
24
  /**
@@ -21,8 +21,33 @@ let Webchat = /*#__PURE__*/function () {
21
21
  _createClass(Webchat, [{
22
22
  key: "render",
23
23
  value: function render() {
24
+ this.applyBehaviourOverride();
24
25
  this.containerToAppendTo.appendChild(this.data.html);
25
26
  }
27
+ }, {
28
+ key: "applyBehaviourOverride",
29
+ value: function applyBehaviourOverride() {
30
+ if (!_core.Configuration.webchat.hasBehaviourOverride || !_core.Configuration.webchat.behaviour) return;
31
+ this.data.html.setAttribute('data-hellotext--webchat-behaviour-value', JSON.stringify(this.serializedBehaviour));
32
+ }
33
+ }, {
34
+ key: "serializedBehaviour",
35
+ get: function () {
36
+ const behaviour = _core.Configuration.webchat.behaviour;
37
+ return {
38
+ trigger: this.serializeTrigger(behaviour.trigger),
39
+ delay_seconds: behaviour.delaySeconds,
40
+ first_visit_only: behaviour.firstVisitOnly,
41
+ once_per_session: behaviour.oncePerSession
42
+ };
43
+ }
44
+ }, {
45
+ key: "serializeTrigger",
46
+ value: function serializeTrigger(trigger) {
47
+ if (trigger === 'onLoad') return 'on_load';
48
+ if (trigger === 'onClick') return 'on_click';
49
+ return trigger;
50
+ }
26
51
  }, {
27
52
  key: "containerToAppendTo",
28
53
  get: function () {
@@ -16,8 +16,33 @@ var Webchat = /*#__PURE__*/function () {
16
16
  _createClass(Webchat, [{
17
17
  key: "render",
18
18
  value: function render() {
19
+ this.applyBehaviourOverride();
19
20
  this.containerToAppendTo.appendChild(this.data.html);
20
21
  }
22
+ }, {
23
+ key: "applyBehaviourOverride",
24
+ value: function applyBehaviourOverride() {
25
+ if (!Configuration.webchat.hasBehaviourOverride || !Configuration.webchat.behaviour) return;
26
+ this.data.html.setAttribute('data-hellotext--webchat-behaviour-value', JSON.stringify(this.serializedBehaviour));
27
+ }
28
+ }, {
29
+ key: "serializedBehaviour",
30
+ get: function get() {
31
+ var behaviour = Configuration.webchat.behaviour;
32
+ return {
33
+ trigger: this.serializeTrigger(behaviour.trigger),
34
+ delay_seconds: behaviour.delaySeconds,
35
+ first_visit_only: behaviour.firstVisitOnly,
36
+ once_per_session: behaviour.oncePerSession
37
+ };
38
+ }
39
+ }, {
40
+ key: "serializeTrigger",
41
+ value: function serializeTrigger(trigger) {
42
+ if (trigger === 'onLoad') return 'on_load';
43
+ if (trigger === 'onClick') return 'on_click';
44
+ return trigger;
45
+ }
21
46
  }, {
22
47
  key: "containerToAppendTo",
23
48
  get: function get() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellotext/hellotext",
3
- "version": "2.3.9",
3
+ "version": "2.4.0",
4
4
  "description": "Hellotext JavaScript Client",
5
5
  "source": "src/index.js",
6
6
  "main": "lib/index.cjs",
@@ -16,6 +16,8 @@ class WebchatsAPI {
16
16
  url.searchParams.append(`style[${key}]`, value)
17
17
  })
18
18
 
19
+ this.appendWebchatOverrides(url)
20
+
19
21
  url.searchParams.append('placement', Configuration.webchat.placement)
20
22
 
21
23
  const response = await fetch(url, {
@@ -32,6 +34,21 @@ class WebchatsAPI {
32
34
 
33
35
  return new DOMParser().parseFromString(data.html, 'text/html').querySelector('article')
34
36
  }
37
+
38
+ static appendWebchatOverrides(url) {
39
+ const { appearance, whatsapp } = Configuration.webchat
40
+
41
+ this.appendIfSupplied(url, 'webchat[appearance][header][name]', appearance.header?.name)
42
+ this.appendIfSupplied(url, 'webchat[appearance][launcher][icon_url]', appearance.launcher?.iconUrl)
43
+ this.appendIfSupplied(url, 'webchat[handoff][identifier]', whatsapp.number)
44
+ this.appendIfSupplied(url, 'webchat[handoff][restrict_to_channel]', whatsapp.restrictToChannel)
45
+ }
46
+
47
+ static appendIfSupplied(url, key, value) {
48
+ if (value === undefined || value === null) return
49
+
50
+ url.searchParams.append(key, String(value))
51
+ }
35
52
  }
36
53
 
37
54
  export default WebchatsAPI
@@ -4,20 +4,22 @@ import { Configuration } from '../../core'
4
4
  export const usePopover = controller => {
5
5
  Object.assign(controller, {
6
6
  show() {
7
+ this.cancelBehaviourOpen?.()
7
8
  this.openValue = true
8
9
  },
9
10
  hide() {
10
11
  this.openValue = false
11
12
  },
12
13
  toggle() {
14
+ this.cancelBehaviourOpen?.()
13
15
  this.openValue = !this.openValue
14
16
  },
15
- setupFloatingUI({ trigger, popover }) {
17
+ setupFloatingUI({ trigger, popover, strategy }) {
16
18
  this.floatingUICleanup = autoUpdate(trigger, popover, () => {
17
19
  computePosition(trigger, popover, {
18
20
  placement: this.placementValue,
19
21
  middleware: this.middlewares,
20
- strategy: Configuration.webchat.strategy,
22
+ strategy: strategy || Configuration.webchat.strategy,
21
23
  }).then(({ x, y, strategy }) => {
22
24
  const newStyle = {
23
25
  left: `${x}px`,
@@ -1,11 +1,6 @@
1
1
  import { autoPlacement, offset, shift } from '@floating-ui/dom'
2
2
  import { Controller } from '@hotwired/stimulus'
3
3
 
4
- import en from '@emoji-mart/data/i18n/en.json'
5
- import es from '@emoji-mart/data/i18n/es.json'
6
-
7
- import { Picker } from 'emoji-mart'
8
-
9
4
  import { usePopover } from '../mixins/usePopover'
10
5
 
11
6
  export default class extends Controller {
@@ -22,19 +17,29 @@ export default class extends Controller {
22
17
 
23
18
  initialize() {
24
19
  this.onEmojiSelect = this.onEmojiSelect.bind(this)
20
+ this.pickerLoaded = false
21
+ this.pickerLoadPromise = null
22
+ this.connected = false
25
23
  super.initialize()
26
24
  }
27
25
 
28
26
  connect() {
27
+ this.connected = true
28
+
29
29
  usePopover(this)
30
30
 
31
- this.setupFloatingUI({ trigger: this.buttonTarget, popover: this.popoverTarget })
32
- this.popoverTarget.appendChild(this.pickerObject)
31
+ this.setupFloatingUI({
32
+ trigger: this.buttonTarget,
33
+ popover: this.popoverTarget,
34
+ strategy: 'absolute',
35
+ })
33
36
 
34
37
  super.connect()
35
38
  }
36
39
 
37
40
  disconnect() {
41
+ this.connected = false
42
+ this.pickerLoadPromise = null
38
43
  this.floatingUICleanup()
39
44
  super.disconnect()
40
45
  }
@@ -53,7 +58,44 @@ export default class extends Controller {
53
58
  }
54
59
  }
55
60
 
56
- get pickerObject() {
61
+ async onPopoverOpened() {
62
+ await this.loadPicker()
63
+ }
64
+
65
+ async loadPicker() {
66
+ if (this.pickerLoaded) return
67
+
68
+ this.pickerLoadPromise ||= this.loadPickerDependencies()
69
+
70
+ const { Picker, i18n } = await this.pickerLoadPromise
71
+
72
+ if (!this.connected || this.pickerLoaded) return
73
+
74
+ this.popoverTarget.appendChild(this.buildPicker(Picker, i18n))
75
+ this.pickerLoaded = true
76
+ }
77
+
78
+ async loadPickerDependencies() {
79
+ const [pickerModule, i18nModule] = await Promise.all([
80
+ import(/* webpackChunkName: "webchat-emoji" */ 'emoji-mart'),
81
+ this.loadI18n(),
82
+ ])
83
+
84
+ return {
85
+ Picker: pickerModule.Picker,
86
+ i18n: i18nModule.default || i18nModule,
87
+ }
88
+ }
89
+
90
+ loadI18n() {
91
+ if (Hellotext.business.locale === 'es') {
92
+ return import(/* webpackChunkName: "webchat-emoji-es" */ '@emoji-mart/data/i18n/es.json')
93
+ }
94
+
95
+ return import(/* webpackChunkName: "webchat-emoji-en" */ '@emoji-mart/data/i18n/en.json')
96
+ }
97
+
98
+ buildPicker(Picker, i18n) {
57
99
  return new Picker({
58
100
  onEmojiSelect: this.onEmojiSelect,
59
101
  theme: 'light',
@@ -62,7 +104,7 @@ export default class extends Controller {
62
104
  skinTonePosition: 'none',
63
105
  emojiSize: this.sizeValue,
64
106
  perLine: this.perLineValue,
65
- i18n: Hellotext.business.locale === 'es' ? es : en,
107
+ i18n,
66
108
  })
67
109
  }
68
110
 
@@ -0,0 +1,81 @@
1
+ // This mixin owns automatic opening only. The webchat controller still owns the
2
+ // Stimulus lifecycle and `usePopover` still owns the mechanics of opening and
3
+ // closing the popover. Keeping this policy here gives the configured behaviour
4
+ // value one place to translate into timers, storage gates, and cleanup.
5
+ export const useBehaviour = controller => {
6
+ Object.assign(controller, {
7
+ // Called from `connect` after the controller has wired channels, targets,
8
+ // and popover state. Even a zero-second delay goes through a timeout so the
9
+ // automatic open happens after the mount pass rather than during setup.
10
+ scheduleBehaviourOpen() {
11
+ if (!this.shouldAutoOpenFromBehaviour()) return
12
+
13
+ const delay = Number(this.behaviourValue.delay_seconds || 0) * 1000
14
+
15
+ this.behaviourOpenTimeout = window.setTimeout(() => {
16
+ this.behaviourOpenTimeout = null
17
+
18
+ if (this.openValue) return
19
+
20
+ this.openValue = true
21
+ this.markBehaviourAutoOpened()
22
+ }, delay)
23
+ },
24
+
25
+ // This is the shared escape hatch for lifecycle teardown and manual user
26
+ // intent. `disconnect` calls it to avoid orphaned timers; `usePopover`
27
+ // calls it before show/toggle so a delayed behaviour cannot reopen the chat
28
+ // or consume gates after the user has already acted.
29
+ cancelBehaviourOpen() {
30
+ window.clearTimeout(this.behaviourOpenTimeout)
31
+ this.behaviourOpenTimeout = null
32
+ },
33
+
34
+ // Eligibility is deliberately read-only. A visit should only be marked as
35
+ // auto-opened after the timer opens the widget, not when we merely discover
36
+ // that it would be allowed. That keeps blocked, cancelled, and manually
37
+ // opened visits from being counted as behaviour-driven opens.
38
+ shouldAutoOpenFromBehaviour() {
39
+ const behaviour = this.behaviourValue
40
+
41
+ if (!behaviour || behaviour.trigger !== 'on_load') return false
42
+
43
+ if (behaviour.first_visit_only && localStorage.getItem(this.firstVisitKey())) {
44
+ return false
45
+ }
46
+
47
+ if (behaviour.once_per_session && sessionStorage.getItem(this.sessionKey())) {
48
+ return false
49
+ }
50
+
51
+ return true
52
+ },
53
+
54
+ // The gates are independent because they answer different product
55
+ // questions. `first_visit_only` is a long-lived visitor gate in
56
+ // localStorage; `once_per_session` is intentionally softer and resets with
57
+ // sessionStorage.
58
+ markBehaviourAutoOpened() {
59
+ if (this.behaviourValue.first_visit_only) {
60
+ localStorage.setItem(this.firstVisitKey(), '1')
61
+ }
62
+
63
+ if (this.behaviourValue.once_per_session) {
64
+ sessionStorage.setItem(this.sessionKey(), '1')
65
+ }
66
+ },
67
+
68
+ // Records that this widget has already used its long-lived first-visit
69
+ // automatic open. This is the durable gate behind `first_visit_only`.
70
+ firstVisitKey() {
71
+ return `hellotext--webchat--${this.idValue}--auto-opened`
72
+ },
73
+
74
+ // Records that this widget has already used its automatic open during the
75
+ // current browser session. This is the shorter-lived gate behind
76
+ // `once_per_session`.
77
+ sessionKey() {
78
+ return `hellotext--webchat--${this.idValue}--auto-opened-session`
79
+ },
80
+ })
81
+ }