@hellotext/hellotext 2.1.1 → 2.1.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.
Files changed (44) hide show
  1. package/dist/hellotext.js +1 -1
  2. package/lib/api/forms.cjs +2 -1
  3. package/lib/api/forms.js +2 -1
  4. package/lib/api/webchat/messages.cjs +4 -3
  5. package/lib/api/webchat/messages.js +4 -2
  6. package/lib/api/webchats.cjs +2 -0
  7. package/lib/api/webchats.js +3 -1
  8. package/lib/builders/logo_builder.cjs +1 -1
  9. package/lib/builders/logo_builder.js +1 -1
  10. package/lib/controllers/webchat/emoji_picker_controller.cjs +6 -6
  11. package/lib/controllers/webchat/emoji_picker_controller.js +5 -14
  12. package/lib/controllers/webchat_controller.cjs +63 -15
  13. package/lib/controllers/webchat_controller.js +63 -15
  14. package/lib/core/configuration/locale.cjs +87 -0
  15. package/lib/core/configuration/locale.js +81 -0
  16. package/lib/core/configuration.cjs +10 -0
  17. package/lib/core/configuration.js +10 -0
  18. package/lib/core/index.cjs +15 -1
  19. package/lib/core/index.js +4 -2
  20. package/lib/locales/en.cjs +1 -1
  21. package/lib/locales/en.js +1 -1
  22. package/lib/locales/es.cjs +1 -1
  23. package/lib/locales/es.js +1 -1
  24. package/lib/models/business.cjs +11 -0
  25. package/lib/models/business.js +11 -0
  26. package/lib/models/form_collection.cjs +1 -0
  27. package/lib/models/form_collection.js +2 -1
  28. package/lib/models/index.cjs +4 -4
  29. package/lib/models/index.js +4 -4
  30. package/package.json +2 -1
  31. package/src/api/forms.js +3 -1
  32. package/src/api/webchat/messages.js +8 -6
  33. package/src/api/webchats.js +3 -1
  34. package/src/builders/logo_builder.js +1 -1
  35. package/src/controllers/webchat/emoji_picker_controller.js +9 -15
  36. package/src/controllers/webchat_controller.js +69 -19
  37. package/src/core/configuration/locale.js +50 -0
  38. package/src/core/configuration.js +10 -0
  39. package/src/core/index.js +3 -1
  40. package/src/locales/en.js +4 -4
  41. package/src/locales/es.js +4 -4
  42. package/src/models/business.js +12 -0
  43. package/src/models/form_collection.js +2 -1
  44. package/src/models/index.js +3 -3
@@ -5,6 +5,7 @@ import WebchatMessagesAPI from '../api/webchat/messages'
5
5
  import WebchatChannel from '../channels/webchat_channel'
6
6
  import Hellotext from '../hellotext'
7
7
 
8
+ import { Locale } from '../core'
8
9
  import { Webchat as WebchatConfiguration, behaviors } from '../core/configuration/webchat'
9
10
 
10
11
  import { LogoBuilder } from '../builders/logo_builder'
@@ -21,8 +22,11 @@ export default class extends Controller {
21
22
  autoPlacement: { type: Boolean, default: false },
22
23
  disabled: { type: Boolean, default: false },
23
24
  nextPage: { type: Number, default: undefined },
25
+ fullScreenThreshold: { type: Number, default: 1024 },
24
26
  }
25
27
 
28
+ static classes = ['fadeOut']
29
+
26
30
  static targets = [
27
31
  'trigger',
28
32
  'popover',
@@ -88,7 +92,7 @@ export default class extends Controller {
88
92
  this.toolbarTarget.appendChild(LogoBuilder.build())
89
93
  }
90
94
 
91
- if (localStorage.getItem(`hellotext--webchat--${this.idValue}`) === 'opened') {
95
+ if (this.shouldOpenOnMount) {
92
96
  this.openValue = true
93
97
  }
94
98
 
@@ -205,20 +209,31 @@ export default class extends Controller {
205
209
  }
206
210
  }
207
211
 
212
+ closePopover() {
213
+ this.popoverTarget.classList.add(...this.fadeOutClasses)
214
+
215
+ setTimeout(() => {
216
+ this.openValue = false
217
+ }, 250)
218
+ }
219
+
208
220
  onPopoverOpened() {
209
- this.inputTarget.focus()
221
+ if (!this.onMobile) {
222
+ this.inputTarget.focus()
223
+ }
210
224
 
211
225
  if (!this.scrolled) {
212
- this.messagesContainerTarget.scroll({
213
- top: this.messagesContainerTarget.scrollHeight,
214
- behavior: 'instant',
226
+ requestAnimationFrame(() => {
227
+ this.messagesContainerTarget.scroll({
228
+ top: this.messagesContainerTarget.scrollHeight,
229
+ behavior: 'instant',
230
+ })
215
231
  })
216
232
 
217
233
  this.scrolled = true
218
234
  }
219
235
 
220
236
  Hellotext.eventEmitter.dispatch('webchat:opened')
221
-
222
237
  localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'opened')
223
238
 
224
239
  if (this.unreadCounterTarget.style.display === 'none') return
@@ -231,11 +246,6 @@ export default class extends Controller {
231
246
 
232
247
  onPopoverClosed() {
233
248
  Hellotext.eventEmitter.dispatch('webchat:closed')
234
-
235
- setTimeout(() => {
236
- this.inputTarget.value = ''
237
- })
238
-
239
249
  localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'closed')
240
250
  }
241
251
 
@@ -264,7 +274,7 @@ export default class extends Controller {
264
274
  }
265
275
 
266
276
  onMessageReceived(message) {
267
- const { body, attachments } = message
277
+ const { id, body, attachments } = message
268
278
 
269
279
  const div = document.createElement('div')
270
280
  div.innerHTML = body
@@ -295,7 +305,10 @@ export default class extends Controller {
295
305
  element.scrollIntoView({ behavior: 'smooth' })
296
306
  this.setOfflineTimeout()
297
307
 
298
- if (this.openValue) return
308
+ if (this.openValue) {
309
+ this.messagesAPI.markAsSeen(id)
310
+ return
311
+ }
299
312
 
300
313
  this.unreadCounterTarget.style.display = 'flex'
301
314
 
@@ -328,13 +341,22 @@ export default class extends Controller {
328
341
  attachments: this.files,
329
342
  }
330
343
 
331
- formData.append('message[body]', this.inputTarget.value)
344
+ if (this.inputTarget.value.trim().length === 0 && this.files.length === 0) {
345
+ return
346
+ }
347
+
348
+ if (this.inputTarget.value.trim().length > 0) {
349
+ formData.append('message[body]', this.inputTarget.value)
350
+ } else {
351
+ delete message.body
352
+ }
332
353
 
333
354
  this.files.forEach(file => {
334
355
  formData.append('message[attachments][]', file)
335
356
  })
336
357
 
337
358
  formData.append('session', Hellotext.session)
359
+ formData.append('locale', Locale.toString())
338
360
 
339
361
  const element = this.messageTemplateTarget.cloneNode(true)
340
362
 
@@ -344,7 +366,12 @@ export default class extends Controller {
344
366
  element.style.removeProperty('display')
345
367
 
346
368
  element.setAttribute('data-hellotext--webchat-target', 'message')
347
- element.querySelector('[data-body]').innerText = this.inputTarget.value
369
+
370
+ if (this.inputTarget.value.trim().length > 0) {
371
+ element.querySelector('[data-body]').innerText = this.inputTarget.value
372
+ } else {
373
+ element.querySelector('[data-message-bubble]').remove()
374
+ }
348
375
 
349
376
  const attachments = this.attachmentContainerTarget.querySelectorAll('img')
350
377
 
@@ -364,6 +391,10 @@ export default class extends Controller {
364
391
 
365
392
  this.inputTarget.value = ''
366
393
  this.files = []
394
+
395
+ this.attachmentInputTarget.value = ''
396
+ this.attachmentContainerTarget.innerHTML = ''
397
+
367
398
  this.attachmentContainerTarget.style.display = 'none'
368
399
  this.errorMessageContainerTarget.style.display = 'none'
369
400
 
@@ -401,9 +432,10 @@ export default class extends Controller {
401
432
  onFileInputChange() {
402
433
  this.errorMessageContainerTarget.style.display = 'none'
403
434
 
404
- this.files = Array.from(this.attachmentInputTarget.files)
435
+ const newFiles = Array.from(this.attachmentInputTarget.files)
436
+ this.attachmentInputTarget.value = ''
405
437
 
406
- const fileMaxSizeTooMuch = this.files.find(file => {
438
+ const fileMaxSizeTooMuch = newFiles.find(file => {
407
439
  const type = file.type.split('/')[0]
408
440
 
409
441
  if (['image', 'video', 'audio'].includes(type)) {
@@ -421,11 +453,14 @@ export default class extends Controller {
421
453
  '%{limit}',
422
454
  this.byteToMegabyte(this.mediaValue[mediaType].max_size),
423
455
  )
424
- return
456
+
457
+ return (this.errorMessageContainerTarget.style.display = 'block')
425
458
  }
426
459
 
460
+ this.files = [...this.files, ...newFiles]
427
461
  this.errorMessageContainerTarget.innerText = ''
428
- this.files.forEach(file => this.createAttachmentElement(file))
462
+
463
+ newFiles.forEach(file => this.createAttachmentElement(file))
429
464
  this.inputTarget.focus()
430
465
  }
431
466
 
@@ -448,12 +483,16 @@ export default class extends Controller {
448
483
  this.attachmentContainerTarget.style.display = 'flex'
449
484
  } else {
450
485
  const main = element.querySelector('main')
486
+
451
487
  main.style.height = '5rem'
452
488
  main.style.borderRadius = '0.375rem'
453
489
  main.style.backgroundColor = '#e5e7eb'
454
490
  main.style.padding = '0.25rem'
455
491
 
456
492
  element.querySelector('p[data-attachment-name]').innerText = file.name
493
+
494
+ this.attachmentContainerTarget.appendChild(element)
495
+ this.attachmentContainerTarget.style.display = 'flex'
457
496
  }
458
497
  }
459
498
 
@@ -461,6 +500,7 @@ export default class extends Controller {
461
500
  const attachment = currentTarget.closest("[data-hellotext--webchat-target='attachment']")
462
501
 
463
502
  this.files = this.files.filter(file => file.name !== attachment.dataset.name)
503
+ this.attachmentInputTarget.value = ''
464
504
 
465
505
  attachment.remove()
466
506
  this.inputTarget.focus()
@@ -513,4 +553,14 @@ export default class extends Controller {
513
553
  get middlewares() {
514
554
  return [offset(5), shift({ padding: 24 }), flip()]
515
555
  }
556
+
557
+ get shouldOpenOnMount() {
558
+ return (
559
+ localStorage.getItem(`hellotext--webchat--${this.idValue}`) === 'opened' && !this.onMobile
560
+ )
561
+ }
562
+
563
+ get onMobile() {
564
+ return window.matchMedia(`(max-width: ${this.fullScreenThresholdValue}px)`).matches
565
+ }
516
566
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @class Locale
3
+ * @classdesc
4
+ * Handles locale detection and configuration for the Hellotext library.
5
+ * Provides automatic locale detection from HTML lang attribute, meta tags,
6
+ * and browser language with fallback to 'en'.
7
+ */
8
+ class Locale {
9
+ static _identifier
10
+
11
+ /**
12
+ * Sets the locale identifier explicitly.
13
+ * @param {string} value - The locale identifier (e.g., 'en', 'es')
14
+ */
15
+ static set identifier(value) {
16
+ this._identifier = value
17
+ }
18
+
19
+ /**
20
+ * Gets the effective locale identifier.
21
+ * Falls back to auto-detection if not explicitly set.
22
+ * @returns {string} The locale identifier
23
+ */
24
+ static get identifier() {
25
+ if (this._identifier) return this._identifier
26
+ return this.#fromHtmlLangProperty || this.#fromMetaTag || this.#fromBrowserLanguage || 'en'
27
+ }
28
+
29
+ /**
30
+ * Returns the locale identifier as a string.
31
+ * @returns {string} The locale identifier
32
+ */
33
+ static toString() {
34
+ return this.identifier
35
+ }
36
+
37
+ static get #fromHtmlLangProperty() {
38
+ return document?.documentElement?.lang
39
+ }
40
+
41
+ static get #fromMetaTag() {
42
+ return document?.querySelector('meta[name="locale"]')?.content
43
+ }
44
+
45
+ static get #fromBrowserLanguage() {
46
+ return navigator?.language?.split('-')[0] // Extract primary language
47
+ }
48
+ }
49
+
50
+ export { Locale }
@@ -1,4 +1,5 @@
1
1
  import { Forms } from './configuration/forms'
2
+ import { Locale } from './configuration/locale'
2
3
  import { Webchat } from './configuration/webchat'
3
4
 
4
5
  /**
@@ -9,6 +10,7 @@ import { Webchat } from './configuration/webchat'
9
10
  * @property {String} [session] - session id
10
11
  * @property {Forms} [forms] - form configuration
11
12
  * @property {Webchat} [webchat] - webchat configuration
13
+ * @property {Locale} [locale] - locale configuration
12
14
  */
13
15
  class Configuration {
14
16
  static apiRoot = 'https://api.hellotext.com/v1'
@@ -43,6 +45,14 @@ class Configuration {
43
45
  return this
44
46
  }
45
47
 
48
+ static set locale(locale) {
49
+ Locale.identifier = locale
50
+ }
51
+
52
+ static get locale() {
53
+ return Locale.toString()
54
+ }
55
+
46
56
  static endpoint(path) {
47
57
  return `${this.apiRoot}/${path}`
48
58
  }
package/src/core/index.js CHANGED
@@ -1,2 +1,4 @@
1
- export { default as Event } from './event'
2
1
  export { Configuration } from './configuration'
2
+ export { Locale } from './configuration/locale'
3
+ export { Webchat } from './configuration/webchat'
4
+ export { default as Event } from './event'
package/src/locales/en.js CHANGED
@@ -1,15 +1,15 @@
1
1
  export default {
2
2
  white_label: {
3
- powered_by: 'Powered by',
3
+ powered_by: 'By',
4
4
  },
5
5
  errors: {
6
6
  parameter_not_unique: 'This value is taken.',
7
- blank: 'This field is required.'
7
+ blank: 'This field is required.',
8
8
  },
9
9
  forms: {
10
10
  phone: 'Click the link sent via SMS to verify your submission.',
11
11
  email: 'Click the link sent via email to verify your submission.',
12
12
  phone_and_email: 'Click the links sent via SMS and email to verify your submission.',
13
- none: 'Your submission has been received.'
14
- }
13
+ none: 'Your submission has been received.',
14
+ },
15
15
  }
package/src/locales/es.js CHANGED
@@ -1,15 +1,15 @@
1
1
  export default {
2
2
  white_label: {
3
- powered_by: 'Desarrollado por',
3
+ powered_by: 'Por',
4
4
  },
5
5
  errors: {
6
6
  parameter_not_unique: 'Este valor ya está en uso.',
7
- blank: 'Este campo es obligatorio.'
7
+ blank: 'Este campo es obligatorio.',
8
8
  },
9
9
  forms: {
10
10
  phone: 'Haga clic en el enlace enviado por SMS para verificar su envío.',
11
11
  email: 'Haga clic en el enlace enviado por e-mail para verificar su envío.',
12
12
  phone_and_email: 'Haga clic en los enlaces enviados por SMS y e-mail para verificar su envío.',
13
- none: 'Su envío ha sido recibido.'
14
- }
13
+ none: 'Su envío ha sido recibido.',
14
+ },
15
15
  }
@@ -30,6 +30,18 @@ class Business {
30
30
  return this.data.whitelist !== 'disabled'
31
31
  }
32
32
 
33
+ setLocale(locale) {
34
+ if (!locales[locale]) {
35
+ return console.warn(`Locale ${locale} not found`)
36
+ }
37
+
38
+ if (!this.data) {
39
+ this.data = {}
40
+ }
41
+
42
+ this.data.locale = locale
43
+ }
44
+
33
45
  get locale() {
34
46
  return locales[this.data.locale]
35
47
  }
@@ -1,7 +1,7 @@
1
1
  import API from '../api/forms'
2
2
  import Hellotext from '../hellotext'
3
3
 
4
- import { Configuration } from '../core'
4
+ import { Configuration, Locale } from '../core'
5
5
  import { Form } from './form'
6
6
 
7
7
  import { NotInitializedError } from '../errors'
@@ -84,6 +84,7 @@ class FormCollection {
84
84
 
85
85
  if (!Hellotext.business.data) {
86
86
  Hellotext.business.setData(data.business)
87
+ Hellotext.business.setLocale(Locale.toString())
87
88
  }
88
89
 
89
90
  if (!Hellotext.business.enabledWhitelist) {
@@ -1,7 +1,7 @@
1
1
  export { Business } from './business'
2
- export { Form } from './form'
3
- export { Query } from './query'
4
2
  export { Cookies } from './cookies'
3
+ export { Form } from './form'
5
4
  export { FormCollection } from './form_collection'
6
- export { Webchat } from './webchat'
5
+ export { Query } from './query'
7
6
  export { Session } from './session'
7
+ export { Webchat } from './webchat'