@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.
- package/dist/hellotext.js +1 -1
- package/dist/webchat-emoji-en.js +1 -0
- package/dist/webchat-emoji-es.js +1 -0
- package/dist/webchat-emoji.js +1 -0
- package/index.d.ts +30 -4
- package/lib/api/webchats.cjs +20 -0
- package/lib/api/webchats.js +20 -0
- package/lib/controllers/mixins/usePopover.cjs +7 -2
- package/lib/controllers/mixins/usePopover.js +7 -2
- package/lib/controllers/webchat/emoji_picker_controller.cjs +49 -10
- package/lib/controllers/webchat/emoji_picker_controller.js +66 -8
- package/lib/controllers/webchat/useBehaviour.cjs +74 -0
- package/lib/controllers/webchat/useBehaviour.js +67 -0
- package/lib/controllers/webchat/useOpeningSequence.cjs +103 -0
- package/lib/controllers/webchat/useOpeningSequence.js +96 -0
- package/lib/controllers/webchat/useTeaser.cjs +163 -0
- package/lib/controllers/webchat/useTeaser.js +156 -0
- package/lib/controllers/webchat_controller.cjs +165 -22
- package/lib/controllers/webchat_controller.js +173 -24
- package/lib/core/configuration/webchat.cjs +117 -42
- package/lib/core/configuration/webchat.js +121 -43
- package/lib/hellotext.cjs +28 -4
- package/lib/hellotext.js +28 -1
- package/lib/models/business.cjs +2 -0
- package/lib/models/business.js +2 -0
- package/lib/models/webchat.cjs +25 -0
- package/lib/models/webchat.js +25 -0
- package/package.json +1 -1
- package/src/api/webchats.js +17 -0
- package/src/controllers/mixins/usePopover.js +4 -2
- package/src/controllers/webchat/emoji_picker_controller.js +51 -9
- package/src/controllers/webchat/useBehaviour.js +81 -0
- package/src/controllers/webchat/useOpeningSequence.js +127 -0
- package/src/controllers/webchat/useTeaser.js +192 -0
- package/src/controllers/webchat_controller.js +187 -23
- package/src/core/configuration/webchat.js +127 -43
- package/src/hellotext.js +32 -4
- package/src/models/business.js +2 -0
- package/src/models/webchat.js +28 -0
|
@@ -33,13 +33,13 @@ var strategies = {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* @typedef {'modal' | 'popover'}
|
|
36
|
+
* @typedef {'modal' | 'popover'} Mode
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* @enum {
|
|
40
|
+
* @enum {Mode}
|
|
41
41
|
*/
|
|
42
|
-
var
|
|
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 {
|
|
62
|
-
* @property {
|
|
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 (
|
|
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(
|
|
186
|
-
var [key, value] =
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
40
|
-
|
|
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 :
|
|
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
|
*
|
package/lib/models/business.cjs
CHANGED
|
@@ -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`.
|
package/lib/models/business.js
CHANGED
|
@@ -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
|
/**
|
package/lib/models/webchat.cjs
CHANGED
|
@@ -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 () {
|
package/lib/models/webchat.js
CHANGED
|
@@ -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
package/src/api/webchats.js
CHANGED
|
@@ -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({
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|