diff --git a/__tests__/alert_initialization_test.js b/__tests__/alert_initialization_test.js index 981a8784..3c02064c 100644 --- a/__tests__/alert_initialization_test.js +++ b/__tests__/alert_initialization_test.js @@ -1,7 +1,7 @@ import { Application } from '@hotwired/stimulus' import Hellotext from '../src/hellotext' import API from '../src/api' -import { Business, Push } from '../src/models' +import { Business, Push, Webchat } from '../src/models' import AlertController from '../src/controllers/alert_controller' const html = ` @@ -129,6 +129,16 @@ describe('Smart Alert initialization', () => { expect(document.querySelector('article')).toBeNull() }) + it('does not mount an alert when another widget fails to load', async () => { + hydrate(businessData({ webchat: { id: 'broken-webchat' } })) + jest.spyOn(Webchat, 'load').mockRejectedValue(new Error('Unable to load webchat')) + + await expect(initialize()).rejects.toThrow('Unable to load webchat') + + expect(Hellotext.alert).toBeNull() + expect(document.querySelector('article')).toBeNull() + }) + it('removes the old alert when reinitialized with Push disabled', async () => { await initialize() const previous = Hellotext.alert diff --git a/__tests__/api/identifications_test.js b/__tests__/api/identifications_test.js new file mode 100644 index 00000000..33edc6d5 --- /dev/null +++ b/__tests__/api/identifications_test.js @@ -0,0 +1,39 @@ +/** + * @jest-environment jsdom + */ + +import IdentificationsAPI from '../../src/api/identifications' +import Hellotext from '../../src/hellotext' +import { Configuration } from '../../src/core' + +describe('IdentificationsAPI', () => { + beforeEach(() => { + Configuration.apiRoot = 'https://api.hellotext.test/v1' + Hellotext.business = { id: 'business-id' } + jest.spyOn(Hellotext, 'session', 'get').mockReturnValue('session-123') + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ status: 'completed' }), + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + Configuration.apiRoot = 'https://api.hellotext.com/v1' + }) + + it('checks a receipt within the current business and session', async () => { + const response = await IdentificationsAPI.status('receipt-1') + const [request, options] = global.fetch.mock.calls[0] + const url = new URL(request) + + expect(url.pathname).toBe('/v1/public/identifications/receipt-1') + expect(url.search).toBe('') + expect(options).toEqual({ + method: 'GET', + headers: { ...Hellotext.headers, 'X-Hellotext-Session': 'session-123' }, + }) + expect(response.succeeded).toBe(true) + }) +}) diff --git a/__tests__/api/popups_test.js b/__tests__/api/popups_test.js index bd1bfecd..ba97093f 100644 --- a/__tests__/api/popups_test.js +++ b/__tests__/api/popups_test.js @@ -48,6 +48,9 @@ describe('PopupsAPI', () => { expect(url.searchParams.get('locale')).toBe('es') expect(url.searchParams.get('device')).toBe('desktop') expect(global.fetch.mock.calls[0][1].headers.Authorization).toBe('Bearer business-id') + // Rails withholds a popup that depends on browser rules from a runtime that does not + // declare it can check them. + expect(global.fetch.mock.calls[0][1].headers['X-Hellotext-Popup-Rules']).toBe('1') expect(element.id).toBe('popup-widget') expect(Hellotext.business.setData).toHaveBeenCalledWith({ id: 'business-id' }) expect(Hellotext.business.setLocale).toHaveBeenCalledWith('es') diff --git a/__tests__/controllers/message_controller_test.js b/__tests__/controllers/message_controller_test.js index 4293faf6..551b8104 100644 --- a/__tests__/controllers/message_controller_test.js +++ b/__tests__/controllers/message_controller_test.js @@ -378,6 +378,15 @@ describe('MessageController', () => { expect(Hellotext.track).not.toHaveBeenCalled() }) + it('does not record cart activity before the platform confirms success', () => { + const recordActivity = jest.spyOn(Hellotext, 'recordActivity').mockImplementation(() => {}) + + controller.addToCart({ currentTarget: mockButton }) + + expect(recordActivity).not.toHaveBeenCalled() + recordActivity.mockRestore() + }) + it('saves the message UTM before dispatching the cart addition', () => { const originalPage = Hellotext.page const save = jest.fn() diff --git a/__tests__/controllers/popup_controller_test.js b/__tests__/controllers/popup_controller_test.js index 6da44721..23cb0590 100644 --- a/__tests__/controllers/popup_controller_test.js +++ b/__tests__/controllers/popup_controller_test.js @@ -5,6 +5,7 @@ import PopupController from '../../src/controllers/popup_controller' import PopupsAPI from '../../src/api/popups' import Hellotext from '../../src/hellotext' +import { Cookies } from '../../src/models/cookies' describe('PopupController', () => { let controller @@ -89,6 +90,8 @@ describe('PopupController', () => { controller.captureValue = { capture_id: 'capture-id' } controller.deviceValue = 'all' controller.idValue = id + // Before initialize(): that is where the controller builds its display rules. + controller.rulesValue = { lanes: [] } controller.initialize() return { @@ -132,6 +135,186 @@ describe('PopupController', () => { document.body.innerHTML = '' }) + // A rule may target any of the three campaign parameters. The URL the visitor is on answers + // for itself, and what it carried is remembered for the rest of the visit — persisted + // attribution, which outlives the visit by years, is never the fallback. + describe('UTM rules', () => { + const utmRule = (field, value) => ({ + lanes: [[{ type: 'condition', field, operator: 'is', values: [value] }]], + }) + const flushTimers = () => new Promise(resolve => setTimeout(resolve, 0)) + let originalPage + + // The rule has to be in place before initialize(), which is where the controller builds + // it. Assigning rulesValue after that leaves the controller evaluating an empty rule set, + // which matches every page and would let these tests pass without reading their rule. + const connectWith = rules => { + const built = buildController({ hasBubble: false }) + controller.rulesValue = rules + controller.initialize() + controller.connect() + + return built + } + + beforeEach(() => { + originalPage = Hellotext.page + // Set, and expected to stay unread: this is the attribution the browser persisted. + Hellotext.page = { utmParams: { source: 'google', medium: 'cpc' } } + Hellotext.visitBusinessId = 'business-1' + Hellotext.visitCampaign = {} + window.sessionStorage.clear() + }) + + afterEach(() => { + controller?.disconnect() + Hellotext.page = originalPage + Hellotext.visitCampaign = {} + window.history.replaceState({}, '', '/') + }) + + it('matches a campaign the URL carries without a source or medium', () => { + window.history.replaceState({}, '', '/landing?utm_campaign=spring') + + const { element } = connectWith(utmRule('session.utm_campaign', 'spring')) + + expect(element.hidden).toBe(false) + }) + + it('ignores capitalization while keeping each value as the link wrote it', () => { + window.history.replaceState({}, '', '/landing?utm_source=Google&utm_medium=Paid_Social&utm_campaign=Spring') + + const { element } = connectWith(utmRule('session.utm_source', 'google')) + + expect(element.hidden).toBe(false) + expect(controller.pageContext().utm).toEqual({ + source: 'Google', + medium: 'Paid_Social', + campaign: 'Spring', + }) + + controller.disconnect() + const campaign = connectWith(utmRule('session.utm_campaign', 'spring')) + + expect(campaign.element.hidden).toBe(false) + }) + + it('keeps an encoded literal plus distinct from a space in campaign values', () => { + window.history.replaceState({}, '', '/landing?utm_campaign=Black%2BFriday') + + const plus = connectWith(utmRule('session.utm_campaign', 'black+friday')) + + expect(plus.element.hidden).toBe(false) + expect(controller.pageContext().utm).toEqual({ campaign: 'Black+Friday' }) + + controller.disconnect() + const words = connectWith(utmRule('session.utm_campaign', 'black friday')) + + expect(words.element.hidden).toBe(true) + }) + + it('keeps the campaign this visit arrived with once the URL drops it', () => { + Hellotext.rememberVisitCampaign({ campaign: 'spring' }) + window.history.replaceState({}, '', '/products/42') + + const { element } = connectWith(utmRule('session.utm_campaign', 'spring')) + + expect(element.hidden).toBe(false) + }) + + // `hello_utm` records only a complete source and medium pair and survives for years, so + // an old campaign must never decide a popup for a visit that arrived some other way. + it('never falls back to the attribution persisted for the browser', () => { + window.history.replaceState({}, '', '/landing') + + const { element } = connectWith(utmRule('session.utm_source', 'google')) + + expect(element.hidden).toBe(true) + expect(controller.pageContext().utm).toEqual({}) + }) + + it('lets the URL replace the remembered campaign rather than merge with it', () => { + Hellotext.rememberVisitCampaign({ source: 'google', medium: 'cpc' }) + window.history.replaceState({}, '', '/landing?utm_campaign=spring') + + const { element } = connectWith(utmRule('session.utm_source', 'google')) + + expect(element.hidden).toBe(true) + expect(controller.pageContext().utm).toEqual({ campaign: 'spring' }) + }) + + it('ignores parameters that name no campaign', () => { + Hellotext.rememberVisitCampaign({ source: 'google', medium: 'cpc' }) + window.history.replaceState({}, '', '/landing?utm_term=shoes') + + const { element } = connectWith(utmRule('session.utm_source', 'google')) + + expect(element.hidden).toBe(false) + }) + + it('falls back when UTM values are blank and reads campaign parameters from a hash route', () => { + Hellotext.rememberVisitCampaign({ source: 'Google', medium: 'CPC' }) + window.history.replaceState({}, '', '/landing?utm_campaign=%20') + + const { element } = connectWith(utmRule('session.utm_source', 'google')) + expect(element.hidden).toBe(false) + + controller.disconnect() + window.history.replaceState({}, '', '/?affiliate=1#/landing?utm_campaign=spring') + const hashRoute = connectWith(utmRule('session.utm_source', 'google')) + + expect(hashRoute.element.hidden).toBe(true) + }) + + it('prefers a document campaign over campaign parameters inside the hash route', () => { + window.history.replaceState( + {}, + '', + '/?utm_source=paid#/landing?utm_campaign=spring', + ) + + const { element } = connectWith(utmRule('session.utm_source', 'paid')) + + expect(element.hidden).toBe(false) + expect(controller.pageContext().utm).toEqual({ source: 'paid' }) + }) + + it('uses the first duplicate UTM parameter without changing persisted attribution', () => { + const set = jest.spyOn(Cookies, 'set') + window.history.replaceState({}, '', '/landing?utm_source=First&utm_source=Second') + + const { element } = connectWith(utmRule('session.utm_source', 'first')) + + expect(element.hidden).toBe(false) + expect(controller.pageContext().utm).toEqual({ source: 'First' }) + expect(set).not.toHaveBeenCalled() + }) + + it('re-reads the URL after a SPA route adds a campaign', async () => { + window.history.replaceState({}, '', '/landing') + + const { element } = connectWith(utmRule('session.utm_source', 'newsletter')) + expect(element.hidden).toBe(true) + + window.history.pushState({}, '', '/offer?utm_source=newsletter') + await flushTimers() + + expect(element.hidden).toBe(false) + }) + }) + + describe('browser detection', () => { + it.each([ + ['Opera', 'Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0'], + ['Samsung Internet', 'Mozilla/5.0 Chrome/120.0.0.0 Mobile Safari/537.36 SamsungBrowser/23.0'], + ])('does not classify %s as Chrome', (_browser, userAgent) => { + jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue(userAgent) + buildController() + + expect(controller.browserName()).toBeUndefined() + }) + }) + it('shows the bubble first and opens the dialog when clicked', () => { const { element, bubble, dialog } = buildController() @@ -309,6 +492,49 @@ describe('PopupController', () => { expect(controller.submitButtonTargets.every(button => !button.disabled)).toBe(true) }) + it('returns to the step containing a field the server rejects', async () => { + const { emailInput, phoneInput, stepOne, stepTwo } = buildController({ hasBubble: false }) + PopupsAPI.submit.mockResolvedValueOnce({ + failed: true, + json: jest.fn().mockResolvedValue({ + errors: [{ parameter: 'email', description: 'Email is already in use.' }], + }), + }) + + controller.connect() + emailInput.value = 'customer@example.com' + await controller.next() + phoneInput.value = '+15551234567' + await controller.submit() + + expect(controller.stepIndex).toBe(0) + expect(stepOne.hidden).toBe(false) + expect(stepTwo.hidden).toBe(true) + expect(emailInput.validationMessage).toBe('Email is already in use.') + }) + + it('returns to the step associated with a rejected field outside its layout wrapper', async () => { + const { emailInput, phoneInput, stepOne, stepTwo } = buildController({ hasBubble: false }) + stepOne.removeChild(emailInput) + controller.element.appendChild(emailInput) + PopupsAPI.submit.mockResolvedValueOnce({ + failed: true, + json: jest.fn().mockResolvedValue({ + errors: [{ parameter: 'email', description: 'Email is already in use.' }], + }), + }) + + controller.connect() + emailInput.value = 'customer@example.com' + phoneInput.value = '+15551234567' + controller.showStep(1) + await controller.submit() + + expect(controller.stepIndex).toBe(0) + expect(stepOne.hidden).toBe(false) + expect(stepTwo.hidden).toBe(true) + }) + it('shows a one-minute resend cooldown and the change action for the submitted identity', async () => { jest.useFakeTimers() jest.setSystemTime(new Date('2026-08-24T12:00:00Z')) @@ -480,6 +706,44 @@ describe('PopupController', () => { ) }) + it('submits a prefixed phone value in the identity and metadata fields', () => { + const { emailInput, phoneInput } = buildController({ hasBubble: false }) + + emailInput.required = false + phoneInput.dataset.popupPhonePrefix = '+58' + phoneInput.value = '04126625353' + + expect(controller.submissionPayload()).toEqual( + expect.objectContaining({ + phone: '+584126625353', + metadata: expect.objectContaining({ + fields: expect.objectContaining({ phone: '+584126625353' }), + steps: expect.arrayContaining([ + expect.objectContaining({ fields: expect.objectContaining({ phone: '+584126625353' }) }), + ]), + }), + }), + ) + }) + + it('keeps an empty optional phone blank when it has a country prefix', () => { + const { emailInput, phoneInput } = buildController({ hasBubble: false }) + + phoneInput.required = false + phoneInput.dataset.popupPhonePrefix = '+58' + phoneInput.value = '' + emailInput.value = 'customer@example.com' + + expect(controller.submissionPayload()).toEqual( + expect.objectContaining({ + phone: '', + metadata: expect.objectContaining({ + fields: expect.objectContaining({ phone: '' }), + }), + }), + ) + }) + it('uses the backend delivery channel and destination in the completed step', () => { const { completed, emailInput, phoneInput } = buildController({ hasBubble: false }) diff --git a/__tests__/controllers/popup_display_rules_test.js b/__tests__/controllers/popup_display_rules_test.js new file mode 100644 index 00000000..bed90be9 --- /dev/null +++ b/__tests__/controllers/popup_display_rules_test.js @@ -0,0 +1,462 @@ +/** + * @jest-environment jsdom + */ + +import PopupController from '../../src/controllers/popup_controller' +import Hellotext from '../../src/hellotext' + +describe('PopupController display rules', () => { + let controller + + const buildController = ({ lanes = [], hasBubble = false } = {}) => { + const element = document.createElement('article') + const dialog = document.createElement('section') + const step = document.createElement('section') + + step.dataset.stepId = 'step-one' + dialog.append(step) + element.append(dialog) + document.body.appendChild(element) + + controller = new PopupController() + Object.defineProperty(controller, 'element', { value: element, configurable: true }) + + controller.dialogTarget = dialog + controller.stepTargets = [step] + controller.inputTargets = [] + controller.submitButtonTargets = [] + Object.defineProperties(controller, { + hasResendButtonTarget: { value: false, configurable: true }, + hasChangeDestinationButtonTarget: { value: false, configurable: true }, + hasGlobalErrorTarget: { value: false, configurable: true }, + }) + controller.hasBubbleTarget = hasBubble + controller.hasBubbleValue = hasBubble + controller.captureValue = {} + controller.deviceValue = 'all' + controller.idValue = 'popup-id' + // Before initialize(): that is where the controller builds its display rules. + controller.rulesValue = { lanes } + controller.initialize() + + return { element, dialog } + } + + const lane = (...conditions) => conditions.map(([field, operator, values]) => ({ + field, + operator, + values: [].concat(values), + })) + + beforeEach(() => { + window.history.replaceState({}, '', '/') + window.localStorage.clear() + window.sessionStorage.clear() + Hellotext.activities.clear() + jest.spyOn(Hellotext.eventEmitter, 'dispatch').mockImplementation(() => {}) + }) + + afterEach(() => { + controller?.disconnect() + controller = undefined + document.body.innerHTML = '' + jest.restoreAllMocks() + jest.useRealTimers() + }) + + it('displays a popup with no rules', () => { + const { element } = buildController() + + controller.connect() + + expect(element.hidden).toBe(false) + }) + + it('displays when the page matches a lane', () => { + const { element } = buildController({ lanes: [lane(['page.path', 'is', '/'])] }) + + controller.connect() + + expect(element.hidden).toBe(false) + }) + + it('stays hidden when the page does not match', () => { + const { element } = buildController({ lanes: [lane(['page.path', 'contains', '/sale'])] }) + + controller.connect() + + expect(element.hidden).toBe(true) + }) + + it('fails a lane containing a malformed sibling beside a matching positive rule', () => { + const conditions = lane(['page.path', 'contains', '/']) + conditions.push({ field: 'page.path', operator: 'unknown', values: ['/products'] }) + const { element } = buildController({ lanes: [conditions] }) + + controller.connect() + + expect(element.hidden).toBe(true) + }) + + // The server strips visitor conditions once it has decided them, so a surviving lane can + // arrive empty and the popup should display. + it('displays when the server already satisfied every condition in a lane', () => { + const { element } = buildController({ lanes: [[]] }) + + controller.connect() + + expect(element.hidden).toBe(false) + }) + + describe('measurements', () => { + it('keeps re-checking until the visitor scrolls far enough', () => { + const { element } = buildController({ lanes: [lane(['session.scroll_depth', 'at_least', 50])] }) + + jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockReturnValue(4000) + window.innerWidth = 1200 + Object.defineProperty(window, 'innerHeight', { value: 1000, configurable: true }) + Object.defineProperty(window, 'scrollY', { value: 0, configurable: true, writable: true }) + + controller.connect() + expect(element.hidden).toBe(true) + + window.scrollY = 1100 + window.dispatchEvent(new Event('scroll')) + + expect(element.hidden).toBe(false) + }) + + it('displays once enough time has passed on the page', () => { + jest.useFakeTimers() + const { element } = buildController({ lanes: [lane(['session.time_on_page', 'at_least', 5])] }) + + controller.connect() + expect(element.hidden).toBe(true) + + jest.advanceTimersByTime(6000) + + expect(element.hidden).toBe(false) + }) + + // A popup without a measured rule is decided once, so it must not install a scroll + // listener or an interval that would run for the life of the page. + it('does not watch measurements when no rule needs one', () => { + const listener = jest.spyOn(window, 'addEventListener') + buildController({ lanes: [lane(['page.path', 'contains', '/sale'])] }) + + controller.connect() + + expect(listener).not.toHaveBeenCalledWith('scroll', expect.anything(), expect.anything()) + expect(controller.measurementTimer).toBeUndefined() + }) + + // "Shown" means actually displayed. Once it displays, the watchers stop so the popup is + // never evaluated — or counted — a second time. + it('stops watching once the popup displays', () => { + jest.useFakeTimers() + buildController({ lanes: [lane(['session.time_on_page', 'at_least', 1])] }) + + controller.connect() + jest.advanceTimersByTime(2000) + + expect(controller.displayed).toBe(true) + expect(controller.measurementTimer).toBeUndefined() + }) + }) + + describe('current-visit activity', () => { + it('re-evaluates when a matching tracked activity occurs', () => { + Hellotext.eventEmitter.dispatch.mockRestore() + const { element } = buildController({ + lanes: [lane(['activity.product_viewed', 'occurred', []])], + }) + + controller.connect() + expect(element.hidden).toBe(true) + + Hellotext.recordActivity('product.viewed') + + expect(element.hidden).toBe(false) + expect(controller.onActivity).toBeUndefined() + }) + + it('does not match an unrelated activity from the same visit', () => { + const { element } = buildController({ + lanes: [lane(['activity.cart_added', 'occurred', []])], + }) + + Hellotext.activities.add('activity.product_viewed') + controller.connect() + + expect(element.hidden).toBe(true) + }) + }) + + // The campaign a visit arrives with has to survive the rest of that visit: a popup that + // waits for scroll or time is almost never decided on the landing page itself. + describe('campaign this visit arrived with', () => { + const utmRules = (...conditions) => [lane(...conditions)] + + beforeEach(() => { + Hellotext.visitCampaign = {} + Hellotext.visitBusinessId = 'business-1' + }) + + it('keeps the landing campaign after the site navigates past it', () => { + window.history.replaceState({}, '', '/?utm_campaign=spring') + Hellotext.initializeVisitSignals('business-1') + window.history.replaceState({}, '', '/products/42') + + const { element } = buildController({ + lanes: utmRules(['session.utm_campaign', 'is', 'spring']), + }) + controller.connect() + + expect(element.hidden).toBe(false) + }) + + // `hello_utm` only ever holds a complete source and medium pair, and it outlives the + // visit by years. A campaign-only landing must not fall back onto it. + it('never falls back to the campaign persisted for the browser', () => { + Hellotext.page = { utmParams: { source: 'google', medium: 'cpc' } } + window.history.replaceState({}, '', '/?utm_campaign=spring') + Hellotext.initializeVisitSignals('business-1') + window.history.replaceState({}, '', '/products/42') + + const { element } = buildController({ + lanes: utmRules(['session.utm_source', 'is', 'google']), + }) + controller.connect() + + expect(element.hidden).toBe(true) + expect(controller.pageContext().utm).toEqual({ campaign: 'spring' }) + Hellotext.page = undefined + }) + + it('replaces the remembered campaign when a later URL carries its own', () => { + window.history.replaceState({}, '', '/?utm_source=instagram&utm_medium=social') + Hellotext.initializeVisitSignals('business-1') + buildController() + window.history.replaceState({}, '', '/?utm_campaign=spring') + + expect(controller.pageContext().utm).toEqual({ campaign: 'spring' }) + + window.history.replaceState({}, '', '/products/42') + expect(controller.pageContext().utm).toEqual({ campaign: 'spring' }) + }) + + it('does not inherit a campaign from another visit or another business', () => { + window.history.replaceState({}, '', '/?utm_campaign=spring') + Hellotext.initializeVisitSignals('business-1') + + // A new tab starts with empty session storage, and a second business keeps its own. + window.sessionStorage.clear() + window.history.replaceState({}, '', '/products/42') + Hellotext.visitBusinessId = undefined + Hellotext.initializeVisitSignals('business-2') + + buildController() + expect(controller.pageContext().utm).toEqual({}) + }) + + it('reads the URL and stays quiet when session storage is unavailable', () => { + jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('denied') + }) + window.history.replaceState({}, '', '/?utm_campaign=spring') + Hellotext.initializeVisitSignals('business-1') + + buildController() + expect(controller.pageContext().utm).toEqual({ campaign: 'spring' }) + }) + + it('does not touch persisted attribution while a popup is evaluated', () => { + const previous = document.cookie + window.history.replaceState({}, '', '/?utm_campaign=spring') + Hellotext.initializeVisitSignals('business-1') + + buildController() + controller.connect() + controller.pageContext() + + expect(document.cookie).toBe(previous) + }) + }) + + it('builds rule context from visit signals and the campaign this visit arrived with', () => { + buildController() + Hellotext.pageViews = 4 + Hellotext.visitorType = 'returning' + Hellotext.visitCampaign = { source: 'instagram', medium: 'social' } + Object.defineProperty(window.navigator, 'languages', { + value: ['es-VE'], + configurable: true, + }) + controller.connectedAt = Date.now() + + expect(controller.pageContext()).toEqual( + expect.objectContaining({ + pageViews: 4, + language: 'es', + visitorType: 'returning', + utm: { source: 'instagram', medium: 'social' }, + }), + ) + }) + + it('does not display again after the visitor dismisses it', () => { + const { element } = buildController() + + controller.connect() + controller.close() + controller.evaluateDisplay() + + expect(element.hidden).toBe(true) + }) + + describe('SPA navigation', () => { + it('keeps counting routes when the popup has no navigation rules', () => { + jest.useFakeTimers() + const recordPageView = jest.spyOn(Hellotext, 'recordPageView') + buildController() + + controller.connect() + window.history.pushState({}, '', '/next') + jest.runOnlyPendingTimers() + + expect(recordPageView).toHaveBeenCalledTimes(1) + }) + + it('re-evaluates page rules after pushState', () => { + jest.useFakeTimers() + const { element } = buildController({ lanes: [lane(['page.path', 'contains', '/sale'])] }) + + controller.connect() + expect(element.hidden).toBe(true) + + window.history.pushState({}, '', '/sale') + jest.runOnlyPendingTimers() + + expect(element.hidden).toBe(false) + }) + + it('re-evaluates title rules after Turbo renders', () => { + jest.useFakeTimers() + document.title = 'Home' + const { element } = buildController({ lanes: [lane(['page.title', 'contains', 'sale'])] }) + + controller.connect() + expect(element.hidden).toBe(true) + + document.title = 'Sale' + window.dispatchEvent(new Event('turbo:render')) + jest.runOnlyPendingTimers() + + expect(element.hidden).toBe(false) + }) + + it.each(['pushState', 'replaceState'])( + 're-evaluates title rules after %s even when the URL is unchanged', + method => { + jest.useFakeTimers() + document.title = 'Home' + const { element } = buildController({ lanes: [lane(['page.title', 'contains', 'sale'])] }) + + controller.connect() + expect(element.hidden).toBe(true) + + window.history[method]({}, '', '/') + document.title = 'Sale' + jest.runOnlyPendingTimers() + + expect(element.hidden).toBe(false) + }, + ) + + it('re-evaluates title rules after an asynchronous title update', async () => { + jest.useFakeTimers() + document.title = 'Home' + const { element } = buildController({ lanes: [lane(['page.title', 'contains', 'sale'])] }) + + controller.connect() + window.history.pushState({}, '', '/sale') + jest.runOnlyPendingTimers() + expect(element.hidden).toBe(true) + + document.title = 'Sale' + await Promise.resolve() + jest.runOnlyPendingTimers() + + expect(element.hidden).toBe(false) + }) + + it('restores history methods and cancels pending navigation work on disconnect', () => { + jest.useFakeTimers() + const originalPushState = window.history.pushState + const originalReplaceState = window.history.replaceState + buildController({ lanes: [lane(['page.path', 'contains', '/sale'])] }) + + controller.connect() + window.history.pushState({}, '', '/sale') + controller.disconnect() + jest.runOnlyPendingTimers() + + expect(window.history.pushState).toBe(originalPushState) + expect(window.history.replaceState).toBe(originalReplaceState) + expect(controller.element.hidden).toBe(true) + }) + + it('keeps a downstream history wrapper functional after disconnect', () => { + jest.useFakeTimers() + const { element } = buildController({ lanes: [lane(['page.path', 'contains', '/sale'])] }) + + controller.connect() + const popupPushState = window.history.pushState + const downstreamPushState = jest.fn(function (...args) { + return popupPushState.apply(this, args) + }) + window.history.pushState = downstreamPushState + const evaluateDisplay = jest.spyOn(controller, 'evaluateDisplay') + + controller.disconnect() + + expect(window.history.pushState).toBe(downstreamPushState) + expect(() => window.history.pushState({}, '', '/sale')).not.toThrow() + jest.runOnlyPendingTimers() + expect(downstreamPushState).toHaveBeenCalledTimes(1) + expect(evaluateDisplay).not.toHaveBeenCalled() + expect(element.hidden).toBe(true) + }) + + it('restarts time on page after navigation', () => { + jest.useFakeTimers() + const { element } = buildController({ lanes: [lane(['session.time_on_page', 'at_least', 5])] }) + + controller.connect() + controller.connectedAt = Date.now() + jest.advanceTimersByTime(4000) + window.history.pushState({}, '', '/sale') + jest.runOnlyPendingTimers() + jest.advanceTimersByTime(2000) + + expect(element.hidden).toBe(true) + + jest.advanceTimersByTime(3000) + expect(element.hidden).toBe(false) + }) + + it('keeps time on page across same-route state updates', () => { + jest.useFakeTimers() + const { element } = buildController({ lanes: [lane(['session.time_on_page', 'at_least', 5])] }) + + controller.connect() + controller.connectedAt = Date.now() + jest.advanceTimersByTime(4000) + window.history.replaceState({}, '', '/?filter=available') + jest.runOnlyPendingTimers() + jest.advanceTimersByTime(1000) + + expect(element.hidden).toBe(false) + }) + }) +}) diff --git a/__tests__/core/event_test.js b/__tests__/core/event_test.js index bc906030..46ad6043 100644 --- a/__tests__/core/event_test.js +++ b/__tests__/core/event_test.js @@ -15,6 +15,10 @@ describe(".valid", function () { expect(Event.valid("popup:closed")).toEqual(true) }); + it("is true for popup activity events", () => { + expect(Event.valid("activity:occurred")).toEqual(true) + }); + it("is false when event name is not defined", () => { expect(Event.valid("undefined-event")).toEqual(false) }); diff --git a/__tests__/fixtures/page_path_cases.json b/__tests__/fixtures/page_path_cases.json new file mode 100644 index 00000000..234f8fb9 --- /dev/null +++ b/__tests__/fixtures/page_path_cases.json @@ -0,0 +1,63 @@ +{ + "about": "Canonical page path cases shared by every implementation. Identical copies live in hellotext.js (__tests__/fixtures/page_path_cases.json) and in the Rails app (spec/fixtures/files/popup_rules/page_path_cases.json); change both together.", + "cases": [ + { "mode": "exact", "input": "/sale", "expected": "/sale" }, + { "mode": "exact", "input": "/Sale/", "expected": "/sale" }, + { "mode": "exact", "input": "sale", "expected": "/sale" }, + { "mode": "exact", "input": " /sale ", "expected": "/sale" }, + { "mode": "exact", "input": " /sale ", "expected": "/sale" }, + { "mode": "exact", "input": "https://Tienda.com//Sale/?x=1#top", "expected": "/sale" }, + { "mode": "exact", "input": "HTTP://tienda.com:8080/sale", "expected": "/sale" }, + { "mode": "exact", "input": "//tienda.com/sale", "expected": "/sale" }, + { "mode": "exact", "input": "tienda.com/products/Zapato-Rojo/", "expected": "" }, + { "mode": "exact", "input": "www.tienda.com/sale", "expected": "" }, + { "mode": "exact", "input": "shop.test:3000/sale", "expected": "" }, + { "mode": "exact", "input": "otra.com/sale", "expected": "" }, + { "mode": "exact", "input": "sitemap.xml", "expected": "/sitemap.xml" }, + { "mode": "exact", "input": "https://tienda.com", "expected": "/" }, + { "mode": "exact", "input": "tienda.com", "expected": "/tienda.com" }, + { "mode": "exact", "input": "?x=1", "expected": "/" }, + { "mode": "exact", "input": "/caf%C3%A9", "expected": "/café" }, + { "mode": "exact", "input": "/CAFÉ", "expected": "/café" }, + { "mode": "exact", "input": "/café", "expected": "/café" }, + { "mode": "exact", "input": "/%E2%82%AC", "expected": "/€" }, + { "mode": "exact", "input": "/bad%E0%A4%A", "expected": "/bad%e0%a4%a" }, + { "mode": "exact", "input": "/mi%20pagina", "expected": "/mi pagina" }, + { "mode": "exact", "input": "/a%2Fb", "expected": "/a%2fb" }, + { "mode": "exact", "input": "/a%3Fb%23c%25d", "expected": "/a%3fb%23c%25d" }, + { "mode": "exact", "input": "/a+b", "expected": "/a+b" }, + { "mode": "exact", "input": "/ΟΔΟΣ", "expected": "/οδοσ" }, + { "mode": "exact", "input": "/a/./b/../c", "expected": "/a/c" }, + { "mode": "exact", "input": "/../sale", "expected": "/sale" }, + { "mode": "exact", "input": "/index.html", "expected": "/" }, + { "mode": "exact", "input": "/blog/index.php", "expected": "/blog" }, + { "mode": "exact", "input": "/blog/INDEX.HTM", "expected": "/blog" }, + { "mode": "exact", "input": "/indexes", "expected": "/indexes" }, + { "mode": "exact", "input": "/#/products/42", "expected": "/products/42" }, + { "mode": "exact", "input": "/#!/products/42?ref=x", "expected": "/products/42" }, + { "mode": "exact", "input": "/app#/settings", "expected": "/app/settings" }, + { "mode": "exact", "input": "/sale#top", "expected": "/sale" }, + { "mode": "exact", "input": "mailto:hello@shop.test", "expected": "" }, + { "mode": "exact", "input": "javascript:alert(1)", "expected": "" }, + { "mode": "exact", "input": "data:text/plain,hello", "expected": "" }, + + { "mode": "contains", "input": "sale", "expected": "sale" }, + { "mode": "contains", "input": "Rojo", "expected": "rojo" }, + { "mode": "contains", "input": "/Sale/", "expected": "/sale/" }, + { "mode": "contains", "input": "https://tienda.com/Sale/?x=1", "expected": "/sale/" }, + { "mode": "contains", "input": "tienda.com/products/", "expected": "" }, + { "mode": "contains", "input": "products/", "expected": "products/" }, + { "mode": "contains", "input": "/a//b/", "expected": "/a/b/" }, + { "mode": "contains", "input": "/caf%C3%A9", "expected": "/café" }, + { "mode": "contains", "input": "/blog/index.html", "expected": "/blog" }, + { "mode": "contains", "input": "#/products", "expected": "/products" }, + { "mode": "contains", "input": "sitemap.xml", "expected": "sitemap.xml" }, + { "mode": "contains", "input": "/../sale", "expected": "/../sale" }, + { "mode": "contains", "input": "/", "expected": "" }, + { "mode": "contains", "input": "https://tienda.com", "expected": "" }, + { "mode": "contains", "input": "https://tienda.com/", "expected": "" }, + { "mode": "contains", "input": "index.html", "expected": "" }, + { "mode": "contains", "input": "?utm_source=x", "expected": "" }, + { "mode": "contains", "input": " ", "expected": "" } + ] +} diff --git a/__tests__/hellotext_test.js b/__tests__/hellotext_test.js index 512f7fe8..67dfd890 100644 --- a/__tests__/hellotext_test.js +++ b/__tests__/hellotext_test.js @@ -1,7 +1,7 @@ import Hellotext from "../src/hellotext"; import API from "../src/api"; import { Configuration } from "../src/core"; -import { Popup, Push, Session, Webchat, WhatsAppWidget } from "../src/models"; +import { Business, Popup, Push, Session, Webchat, WhatsAppWidget } from "../src/models"; const getCookieValue = name => document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() @@ -41,6 +41,107 @@ afterEach(() => { document.querySelectorAll('link[rel="stylesheet"]').forEach(link => link.remove()) }); +describe('popup visit signals', () => { + beforeEach(() => { + window.localStorage.clear() + window.sessionStorage.clear() + Hellotext.activities = new Set() + Hellotext.visitBusinessId = undefined + Hellotext.lastPageUrl = undefined + Hellotext.lastPageRoute = undefined + Hellotext.visitStartedAt = undefined + }) + + it('keeps activities and page counts across page loads in the same visit', () => { + Hellotext.initializeVisitSignals('business-id') + Hellotext.recordActivity('product.viewed') + + Hellotext.activities = new Set() + Hellotext.visitBusinessId = undefined + Hellotext.lastPageUrl = undefined + Hellotext.lastPageRoute = undefined + Hellotext.initializeVisitSignals('business-id') + + expect(Hellotext.pageViews).toBe(2) + expect(Hellotext.visitorType).toBe('new') + expect(Hellotext.activities).toContain('activity.product_viewed') + }) + + it('measures the first page from the document start', () => { + Hellotext.initializeVisitSignals('business-id') + + expect(Hellotext.pageStartedAt).toBe(window.performance.timeOrigin) + expect(Hellotext.visitStartedAt).toBe(window.performance.timeOrigin) + }) + + it('does not count query changes or ordinary anchors as new pages', () => { + window.history.replaceState({}, '', '/products?utm_source=email#details') + Hellotext.initializeVisitSignals('business-id') + + window.history.replaceState({}, '', '/products?color=blue#reviews') + Hellotext.initializeVisitSignals('business-id') + + expect(Hellotext.pageViews).toBe(1) + }) + + it('counts a changed hash route but ignores its query parameters', () => { + window.history.replaceState({}, '', '/#/products?color=red') + Hellotext.initializeVisitSignals('business-id') + + window.history.replaceState({}, '', '/#/products?color=blue') + Hellotext.initializeVisitSignals('business-id') + expect(Hellotext.pageViews).toBe(1) + + window.history.replaceState({}, '', '/#/checkout') + Hellotext.initializeVisitSignals('business-id') + expect(Hellotext.pageViews).toBe(2) + }) + + it('treats hashbang and plain hash routes as the same page', () => { + window.history.replaceState({}, '', '/#!/products') + Hellotext.initializeVisitSignals('business-id') + + window.history.replaceState({}, '', '/#/products') + Hellotext.initializeVisitSignals('business-id') + + expect(Hellotext.pageViews).toBe(1) + }) + + it('starts timing at initialization when an SPA changed routes before the SDK loaded', () => { + const getEntriesByType = window.performance.getEntriesByType + Object.defineProperty(window.performance, 'getEntriesByType', { + configurable: true, + value: jest.fn().mockReturnValue([{ name: 'http://localhost/' }]), + }) + const now = jest.spyOn(Date, 'now').mockReturnValue(1234) + window.history.replaceState({}, '', '/products') + + Hellotext.initializeVisitSignals('business-id') + + expect(Hellotext.pageStartedAt).toBe(1234) + + Object.defineProperty(window.performance, 'getEntriesByType', { + configurable: true, + value: getEntriesByType, + }) + now.mockRestore() + window.history.replaceState({}, '', '/') + }) + + it('recognizes a visitor after a new browser session starts', () => { + Hellotext.initializeVisitSignals('business-id') + + window.sessionStorage.clear() + Hellotext.visitBusinessId = undefined + Hellotext.lastPageUrl = undefined + Hellotext.lastPageRoute = undefined + Hellotext.initializeVisitSignals('business-id') + + expect(Hellotext.visitorType).toBe('returning') + expect(Hellotext.pageViews).toBe(1) + }) +}) + describe("when trying to call methods before initializing the class", () => { it("raises an error when Hellotext.track is called", () => { expect(Hellotext.track("page.viewed")).rejects.toThrowError() @@ -76,6 +177,7 @@ describe("when initializing business metadata", () => { Configuration.whatsapp.number = null Configuration.whatsapp.body = null Hellotext.popup = undefined + Hellotext.identificationPending = false }) it("fetches public business data by default and stores it", async () => { @@ -88,6 +190,32 @@ describe("when initializing business metadata", () => { expect(Hellotext.business.data).toEqual(business) }) + it('cancels pending identification when the same business starts a new session', async () => { + Session.session = 'old-session' + Hellotext.visitBusinessId = 'xy76ks' + Hellotext.identificationPending = true + const cancel = jest.spyOn(Hellotext, 'cancelIdentificationPolling') + + await Hellotext.initialize('xy76ks', { session: 'new-session', popup: false }) + + expect(Hellotext.identificationPending).toBe(false) + expect(cancel).toHaveBeenCalled() + }) + + it('keeps pending identification current when the same visit is reinitialized', async () => { + Session.session = 'same-session' + Hellotext.visitBusinessId = 'xy76ks' + Hellotext.identificationVersion = 7 + Hellotext.identificationPending = true + const cancel = jest.spyOn(Hellotext, 'cancelIdentificationPolling') + + await Hellotext.initialize('xy76ks', { session: 'same-session', popup: false }) + + expect(Hellotext.identificationPending).toBe(true) + expect(Hellotext.identificationCurrent(7, 'xy76ks', 'same-session')).toBe(true) + expect(cancel).not.toHaveBeenCalled() + }) + it("loads the dashboard webchat when no explicit webchat config is passed", async () => { mockBusinessFetch(defaultBusiness({ webchat: { id: "dashboard-webchat" } })) @@ -457,6 +585,8 @@ describe("when the class is initialized successfully", () => { }); describe("when tracking events", () => { + beforeEach(() => Hellotext.activities.clear()) + it("success attribute is true when response from the server is received successfully", async () => { global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({received: "success"}), @@ -468,6 +598,71 @@ describe("when the class is initialized successfully", () => { expect(response.succeeded).toEqual(true) }); + it("records supported popup activity only after the server accepts it", async () => { + global.fetch = jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({received: "success"}), + status: 200 + }) + + await Hellotext.track("product.viewed") + + expect(Hellotext.activities).toContain('activity.product_viewed') + }); + + it('does not apply an accepted activity to a runtime that changed while it was pending', async () => { + let resolve + global.fetch = jest.fn().mockReturnValue(new Promise(result => { resolve = result })) + + const tracked = Hellotext.track('product.viewed') + Hellotext.business = { id: 'other-business' } + Hellotext.visitBusinessId = 'other-business' + resolve({ json: jest.fn().mockResolvedValue({ received: 'success' }), status: 200 }) + + await tracked + + expect(Hellotext.activities).not.toContain('activity.product_viewed') + }) + + it('accepts a Unix-seconds activity from an earlier page in the current visit', async () => { + global.fetch = jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({ received: 'success' }), + status: 200, + }) + Hellotext.visitStartedAt = Date.parse('2026-09-16T12:00:00Z') + Hellotext.pageStartedAt = Date.parse('2026-09-16T12:05:00Z') + + await Hellotext.track('product.viewed', { + tracked_at: Date.parse('2026-09-16T12:02:00Z') / 1000, + }) + + expect(Hellotext.activities).toContain('activity.product_viewed') + }) + + it('rejects an accepted activity timestamped before the current visit', async () => { + global.fetch = jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({ received: 'success' }), + status: 200, + }) + Hellotext.visitStartedAt = Date.parse('2026-09-16T12:00:00Z') + + await Hellotext.track('product.viewed', { + tracked_at: Date.parse('2026-09-16T11:59:00Z') / 1000, + }) + + expect(Hellotext.activities).not.toContain('activity.product_viewed') + }) + + it("records an accepted cart addition for popup activity rules", async () => { + global.fetch = jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({received: "success"}), + status: 200 + }) + + await Hellotext.track("cart.added") + + expect(Hellotext.activities).toContain('activity.cart_added') + }); + it("success attribute is false when response from the server is rejected", async () => { global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({}), @@ -479,6 +674,29 @@ describe("when the class is initialized successfully", () => { expect(response.failed).toEqual(true) }); + it("does not record popup activity when tracking is rejected", async () => { + global.fetch = jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({}), + status: 422 + }) + + await Hellotext.track("cart.added") + + expect(Hellotext.activities).not.toContain('activity.cart_added') + }); + + it("maps both supported purchase actions to purchase completed", async () => { + global.fetch = jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({received: "success"}), + status: 200 + }) + + await Hellotext.track("order.placed") + await Hellotext.track("product.purchased") + + expect([...Hellotext.activities]).toEqual(['activity.purchase_completed']) + }); + it("includes UTM parameters in the request body", async () => { global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({received: "success"}), @@ -754,6 +972,128 @@ describe("when the class is initialized successfully", () => { expect(getCookieValue("hello_user_identification_hash")).toMatch(/^v1:/) }) + it('keeps the popup hidden and defers the fingerprint until identification completes', async () => { + jest.useFakeTimers() + const popup = { unmount: jest.fn() } + const loadedPopup = { unmount: jest.fn() } + Hellotext.popup = popup + Hellotext.popupRuntime = { + config: { id: 'popup-id', container: 'body' }, + businessContext: Hellotext.business, + initializationVersion: Hellotext.initializationVersion, + } + const loadPopup = jest.spyOn(Popup, 'load').mockResolvedValue(loadedPopup) + global.fetch = jest + .fn() + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ identification_receipt: 'receipt-1' }), + status: 200, + ok: true, + }) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ status: 'pending' }), + status: 202, + ok: true, + }) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ status: 'completed' }), + status: 200, + ok: true, + }) + + await Hellotext.identify('user_pending', { source: 'shopify' }) + await Promise.resolve() + + expect(popup.unmount).toHaveBeenCalled() + expect(getCookieValue('hello_user_identification_hash')).toBeUndefined() + expect(Popup.load).not.toHaveBeenCalled() + + jest.advanceTimersByTime(100) + await Hellotext.identificationCompletion + + expect(getCookieValue('hello_user_identification_hash')).toMatch(/^v1:/) + expect(Popup.load).toHaveBeenCalledWith( + 'popup-id', + expect.objectContaining({ container: 'body' }), + ) + expect(Hellotext.popup).toBe(loadedPopup) + loadPopup.mockRestore() + Hellotext.popupRuntime = undefined + Hellotext.popup = undefined + jest.useRealTimers() + }) + + it('cancels stale receipt polling when a newer identification wins', async () => { + jest.useFakeTimers() + global.fetch = jest + .fn() + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ identification_receipt: 'receipt-1' }), + status: 200, + ok: true, + }) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ status: 'pending' }), + status: 202, + ok: true, + }) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ identification_receipt: 'receipt-2' }), + status: 200, + ok: true, + }) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ status: 'completed' }), + status: 200, + ok: true, + }) + + await Hellotext.identify('first-user', { source: 'shopify' }) + await Promise.resolve() + await Hellotext.identify('second-user', { source: 'shopify' }) + await Hellotext.identificationCompletion + jest.runOnlyPendingTimers() + + expect(getCookieValue('hello_user_id')).toBe('second-user') + expect(global.fetch).toHaveBeenCalledTimes(4) + jest.useRealTimers() + }) + + it('restores anonymous popup evaluation when identification fails terminally', async () => { + const loadedPopup = { unmount: jest.fn() } + Hellotext.popupRuntime = { + config: { id: 'popup-id', container: 'body' }, + businessContext: Hellotext.business, + initializationVersion: Hellotext.initializationVersion, + } + const loadPopup = jest.spyOn(Popup, 'load').mockResolvedValue(loadedPopup) + global.fetch = jest + .fn() + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ identification_receipt: 'receipt-1' }), + status: 200, + ok: true, + }) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ status: 'failed' }), + status: 422, + ok: false, + }) + + await Hellotext.identify('failed-user', { source: 'shopify' }) + await Hellotext.identificationCompletion + + expect(Hellotext.identificationPending).toBe(false) + expect(getCookieValue('hello_user_identification_hash')).toBeUndefined() + expect(Popup.load).toHaveBeenCalledWith( + 'popup-id', + expect.objectContaining({ container: 'body' }), + ) + loadPopup.mockRestore() + Hellotext.popupRuntime = undefined + Hellotext.popup = undefined + }) + it("does not set cookies when identification fails", async () => { global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({error: "invalid data"}), @@ -1019,6 +1359,18 @@ describe('when initializing Push', () => { expect(Hellotext.isInitialized).toBe(true) }) + it('does not synchronize Push when loading a required surface fails', async () => { + loadWebchat.mockRejectedValueOnce(new Error('surface failed')) + mockBusinessFetch(defaultBusiness({ + push: { public_key: 'business-public-key' }, + webchat: { id: 'dashboard-webchat' }, + })) + + await expect(Hellotext.initialize('xy76ks')).rejects.toThrow('surface failed') + + expect(initializePush).not.toHaveBeenCalled() + }) + it('resets omitted Push options when initializing another business', async () => { mockBusinessFetch(defaultBusiness({ id: 'business-a', diff --git a/__tests__/models/form_collection_test.js b/__tests__/models/form_collection_test.js index d3c51d4a..f2c782bf 100644 --- a/__tests__/models/form_collection_test.js +++ b/__tests__/models/form_collection_test.js @@ -91,6 +91,46 @@ describe('collect', () => { expect(fetch).not.toHaveBeenCalled() }) + it('discards forms that finish loading after the active business changes', async () => { + let resolveResponse + global.fetch = jest.fn().mockReturnValue( + new Promise(resolve => { + resolveResponse = resolve + }), + ) + document.body.innerHTML = `
` + const forms = Hellotext.forms + const collected = jest.spyOn(Hellotext.eventEmitter, 'dispatch') + + const collection = forms.collect() + Hellotext.visitBusinessId = 'another-business' + resolveResponse({ json: jest.fn().mockResolvedValue({ id: 1 }) }) + await collection + + expect(forms.length).toBe(0) + expect(collected).not.toHaveBeenCalledWith('forms:collected', forms) + expect(forms.fetching).toBe(false) + }) + + it('discards forms from an earlier initialization of the same business', async () => { + let resolveResponse + global.fetch = jest.fn().mockReturnValue( + new Promise(resolve => { + resolveResponse = resolve + }), + ) + document.body.innerHTML = `
` + const forms = Hellotext.forms + + const collection = forms.collect() + Hellotext.initializationVersion += 1 + resolveResponse({ json: jest.fn().mockResolvedValue({ id: 1 }) }) + await collection + + expect(forms.length).toBe(0) + expect(forms.fetching).toBe(false) + }) + it('emits forms:collected event after successful collection', async () => { const eventSpy = jest.spyOn(Hellotext.eventEmitter, 'dispatch') diff --git a/__tests__/models/form_test.js b/__tests__/models/form_test.js index dfaa385b..b732661f 100644 --- a/__tests__/models/form_test.js +++ b/__tests__/models/form_test.js @@ -95,6 +95,14 @@ describe('mount', () => { }) describe('markAsCompleted', () => { + beforeEach(() => { + Hellotext.visitBusinessId = 'business-1' + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + it('saves the form as completed in localStorage', () => { const form = new Form({ id: 1 }) form.markAsCompleted() @@ -108,6 +116,25 @@ describe('markAsCompleted', () => { form.markAsCompleted() expect(emit).toHaveBeenCalled() }) + + it('records a form activity for popup display rules', () => { + const form = new Form({ id: 1 }) + const recordActivity = jest.spyOn(Hellotext, 'recordActivity') + + form.markAsCompleted() + + expect(recordActivity).toHaveBeenCalledWith('form.completed') + }) + + it('does not attribute a pending form completion to a later business', () => { + const form = new Form({ id: 1 }) + const recordActivity = jest.spyOn(Hellotext, 'recordActivity') + Hellotext.visitBusinessId = 'business-2' + + form.markAsCompleted() + + expect(recordActivity).not.toHaveBeenCalled() + }) }) describe('localeAuthKey', () => { diff --git a/__tests__/models/page_path_test.js b/__tests__/models/page_path_test.js new file mode 100644 index 00000000..326f18fd --- /dev/null +++ b/__tests__/models/page_path_test.js @@ -0,0 +1,22 @@ +import { PagePath } from '../../src/models/page_path' +import fixture from '../fixtures/page_path_cases.json' + +describe('PagePath', () => { + // The Rails editor and Popup::DisplayRules::PagePath run these same cases, which is what + // keeps the path a merchant saves identical to the one the browser compares. + it.each(fixture.cases)('canonicalizes %j', ({ mode, input, expected }) => { + expect(PagePath.canonical(input, { mode })).toBe(expected) + }) + + it('reads contains and its exclusion as fragments and every other operator as a whole path', () => { + expect(PagePath.modeFor('contains')).toBe(PagePath.CONTAINS) + expect(PagePath.modeFor('does_not_contain')).toBe(PagePath.CONTAINS) + expect(PagePath.modeFor('is')).toBe(PagePath.EXACT) + expect(PagePath.modeFor('is_not')).toBe(PagePath.EXACT) + }) + + it('treats a missing value as empty', () => { + expect(PagePath.canonical(undefined)).toBe('') + expect(PagePath.canonical(null, { mode: PagePath.CONTAINS })).toBe('') + }) +}) diff --git a/__tests__/models/popup_display_rules_test.js b/__tests__/models/popup_display_rules_test.js new file mode 100644 index 00000000..5d4c7188 --- /dev/null +++ b/__tests__/models/popup_display_rules_test.js @@ -0,0 +1,440 @@ +import { PopupDisplayRules } from '../../src/models/popup_display_rules' + +function rules(...lanes) { + return new PopupDisplayRules({ + lanes: lanes.map(lane => + lane.map(([field, operator, values]) => ({ field, operator, values: [].concat(values) })), + ), + }) +} + +const page = context => ({ url: 'https://shop.test', path: '/', title: '', ...context }) + +describe('PopupDisplayRules', () => { + it('matches everything when the payload carries no lanes', () => { + expect(new PopupDisplayRules({ lanes: [] }).matches(page())).toBe(true) + }) + + it('fails closed unless the payload explicitly supplies a lanes array', () => { + expect(new PopupDisplayRules(undefined).matches(page())).toBe(false) + expect(new PopupDisplayRules(null).matches(page())).toBe(false) + expect(new PopupDisplayRules({}).matches(page())).toBe(false) + expect(new PopupDisplayRules({ lanes: null }).matches(page())).toBe(false) + expect(new PopupDisplayRules({ lanes: {} }).matches(page())).toBe(false) + }) + + it('requires every distinct field inside one lane', () => { + const definition = rules([ + ['page.path', 'contains', '/sale'], + ['page.title', 'contains', 'shoes'], + ]) + + expect(definition.matches(page({ path: '/sale/shoes', title: 'Running shoes' }))).toBe(true) + expect(definition.matches(page({ path: '/sale/shoes', title: 'Running hats' }))).toBe(false) + }) + + it('treats positive conditions for the same field as alternatives', () => { + const definition = rules([ + ['page.path', 'is', '/return-policy'], + ['page.path', 'contains', '/products/'], + ]) + + expect(definition.matches(page({ path: '/return-policy' }))).toBe(true) + expect(definition.matches(page({ path: '/products/574-core' }))).toBe(true) + expect(definition.matches(page({ path: '/blog' }))).toBe(false) + }) + + describe('Page URL spellings', () => { + it('matches a saved path however the site spells the page', () => { + const definition = rules([['page.path', 'is', '/sale']]) + + expect(definition.matches(page({ path: '/sale/' }))).toBe(true) + expect(definition.matches(page({ path: '/SALE' }))).toBe(true) + expect(definition.matches(page({ path: '/sale/index.html' }))).toBe(true) + expect(definition.matches(page({ path: '/sales' }))).toBe(false) + }) + + it('matches an accented path the browser reports percent-encoded', () => { + expect(rules([['page.path', 'is', '/café']]).matches(page({ path: '/caf%C3%A9' }))).toBe( + true, + ) + }) + + it('reads the route of a hash-routed site and ignores an in-page anchor', () => { + const definition = rules([['page.path', 'is', '/products/42']]) + + expect(definition.matches(page({ path: '/', hash: '#/products/42' }))).toBe(true) + expect(definition.matches(page({ path: '/', hash: '#!/products/42' }))).toBe(true) + expect(definition.matches(page({ path: '/products/42', hash: '#reviews' }))).toBe(true) + expect(definition.matches(page({ path: '/', hash: '#top' }))).toBe(false) + }) + + it('uses the path from a full URL and fails closed for an ambiguous bare domain', () => { + const definition = rules([['page.path', 'is', 'https://shop.test/sale']]) + + expect(definition.matches(page({ url: 'https://shop.test/sale', path: '/sale' }))).toBe(true) + expect(rules([['page.path', 'is', 'shop.test/sale']]).matches(page({ path: '/sale' }))).toBe(false) + }) + + it('does not turn an encoded slash into a path separator', () => { + const definition = rules([['page.path', 'is', '/a%2Fb']]) + + expect(definition.matches(page({ path: '/a%2Fb' }))).toBe(true) + expect(definition.matches(page({ path: '/a/b' }))).toBe(false) + }) + + it('keeps a trailing slash typed into contains as the pages under that path', () => { + const definition = rules([['page.path', 'contains', '/blog/']]) + + expect(definition.matches(page({ path: '/blog/first-post' }))).toBe(true) + expect(definition.matches(page({ path: '/blog' }))).toBe(false) + expect(definition.matches(page({ path: '/blog-news' }))).toBe(false) + }) + + it('fails closed on a fragment that would match every page', () => { + expect(rules([['page.path', 'contains', '/']]).matches(page({ path: '/sale' }))).toBe(false) + expect(rules([['page.path', 'does_not_contain', '/']]).matches(page({ path: '/sale' }))).toBe( + false, + ) + }) + }) + + // Campaign values are written into links by people and ad platforms, so rules cannot depend + // on capitalization. Query strings are decoded before they reach this evaluator. + describe('campaign spellings', () => { + const visit = utm => page({ utm }) + + it('ignores capitalization in every campaign field', () => { + expect( + rules([['session.utm_campaign', 'is', 'black friday']]).matches( + visit({ campaign: 'Black Friday' }), + ), + ).toBe(true) + expect( + rules([['session.utm_source', 'is', 'Instagram']]).matches(visit({ source: 'instagram' })), + ).toBe(true) + }) + + it('preserves a literal plus sign after the URL has been decoded', () => { + const words = rules([['session.utm_campaign', 'is', 'black friday']]) + const plus = rules([['session.utm_campaign', 'is', 'black+friday']]) + + expect(words.matches(visit({ campaign: 'Black Friday' }))).toBe(true) + expect(plus.matches(visit({ campaign: 'Black+Friday' }))).toBe(true) + expect(words.matches(visit({ campaign: 'Black+Friday' }))).toBe(false) + expect(plus.matches(visit({ campaign: 'Black Friday' }))).toBe(false) + }) + }) + + it('requires all exclusions for the same field', () => { + const definition = rules([ + ['page.path', 'does_not_contain', '/checkout'], + ['page.path', 'is_not', '/cart'], + ]) + + expect(definition.matches(page({ path: '/products/574-core' }))).toBe(true) + expect(definition.matches(page({ path: '/checkout' }))).toBe(false) + expect(definition.matches(page({ path: '/cart' }))).toBe(false) + }) + + it('combines positive alternatives and exclusions with other fields', () => { + const definition = rules([ + ['page.path', 'is', '/return-policy'], + ['page.path', 'contains', '/products/'], + ['page.path', 'does_not_contain', '/checkout'], + ['page.title', 'contains', 'shoes'], + ]) + + expect(definition.matches(page({ path: '/products/574-core', title: 'Running shoes' }))).toBe(true) + expect(definition.matches(page({ path: '/products/checkout', title: 'Running shoes' }))).toBe(false) + expect(definition.matches(page({ path: '/products/574-core', title: 'Coats' }))).toBe(false) + }) + + // Every field authored as includable and excludable rows reads the same way, not just + // Page URL: before this, the same rule on any other text field required both halves at + // once and could never match. + it('reads every list-valued field as alternatives plus exclusions', () => { + const definition = rules([ + ['page.title', 'is', 'Sale'], + ['page.title', 'contains', 'shoes'], + ]) + + expect(definition.matches(page({ title: 'Sale' }))).toBe(true) + expect(definition.matches(page({ title: 'Running shoes' }))).toBe(true) + expect(definition.matches(page({ title: 'Coats' }))).toBe(false) + }) + + // A threshold holds one number, so repeated conditions are requirements rather than + // alternatives — treating them as alternatives would quietly widen who sees the popup. + it('keeps fields that hold no list as ordinary AND conditions', () => { + const definition = rules([ + ['session.scroll_depth', 'at_least', 50], + ['session.scroll_depth', 'at_least', 80], + ]) + + expect(definition.matches(page({ scrollDepth: 90 }))).toBe(true) + expect(definition.matches(page({ scrollDepth: 60 }))).toBe(false) + }) + + it('matches when any lane matches', () => { + const definition = rules( + [['page.path', 'contains', '/sale']], + [['page.path', 'contains', '/outlet']], + ) + + expect(definition.matches(page({ path: '/outlet/new' }))).toBe(true) + expect(definition.matches(page({ path: '/blog' }))).toBe(false) + }) + + it('treats several values in one condition as alternatives', () => { + const definition = rules([['page.path', 'contains', ['/sale', '/outlet']]]) + + expect(definition.matches(page({ path: '/outlet' }))).toBe(true) + expect(definition.matches(page({ path: '/blog' }))).toBe(false) + }) + + it('compares strings case-insensitively', () => { + expect( + rules([['page.title', 'contains', 'SHOES']]).matches(page({ title: 'Running shoes' })), + ).toBe(true) + }) + + // Prefix and suffix matching left the catalog: neither has a negative twin, so a row + // carrying one could never be flipped to an exclusion in the editor. + it('rejects removed fields and the operators that lost their twin', () => { + expect(rules([['page.url', 'contains', 'shop.test']]).matches(page())).toBe(false) + + for (const field of ['page.path', 'page.title', 'session.referrer']) { + const value = field === 'page.path' ? '/sale' : 'Sale' + const context = field === 'page.path' ? { path: '/sale' } : { [field.split('.')[1]]: 'Sale' } + + expect(rules([[field, 'starts_with', value.slice(0, 2)]]).matches(page(context))).toBe(false) + expect(rules([[field, 'ends_with', value.slice(-2)]]).matches(page(context))).toBe(false) + } + }) + + // A closed set matches a whole value or none of it, so a substring operator on one is a + // condition the server would never have saved. + it('rejects substring operators on closed sets', () => { + expect(rules([['session.browser', 'contains', 'chr']]).matches(page({ browser: 'chrome' }))).toBe(false) + expect(rules([['session.language', 'contains', 'e']]).matches(page({ language: 'es' }))).toBe(false) + }) + + describe('negative operators', () => { + it('matches a page that does not carry the value', () => { + const definition = rules([['page.path', 'does_not_contain', '/cart']]) + + expect(definition.matches(page({ path: '/sale' }))).toBe(true) + expect(definition.matches(page({ path: '/cart' }))).toBe(false) + }) + + // Complement semantics: a visitor with no referrer satisfies "referrer is not google". + it('matches when the value is missing entirely', () => { + expect(rules([['session.referrer', 'does_not_contain', 'google']]).matches(page())).toBe(true) + }) + + it('fails a positive operator when the value is missing', () => { + expect(rules([['session.referrer', 'contains', 'google']]).matches(page())).toBe(false) + }) + }) + + describe('thresholds', () => { + it('matches once the measurement reaches the threshold', () => { + const definition = rules([['session.scroll_depth', 'at_least', 50]]) + + expect(definition.matches(page({ scrollDepth: 50 }))).toBe(true) + expect(definition.matches(page({ scrollDepth: 80 }))).toBe(true) + expect(definition.matches(page({ scrollDepth: 20 }))).toBe(false) + }) + + it('matches the number of pages viewed in the current visit', () => { + expect(rules([['session.page_views', 'at_least', 3]]).matches(page({ pageViews: 3 }))).toBe( + true, + ) + expect(rules([['session.page_views', 'at_least', 3]]).matches(page({ pageViews: 2 }))).toBe( + false, + ) + }) + + it('does not match before a measurement exists', () => { + expect(rules([['session.time_on_page', 'at_least', 5]]).matches(page())).toBe(false) + }) + + // Mirrors spec/models/popup/display_rules/page_evaluator_spec.rb so the two copies of + // this comparison cannot drift. + it('compares a measurement from either side', () => { + const expectations = { + at_least: { 50: true, 49: false, 51: true }, + at_most: { 50: true, 49: true, 51: false }, + greater_than: { 50: false, 49: false, 51: true }, + less_than: { 50: false, 49: true, 51: false }, + } + + Object.entries(expectations).forEach(([operator, cases]) => { + const definition = rules([['session.scroll_depth', operator, 50]]) + + Object.entries(cases).forEach(([actual, expected]) => { + expect(definition.matches(page({ scrollDepth: Number(actual) }))).toBe(expected) + }) + }) + }) + + // A range is two conditions, not a two-valued one: the lane already ANDs repeated + // conditions on a threshold. + it('reads a pair of bounds in one lane as a range', () => { + const definition = rules([ + ['session.scroll_depth', 'at_least', 25], + ['session.scroll_depth', 'at_most', 75], + ]) + + expect(definition.matches(page({ scrollDepth: 50 }))).toBe(true) + expect(definition.matches(page({ scrollDepth: 25 }))).toBe(true) + expect(definition.matches(page({ scrollDepth: 75 }))).toBe(true) + expect(definition.matches(page({ scrollDepth: 24 }))).toBe(false) + expect(definition.matches(page({ scrollDepth: 76 }))).toBe(false) + }) + + // The downward comparisons are the ones an absent measurement could wrongly satisfy: + // zero is "at most 2", but nothing has been counted yet. + it('does not satisfy a downward comparison before anything is measured', () => { + expect(rules([['session.page_views', 'at_most', 2]]).matches(page())).toBe(false) + expect(rules([['session.page_views', 'less_than', 2]]).matches(page())).toBe(false) + }) + + it('refuses a comparison the catalog does not offer', () => { + expect(rules([['session.scroll_depth', 'between', 50]]).matches(page({ scrollDepth: 60 }))).toBe( + false, + ) + }) + + it('reports whether the runtime has to keep re-checking', () => { + expect(rules([['session.scroll_depth', 'at_least', 50]]).needsMeasurements).toBe(true) + expect(rules([['session.time_on_page', 'at_least', 5]]).needsMeasurements).toBe(true) + expect(rules([['page.path', 'contains', '/sale']]).needsMeasurements).toBe(false) + expect(new PopupDisplayRules({ lanes: [] }).needsMeasurements).toBe(false) + }) + }) + + it('matches browser language, visitor type and persisted UTM values', () => { + const context = page({ + language: 'es', + visitorType: 'returning', + utm: { source: 'instagram', medium: 'social', campaign: 'summer' }, + }) + + expect(rules([['session.language', 'is', 'es']]).matches(context)).toBe(true) + expect(rules([['session.visitor_type', 'is', 'returning']]).matches(context)).toBe(true) + expect(rules([['session.utm_source', 'contains', 'insta']]).matches(context)).toBe(true) + expect(rules([['session.utm_medium', 'is', 'social']]).matches(context)).toBe(true) + expect(rules([['session.utm_campaign', 'contains', 'mer']]).matches(context)).toBe(true) + }) + + it('rejects visitor types outside the browser contract', () => { + expect( + rules([['session.visitor_type', 'is', 'sometimes']]).matches( + page({ visitorType: 'sometimes' }), + ), + ).toBe(false) + }) + + it('rejects languages outside the Americas browser-language catalog', () => { + expect(rules([['session.language', 'is', 'de']]).matches(page({ language: 'de' }))).toBe( + false, + ) + }) + + it('matches the browser and excludes it', () => { + const context = page({ browser: 'safari' }) + + expect(rules([['session.browser', 'is', 'safari']]).matches(context)).toBe(true) + expect(rules([['session.browser', 'is', 'chrome']]).matches(context)).toBe(false) + expect(rules([['session.browser', 'is_not', 'chrome']]).matches(context)).toBe(true) + }) + + // A browser the runtime could not name reports nothing. `is` must not match on that, and + // `is not` must, the way every other missing value behaves. + it('treats an unnamed browser as a missing value', () => { + expect(rules([['session.browser', 'is', 'chrome']]).matches(page({}))).toBe(false) + expect(rules([['session.browser', 'is_not', 'chrome']]).matches(page({}))).toBe(true) + }) + + it('rejects a browser outside the closed set', () => { + expect( + rules([['session.browser', 'is', 'netscape']]).matches(page({ browser: 'netscape' })), + ).toBe(false) + }) + + describe('activity conditions', () => { + it('matches a supported activity observed in the current visit', () => { + const definition = rules([['activity.product_viewed', 'occurred', []]]) + + expect( + definition.matches(page({ activities: new Set(['activity.product_viewed']) })), + ).toBe(true) + expect(definition.matches(page({ activities: new Set() }))).toBe(false) + expect(definition.needsActivities).toBe(true) + }) + + it('fails closed for values or operators outside the event contract', () => { + expect( + rules([['activity.product_viewed', 'occurred', ['once']]]).matches( + page({ activities: new Set(['activity.product_viewed']) }), + ), + ).toBe(false) + expect( + rules([['activity.product_viewed', 'is', []]]).matches( + page({ activities: new Set(['activity.product_viewed']) }), + ), + ).toBe(false) + }) + }) + + // The server strips visitor conditions after deciding them, so a lane can arrive empty. + // An empty lane is satisfied and the popup displays. + it('treats a lane emptied by server-side evaluation as satisfied', () => { + expect(new PopupDisplayRules({ lanes: [[]] }).matches(page())).toBe(true) + }) + + it('ignores a field it does not know instead of throwing', () => { + expect(rules([['profile.country', 'is', 'uy']]).matches(page())).toBe(false) + }) + + it('fails closed for malformed lanes and conditions', () => { + expect(new PopupDisplayRules({ lanes: [null] }).matches(page())).toBe(false) + expect(new PopupDisplayRules({ lanes: [[null]] }).matches(page())).toBe(false) + expect( + new PopupDisplayRules({ + lanes: [[{ field: 'page.path', operator: 'does_not_contain', values: [] }]], + }).matches(page()), + ).toBe(false) + }) + + it('does not throw for malformed payloads', () => { + expect(() => + new PopupDisplayRules({ lanes: [{ field: 'page.path' }] }).matches(page()), + ).not.toThrow() + }) + + it('fails closed for values outside the catalog bounds', () => { + expect( + new PopupDisplayRules({ + lanes: [[{ field: 'session.scroll_depth', operator: 'at_least', values: [''] }]], + }).matches(page({ scrollDepth: 100 })), + ).toBe(false) + expect( + new PopupDisplayRules({ + lanes: [[{ field: 'session.time_on_page', operator: 'at_least', values: [3601] }]], + }).matches(page({ timeOnPage: 3601 })), + ).toBe(false) + expect( + new PopupDisplayRules({ + lanes: [[{ field: 'page.path', operator: 'contains', values: [' '] }]], + }).matches(page()), + ).toBe(false) + expect( + new PopupDisplayRules({ + lanes: [[{ field: 'page.path', operator: 'contains', values: ['a'.repeat(513)] }]], + }).matches(page()), + ).toBe(false) + }) +}) diff --git a/__tests__/models/utm_test.js b/__tests__/models/utm_test.js index 797667bb..3a507c1b 100644 --- a/__tests__/models/utm_test.js +++ b/__tests__/models/utm_test.js @@ -46,6 +46,34 @@ describe('UTM', () => { jest.useRealTimers() }) + describe('paramsFrom', () => { + it('reads every campaign parameter a query string carries', () => { + expect( + UTM.paramsFrom('?utm_source=google&utm_medium=cpc&utm_campaign=spring&utm_term=shoes&utm_content=ad1'), + ).toEqual({ source: 'google', medium: 'cpc', campaign: 'spring', term: 'shoes', content: 'ad1' }) + }) + + it('keeps a lone parameter the persisted attribution would ignore', () => { + expect(UTM.paramsFrom('?utm_campaign=spring')).toEqual({ campaign: 'spring' }) + }) + + it('leaves out absent and blank parameters', () => { + expect(UTM.paramsFrom('?utm_source=&page=2')).toEqual({}) + expect(UTM.paramsFrom('')).toEqual({}) + }) + + it('uses the first value when a campaign parameter is repeated', () => { + expect(UTM.paramsFrom('?utm_source=first&utm_source=second')).toEqual({ source: 'first' }) + }) + + it('uses standard query-string decoding without losing literal plus signs', () => { + expect(UTM.paramsFrom('?utm_campaign=Black+Friday')).toEqual({ campaign: 'Black Friday' }) + expect(UTM.paramsFrom('?utm_campaign=Black%20Friday')).toEqual({ campaign: 'Black Friday' }) + expect(UTM.paramsFrom('?utm_campaign=Black%2BFriday')).toEqual({ campaign: 'Black+Friday' }) + expect(UTM.paramsFrom('?utm_campaign=Black%252BFriday')).toEqual({ campaign: 'Black%2BFriday' }) + }) + }) + describe('constructor', () => { it('stores UTM parameters in cookies when utm_source and utm_medium are present', () => { window.location.search = '?utm_source=google&utm_medium=cpc&utm_campaign=summer_sale&utm_term=shoes&utm_content=ad1' diff --git a/dist/hellotext.js b/dist/hellotext.js index 327429cf..6dc05321 100644 --- a/dist/hellotext.js +++ b/dist/hellotext.js @@ -1,2 +1,2 @@ /*! For license information please see hellotext.js.LICENSE.txt */ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Hellotext=t():e.Hellotext=t()}(Object("undefined"!=typeof self?self:this),()=>(()=>{"use strict";var e={891(e,t,s){s.d(t,{lg:()=>G,xI:()=>ne});class i{constructor(e,t,s){this.eventTarget=e,this.eventName=t,this.eventOptions=s,this.unorderedBindings=new Set}connect(){this.eventTarget.addEventListener(this.eventName,this,this.eventOptions)}disconnect(){this.eventTarget.removeEventListener(this.eventName,this,this.eventOptions)}bindingConnected(e){this.unorderedBindings.add(e)}bindingDisconnected(e){this.unorderedBindings.delete(e)}handleEvent(e){const t=function(e){if("immediatePropagationStopped"in e)return e;{const{stopImmediatePropagation:t}=e;return Object.assign(e,{immediatePropagationStopped:!1,stopImmediatePropagation(){this.immediatePropagationStopped=!0,t.call(this)}})}}(e);for(const e of this.bindings){if(t.immediatePropagationStopped)break;e.handleEvent(t)}}hasBindings(){return this.unorderedBindings.size>0}get bindings(){return Array.from(this.unorderedBindings).sort((e,t)=>{const s=e.index,i=t.index;return si?1:0})}}class n{constructor(e){this.application=e,this.eventListenerMaps=new Map,this.started=!1}start(){this.started||(this.started=!0,this.eventListeners.forEach(e=>e.connect()))}stop(){this.started&&(this.started=!1,this.eventListeners.forEach(e=>e.disconnect()))}get eventListeners(){return Array.from(this.eventListenerMaps.values()).reduce((e,t)=>e.concat(Array.from(t.values())),[])}bindingConnected(e){this.fetchEventListenerForBinding(e).bindingConnected(e)}bindingDisconnected(e,t=!1){this.fetchEventListenerForBinding(e).bindingDisconnected(e),t&&this.clearEventListenersForBinding(e)}handleError(e,t,s={}){this.application.handleError(e,`Error ${t}`,s)}clearEventListenersForBinding(e){const t=this.fetchEventListenerForBinding(e);t.hasBindings()||(t.disconnect(),this.removeMappedEventListenerFor(e))}removeMappedEventListenerFor(e){const{eventTarget:t,eventName:s,eventOptions:i}=e,n=this.fetchEventListenerMapForEventTarget(t),r=this.cacheKey(s,i);n.delete(r),0==n.size&&this.eventListenerMaps.delete(t)}fetchEventListenerForBinding(e){const{eventTarget:t,eventName:s,eventOptions:i}=e;return this.fetchEventListener(t,s,i)}fetchEventListener(e,t,s){const i=this.fetchEventListenerMapForEventTarget(e),n=this.cacheKey(t,s);let r=i.get(n);return r||(r=this.createEventListener(e,t,s),i.set(n,r)),r}createEventListener(e,t,s){const n=new i(e,t,s);return this.started&&n.connect(),n}fetchEventListenerMapForEventTarget(e){let t=this.eventListenerMaps.get(e);return t||(t=new Map,this.eventListenerMaps.set(e,t)),t}cacheKey(e,t){const s=[e];return Object.keys(t).sort().forEach(e=>{s.push(`${t[e]?"":"!"}${e}`)}),s.join(":")}}const r={stop:({event:e,value:t})=>(t&&e.stopPropagation(),!0),prevent:({event:e,value:t})=>(t&&e.preventDefault(),!0),self:({event:e,value:t,element:s})=>!t||s===e.target},o=/^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;function a(e){return e.replace(/(?:[_-])([a-z0-9])/g,(e,t)=>t.toUpperCase())}function c(e){return a(e.replace(/--/g,"-").replace(/__/g,"_"))}function l(e){return e.charAt(0).toUpperCase()+e.slice(1)}function h(e){return e.replace(/([A-Z])/g,(e,t)=>`-${t.toLowerCase()}`)}function u(e){return null!=e}function d(e,t){return Object.prototype.hasOwnProperty.call(e,t)}const p=["meta","ctrl","alt","shift"];class m{constructor(e,t,s,i){this.element=e,this.index=t,this.eventTarget=s.eventTarget||e,this.eventName=s.eventName||function(e){const t=e.tagName.toLowerCase();if(t in g)return g[t](e)}(e)||f("missing event name"),this.eventOptions=s.eventOptions||{},this.identifier=s.identifier||f("missing identifier"),this.methodName=s.methodName||f("missing method name"),this.keyFilter=s.keyFilter||"",this.schema=i}static forToken(e,t){return new this(e.element,e.index,function(e){const t=e.trim().match(o)||[];let s=t[2],i=t[3];return i&&!["keydown","keyup","keypress"].includes(s)&&(s+=`.${i}`,i=""),{eventTarget:(n=t[4],"window"==n?window:"document"==n?document:void 0),eventName:s,eventOptions:t[7]?(r=t[7],r.split(":").reduce((e,t)=>Object.assign(e,{[t.replace(/^!/,"")]:!/^!/.test(t)}),{})):{},identifier:t[5],methodName:t[6],keyFilter:t[1]||i};var n,r}(e.content),t)}toString(){const e=this.keyFilter?`.${this.keyFilter}`:"",t=this.eventTargetName?`@${this.eventTargetName}`:"";return`${this.eventName}${e}${t}->${this.identifier}#${this.methodName}`}shouldIgnoreKeyboardEvent(e){if(!this.keyFilter)return!1;const t=this.keyFilter.split("+");if(this.keyFilterDissatisfied(e,t))return!0;const s=t.filter(e=>!p.includes(e))[0];return!!s&&(d(this.keyMappings,s)||f(`contains unknown key filter: ${this.keyFilter}`),this.keyMappings[s].toLowerCase()!==e.key.toLowerCase())}shouldIgnoreMouseEvent(e){if(!this.keyFilter)return!1;const t=[this.keyFilter];return!!this.keyFilterDissatisfied(e,t)}get params(){const e={},t=new RegExp(`^data-${this.identifier}-(.+)-param$`,"i");for(const{name:s,value:i}of Array.from(this.element.attributes)){const n=s.match(t),r=n&&n[1];r&&(e[a(r)]=b(i))}return e}get eventTargetName(){return(e=this.eventTarget)==window?"window":e==document?"document":void 0;var e}get keyMappings(){return this.schema.keyMappings}keyFilterDissatisfied(e,t){const[s,i,n,r]=p.map(e=>t.includes(e));return e.metaKey!==s||e.ctrlKey!==i||e.altKey!==n||e.shiftKey!==r}}const g={a:()=>"click",button:()=>"click",form:()=>"submit",details:()=>"toggle",input:e=>"submit"==e.getAttribute("type")?"click":"input",select:()=>"change",textarea:()=>"input"};function f(e){throw new Error(e)}function b(e){try{return JSON.parse(e)}catch(t){return e}}class y{constructor(e,t){this.context=e,this.action=t}get index(){return this.action.index}get eventTarget(){return this.action.eventTarget}get eventOptions(){return this.action.eventOptions}get identifier(){return this.context.identifier}handleEvent(e){const t=this.prepareActionEvent(e);this.willBeInvokedByEvent(e)&&this.applyEventModifiers(t)&&this.invokeWithEvent(t)}get eventName(){return this.action.eventName}get method(){const e=this.controller[this.methodName];if("function"==typeof e)return e;throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)}applyEventModifiers(e){const{element:t}=this.action,{actionDescriptorFilters:s}=this.context.application,{controller:i}=this.context;let n=!0;for(const[r,o]of Object.entries(this.eventOptions))if(r in s){const a=s[r];n=n&&a({name:r,value:o,event:e,element:t,controller:i})}return n}prepareActionEvent(e){return Object.assign(e,{params:this.action.params})}invokeWithEvent(e){const{target:t,currentTarget:s}=e;try{this.method.call(this.controller,e),this.context.logDebugActivity(this.methodName,{event:e,target:t,currentTarget:s,action:this.methodName})}catch(t){const{identifier:s,controller:i,element:n,index:r}=this,o={identifier:s,controller:i,element:n,index:r,event:e};this.context.handleError(t,`invoking action "${this.action}"`,o)}}willBeInvokedByEvent(e){const t=e.target;return!(e instanceof KeyboardEvent&&this.action.shouldIgnoreKeyboardEvent(e))&&!(e instanceof MouseEvent&&this.action.shouldIgnoreMouseEvent(e))&&(this.element===t||(t instanceof Element&&this.element.contains(t)?this.scope.containsElement(t):this.scope.containsElement(this.action.element)))}get controller(){return this.context.controller}get methodName(){return this.action.methodName}get element(){return this.scope.element}get scope(){return this.context.scope}}class v{constructor(e,t){this.mutationObserverInit={attributes:!0,childList:!0,subtree:!0},this.element=e,this.started=!1,this.delegate=t,this.elements=new Set,this.mutationObserver=new MutationObserver(e=>this.processMutations(e))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,this.mutationObserverInit),this.refresh())}pause(e){this.started&&(this.mutationObserver.disconnect(),this.started=!1),e(),this.started||(this.mutationObserver.observe(this.element,this.mutationObserverInit),this.started=!0)}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started){const e=new Set(this.matchElementsInTree());for(const t of Array.from(this.elements))e.has(t)||this.removeElement(t);for(const t of Array.from(e))this.addElement(t)}}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){"attributes"==e.type?this.processAttributeChange(e.target,e.attributeName):"childList"==e.type&&(this.processRemovedNodes(e.removedNodes),this.processAddedNodes(e.addedNodes))}processAttributeChange(e,t){this.elements.has(e)?this.delegate.elementAttributeChanged&&this.matchElement(e)?this.delegate.elementAttributeChanged(e,t):this.removeElement(e):this.matchElement(e)&&this.addElement(e)}processRemovedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.processTree(e,this.removeElement)}}processAddedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.elementIsActive(e)&&this.processTree(e,this.addElement)}}matchElement(e){return this.delegate.matchElement(e)}matchElementsInTree(e=this.element){return this.delegate.matchElementsInTree(e)}processTree(e,t){for(const s of this.matchElementsInTree(e))t.call(this,s)}elementFromNode(e){if(e.nodeType==Node.ELEMENT_NODE)return e}elementIsActive(e){return e.isConnected==this.element.isConnected&&this.element.contains(e)}addElement(e){this.elements.has(e)||this.elementIsActive(e)&&(this.elements.add(e),this.delegate.elementMatched&&this.delegate.elementMatched(e))}removeElement(e){this.elements.has(e)&&(this.elements.delete(e),this.delegate.elementUnmatched&&this.delegate.elementUnmatched(e))}}class w{constructor(e,t,s){this.attributeName=t,this.delegate=s,this.elementObserver=new v(e,this)}get element(){return this.elementObserver.element}get selector(){return`[${this.attributeName}]`}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get started(){return this.elementObserver.started}matchElement(e){return e.hasAttribute(this.attributeName)}matchElementsInTree(e){const t=this.matchElement(e)?[e]:[],s=Array.from(e.querySelectorAll(this.selector));return t.concat(s)}elementMatched(e){this.delegate.elementMatchedAttribute&&this.delegate.elementMatchedAttribute(e,this.attributeName)}elementUnmatched(e){this.delegate.elementUnmatchedAttribute&&this.delegate.elementUnmatchedAttribute(e,this.attributeName)}elementAttributeChanged(e,t){this.delegate.elementAttributeValueChanged&&this.attributeName==t&&this.delegate.elementAttributeValueChanged(e,t)}}function T(e,t){let s=e.get(t);return s||(s=new Set,e.set(t,s)),s}class S{constructor(){this.valuesByKey=new Map}get keys(){return Array.from(this.valuesByKey.keys())}get values(){return Array.from(this.valuesByKey.values()).reduce((e,t)=>e.concat(Array.from(t)),[])}get size(){return Array.from(this.valuesByKey.values()).reduce((e,t)=>e+t.size,0)}add(e,t){!function(e,t,s){T(e,t).add(s)}(this.valuesByKey,e,t)}delete(e,t){!function(e,t,s){T(e,t).delete(s),function(e,t){const s=e.get(t);null!=s&&0==s.size&&e.delete(t)}(e,t)}(this.valuesByKey,e,t)}has(e,t){const s=this.valuesByKey.get(e);return null!=s&&s.has(t)}hasKey(e){return this.valuesByKey.has(e)}hasValue(e){return Array.from(this.valuesByKey.values()).some(t=>t.has(e))}getValuesForKey(e){const t=this.valuesByKey.get(e);return t?Array.from(t):[]}getKeysForValue(e){return Array.from(this.valuesByKey).filter(([t,s])=>s.has(e)).map(([e,t])=>e)}}class C{constructor(e,t,s,i){this._selector=t,this.details=i,this.elementObserver=new v(e,this),this.delegate=s,this.matchesByElement=new S}get started(){return this.elementObserver.started}get selector(){return this._selector}set selector(e){this._selector=e,this.refresh()}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get element(){return this.elementObserver.element}matchElement(e){const{selector:t}=this;if(t){const s=e.matches(t);return this.delegate.selectorMatchElement?s&&this.delegate.selectorMatchElement(e,this.details):s}return!1}matchElementsInTree(e){const{selector:t}=this;if(t){const s=this.matchElement(e)?[e]:[],i=Array.from(e.querySelectorAll(t)).filter(e=>this.matchElement(e));return s.concat(i)}return[]}elementMatched(e){const{selector:t}=this;t&&this.selectorMatched(e,t)}elementUnmatched(e){const t=this.matchesByElement.getKeysForValue(e);for(const s of t)this.selectorUnmatched(e,s)}elementAttributeChanged(e,t){const{selector:s}=this;if(s){const t=this.matchElement(e),i=this.matchesByElement.has(s,e);t&&!i?this.selectorMatched(e,s):!t&&i&&this.selectorUnmatched(e,s)}}selectorMatched(e,t){this.delegate.selectorMatched(e,t,this.details),this.matchesByElement.add(t,e)}selectorUnmatched(e,t){this.delegate.selectorUnmatched(e,t,this.details),this.matchesByElement.delete(t,e)}}class O{constructor(e,t){this.element=e,this.delegate=t,this.started=!1,this.stringMap=new Map,this.mutationObserver=new MutationObserver(e=>this.processMutations(e))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,{attributes:!0,attributeOldValue:!0}),this.refresh())}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started)for(const e of this.knownAttributeNames)this.refreshAttribute(e,null)}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){const t=e.attributeName;t&&this.refreshAttribute(t,e.oldValue)}refreshAttribute(e,t){const s=this.delegate.getStringMapKeyForAttribute(e);if(null!=s){this.stringMap.has(e)||this.stringMapKeyAdded(s,e);const i=this.element.getAttribute(e);if(this.stringMap.get(e)!=i&&this.stringMapValueChanged(i,s,t),null==i){const t=this.stringMap.get(e);this.stringMap.delete(e),t&&this.stringMapKeyRemoved(s,e,t)}else this.stringMap.set(e,i)}}stringMapKeyAdded(e,t){this.delegate.stringMapKeyAdded&&this.delegate.stringMapKeyAdded(e,t)}stringMapValueChanged(e,t,s){this.delegate.stringMapValueChanged&&this.delegate.stringMapValueChanged(e,t,s)}stringMapKeyRemoved(e,t,s){this.delegate.stringMapKeyRemoved&&this.delegate.stringMapKeyRemoved(e,t,s)}get knownAttributeNames(){return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))}get currentAttributeNames(){return Array.from(this.element.attributes).map(e=>e.name)}get recordedAttributeNames(){return Array.from(this.stringMap.keys())}}class E{constructor(e,t,s){this.attributeObserver=new w(e,t,this),this.delegate=s,this.tokensByElement=new S}get started(){return this.attributeObserver.started}start(){this.attributeObserver.start()}pause(e){this.attributeObserver.pause(e)}stop(){this.attributeObserver.stop()}refresh(){this.attributeObserver.refresh()}get element(){return this.attributeObserver.element}get attributeName(){return this.attributeObserver.attributeName}elementMatchedAttribute(e){this.tokensMatched(this.readTokensForElement(e))}elementAttributeValueChanged(e){const[t,s]=this.refreshTokensForElement(e);this.tokensUnmatched(t),this.tokensMatched(s)}elementUnmatchedAttribute(e){this.tokensUnmatched(this.tokensByElement.getValuesForKey(e))}tokensMatched(e){e.forEach(e=>this.tokenMatched(e))}tokensUnmatched(e){e.forEach(e=>this.tokenUnmatched(e))}tokenMatched(e){this.delegate.tokenMatched(e),this.tokensByElement.add(e.element,e)}tokenUnmatched(e){this.delegate.tokenUnmatched(e),this.tokensByElement.delete(e.element,e)}refreshTokensForElement(e){const t=this.tokensByElement.getValuesForKey(e),s=this.readTokensForElement(e),i=function(e,t){const s=Math.max(e.length,t.length);return Array.from({length:s},(s,i)=>[e[i],t[i]])}(t,s).findIndex(([e,t])=>{return i=t,!((s=e)&&i&&s.index==i.index&&s.content==i.content);var s,i});return-1==i?[[],[]]:[t.slice(i),s.slice(i)]}readTokensForElement(e){const t=this.attributeName;return function(e,t,s){return e.trim().split(/\s+/).filter(e=>e.length).map((e,i)=>({element:t,attributeName:s,content:e,index:i}))}(e.getAttribute(t)||"",e,t)}}class A{constructor(e,t,s){this.tokenListObserver=new E(e,t,this),this.delegate=s,this.parseResultsByToken=new WeakMap,this.valuesByTokenByElement=new WeakMap}get started(){return this.tokenListObserver.started}start(){this.tokenListObserver.start()}stop(){this.tokenListObserver.stop()}refresh(){this.tokenListObserver.refresh()}get element(){return this.tokenListObserver.element}get attributeName(){return this.tokenListObserver.attributeName}tokenMatched(e){const{element:t}=e,{value:s}=this.fetchParseResultForToken(e);s&&(this.fetchValuesByTokenForElement(t).set(e,s),this.delegate.elementMatchedValue(t,s))}tokenUnmatched(e){const{element:t}=e,{value:s}=this.fetchParseResultForToken(e);s&&(this.fetchValuesByTokenForElement(t).delete(e),this.delegate.elementUnmatchedValue(t,s))}fetchParseResultForToken(e){let t=this.parseResultsByToken.get(e);return t||(t=this.parseToken(e),this.parseResultsByToken.set(e,t)),t}fetchValuesByTokenForElement(e){let t=this.valuesByTokenByElement.get(e);return t||(t=new Map,this.valuesByTokenByElement.set(e,t)),t}parseToken(e){try{return{value:this.delegate.parseValueForToken(e)}}catch(e){return{error:e}}}}class x{constructor(e,t){this.context=e,this.delegate=t,this.bindingsByAction=new Map}start(){this.valueListObserver||(this.valueListObserver=new A(this.element,this.actionAttribute,this),this.valueListObserver.start())}stop(){this.valueListObserver&&(this.valueListObserver.stop(),delete this.valueListObserver,this.disconnectAllActions())}get element(){return this.context.element}get identifier(){return this.context.identifier}get actionAttribute(){return this.schema.actionAttribute}get schema(){return this.context.schema}get bindings(){return Array.from(this.bindingsByAction.values())}connectAction(e){const t=new y(this.context,e);this.bindingsByAction.set(e,t),this.delegate.bindingConnected(t)}disconnectAction(e){const t=this.bindingsByAction.get(e);t&&(this.bindingsByAction.delete(e),this.delegate.bindingDisconnected(t))}disconnectAllActions(){this.bindings.forEach(e=>this.delegate.bindingDisconnected(e,!0)),this.bindingsByAction.clear()}parseValueForToken(e){const t=m.forToken(e,this.schema);if(t.identifier==this.identifier)return t}elementMatchedValue(e,t){this.connectAction(t)}elementUnmatchedValue(e,t){this.disconnectAction(t)}}class M{constructor(e,t){this.context=e,this.receiver=t,this.stringMapObserver=new O(this.element,this),this.valueDescriptorMap=this.controller.valueDescriptorMap}start(){this.stringMapObserver.start(),this.invokeChangedCallbacksForDefaultValues()}stop(){this.stringMapObserver.stop()}get element(){return this.context.element}get controller(){return this.context.controller}getStringMapKeyForAttribute(e){if(e in this.valueDescriptorMap)return this.valueDescriptorMap[e].name}stringMapKeyAdded(e,t){const s=this.valueDescriptorMap[t];this.hasValue(e)||this.invokeChangedCallback(e,s.writer(this.receiver[e]),s.writer(s.defaultValue))}stringMapValueChanged(e,t,s){const i=this.valueDescriptorNameMap[t];null!==e&&(null===s&&(s=i.writer(i.defaultValue)),this.invokeChangedCallback(t,e,s))}stringMapKeyRemoved(e,t,s){const i=this.valueDescriptorNameMap[e];this.hasValue(e)?this.invokeChangedCallback(e,i.writer(this.receiver[e]),s):this.invokeChangedCallback(e,i.writer(i.defaultValue),s)}invokeChangedCallbacksForDefaultValues(){for(const{key:e,name:t,defaultValue:s,writer:i}of this.valueDescriptors)null==s||this.controller.data.has(e)||this.invokeChangedCallback(t,i(s),void 0)}invokeChangedCallback(e,t,s){const i=`${e}Changed`,n=this.receiver[i];if("function"==typeof n){const i=this.valueDescriptorNameMap[e];try{const e=i.reader(t);let r=s;s&&(r=i.reader(s)),n.call(this.receiver,e,r)}catch(e){throw e instanceof TypeError&&(e.message=`Stimulus Value "${this.context.identifier}.${i.name}" - ${e.message}`),e}}}get valueDescriptors(){const{valueDescriptorMap:e}=this;return Object.keys(e).map(t=>e[t])}get valueDescriptorNameMap(){const e={};return Object.keys(this.valueDescriptorMap).forEach(t=>{const s=this.valueDescriptorMap[t];e[s.name]=s}),e}hasValue(e){const t=`has${l(this.valueDescriptorNameMap[e].name)}`;return this.receiver[t]}}class k{constructor(e,t){this.context=e,this.delegate=t,this.targetsByName=new S}start(){this.tokenListObserver||(this.tokenListObserver=new E(this.element,this.attributeName,this),this.tokenListObserver.start())}stop(){this.tokenListObserver&&(this.disconnectAllTargets(),this.tokenListObserver.stop(),delete this.tokenListObserver)}tokenMatched({element:e,content:t}){this.scope.containsElement(e)&&this.connectTarget(e,t)}tokenUnmatched({element:e,content:t}){this.disconnectTarget(e,t)}connectTarget(e,t){var s;this.targetsByName.has(t,e)||(this.targetsByName.add(t,e),null===(s=this.tokenListObserver)||void 0===s||s.pause(()=>this.delegate.targetConnected(e,t)))}disconnectTarget(e,t){var s;this.targetsByName.has(t,e)&&(this.targetsByName.delete(t,e),null===(s=this.tokenListObserver)||void 0===s||s.pause(()=>this.delegate.targetDisconnected(e,t)))}disconnectAllTargets(){for(const e of this.targetsByName.keys)for(const t of this.targetsByName.getValuesForKey(e))this.disconnectTarget(t,e)}get attributeName(){return`data-${this.context.identifier}-target`}get element(){return this.context.element}get scope(){return this.context.scope}}function I(e,t){const s=L(e);return Array.from(s.reduce((e,s)=>(function(e,t){const s=e[t];return Array.isArray(s)?s:[]}(s,t).forEach(t=>e.add(t)),e),new Set))}function L(e){const t=[];for(;e;)t.push(e),e=Object.getPrototypeOf(e);return t.reverse()}class P{constructor(e,t){this.started=!1,this.context=e,this.delegate=t,this.outletsByName=new S,this.outletElementsByName=new S,this.selectorObserverMap=new Map,this.attributeObserverMap=new Map}start(){this.started||(this.outletDefinitions.forEach(e=>{this.setupSelectorObserverForOutlet(e),this.setupAttributeObserverForOutlet(e)}),this.started=!0,this.dependentContexts.forEach(e=>e.refresh()))}refresh(){this.selectorObserverMap.forEach(e=>e.refresh()),this.attributeObserverMap.forEach(e=>e.refresh())}stop(){this.started&&(this.started=!1,this.disconnectAllOutlets(),this.stopSelectorObservers(),this.stopAttributeObservers())}stopSelectorObservers(){this.selectorObserverMap.size>0&&(this.selectorObserverMap.forEach(e=>e.stop()),this.selectorObserverMap.clear())}stopAttributeObservers(){this.attributeObserverMap.size>0&&(this.attributeObserverMap.forEach(e=>e.stop()),this.attributeObserverMap.clear())}selectorMatched(e,t,{outletName:s}){const i=this.getOutlet(e,s);i&&this.connectOutlet(i,e,s)}selectorUnmatched(e,t,{outletName:s}){const i=this.getOutletFromMap(e,s);i&&this.disconnectOutlet(i,e,s)}selectorMatchElement(e,{outletName:t}){const s=this.selector(t),i=this.hasOutlet(e,t),n=e.matches(`[${this.schema.controllerAttribute}~=${t}]`);return!!s&&i&&n&&e.matches(s)}elementMatchedAttribute(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}elementAttributeValueChanged(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}elementUnmatchedAttribute(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}connectOutlet(e,t,s){var i;this.outletElementsByName.has(s,t)||(this.outletsByName.add(s,e),this.outletElementsByName.add(s,t),null===(i=this.selectorObserverMap.get(s))||void 0===i||i.pause(()=>this.delegate.outletConnected(e,t,s)))}disconnectOutlet(e,t,s){var i;this.outletElementsByName.has(s,t)&&(this.outletsByName.delete(s,e),this.outletElementsByName.delete(s,t),null===(i=this.selectorObserverMap.get(s))||void 0===i||i.pause(()=>this.delegate.outletDisconnected(e,t,s)))}disconnectAllOutlets(){for(const e of this.outletElementsByName.keys)for(const t of this.outletElementsByName.getValuesForKey(e))for(const s of this.outletsByName.getValuesForKey(e))this.disconnectOutlet(s,t,e)}updateSelectorObserverForOutlet(e){const t=this.selectorObserverMap.get(e);t&&(t.selector=this.selector(e))}setupSelectorObserverForOutlet(e){const t=this.selector(e),s=new C(document.body,t,this,{outletName:e});this.selectorObserverMap.set(e,s),s.start()}setupAttributeObserverForOutlet(e){const t=this.attributeNameForOutletName(e),s=new w(this.scope.element,t,this);this.attributeObserverMap.set(e,s),s.start()}selector(e){return this.scope.outlets.getSelectorForOutletName(e)}attributeNameForOutletName(e){return this.scope.schema.outletAttributeForScope(this.identifier,e)}getOutletNameFromOutletAttributeName(e){return this.outletDefinitions.find(t=>this.attributeNameForOutletName(t)===e)}get outletDependencies(){const e=new S;return this.router.modules.forEach(t=>{I(t.definition.controllerConstructor,"outlets").forEach(s=>e.add(s,t.identifier))}),e}get outletDefinitions(){return this.outletDependencies.getKeysForValue(this.identifier)}get dependentControllerIdentifiers(){return this.outletDependencies.getValuesForKey(this.identifier)}get dependentContexts(){const e=this.dependentControllerIdentifiers;return this.router.contexts.filter(t=>e.includes(t.identifier))}hasOutlet(e,t){return!!this.getOutlet(e,t)||!!this.getOutletFromMap(e,t)}getOutlet(e,t){return this.application.getControllerForElementAndIdentifier(e,t)}getOutletFromMap(e,t){return this.outletsByName.getValuesForKey(t).find(t=>t.element===e)}get scope(){return this.context.scope}get schema(){return this.context.schema}get identifier(){return this.context.identifier}get application(){return this.context.application}get router(){return this.application.router}}class _{constructor(e,t){this.logDebugActivity=(e,t={})=>{const{identifier:s,controller:i,element:n}=this;t=Object.assign({identifier:s,controller:i,element:n},t),this.application.logDebugActivity(this.identifier,e,t)},this.module=e,this.scope=t,this.controller=new e.controllerConstructor(this),this.bindingObserver=new x(this,this.dispatcher),this.valueObserver=new M(this,this.controller),this.targetObserver=new k(this,this),this.outletObserver=new P(this,this);try{this.controller.initialize(),this.logDebugActivity("initialize")}catch(e){this.handleError(e,"initializing controller")}}connect(){this.bindingObserver.start(),this.valueObserver.start(),this.targetObserver.start(),this.outletObserver.start();try{this.controller.connect(),this.logDebugActivity("connect")}catch(e){this.handleError(e,"connecting controller")}}refresh(){this.outletObserver.refresh()}disconnect(){try{this.controller.disconnect(),this.logDebugActivity("disconnect")}catch(e){this.handleError(e,"disconnecting controller")}this.outletObserver.stop(),this.targetObserver.stop(),this.valueObserver.stop(),this.bindingObserver.stop()}get application(){return this.module.application}get identifier(){return this.module.identifier}get schema(){return this.application.schema}get dispatcher(){return this.application.dispatcher}get element(){return this.scope.element}get parentElement(){return this.element.parentElement}handleError(e,t,s={}){const{identifier:i,controller:n,element:r}=this;s=Object.assign({identifier:i,controller:n,element:r},s),this.application.handleError(e,`Error ${t}`,s)}targetConnected(e,t){this.invokeControllerMethod(`${t}TargetConnected`,e)}targetDisconnected(e,t){this.invokeControllerMethod(`${t}TargetDisconnected`,e)}outletConnected(e,t,s){this.invokeControllerMethod(`${c(s)}OutletConnected`,e,t)}outletDisconnected(e,t,s){this.invokeControllerMethod(`${c(s)}OutletDisconnected`,e,t)}invokeControllerMethod(e,...t){const s=this.controller;"function"==typeof s[e]&&s[e](...t)}}const N="function"==typeof Object.getOwnPropertySymbols?e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]:Object.getOwnPropertyNames,D=(()=>{function e(e){function t(){return Reflect.construct(e,arguments,new.target)}return t.prototype=Object.create(e.prototype,{constructor:{value:t}}),Reflect.setPrototypeOf(t,e),t}try{return function(){const t=e(function(){this.a.call(this)});t.prototype.a=function(){},new t}(),e}catch(e){return e=>class extends e{}}})();class F{constructor(e,t){this.application=e,this.definition=function(e){return{identifier:e.identifier,controllerConstructor:(t=e.controllerConstructor,function(e,t){const s=D(e),i=function(e,t){return N(t).reduce((s,i)=>{const n=function(e,t,s){const i=Object.getOwnPropertyDescriptor(e,s);if(!i||!("value"in i)){const e=Object.getOwnPropertyDescriptor(t,s).value;return i&&(e.get=i.get||e.get,e.set=i.set||e.set),e}}(e,t,i);return n&&Object.assign(s,{[i]:n}),s},{})}(e.prototype,t);return Object.defineProperties(s.prototype,i),s}(t,function(e){return I(e,"blessings").reduce((t,s)=>{const i=s(e);for(const e in i){const s=t[e]||{};t[e]=Object.assign(s,i[e])}return t},{})}(t)))};var t}(t),this.contextsByScope=new WeakMap,this.connectedContexts=new Set}get identifier(){return this.definition.identifier}get controllerConstructor(){return this.definition.controllerConstructor}get contexts(){return Array.from(this.connectedContexts)}connectContextForScope(e){const t=this.fetchContextForScope(e);this.connectedContexts.add(t),t.connect()}disconnectContextForScope(e){const t=this.contextsByScope.get(e);t&&(this.connectedContexts.delete(t),t.disconnect())}fetchContextForScope(e){let t=this.contextsByScope.get(e);return t||(t=new _(this,e),this.contextsByScope.set(e,t)),t}}class R{constructor(e){this.scope=e}has(e){return this.data.has(this.getDataKey(e))}get(e){return this.getAll(e)[0]}getAll(e){return(this.data.get(this.getDataKey(e))||"").match(/[^\s]+/g)||[]}getAttributeName(e){return this.data.getAttributeNameForKey(this.getDataKey(e))}getDataKey(e){return`${e}-class`}get data(){return this.scope.data}}class B{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get(e){const t=this.getAttributeNameForKey(e);return this.element.getAttribute(t)}set(e,t){const s=this.getAttributeNameForKey(e);return this.element.setAttribute(s,t),this.get(e)}has(e){const t=this.getAttributeNameForKey(e);return this.element.hasAttribute(t)}delete(e){if(this.has(e)){const t=this.getAttributeNameForKey(e);return this.element.removeAttribute(t),!0}return!1}getAttributeNameForKey(e){return`data-${this.identifier}-${h(e)}`}}class ${constructor(e){this.warnedKeysByObject=new WeakMap,this.logger=e}warn(e,t,s){let i=this.warnedKeysByObject.get(e);i||(i=new Set,this.warnedKeysByObject.set(e,i)),i.has(t)||(i.add(t),this.logger.warn(s,e))}}function j(e,t){return`[${e}~="${t}"]`}class V{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce((e,t)=>e||this.findTarget(t)||this.findLegacyTarget(t),void 0)}findAll(...e){return e.reduce((e,t)=>[...e,...this.findAllTargets(t),...this.findAllLegacyTargets(t)],[])}findTarget(e){const t=this.getSelectorForTargetName(e);return this.scope.findElement(t)}findAllTargets(e){const t=this.getSelectorForTargetName(e);return this.scope.findAllElements(t)}getSelectorForTargetName(e){return j(this.schema.targetAttributeForScope(this.identifier),e)}findLegacyTarget(e){const t=this.getLegacySelectorForTargetName(e);return this.deprecate(this.scope.findElement(t),e)}findAllLegacyTargets(e){const t=this.getLegacySelectorForTargetName(e);return this.scope.findAllElements(t).map(t=>this.deprecate(t,e))}getLegacySelectorForTargetName(e){const t=`${this.identifier}.${e}`;return j(this.schema.targetAttribute,t)}deprecate(e,t){if(e){const{identifier:s}=this,i=this.schema.targetAttribute,n=this.schema.targetAttributeForScope(s);this.guide.warn(e,`target:${t}`,`Please replace ${i}="${s}.${t}" with ${n}="${t}". The ${i} attribute is deprecated and will be removed in a future version of Stimulus.`)}return e}get guide(){return this.scope.guide}}class q{constructor(e,t){this.scope=e,this.controllerElement=t}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce((e,t)=>e||this.findOutlet(t),void 0)}findAll(...e){return e.reduce((e,t)=>[...e,...this.findAllOutlets(t)],[])}getSelectorForOutletName(e){const t=this.schema.outletAttributeForScope(this.identifier,e);return this.controllerElement.getAttribute(t)}findOutlet(e){const t=this.getSelectorForOutletName(e);if(t)return this.findElement(t,e)}findAllOutlets(e){const t=this.getSelectorForOutletName(e);return t?this.findAllElements(t,e):[]}findElement(e,t){return this.scope.queryElements(e).filter(s=>this.matchesElement(s,e,t))[0]}findAllElements(e,t){return this.scope.queryElements(e).filter(s=>this.matchesElement(s,e,t))}matchesElement(e,t,s){const i=e.getAttribute(this.scope.schema.controllerAttribute)||"";return e.matches(t)&&i.split(" ").includes(s)}}class U{constructor(e,t,s,i){this.targets=new V(this),this.classes=new R(this),this.data=new B(this),this.containsElement=e=>e.closest(this.controllerSelector)===this.element,this.schema=e,this.element=t,this.identifier=s,this.guide=new $(i),this.outlets=new q(this.documentScope,t)}findElement(e){return this.element.matches(e)?this.element:this.queryElements(e).find(this.containsElement)}findAllElements(e){return[...this.element.matches(e)?[this.element]:[],...this.queryElements(e).filter(this.containsElement)]}queryElements(e){return Array.from(this.element.querySelectorAll(e))}get controllerSelector(){return j(this.schema.controllerAttribute,this.identifier)}get isDocumentScope(){return this.element===document.documentElement}get documentScope(){return this.isDocumentScope?this:new U(this.schema,document.documentElement,this.identifier,this.guide.logger)}}class z{constructor(e,t,s){this.element=e,this.schema=t,this.delegate=s,this.valueListObserver=new A(this.element,this.controllerAttribute,this),this.scopesByIdentifierByElement=new WeakMap,this.scopeReferenceCounts=new WeakMap}start(){this.valueListObserver.start()}stop(){this.valueListObserver.stop()}get controllerAttribute(){return this.schema.controllerAttribute}parseValueForToken(e){const{element:t,content:s}=e;return this.parseValueForElementAndIdentifier(t,s)}parseValueForElementAndIdentifier(e,t){const s=this.fetchScopesByIdentifierForElement(e);let i=s.get(t);return i||(i=this.delegate.createScopeForElementAndIdentifier(e,t),s.set(t,i)),i}elementMatchedValue(e,t){const s=(this.scopeReferenceCounts.get(t)||0)+1;this.scopeReferenceCounts.set(t,s),1==s&&this.delegate.scopeConnected(t)}elementUnmatchedValue(e,t){const s=this.scopeReferenceCounts.get(t);s&&(this.scopeReferenceCounts.set(t,s-1),1==s&&this.delegate.scopeDisconnected(t))}fetchScopesByIdentifierForElement(e){let t=this.scopesByIdentifierByElement.get(e);return t||(t=new Map,this.scopesByIdentifierByElement.set(e,t)),t}}class W{constructor(e){this.application=e,this.scopeObserver=new z(this.element,this.schema,this),this.scopesByIdentifier=new S,this.modulesByIdentifier=new Map}get element(){return this.application.element}get schema(){return this.application.schema}get logger(){return this.application.logger}get controllerAttribute(){return this.schema.controllerAttribute}get modules(){return Array.from(this.modulesByIdentifier.values())}get contexts(){return this.modules.reduce((e,t)=>e.concat(t.contexts),[])}start(){this.scopeObserver.start()}stop(){this.scopeObserver.stop()}loadDefinition(e){this.unloadIdentifier(e.identifier);const t=new F(this.application,e);this.connectModule(t);const s=e.controllerConstructor.afterLoad;s&&s.call(e.controllerConstructor,e.identifier,this.application)}unloadIdentifier(e){const t=this.modulesByIdentifier.get(e);t&&this.disconnectModule(t)}getContextForElementAndIdentifier(e,t){const s=this.modulesByIdentifier.get(t);if(s)return s.contexts.find(t=>t.element==e)}proposeToConnectScopeForElementAndIdentifier(e,t){const s=this.scopeObserver.parseValueForElementAndIdentifier(e,t);s?this.scopeObserver.elementMatchedValue(s.element,s):console.error(`Couldn't find or create scope for identifier: "${t}" and element:`,e)}handleError(e,t,s){this.application.handleError(e,t,s)}createScopeForElementAndIdentifier(e,t){return new U(this.schema,e,t,this.logger)}scopeConnected(e){this.scopesByIdentifier.add(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.connectContextForScope(e)}scopeDisconnected(e){this.scopesByIdentifier.delete(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.disconnectContextForScope(e)}connectModule(e){this.modulesByIdentifier.set(e.identifier,e),this.scopesByIdentifier.getValuesForKey(e.identifier).forEach(t=>e.connectContextForScope(t))}disconnectModule(e){this.modulesByIdentifier.delete(e.identifier),this.scopesByIdentifier.getValuesForKey(e.identifier).forEach(t=>e.disconnectContextForScope(t))}}const K={controllerAttribute:"data-controller",actionAttribute:"data-action",targetAttribute:"data-target",targetAttributeForScope:e=>`data-${e}-target`,outletAttributeForScope:(e,t)=>`data-${e}-${t}-outlet`,keyMappings:Object.assign(Object.assign({enter:"Enter",tab:"Tab",esc:"Escape",space:" ",up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",page_up:"PageUp",page_down:"PageDown"},H("abcdefghijklmnopqrstuvwxyz".split("").map(e=>[e,e]))),H("0123456789".split("").map(e=>[e,e])))};function H(e){return e.reduce((e,[t,s])=>Object.assign(Object.assign({},e),{[t]:s}),{})}class G{constructor(e=document.documentElement,t=K){this.logger=console,this.debug=!1,this.logDebugActivity=(e,t,s={})=>{this.debug&&this.logFormattedMessage(e,t,s)},this.element=e,this.schema=t,this.dispatcher=new n(this),this.router=new W(this),this.actionDescriptorFilters=Object.assign({},r)}static start(e,t){const s=new this(e,t);return s.start(),s}async start(){await new Promise(e=>{"loading"==document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()}),this.logDebugActivity("application","starting"),this.dispatcher.start(),this.router.start(),this.logDebugActivity("application","start")}stop(){this.logDebugActivity("application","stopping"),this.dispatcher.stop(),this.router.stop(),this.logDebugActivity("application","stop")}register(e,t){this.load({identifier:e,controllerConstructor:t})}registerActionOption(e,t){this.actionDescriptorFilters[e]=t}load(e,...t){(Array.isArray(e)?e:[e,...t]).forEach(e=>{e.controllerConstructor.shouldLoad&&this.router.loadDefinition(e)})}unload(e,...t){(Array.isArray(e)?e:[e,...t]).forEach(e=>this.router.unloadIdentifier(e))}get controllers(){return this.router.contexts.map(e=>e.controller)}getControllerForElementAndIdentifier(e,t){const s=this.router.getContextForElementAndIdentifier(e,t);return s?s.controller:null}handleError(e,t,s){var i;this.logger.error("%s\n\n%o\n\n%o",t,e,s),null===(i=window.onerror)||void 0===i||i.call(window,t,"",0,0,e)}logFormattedMessage(e,t,s={}){s=Object.assign({application:this},s),this.logger.groupCollapsed(`${e} #${t}`),this.logger.log("details:",Object.assign({},s)),this.logger.groupEnd()}}function J(e,t,s){return e.application.getControllerForElementAndIdentifier(t,s)}function Y(e,t,s){let i=J(e,t,s);return i||(e.application.router.proposeToConnectScopeForElementAndIdentifier(t,s),i=J(e,t,s),i||void 0)}function Z([e,t],s){return function(e){const{token:t,typeDefinition:s}=e,i=`${h(t)}-value`,n=function(e){const{controller:t,token:s,typeDefinition:i}=e,n=function(e){const{controller:t,token:s,typeObject:i}=e,n=u(i.type),r=u(i.default),o=n&&r,a=n&&!r,c=!n&&r,l=X(i.type),h=Q(e.typeObject.default);if(a)return l;if(c)return h;if(l!==h)throw new Error(`The specified default value for the Stimulus Value "${t?`${t}.${s}`:s}" must match the defined type "${l}". The provided default value of "${i.default}" is of type "${h}".`);return o?l:void 0}({controller:t,token:s,typeObject:i}),r=Q(i),o=X(i),a=n||r||o;if(a)return a;throw new Error(`Unknown value type "${t?`${t}.${i}`:s}" for "${s}" value`)}(e);return{type:n,key:i,name:a(i),get defaultValue(){return function(e){const t=X(e);if(t)return ee[t];const s=d(e,"default"),i=d(e,"type"),n=e;if(s)return n.default;if(i){const{type:e}=n,t=X(e);if(t)return ee[t]}return e}(s)},get hasCustomDefaultValue(){return void 0!==Q(s)},reader:te[n],writer:se[n]||se.default}}({controller:s,token:e,typeDefinition:t})}function X(e){switch(e){case Array:return"array";case Boolean:return"boolean";case Number:return"number";case Object:return"object";case String:return"string"}}function Q(e){switch(typeof e){case"boolean":return"boolean";case"number":return"number";case"string":return"string"}return Array.isArray(e)?"array":"[object Object]"===Object.prototype.toString.call(e)?"object":void 0}const ee={get array(){return[]},boolean:!1,number:0,get object(){return{}},string:""},te={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type "array" but instead got value "${e}" of type "${Q(t)}"`);return t},boolean:e=>!("0"==e||"false"==String(e).toLowerCase()),number:e=>Number(e.replace(/_/g,"")),object(e){const t=JSON.parse(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type "object" but instead got value "${e}" of type "${Q(t)}"`);return t},string:e=>e},se={default:function(e){return`${e}`},array:ie,object:ie};function ie(e){return JSON.stringify(e)}class ne{constructor(e){this.context=e}static get shouldLoad(){return!0}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:s={},prefix:i=this.identifier,bubbles:n=!0,cancelable:r=!0}={}){const o=new CustomEvent(i?`${i}:${e}`:e,{detail:s,bubbles:n,cancelable:r});return t.dispatchEvent(o),o}}ne.blessings=[function(e){return I(e,"classes").reduce((e,t)=>{return Object.assign(e,(s=t,{[`${s}Class`]:{get(){const{classes:e}=this;if(e.has(s))return e.get(s);{const t=e.getAttributeName(s);throw new Error(`Missing attribute "${t}"`)}}},[`${s}Classes`]:{get(){return this.classes.getAll(s)}},[`has${l(s)}Class`]:{get(){return this.classes.has(s)}}}));var s},{})},function(e){return I(e,"targets").reduce((e,t)=>{return Object.assign(e,(s=t,{[`${s}Target`]:{get(){const e=this.targets.find(s);if(e)return e;throw new Error(`Missing target element "${s}" for "${this.identifier}" controller`)}},[`${s}Targets`]:{get(){return this.targets.findAll(s)}},[`has${l(s)}Target`]:{get(){return this.targets.has(s)}}}));var s},{})},function(e){const t=function(e,t){return L(e).reduce((e,s)=>(e.push(...function(e,t){const s=e[t];return s?Object.keys(s).map(e=>[e,s[e]]):[]}(s,t)),e),[])}(e,"values"),s={valueDescriptorMap:{get(){return t.reduce((e,t)=>{const s=Z(t,this.identifier),i=this.data.getAttributeNameForKey(s.key);return Object.assign(e,{[i]:s})},{})}}};return t.reduce((e,t)=>Object.assign(e,function(e){const t=Z(e,void 0),{key:s,name:i,reader:n,writer:r}=t;return{[i]:{get(){const e=this.data.get(s);return null!==e?n(e):t.defaultValue},set(e){void 0===e?this.data.delete(s):this.data.set(s,r(e))}},[`has${l(i)}`]:{get(){return this.data.has(s)||t.hasCustomDefaultValue}}}}(t)),s)},function(e){return I(e,"outlets").reduce((e,t)=>Object.assign(e,function(e){const t=c(e);return{[`${t}Outlet`]:{get(){const t=this.outlets.find(e),s=this.outlets.getSelectorForOutletName(e);if(t){const s=Y(this,t,e);if(s)return s;throw new Error(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`)}throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${s}".`)}},[`${t}Outlets`]:{get(){const t=this.outlets.findAll(e);return t.length>0?t.map(t=>{const s=Y(this,t,e);if(s)return s;console.warn(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`,t)}).filter(e=>e):[]}},[`${t}OutletElement`]:{get(){const t=this.outlets.find(e),s=this.outlets.getSelectorForOutletName(e);if(t)return t;throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${s}".`)}},[`${t}OutletElements`]:{get(){return this.outlets.findAll(e)}},[`has${l(t)}Outlet`]:{get(){return this.outlets.has(e)}}}}(t)),{})}],ne.targets=[],ne.outlets=[],ne.values={}},290(e,t,s){s.d(t,{default:()=>ci});var i=s(891);class n extends Error{constructor(e){super(`${e} is not valid. Please provide a valid event name`),this.name="InvalidEvent"}}class r{static events=["session-set","utm-set","forms:collected","form:completed","popup:mounted","popup:opened","popup:closed","alert:shown","alert:dismissed","alert:accepted","webchat:mounted","webchat:opened","webchat:closed","webchat:message:sent","webchat:message:received","cart.added"];static valid(e){return r.exists(e)}static invalid(e){return!this.valid(e)}static exists(e){return void 0!==this.events.find(t=>t===e)}constructor(){this.subscribers={}}addSubscriber(e,t){if(r.invalid(e))throw new n(e);this.subscribers={...this.subscribers,[e]:this.subscribers[e]?[...this.subscribers[e],t]:[t]}}removeSubscriber(e,t){if(r.invalid(e))throw new n(e);this.subscribers[e]&&(this.subscribers[e]=this.subscribers[e].filter(e=>e!==t))}dispatch(e,t){this.subscribers[e]?.forEach(e=>{e(t)})}get listeners(){return 0!==Object.keys(this.subscribers).length}}class o{static autoMount=!0;static successMessage=!0;static assign(e){return e&&Object.entries(e).forEach(([e,t])=>{this[e]=t}),this}static get shouldShowSuccessMessage(){return this.successMessage}}class a{static _identifier;static set identifier(e){this._identifier=e}static get identifier(){return this._identifier?this._identifier:this.#e||this.#t||this.#s||"en"}static toString(){return this.identifier}static get#e(){return"undefined"!=typeof document?document.documentElement?.lang:void 0}static get#t(){return"undefined"!=typeof document?document.querySelector('meta[name="locale"]')?.content:void 0}static get#s(){return"undefined"!=typeof navigator?navigator.language?.split("-")[0]:void 0}}class c{static _id;static _container="body";static _device="auto";static set id(e){this._id=e}static get id(){return this._id}static set container(e){this._container=e}static get container(){return this._container}static set device(e){if(!["auto","mobile","desktop"].includes(e))throw new Error(`Invalid popup device value: ${e}`);this._device=e}static get device(){return this._device}static assign(e){return e&&Object.entries(e).forEach(([e,t])=>{this[e]=t}),this}}const l={TOP_LEFT:"top-left",TOP_RIGHT:"top-right",BOTTOM_LEFT:"bottom-left",BOTTOM_RIGHT:"bottom-right"},h={ABSOLUTE:"absolute",FIXED:"fixed"},u={MODAL:"modal",POPOVER:"popover"};class d{static _id;static _container="body";static _placement="bottom-right";static _style={};static _appearance={};static _whatsapp={};static _mode=u.POPOVER;static _behaviour=null;static _hasBehaviourOverride=!1;static _strategy=null;static set container(e){this._container=e}static get container(){return this._container}static set placement(e){if(!Object.values(l).includes(e))throw new Error(`Invalid placement value: ${e}`);this._placement=e}static get placement(){return this._placement}static set id(e){this._id=e}static get id(){return this._id}static get isSet(){return!!this._id}static get style(){return this._style}static set style(e){if("object"!=typeof e)throw new Error("Style must be an object");Object.entries(e).forEach(([e,t])=>{if(!["primaryColor","secondaryColor","typography"].includes(e))throw new Error(`Invalid style property: ${e}`);if("typography"!==e&&!this.isHexOrRgba(t))throw new Error(`Invalid color value: ${t} for ${e}. Colors must be hex or rgb/a.`)}),this._style=e}static get appearance(){return this._appearance}static set appearance(e){if(!this.isPlainObject(e))throw new Error("Appearance must be an object");Object.entries(e).forEach(([e,t])=>{if(!["header","launcher"].includes(e))throw new Error(`Invalid appearance property: ${e}`);if(!this.isPlainObject(t))throw new Error(`Appearance ${e} must be an object`);Object.entries(t).forEach(([t,s])=>{if("header"===e&&"name"!==t)throw new Error(`Invalid appearance header property: ${t}`);if("launcher"===e&&"iconUrl"!==t)throw new Error(`Invalid appearance launcher property: ${t}`);if(null!=s&&"string"!=typeof s)throw new Error(`Invalid appearance ${e}.${t} value: ${s}`)})}),this._appearance=e}static get whatsapp(){return this._whatsapp}static set whatsapp(e){if(!this.isPlainObject(e))throw new Error("WhatsApp must be an object");Object.entries(e).forEach(([e,t])=>{if(!["number","restrictToChannel"].includes(e))throw new Error(`Invalid WhatsApp property: ${e}`);if(null!=t){if("number"===e&&"string"!=typeof t)throw new Error(`Invalid WhatsApp number value: ${t}`);if("restrictToChannel"===e&&"boolean"!=typeof t)throw new Error(`Invalid WhatsApp restrictToChannel value: ${t}`)}}),this._whatsapp=e}static get mode(){return this._mode}static set mode(e){if(!Object.values(u).includes(e))throw new Error(`Invalid mode value: ${e}`);this._mode=e}static get behaviour(){return this._behaviour}static set behaviour(e){if(null!=e){if("object"!=typeof e||Array.isArray(e))throw new Error(`Invalid behaviour value: ${e}`);this._behaviour=e}else this._behaviour=e}static get hasBehaviourOverride(){return this._hasBehaviourOverride}static set behaviourOverride(e){this._hasBehaviourOverride=!!e}static get strategy(){return this._strategy?this._strategy:"body"==this.container?h.FIXED:h.ABSOLUTE}static set strategy(e){if(e&&!Object.values(h).includes(e))throw new Error(`Invalid strategy value: ${e}`);this._strategy=e}static assign(e){return e&&Object.entries(e).forEach(([e,t])=>{this[e]=t}),this}static isHexOrRgba(e){return/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(e)||/^rgba?\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3},?\s*(0|1|0?\.\d+)?\s*\)$/.test(e)}static isPlainObject(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}}const p={TOP_LEFT:"top-left",TOP_RIGHT:"top-right",BOTTOM_LEFT:"bottom-left",BOTTOM_RIGHT:"bottom-right"};class m{static _id;static _container="body";static _placement="bottom-right";static _appearance={};static _number=null;static _body=null;static set id(e){this._id=e}static get id(){return this._id}static set container(e){this._container=e}static get container(){return this._container}static set placement(e){if(!Object.values(p).includes(e))throw new Error(`Invalid placement value: ${e}`);this._placement=e}static get placement(){return this._placement}static get appearance(){return this._appearance}static set appearance(e){if(!this.isPlainObject(e))throw new Error("Appearance must be an object");Object.entries(e).forEach(([e,t])=>{if("launcher"!==e)throw new Error(`Invalid appearance property: ${e}`);if(!this.isPlainObject(t))throw new Error(`Appearance ${e} must be an object`);Object.entries(t).forEach(([t,s])=>{if("iconUrl"!==t)throw new Error(`Invalid appearance launcher property: ${t}`);if(null!=s&&"string"!=typeof s)throw new Error(`Invalid appearance ${e}.${t} value: ${s}`)})}),this._appearance=e}static get number(){return this._number}static set number(e){if(null!=e&&"string"!=typeof e)throw new Error(`Invalid number value: ${e}`);this._number=e}static get body(){return this._body}static set body(e){if(null!=e&&"string"!=typeof e)throw new Error(`Invalid body value: ${e}`);this._body=e}static assign(e){return e&&Object.entries(e).forEach(([e,t])=>{this[e]=t}),this}static isPlainObject(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}}class g{static serviceWorkerUrl=null;static channelId=null;static assign(e){return this.serviceWorkerUrl=e?.serviceWorkerUrl||null,this.channelId=e?.channelId||null,this}}class f{static apiRoot="https://api.hellotext.com/v1";static actionCableUrl="wss://www.hellotext.com/cable";static autoGenerateSession=!0;static session=null;static forms=o;static popup=c;static webchat=d;static whatsapp=m;static push=g;static assign(e){if(e){const t=Object.prototype.hasOwnProperty.call(e,"apiRoot")&&!Object.prototype.hasOwnProperty.call(e,"actionCableUrl");Object.entries(e).forEach(([e,t])=>{"forms"===e?this.forms=o.assign(t):"popup"===e?this.popup=c.assign(t):"webchat"===e?this.webchat=d.assign(t):"whatsappWidget"===e?this.whatsapp=m.assign(t):"push"===e?this.push=g.assign(t):this[e]=t}),t&&(this.actionCableUrl=this.actionCableUrlForApiRoot(this.apiRoot))}return this}static set locale(e){a.identifier=e}static get locale(){return a.toString()}static endpoint(e){return`${this.apiRoot}/${e}`}static actionCableUrlForApiRoot(e){try{const t=new URL(e),s="https:"===t.protocol?"wss:":"ws:";return t.protocol=s,t.pathname="/cable",t.search="",t.hash="",t.toString()}catch(e){return this.actionCableUrl}}}const b=class{static get endpoint(){return f.endpoint("public/businesses")}static async get(e){return fetch(`${this.endpoint}/${e}`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json","Content-Type":"application/json"}})}};class y{static get inPreviewMode(){return(new this).inPreviewMode}constructor(){this.urlSearchParams=new URLSearchParams(window.location.search)}get(e){return this.urlSearchParams.get(this.toHellotextParam(e))}has(e){return this.urlSearchParams.has(this.toHellotextParam(e))}get inPreviewMode(){return this.has("preview")}get session(){return this.get("session")}toHellotextParam(e){return`hello_${e}`}}class v{#i;constructor(e,t){this.response=t,this.#i=e}get data(){return this.response}async json(){return await this.response.json()}get failed(){return!1===this.#i}get succeeded(){return!0===this.#i}}class w{static get endpoint(){return f.endpoint("track/events")}static async create({headers:e,body:t,keepalive:s=!1}){if(y.inPreviewMode)return new v(!0,{received:!0});const i={method:"POST",headers:e,body:JSON.stringify(t)};s&&(i.keepalive=!0);const n=await fetch(this.endpoint,i);return new v(200===n.status,await n.json())}}class T{static get endpoint(){return f.endpoint("public/forms")}static async get(e){const t=new URL(`${this.endpoint}/${e}`);return t.searchParams.append("session",vt.session),t.searchParams.append("locale",a.toString()),fetch(t,{method:"GET",headers:vt.headers})}static async submit(e,t){const s=await fetch(`${this.endpoint}/${e}/submissions`,{method:"POST",headers:vt.headers,body:JSON.stringify({session:vt.session,...t})});return new v(s.ok,s)}}const S=class{static get endpoint(){return f.endpoint("public/identifications")}static async create(e={}){const t=await fetch(this.endpoint,{method:"POST",headers:vt.headers,body:JSON.stringify({session:vt.session,...e})});return new v(t.ok,t)}},C=class{static get endpoint(){return f.endpoint("public/popups")}static async get(e){const t=new URL(`${this.endpoint}/${e}`);t.searchParams.append("session",vt.session),t.searchParams.append("locale",a.toString()),t.searchParams.append("device",this.runtimeDevice);const s=await this.fetchPopup(t);if(!s.ok)return null;const i=await this.parsePopupResponse(s);return i?(vt.business.data||(vt.business.setData(i.business),vt.business.setLocale(i.locale)),(new DOMParser).parseFromString(i.html,"text/html").querySelector("article")):null}static async submit(e,t,s=this.idempotencyKey()){const i=await fetch(`${this.endpoint}/${e}/submissions`,{method:"POST",headers:{...vt.headers,"Idempotency-Key":s},body:JSON.stringify({session:vt.session,popup_submission:t})});return new v(i.ok,i)}static async resend(e,t,s){const i=await fetch(`${this.endpoint}/${e}/submissions/${t}/resend`,{method:"POST",headers:vt.headers,body:JSON.stringify({token:s})});return new v(i.ok,i)}static async cancel(e,t,s){const i=await fetch(`${this.endpoint}/${e}/submissions/${t}/cancel`,{method:"POST",headers:vt.headers,body:JSON.stringify({token:s})});return new v(i.ok,i)}static idempotencyKey(){return"undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID?crypto.randomUUID():`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}static async fetchPopup(e){try{return await fetch(e,{method:"GET",headers:vt.headers})}catch(e){return{ok:!1}}}static get runtimeDevice(){return"auto"!==f.popup.device?f.popup.device:window.innerWidth<=767?"mobile":"desktop"}static async parsePopupResponse(e){try{return await e.json()}catch(e){return null}}},O=class{static get endpoint(){return f.endpoint("public/webchats")}static async get(e){const t=new URL(`${this.endpoint}/${e}`);t.searchParams.append("session",vt.session),t.searchParams.append("locale",a.toString()),Object.entries(f.webchat.style).forEach(([e,s])=>{t.searchParams.append(`style[${e}]`,s)}),this.appendWebchatOverrides(t),t.searchParams.append("placement",f.webchat.placement);const s=await fetch(t,{method:"GET",headers:vt.headers}),i=await s.json();return vt.business.data||(vt.business.setData(i.business),vt.business.setLocale(i.locale)),(new DOMParser).parseFromString(i.html,"text/html").querySelector("article")}static appendWebchatOverrides(e){const{appearance:t,whatsapp:s}=f.webchat;this.appendIfSupplied(e,"webchat[appearance][header][name]",t.header?.name),this.appendIfSupplied(e,"webchat[appearance][launcher][icon_url]",t.launcher?.iconUrl),this.appendIfSupplied(e,"webchat[handoff][identifier]",s.number),this.appendIfSupplied(e,"webchat[handoff][restrict_to_channel]",s.restrictToChannel)}static appendIfSupplied(e,t,s){null!=s&&e.searchParams.append(t,String(s))}},E=class{static get endpoint(){return f.endpoint("public/widgets/whatsapp")}static async get(e){const t=new URL(`${this.endpoint}/${e}`);t.searchParams.append("locale",a.toString()),t.searchParams.append("placement",f.whatsapp.placement),this.appendWhatsAppOverrides(t);const s=await this.fetchWidget(t);if(!s.ok)return null;const i=await this.parseWidgetResponse(s);return i?(vt.business.data||(vt.business.setData(i.business),vt.business.setLocale(i.locale)),(new DOMParser).parseFromString(i.html,"text/html").querySelector("article")):null}static appendWhatsAppOverrides(e){const{appearance:t,body:s,number:i}=f.whatsapp;this.appendIfSupplied(e,"whatsapp[appearance][launcher][icon_url]",t.launcher?.iconUrl),this.appendIfSupplied(e,"whatsapp[number]",i),this.appendIfSupplied(e,"whatsapp[body]",s)}static appendIfSupplied(e,t,s){null!=s&&e.searchParams.append(t,String(s))}static async fetchWidget(e){try{return await fetch(e,{method:"GET",headers:vt.headers})}catch(e){return{ok:!1}}}static async parseWidgetResponse(e){try{return await e.json()}catch(e){return null}}},A=class{static get endpoint(){return f.endpoint("public/acks")}static async send(e={}){const t={...e,session:vt.session,at:(new Date).toISOString()};fetch(this.endpoint,{method:"POST",headers:vt.headers,body:JSON.stringify(t),keepalive:!0})}},x=class{static get endpoint(){return f.endpoint("public/push/identities")}static async create(e={}){const t=await fetch(this.endpoint,{method:"POST",keepalive:!0,headers:vt.headers,body:JSON.stringify({...e,session:vt.session,origin:window.location.origin})});return new v(t.ok,t)}static async destroy(e={}){const t=await fetch(this.endpoint,{method:"DELETE",keepalive:!0,headers:vt.headers,body:JSON.stringify({...e,session:vt.session,origin:window.location.origin})});return new v(t.ok,t)}},M=class{static get endpoint(){return f.endpoint("public/push/alerts")}static async create({section:e,kind:t,page:s}){const i=await fetch(this.endpoint,{method:"POST",keepalive:!0,headers:vt.headers,body:JSON.stringify({session:vt.session,section:e,kind:t,page:s})});return new v(i.ok,i)}};function k(e){const t=JSON.stringify(e);return"undefined"==typeof Blob?t.length<6e4:new Blob([t]).size<6e4}class I{static get businesses(){return b}static get events(){return w}static get forms(){return T}static get popups(){return C}static get popups(){return C}static get webchats(){return O}static get whatsappWidgets(){return E}static get identifications(){return S}static get acks(){return A}static get pushAlerts(){return M}static get pushIdentities(){return x}}const L="data-hellotext-stylesheet";class P{constructor(e){this.id=e,this.data=null,this.stylesheet=null,this.stylesheetLoaded=Promise.resolve(!1)}async hydrate(){try{const e=await b.get(this.id);if(!1===e.ok)return null;const t=await e.json();return t?(this.setData(t),this.setLocale(a.toString()),t):null}catch(e){return null}}setData(e){this.data=e,"undefined"!=typeof document&&e.style_url?(this.stylesheet=this.constructor.ensureStylesheet(e.style_url),this.stylesheetLoaded=this.constructor.waitForStylesheet(this.stylesheet)):(this.stylesheet=null,this.stylesheetLoaded=Promise.resolve(!1))}static get stylesheetSelector(){return`link[rel="stylesheet"][${L}]`}static ensureStylesheet(e){const t=this.normalizedStylesheetUrl(e),s=this.stylesheetLinks.find(e=>e.href===t);if(s)return s.setAttribute(L,"true"),s;const i=document.createElement("link");return i.rel="stylesheet",i.href=e,i.setAttribute(L,"true"),this.waitForStylesheet(i),document.head.append(i),i}static get stylesheetLinks(){return"undefined"==typeof document?[]:Array.from(document.querySelectorAll(this.stylesheetSelector))}static get latestStylesheet(){return this.stylesheetLinks[this.stylesheetLinks.length-1]}static normalizedStylesheetUrl(e){try{return new URL(e,document.baseURI).href}catch(t){return e}}static waitForStylesheet(e){return e?this.stylesheetIsLoaded(e)?Promise.resolve(!0):"false"===e.dataset.hellotextStylesheetLoaded?Promise.resolve(!1):(e._hellotextStylesheetLoaded||(e._hellotextStylesheetLoaded=new Promise(t=>{let s;const i=i=>{clearTimeout(s),e.removeEventListener("load",n),e.removeEventListener("error",r),e.dataset.hellotextStylesheetLoaded=i?"true":"false",t(i)},n=()=>i(this.stylesheetIsLoaded(e)),r=()=>i(!1);e.addEventListener("load",n),e.addEventListener("error",r),s=setTimeout(()=>i(this.stylesheetIsLoaded(e)),1e4),s.unref&&s.unref()})),e._hellotextStylesheetLoaded):Promise.resolve(!1)}static stylesheetIsLoaded(e){return"true"===e.dataset.hellotextStylesheetLoaded||!!e.sheet}get subscription(){return this.data.subscription}get country(){return this.data.country}get enabledWhitelist(){return"disabled"!==this.data.whitelist}setLocale(e){this.data||(this.data={});const t=e?.toLowerCase().split("-")[0];this.data.locale=Object.prototype.hasOwnProperty.call(this.data.locales||{},t)?t:"en"}get locale(){const e=this.data?.locales;return e?.[this.data.locale]||e?.en}get features(){return this.data.features}}class _{static set(e,t){if("undefined"!=typeof document){const s="https:"===window.location.protocol?"; Secure":"",i=D.getRootDomain(),n=31536e4;document.cookie=i?`${e}=${t}; path=/${s}; domain=${i}; max-age=${n}; SameSite=Lax`:`${e}=${t}; path=/${s}; max-age=${n}; SameSite=Lax`}return"hello_session"===e&&vt.eventEmitter.dispatch("session-set",t),"hello_utm"===e&&vt.eventEmitter.dispatch("utm-set",t),t}static get(e){return"undefined"!=typeof document?document.cookie.match("(^|;)\\s*"+e+"\\s*=\\s*([^;]+)")?.pop():void 0}static delete(e){if("undefined"!=typeof document){const t=D.getRootDomain(),s="https:"===window.location.protocol?"; Secure":"";document.cookie=t?`${e}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT${s}; domain=${t}; SameSite=Lax`:`${e}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT${s}; SameSite=Lax`}}}class N{constructor(){const e=new URLSearchParams(window.location.search),t={source:e.get("utm_source"),medium:e.get("utm_medium"),campaign:e.get("utm_campaign"),term:e.get("utm_term"),content:e.get("utm_content")};this.save(t)}save(e){if(!e.source||!e.medium)return;const t=Object.fromEntries(Object.entries(e).filter(([e,t])=>t));t.observed_at=(new Date).toISOString(),_.set("hello_utm",JSON.stringify(t))}get current(){try{return JSON.parse(_.get("hello_utm"))||{}}catch(e){return{}}}}class D{constructor(e=null){this.utm=new N,this._url=e}get url(){return null!==this._url&&void 0!==this._url?this._url:window.location.href}get title(){return document.title}get path(){if(this._url)try{return new URL(this._url).pathname}catch(e){return"/"}return window.location.pathname}get utmParams(){return this.utm.current}get trackingData(){return{page:{url:this.url,title:this.title,path:this.path},utm_params:this.utmParams}}get domain(){try{const e=this.url;if(!e)return null;const t=new URL(e).hostname;return D.getRootDomain(t)}catch(e){return null}}static getRootDomain(e=null){try{if(!e){if("undefined"==typeof window||!window.location?.hostname)return null;e=window.location.hostname}const t=e.split(".");if(t.length<=1)return e;const s=["myshopify.com","vtexcommercestable.com.br","myvtex.com","wixsite.com"];for(const e of s){const s=e.split(".");if(t.slice(-s.length).join(".")===e&&t.length>s.length)return`.${t.slice(-(s.length+1)).join(".")}`}const i=t[t.length-1],n=t[t.length-2];return t.length>2&&2===i.length&&n.length<=3?`.${t.slice(-3).join(".")}`:`.${t.slice(-2).join(".")}`}catch(e){return null}}}class F{static#n;static#r;static#o;static get session(){return this.#n}static get ackPayload(){return{utm_params:this.#o?.utmParams||{}}}static set session(e){const t=_.get("hello_session");return this.#n=e,_.set("hello_session",e),t!==e&&_.delete("hello_session_ack_at"),_.get("hello_session_ack_at")||(I.acks.send(this.ackPayload),_.set("hello_session_ack_at",(new Date).toISOString())),this.#n}static initialize(e=new D){this.#o=e,this.#r=new y,this.session=this.#r.session||f.session||_.get("hello_session"),!this.session&&f.autoGenerateSession&&(this.session=crypto.randomUUID())}}class R{static build(e){const t=document.createElement("article"),s=document.createElement("label"),i=document.createElement("input");s.innerText=e.label,i.type=e.type,i.required=e.required,i.placeholder=e.placeholder,["first_name","last_name"].includes(e.kind)?(i.type="text",i.id=i.name=e.kind,s.setAttribute("for",e.kind)):(i.type=e.type,"email"===e.type?(i.id=i.name="email",s.setAttribute("for","email")):"tel"===i.type?(i.id=i.name="phone",s.setAttribute("for","phone"),i.value=`+${vt.business.country.prefix}`,i.setAttribute("data-default-value",`+${vt.business.country.prefix}`)):(i.name=i.id=`property_by_id[${e.property}]`,s.setAttribute("for",`property_by_id[${e.property}]`)));const n=document.createElement("main");n.appendChild(s),n.appendChild(i),t.appendChild(n),t.setAttribute("data-hellotext--form-target","inputContainer"),i.setAttribute("data-hellotext--form-target","input");const r=document.createElement("div");return r.setAttribute("data-error-container",""),t.appendChild(r),t}}class B{static build(){const e=document.createElement("div");return e.innerHTML=this.#a(),e.firstElementChild}static#a(){const e=`https://www.hellotext.com?hello_session=${vt.session}`;return`\n
\n ${vt.business.locale.white_label.powered_by}\n\n \n \n Hellotext\n \n \n \n
\n `}}function $(e,t){(null==t||t>e.length)&&(t=e.length);for(var s=0,i=Array(t);s2?s-2:0),n=2;n1?t-1:0),i=1;i1?s-1:0),n=1;n2&&void 0!==arguments[2]?arguments[2]:ne;if(q&&q(e,null),!ie(t))return e;let i=t.length;for(;i--;){let n=t[i];if("string"==typeof n){const e=s(n);e!==n&&(U(t)||(t[i]=e),n=e)}e[n]=!0}return e}function Te(e){for(let t=0;t/g),Re=H(/\${[\w\W]*/g),Be=H(/^data-[\-\w.\u00B7-\uFFFF]+$/),$e=H(/^aria-[\-\w]+$/),je=H(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Ve=H(/^(?:\w+script|data):/i),qe=H(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Ue=H(/^html$/i),ze=H(/^[a-z][.\w]*(-[.\w]+)+$/i),We=H(/<[/\w!]/g),Ke=H(/<[/\w]/g),He=H(/<\/no(script|embed|frames)/i),Ge=H(/\/>/i),Je=["style","script","xmp","iframe","noembed","noframes","plaintext","noscript"],Ye=K(we({},Je)),Ze=function(){const e={};return X(Je,t=>{e[t]=H(new RegExp("])","i"))}),K(e)}(),Xe=function(){return"undefined"==typeof window?null:window},Qe=function(e,t,s,i){return me(e,t)&&ie(e[t])?we(i.base?Se(i.base):{},e[t],i.transform):s},et=function(e,t,s){const i=me(e,t)?e[t]:void 0;return i&&"object"==typeof i?Se(i):s()};var tt=function e(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Xe();const s=t=>e(t);if(s.version="3.4.15",s.removed=[],!t||!t.document||9!==t.document.nodeType||!t.Element)return s.isSupported=!1,s;let i=t.document;const n=i,r=n.currentScript;t.DocumentFragment;const o=t.HTMLTemplateElement,a=t.Node,c=t.Element,l=t.NodeFilter;void 0===t.NamedNodeMap&&(t.NamedNodeMap||t.MozNamedAttrMap),t.HTMLFormElement;const h=t.DOMParser,u=t.trustedTypes,d=c.prototype,p=Ce(d,"cloneNode"),m=Ce(d,"remove"),g=Ce(d,"removeAttributeNode"),f=Ce(d,"nextSibling"),b=Ce(d,"childNodes"),y=Ce(d,"parentNode"),v=Ce(d,"shadowRoot"),w=Ce(d,"attributes"),T=a&&a.prototype?Ce(a.prototype,"nodeType"):null,S=a&&a.prototype?Ce(a.prototype,"nodeName"):null,C=a&&a.prototype?Ce(a.prototype,"ownerDocument"):null,O=function(e){return T?T(e):e.nodeType},E=function(e){return S?S(e):e.nodeName};if("function"==typeof o){const e=i.createElement("template");e.content&&e.content.ownerDocument&&(i=e.content.ownerDocument)}let A,x,M="",k=!1,I=0;const L=function(){if(I>0)throw be('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.')},P=function(e){L(),I++;try{return A.createHTML(e)}finally{I--}},_=i,N=_.implementation,D=_.createNodeIterator,F=_.createDocumentFragment,R=_.getElementsByTagName,B=n.importNode;let $={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};s.isSupported="function"==typeof V&&"function"==typeof y&&N&&void 0!==N.createHTMLDocument;const j=De,q=Fe,U=Re,z=Be,W=$e,J=Ve,Y=qe,Z=ze;let ye=je,ve=null;const Te=we({},[...Oe,...Ee,...Ae,...Me,...Ie]);let Je=null;const tt=we({},[...Le,...Pe,..._e,...Ne]);let st=Object.seal(G(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),it=null,nt=null;const rt=Object.seal(G(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let ot=!0,at=!0,ct=!1,lt=!0,ht=!1,ut=!0,dt=!1,pt=!1,mt=null,gt=null,ft=!1,bt=!1,yt=!1,vt=!1,wt=!0,Tt=!1;const St="user-content-";let Ct=!0,Ot=!1,Et={},At=null;const xt=we({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","selectedcontent","style","svg","template","thead","title","video","xmp"]);let Mt=null;const kt=we({},["audio","video","img","source","image","track"]);let It=null;const Lt=we({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Pt="http://www.w3.org/1998/Math/MathML",_t="http://www.w3.org/2000/svg",Nt="http://www.w3.org/1999/xhtml";let Dt=Nt,Ft=!1,Rt=null;const Bt=we({},[Pt,_t,Nt],re),$t=K(["mi","mo","mn","ms","mtext"]);let jt=we({},$t);const Vt=K(["annotation-xml"]);let qt=we({},Vt);const Ut=we({},["title","style","font","a","script"]);let zt=null;const Wt=["application/xhtml+xml","text/html"];let Kt=null,Ht=null;const Gt=i.createElement("form"),Jt=function(e){return e instanceof RegExp||e instanceof Function},Yt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(Ht&&Ht===e)return;e&&"object"==typeof e||(e={}),e=Se(e),zt=-1===Wt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,Kt="application/xhtml+xml"===zt?re:ne,ve=Qe(e,"ALLOWED_TAGS",Te,{transform:Kt}),Je=Qe(e,"ALLOWED_ATTR",tt,{transform:Kt}),Rt=Qe(e,"ALLOWED_NAMESPACES",Bt,{transform:re}),It=Qe(e,"ADD_URI_SAFE_ATTR",Lt,{transform:Kt,base:Lt}),Mt=Qe(e,"ADD_DATA_URI_TAGS",kt,{transform:Kt,base:kt}),At=Qe(e,"FORBID_CONTENTS",xt,{transform:Kt}),it=Qe(e,"FORBID_TAGS",Se({}),{transform:Kt}),nt=Qe(e,"FORBID_ATTR",Se({}),{transform:Kt}),Et=!!me(e,"USE_PROFILES")&&(e.USE_PROFILES&&"object"==typeof e.USE_PROFILES?Se(e.USE_PROFILES):e.USE_PROFILES),ot=!1!==e.ALLOW_ARIA_ATTR,at=!1!==e.ALLOW_DATA_ATTR,ct=e.ALLOW_UNKNOWN_PROTOCOLS||!1,lt=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ht=e.SAFE_FOR_TEMPLATES||!1,ut=!1!==e.SAFE_FOR_XML,dt=e.WHOLE_DOCUMENT||!1,bt=e.RETURN_DOM||!1,yt=e.RETURN_DOM_FRAGMENT||!1,vt=e.RETURN_TRUSTED_TYPE||!1,ft=e.FORCE_BODY||!1,wt=!1!==e.SANITIZE_DOM,Tt=e.SANITIZE_NAMED_PROPS||!1,Ct=!1!==e.KEEP_CONTENT,Ot=e.IN_PLACE||!1,ye=function(e){try{return fe(e,""),!0}catch(e){return!1}}(e.ALLOWED_URI_REGEXP)?e.ALLOWED_URI_REGEXP:je,Dt="string"==typeof e.NAMESPACE?e.NAMESPACE:Nt,jt=et(e,"MATHML_TEXT_INTEGRATION_POINTS",()=>we({},$t)),qt=et(e,"HTML_INTEGRATION_POINTS",()=>we({},Vt));const t=et(e,"CUSTOM_ELEMENT_HANDLING",()=>G(null));if(st=G(null),me(t,"tagNameCheck")&&Jt(t.tagNameCheck)&&(st.tagNameCheck=t.tagNameCheck),me(t,"attributeNameCheck")&&Jt(t.attributeNameCheck)&&(st.attributeNameCheck=t.attributeNameCheck),me(t,"allowCustomizedBuiltInElements")&&"boolean"==typeof t.allowCustomizedBuiltInElements&&(st.allowCustomizedBuiltInElements=t.allowCustomizedBuiltInElements),H(st),ht&&(at=!1),yt&&(bt=!0),Et&&(ve=we({},Ie),Je=G(null),!0===Et.html&&(we(ve,Oe),we(Je,Le)),!0===Et.svg&&(we(ve,Ee),we(Je,Pe),we(Je,Ne)),!0===Et.svgFilters&&(we(ve,Ae),we(Je,Pe),we(Je,Ne)),!0===Et.mathMl&&(we(ve,Me),we(Je,_e),we(Je,Ne))),rt.tagCheck=null,rt.attributeCheck=null,me(e,"ADD_TAGS")&&("function"==typeof e.ADD_TAGS?rt.tagCheck=e.ADD_TAGS:ie(e.ADD_TAGS)&&(ve===Te&&(ve=Se(ve)),we(ve,e.ADD_TAGS,Kt))),me(e,"ADD_ATTR")&&("function"==typeof e.ADD_ATTR?rt.attributeCheck=e.ADD_ATTR:ie(e.ADD_ATTR)&&(Je===tt&&(Je=Se(Je)),we(Je,e.ADD_ATTR,Kt))),me(e,"ADD_FORBID_CONTENTS")&&ie(e.ADD_FORBID_CONTENTS)&&(At===xt&&(At=Se(At)),we(At,e.ADD_FORBID_CONTENTS,Kt)),Ct&&(ve["#text"]=!0),dt&&we(ve,["html","head","body"]),ve.table&&(we(ve,["tbody"]),delete it.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw be('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw be('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const t=A;A=e.TRUSTED_TYPES_POLICY;try{M=P("")}catch(e){throw A=t,e}}else null===e.TRUSTED_TYPES_POLICY?(A=void 0,M=""):(void 0===A&&(k||(x=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let s=null;const i="data-tt-policy-suffix";t&&t.hasAttribute(i)&&(s=t.getAttribute(i));const n="dompurify"+(s?"#"+s:"");try{return e.createPolicy(n,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+n+" could not be created."),null}}(u,r),k=!0),A=x),A&&"string"==typeof M&&(M=P("")));K&&K(e),Ht=e},Zt=we({},[...Ee,...Ae,...xe]),Xt=we({},[...Me,...ke]),Qt=function(e){te(s.removed,{element:e});try{y(e).removeChild(e)}catch(t){if(m(e),!y(e))throw be("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},es=function(e,t,s){try{g(e,t)}catch(t){try{e.removeAttribute(s)}catch(e){}}},ts=function(e){ns(e);const t=b(e);if(t){const e=[];X(t,t=>{te(e,t)}),X(e,e=>{try{m(e)}catch(e){}})}const s=w(e);if(s)for(let t=s.length-1;t>=0;--t){const i=s[t],n=i&&i.name;"string"==typeof n&&es(e,i,n)}},ss=function(e,t,i){if(!i)try{i=t.getAttributeNode(e)}catch(e){i=null}te(s.removed,{attribute:i||null,from:t});try{i?g(t,i):t.removeAttribute(e)}catch(s){try{t.removeAttribute(e)}catch(e){}}if("is"===e)if(bt||yt)try{Qt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},is=function(e){const t=w(e);if(t)for(let s=t.length-1;s>=0;--s){const i=t[s],n=i&&i.name;"string"!=typeof n||Je[Kt(n)]||es(e,i,n)}},ns=function(e){const t=[e];for(;t.length>0;){const e=t.pop();1===O(e)&&is(e);const s=b(e);if(s)for(let e=s.length-1;e>=0;--e)t.push(s[e])}},rs=function(e,t){return!!ut&&("patchsrc"===e||"for"===e&&"label"!==t&&"output"!==t)},os=function(e){let t=null,s=null;if(ft)e=""+e;else{const t=oe(e,/^[\r\n\t ]+/);s=t&&t[0]}"application/xhtml+xml"===zt&&Dt===Nt&&(e=''+e+"");const n=A?P(e):e;if(Dt===Nt)try{t=(new h).parseFromString(n,zt)}catch(e){}if(!t||!t.documentElement){t=N.createDocument(Dt,"template",null);try{t.documentElement.innerHTML=Ft?M:n}catch(e){}}const r=t.body||t.documentElement;return e&&s&&r.insertBefore(i.createTextNode(s),r.childNodes[0]||null),Dt===Nt?R.call(t,dt?"html":"body")[0]:dt?t.documentElement:r},as=function(e){const t=C?C(e):e.ownerDocument;return D.call(t||e,e,l.SHOW_ELEMENT|l.SHOW_COMMENT|l.SHOW_TEXT|l.SHOW_PROCESSING_INSTRUCTION|l.SHOW_CDATA_SECTION,null)},cs=function(e){return e=ae(e,j," "),e=ae(e,q," "),ae(e,U," ")},ls=function(e){var t;e.normalize();const s=C?C(e):e.ownerDocument,i=D.call(s||e,e,l.SHOW_TEXT|l.SHOW_COMMENT|l.SHOW_CDATA_SECTION|l.SHOW_PROCESSING_INSTRUCTION,null);let n=i.nextNode();for(;n;)n.data=cs(n.data),n=i.nextNode();const r=null===(t=e.querySelectorAll)||void 0===t?void 0:t.call(e,"template");r&&X(r,e=>{us(e.content)&&ls(e.content)})},hs=function(e){const t=S?S(e):null;return"string"==typeof t&&"form"===Kt(t)&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||e.attributes!==w(e)||"function"!=typeof e.removeAttribute||"function"!=typeof e.removeAttributeNode||"function"!=typeof e.getAttributeNode||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes||e.nodeType!==T(e)||e.childNodes!==b(e))},us=function(e){if(!T||"object"!=typeof e||null===e)return!1;try{return 11===T(e)}catch(e){return!1}},ds=function(e){if(!T||"object"!=typeof e||null===e)return!1;try{return"number"==typeof T(e)}catch(e){return!1}};function ps(e,t,i){0!==e.length&&X(e,e=>{e.call(s,t,i,Ht)})}const ms=function(e,t){if(e instanceof RegExp)return fe(e,t);if(e instanceof Function){for(var s=arguments.length,i=new Array(s>2?s-2:0),n=2;n=0;--n){const r=e===s?p(i[n],!0):i[n];t.insertBefore(r,f(e))}}return Qt(e),!0}(e,i,t);return!1===s&&ps($.afterSanitizeElements,e,null),s}if(1===O(e)&&!function(e){let t=y(e);t&&t.tagName||(t={namespaceURI:Dt,tagName:"template"});const s=ne(e.tagName),i=ne(t.tagName);return!!Rt[e.namespaceURI]&&(e.namespaceURI===_t?function(e,t,s){return t.namespaceURI===Nt?"svg"===e:t.namespaceURI===Pt?"svg"===e&&("annotation-xml"===s||jt[s]):Boolean(Zt[e])}(s,t,i):e.namespaceURI===Pt?function(e,t,s){return t.namespaceURI===Nt?"math"===e:t.namespaceURI===_t?"math"===e&&qt[s]:Boolean(Xt[e])}(s,t,i):e.namespaceURI===Nt?function(e,t,s){return!(t.namespaceURI===_t&&!qt[s])&&!(t.namespaceURI===Pt&&!jt[s])&&!Xt[e]&&(Ut[e]||!Zt[e])}(s,t,i):!("application/xhtml+xml"!==zt||!Rt[e.namespaceURI]))}(e))return Qt(e),!0;if(("noscript"===i||"noembed"===i||"noframes"===i)&&fe(He,e.innerHTML))return Qt(e),!0;if(ht&&3===e.nodeType){const t=cs(e.textContent);e.textContent!==t&&(te(s.removed,{element:e.cloneNode()}),e.textContent=t)}return ps($.afterSanitizeElements,e,null),!1},ys=function(e,t,s){if(nt[t])return!1;if(rs(t,e))return!1;if(wt&&("id"===t||"name"===t)&&(s in i||s in Gt))return!1;const n=Je[t]||rt.attributeCheck instanceof Function&&rt.attributeCheck(t,e);return!(!at||!fe(z,t))||!(!ot||!fe(W,t))||(n?!(!It[t]&&!fe(ye,ae(s,Y,""))&&("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==ce(s,"data:")||!Mt[e])&&(!ct||fe(J,ae(s,Y,"")))&&s):ws(e)&&ms(st.tagNameCheck,e)&&ms(st.attributeNameCheck,t,e)||"is"===t&&st.allowCustomizedBuiltInElements&&ms(st.tagNameCheck,s))},vs=we({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),ws=function(e){return!vs[ne(e)]&&fe(Z,e)},Ts=function(e,t,s,i){if(A&&"object"==typeof u&&"function"==typeof u.getAttributeType&&!s)switch(u.getAttributeType(e,t)){case"TrustedHTML":return P(i);case"TrustedScriptURL":return function(e){L(),I++;try{return A.createScriptURL(e)}finally{I--}}(i)}return i},Ss=function(e,t,s,i){try{return s?e.setAttributeNS(s,t,i):e.setAttribute(t,i),!hs(e)||(Qt(e),!1)}catch(s){return ss(t,e),!1}},Cs=function(e){ps($.beforeSanitizeAttributes,e,null);const t=e.attributes;if(!t||hs(e))return;Je=gs($.uponSanitizeAttribute,Je,tt,gt);const i={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Je,forceKeepAttr:void 0};let n=t.length;const r=Kt(e.nodeName);for(;n--;){const o=t[n],a=o.name,c=o.namespaceURI,l=o.value,h=Kt(a),u=l;let d="value"===a?u:le(u),p=!1;i.attrName=h,i.attrValue=d,i.keepAttr=!0,i.forceKeepAttr=void 0,ps($.uponSanitizeAttribute,e,i),d=i.attrValue,!Tt||"id"!==h&&"name"!==h||0===ce(d,St)||(ss(a,e,o),d=St+d,p=!0),ut&&fe(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,d)||"attributename"===h&&oe(d,"href")?ss(a,e,o):i.forceKeepAttr||(i.keepAttr&&(lt||!fe(Ge,d))?(ht&&(d=cs(d)),ys(r,h,d)?(d=Ts(r,h,c,d),d!==u&&Ss(e,a,c,d)&&p&&ee(s.removed)):ss(a,e,o)):ss(a,e,o))}ps($.afterSanitizeAttributes,e,null)},Os=function(e){let t=null;const s=as(e);for(ps($.beforeSanitizeShadowDOM,e,null);t=s.nextNode();)if(ps($.uponSanitizeShadowNode,t,null),bs(t,e),Cs(t),us(t.content)&&Os(t.content),1===O(t)){const e=v(t);us(e)&&(Es(e),Os(e))}ps($.afterSanitizeShadowDOM,e,null)},Es=function(e){const t=[{node:e,shadow:null}];for(;t.length>0;){const e=t.pop();if(e.shadow){Os(e.shadow);continue}const s=e.node,i=1===O(s),n=b(s);if(n)for(let e=n.length-1;e>=0;--e)t.push({node:n[e],shadow:null});if(i){const e=S?S(s):null;if("string"==typeof e&&"template"===Kt(e)){const e=s.content;us(e)&&t.push({node:e,shadow:null})}}if(i){const e=v(s);us(e)&&t.push({node:null,shadow:e},{node:e,shadow:null})}}};return s.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=null,r=null,o=null,a=null;if(Ft=!e,Ft&&(e="\x3c!--\x3e"),"string"!=typeof e&&!ds(e)&&"string"!=typeof(e=function(e){switch(typeof e){case"string":return e;case"number":return he(e);case"boolean":return ue(e);case"bigint":return de?de(e):"0";case"symbol":return pe?pe(e):"Symbol()";case"undefined":default:return ge(e);case"function":case"object":{if(null===e)return ge(e);const t=e,s=Ce(t,"toString");if("function"==typeof s){const e=s(t);return"string"==typeof e?e:ge(e)}return ge(e)}}}(e)))throw be("dirty is not a string, aborting");if(!s.isSupported)return e;pt?(ve=mt,Je=gt):Yt(t),($.uponSanitizeElement.length>0||$.uponSanitizeAttribute.length>0)&&(ve=Se(ve)),$.uponSanitizeAttribute.length>0&&(Je=Se(Je)),s.removed=[];const c=Ot&&"string"!=typeof e&&ds(e);if(c){!function(e){if(!ut)return;const t=[e];for(;t.length>0;){const e=t.pop(),s=O(e);if(7===s||8===s&&fe(Ke,e.data)){try{m(e)}catch(e){}continue}if(1===s){const t=e,s=Kt(E(e));try{t.hasAttribute&&t.hasAttribute("patchsrc")&&t.removeAttribute("patchsrc"),t.hasAttribute&&t.hasAttribute("for")&&rs("for",s)&&t.removeAttribute("for")}catch(e){}}const i=b(e);if(i)for(let e=i.length-1;e>=0;--e)t.push(i[e])}}(e);const t=E(e);if("string"==typeof t){const s=Kt(t);if(!ve[s]||it[s])throw ts(e),be("root node is forbidden and cannot be sanitized in-place")}if(hs(e))throw ts(e),be("root node is clobbered and cannot be sanitized in-place");try{Es(e)}catch(t){throw ts(e),t}}else if(ds(e))i=os("\x3c!----\x3e"),r=i.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?i=r:i.appendChild(r),Es(i);else{if(!bt&&!ht&&!dt&&-1===e.indexOf("<"))return A&&vt?P(e):e;if(i=os(e),!i)return bt?null:vt?M:""}i&&ft&&Qt(i.firstChild);const l=c?e:i;try{const e=as(l);for(;o=e.nextNode();)bs(o,l),Cs(o),us(o.content)&&Os(o.content)}catch(t){throw c&&(ts(e),X(s.removed,e=>{e.element&&ns(e.element)})),t}if(c)return X(s.removed,e=>{e.element&&ns(e.element)}),ht&&ls(e),e;if(bt){if(ht&&ls(i),yt)for(a=F.call(i.ownerDocument);i.firstChild;)a.appendChild(i.firstChild);else a=i;return(Je.shadowroot||Je.shadowrootmode)&&(a=B.call(n,a,!0)),a}let h=dt?i.outerHTML:i.innerHTML;return dt&&ve["!doctype"]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&fe(Ue,i.ownerDocument.doctype.name)&&(h="\n"+h),ht&&(h=cs(h)),A&&vt?P(h):h},s.setConfig=function(){Yt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),pt=!0,mt=ve,gt=Je},s.clearConfig=function(){Ht=null,pt=!1,mt=null,gt=null,A=x,M=""},s.isValidAttribute=function(e,t,s){Ht||Yt({});const i=Kt(e),n=Kt(t);return ys(i,n,s)},s.addHook=function(e,t){"function"==typeof t&&me($,e)&&te($[e],t)},s.removeHook=function(e,t){if(me($,e)){if(void 0!==t){const s=Q($[e],t);return-1===s?void 0:se($[e],s,1)[0]}return ee($[e])}},s.removeHooks=function(e){me($,e)&&($[e]=[])},s.removeAllHooks=function(){$={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},s}();const st={ADD_ATTR:["target"],ALLOW_DATA_ATTR:!1,RETURN_DOM_FRAGMENT:!0,USE_PROFILES:{html:!0}},it={ADD_ATTR:["target"],ADD_TAGS:["hellotext-icon"],RETURN_DOM_FRAGMENT:!0,USE_PROFILES:{html:!0,svg:!0}};function nt(e,t){const s=tt.sanitize(e,t);return s.querySelectorAll('a[target="_blank"]').forEach(e=>{const t=new Set(e.rel.split(/\s+/).filter(Boolean));t.add("noopener"),t.add("noreferrer"),e.rel=Array.from(t).join(" ")}),s}function rt(e,t){e.replaceChildren(function(e){return nt(e,st)}(t))}class ot{constructor(e,t=null){this.data=e,this.element=t||document.querySelector(`[data-hello-form="${this.id}"]`)||document.createElement("form")}async mount({ifCompleted:e=!0}={}){if(e&&this.hasBeenCompleted)return this.element?.remove(),vt.eventEmitter.dispatch("form:completed",{id:this.id,...JSON.parse(localStorage.getItem(`hello-form-${this.id}`))});const t=this.data.steps[0];this.buildHeader(t.header),this.buildInputs(t.inputs),this.buildButton(t.button),this.buildFooter(t.footer),this.elementAttributes.forEach(e=>{this.element.setAttribute(e.name,e.value)}),document.contains(this.element)||document.body.appendChild(this.element),vt.business.features.white_label||this.element.prepend(B.build())}buildHeader(e){const t=this.#c("[data-form-header]","header");rt(t,e.content),this.element.querySelector("[data-form-header]")?this.element.querySelector("[data-form-header]").replaceWith(t):this.element.prepend(t)}buildInputs(e){const t=this.#c("[data-form-inputs]","main");e.map(e=>R.build(e)).forEach(e=>t.appendChild(e)),this.element.querySelector("[data-form-inputs]")?this.element.querySelector("[data-form-inputs]").replaceWith(t):this.element.querySelector("[data-form-header]").insertAdjacentHTML("afterend",t.outerHTML)}buildButton(e){const t=this.#c("[data-form-button]","button");t.innerText=e.text,t.setAttribute("data-action","click->hellotext--form#submit"),t.setAttribute("data-hellotext--form-target","button"),this.element.querySelector("[data-form-button]")?this.element.querySelector("[data-form-button]").replaceWith(t):this.element.querySelector("[data-form-inputs]").insertAdjacentHTML("afterend",t.outerHTML)}buildFooter(e){const t=this.#c("[data-form-footer]","footer");rt(t,e.content),this.element.querySelector("[data-form-footer]")?this.element.querySelector("[data-form-footer]").replaceWith(t):this.element.appendChild(t)}markAsCompleted(e){const t={state:"completed",id:this.id,data:e,completedAt:(new Date).getTime()};localStorage.setItem(`hello-form-${this.id}`,JSON.stringify(t)),vt.eventEmitter.dispatch("form:completed",t)}get hasBeenCompleted(){return null!==localStorage.getItem(`hello-form-${this.id}`)}get id(){return this.data.id}get localeAuthKey(){const e=this.data.steps[0];return e.inputs.some(e=>"email"===e.kind)&&e.inputs.some(e=>"phone"===e.kind)?"phone_and_email":e.inputs.some(e=>"email"===e.kind)?"email":e.inputs.some(e=>"phone"===e.kind)?"phone":"none"}get elementAttributes(){return[{name:"data-controller",value:"hellotext--form"},{name:"data-hello-form",value:this.id},{name:"data-hellotext--form-data-value",value:JSON.stringify(this.data)}]}#c(e,t){const s=this.element.querySelector(e);if(s)return s.cloneNode(!0);const i=document.createElement(t);return i.setAttribute(e.replace("[","").replace("]",""),""),i}}class at extends Error{constructor(){super("You need to initialize before tracking events. Call Hellotext.initialize and pass your public business id"),this.name="NotInitializedError"}}class ct{constructor(){this.forms=[],this.includes=this.includes.bind(this),this.excludes=this.excludes.bind(this),this.add=this.add.bind(this),"undefined"!=typeof MutationObserver&&(this.mutationObserver=new MutationObserver(this.formMutationObserver.bind(this)),this.mutationObserver.observe(document.body,{childList:!0,subtree:!0}))}collectExistingFormsOnPage(){Array.from(document.querySelectorAll("[data-hello-form]")).length>0&&this.collect()}formMutationObserver(e){e.find(e=>"childList"===e.type&&e.addedNodes.length>0)&&Array.from(document.querySelectorAll("[data-hello-form]")).length>0&&this.collect()}async collect(){if(vt.notInitialized)throw new at;if(this.fetching)return;if("undefined"==typeof document||!("querySelectorAll"in document))return console.warn("Document is not defined, collection is not possible. Please make sure to initialize the library after the document is loaded.");const e=this.#l;if(0===e.length)return;const t=e.map(e=>T.get(e).then(e=>e.json()));this.fetching=!0,await Promise.all(t).then(e=>e.forEach(this.add)).then(()=>vt.eventEmitter.dispatch("forms:collected",this)).then(()=>this.fetching=!1),f.forms.autoMount&&this.forms.forEach(e=>e.mount())}forEach(e){this.forms.forEach(e)}map(e){return this.forms.map(e)}add(e){this.includes(e.id)||(vt.business.data||(vt.business.setData(e.business),vt.business.setLocale(a.toString())),vt.business.enabledWhitelist||console.warn("No whitelist has been configured. It is advised to whitelist the domain to avoid bots from submitting forms."),this.forms.push(new ot(e)))}getById(e){return this.forms.find(t=>t.id===e)}getByIndex(e){return this.forms[e]}includes(e){return this.forms.some(t=>t.id===e)}excludes(e){return!this.includes(e)}get length(){return this.forms.length}get#l(){return Array.from(document.querySelectorAll("[data-hello-form]")).map(e=>e.dataset.helloForm).filter(this.excludes)}}class lt{constructor(e){this.publicKey=e.public_key,this.serviceWorkerUrl=f.push.serviceWorkerUrl,this.channelId=f.push.channelId,this.ready=null,this.registrationPromise=null,this.subscribePromise=null,this.unsubscribePromise=null,this.syncPromise=null,this.subscription=null,this.retryTimeout=null,this.retryAttempts=0,this.disposed=!1}initialize(){return this.ready=this.restoreSubscription(),this.ready}async restoreSubscription(){const e=await this.getRegistration(),t=await e.pushManager.getSubscription();!this.disposed&&!this.unsubscribePromise&&t&&this.owns(t)&&(this.subscription=t,await this.sync())}subscribe(){if(this.disposed)return Promise.resolve();if(this.unsubscribePromise)return Promise.reject(new Error("Push unsubscribe is in progress"));if(this.subscribePromise)return this.subscribePromise;const e="default"===Notification.permission?Notification.requestPermission():Promise.resolve(Notification.permission);return this.subscribePromise=this.createSubscription(e).finally(()=>{this.subscribePromise=null}),this.subscribePromise}async createSubscription(e){if("granted"!==await e)throw new Error("Push permission was not granted");const t=await this.getRegistration();if(this.disposed)return;let s=await t.pushManager.getSubscription();if(!this.disposed){if(s&&!this.owns(s))throw new Error("The existing Push subscription belongs to a different application");if(s||=await t.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:this.applicationServerKey}),!this.disposed)return this.subscription=s,this.sync()}}unsubscribe(){return this.disposed?Promise.resolve():(this.unsubscribePromise||(this.clearRetryTimeout(),this.unsubscribePromise=this.removeSubscription().finally(()=>{this.unsubscribePromise=null})),this.unsubscribePromise)}async removeSubscription(){await(this.ready?.catch(()=>{})),await(this.subscribePromise?.catch(()=>{})),await(this.syncPromise?.catch(()=>{}));const e=await this.getRegistration(),t=await e.pushManager.getSubscription()||this.subscription;if(this.disposed)return;if(!t)return null;if(!this.owns(t))throw new Error("The existing Push subscription belongs to a different application");this.subscription=t;const s=await I.pushIdentities.destroy({subscription:t.toJSON()});return s.failed?s:this.disposed?void 0:(await t.unsubscribe(),this.subscription=null,this.clearRetryTimeout(),s)}sync(){return this.disposed?Promise.resolve():(this.syncPromise||(this.clearRetryTimeout(),this.syncPromise=this.registerIdentity().finally(()=>{this.syncPromise=null})),this.syncPromise)}async registerIdentity(){try{const e=await I.pushIdentities.create({subscription:this.subscription.toJSON(),...this.channelId?{channel_id:this.channelId}:{}});return e.succeeded?this.retryAttempts=0:this.scheduleRetry(),e}catch(e){throw this.scheduleRetry(),e}}scheduleRetry(){this.disposed||this.unsubscribePromise||this.retryTimeout||this.retryAttempts>=3||(this.retryTimeout=setTimeout(()=>{this.retryTimeout=null,this.sync().catch(()=>{})},1e3*2**this.retryAttempts),this.retryAttempts+=1)}clearRetryTimeout(){clearTimeout(this.retryTimeout),this.retryTimeout=null}dispose(){this.disposed=!0,this.clearRetryTimeout()}get subscribed(){return!!this.subscription}get applicationServerKey(){const e=this.publicKey.replace(/-/g,"+").replace(/_/g,"/");return Uint8Array.from(atob(e.padEnd(4*Math.ceil(e.length/4),"=")),e=>e.charCodeAt(0))}owns(e){const t=e.options?.applicationServerKey;if(!t)return!1;const s=new Uint8Array(t),i=this.applicationServerKey;return s.length===i.length&&s.every((e,t)=>e===i[t])}getRegistration(){return this.registrationPromise||(this.registrationPromise=this.loadRegistration().catch(e=>{throw this.registrationPromise=null,e})),this.registrationPromise}async loadRegistration(){if(this.serviceWorkerUrl){const e=await navigator.serviceWorker.register(this.serviceWorkerUrl);!e.active||e.installing||e.waiting||await e.update();const t=e.installing||e.waiting||e.active;return"activated"===t?.state?e:new Promise((s,i)=>{const n=setTimeout(()=>r(new Error("Push service worker did not become active")),1e4),r=r=>{clearTimeout(n),t?.removeEventListener("statechange",o),r?i(r):s(e)},o=()=>{"activated"===t?.state&&r(),"redundant"===t?.state&&r(new Error("Push service worker installation failed"))};t?.addEventListener("statechange",o),o()})}return new Promise((e,t)=>{const s=setTimeout(()=>t(new Error("Push service worker is not available")),1e4);navigator.serviceWorker.ready.then(t=>{clearTimeout(s),e(t)},e=>{clearTimeout(s),t(e)})})}static get supported(){return"undefined"!=typeof window&&!0===window.isSecureContext&&"undefined"!=typeof navigator&&"serviceWorker"in navigator&&"undefined"!=typeof PushManager&&"undefined"!=typeof Notification}}class ht{constructor(e,t,s){this.business=t,this.push=s,this.disposed=!1,this.showRequest=0,this.controller=null,this.element=(new DOMParser).parseFromString(e.html,"text/html").querySelector('[data-controller~="hellotext--alert"]'),this.connected=new Promise(e=>{this.resolveConnected=e}),this.onConnect=this.onConnect.bind(this),this.ready=this.render()}onConnect(e){this.controller=e.detail.controller,this.controller.alert=this,this.resolveConnected(!0)}async render(){return!!this.element&&(this.element.addEventListener("hellotext--alert:connected",this.onConnect),!(!await this.business.stylesheetLoaded||this.disposed)&&(document.body.appendChild(this.element),this.connected))}async show(e,t={}){const s=++this.showRequest;return!(!await this.ready||this.disposed||s!==this.showRequest)&&this.controller.show(e,t)}hide(){this.showRequest+=1,this.controller?.close()}dispose(){this.disposed=!0,this.hide(),this.resolveConnected(!1),this.element?.removeEventListener("hellotext--alert:connected",this.onConnect),this.element?.remove()}}class ut{static async load(e){const t=new ut({id:e,html:await I.webchats.get(e)});return t.rendered=t.render(),t}constructor(e){this.data=e,this.mounted=!1,this.rendered=Promise.resolve(!1)}async render(){return this.applyBehaviourOverride(),await this.stylesheetLoaded?(this.containerToAppendTo.appendChild(this.data.html),this.markCoexistingWidgets(),this.mounted=!0,!0):(console.warn("Hellotext webchat was not mounted because its stylesheet failed to load."),!1)}applyBehaviourOverride(){f.webchat.hasBehaviourOverride&&f.webchat.behaviour&&this.data.html.setAttribute("data-hellotext--webchat-behaviour-value",JSON.stringify(this.serializedBehaviour))}get serializedBehaviour(){const e=f.webchat.behaviour;return{trigger:this.serializeTrigger(e.trigger),delay_seconds:e.delaySeconds,first_visit_only:e.firstVisitOnly,once_per_session:e.oncePerSession}}serializeTrigger(e){return"onLoad"===e?"on_load":"onClick"===e?"on_click":e}get containerToAppendTo(){return document.querySelector(f.webchat.container)}get stylesheetLoaded(){return P.waitForStylesheet(P.latestStylesheet)}markCoexistingWidgets(){const e=this.data.html,t=document.querySelector(".hellotext--whatsapp-widget");e&&t&&(e.classList.add("hellotext--with-whatsapp-widget"),t.classList.add("hellotext--with-webchat"))}}class dt{static async load(e){const t=new dt({id:e,html:await I.whatsappWidgets.get(e)});return t.rendered=t.render(),t}constructor(e){this.data=e,this.mounted=!1,this.rendered=Promise.resolve(!1)}async render(){if(!this.data.html)return!1;const e=this.containerToAppendTo;return e?await this.stylesheetLoaded?(e.appendChild(this.data.html),this.markCoexistingWidgets(),this.mounted=!0,!0):(console.warn("Hellotext WhatsApp widget was not mounted because its stylesheet failed to load."),!1):(console.warn(`Hellotext WhatsApp widget was not mounted because the container ${f.whatsapp.container} was not found.`),!1)}get containerToAppendTo(){try{return document.querySelector(f.whatsapp.container)}catch(e){return null}}get stylesheetLoaded(){return P.waitForStylesheet(P.latestStylesheet)}markCoexistingWidgets(){const e=document.querySelector(".hellotext--webchat:not(.hellotext--whatsapp-widget)"),t=this.data.html;e&&t&&(e.classList.add("hellotext--with-whatsapp-widget"),t.classList.add("hellotext--with-webchat"))}}class pt{static async load(e,t={}){const s=new pt({id:e,html:await I.popups.get(e)},t);return s.rendered=s.render(),s}constructor(e,{container:t=f.popup.container,shouldMount:s=()=>!0}={}){this.data=e,this.container=t,this.mounted=!1,this.rendered=Promise.resolve(!1),this.shouldMount=s}async render(){if(!this.data.html||!this.shouldMount())return!1;const e=this.containerToAppendTo;return e?!!this.shouldMount()&&(e.appendChild(this.data.html),this.mounted=!0,this.shouldMount()||this.unmount(),this.mounted):(console.warn(`Hellotext popup was not mounted because the container ${this.container} was not found.`),!1)}unmount(){this.data.html?.remove(),this.mounted=!1}get containerToAppendTo(){try{return document.querySelector(this.container)}catch(e){return null}}}class mt{static get id(){return _.get("hello_user_id")}static get source(){return _.get("hello_user_source")}static get fingerprint(){return _.get("hello_user_identification_hash")}static remember(e,t,s){t&&_.set("hello_user_source",t),s&&_.set("hello_user_identification_hash",s),_.set("hello_user_id",e)}static forget(){_.delete("hello_user_id"),_.delete("hello_user_source"),_.delete("hello_user_identification_hash")}static get identificationData(){return this.id?{id:this.id,source:this.source}:{}}}function gt(e){if(null!=e){if("string"==typeof e){const t=e.trim();return""===t?void 0:t}if(Array.isArray(e))return e.map(e=>gt(e)).filter(e=>void 0!==e);if(e instanceof Date)return e.toISOString();if("object"==typeof e){const t=Object.keys(e).sort((e,t)=>e.localeCompare(t)).reduce((t,s)=>{const i=gt(e[s]);return void 0!==i&&(t[s]=i),t},{});return Object.keys(t).length>0?t:void 0}return"number"==typeof e||"boolean"==typeof e?e:void 0}}function ft(e,t,s={}){const i=gt({session:e,user_id:t,...s})||{};return JSON.stringify(i)}class bt{static matches(e,t){return!!e&&e===t}static async generate(e,t,s={}){return await async function(e){if(!globalThis.crypto?.subtle||"undefined"==typeof TextEncoder)return function(e){let t=5381;for(let s=0;s>>0).toString(16)}`}(e);const t=await globalThis.crypto.subtle.digest("SHA-256",(new TextEncoder).encode(e));return`v1:${Array.from(new Uint8Array(t)).map(e=>e.toString(16).padStart(2,"0")).join("")}`}(ft(e,t,s))}}class yt{static eventEmitter=new r;static forms;static business;static popup;static webchat;static whatsapp;static push;static alert;static initializationVersion=0;static async initialize(e,t={}){const s=++this.initializationVersion;this.popup?.unmount?.(),this.popup=void 0,this.alert?.dispose(),this.alert=null,this.push?.dispose(),this.push=null;const i=new P(e);this.business=i,this.page=new D,f.assign({push:{},...t}),F.initialize(this.page),this.forms=new ct,this.query=new y;const n=await i.hydrate();if(this.business!==i)return;!1!==t.push&&n?.push?.public_key&<.supported&&(this.push=new lt(n.push),this.push.initialize().catch(e=>{console.warn("Hellotext Push initialization failed:",e)}),n.alert?.html&&(this.alert=new ht(n.alert,i,this.push)));const r=!1!==t.popup&&this.deepMergePlainObjects(n&&n.popup||{},t.popup||{}),o=!1!==t.webchat&&this.mergeWebchatConfig(n&&n.webchat||{},t.webchat||{}),a=!1!==t.whatsappWidget&&this.mergeWhatsAppConfig(n&&n.whatsapp||{},t.whatsappWidget||{}),c=t.webchat&&!1!==t.webchat&&Object.prototype.hasOwnProperty.call(t.webchat,"behaviour");f.webchat.behaviourOverride=c;const l=[];if(o&&o.id&&(f.webchat.assign(o),l.push(ut.load(o.id).then(e=>{this.business===i&&(this.webchat=e)}))),a&&a.id&&(f.whatsapp.assign(a),l.push(dt.load(a.id).then(e=>{this.business===i&&(this.whatsapp=e)}))),r&&r.id){const e={container:"body",device:"auto",...r};f.popup.assign(e),l.push(pt.load(e.id,{container:e.container,shouldMount:()=>this.business===i&&this.initializationVersion===s}).then(e=>{this.business===i&&this.initializationVersion===s&&(this.popup=e)}))}await Promise.all(l),this.business===i&&this.initializationVersion===s&&"undefined"!=typeof MutationObserver&&this.forms.collectExistingFormsOnPage()}static mergeWebchatConfig(e,t){return this.deepMergePlainObjects(e,t)}static mergeWhatsAppConfig(e,t){return this.deepMergePlainObjects(e,t)}static deepMergePlainObjects(e,t){const s={...e};return Object.entries(t).forEach(([e,t])=>{this.isPlainObject(t)&&this.isPlainObject(s[e])?s[e]=this.deepMergePlainObjects(s[e],t):s[e]=t}),s}static isPlainObject(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}static async track(e,t={}){if(this.notInitialized)throw new at;const s={...t&&t.headers||{},...this.headers},i={...mt.identificationData,...t.user_parameters||{}},n=t&&t.url?new D(t.url):this.page,r={session:this.session,user_parameters:i,action:e,...t,...n.trackingData};return delete r.headers,await I.events.create({headers:s,body:r,keepalive:k(r)})}static async identify(e,t={}){const s=await bt.generate(this.session,e,t);if(bt.matches(mt.fingerprint,s))return new v(!0,{json:async()=>({already_identified:!0})});const i=await I.identifications.create({user_id:e,...t});return i.succeeded&&mt.remember(e,t.source,s),i}static forget(){mt.forget()}static on(e,t){this.eventEmitter.addSubscriber(e,t)}static removeEventListener(e,t){this.eventEmitter.removeSubscriber(e,t)}static get session(){return F.session}static get isInitialized(){return void 0!==F.session}static get notInitialized(){return!this.business||void 0===this.business.id}static get headers(){if(this.notInitialized)throw new at;return{Authorization:`Bearer ${this.business.id}`,Accept:"application/json","Content-Type":"application/json"}}}const vt=yt,wt=new Map,Tt=class extends i.xI{static values={sections:Array};static targets=["title","description","primaryAction","secondaryAction"];initialize(){this.showRequest=0,this.submitting=!1,this.onStorage=this.onStorage.bind(this)}connect(){this.dispatch("connected",{detail:{controller:this}}),window.addEventListener("storage",this.onStorage)}disconnect(){this.close(),window.removeEventListener("storage",this.onStorage)}onStorage(e){e.key===this.storageKey&&this.dismissal.dismissedUntil>Date.now()&&this.close()}async show(e,{force:t=!1,title:s,description:i,primaryAction:n,secondaryAction:r}={}){const o=++this.showRequest,a=this.sectionsValue.find(t=>t.kind===e);return a?(await(this.alert.push.ready?.catch(()=>{})),!(o!==this.showRequest||this.alert.disposed||!this.element.isConnected||(this.unavailable||!t&&this.dismissal.dismissedUntil>Date.now()?(this.close(),1):(this.titleTarget.textContent=s??a.title,this.descriptionTarget.textContent=i??a.description,this.primaryActionTarget.textContent=n??a.primary_action,this.secondaryActionTarget.textContent=r??a.secondary_action,this.kind=e,this.page=vt.page.trackingData.page,this.element.hidden=!1,this.record("shown"),vt.eventEmitter.dispatch("alert:shown",{kind:e}),0)))):(this.close(),!1)}hide(){if(this.element.hidden||this.submitting)return;const e=this.dismissal.dismissals+1,t={dismissals:e,dismissedUntil:Date.now()+864e5*(1===e?7:30)};wt.set(this.storageKey,t);try{localStorage.setItem(this.storageKey,JSON.stringify(t))}catch(e){}this.close(),this.record("dismissed"),vt.eventEmitter.dispatch("alert:dismissed",{kind:this.kind})}close(){this.showRequest+=1,this.element.hidden=!0}async subscribe(){if(!this.submitting&&!this.element.hidden){if(this.unavailable)return this.close();this.submitting=!0,this.primaryActionTarget.disabled=!0,this.secondaryActionTarget.disabled=!0,this.element.setAttribute("aria-busy","true");try{this.record("accepted"),vt.eventEmitter.dispatch("alert:accepted",{kind:this.kind});const e=await this.alert.push.subscribe();return this.unavailable&&this.close(),e?.failed&&this.dispatch("error",{detail:{response:e}}),e}catch(e){this.unavailable&&this.close(),this.dispatch("error",{detail:{error:e}})}finally{this.submitting=!1,this.primaryActionTarget.disabled=!1,this.secondaryActionTarget.disabled=!1,this.element.removeAttribute("aria-busy")}}}async record(e){try{const t=await I.pushAlerts.create({section:this.kind,kind:e,page:this.page});t.failed&&console.warn("Hellotext Smart Alert submission failed:",t)}catch(e){console.warn("Hellotext Smart Alert submission failed:",e)}}get unavailable(){return this.alert.push.disposed||this.alert.push.subscribed||"undefined"==typeof Notification||"denied"===Notification.permission}get storageKey(){return`hellotext:alert:${this.alert.business.id}`}get dismissal(){try{const e=JSON.parse(localStorage.getItem(this.storageKey));Number.isSafeInteger(e?.dismissals)&&e.dismissals>0&&Number.isFinite(e.dismissedUntil)&&e.dismissedUntil>=0&&wt.set(this.storageKey,e)}catch(e){}return wt.get(this.storageKey)||{dismissals:0,dismissedUntil:0}}},St=class extends i.xI{static values={data:Object,step:{type:Number,default:1}};static targets=["inputContainer","input","button","otpContainer"];initialize(){this.form=new ot(this.dataValue,this.element)}connect(){super.connect(),this.element.addEventListener("submit",this.submit.bind(this)),"INPUT"!==document.activeElement.tagName&&this.inputTargets[0].focus()}async submit(e){if(e.preventDefault(),this.invalid)return this.showErrorMessages();this.clearErrorMessages(),this.formData=Object.fromEntries(new FormData(this.element)),this.buttonTarget.disabled=!0;const t=await T.submit(this.form.id,this.formData);this.buttonTarget.disabled=!1;const s=await t.json();if(t.failed)return s.errors.forEach(e=>{const{type:t,parameter:s}=e,i=this.inputTargets.find(e=>e.name===s);i.setCustomValidity(vt.business.locale.errors[t]),i.reportValidity(),i.addEventListener("input",()=>{i.setCustomValidity(""),i.reportValidity()})}),this.showErrorMessages();this.buttonTarget.style.display="none",this.element.querySelectorAll("input").forEach(e=>e.disabled=!0),this.completed()}completed(){if(this.form.markAsCompleted(this.formData),!f.forms.shouldShowSuccessMessage)return this.element.remove();"string"==typeof f.forms.successMessage?this.element.innerHTML=f.forms.successMessage:this.element.innerHTML=vt.business.locale.forms[this.form.localeAuthKey]}showErrorMessages(){this.inputTargets.forEach(e=>{const t=e.closest("article").querySelector("[data-error-container]");e.validity.valid?t.innerText="":t.innerText=e.validationMessage})}clearErrorMessages(){this.inputTargets.forEach(e=>{e.setCustomValidity(""),e.closest("article").querySelector("[data-error-container]").innerText=""})}inputTargetConnected(e){e.getAttribute("data-default-value")&&(e.value=e.getAttribute("data-default-value"))}get requiredInputs(){return this.inputTargets.filter(e=>e.required)}get invalid(){return!this.element.checkValidity()}},Ct=class extends i.xI{static values={fadeDistance:{type:Number,default:64},id:String,kind:String,pageStartOffset:{type:Number,default:0},utm:Object};static targets=["carouselContainer","leftFade","rightFade","carouselCard"];connect(){this.updateFades(),this.observeContainerSize()}disconnect(){this.resizeObserver?.disconnect()}setId({detail:e}){this.idValue=e,this.element.id=e}onScroll(){this.updateFades()}quickReply({currentTarget:e}){const t=e.closest('[data-hellotext--message-target="carouselCard"]'),s=e.closest('[data-controller~="hellotext--message"]'),i=e.dataset.text||e.textContent.trim();this.dispatch("quickReply",{detail:{id:this.idValue,product:t?.dataset.id,buttonId:e.dataset.id,body:i,cardElement:t||s||e}})}addToCart({currentTarget:e}){const t=e.closest('[data-hellotext--message-target="carouselCard"]'),{id:s,reference:i,source:n}=t.dataset,r={product:s,quantity:1};this.hasUtmValue&&vt.page.utm.save(this.utmValue),vt.eventEmitter.dispatch("cart.added",{object_parameters:{items:[{...r,...i&&{reference:i},...n&&{source:n}}]},source:{kind:this.kindValue,message_id:this.idValue,button_id:e.dataset.id}})}moveToLeft(){if(!this.hasCarouselContainerTarget)return;const e=this.getPreviousPageScrollLeft(),t=this.carouselContainerTarget.scrollLeft-e;t<1||this.carouselContainerTarget.scrollBy({left:-t,behavior:"smooth"})}moveToRight(){if(!this.hasCarouselContainerTarget)return;const e=this.getNextPageScrollLeft()-this.carouselContainerTarget.scrollLeft;e<1||this.carouselContainerTarget.scrollBy({left:e,behavior:"smooth"})}getScrollAmount(){return this.getCardScrollAmount()}getCardScrollAmount(){const e=this.carouselContainerTarget.querySelector(".message__carousel_card");return e?e.offsetWidth+this.getGap():280}getPageScrollAmount(){const e=this.carouselContainerTarget.clientWidth-this.getGap();return e>0?e:this.getCardScrollAmount()}getNextPageScrollLeft(){const e=this.getCurrentScrollLeft(),t=e+this.carouselContainerTarget.clientWidth,s=this.getCardMetrics().find(e=>e.end>t+1),i=s?this.getPageAlignedScrollLeft(s.start):e+this.getPageScrollAmount(),n=e+this.getPageScrollAmount();return this.clampScrollLeft(i>e+1?i:n)}getPreviousPageScrollLeft(){const e=this.getCurrentScrollLeft();if(e<=1)return 0;const t=Math.max(e-this.getPageScrollAmount(),0);if(t<=1)return 0;const s=this.getCardMetrics(),i=s.find(s=>s.start>=t-1&&s.startt.start{const t=this.getCardScrollLeft(e);return{start:t,end:t+e.offsetWidth}})}getCardScrollLeft(e){const t=e.getBoundingClientRect(),s=this.carouselContainerTarget.getBoundingClientRect();return t.left||t.width||s.left||s.width?t.left-s.left+this.carouselContainerTarget.scrollLeft:e.offsetLeft||0}getCurrentScrollLeft(){return this.clampScrollLeft(this.carouselContainerTarget.scrollLeft)}clampScrollLeft(e){const t=Math.max(this.carouselContainerTarget.scrollWidth-this.carouselContainerTarget.clientWidth,0);return Math.min(Math.max(e,0),t)}getGap(){const e=window.getComputedStyle(this.carouselContainerTarget),t=Number.parseFloat(e.columnGap||e.gap);return Number.isFinite(t)?t:16}getFadeDistance(){return Number.isFinite(this.fadeDistanceValue)?this.fadeDistanceValue:64}getPageStartOffset(){return Number.isFinite(this.pageStartOffsetValue)?this.pageStartOffsetValue:0}observeContainerSize(){this.hasCarouselContainerTarget&&window.ResizeObserver&&(this.resizeObserver=new ResizeObserver(()=>this.updateFades()),this.resizeObserver.observe(this.carouselContainerTarget))}updateFades(){if(!this.hasCarouselContainerTarget)return;const e=Math.max(this.carouselContainerTarget.scrollWidth-this.carouselContainerTarget.clientWidth,0);if(e<=1)return this.hideFade(this.leftFadeTarget),void this.hideFade(this.rightFadeTarget);const t=Math.min(Math.max(this.carouselContainerTarget.scrollLeft,0),e),s=this.getFadeDistance();this.setFadeOpacity(this.leftFadeTarget,t/s),this.setFadeOpacity(this.rightFadeTarget,(e-t)/s)}setFadeOpacity(e,t){const s=Math.min(Math.max(t,0),1);s<=.05?this.hideFade(e):(e.classList.remove("hidden"),e.removeAttribute("disabled"),e.removeAttribute("tabindex"),e.setAttribute("aria-hidden","false"),e.style.opacity=s.toFixed(3),e.style.pointerEvents="auto")}hideFade(e){e.style.opacity="0",e.style.pointerEvents="none",e.setAttribute("aria-hidden","true"),e.setAttribute("tabindex","-1"),"disabled"in e&&(e.disabled=!0),e.classList.add("hidden")}},Ot=class extends i.xI{static targets=["bubble","dialog","step","completed","input","submitButton","globalError","resendButton","changeDestinationButton","deliveryCopy","noDeliveryCopy"];static values={capture:Object,device:String,hasBubble:Boolean,id:String};initialize(){this.stepIndex=0,this.resendLabel=this.hasResendButtonTarget?this.resendButtonTarget.textContent.trim():""}connect(){vt.eventEmitter.dispatch("popup:mounted"),this.evaluateDisplay()}disconnect(){this.stopResendCooldown()}open(e){e&&e.preventDefault(),this.hasBubbleTarget&&(this.bubbleTarget.hidden=!0),this.dialogTarget.hidden=!1,vt.eventEmitter.dispatch("popup:opened")}close(e){e&&e.preventDefault(),this.dismissed=!0,this.dialogTarget.hidden=!0,this.hasBubbleTarget&&(this.bubbleTarget.hidden=!0),this.element.hidden=!0,vt.eventEmitter.dispatch("popup:closed")}async next(e){e&&e.preventDefault(),this.clearCustomValidity(),this.currentStepValid()?(this.clearErrorMessages(this.currentStepInputs),this.stepIndex{e.disabled=!0});try{const e=this.submissionPayload(),t=await C.submit(this.idValue,e,this.idempotencyKeyFor(e));if(t.failed)return void await this.handleSubmissionError(t);const s=await t.json();this.submissionId=s.id,this.submissionVerificationState=s.verification_state,this.submissionActionToken=s.action_token,this.submissionDeliveryStatus=s.delivery_status,this.submissionDeliveryChannel=s.delivery_channel,this.submissionDestination=s.destination,this.resetSubmissionRequest()}catch(e){return void this.showGlobalError()}finally{this.submitButtonTargets.forEach(e=>{e.disabled=!1})}this.showCompleted()}else this.showErrorMessages(this.currentStepInputs)}evaluateDisplay(){!this.dismissed&&this.matchesDevice()?this.showInitialState():this.element.hidden=!0}showInitialState(){if(this.element.hidden=!1,this.hasBubbleValue&&this.hasBubbleTarget)return this.bubbleTarget.hidden=!1,void(this.dialogTarget.hidden=!0);this.hasBubbleTarget&&(this.bubbleTarget.hidden=!0),this.dialogTarget.hidden=!1,vt.eventEmitter.dispatch("popup:opened")}showStep(e){this.stepIndex=e,this.stepTargets.forEach((t,s)=>{t.hidden=s!==e}),this.completedTarget.hidden=!0}showCompleted(){this.stepTargets.forEach(e=>{e.hidden=!0}),this.interpolateCompletionCopy(),this.configureCompletionActions(),this.completedTarget.hidden=!1}interpolateCompletionCopy(){const e=this.completedIdentity;if(!e)return;const t={destination:e.value,channel:this.submissionDeliveryChannel||e.kind};this.completionTextTemplates.forEach(({node:e,template:s})=>{e.nodeValue=s.replace(/\{(destination|channel)\}/g,(e,s)=>t[s]||e)})}identityValue(e){const t=this.inputValue(e).trim();if("phone"!==e.dataset.popupFieldKind||t.startsWith("+"))return t;const s=e.dataset.popupPhonePrefix;return s?`${s}${t.replace(/^0+/,"")}`:t}configureCompletionActions(){const e="not_required"!==this.submissionDeliveryStatus;if(this.revealCompletionCopy(e),!e)return void this.completedTarget.querySelector("[data-delivery-actions]")?.setAttribute("hidden","");const t=this.completedIdentity;t&&(this.hasChangeDestinationButtonTarget&&(this.changeDestinationButtonTarget.textContent=this.changeDestinationButtonTarget.dataset[`${t.kind}Label`],this.changeDestinationButtonTarget.hidden=!1),this.submissionId&&this.submissionActionToken&&"queued"===this.submissionDeliveryStatus&&"unverified"===this.submissionVerificationState&&this.hasResendButtonTarget&&(this.resendButtonTarget.hidden=!1,this.startResendCooldown(60)))}async resend(e){if(e&&e.preventDefault(),this.submissionId&&this.submissionActionToken&&!this.resendPending&&!this.resendCooldownActive&&this.completedIdentity){this.resendPending=!0,this.resendButtonTarget.disabled=!0;try{const e=await C.resend(this.idValue,this.submissionId,this.submissionActionToken),t=Number(e.data.headers?.get("Retry-After"))||60;e.succeeded||429===e.data.status?this.startResendCooldown(t):this.resendButtonTarget.disabled=!1}catch(e){this.resendButtonTarget.disabled=!1}finally{this.resendPending=!1}}}async changeDestination(e){if(e&&e.preventDefault(),!this.submissionId||!this.submissionActionToken||this.changeDestinationPending)return;const t=this.completedIdentity?.input;if(!t)return;const s=this.stepTargets.findIndex(e=>e.dataset.stepId===t.dataset.popupStepId);if(!(s<0)){this.changeDestinationPending=!0,this.changeDestinationButtonTarget.disabled=!0;try{if((await C.cancel(this.idValue,this.submissionId,this.submissionActionToken)).failed)return;this.stopResendCooldown(),this.submissionId=null,this.submissionActionToken=null,this.submissionVerificationState=null,this.submissionDeliveryStatus=null,this.submissionDeliveryChannel=null,this.submissionDestination=null,this.resetSubmissionRequest(),this.showStep(s),t.focus()}catch(e){}finally{this.changeDestinationPending=!1,this.changeDestinationButtonTarget.disabled=!1}}}startResendCooldown(e){this.stopResendCooldown(),this.resendCooldownEndsAt=Date.now()+1e3*Math.max(e,1),this.updateResendCountdown(),this.resendTimer=window.setInterval(()=>this.updateResendCountdown(),1e3)}stopResendCooldown(){this.resendTimer&&window.clearInterval(this.resendTimer),this.resendTimer=null,this.resendCooldownEndsAt=null}updateResendCountdown(){const e=Math.max(0,Math.ceil((this.resendCooldownEndsAt-Date.now())/1e3));if(0===e)return this.stopResendCooldown(),this.resendButtonTarget.textContent=this.resendLabel,void(this.resendButtonTarget.disabled=!1);const t=`${Math.floor(e/60)}:${String(e%60).padStart(2,"0")}`,s=this.resendButtonTarget.dataset.countdownLabel||`${this.resendLabel} %{time}`;this.resendButtonTarget.textContent=s.replace("%{time}",t),this.resendButtonTarget.disabled=!0}get resendCooldownActive(){return this.resendCooldownEndsAt>Date.now()}get completionIdentity(){return this.identityInputs.map(e=>({input:e,kind:e.dataset.popupFieldKind,value:this.identityValue(e)})).find(({value:e})=>e)}get completedIdentity(){if(this.submissionDestination&&this.submissionDeliveryChannel){const e="email"===this.submissionDeliveryChannel?"email":"phone";return{input:this.identityInputs.find(t=>t.dataset.popupFieldKind===e),kind:e,value:this.submissionDestination}}return this.completionIdentity}revealCompletionCopy(e){this.hasDeliveryCopyTarget&&this.deliveryCopyTargets.forEach(t=>{t.hidden=!e}),this.hasNoDeliveryCopyTarget&&this.noDeliveryCopyTargets.forEach(t=>{t.hidden=e})}currentStepValid(){return this.currentStepInputs.every(e=>e.checkValidity())}showErrorMessages(e){e.forEach(e=>{const t=e.closest(".hellotext--popup-field")?.querySelector("[data-error-container]");t&&(t.textContent=e.validity.valid?"":e.validationMessage)})}clearErrorMessages(e=this.inputTargets){e.forEach(e=>{const t=e.closest(".hellotext--popup-field")?.querySelector("[data-error-container]");t&&(t.textContent="")})}clearCustomValidity(){this.inputTargets.forEach(e=>e.setCustomValidity(""))}clearGlobalError(){this.hasGlobalErrorTarget&&(this.globalErrorTarget.textContent="",this.globalErrorTarget.hidden=!0)}showGlobalError(e=null){this.hasGlobalErrorTarget&&(this.globalErrorTarget.textContent=e||this.globalErrorTarget.dataset.submitError||"Unable to submit. Please try again.",this.globalErrorTarget.hidden=!1)}async handleSubmissionError(e){let t;try{t=await e.json()}catch(e){return void this.showGlobalError()}const s=t.errors||[],i=[];s.forEach(e=>{const t=this.inputForError(e);t?(t.setCustomValidity(e.description||t.validationMessage),t.reportValidity()):e.description&&i.push(e.description)}),this.showErrorMessages(this.inputTargets),i.length?this.showGlobalError(i.join(" ")):s.length||this.showGlobalError()}inputForError(e){const t=e.parameter;return t?this.inputTargets.find(e=>e.dataset.popupFieldKind===t||e.dataset.popupFieldKey===t):null}submissionPayload(){const e={metadata:{capture:this.captureValue||{},fields:{},steps:[]}};return this.stepTargets.forEach(t=>{const s={};this.inputsForStep(t).forEach(t=>{const i=this.inputValue(t),n=t.dataset.popupFieldKey||t.name;s[n]=i,e.metadata.fields[n]=i,"email"===t.dataset.popupFieldKind&&(e.email=i),"phone"===t.dataset.popupFieldKind&&(e.phone=i)}),e.metadata.steps.push({id:t.dataset.stepId,name:t.dataset.stepName,fields:s})}),e}idempotencyKeyFor(e){const t=JSON.stringify(e);return this.submissionPayloadSnapshot!==t&&(this.submissionPayloadSnapshot=t,this.submissionIdempotencyKey=C.idempotencyKey()),this.submissionIdempotencyKey}resetSubmissionRequest(){this.submissionPayloadSnapshot=null,this.submissionIdempotencyKey=null}inputValue(e){return"checkbox"===e.type?e.checked:e.value}inputsForStep(e){return this.inputTargets.filter(t=>t.dataset.popupStepId===e.dataset.stepId)}get identityInputs(){const e=this.inputTargets.filter(e=>["email","phone"].includes(e.dataset.popupFieldKind));return e.filter(e=>e.required).concat(e.filter(e=>!e.required))}get completionTextTemplates(){if(this._completionTextTemplates)return this._completionTextTemplates;const e=document.createTreeWalker(this.completedTarget,NodeFilter.SHOW_TEXT);for(this._completionTextTemplates=[];e.nextNode();)this._completionTextTemplates.push({node:e.currentNode,template:e.currentNode.nodeValue});return this._completionTextTemplates}matchesDevice(){return"all"===this.deviceValue||("mobile"===this.deviceValue?window.innerWidth<768:"desktop"!==this.deviceValue||window.innerWidth>=768)}get currentStep(){return this.stepTargets[this.stepIndex]}get currentStepInputs(){return this.inputsForStep(this.currentStep)}},Et=["start","end"],At=["top","right","bottom","left"].reduce((e,t)=>e.concat(t,t+"-"+Et[0],t+"-"+Et[1]),[]),xt=Math.min,Mt=Math.max,kt=Math.round,It=Math.floor,Lt=e=>({x:e,y:e}),Pt={left:"right",right:"left",bottom:"top",top:"bottom"},_t={start:"end",end:"start"};function Nt(e,t,s){return Mt(e,xt(t,s))}function Dt(e,t){return"function"==typeof e?e(t):e}function Ft(e){return e.split("-")[0]}function Rt(e){return e.split("-")[1]}function Bt(e){return"x"===e?"y":"x"}function $t(e){return"y"===e?"height":"width"}const jt=new Set(["top","bottom"]);function Vt(e){return jt.has(Ft(e))?"y":"x"}function qt(e){return Bt(Vt(e))}function Ut(e,t,s){void 0===s&&(s=!1);const i=Rt(e),n=qt(e),r=$t(n);let o="x"===n?i===(s?"end":"start")?"right":"left":"start"===i?"bottom":"top";return t.reference[r]>t.floating[r]&&(o=Jt(o)),[o,Jt(o)]}function zt(e){return e.replace(/start|end/g,e=>_t[e])}const Wt=["left","right"],Kt=["right","left"],Ht=["top","bottom"],Gt=["bottom","top"];function Jt(e){return e.replace(/left|right|bottom|top/g,e=>Pt[e])}function Yt(e){const{x:t,y:s,width:i,height:n}=e;return{width:i,height:n,top:s,left:t,right:t+i,bottom:s+n,x:t,y:s}}function Zt(e,t,s){let{reference:i,floating:n}=e;const r=Vt(t),o=qt(t),a=$t(o),c=Ft(t),l="y"===r,h=i.x+i.width/2-n.width/2,u=i.y+i.height/2-n.height/2,d=i[a]/2-n[a]/2;let p;switch(c){case"top":p={x:h,y:i.y-n.height};break;case"bottom":p={x:h,y:i.y+i.height};break;case"right":p={x:i.x+i.width,y:u};break;case"left":p={x:i.x-n.width,y:u};break;default:p={x:i.x,y:i.y}}switch(Rt(t)){case"start":p[o]-=d*(s&&l?-1:1);break;case"end":p[o]+=d*(s&&l?-1:1)}return p}async function Xt(e,t){var s;void 0===t&&(t={});const{x:i,y:n,platform:r,rects:o,elements:a,strategy:c}=e,{boundary:l="clippingAncestors",rootBoundary:h="viewport",elementContext:u="floating",altBoundary:d=!1,padding:p=0}=Dt(t,e),m=function(e){return"number"!=typeof e?function(e){return{top:0,right:0,bottom:0,left:0,...e}}(e):{top:e,right:e,bottom:e,left:e}}(p),g=a[d?"floating"===u?"reference":"floating":u],f=Yt(await r.getClippingRect({element:null==(s=await(null==r.isElement?void 0:r.isElement(g)))||s?g:g.contextElement||await(null==r.getDocumentElement?void 0:r.getDocumentElement(a.floating)),boundary:l,rootBoundary:h,strategy:c})),b="floating"===u?{x:i,y:n,width:o.floating.width,height:o.floating.height}:o.reference,y=await(null==r.getOffsetParent?void 0:r.getOffsetParent(a.floating)),v=await(null==r.isElement?void 0:r.isElement(y))&&await(null==r.getScale?void 0:r.getScale(y))||{x:1,y:1},w=Yt(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({elements:a,rect:b,offsetParent:y,strategy:c}):b);return{top:(f.top-w.top+m.top)/v.y,bottom:(w.bottom-f.bottom+m.bottom)/v.y,left:(f.left-w.left+m.left)/v.x,right:(w.right-f.right+m.right)/v.x}}const Qt=new Set(["left","top"]);function es(){return"undefined"!=typeof window}function ts(e){return ns(e)?(e.nodeName||"").toLowerCase():"#document"}function ss(e){var t;return(null==e||null==(t=e.ownerDocument)?void 0:t.defaultView)||window}function is(e){var t;return null==(t=(ns(e)?e.ownerDocument:e.document)||window.document)?void 0:t.documentElement}function ns(e){return!!es()&&(e instanceof Node||e instanceof ss(e).Node)}function rs(e){return!!es()&&(e instanceof Element||e instanceof ss(e).Element)}function os(e){return!!es()&&(e instanceof HTMLElement||e instanceof ss(e).HTMLElement)}function as(e){return!(!es()||"undefined"==typeof ShadowRoot)&&(e instanceof ShadowRoot||e instanceof ss(e).ShadowRoot)}const cs=new Set(["inline","contents"]);function ls(e){const{overflow:t,overflowX:s,overflowY:i,display:n}=Ts(e);return/auto|scroll|overlay|hidden|clip/.test(t+i+s)&&!cs.has(n)}const hs=new Set(["table","td","th"]);function us(e){return hs.has(ts(e))}const ds=[":popover-open",":modal"];function ps(e){return ds.some(t=>{try{return e.matches(t)}catch(e){return!1}})}const ms=["transform","translate","scale","rotate","perspective"],gs=["transform","translate","scale","rotate","perspective","filter"],fs=["paint","layout","strict","content"];function bs(e){const t=ys(),s=rs(e)?Ts(e):e;return ms.some(e=>!!s[e]&&"none"!==s[e])||!!s.containerType&&"normal"!==s.containerType||!t&&!!s.backdropFilter&&"none"!==s.backdropFilter||!t&&!!s.filter&&"none"!==s.filter||gs.some(e=>(s.willChange||"").includes(e))||fs.some(e=>(s.contain||"").includes(e))}function ys(){return!("undefined"==typeof CSS||!CSS.supports)&&CSS.supports("-webkit-backdrop-filter","none")}const vs=new Set(["html","body","#document"]);function ws(e){return vs.has(ts(e))}function Ts(e){return ss(e).getComputedStyle(e)}function Ss(e){return rs(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function Cs(e){if("html"===ts(e))return e;const t=e.assignedSlot||e.parentNode||as(e)&&e.host||is(e);return as(t)?t.host:t}function Os(e){const t=Cs(e);return ws(t)?e.ownerDocument?e.ownerDocument.body:e.body:os(t)&&ls(t)?t:Os(t)}function Es(e,t,s){var i;void 0===t&&(t=[]),void 0===s&&(s=!0);const n=Os(e),r=n===(null==(i=e.ownerDocument)?void 0:i.body),o=ss(n);if(r){const e=As(o);return t.concat(o,o.visualViewport||[],ls(n)?n:[],e&&s?Es(e):[])}return t.concat(n,Es(n,[],s))}function As(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function xs(e){const t=Ts(e);let s=parseFloat(t.width)||0,i=parseFloat(t.height)||0;const n=os(e),r=n?e.offsetWidth:s,o=n?e.offsetHeight:i,a=kt(s)!==r||kt(i)!==o;return a&&(s=r,i=o),{width:s,height:i,$:a}}function Ms(e){return rs(e)?e:e.contextElement}function ks(e){const t=Ms(e);if(!os(t))return Lt(1);const s=t.getBoundingClientRect(),{width:i,height:n,$:r}=xs(t);let o=(r?kt(s.width):s.width)/i,a=(r?kt(s.height):s.height)/n;return o&&Number.isFinite(o)||(o=1),a&&Number.isFinite(a)||(a=1),{x:o,y:a}}const Is=Lt(0);function Ls(e){const t=ss(e);return ys()&&t.visualViewport?{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}:Is}function Ps(e,t,s,i){void 0===t&&(t=!1),void 0===s&&(s=!1);const n=e.getBoundingClientRect(),r=Ms(e);let o=Lt(1);t&&(i?rs(i)&&(o=ks(i)):o=ks(e));const a=function(e,t,s){return void 0===t&&(t=!1),!(!s||t&&s!==ss(e))&&t}(r,s,i)?Ls(r):Lt(0);let c=(n.left+a.x)/o.x,l=(n.top+a.y)/o.y,h=n.width/o.x,u=n.height/o.y;if(r){const e=ss(r),t=i&&rs(i)?ss(i):i;let s=e,n=As(s);for(;n&&i&&t!==s;){const e=ks(n),t=n.getBoundingClientRect(),i=Ts(n),r=t.left+(n.clientLeft+parseFloat(i.paddingLeft))*e.x,o=t.top+(n.clientTop+parseFloat(i.paddingTop))*e.y;c*=e.x,l*=e.y,h*=e.x,u*=e.y,c+=r,l+=o,s=ss(n),n=As(s)}}return Yt({width:h,height:u,x:c,y:l})}function _s(e,t){const s=Ss(e).scrollLeft;return t?t.left+s:Ps(is(e)).left+s}function Ns(e,t,s){void 0===s&&(s=!1);const i=e.getBoundingClientRect();return{x:i.left+t.scrollLeft-(s?0:_s(e,i)),y:i.top+t.scrollTop}}const Ds=new Set(["absolute","fixed"]);function Fs(e,t,s){let i;if("viewport"===t)i=function(e,t){const s=ss(e),i=is(e),n=s.visualViewport;let r=i.clientWidth,o=i.clientHeight,a=0,c=0;if(n){r=n.width,o=n.height;const e=ys();(!e||e&&"fixed"===t)&&(a=n.offsetLeft,c=n.offsetTop)}return{width:r,height:o,x:a,y:c}}(e,s);else if("document"===t)i=function(e){const t=is(e),s=Ss(e),i=e.ownerDocument.body,n=Mt(t.scrollWidth,t.clientWidth,i.scrollWidth,i.clientWidth),r=Mt(t.scrollHeight,t.clientHeight,i.scrollHeight,i.clientHeight);let o=-s.scrollLeft+_s(e);const a=-s.scrollTop;return"rtl"===Ts(i).direction&&(o+=Mt(t.clientWidth,i.clientWidth)-n),{width:n,height:r,x:o,y:a}}(is(e));else if(rs(t))i=function(e,t){const s=Ps(e,!0,"fixed"===t),i=s.top+e.clientTop,n=s.left+e.clientLeft,r=os(e)?ks(e):Lt(1);return{width:e.clientWidth*r.x,height:e.clientHeight*r.y,x:n*r.x,y:i*r.y}}(t,s);else{const s=Ls(e);i={x:t.x-s.x,y:t.y-s.y,width:t.width,height:t.height}}return Yt(i)}function Rs(e,t){const s=Cs(e);return!(s===t||!rs(s)||ws(s))&&("fixed"===Ts(s).position||Rs(s,t))}function Bs(e,t,s){const i=os(t),n=is(t),r="fixed"===s,o=Ps(e,!0,r,t);let a={scrollLeft:0,scrollTop:0};const c=Lt(0);function l(){c.x=_s(n)}if(i||!i&&!r)if(("body"!==ts(t)||ls(n))&&(a=Ss(t)),i){const e=Ps(t,!0,r,t);c.x=e.x+t.clientLeft,c.y=e.y+t.clientTop}else n&&l();r&&!i&&n&&l();const h=!n||i||r?Lt(0):Ns(n,a);return{x:o.left+a.scrollLeft-c.x-h.x,y:o.top+a.scrollTop-c.y-h.y,width:o.width,height:o.height}}function $s(e){return"static"===Ts(e).position}function js(e,t){if(!os(e)||"fixed"===Ts(e).position)return null;if(t)return t(e);let s=e.offsetParent;return is(e)===s&&(s=s.ownerDocument.body),s}function Vs(e,t){const s=ss(e);if(ps(e))return s;if(!os(e)){let t=Cs(e);for(;t&&!ws(t);){if(rs(t)&&!$s(t))return t;t=Cs(t)}return s}let i=js(e,t);for(;i&&us(i)&&$s(i);)i=js(i,t);return i&&ws(i)&&$s(i)&&!bs(i)?s:i||function(e){let t=Cs(e);for(;os(t)&&!ws(t);){if(bs(t))return t;if(ps(t))return null;t=Cs(t)}return null}(e)||s}const qs={convertOffsetParentRelativeRectToViewportRelativeRect:function(e){let{elements:t,rect:s,offsetParent:i,strategy:n}=e;const r="fixed"===n,o=is(i),a=!!t&&ps(t.floating);if(i===o||a&&r)return s;let c={scrollLeft:0,scrollTop:0},l=Lt(1);const h=Lt(0),u=os(i);if((u||!u&&!r)&&(("body"!==ts(i)||ls(o))&&(c=Ss(i)),os(i))){const e=Ps(i);l=ks(i),h.x=e.x+i.clientLeft,h.y=e.y+i.clientTop}const d=!o||u||r?Lt(0):Ns(o,c,!0);return{width:s.width*l.x,height:s.height*l.y,x:s.x*l.x-c.scrollLeft*l.x+h.x+d.x,y:s.y*l.y-c.scrollTop*l.y+h.y+d.y}},getDocumentElement:is,getClippingRect:function(e){let{element:t,boundary:s,rootBoundary:i,strategy:n}=e;const r=[..."clippingAncestors"===s?ps(t)?[]:function(e,t){const s=t.get(e);if(s)return s;let i=Es(e,[],!1).filter(e=>rs(e)&&"body"!==ts(e)),n=null;const r="fixed"===Ts(e).position;let o=r?Cs(e):e;for(;rs(o)&&!ws(o);){const t=Ts(o),s=bs(o);s||"fixed"!==t.position||(n=null),(r?!s&&!n:!s&&"static"===t.position&&n&&Ds.has(n.position)||ls(o)&&!s&&Rs(e,o))?i=i.filter(e=>e!==o):n=t,o=Cs(o)}return t.set(e,i),i}(t,this._c):[].concat(s),i],o=r[0],a=r.reduce((e,s)=>{const i=Fs(t,s,n);return e.top=Mt(i.top,e.top),e.right=xt(i.right,e.right),e.bottom=xt(i.bottom,e.bottom),e.left=Mt(i.left,e.left),e},Fs(t,o,n));return{width:a.right-a.left,height:a.bottom-a.top,x:a.left,y:a.top}},getOffsetParent:Vs,getElementRects:async function(e){const t=this.getOffsetParent||Vs,s=this.getDimensions,i=await s(e.floating);return{reference:Bs(e.reference,await t(e.floating),e.strategy),floating:{x:0,y:0,width:i.width,height:i.height}}},getClientRects:function(e){return Array.from(e.getClientRects())},getDimensions:function(e){const{width:t,height:s}=xs(e);return{width:t,height:s}},getScale:ks,isElement:rs,isRTL:function(e){return"rtl"===Ts(e).direction}};function Us(e,t){return e.x===t.x&&e.y===t.y&&e.width===t.width&&e.height===t.height}const zs=function(e){return void 0===e&&(e=0),{name:"offset",options:e,async fn(t){var s,i;const{x:n,y:r,placement:o,middlewareData:a}=t,c=await async function(e,t){const{placement:s,platform:i,elements:n}=e,r=await(null==i.isRTL?void 0:i.isRTL(n.floating)),o=Ft(s),a=Rt(s),c="y"===Vt(s),l=Qt.has(o)?-1:1,h=r&&c?-1:1,u=Dt(t,e);let{mainAxis:d,crossAxis:p,alignmentAxis:m}="number"==typeof u?{mainAxis:u,crossAxis:0,alignmentAxis:null}:{mainAxis:u.mainAxis||0,crossAxis:u.crossAxis||0,alignmentAxis:u.alignmentAxis};return a&&"number"==typeof m&&(p="end"===a?-1*m:m),c?{x:p*h,y:d*l}:{x:d*l,y:p*h}}(t,e);return o===(null==(s=a.offset)?void 0:s.placement)&&null!=(i=a.arrow)&&i.alignmentOffset?{}:{x:n+c.x,y:r+c.y,data:{...c,placement:o}}}}},Ws=function(e){return void 0===e&&(e={}),{name:"shift",options:e,async fn(t){const{x:s,y:i,placement:n}=t,{mainAxis:r=!0,crossAxis:o=!1,limiter:a={fn:e=>{let{x:t,y:s}=e;return{x:t,y:s}}},...c}=Dt(e,t),l={x:s,y:i},h=await Xt(t,c),u=Vt(Ft(n)),d=Bt(u);let p=l[d],m=l[u];if(r){const e="y"===d?"bottom":"right";p=Nt(p+h["y"===d?"top":"left"],p,p-h[e])}if(o){const e="y"===u?"bottom":"right";m=Nt(m+h["y"===u?"top":"left"],m,m-h[e])}const g=a.fn({...t,[d]:p,[u]:m});return{...g,data:{x:g.x-s,y:g.y-i,enabled:{[d]:r,[u]:o}}}}}},Ks=function(e){return void 0===e&&(e={}),{name:"flip",options:e,async fn(t){var s,i;const{placement:n,middlewareData:r,rects:o,initialPlacement:a,platform:c,elements:l}=t,{mainAxis:h=!0,crossAxis:u=!0,fallbackPlacements:d,fallbackStrategy:p="bestFit",fallbackAxisSideDirection:m="none",flipAlignment:g=!0,...f}=Dt(e,t);if(null!=(s=r.arrow)&&s.alignmentOffset)return{};const b=Ft(n),y=Vt(a),v=Ft(a)===a,w=await(null==c.isRTL?void 0:c.isRTL(l.floating)),T=d||(v||!g?[Jt(a)]:function(e){const t=Jt(e);return[zt(e),t,zt(t)]}(a)),S="none"!==m;!d&&S&&T.push(...function(e,t,s,i){const n=Rt(e);let r=function(e,t,s){switch(e){case"top":case"bottom":return s?t?Kt:Wt:t?Wt:Kt;case"left":case"right":return t?Ht:Gt;default:return[]}}(Ft(e),"start"===s,i);return n&&(r=r.map(e=>e+"-"+n),t&&(r=r.concat(r.map(zt)))),r}(a,g,m,w));const C=[a,...T],O=await Xt(t,f),E=[];let A=(null==(i=r.flip)?void 0:i.overflows)||[];if(h&&E.push(O[b]),u){const e=Ut(n,o,w);E.push(O[e[0]],O[e[1]])}if(A=[...A,{placement:n,overflows:E}],!E.every(e=>e<=0)){var x,M;const e=((null==(x=r.flip)?void 0:x.index)||0)+1,t=C[e];if(t&&("alignment"!==u||y===Vt(t)||A.every(e=>Vt(e.placement)!==y||e.overflows[0]>0)))return{data:{index:e,overflows:A},reset:{placement:t}};let s=null==(M=A.filter(e=>e.overflows[0]<=0).sort((e,t)=>e.overflows[1]-t.overflows[1])[0])?void 0:M.placement;if(!s)switch(p){case"bestFit":{var k;const e=null==(k=A.filter(e=>{if(S){const t=Vt(e.placement);return t===y||"y"===t}return!0}).map(e=>[e.placement,e.overflows.filter(e=>e>0).reduce((e,t)=>e+t,0)]).sort((e,t)=>e[1]-t[1])[0])?void 0:k[0];e&&(s=e);break}case"initialPlacement":s=a}if(n!==s)return{reset:{placement:s}}}return{}}}},Hs=e=>{Object.assign(e,{show(){this.cancelBehaviourOpen?.(),this.openValue=!0},hide(){this.openValue=!1},toggle(){this.cancelBehaviourOpen?.(),this.openValue=!this.openValue},setupFloatingUI({trigger:e,popover:t,strategy:s}){this.floatingUICleanup=function(e,t,s,i){void 0===i&&(i={});const{ancestorScroll:n=!0,ancestorResize:r=!0,elementResize:o="function"==typeof ResizeObserver,layoutShift:a="function"==typeof IntersectionObserver,animationFrame:c=!1}=i,l=Ms(e),h=n||r?[...l?Es(l):[],...Es(t)]:[];h.forEach(e=>{n&&e.addEventListener("scroll",s,{passive:!0}),r&&e.addEventListener("resize",s)});const u=l&&a?function(e,t){let s,i=null;const n=is(e);function r(){var e;clearTimeout(s),null==(e=i)||e.disconnect(),i=null}return function o(a,c){void 0===a&&(a=!1),void 0===c&&(c=1),r();const l=e.getBoundingClientRect(),{left:h,top:u,width:d,height:p}=l;if(a||t(),!d||!p)return;const m={rootMargin:-It(u)+"px "+-It(n.clientWidth-(h+d))+"px "+-It(n.clientHeight-(u+p))+"px "+-It(h)+"px",threshold:Mt(0,xt(1,c))||1};let g=!0;function f(t){const i=t[0].intersectionRatio;if(i!==c){if(!g)return o();i?o(!1,i):s=setTimeout(()=>{o(!1,1e-7)},1e3)}1!==i||Us(l,e.getBoundingClientRect())||o(),g=!1}try{i=new IntersectionObserver(f,{...m,root:n.ownerDocument})}catch(e){i=new IntersectionObserver(f,m)}i.observe(e)}(!0),r}(l,s):null;let d,p=-1,m=null;o&&(m=new ResizeObserver(e=>{let[i]=e;i&&i.target===l&&m&&(m.unobserve(t),cancelAnimationFrame(p),p=requestAnimationFrame(()=>{var e;null==(e=m)||e.observe(t)})),s()}),l&&!c&&m.observe(l),m.observe(t));let g=c?Ps(e):null;return c&&function t(){const i=Ps(e);g&&!Us(g,i)&&s(),g=i,d=requestAnimationFrame(t)}(),s(),()=>{var e;h.forEach(e=>{n&&e.removeEventListener("scroll",s),r&&e.removeEventListener("resize",s)}),null==u||u(),null==(e=m)||e.disconnect(),m=null,c&&cancelAnimationFrame(d)}}(e,t,()=>{((e,t,s)=>{const i=new Map,n={platform:qs,...s},r={...n.platform,_c:i};return(async(e,t,s)=>{const{placement:i="bottom",strategy:n="absolute",middleware:r=[],platform:o}=s,a=r.filter(Boolean),c=await(null==o.isRTL?void 0:o.isRTL(t));let l=await o.getElementRects({reference:e,floating:t,strategy:n}),{x:h,y:u}=Zt(l,i,c),d=i,p={},m=0;for(let s=0;s{const n={left:`${e}px`,top:`${s}px`,position:i};Object.assign(t.style,n)})})},openValueChanged(){this.disabledValue||(this.openValue?(this.preparePopoverOpenAnimation?.(),this.popoverTarget.showPopover(),this.popoverTarget.setAttribute("aria-expanded","true"),this.onPopoverOpened&&this.onPopoverOpened()):(this.popoverTarget.hidePopover(),this.popoverTarget.removeAttribute("aria-expanded"),this.onPopoverClosed&&this.onPopoverClosed()))}})},Gs=class extends i.xI{static targets=["button","popover"];static values={placement:{type:String,default:"bottom-end"},open:{type:Boolean,default:!1},autoPlacement:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},size:{type:Number,default:24},perLine:{type:Number,default:9}};initialize(){this.onEmojiSelect=this.onEmojiSelect.bind(this),this.pickerLoaded=!1,this.pickerLoadPromise=null,this.connected=!1,super.initialize()}connect(){this.connected=!0,Hs(this),this.setupFloatingUI({trigger:this.buttonTarget,popover:this.popoverTarget,strategy:"absolute"}),super.connect()}disconnect(){this.connected=!1,this.pickerLoadPromise=null,this.floatingUICleanup(),super.disconnect()}onEmojiSelect(e){this.dispatch("selected",{detail:e.native}),this.hide()}onClickOutside(e){this.openValue&&e.target.nodeType&&!1===this.element.contains(e.target)&&(this.openValue=!1)}async onPopoverOpened(){await this.loadPicker()}async loadPicker(){if(this.pickerLoaded)return;this.pickerLoadPromise||=this.loadPickerDependencies();const{Picker:e,i18n:t}=await this.pickerLoadPromise;this.connected&&!this.pickerLoaded&&(this.popoverTarget.appendChild(this.buildPicker(e,t)),this.pickerLoaded=!0)}async loadPickerDependencies(){const[e,t]=await Promise.all([s.e(160).then(s.bind(s,405)),this.loadI18n()]);return{Picker:e.Picker,i18n:t.default||t}}loadI18n(){return"es"===Hellotext.business.locale?s.e(437).then(s.t.bind(s,9,19)):s.e(200).then(s.t.bind(s,714,19))}buildPicker(e,t){return new e({onEmojiSelect:this.onEmojiSelect,theme:"light",dynamicWidth:!0,previewPosition:"none",skinTonePosition:"none",emojiSize:this.sizeValue,perLine:this.perLineValue,i18n:t})}get middlewares(){return[zs(5),Ws({padding:24}),(e={allowedPlacements:["top","bottom"]},void 0===e&&(e={}),{name:"autoPlacement",options:e,async fn(t){var s,i,n;const{rects:r,middlewareData:o,placement:a,platform:c,elements:l}=t,{crossAxis:h=!1,alignment:u,allowedPlacements:d=At,autoAlignment:p=!0,...m}=Dt(e,t),g=void 0!==u||d===At?function(e,t,s){return(e?[...s.filter(t=>Rt(t)===e),...s.filter(t=>Rt(t)!==e)]:s.filter(e=>Ft(e)===e)).filter(s=>!e||Rt(s)===e||!!t&&zt(s)!==s)}(u||null,p,d):d,f=await Xt(t,m),b=(null==(s=o.autoPlacement)?void 0:s.index)||0,y=g[b];if(null==y)return{};const v=Ut(y,r,await(null==c.isRTL?void 0:c.isRTL(l.floating)));if(a!==y)return{reset:{placement:g[0]}};const w=[f[Ft(y)],f[v[0]],f[v[1]]],T=[...(null==(i=o.autoPlacement)?void 0:i.overflows)||[],{placement:y,overflows:w}],S=g[b+1];if(S)return{data:{index:b+1,overflows:T},reset:{placement:S}};const C=T.map(e=>{const t=Rt(e.placement);return[e.placement,t&&h?e.overflows.slice(0,2).reduce((e,t)=>e+t,0):e.overflows[0],e.overflows]}).sort((e,t)=>e[1]-t[1]),O=(null==(n=C.filter(e=>e[2].slice(0,Rt(e[0])?2:3).every(e=>e<=0))[0])?void 0:n[0])||C[0][0];return O!==a?{data:{index:b+1,overflows:T},reset:{placement:O}}:{}}})];var e}};class Js{static get endpoint(){return f.endpoint("public/webchats/:id/messages")}constructor(e){this.webchatId=e}async index(e){const t=new URL(this.url);return Object.entries(e).forEach(([e,s])=>{t.searchParams.append(e,s)}),await fetch(t,{method:"GET",headers:vt.headers})}catchUp(e){return this.index({after_id:e,session:vt.session})}async create(e){const t=await fetch(this.url,{method:"POST",headers:{Authorization:`Bearer ${vt.business.id}`},body:e});return new v(t.ok,t)}markAsSeen(e=null){const t=e?this.url+`/${e}`:this.url+"/seen";fetch(t,{method:"PATCH",headers:vt.headers,body:JSON.stringify({session:vt.session})})}get url(){return Js.endpoint.replace(":id",this.webchatId)}}const Ys=Js;class Zs{static webSocket;static channels=new Set;static messageHandlers=new Set;static disconnectHandlers=new Set;static subscriptionConfirmHandlers=new Set;static reconnectTimeout=null;static reconnectAttempts=0;static reconnectBaseDelay=500;static reconnectMaxDelay=1e4;static reconnectJitter=.3;static needsResubscribe=!1;constructor(){Zs.channels.add(this)}send({command:e,identifier:t,data:s}){const i={command:e,identifier:JSON.stringify(t),data:JSON.stringify(s||{})},n=Zs.ensureWebSocket(),r=JSON.stringify(i);n.readyState===WebSocket.OPEN?n.send(r):n.addEventListener("open",()=>{n.send(r)})}onMessage(e){const t=t=>{const s=JSON.parse(t.data),{type:i,message:n}=s;this.ignoredEvents.includes(i)||e(n)};Zs.messageHandlers.add(t),Zs.ensureWebSocket().addEventListener("message",t)}onDisconnect(e){Zs.disconnectHandlers.add(e)}onSubscriptionConfirmed(e){Zs.subscriptionConfirmHandlers.add(e)}get webSocket(){return Zs.ensureWebSocket()}static ensureWebSocket(){return this.webSocket&&!this.closedWebSocket(this.webSocket)?this.webSocket:(this.webSocket&&(this.needsResubscribe=!0),this.openWebSocket())}static openWebSocket(){this.clearReconnectTimeout();const e=new WebSocket(f.actionCableUrl);return this.webSocket=e,this.installWebSocketHandlers(e),e}static installWebSocketHandlers(e){e.addEventListener("open",()=>this.handleOpen(e)),e.addEventListener("close",()=>this.handleDisconnect(e)),e.addEventListener("error",()=>this.handleDisconnect(e)),e.addEventListener("message",e=>this.handleControlMessage(e)),this.messageHandlers.forEach(t=>{e.addEventListener("message",t)})}static handleOpen(e){e===this.webSocket&&(this.reconnectAttempts=0,this.needsResubscribe&&(this.needsResubscribe=!1,this.resubscribeChannels()))}static handleControlMessage(e){let t;try{t=JSON.parse(e.data)}catch{return}"confirm_subscription"===t.type&&this.subscriptionConfirmHandlers.forEach(e=>e(t.identifier))}static handleDisconnect(e){e===this.webSocket&&(this.disconnectHandlers.forEach(e=>e()),this.webSocket=null,this.needsResubscribe=!0,this.scheduleReconnect())}static scheduleReconnect(){this.reconnectTimeout||(this.reconnectTimeout=setTimeout(()=>{this.reconnectTimeout=null,this.reconnectAttempts+=1,this.openWebSocket()},this.reconnectDelay))}static clearReconnectTimeout(){this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null)}static resubscribeChannels(){this.channels.forEach(e=>{const t=e.resubscribe||e.subscribe;"function"==typeof t&&t.call(e)})}static closedWebSocket(e){return e.readyState===WebSocket.CLOSED||e.readyState===WebSocket.CLOSING}static get reconnectDelay(){const e=Math.min(this.reconnectMaxDelay,this.reconnectBaseDelay*2**this.reconnectAttempts);return e+Math.round(e*this.reconnectJitter*Math.random())}get ignoredEvents(){return["ping","confirm_subscription","welcome"]}}const Xs=Zs,Qs=class extends Xs{constructor(e,t,s){super(),this.id=e,this.session=t,this.conversation=s,this.subscribed=!1,this.awaitingReconnectConfirmation=!1,this.reconnectCallbacks=new Set,this.onSubscriptionConfirmed(e=>this.handleSubscriptionConfirmed(e)),this.subscribe()}subscribe(){this.subscribed=!0;const e={channel:"WebchatChannel",id:this.id,session:this.session,conversation:this.conversation};this.send({command:"subscribe",identifier:e})}unsubscribe(){this.subscribed=!1;const e={channel:"WebchatChannel",id:this.id,session:this.session,conversation:this.conversation};this.send({command:"unsubscribe",identifier:e})}resubscribe(){!1!==this.subscribed&&(this.awaitingReconnectConfirmation=!0,this.subscribe())}onReconnect(e){this.reconnectCallbacks.add(e)}handleSubscriptionConfirmed(e){this.awaitingReconnectConfirmation&&this.matchesIdentifier(e)&&(this.awaitingReconnectConfirmation=!1,this.reconnectCallbacks.forEach(e=>e()))}matchesIdentifier(e){let t;try{t="string"==typeof e?JSON.parse(e):e}catch{return!1}return"WebchatChannel"===t.channel&&t.id===this.id&&t.session===this.session&&t.conversation===this.conversation}startTypingIndicator(){const e={channel:"WebchatChannel",id:this.id,session:this.session,conversation:this.conversation};this.send({command:"message",identifier:e,data:{action:"started_typing"}})}stopTypingIndicator(){const e={channel:"WebchatChannel",id:this.id,session:this.session,conversation:this.conversation};this.send({command:"typing:stop",identifier:e,data:{action:"stopped_typing"}})}onMessage(e){super.onMessage(t=>{"message"===t.type&&e(t)})}onReaction(e){super.onMessage(t=>{"reaction.create"!==t.type&&"reaction.destroy"!==t.type||e(t)})}onTypingStart(e){super.onMessage(t=>{"started_typing"===t.type&&e(t)})}updateSubscriptionWith(e){this.unsubscribe(),setTimeout(()=>{this.conversation=e,this.subscribe()},1e3)}},ei=e=>{Object.assign(e,{scheduleBehaviourOpen(){if(!this.shouldAutoOpenFromBehaviour())return;const e=1e3*Number(this.behaviourValue.delay_seconds||0);this.behaviourOpenTimeout=window.setTimeout(()=>{this.behaviourOpenTimeout=null,this.openValue||(this.openValue=!0,this.markBehaviourAutoOpened())},e)},cancelBehaviourOpen(){window.clearTimeout(this.behaviourOpenTimeout),this.behaviourOpenTimeout=null},shouldAutoOpenFromBehaviour(){const e=this.behaviourValue;return!(!e||"on_load"!==e.trigger||e.first_visit_only&&localStorage.getItem(this.firstVisitKey())||e.once_per_session&&sessionStorage.getItem(this.sessionKey()))},markBehaviourAutoOpened(){this.behaviourValue.first_visit_only&&localStorage.setItem(this.firstVisitKey(),"1"),this.behaviourValue.once_per_session&&sessionStorage.setItem(this.sessionKey(),"1")},firstVisitKey(){return`hellotext--webchat--${this.idValue}--auto-opened`},sessionKey(){return`hellotext--webchat--${this.idValue}--auto-opened-session`}})},ti=e=>{Object.assign(e,{setupOpeningSequence(){this.openingSequenceStarted=!1,this.openingSequenceCancelled=!1,this.openingSequenceTimeout=null,this.openingSequenceMessages=[],this.revealedOpeningSequenceMessageIds=[]},teardownOpeningSequence(){this.cancelOpeningSequence()},startOpeningSequence(){this.openingSequenceMessages=Array.from(this.openingSequenceMessageTargets||[]),this.openingSequenceCanStart()&&(this.openingSequenceStarted=!0,this.openingSequenceCancelled=!1,this.revealedOpeningSequenceMessageIds=[],this.playOpeningSequenceMessageAt(0))},openingSequenceCanStart(){return!this.conversationIdValue&&this.hasOpeningSequenceTarget&&this.openingSequenceMessages.length>0&&!this.openingSequenceStarted},playOpeningSequenceMessageAt(e){const t=this.openingSequenceMessages[e];if(!t)return;const s=1e3*this.openingSequenceMessageDelay(t);this.openingSequenceTimeout=window.setTimeout(()=>{this.openingSequenceTimeout=null,this.openingSequenceCancelled||(this.revealOpeningSequenceMessage(t),this.playOpeningSequenceMessageAt(e+1))},s)},revealOpeningSequenceMessage(e){this.messagesContainerTarget.insertBefore(e,this.messageTemplateTarget),e.hidden=!1,this.recordOpeningSequenceMessage(e),this.scrollOpeningSequenceToBottom()},recordOpeningSequenceMessage(e){const t=e.dataset.openingSequenceMessageId;t&&!this.revealedOpeningSequenceMessageIds.includes(t)&&this.revealedOpeningSequenceMessageIds.push(t)},openingSequenceMessageDelay(e){const t=Number(e.dataset.delaySeconds||0);return Number.isFinite(t)?t:0},scrollOpeningSequenceToBottom(){this.messagesContainerTarget.scroll&&this.messagesContainerTarget.scroll({top:this.messagesContainerTarget.scrollHeight,behavior:"smooth"})},cancelOpeningSequence(){this.openingSequenceCancelled=!0,null!==this.openingSequenceTimeout&&void 0!==this.openingSequenceTimeout&&(window.clearTimeout(this.openingSequenceTimeout),this.openingSequenceTimeout=null)},appendOpeningSequenceMessageIds(e){this.cancelOpeningSequence(),(this.revealedOpeningSequenceMessageIds||[]).forEach(t=>{e.append("message[opening_sequence_message_ids][]",t)})},clearRevealedOpeningSequenceMessageIds(){this.revealedOpeningSequenceMessageIds=[]}})},si=e=>{Object.assign(e,{setupTeaser(){this.teaserCycleTimeout=null,this.teaserMessages=[],this.boundOnTeaserClick=this.boundOnTeaserClick||this.onTeaserClick.bind(this),this.hasTeaserTarget&&(this.teaserTarget.addEventListener("click",this.boundOnTeaserClick),this.startTeaserPresentation())},teardownTeaser(){this.stopTeaserCycle(),this.hasTeaserTarget&&this.boundOnTeaserClick&&this.teaserTarget.removeEventListener("click",this.boundOnTeaserClick)},collectTeaserMessages(){return this.hasTeaserTarget?Array.from(this.teaserTarget.querySelectorAll("[data-teaser-message]")):[]},startTeaserPresentation(){this.stopTeaserCycle(),this.teaserMessages=this.collectTeaserMessages(),this.hasTeaserTarget&&(0!==this.teaserMessages.length?this.openValue||this.conversationIdValue||this.hasRenderedConversationMessages()?this.dismissTeaserForSession():this.teaserSeenForSession()?this.hideTeaser():(this.teaserTarget.classList.remove("invisible"),this.showTeaserMessage(0),this.teaserMessages.length<2||this.scheduleNextTeaserMessage(0)):this.hideTeaser())},scheduleNextTeaserMessage(e){const t=e+1;if(t>=this.teaserMessages.length)return;const s=this.teaserMessages[e],i=this.teaserPresentationDelay(s);this.teaserCycleTimeout=window.setTimeout(()=>{this.teaserCycleTimeout=null,this.showTeaserMessage(t),this.scheduleNextTeaserMessage(t)},i)},showTeaserMessage(e){this.teaserMessages.forEach((t,s)=>{t.classList.toggle("hidden",s!==e)})},stopTeaserCycle(){null!==this.teaserCycleTimeout&&void 0!==this.teaserCycleTimeout&&(window.clearTimeout(this.teaserCycleTimeout),this.teaserCycleTimeout=null)},teaserMessageDelay(e){const t=Number(e.dataset.delaySeconds||0);return Number.isFinite(t)?t:0},teaserPresentationDelay(e){return Math.max(1e3*this.teaserMessageDelay(e),250)},hasRenderedConversationMessages(){let e=[];try{e=Array.from(this.messageTargets||[])}catch(t){e=[]}return e.some(e=>e!==this.messageTemplateTarget)},teaserSeenKey(){const e=this.hasTeaserTarget?this.teaserTarget.dataset.teaserVersion:"",t=e?`:${e}`:"";return`hellotext:webchat:${this.idValue||this.element.id}:teaser-seen${t}`},teaserSeenForSession(){try{return"true"===window.sessionStorage.getItem(this.teaserSeenKey())}catch(e){return!1}},markTeaserSeenForSession(){try{window.sessionStorage.setItem(this.teaserSeenKey(),"true")}catch(e){}},dismissTeaserForSession(){this.markTeaserSeenForSession(),this.hideTeaser()},hideTeaser(){this.stopTeaserCycle(),this.hasTeaserTarget&&this.teaserTarget.classList.add("invisible")},onTeaserClick(e){e.target.closest("a")||(this.dismissTeaserForSession(),this.show())}})},ii={hour:"numeric",minute:"2-digit"},ni=/Android|iPhone|iPad|iPod/i,ri={capture:!0,passive:!0},oi=class extends i.xI{static messageTimestampFormatters={};static values={id:String,conversationId:String,media:Object,fileSizeErrorMessage:String,placement:{type:String,default:"bottom-end"},open:{type:Boolean,default:!1},autoPlacement:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},nextPage:{type:Number,default:void 0},fullScreenThreshold:{type:Number,default:1024},typingIndicatorKeepAlive:{type:Number,default:3e4},offset:{type:Number,default:24},padding:{type:Number,default:24},optimisticTypingIndicatorWait:{type:Number,default:1e3},teaser:Object,messageTeaser:String,behaviour:Object};static classes=["fadeOut"];static targets=["trigger","popover","input","attachmentInput","attachmentButton","errorMessageContainer","attachmentTemplate","attachmentContainer","attachment","messageTemplate","messagesContainer","title","attachmentImage","footer","toolbar","message","unreadCounter","typingIndicator","typingIndicatorTemplate","teaser","teaserMessage","inboundMessageTeaser","inboundMessageTeaserBody","openingSequence","openingSequenceMessage"];initialize(){this.messagesAPI=new Ys(this.idValue),this.webChatChannel=new Qs(this.idValue,vt.session,this.conversationIdValue),this.files=[],this.messageIds=new Set,this.catchUpAfterMessageId=null,this.fetchingCatchUpMessages=!1,this.onMessageReceived=this.onMessageReceived.bind(this),this.onMessageReaction=this.onMessageReaction.bind(this),this.onTypingStart=this.onTypingStart.bind(this),this.captureCatchUpCursor=this.captureCatchUpCursor.bind(this),this.catchUpMessages=this.catchUpMessages.bind(this),this.onScroll=this.onScroll.bind(this),this.onOutboundMessageSent=this.onOutboundMessageSent.bind(this),this.closePopoverOnEscape=this.closePopoverOnEscape.bind(this),this.broadcastChannel=new BroadcastChannel(`hellotext--webchat--${this.idValue}`),this.webChatChannel.onDisconnect(this.captureCatchUpCursor),this.webChatChannel.onReconnect(this.catchUpMessages),super.initialize()}connect(){ei(this),Hs(this),ti(this),si(this),this.setupFloatingUI({trigger:this.triggerTarget,popover:this.popoverTarget}),this.hasTeaserTarget&&this.setupFloatingUI({trigger:this.triggerTarget,popover:this.teaserTarget,strategy:"absolute"}),this.setupTeaser(),this.setupOpeningSequence(),this.localizeMessageTimestamps(),this.webChatChannel.onMessage(this.onMessageReceived),this.webChatChannel.onTypingStart(this.onTypingStart),this.webChatChannel.onReaction(this.onMessageReaction),this.setupMessagesContainerScrollIsolation(),this.messagesContainerTarget.addEventListener("scroll",this.onScroll),this.messagesContainerTarget.addEventListener("wheel",this.stopHostScrollPropagation,ri),this.messagesContainerTarget.addEventListener("touchmove",this.stopHostScrollPropagation,ri),this.shouldOpenOnMount&&(this.openValue=!0),vt.eventEmitter.dispatch("webchat:mounted"),this.broadcastChannel.addEventListener("message",this.onOutboundMessageSent),window.addEventListener("keydown",this.closePopoverOnEscape,!0),this.scheduleBehaviourOpen(),super.connect()}disconnect(){this.cancelBehaviourOpen(),this.clearPopoverOpenAnimation(),this.teardownTeaser(),this.teardownOpeningSequence(),this.broadcastChannel.removeEventListener("message",this.onOutboundMessageSent),this.messagesContainerTarget.removeEventListener("scroll",this.onScroll),this.messagesContainerTarget.removeEventListener("wheel",this.stopHostScrollPropagation,ri),this.messagesContainerTarget.removeEventListener("touchmove",this.stopHostScrollPropagation,ri),window.removeEventListener("keydown",this.closePopoverOnEscape,!0),this.clearTypingIndicator(),this.broadcastChannel.close(),this.floatingUICleanup(),super.disconnect()}setupMessagesContainerScrollIsolation(){this.messagesContainerTarget.style.overscrollBehavior="contain",this.messagesContainerTarget.style.webkitOverflowScrolling="touch",this.messagesContainerTarget.style.touchAction="pan-y",this.messagesContainerTarget.setAttribute("data-lenis-prevent",""),this.messagesContainerTarget.setAttribute("data-lenis-prevent-wheel",""),this.messagesContainerTarget.setAttribute("data-lenis-prevent-touch","")}stopHostScrollPropagation(e){e.stopPropagation()}onTypingStart(){if(this.typingIndicatorVisible)return this.resetTypingIndicatorTimer();this.showTypingIndicator()}showOptimisticTypingIndicator(){this.typingIndicatorVisible||this.showTypingIndicator()}showTypingIndicator(){this.clearTypingIndicator(),this.typingIndicatorVisible=!0;const e=this.typingIndicatorTemplateTarget.cloneNode(!0);e.setAttribute("data-hellotext--webchat-target","typingIndicator"),e.style.display="flex",this.messagesContainerTarget.appendChild(e),requestAnimationFrame(()=>{this.messagesContainerTarget.scroll({top:this.messagesContainerTarget.scrollHeight,behavior:"instant"})});const t=this.typingIndicatorKeepAliveValue;this.incomingTypingIndicatorTimeout=setTimeout(()=>{this.clearTypingIndicator()},t)}resetTypingIndicatorTimer(){if(!this.typingIndicatorVisible)return;clearTimeout(this.incomingTypingIndicatorTimeout),clearTimeout(this.optimisticTypingTimeout);const e=this.typingIndicatorKeepAliveValue;this.incomingTypingIndicatorTimeout=setTimeout(()=>{this.clearTypingIndicator()},e)}clearTypingIndicator(){this.hasTypingIndicatorTarget&&this.typingIndicatorTarget.remove(),this.typingIndicatorVisible=!1,clearTimeout(this.incomingTypingIndicatorTimeout),clearTimeout(this.optimisticTypingTimeout)}onMessageInputChange(){this.resizeInput(),clearTimeout(this.typingIndicatorTimeout),this.hasSentTypingIndicator||(this.webChatChannel.startTypingIndicator(),this.hasSentTypingIndicator=!0),this.typingIndicatorTimeout=setTimeout(()=>{this.hasSentTypingIndicator=!1},3e3)}onOutboundMessageSent(e){const{data:t}=e,s={"message:sent":e=>{const t=(new DOMParser).parseFromString(e.element,"text/html").body.firstElementChild;this.localizeMessageTimestamps(t),this.typingIndicatorVisible&&this.hasTypingIndicatorTarget?this.messagesContainerTarget.insertBefore(t,this.typingIndicatorTarget):this.messagesContainerTarget.appendChild(t),t.scrollIntoView({behavior:"instant"})},"message:failed":e=>{const t=this.messagesContainerTarget.querySelector(`#${e.id}`);this.markMessageFailed(t,e.reason)}};s[t.type]?s[t.type](t):console.log(`Unhandled message event: ${t.type}`)}async onScroll(){if(this.messagesContainerTarget.scrollTop>300||!this.nextPageValue||this.fetchingNextPage)return;this.fetchingNextPage=!0;const e=await this.messagesAPI.index({page:this.nextPageValue,session:vt.session}),{next:t,messages:s}=await e.json();this.nextPageValue=t,this.oldScrollHeight=this.messagesContainerTarget.scrollHeight,s.forEach(e=>{const{body:t,attachments:s}=e,i=e.created_at||e.createdAt,n=this.messageTemplateTarget.cloneNode(!0);n.classList.add("hellotext--webchat-message"),n.setAttribute("data-hellotext--webchat-target","message"),n.setAttribute("data-id",e.id),i&&n.setAttribute("data-created-at",i),n.style.removeProperty("display"),rt(n.querySelector("[data-body]"),t),"received"===e.state?n.classList.add("received"):n.classList.remove("received"),s&&s.forEach(e=>{const t=this.attachmentImageTarget.cloneNode(!0);t.removeAttribute("data-hellotext--webchat-target"),t.src=e,t.style.display="block",this.messageAttachmentsContainer(n)?.appendChild(t)}),n.setAttribute("data-body",t),this.localizeMessageTimestamp(n.querySelector("[data-message-timestamp]"),i),this.messagesContainerTarget.prepend(n)}),this.messagesContainerTarget.scroll({top:this.messagesContainerTarget.scrollHeight-this.oldScrollHeight,behavior:"instant"}),this.fetchingNextPage=!1}onClickOutside(e){d.mode===u.POPOVER&&this.openValue&&e.target.nodeType&&!1===this.element.contains(e.target)&&(this.openValue=!1)}closePopover(){this.clearPopoverOpenAnimation(),this.popoverTarget.classList.remove(...this.fadeOutClasses),this.openValue=!1}preparePopoverOpenAnimation(){this.clearPopoverOpenAnimation(),this.popoverTarget.classList.remove(...this.fadeOutClasses),this.popoverTarget.classList.add("hellotext--webchat-popover-opening"),this.popoverOpenAnimationTimeout=setTimeout(()=>{this.popoverTarget.classList.remove("hellotext--webchat-popover-opening"),this.popoverOpenAnimationTimeout=null},120)}clearPopoverOpenAnimation(){this.popoverOpenAnimationTimeout&&(clearTimeout(this.popoverOpenAnimationTimeout),this.popoverOpenAnimationTimeout=null),this.popoverTarget?.classList.remove("hellotext--webchat-popover-opening")}onPopoverOpened(){this.popoverTarget.classList.remove(...this.fadeOutClasses),this.dismissTeaserForSession?.(),this.onMobile||this.focusComposeInput(),this.scrolled||(requestAnimationFrame(()=>{this.messagesContainerTarget.scroll({top:this.messagesContainerTarget.scrollHeight,behavior:"instant"})}),this.scrolled=!0),vt.eventEmitter.dispatch("webchat:opened"),localStorage.setItem(`hellotext--webchat--${this.idValue}`,"opened"),this.messageTeaserValue&&(this.messageTeaserValue=null),this.startOpeningSequence(),"none"!==this.unreadCounterTarget.style.display&&(this.unreadCounterTarget.style.display="none",this.unreadCounterTarget.innerText="0",this.messagesAPI.markAsSeen())}onPopoverClosed(){this.clearPopoverOpenAnimation(),vt.eventEmitter.dispatch("webchat:closed"),localStorage.setItem(`hellotext--webchat--${this.idValue}`,"closed")}onMessageReaction(e){const{message:t,reaction:s,type:i}=e,n=this.messageTargets.find(e=>e.dataset.id===t).querySelector("[data-reactions]");if("reaction.destroy"===i)return n.querySelector(`[data-id="${s.id}"]`).remove();if(n.querySelector(`[data-id="${s.id}"]`))n.querySelector(`[data-id="${s.id}"]`).innerText=s.emoji;else{const e=document.createElement("span");e.innerText=s.emoji,e.setAttribute("data-id",s.id),n.appendChild(e)}}onMessageReceived(e,t={}){const{id:s,body:i,attachments:n,teaser:r}=e,o=e.created_at||e.createdAt;if(!this.claimMessageId(s))return;if(this.hideTeaser?.(),e.carousel)return this.insertCarouselMessage(e,t);const a=this.messageTemplateTarget.cloneNode(!0);a.classList.add("hellotext--webchat-message"),a.style.display="flex",rt(a.querySelector("[data-body]"),i),a.setAttribute("data-id",s),a.setAttribute("data-hellotext--webchat-target","message"),this.setMessageCreatedAt(a,o),this.localizeMessageTimestamp(a.querySelector("[data-message-timestamp]"),o),n&&n.forEach(e=>{const t=this.attachmentImageTarget.cloneNode(!0);t.src=e,t.style.display="block",this.messageAttachmentsContainer(a)?.appendChild(t)}),this.clearTypingIndicator(),this.insertMessageElement(a),vt.eventEmitter.dispatch("webchat:message:received",{...e,body:a.querySelector("[data-body]").innerText}),!1!==t.scroll&&a.scrollIntoView({behavior:"smooth"}),this.updateMessageTeaser(r),this.openValue?this.messagesAPI.markAsSeen(s):this.incrementUnreadCounter()}claimMessageId(e){const t=this.messageTargets||[];return!this.messageIds.has(e)&&(this.messageIds.add(e),!t.some(t=>t.dataset.id===e))}captureCatchUpCursor(){this.catchUpAfterMessageId=this.lastRenderedMessageId}async catchUpMessages(){const e=this.catchUpAfterMessageId;if(e&&!this.fetchingCatchUpMessages){this.fetchingCatchUpMessages=!0;try{const t=await this.messagesAPI.catchUp(e),{messages:s=[]}=await t.json();s.forEach(e=>this.onMessageReceived(e,{scroll:!1})),this.catchUpAfterMessageId=this.lastRenderedMessageId}finally{this.fetchingCatchUpMessages=!1}}}get lastRenderedMessageId(){const e=this.persistedMessageElements;return e[e.length-1]?.dataset.id||null}get persistedMessageElements(){return Array.from(this.messagesContainerTarget.querySelectorAll(".hellotext--webchat-message[data-id]"))}setMessageCreatedAt(e,t){t&&e.setAttribute("data-created-at",t)}insertMessageElement(e){const t=this.nextMessageElementFor(e);t?this.messagesContainerTarget.insertBefore(e,t):this.messagesContainerTarget.appendChild(e)}nextMessageElementFor(e){const t=Date.parse(e.dataset.createdAt);return Number.isNaN(t)?null:this.persistedMessageElements.find(s=>{if(s===e)return!1;const i=Date.parse(s.dataset.createdAt);return!Number.isNaN(i)&&i>t})}updateMessageTeaser(e){this.messageTeaserValue=e,this.messageTeaserValue&&this.hasTeaserTarget&&this.hasInboundMessageTeaserTarget&&this.hasInboundMessageTeaserBodyTarget&&(this.teaserMessageTargets.forEach(e=>e.classList.add("hidden")),this.inboundMessageTeaserBodyTarget.textContent=this.messageTeaserValue,this.inboundMessageTeaserTarget.classList.remove("hidden"),this.teaserTarget.classList.toggle("invisible",this.openValue))}insertCarouselMessage(e,t={}){const s=e.html,i=e.created_at||e.createdAt,n=function(e){return nt(e,it)}(s).firstElementChild;n.classList.add("hellotext--webchat-message"),n.setAttribute("data-id",e.id),n.setAttribute("data-hellotext--webchat-target","message"),this.setMessageCreatedAt(n,i),this.localizeMessageTimestamps(n),this.clearTypingIndicator(),this.insertMessageElement(n),!1!==t.scroll&&n.scrollIntoView({behavior:"smooth"}),vt.eventEmitter.dispatch("webchat:message:received",{...e,body:n.querySelector("[data-body]")?.innerText||""}),this.updateMessageTeaser(e.teaser),this.openValue?this.messagesAPI.markAsSeen(e.id):this.incrementUnreadCounter()}resizeInput(){this.inputTarget.style.height="auto";const e=this.inputTarget.scrollHeight;this.inputTarget.style.height=`${Math.min(e,96)}px`}async sendQuickReplyMessage({detail:{id:e,product:t,buttonId:s,body:i,cardElement:n}}){this.dismissTeaserForSession?.();const r=new FormData;r.append("message[body]",i),e&&r.append("message[replied_to]",e),t&&r.append("message[product]",t),s&&r.append("message[button]",s),r.append("session",vt.session),r.append("locale",a.toString()),this.appendOpeningSequenceMessageIds(r);const o=this.buildMessageElement(),c=n?.querySelector("img")?.cloneNode(!0);o.querySelector("[data-body]").innerText=i,c&&(c.removeAttribute("width"),c.removeAttribute("height"),this.messageAttachmentsContainer(o)?.appendChild(c)),this.typingIndicatorVisible&&this.hasTypingIndicatorTarget?this.messagesContainerTarget.insertBefore(o,this.typingIndicatorTarget):this.messagesContainerTarget.appendChild(o),o.scrollIntoView({behavior:"smooth"}),this.broadcastChannel.postMessage({type:"message:sent",element:o.outerHTML});const l=await this.messagesAPI.create(r);if(l.failed)return clearTimeout(this.optimisticTypingTimeout),this.markMessageFailedFromResponse(l,o);const h=await l.json();this.dispatch("set:id",{target:o,detail:h.id}),this.localizeMessageTimestamp(o.querySelector("[data-message-timestamp]"),h.created_at||h.createdAt),this.clearRevealedOpeningSequenceMessageIds();const u={id:h.id,body:i,attachments:c?[c.src]:[],replied_to:e,product:t,button:s,type:"quick_reply"};vt.eventEmitter.dispatch("webchat:message:sent",u)}async sendTeaserQuickReply(e){e.preventDefault(),e.stopPropagation();const t=e.currentTarget,s=(t.dataset.value||"").trim(),i=[t.dataset.text,t.textContent].map(e=>(e||"").trim()).find(e=>e.length>0),n=s||i;if(!n)return;this.dismissTeaserForSession?.(),this.show();const r=(t.dataset.type||"").trim()||"quick_reply",o=new FormData;o.append("message[body]",n),o.append("session",vt.session),o.append("locale",a.toString()),this.appendOpeningSequenceMessageIds(o);const c=this.buildMessageElement();c.querySelector("[data-body]").innerText=n,this.typingIndicatorVisible&&this.hasTypingIndicatorTarget?this.messagesContainerTarget.insertBefore(c,this.typingIndicatorTarget):this.messagesContainerTarget.appendChild(c),c.scrollIntoView({behavior:"smooth"}),this.broadcastChannel.postMessage({type:"message:sent",element:c.outerHTML}),this.typingIndicatorVisible||(clearTimeout(this.optimisticTypingTimeout),this.optimisticTypingTimeout=setTimeout(()=>{this.showOptimisticTypingIndicator()},this.optimisticTypingIndicatorWaitValue));const l=await this.messagesAPI.create(o);if(l.failed)return clearTimeout(this.optimisticTypingTimeout),this.markMessageFailedFromResponse(l,c);const h=await l.json();c.setAttribute("data-id",h.id),this.localizeMessageTimestamp(c.querySelector("[data-message-timestamp]"),h.created_at||h.createdAt),this.clearRevealedOpeningSequenceMessageIds(),vt.eventEmitter.dispatch("webchat:message:sent",{id:h.id,body:n,attachments:[],type:"quick_reply",teaser:{text:i||n,value:s||n,type:r}}),h.conversation&&h.conversation!==this.conversationIdValue&&(this.conversationIdValue=h.conversation,this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)),this.typingIndicatorVisible&&this.resetTypingIndicatorTimer()}async sendMessage(e){const t={body:this.inputTarget.value,attachments:this.files};if(0===this.inputTarget.value.trim().length&&0===this.files.length)return void(e&&e.target&&e.preventDefault());this.dismissTeaserForSession?.();const s=new FormData;this.inputTarget.value.trim().length>0?s.append("message[body]",this.inputTarget.value):delete t.body,this.files.forEach(e=>{s.append("message[attachments][]",e)}),s.append("session",vt.session),s.append("locale",a.toString()),this.appendOpeningSequenceMessageIds(s);const i=this.buildMessageElement();this.inputTarget.value.trim().length>0?i.querySelector("[data-body]").innerText=this.inputTarget.value:i.querySelector("[data-message-bubble]").remove();const n=this.attachmentContainerTarget.querySelectorAll("img");n.length>0&&n.forEach(e=>{this.messageAttachmentsContainer(i)?.appendChild(e.cloneNode(!0))}),this.typingIndicatorVisible&&this.hasTypingIndicatorTarget?this.messagesContainerTarget.insertBefore(i,this.typingIndicatorTarget):this.messagesContainerTarget.appendChild(i),i.scrollIntoView({behavior:"smooth"}),this.broadcastChannel.postMessage({type:"message:sent",element:i.outerHTML}),this.inputTarget.value="",this.resizeInput(),this.files=[],this.attachmentInputTarget.value="",this.attachmentContainerTarget.innerHTML="",this.attachmentContainerTarget.style.display="none",this.errorMessageContainerTarget.style.display="none",this.focusComposeInput(),this.typingIndicatorVisible||(clearTimeout(this.optimisticTypingTimeout),this.optimisticTypingTimeout=setTimeout(()=>{this.showOptimisticTypingIndicator()},this.optimisticTypingIndicatorWaitValue));const r=await this.messagesAPI.create(s);if(r.failed)return clearTimeout(this.optimisticTypingTimeout),this.markMessageFailedFromResponse(r,i);const o=await r.json();i.setAttribute("data-id",o.id),t.id=o.id,this.localizeMessageTimestamp(i.querySelector("[data-message-timestamp]"),o.created_at||o.createdAt),this.clearRevealedOpeningSequenceMessageIds(),vt.eventEmitter.dispatch("webchat:message:sent",t),o.conversation!==this.conversationIdValue&&(this.conversationIdValue=o.conversation,this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)),this.typingIndicatorVisible&&this.resetTypingIndicatorTimer(),this.attachmentContainerTarget.style.display=""}buildMessageElement(){const e=this.messageTemplateTarget.cloneNode(!0);return e.id=`hellotext--webchat--${this.idValue}--message--${Date.now()}`,e.classList.add("received"),e.style.removeProperty("display"),e.setAttribute("data-controller","hellotext--message"),e.setAttribute("data-hellotext--webchat-target","message"),this.localizeMessageTimestamp(e.querySelector("[data-message-timestamp]"),new Date),e}focusCompose(e){const{target:t}=e,s=["button","a","input","textarea","select","label",'[role="button"]',"em-emoji-picker",'[data-hellotext--webchat--emoji-target~="popover"]','[data-controller~="hellotext--webchat--emoji"]'].join(", ");this.hasInputTarget&&!t.closest(s)&&this.focusComposeInput({moveCursorToEnd:!0})&&e.preventDefault()}closePopoverFromHeader(e){const{target:t}=e;t.closest(".hellotext--webchat-header-channel-button, .hellotext--webchat-close-button")||(e.preventDefault(),this.closePopover())}closePopoverOnEscape(e){"Escape"===e.key&&this.openValue&&(e.preventDefault(),e.stopPropagation(),this.closePopover(),this.triggerTarget?.focus?.())}async markMessageFailedFromResponse(e,t){const s=await this.messageFailureReason(e);this.markMessageFailed(t,s),this.broadcastChannel.postMessage({type:"message:failed",id:t.id,reason:s})}markMessageFailed(e,t){if(!e)return;if(e.classList.add("failed"),!t)return;const s=e.querySelector("[data-message-timestamp]");s&&(s.textContent=t)}localizeMessageTimestamps(e=this.element){e&&(e.matches?.("time[datetime][data-message-timestamp]")?[e]:Array.from(e.querySelectorAll?.("time[datetime][data-message-timestamp]")||[])).forEach(e=>this.localizeMessageTimestamp(e))}localizeMessageTimestamp(e,t=e?.getAttribute("datetime")){if(!e||!t)return;const s=t instanceof Date?t:new Date(t);Number.isNaN(s.getTime())||(e.setAttribute("datetime",s.toISOString()),e.textContent=this.formatMessageTimestamp(s))}formatMessageTimestamp(e){return this.constructor.messageTimestampFormatterFor(a.toString()).format(e)}static messageTimestampFormatterFor(e){const t=e||"default";return this.messageTimestampFormatters[t]||(this.messageTimestampFormatters[t]=this.buildMessageTimestampFormatter(e)),this.messageTimestampFormatters[t]}static buildMessageTimestampFormatter(e){try{return new Intl.DateTimeFormat(e||void 0,ii)}catch(e){return new Intl.DateTimeFormat(void 0,ii)}}async messageFailureReason(e){const t=e?.data||e?.response,s=t?.statusText||"Message failed";try{const e=t?.clone?t.clone():t,s=await(e?.json?.()),i=this.messageFailureReasonFromPayload(s);if(i)return i}catch(e){}try{const e=t?.clone?t.clone():t,i=await(e?.text?.());return this.messageFailureReasonFromText(i)||s}catch(e){return s}}messageFailureReasonFromText(e){if("string"!=typeof e)return null;const t=e.trim();if(!t||t.startsWith("<"))return null;try{return this.messageFailureReasonFromPayload(JSON.parse(t))||t}catch(e){return t}}messageFailureReasonFromPayload(e){return e?[e.error?.message,e.message,e.errors?.message,e.errors?.[0]?.message,e.errors?.[0]?.description].find(e=>"string"==typeof e&&e.trim().length>0):null}messageAttachmentsContainer(e){return e.querySelector("[data-attachments-container], [data-attachment-container]")}incrementUnreadCounter(){this.unreadCounterTarget.style.display="flex";const e=(parseInt(this.unreadCounterTarget.innerText)||0)+1;this.unreadCounterTarget.innerText=Math.min(e,9)}openAttachment(){this.attachmentInputTarget.click()}onFileInputChange(){this.errorMessageContainerTarget.style.display="none";const e=Array.from(this.attachmentInputTarget.files);this.attachmentInputTarget.value="";const t=e.find(e=>{const t=e.type.split("/")[0];return["image","video","audio"].includes(t)?this.mediaValue[t].max_sizethis.createAttachmentElement(e)),this.focusComposeInput()}createAttachmentElement(e){const t=this.attachmentElement();if(this.attachmentContainerTarget.style.display="",t.setAttribute("data-name",e.name),e.type.startsWith("image/")){const s=this.attachmentImageTarget.cloneNode(!0);s.src=URL.createObjectURL(e),s.style.display="block",t.appendChild(s),this.attachmentContainerTarget.appendChild(t),this.attachmentContainerTarget.style.display="flex"}else{const s=t.querySelector("main");s.style.height="5rem",s.style.borderRadius="0.375rem",s.style.backgroundColor="#e5e7eb",s.style.padding="0.25rem",t.querySelector("p[data-attachment-name]").innerText=e.name,this.attachmentContainerTarget.appendChild(t),this.attachmentContainerTarget.style.display="flex"}}removeAttachment({currentTarget:e}){const t=e.closest("[data-hellotext--webchat-target='attachment']");this.files=this.files.filter(e=>e.name!==t.dataset.name),this.attachmentInputTarget.value="",t.remove(),this.focusComposeInput()}attachmentTargetDisconnected(){0===this.attachmentTargets.length&&(this.attachmentContainerTarget.innerHTML="",this.attachmentContainerTarget.style.display="none")}attachmentElement(){const e=this.attachmentTemplateTarget.cloneNode(!0);return e.removeAttribute("hidden"),e.style.display="flex",e.setAttribute("data-hellotext--webchat-target","attachment"),e}onEmojiSelect({detail:e}){const t=this.inputTarget.value,s=this.inputTarget.selectionStart,i=this.inputTarget.selectionEnd;this.inputTarget.value=t.slice(0,s)+e+t.slice(i),this.inputTarget.selectionStart=this.inputTarget.selectionEnd=s+e.length,this.focusComposeInput()}focusComposeInput({moveCursorToEnd:e=!1}={}){if(!this.shouldAutofocusCompose)return!1;if(!1===this.hasInputTarget)return!1;if(void 0===this.hasInputTarget&&!this.inputTarget)return!1;if(this.inputTarget.focus(),e&&"number"==typeof this.inputTarget.selectionStart){const e=this.inputTarget.value.length;this.inputTarget.setSelectionRange(e,e)}return!0}byteToMegabyte(e){return Math.ceil(e/1024/1024)}get middlewares(){return[zs(this.offsetValue),Ws({padding:this.paddingValue}),Ks()]}get shouldOpenOnMount(){return"opened"===localStorage.getItem(`hellotext--webchat--${this.idValue}`)&&!this.onMobile}get shouldAutofocusCompose(){return!this.usesVirtualKeyboard}get usesVirtualKeyboard(){if("undefined"==typeof navigator)return!1;const e=navigator.userAgent||"",t="MacIntel"===navigator.platform&&navigator.maxTouchPoints>1,s=ni.test(e),i=!0===navigator.userAgentData?.mobile;return s||t||i||this.hasTouchOnlyPointer}get hasTouchOnlyPointer(){return"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(pointer: coarse)").matches&&window.matchMedia("(hover: none)").matches}get onMobile(){return"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia(`(max-width: ${this.fullScreenThresholdValue}px)`).matches}},ai=i.lg.start();ai.register("hellotext--alert",Tt),ai.register("hellotext--form",St),ai.register("hellotext--popup",Ot),ai.register("hellotext--webchat",oi),ai.register("hellotext--webchat--emoji",Gs),ai.register("hellotext--message",Ct),window.Hellotext=vt;const ci=vt}};const t={};function s(i){const n=t[i];if(void 0!==n)return n.exports;const r=t[i]={exports:{}};return e[i](r,r.exports,s),r.exports}s.m=e,(()=>{const e=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;let t;s.t=function(i,n){if(1&n&&(i=this(i)),8&n)return i;if("object"==typeof i&&i){if(4&n&&i.__esModule)return i;if(16&n&&"function"==typeof i.then)return i}const r=Object.create(null);s.r(r);const o={};t=t||[null,e({}),e([]),e(e)];for(var a=2&n&&i;("object"==typeof a||"function"==typeof a)&&!~t.indexOf(a);a=e(a))Object.getOwnPropertyNames(a).forEach(e=>o[e]=()=>i[e]);return o.default=()=>i,s.d(r,o),r}})(),s.d=(e,t)=>{if(Array.isArray(t))for(var i=0;iPromise.all(Object.keys(s.f).reduce((t,i)=>(s.f[i](e,t),t),[])),s.u=e=>({160:"webchat-emoji",200:"webchat-emoji-en",437:"webchat-emoji-es"}[e]+".js"),s.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),s.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const e={},t="Hellotext:";s.l=(i,n,r,o)=>{if(e[i])return void e[i].push(n);let a,c;if(void 0!==r){const e=document.getElementsByTagName("script");for(var l=0;l{a.onerror=a.onload=null,clearTimeout(u);const n=e[i];if(delete e[i],a.parentNode?.removeChild(a),n?.forEach(e=>e(s)),t)return t(s)},u=setTimeout(h.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=h.bind(null,a.onerror),a.onload=h.bind(null,a.onload),c&&document.head.appendChild(a)}})(),s.r=e=>{Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{let e;s.g.importScripts&&(e=s.g.location+"");const t=s.g.document;if(!e&&t&&("SCRIPT"===t.currentScript?.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){const s=t.getElementsByTagName("script");if(s.length){let t=s.length-1;for(;t>-1&&(!e||!/^http(s?):/.test(e));)e=s[t--].src}}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),s.p=e})(),(()=>{const e={792:0};s.f.j=(t,i)=>{let n=s.o(e,t)?e[t]:void 0;if(0!==n)if(n)i.push(n[2]);else{const r=new Promise((s,i)=>n=e[t]=[s,i]);i.push(n[2]=r);const o=s.p+s.u(t),a=new Error,c=i=>{if(s.o(e,t)&&(n=e[t],0!==n&&(e[t]=void 0),n)){const e=i&&("load"===i.type?"missing":i.type),s=i&&i.target&&i.target.src;a.message="Loading chunk "+t+" failed.\n("+e+": "+s+")",a.name="ChunkLoadError",a.type=e,a.request=s,n[1](a)}};s.l(o,c,"chunk-"+t,t)}};const t=(t,i)=>{let[n,r,o]=i;var a,c,l=0;if(n.some(t=>0!==e[t])){for(a in r)s.o(r,a)&&(s.m[a]=r[a]);o&&o(s)}for(t&&t(i);l(()=>{"use strict";var e={891(e,t,s){s.d(t,{lg:()=>G,xI:()=>ne});class i{constructor(e,t,s){this.eventTarget=e,this.eventName=t,this.eventOptions=s,this.unorderedBindings=new Set}connect(){this.eventTarget.addEventListener(this.eventName,this,this.eventOptions)}disconnect(){this.eventTarget.removeEventListener(this.eventName,this,this.eventOptions)}bindingConnected(e){this.unorderedBindings.add(e)}bindingDisconnected(e){this.unorderedBindings.delete(e)}handleEvent(e){const t=function(e){if("immediatePropagationStopped"in e)return e;{const{stopImmediatePropagation:t}=e;return Object.assign(e,{immediatePropagationStopped:!1,stopImmediatePropagation(){this.immediatePropagationStopped=!0,t.call(this)}})}}(e);for(const e of this.bindings){if(t.immediatePropagationStopped)break;e.handleEvent(t)}}hasBindings(){return this.unorderedBindings.size>0}get bindings(){return Array.from(this.unorderedBindings).sort((e,t)=>{const s=e.index,i=t.index;return si?1:0})}}class n{constructor(e){this.application=e,this.eventListenerMaps=new Map,this.started=!1}start(){this.started||(this.started=!0,this.eventListeners.forEach(e=>e.connect()))}stop(){this.started&&(this.started=!1,this.eventListeners.forEach(e=>e.disconnect()))}get eventListeners(){return Array.from(this.eventListenerMaps.values()).reduce((e,t)=>e.concat(Array.from(t.values())),[])}bindingConnected(e){this.fetchEventListenerForBinding(e).bindingConnected(e)}bindingDisconnected(e,t=!1){this.fetchEventListenerForBinding(e).bindingDisconnected(e),t&&this.clearEventListenersForBinding(e)}handleError(e,t,s={}){this.application.handleError(e,`Error ${t}`,s)}clearEventListenersForBinding(e){const t=this.fetchEventListenerForBinding(e);t.hasBindings()||(t.disconnect(),this.removeMappedEventListenerFor(e))}removeMappedEventListenerFor(e){const{eventTarget:t,eventName:s,eventOptions:i}=e,n=this.fetchEventListenerMapForEventTarget(t),r=this.cacheKey(s,i);n.delete(r),0==n.size&&this.eventListenerMaps.delete(t)}fetchEventListenerForBinding(e){const{eventTarget:t,eventName:s,eventOptions:i}=e;return this.fetchEventListener(t,s,i)}fetchEventListener(e,t,s){const i=this.fetchEventListenerMapForEventTarget(e),n=this.cacheKey(t,s);let r=i.get(n);return r||(r=this.createEventListener(e,t,s),i.set(n,r)),r}createEventListener(e,t,s){const n=new i(e,t,s);return this.started&&n.connect(),n}fetchEventListenerMapForEventTarget(e){let t=this.eventListenerMaps.get(e);return t||(t=new Map,this.eventListenerMaps.set(e,t)),t}cacheKey(e,t){const s=[e];return Object.keys(t).sort().forEach(e=>{s.push(`${t[e]?"":"!"}${e}`)}),s.join(":")}}const r={stop:({event:e,value:t})=>(t&&e.stopPropagation(),!0),prevent:({event:e,value:t})=>(t&&e.preventDefault(),!0),self:({event:e,value:t,element:s})=>!t||s===e.target},a=/^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;function o(e){return e.replace(/(?:[_-])([a-z0-9])/g,(e,t)=>t.toUpperCase())}function c(e){return o(e.replace(/--/g,"-").replace(/__/g,"_"))}function l(e){return e.charAt(0).toUpperCase()+e.slice(1)}function h(e){return e.replace(/([A-Z])/g,(e,t)=>`-${t.toLowerCase()}`)}function u(e){return null!=e}function d(e,t){return Object.prototype.hasOwnProperty.call(e,t)}const p=["meta","ctrl","alt","shift"];class m{constructor(e,t,s,i){this.element=e,this.index=t,this.eventTarget=s.eventTarget||e,this.eventName=s.eventName||function(e){const t=e.tagName.toLowerCase();if(t in g)return g[t](e)}(e)||f("missing event name"),this.eventOptions=s.eventOptions||{},this.identifier=s.identifier||f("missing identifier"),this.methodName=s.methodName||f("missing method name"),this.keyFilter=s.keyFilter||"",this.schema=i}static forToken(e,t){return new this(e.element,e.index,function(e){const t=e.trim().match(a)||[];let s=t[2],i=t[3];return i&&!["keydown","keyup","keypress"].includes(s)&&(s+=`.${i}`,i=""),{eventTarget:(n=t[4],"window"==n?window:"document"==n?document:void 0),eventName:s,eventOptions:t[7]?(r=t[7],r.split(":").reduce((e,t)=>Object.assign(e,{[t.replace(/^!/,"")]:!/^!/.test(t)}),{})):{},identifier:t[5],methodName:t[6],keyFilter:t[1]||i};var n,r}(e.content),t)}toString(){const e=this.keyFilter?`.${this.keyFilter}`:"",t=this.eventTargetName?`@${this.eventTargetName}`:"";return`${this.eventName}${e}${t}->${this.identifier}#${this.methodName}`}shouldIgnoreKeyboardEvent(e){if(!this.keyFilter)return!1;const t=this.keyFilter.split("+");if(this.keyFilterDissatisfied(e,t))return!0;const s=t.filter(e=>!p.includes(e))[0];return!!s&&(d(this.keyMappings,s)||f(`contains unknown key filter: ${this.keyFilter}`),this.keyMappings[s].toLowerCase()!==e.key.toLowerCase())}shouldIgnoreMouseEvent(e){if(!this.keyFilter)return!1;const t=[this.keyFilter];return!!this.keyFilterDissatisfied(e,t)}get params(){const e={},t=new RegExp(`^data-${this.identifier}-(.+)-param$`,"i");for(const{name:s,value:i}of Array.from(this.element.attributes)){const n=s.match(t),r=n&&n[1];r&&(e[o(r)]=y(i))}return e}get eventTargetName(){return(e=this.eventTarget)==window?"window":e==document?"document":void 0;var e}get keyMappings(){return this.schema.keyMappings}keyFilterDissatisfied(e,t){const[s,i,n,r]=p.map(e=>t.includes(e));return e.metaKey!==s||e.ctrlKey!==i||e.altKey!==n||e.shiftKey!==r}}const g={a:()=>"click",button:()=>"click",form:()=>"submit",details:()=>"toggle",input:e=>"submit"==e.getAttribute("type")?"click":"input",select:()=>"change",textarea:()=>"input"};function f(e){throw new Error(e)}function y(e){try{return JSON.parse(e)}catch(t){return e}}class b{constructor(e,t){this.context=e,this.action=t}get index(){return this.action.index}get eventTarget(){return this.action.eventTarget}get eventOptions(){return this.action.eventOptions}get identifier(){return this.context.identifier}handleEvent(e){const t=this.prepareActionEvent(e);this.willBeInvokedByEvent(e)&&this.applyEventModifiers(t)&&this.invokeWithEvent(t)}get eventName(){return this.action.eventName}get method(){const e=this.controller[this.methodName];if("function"==typeof e)return e;throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)}applyEventModifiers(e){const{element:t}=this.action,{actionDescriptorFilters:s}=this.context.application,{controller:i}=this.context;let n=!0;for(const[r,a]of Object.entries(this.eventOptions))if(r in s){const o=s[r];n=n&&o({name:r,value:a,event:e,element:t,controller:i})}return n}prepareActionEvent(e){return Object.assign(e,{params:this.action.params})}invokeWithEvent(e){const{target:t,currentTarget:s}=e;try{this.method.call(this.controller,e),this.context.logDebugActivity(this.methodName,{event:e,target:t,currentTarget:s,action:this.methodName})}catch(t){const{identifier:s,controller:i,element:n,index:r}=this,a={identifier:s,controller:i,element:n,index:r,event:e};this.context.handleError(t,`invoking action "${this.action}"`,a)}}willBeInvokedByEvent(e){const t=e.target;return!(e instanceof KeyboardEvent&&this.action.shouldIgnoreKeyboardEvent(e))&&!(e instanceof MouseEvent&&this.action.shouldIgnoreMouseEvent(e))&&(this.element===t||(t instanceof Element&&this.element.contains(t)?this.scope.containsElement(t):this.scope.containsElement(this.action.element)))}get controller(){return this.context.controller}get methodName(){return this.action.methodName}get element(){return this.scope.element}get scope(){return this.context.scope}}class v{constructor(e,t){this.mutationObserverInit={attributes:!0,childList:!0,subtree:!0},this.element=e,this.started=!1,this.delegate=t,this.elements=new Set,this.mutationObserver=new MutationObserver(e=>this.processMutations(e))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,this.mutationObserverInit),this.refresh())}pause(e){this.started&&(this.mutationObserver.disconnect(),this.started=!1),e(),this.started||(this.mutationObserver.observe(this.element,this.mutationObserverInit),this.started=!0)}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started){const e=new Set(this.matchElementsInTree());for(const t of Array.from(this.elements))e.has(t)||this.removeElement(t);for(const t of Array.from(e))this.addElement(t)}}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){"attributes"==e.type?this.processAttributeChange(e.target,e.attributeName):"childList"==e.type&&(this.processRemovedNodes(e.removedNodes),this.processAddedNodes(e.addedNodes))}processAttributeChange(e,t){this.elements.has(e)?this.delegate.elementAttributeChanged&&this.matchElement(e)?this.delegate.elementAttributeChanged(e,t):this.removeElement(e):this.matchElement(e)&&this.addElement(e)}processRemovedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.processTree(e,this.removeElement)}}processAddedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.elementIsActive(e)&&this.processTree(e,this.addElement)}}matchElement(e){return this.delegate.matchElement(e)}matchElementsInTree(e=this.element){return this.delegate.matchElementsInTree(e)}processTree(e,t){for(const s of this.matchElementsInTree(e))t.call(this,s)}elementFromNode(e){if(e.nodeType==Node.ELEMENT_NODE)return e}elementIsActive(e){return e.isConnected==this.element.isConnected&&this.element.contains(e)}addElement(e){this.elements.has(e)||this.elementIsActive(e)&&(this.elements.add(e),this.delegate.elementMatched&&this.delegate.elementMatched(e))}removeElement(e){this.elements.has(e)&&(this.elements.delete(e),this.delegate.elementUnmatched&&this.delegate.elementUnmatched(e))}}class w{constructor(e,t,s){this.attributeName=t,this.delegate=s,this.elementObserver=new v(e,this)}get element(){return this.elementObserver.element}get selector(){return`[${this.attributeName}]`}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get started(){return this.elementObserver.started}matchElement(e){return e.hasAttribute(this.attributeName)}matchElementsInTree(e){const t=this.matchElement(e)?[e]:[],s=Array.from(e.querySelectorAll(this.selector));return t.concat(s)}elementMatched(e){this.delegate.elementMatchedAttribute&&this.delegate.elementMatchedAttribute(e,this.attributeName)}elementUnmatched(e){this.delegate.elementUnmatchedAttribute&&this.delegate.elementUnmatchedAttribute(e,this.attributeName)}elementAttributeChanged(e,t){this.delegate.elementAttributeValueChanged&&this.attributeName==t&&this.delegate.elementAttributeValueChanged(e,t)}}function T(e,t){let s=e.get(t);return s||(s=new Set,e.set(t,s)),s}class S{constructor(){this.valuesByKey=new Map}get keys(){return Array.from(this.valuesByKey.keys())}get values(){return Array.from(this.valuesByKey.values()).reduce((e,t)=>e.concat(Array.from(t)),[])}get size(){return Array.from(this.valuesByKey.values()).reduce((e,t)=>e+t.size,0)}add(e,t){!function(e,t,s){T(e,t).add(s)}(this.valuesByKey,e,t)}delete(e,t){!function(e,t,s){T(e,t).delete(s),function(e,t){const s=e.get(t);null!=s&&0==s.size&&e.delete(t)}(e,t)}(this.valuesByKey,e,t)}has(e,t){const s=this.valuesByKey.get(e);return null!=s&&s.has(t)}hasKey(e){return this.valuesByKey.has(e)}hasValue(e){return Array.from(this.valuesByKey.values()).some(t=>t.has(e))}getValuesForKey(e){const t=this.valuesByKey.get(e);return t?Array.from(t):[]}getKeysForValue(e){return Array.from(this.valuesByKey).filter(([t,s])=>s.has(e)).map(([e,t])=>e)}}class C{constructor(e,t,s,i){this._selector=t,this.details=i,this.elementObserver=new v(e,this),this.delegate=s,this.matchesByElement=new S}get started(){return this.elementObserver.started}get selector(){return this._selector}set selector(e){this._selector=e,this.refresh()}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get element(){return this.elementObserver.element}matchElement(e){const{selector:t}=this;if(t){const s=e.matches(t);return this.delegate.selectorMatchElement?s&&this.delegate.selectorMatchElement(e,this.details):s}return!1}matchElementsInTree(e){const{selector:t}=this;if(t){const s=this.matchElement(e)?[e]:[],i=Array.from(e.querySelectorAll(t)).filter(e=>this.matchElement(e));return s.concat(i)}return[]}elementMatched(e){const{selector:t}=this;t&&this.selectorMatched(e,t)}elementUnmatched(e){const t=this.matchesByElement.getKeysForValue(e);for(const s of t)this.selectorUnmatched(e,s)}elementAttributeChanged(e,t){const{selector:s}=this;if(s){const t=this.matchElement(e),i=this.matchesByElement.has(s,e);t&&!i?this.selectorMatched(e,s):!t&&i&&this.selectorUnmatched(e,s)}}selectorMatched(e,t){this.delegate.selectorMatched(e,t,this.details),this.matchesByElement.add(t,e)}selectorUnmatched(e,t){this.delegate.selectorUnmatched(e,t,this.details),this.matchesByElement.delete(t,e)}}class A{constructor(e,t){this.element=e,this.delegate=t,this.started=!1,this.stringMap=new Map,this.mutationObserver=new MutationObserver(e=>this.processMutations(e))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,{attributes:!0,attributeOldValue:!0}),this.refresh())}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started)for(const e of this.knownAttributeNames)this.refreshAttribute(e,null)}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){const t=e.attributeName;t&&this.refreshAttribute(t,e.oldValue)}refreshAttribute(e,t){const s=this.delegate.getStringMapKeyForAttribute(e);if(null!=s){this.stringMap.has(e)||this.stringMapKeyAdded(s,e);const i=this.element.getAttribute(e);if(this.stringMap.get(e)!=i&&this.stringMapValueChanged(i,s,t),null==i){const t=this.stringMap.get(e);this.stringMap.delete(e),t&&this.stringMapKeyRemoved(s,e,t)}else this.stringMap.set(e,i)}}stringMapKeyAdded(e,t){this.delegate.stringMapKeyAdded&&this.delegate.stringMapKeyAdded(e,t)}stringMapValueChanged(e,t,s){this.delegate.stringMapValueChanged&&this.delegate.stringMapValueChanged(e,t,s)}stringMapKeyRemoved(e,t,s){this.delegate.stringMapKeyRemoved&&this.delegate.stringMapKeyRemoved(e,t,s)}get knownAttributeNames(){return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))}get currentAttributeNames(){return Array.from(this.element.attributes).map(e=>e.name)}get recordedAttributeNames(){return Array.from(this.stringMap.keys())}}class E{constructor(e,t,s){this.attributeObserver=new w(e,t,this),this.delegate=s,this.tokensByElement=new S}get started(){return this.attributeObserver.started}start(){this.attributeObserver.start()}pause(e){this.attributeObserver.pause(e)}stop(){this.attributeObserver.stop()}refresh(){this.attributeObserver.refresh()}get element(){return this.attributeObserver.element}get attributeName(){return this.attributeObserver.attributeName}elementMatchedAttribute(e){this.tokensMatched(this.readTokensForElement(e))}elementAttributeValueChanged(e){const[t,s]=this.refreshTokensForElement(e);this.tokensUnmatched(t),this.tokensMatched(s)}elementUnmatchedAttribute(e){this.tokensUnmatched(this.tokensByElement.getValuesForKey(e))}tokensMatched(e){e.forEach(e=>this.tokenMatched(e))}tokensUnmatched(e){e.forEach(e=>this.tokenUnmatched(e))}tokenMatched(e){this.delegate.tokenMatched(e),this.tokensByElement.add(e.element,e)}tokenUnmatched(e){this.delegate.tokenUnmatched(e),this.tokensByElement.delete(e.element,e)}refreshTokensForElement(e){const t=this.tokensByElement.getValuesForKey(e),s=this.readTokensForElement(e),i=function(e,t){const s=Math.max(e.length,t.length);return Array.from({length:s},(s,i)=>[e[i],t[i]])}(t,s).findIndex(([e,t])=>{return i=t,!((s=e)&&i&&s.index==i.index&&s.content==i.content);var s,i});return-1==i?[[],[]]:[t.slice(i),s.slice(i)]}readTokensForElement(e){const t=this.attributeName;return function(e,t,s){return e.trim().split(/\s+/).filter(e=>e.length).map((e,i)=>({element:t,attributeName:s,content:e,index:i}))}(e.getAttribute(t)||"",e,t)}}class O{constructor(e,t,s){this.tokenListObserver=new E(e,t,this),this.delegate=s,this.parseResultsByToken=new WeakMap,this.valuesByTokenByElement=new WeakMap}get started(){return this.tokenListObserver.started}start(){this.tokenListObserver.start()}stop(){this.tokenListObserver.stop()}refresh(){this.tokenListObserver.refresh()}get element(){return this.tokenListObserver.element}get attributeName(){return this.tokenListObserver.attributeName}tokenMatched(e){const{element:t}=e,{value:s}=this.fetchParseResultForToken(e);s&&(this.fetchValuesByTokenForElement(t).set(e,s),this.delegate.elementMatchedValue(t,s))}tokenUnmatched(e){const{element:t}=e,{value:s}=this.fetchParseResultForToken(e);s&&(this.fetchValuesByTokenForElement(t).delete(e),this.delegate.elementUnmatchedValue(t,s))}fetchParseResultForToken(e){let t=this.parseResultsByToken.get(e);return t||(t=this.parseToken(e),this.parseResultsByToken.set(e,t)),t}fetchValuesByTokenForElement(e){let t=this.valuesByTokenByElement.get(e);return t||(t=new Map,this.valuesByTokenByElement.set(e,t)),t}parseToken(e){try{return{value:this.delegate.parseValueForToken(e)}}catch(e){return{error:e}}}}class x{constructor(e,t){this.context=e,this.delegate=t,this.bindingsByAction=new Map}start(){this.valueListObserver||(this.valueListObserver=new O(this.element,this.actionAttribute,this),this.valueListObserver.start())}stop(){this.valueListObserver&&(this.valueListObserver.stop(),delete this.valueListObserver,this.disconnectAllActions())}get element(){return this.context.element}get identifier(){return this.context.identifier}get actionAttribute(){return this.schema.actionAttribute}get schema(){return this.context.schema}get bindings(){return Array.from(this.bindingsByAction.values())}connectAction(e){const t=new b(this.context,e);this.bindingsByAction.set(e,t),this.delegate.bindingConnected(t)}disconnectAction(e){const t=this.bindingsByAction.get(e);t&&(this.bindingsByAction.delete(e),this.delegate.bindingDisconnected(t))}disconnectAllActions(){this.bindings.forEach(e=>this.delegate.bindingDisconnected(e,!0)),this.bindingsByAction.clear()}parseValueForToken(e){const t=m.forToken(e,this.schema);if(t.identifier==this.identifier)return t}elementMatchedValue(e,t){this.connectAction(t)}elementUnmatchedValue(e,t){this.disconnectAction(t)}}class M{constructor(e,t){this.context=e,this.receiver=t,this.stringMapObserver=new A(this.element,this),this.valueDescriptorMap=this.controller.valueDescriptorMap}start(){this.stringMapObserver.start(),this.invokeChangedCallbacksForDefaultValues()}stop(){this.stringMapObserver.stop()}get element(){return this.context.element}get controller(){return this.context.controller}getStringMapKeyForAttribute(e){if(e in this.valueDescriptorMap)return this.valueDescriptorMap[e].name}stringMapKeyAdded(e,t){const s=this.valueDescriptorMap[t];this.hasValue(e)||this.invokeChangedCallback(e,s.writer(this.receiver[e]),s.writer(s.defaultValue))}stringMapValueChanged(e,t,s){const i=this.valueDescriptorNameMap[t];null!==e&&(null===s&&(s=i.writer(i.defaultValue)),this.invokeChangedCallback(t,e,s))}stringMapKeyRemoved(e,t,s){const i=this.valueDescriptorNameMap[e];this.hasValue(e)?this.invokeChangedCallback(e,i.writer(this.receiver[e]),s):this.invokeChangedCallback(e,i.writer(i.defaultValue),s)}invokeChangedCallbacksForDefaultValues(){for(const{key:e,name:t,defaultValue:s,writer:i}of this.valueDescriptors)null==s||this.controller.data.has(e)||this.invokeChangedCallback(t,i(s),void 0)}invokeChangedCallback(e,t,s){const i=`${e}Changed`,n=this.receiver[i];if("function"==typeof n){const i=this.valueDescriptorNameMap[e];try{const e=i.reader(t);let r=s;s&&(r=i.reader(s)),n.call(this.receiver,e,r)}catch(e){throw e instanceof TypeError&&(e.message=`Stimulus Value "${this.context.identifier}.${i.name}" - ${e.message}`),e}}}get valueDescriptors(){const{valueDescriptorMap:e}=this;return Object.keys(e).map(t=>e[t])}get valueDescriptorNameMap(){const e={};return Object.keys(this.valueDescriptorMap).forEach(t=>{const s=this.valueDescriptorMap[t];e[s.name]=s}),e}hasValue(e){const t=`has${l(this.valueDescriptorNameMap[e].name)}`;return this.receiver[t]}}class I{constructor(e,t){this.context=e,this.delegate=t,this.targetsByName=new S}start(){this.tokenListObserver||(this.tokenListObserver=new E(this.element,this.attributeName,this),this.tokenListObserver.start())}stop(){this.tokenListObserver&&(this.disconnectAllTargets(),this.tokenListObserver.stop(),delete this.tokenListObserver)}tokenMatched({element:e,content:t}){this.scope.containsElement(e)&&this.connectTarget(e,t)}tokenUnmatched({element:e,content:t}){this.disconnectTarget(e,t)}connectTarget(e,t){var s;this.targetsByName.has(t,e)||(this.targetsByName.add(t,e),null===(s=this.tokenListObserver)||void 0===s||s.pause(()=>this.delegate.targetConnected(e,t)))}disconnectTarget(e,t){var s;this.targetsByName.has(t,e)&&(this.targetsByName.delete(t,e),null===(s=this.tokenListObserver)||void 0===s||s.pause(()=>this.delegate.targetDisconnected(e,t)))}disconnectAllTargets(){for(const e of this.targetsByName.keys)for(const t of this.targetsByName.getValuesForKey(e))this.disconnectTarget(t,e)}get attributeName(){return`data-${this.context.identifier}-target`}get element(){return this.context.element}get scope(){return this.context.scope}}function k(e,t){const s=P(e);return Array.from(s.reduce((e,s)=>(function(e,t){const s=e[t];return Array.isArray(s)?s:[]}(s,t).forEach(t=>e.add(t)),e),new Set))}function P(e){const t=[];for(;e;)t.push(e),e=Object.getPrototypeOf(e);return t.reverse()}class L{constructor(e,t){this.started=!1,this.context=e,this.delegate=t,this.outletsByName=new S,this.outletElementsByName=new S,this.selectorObserverMap=new Map,this.attributeObserverMap=new Map}start(){this.started||(this.outletDefinitions.forEach(e=>{this.setupSelectorObserverForOutlet(e),this.setupAttributeObserverForOutlet(e)}),this.started=!0,this.dependentContexts.forEach(e=>e.refresh()))}refresh(){this.selectorObserverMap.forEach(e=>e.refresh()),this.attributeObserverMap.forEach(e=>e.refresh())}stop(){this.started&&(this.started=!1,this.disconnectAllOutlets(),this.stopSelectorObservers(),this.stopAttributeObservers())}stopSelectorObservers(){this.selectorObserverMap.size>0&&(this.selectorObserverMap.forEach(e=>e.stop()),this.selectorObserverMap.clear())}stopAttributeObservers(){this.attributeObserverMap.size>0&&(this.attributeObserverMap.forEach(e=>e.stop()),this.attributeObserverMap.clear())}selectorMatched(e,t,{outletName:s}){const i=this.getOutlet(e,s);i&&this.connectOutlet(i,e,s)}selectorUnmatched(e,t,{outletName:s}){const i=this.getOutletFromMap(e,s);i&&this.disconnectOutlet(i,e,s)}selectorMatchElement(e,{outletName:t}){const s=this.selector(t),i=this.hasOutlet(e,t),n=e.matches(`[${this.schema.controllerAttribute}~=${t}]`);return!!s&&i&&n&&e.matches(s)}elementMatchedAttribute(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}elementAttributeValueChanged(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}elementUnmatchedAttribute(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}connectOutlet(e,t,s){var i;this.outletElementsByName.has(s,t)||(this.outletsByName.add(s,e),this.outletElementsByName.add(s,t),null===(i=this.selectorObserverMap.get(s))||void 0===i||i.pause(()=>this.delegate.outletConnected(e,t,s)))}disconnectOutlet(e,t,s){var i;this.outletElementsByName.has(s,t)&&(this.outletsByName.delete(s,e),this.outletElementsByName.delete(s,t),null===(i=this.selectorObserverMap.get(s))||void 0===i||i.pause(()=>this.delegate.outletDisconnected(e,t,s)))}disconnectAllOutlets(){for(const e of this.outletElementsByName.keys)for(const t of this.outletElementsByName.getValuesForKey(e))for(const s of this.outletsByName.getValuesForKey(e))this.disconnectOutlet(s,t,e)}updateSelectorObserverForOutlet(e){const t=this.selectorObserverMap.get(e);t&&(t.selector=this.selector(e))}setupSelectorObserverForOutlet(e){const t=this.selector(e),s=new C(document.body,t,this,{outletName:e});this.selectorObserverMap.set(e,s),s.start()}setupAttributeObserverForOutlet(e){const t=this.attributeNameForOutletName(e),s=new w(this.scope.element,t,this);this.attributeObserverMap.set(e,s),s.start()}selector(e){return this.scope.outlets.getSelectorForOutletName(e)}attributeNameForOutletName(e){return this.scope.schema.outletAttributeForScope(this.identifier,e)}getOutletNameFromOutletAttributeName(e){return this.outletDefinitions.find(t=>this.attributeNameForOutletName(t)===e)}get outletDependencies(){const e=new S;return this.router.modules.forEach(t=>{k(t.definition.controllerConstructor,"outlets").forEach(s=>e.add(s,t.identifier))}),e}get outletDefinitions(){return this.outletDependencies.getKeysForValue(this.identifier)}get dependentControllerIdentifiers(){return this.outletDependencies.getValuesForKey(this.identifier)}get dependentContexts(){const e=this.dependentControllerIdentifiers;return this.router.contexts.filter(t=>e.includes(t.identifier))}hasOutlet(e,t){return!!this.getOutlet(e,t)||!!this.getOutletFromMap(e,t)}getOutlet(e,t){return this.application.getControllerForElementAndIdentifier(e,t)}getOutletFromMap(e,t){return this.outletsByName.getValuesForKey(t).find(t=>t.element===e)}get scope(){return this.context.scope}get schema(){return this.context.schema}get identifier(){return this.context.identifier}get application(){return this.context.application}get router(){return this.application.router}}class _{constructor(e,t){this.logDebugActivity=(e,t={})=>{const{identifier:s,controller:i,element:n}=this;t=Object.assign({identifier:s,controller:i,element:n},t),this.application.logDebugActivity(this.identifier,e,t)},this.module=e,this.scope=t,this.controller=new e.controllerConstructor(this),this.bindingObserver=new x(this,this.dispatcher),this.valueObserver=new M(this,this.controller),this.targetObserver=new I(this,this),this.outletObserver=new L(this,this);try{this.controller.initialize(),this.logDebugActivity("initialize")}catch(e){this.handleError(e,"initializing controller")}}connect(){this.bindingObserver.start(),this.valueObserver.start(),this.targetObserver.start(),this.outletObserver.start();try{this.controller.connect(),this.logDebugActivity("connect")}catch(e){this.handleError(e,"connecting controller")}}refresh(){this.outletObserver.refresh()}disconnect(){try{this.controller.disconnect(),this.logDebugActivity("disconnect")}catch(e){this.handleError(e,"disconnecting controller")}this.outletObserver.stop(),this.targetObserver.stop(),this.valueObserver.stop(),this.bindingObserver.stop()}get application(){return this.module.application}get identifier(){return this.module.identifier}get schema(){return this.application.schema}get dispatcher(){return this.application.dispatcher}get element(){return this.scope.element}get parentElement(){return this.element.parentElement}handleError(e,t,s={}){const{identifier:i,controller:n,element:r}=this;s=Object.assign({identifier:i,controller:n,element:r},s),this.application.handleError(e,`Error ${t}`,s)}targetConnected(e,t){this.invokeControllerMethod(`${t}TargetConnected`,e)}targetDisconnected(e,t){this.invokeControllerMethod(`${t}TargetDisconnected`,e)}outletConnected(e,t,s){this.invokeControllerMethod(`${c(s)}OutletConnected`,e,t)}outletDisconnected(e,t,s){this.invokeControllerMethod(`${c(s)}OutletDisconnected`,e,t)}invokeControllerMethod(e,...t){const s=this.controller;"function"==typeof s[e]&&s[e](...t)}}const N="function"==typeof Object.getOwnPropertySymbols?e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]:Object.getOwnPropertyNames,D=(()=>{function e(e){function t(){return Reflect.construct(e,arguments,new.target)}return t.prototype=Object.create(e.prototype,{constructor:{value:t}}),Reflect.setPrototypeOf(t,e),t}try{return function(){const t=e(function(){this.a.call(this)});t.prototype.a=function(){},new t}(),e}catch(e){return e=>class extends e{}}})();class F{constructor(e,t){this.application=e,this.definition=function(e){return{identifier:e.identifier,controllerConstructor:(t=e.controllerConstructor,function(e,t){const s=D(e),i=function(e,t){return N(t).reduce((s,i)=>{const n=function(e,t,s){const i=Object.getOwnPropertyDescriptor(e,s);if(!i||!("value"in i)){const e=Object.getOwnPropertyDescriptor(t,s).value;return i&&(e.get=i.get||e.get,e.set=i.set||e.set),e}}(e,t,i);return n&&Object.assign(s,{[i]:n}),s},{})}(e.prototype,t);return Object.defineProperties(s.prototype,i),s}(t,function(e){return k(e,"blessings").reduce((t,s)=>{const i=s(e);for(const e in i){const s=t[e]||{};t[e]=Object.assign(s,i[e])}return t},{})}(t)))};var t}(t),this.contextsByScope=new WeakMap,this.connectedContexts=new Set}get identifier(){return this.definition.identifier}get controllerConstructor(){return this.definition.controllerConstructor}get contexts(){return Array.from(this.connectedContexts)}connectContextForScope(e){const t=this.fetchContextForScope(e);this.connectedContexts.add(t),t.connect()}disconnectContextForScope(e){const t=this.contextsByScope.get(e);t&&(this.connectedContexts.delete(t),t.disconnect())}fetchContextForScope(e){let t=this.contextsByScope.get(e);return t||(t=new _(this,e),this.contextsByScope.set(e,t)),t}}class R{constructor(e){this.scope=e}has(e){return this.data.has(this.getDataKey(e))}get(e){return this.getAll(e)[0]}getAll(e){return(this.data.get(this.getDataKey(e))||"").match(/[^\s]+/g)||[]}getAttributeName(e){return this.data.getAttributeNameForKey(this.getDataKey(e))}getDataKey(e){return`${e}-class`}get data(){return this.scope.data}}class B{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get(e){const t=this.getAttributeNameForKey(e);return this.element.getAttribute(t)}set(e,t){const s=this.getAttributeNameForKey(e);return this.element.setAttribute(s,t),this.get(e)}has(e){const t=this.getAttributeNameForKey(e);return this.element.hasAttribute(t)}delete(e){if(this.has(e)){const t=this.getAttributeNameForKey(e);return this.element.removeAttribute(t),!0}return!1}getAttributeNameForKey(e){return`data-${this.identifier}-${h(e)}`}}class V{constructor(e){this.warnedKeysByObject=new WeakMap,this.logger=e}warn(e,t,s){let i=this.warnedKeysByObject.get(e);i||(i=new Set,this.warnedKeysByObject.set(e,i)),i.has(t)||(i.add(t),this.logger.warn(s,e))}}function j(e,t){return`[${e}~="${t}"]`}class ${constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce((e,t)=>e||this.findTarget(t)||this.findLegacyTarget(t),void 0)}findAll(...e){return e.reduce((e,t)=>[...e,...this.findAllTargets(t),...this.findAllLegacyTargets(t)],[])}findTarget(e){const t=this.getSelectorForTargetName(e);return this.scope.findElement(t)}findAllTargets(e){const t=this.getSelectorForTargetName(e);return this.scope.findAllElements(t)}getSelectorForTargetName(e){return j(this.schema.targetAttributeForScope(this.identifier),e)}findLegacyTarget(e){const t=this.getLegacySelectorForTargetName(e);return this.deprecate(this.scope.findElement(t),e)}findAllLegacyTargets(e){const t=this.getLegacySelectorForTargetName(e);return this.scope.findAllElements(t).map(t=>this.deprecate(t,e))}getLegacySelectorForTargetName(e){const t=`${this.identifier}.${e}`;return j(this.schema.targetAttribute,t)}deprecate(e,t){if(e){const{identifier:s}=this,i=this.schema.targetAttribute,n=this.schema.targetAttributeForScope(s);this.guide.warn(e,`target:${t}`,`Please replace ${i}="${s}.${t}" with ${n}="${t}". The ${i} attribute is deprecated and will be removed in a future version of Stimulus.`)}return e}get guide(){return this.scope.guide}}class U{constructor(e,t){this.scope=e,this.controllerElement=t}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce((e,t)=>e||this.findOutlet(t),void 0)}findAll(...e){return e.reduce((e,t)=>[...e,...this.findAllOutlets(t)],[])}getSelectorForOutletName(e){const t=this.schema.outletAttributeForScope(this.identifier,e);return this.controllerElement.getAttribute(t)}findOutlet(e){const t=this.getSelectorForOutletName(e);if(t)return this.findElement(t,e)}findAllOutlets(e){const t=this.getSelectorForOutletName(e);return t?this.findAllElements(t,e):[]}findElement(e,t){return this.scope.queryElements(e).filter(s=>this.matchesElement(s,e,t))[0]}findAllElements(e,t){return this.scope.queryElements(e).filter(s=>this.matchesElement(s,e,t))}matchesElement(e,t,s){const i=e.getAttribute(this.scope.schema.controllerAttribute)||"";return e.matches(t)&&i.split(" ").includes(s)}}class q{constructor(e,t,s,i){this.targets=new $(this),this.classes=new R(this),this.data=new B(this),this.containsElement=e=>e.closest(this.controllerSelector)===this.element,this.schema=e,this.element=t,this.identifier=s,this.guide=new V(i),this.outlets=new U(this.documentScope,t)}findElement(e){return this.element.matches(e)?this.element:this.queryElements(e).find(this.containsElement)}findAllElements(e){return[...this.element.matches(e)?[this.element]:[],...this.queryElements(e).filter(this.containsElement)]}queryElements(e){return Array.from(this.element.querySelectorAll(e))}get controllerSelector(){return j(this.schema.controllerAttribute,this.identifier)}get isDocumentScope(){return this.element===document.documentElement}get documentScope(){return this.isDocumentScope?this:new q(this.schema,document.documentElement,this.identifier,this.guide.logger)}}class z{constructor(e,t,s){this.element=e,this.schema=t,this.delegate=s,this.valueListObserver=new O(this.element,this.controllerAttribute,this),this.scopesByIdentifierByElement=new WeakMap,this.scopeReferenceCounts=new WeakMap}start(){this.valueListObserver.start()}stop(){this.valueListObserver.stop()}get controllerAttribute(){return this.schema.controllerAttribute}parseValueForToken(e){const{element:t,content:s}=e;return this.parseValueForElementAndIdentifier(t,s)}parseValueForElementAndIdentifier(e,t){const s=this.fetchScopesByIdentifierForElement(e);let i=s.get(t);return i||(i=this.delegate.createScopeForElementAndIdentifier(e,t),s.set(t,i)),i}elementMatchedValue(e,t){const s=(this.scopeReferenceCounts.get(t)||0)+1;this.scopeReferenceCounts.set(t,s),1==s&&this.delegate.scopeConnected(t)}elementUnmatchedValue(e,t){const s=this.scopeReferenceCounts.get(t);s&&(this.scopeReferenceCounts.set(t,s-1),1==s&&this.delegate.scopeDisconnected(t))}fetchScopesByIdentifierForElement(e){let t=this.scopesByIdentifierByElement.get(e);return t||(t=new Map,this.scopesByIdentifierByElement.set(e,t)),t}}class W{constructor(e){this.application=e,this.scopeObserver=new z(this.element,this.schema,this),this.scopesByIdentifier=new S,this.modulesByIdentifier=new Map}get element(){return this.application.element}get schema(){return this.application.schema}get logger(){return this.application.logger}get controllerAttribute(){return this.schema.controllerAttribute}get modules(){return Array.from(this.modulesByIdentifier.values())}get contexts(){return this.modules.reduce((e,t)=>e.concat(t.contexts),[])}start(){this.scopeObserver.start()}stop(){this.scopeObserver.stop()}loadDefinition(e){this.unloadIdentifier(e.identifier);const t=new F(this.application,e);this.connectModule(t);const s=e.controllerConstructor.afterLoad;s&&s.call(e.controllerConstructor,e.identifier,this.application)}unloadIdentifier(e){const t=this.modulesByIdentifier.get(e);t&&this.disconnectModule(t)}getContextForElementAndIdentifier(e,t){const s=this.modulesByIdentifier.get(t);if(s)return s.contexts.find(t=>t.element==e)}proposeToConnectScopeForElementAndIdentifier(e,t){const s=this.scopeObserver.parseValueForElementAndIdentifier(e,t);s?this.scopeObserver.elementMatchedValue(s.element,s):console.error(`Couldn't find or create scope for identifier: "${t}" and element:`,e)}handleError(e,t,s){this.application.handleError(e,t,s)}createScopeForElementAndIdentifier(e,t){return new q(this.schema,e,t,this.logger)}scopeConnected(e){this.scopesByIdentifier.add(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.connectContextForScope(e)}scopeDisconnected(e){this.scopesByIdentifier.delete(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.disconnectContextForScope(e)}connectModule(e){this.modulesByIdentifier.set(e.identifier,e),this.scopesByIdentifier.getValuesForKey(e.identifier).forEach(t=>e.connectContextForScope(t))}disconnectModule(e){this.modulesByIdentifier.delete(e.identifier),this.scopesByIdentifier.getValuesForKey(e.identifier).forEach(t=>e.disconnectContextForScope(t))}}const K={controllerAttribute:"data-controller",actionAttribute:"data-action",targetAttribute:"data-target",targetAttributeForScope:e=>`data-${e}-target`,outletAttributeForScope:(e,t)=>`data-${e}-${t}-outlet`,keyMappings:Object.assign(Object.assign({enter:"Enter",tab:"Tab",esc:"Escape",space:" ",up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",page_up:"PageUp",page_down:"PageDown"},H("abcdefghijklmnopqrstuvwxyz".split("").map(e=>[e,e]))),H("0123456789".split("").map(e=>[e,e])))};function H(e){return e.reduce((e,[t,s])=>Object.assign(Object.assign({},e),{[t]:s}),{})}class G{constructor(e=document.documentElement,t=K){this.logger=console,this.debug=!1,this.logDebugActivity=(e,t,s={})=>{this.debug&&this.logFormattedMessage(e,t,s)},this.element=e,this.schema=t,this.dispatcher=new n(this),this.router=new W(this),this.actionDescriptorFilters=Object.assign({},r)}static start(e,t){const s=new this(e,t);return s.start(),s}async start(){await new Promise(e=>{"loading"==document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()}),this.logDebugActivity("application","starting"),this.dispatcher.start(),this.router.start(),this.logDebugActivity("application","start")}stop(){this.logDebugActivity("application","stopping"),this.dispatcher.stop(),this.router.stop(),this.logDebugActivity("application","stop")}register(e,t){this.load({identifier:e,controllerConstructor:t})}registerActionOption(e,t){this.actionDescriptorFilters[e]=t}load(e,...t){(Array.isArray(e)?e:[e,...t]).forEach(e=>{e.controllerConstructor.shouldLoad&&this.router.loadDefinition(e)})}unload(e,...t){(Array.isArray(e)?e:[e,...t]).forEach(e=>this.router.unloadIdentifier(e))}get controllers(){return this.router.contexts.map(e=>e.controller)}getControllerForElementAndIdentifier(e,t){const s=this.router.getContextForElementAndIdentifier(e,t);return s?s.controller:null}handleError(e,t,s){var i;this.logger.error("%s\n\n%o\n\n%o",t,e,s),null===(i=window.onerror)||void 0===i||i.call(window,t,"",0,0,e)}logFormattedMessage(e,t,s={}){s=Object.assign({application:this},s),this.logger.groupCollapsed(`${e} #${t}`),this.logger.log("details:",Object.assign({},s)),this.logger.groupEnd()}}function J(e,t,s){return e.application.getControllerForElementAndIdentifier(t,s)}function Y(e,t,s){let i=J(e,t,s);return i||(e.application.router.proposeToConnectScopeForElementAndIdentifier(t,s),i=J(e,t,s),i||void 0)}function Z([e,t],s){return function(e){const{token:t,typeDefinition:s}=e,i=`${h(t)}-value`,n=function(e){const{controller:t,token:s,typeDefinition:i}=e,n=function(e){const{controller:t,token:s,typeObject:i}=e,n=u(i.type),r=u(i.default),a=n&&r,o=n&&!r,c=!n&&r,l=X(i.type),h=Q(e.typeObject.default);if(o)return l;if(c)return h;if(l!==h)throw new Error(`The specified default value for the Stimulus Value "${t?`${t}.${s}`:s}" must match the defined type "${l}". The provided default value of "${i.default}" is of type "${h}".`);return a?l:void 0}({controller:t,token:s,typeObject:i}),r=Q(i),a=X(i),o=n||r||a;if(o)return o;throw new Error(`Unknown value type "${t?`${t}.${i}`:s}" for "${s}" value`)}(e);return{type:n,key:i,name:o(i),get defaultValue(){return function(e){const t=X(e);if(t)return ee[t];const s=d(e,"default"),i=d(e,"type"),n=e;if(s)return n.default;if(i){const{type:e}=n,t=X(e);if(t)return ee[t]}return e}(s)},get hasCustomDefaultValue(){return void 0!==Q(s)},reader:te[n],writer:se[n]||se.default}}({controller:s,token:e,typeDefinition:t})}function X(e){switch(e){case Array:return"array";case Boolean:return"boolean";case Number:return"number";case Object:return"object";case String:return"string"}}function Q(e){switch(typeof e){case"boolean":return"boolean";case"number":return"number";case"string":return"string"}return Array.isArray(e)?"array":"[object Object]"===Object.prototype.toString.call(e)?"object":void 0}const ee={get array(){return[]},boolean:!1,number:0,get object(){return{}},string:""},te={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type "array" but instead got value "${e}" of type "${Q(t)}"`);return t},boolean:e=>!("0"==e||"false"==String(e).toLowerCase()),number:e=>Number(e.replace(/_/g,"")),object(e){const t=JSON.parse(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type "object" but instead got value "${e}" of type "${Q(t)}"`);return t},string:e=>e},se={default:function(e){return`${e}`},array:ie,object:ie};function ie(e){return JSON.stringify(e)}class ne{constructor(e){this.context=e}static get shouldLoad(){return!0}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:s={},prefix:i=this.identifier,bubbles:n=!0,cancelable:r=!0}={}){const a=new CustomEvent(i?`${i}:${e}`:e,{detail:s,bubbles:n,cancelable:r});return t.dispatchEvent(a),a}}ne.blessings=[function(e){return k(e,"classes").reduce((e,t)=>{return Object.assign(e,(s=t,{[`${s}Class`]:{get(){const{classes:e}=this;if(e.has(s))return e.get(s);{const t=e.getAttributeName(s);throw new Error(`Missing attribute "${t}"`)}}},[`${s}Classes`]:{get(){return this.classes.getAll(s)}},[`has${l(s)}Class`]:{get(){return this.classes.has(s)}}}));var s},{})},function(e){return k(e,"targets").reduce((e,t)=>{return Object.assign(e,(s=t,{[`${s}Target`]:{get(){const e=this.targets.find(s);if(e)return e;throw new Error(`Missing target element "${s}" for "${this.identifier}" controller`)}},[`${s}Targets`]:{get(){return this.targets.findAll(s)}},[`has${l(s)}Target`]:{get(){return this.targets.has(s)}}}));var s},{})},function(e){const t=function(e,t){return P(e).reduce((e,s)=>(e.push(...function(e,t){const s=e[t];return s?Object.keys(s).map(e=>[e,s[e]]):[]}(s,t)),e),[])}(e,"values"),s={valueDescriptorMap:{get(){return t.reduce((e,t)=>{const s=Z(t,this.identifier),i=this.data.getAttributeNameForKey(s.key);return Object.assign(e,{[i]:s})},{})}}};return t.reduce((e,t)=>Object.assign(e,function(e){const t=Z(e,void 0),{key:s,name:i,reader:n,writer:r}=t;return{[i]:{get(){const e=this.data.get(s);return null!==e?n(e):t.defaultValue},set(e){void 0===e?this.data.delete(s):this.data.set(s,r(e))}},[`has${l(i)}`]:{get(){return this.data.has(s)||t.hasCustomDefaultValue}}}}(t)),s)},function(e){return k(e,"outlets").reduce((e,t)=>Object.assign(e,function(e){const t=c(e);return{[`${t}Outlet`]:{get(){const t=this.outlets.find(e),s=this.outlets.getSelectorForOutletName(e);if(t){const s=Y(this,t,e);if(s)return s;throw new Error(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`)}throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${s}".`)}},[`${t}Outlets`]:{get(){const t=this.outlets.findAll(e);return t.length>0?t.map(t=>{const s=Y(this,t,e);if(s)return s;console.warn(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`,t)}).filter(e=>e):[]}},[`${t}OutletElement`]:{get(){const t=this.outlets.find(e),s=this.outlets.getSelectorForOutletName(e);if(t)return t;throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${s}".`)}},[`${t}OutletElements`]:{get(){return this.outlets.findAll(e)}},[`has${l(t)}Outlet`]:{get(){return this.outlets.has(e)}}}}(t)),{})}],ne.targets=[],ne.outlets=[],ne.values={}},955(e,t,s){s.d(t,{default:()=>Li});var i=s(891);class n extends Error{constructor(e){super(`${e} is not valid. Please provide a valid event name`),this.name="InvalidEvent"}}class r{static events=["session-set","utm-set","forms:collected","form:completed","popup:mounted","popup:opened","popup:closed","alert:shown","alert:dismissed","alert:accepted","activity:occurred","webchat:mounted","webchat:opened","webchat:closed","webchat:message:sent","webchat:message:received","cart.added"];static valid(e){return r.exists(e)}static invalid(e){return!this.valid(e)}static exists(e){return void 0!==this.events.find(t=>t===e)}constructor(){this.subscribers={}}addSubscriber(e,t){if(r.invalid(e))throw new n(e);this.subscribers={...this.subscribers,[e]:this.subscribers[e]?[...this.subscribers[e],t]:[t]}}removeSubscriber(e,t){if(r.invalid(e))throw new n(e);this.subscribers[e]&&(this.subscribers[e]=this.subscribers[e].filter(e=>e!==t))}dispatch(e,t){this.subscribers[e]?.forEach(e=>{e(t)})}get listeners(){return 0!==Object.keys(this.subscribers).length}}class a{static autoMount=!0;static successMessage=!0;static assign(e){return e&&Object.entries(e).forEach(([e,t])=>{this[e]=t}),this}static get shouldShowSuccessMessage(){return this.successMessage}}class o{static _identifier;static set identifier(e){this._identifier=e}static get identifier(){return this._identifier?this._identifier:this.#e||this.#t||this.#s||"en"}static toString(){return this.identifier}static get#e(){return"undefined"!=typeof document?document.documentElement?.lang:void 0}static get#t(){return"undefined"!=typeof document?document.querySelector('meta[name="locale"]')?.content:void 0}static get#s(){return"undefined"!=typeof navigator?navigator.language?.split("-")[0]:void 0}}class c{static _id;static _container="body";static _device="auto";static set id(e){this._id=e}static get id(){return this._id}static set container(e){this._container=e}static get container(){return this._container}static set device(e){if(!["auto","mobile","desktop"].includes(e))throw new Error(`Invalid popup device value: ${e}`);this._device=e}static get device(){return this._device}static assign(e){return e&&Object.entries(e).forEach(([e,t])=>{this[e]=t}),this}}const l={TOP_LEFT:"top-left",TOP_RIGHT:"top-right",BOTTOM_LEFT:"bottom-left",BOTTOM_RIGHT:"bottom-right"},h={ABSOLUTE:"absolute",FIXED:"fixed"},u={MODAL:"modal",POPOVER:"popover"};class d{static _id;static _container="body";static _placement="bottom-right";static _style={};static _appearance={};static _whatsapp={};static _mode=u.POPOVER;static _behaviour=null;static _hasBehaviourOverride=!1;static _strategy=null;static set container(e){this._container=e}static get container(){return this._container}static set placement(e){if(!Object.values(l).includes(e))throw new Error(`Invalid placement value: ${e}`);this._placement=e}static get placement(){return this._placement}static set id(e){this._id=e}static get id(){return this._id}static get isSet(){return!!this._id}static get style(){return this._style}static set style(e){if("object"!=typeof e)throw new Error("Style must be an object");Object.entries(e).forEach(([e,t])=>{if(!["primaryColor","secondaryColor","typography"].includes(e))throw new Error(`Invalid style property: ${e}`);if("typography"!==e&&!this.isHexOrRgba(t))throw new Error(`Invalid color value: ${t} for ${e}. Colors must be hex or rgb/a.`)}),this._style=e}static get appearance(){return this._appearance}static set appearance(e){if(!this.isPlainObject(e))throw new Error("Appearance must be an object");Object.entries(e).forEach(([e,t])=>{if(!["header","launcher"].includes(e))throw new Error(`Invalid appearance property: ${e}`);if(!this.isPlainObject(t))throw new Error(`Appearance ${e} must be an object`);Object.entries(t).forEach(([t,s])=>{if("header"===e&&"name"!==t)throw new Error(`Invalid appearance header property: ${t}`);if("launcher"===e&&"iconUrl"!==t)throw new Error(`Invalid appearance launcher property: ${t}`);if(null!=s&&"string"!=typeof s)throw new Error(`Invalid appearance ${e}.${t} value: ${s}`)})}),this._appearance=e}static get whatsapp(){return this._whatsapp}static set whatsapp(e){if(!this.isPlainObject(e))throw new Error("WhatsApp must be an object");Object.entries(e).forEach(([e,t])=>{if(!["number","restrictToChannel"].includes(e))throw new Error(`Invalid WhatsApp property: ${e}`);if(null!=t){if("number"===e&&"string"!=typeof t)throw new Error(`Invalid WhatsApp number value: ${t}`);if("restrictToChannel"===e&&"boolean"!=typeof t)throw new Error(`Invalid WhatsApp restrictToChannel value: ${t}`)}}),this._whatsapp=e}static get mode(){return this._mode}static set mode(e){if(!Object.values(u).includes(e))throw new Error(`Invalid mode value: ${e}`);this._mode=e}static get behaviour(){return this._behaviour}static set behaviour(e){if(null!=e){if("object"!=typeof e||Array.isArray(e))throw new Error(`Invalid behaviour value: ${e}`);this._behaviour=e}else this._behaviour=e}static get hasBehaviourOverride(){return this._hasBehaviourOverride}static set behaviourOverride(e){this._hasBehaviourOverride=!!e}static get strategy(){return this._strategy?this._strategy:"body"==this.container?h.FIXED:h.ABSOLUTE}static set strategy(e){if(e&&!Object.values(h).includes(e))throw new Error(`Invalid strategy value: ${e}`);this._strategy=e}static assign(e){return e&&Object.entries(e).forEach(([e,t])=>{this[e]=t}),this}static isHexOrRgba(e){return/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(e)||/^rgba?\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3},?\s*(0|1|0?\.\d+)?\s*\)$/.test(e)}static isPlainObject(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}}const p={TOP_LEFT:"top-left",TOP_RIGHT:"top-right",BOTTOM_LEFT:"bottom-left",BOTTOM_RIGHT:"bottom-right"};class m{static _id;static _container="body";static _placement="bottom-right";static _appearance={};static _number=null;static _body=null;static set id(e){this._id=e}static get id(){return this._id}static set container(e){this._container=e}static get container(){return this._container}static set placement(e){if(!Object.values(p).includes(e))throw new Error(`Invalid placement value: ${e}`);this._placement=e}static get placement(){return this._placement}static get appearance(){return this._appearance}static set appearance(e){if(!this.isPlainObject(e))throw new Error("Appearance must be an object");Object.entries(e).forEach(([e,t])=>{if("launcher"!==e)throw new Error(`Invalid appearance property: ${e}`);if(!this.isPlainObject(t))throw new Error(`Appearance ${e} must be an object`);Object.entries(t).forEach(([t,s])=>{if("iconUrl"!==t)throw new Error(`Invalid appearance launcher property: ${t}`);if(null!=s&&"string"!=typeof s)throw new Error(`Invalid appearance ${e}.${t} value: ${s}`)})}),this._appearance=e}static get number(){return this._number}static set number(e){if(null!=e&&"string"!=typeof e)throw new Error(`Invalid number value: ${e}`);this._number=e}static get body(){return this._body}static set body(e){if(null!=e&&"string"!=typeof e)throw new Error(`Invalid body value: ${e}`);this._body=e}static assign(e){return e&&Object.entries(e).forEach(([e,t])=>{this[e]=t}),this}static isPlainObject(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}}class g{static serviceWorkerUrl=null;static channelId=null;static assign(e){return this.serviceWorkerUrl=e?.serviceWorkerUrl||null,this.channelId=e?.channelId||null,this}}class f{static apiRoot="https://api.hellotext.com/v1";static actionCableUrl="wss://www.hellotext.com/cable";static autoGenerateSession=!0;static session=null;static forms=a;static popup=c;static webchat=d;static whatsapp=m;static push=g;static assign(e){if(e){const t=Object.prototype.hasOwnProperty.call(e,"apiRoot")&&!Object.prototype.hasOwnProperty.call(e,"actionCableUrl");Object.entries(e).forEach(([e,t])=>{"forms"===e?this.forms=a.assign(t):"popup"===e?this.popup=c.assign(t):"webchat"===e?this.webchat=d.assign(t):"whatsappWidget"===e?this.whatsapp=m.assign(t):"push"===e?this.push=g.assign(t):this[e]=t}),t&&(this.actionCableUrl=this.actionCableUrlForApiRoot(this.apiRoot))}return this}static set locale(e){o.identifier=e}static get locale(){return o.toString()}static endpoint(e){return`${this.apiRoot}/${e}`}static actionCableUrlForApiRoot(e){try{const t=new URL(e),s="https:"===t.protocol?"wss:":"ws:";return t.protocol=s,t.pathname="/cable",t.search="",t.hash="",t.toString()}catch(e){return this.actionCableUrl}}}const y=class{static get endpoint(){return f.endpoint("public/businesses")}static async get(e){return fetch(`${this.endpoint}/${e}`,{method:"GET",headers:{Authorization:`Bearer ${e}`,Accept:"application/json","Content-Type":"application/json"}})}};class b{static get inPreviewMode(){return(new this).inPreviewMode}constructor(){this.urlSearchParams=new URLSearchParams(window.location.search)}get(e){return this.urlSearchParams.get(this.toHellotextParam(e))}has(e){return this.urlSearchParams.has(this.toHellotextParam(e))}get inPreviewMode(){return this.has("preview")}get session(){return this.get("session")}toHellotextParam(e){return`hello_${e}`}}class v{#i;constructor(e,t){this.response=t,this.#i=e,this.jsonPromise=null}get data(){return this.response}async json(){return this.jsonPromise||=Promise.resolve(this.response.json()),await this.jsonPromise}get failed(){return!1===this.#i}get succeeded(){return!0===this.#i}}class w{static get endpoint(){return f.endpoint("track/events")}static async create({headers:e,body:t,keepalive:s=!1}){if(b.inPreviewMode)return new v(!0,{received:!0});const i={method:"POST",headers:e,body:JSON.stringify(t)};s&&(i.keepalive=!0);const n=await fetch(this.endpoint,i);return new v(200===n.status,await n.json())}}class T{static get endpoint(){return f.endpoint("public/forms")}static async get(e){const t=new URL(`${this.endpoint}/${e}`);return t.searchParams.append("session",Tt.session),t.searchParams.append("locale",o.toString()),fetch(t,{method:"GET",headers:Tt.headers})}static async submit(e,t){const s=await fetch(`${this.endpoint}/${e}/submissions`,{method:"POST",headers:Tt.headers,body:JSON.stringify({session:Tt.session,...t})});return new v(s.ok,s)}}const S=class{static get endpoint(){return f.endpoint("public/identifications")}static async create(e={}){const t=await fetch(this.endpoint,{method:"POST",headers:Tt.headers,body:JSON.stringify({session:Tt.session,...e})});return new v(t.ok,t)}static async status(e){const t=new URL(`${this.endpoint}/${e}`),s=await fetch(t,{method:"GET",headers:{...Tt.headers,"X-Hellotext-Session":Tt.session}});return new v(s.ok,s)}},C=class{static get endpoint(){return f.endpoint("public/popups")}static async get(e){const t=new URL(`${this.endpoint}/${e}`);t.searchParams.append("session",Tt.session),t.searchParams.append("locale",o.toString()),t.searchParams.append("device",this.runtimeDevice);const s=await this.fetchPopup(t);if(!s.ok)return null;const i=await this.parsePopupResponse(s);return i&&!1!==i.eligible&&i.html?(Tt.business.data||(Tt.business.setData(i.business),Tt.business.setLocale(i.locale)),(new DOMParser).parseFromString(i.html,"text/html").querySelector("article")):null}static async submit(e,t,s=this.idempotencyKey()){const i=await fetch(`${this.endpoint}/${e}/submissions`,{method:"POST",headers:{...Tt.headers,"Idempotency-Key":s},body:JSON.stringify({session:Tt.session,popup_submission:t})});return new v(i.ok,i)}static async resend(e,t,s){const i=await fetch(`${this.endpoint}/${e}/submissions/${t}/resend`,{method:"POST",headers:Tt.headers,body:JSON.stringify({token:s})});return new v(i.ok,i)}static async cancel(e,t,s){const i=await fetch(`${this.endpoint}/${e}/submissions/${t}/cancel`,{method:"POST",headers:Tt.headers,body:JSON.stringify({token:s})});return new v(i.ok,i)}static idempotencyKey(){return"undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID?crypto.randomUUID():`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}static async fetchPopup(e){try{return await fetch(e,{method:"GET",headers:{...Tt.headers,"X-Hellotext-Popup-Rules":"1"}})}catch(e){return{ok:!1}}}static get runtimeDevice(){return"auto"!==f.popup.device?f.popup.device:window.innerWidth<=767?"mobile":"desktop"}static async parsePopupResponse(e){try{return await e.json()}catch(e){return null}}},A=class{static get endpoint(){return f.endpoint("public/webchats")}static async get(e){const t=new URL(`${this.endpoint}/${e}`);t.searchParams.append("session",Tt.session),t.searchParams.append("locale",o.toString()),Object.entries(f.webchat.style).forEach(([e,s])=>{t.searchParams.append(`style[${e}]`,s)}),this.appendWebchatOverrides(t),t.searchParams.append("placement",f.webchat.placement);const s=await fetch(t,{method:"GET",headers:Tt.headers}),i=await s.json();return Tt.business.data||(Tt.business.setData(i.business),Tt.business.setLocale(i.locale)),(new DOMParser).parseFromString(i.html,"text/html").querySelector("article")}static appendWebchatOverrides(e){const{appearance:t,whatsapp:s}=f.webchat;this.appendIfSupplied(e,"webchat[appearance][header][name]",t.header?.name),this.appendIfSupplied(e,"webchat[appearance][launcher][icon_url]",t.launcher?.iconUrl),this.appendIfSupplied(e,"webchat[handoff][identifier]",s.number),this.appendIfSupplied(e,"webchat[handoff][restrict_to_channel]",s.restrictToChannel)}static appendIfSupplied(e,t,s){null!=s&&e.searchParams.append(t,String(s))}},E=class{static get endpoint(){return f.endpoint("public/widgets/whatsapp")}static async get(e){const t=new URL(`${this.endpoint}/${e}`);t.searchParams.append("locale",o.toString()),t.searchParams.append("placement",f.whatsapp.placement),this.appendWhatsAppOverrides(t);const s=await this.fetchWidget(t);if(!s.ok)return null;const i=await this.parseWidgetResponse(s);return i?(Tt.business.data||(Tt.business.setData(i.business),Tt.business.setLocale(i.locale)),(new DOMParser).parseFromString(i.html,"text/html").querySelector("article")):null}static appendWhatsAppOverrides(e){const{appearance:t,body:s,number:i}=f.whatsapp;this.appendIfSupplied(e,"whatsapp[appearance][launcher][icon_url]",t.launcher?.iconUrl),this.appendIfSupplied(e,"whatsapp[number]",i),this.appendIfSupplied(e,"whatsapp[body]",s)}static appendIfSupplied(e,t,s){null!=s&&e.searchParams.append(t,String(s))}static async fetchWidget(e){try{return await fetch(e,{method:"GET",headers:Tt.headers})}catch(e){return{ok:!1}}}static async parseWidgetResponse(e){try{return await e.json()}catch(e){return null}}},O=class{static get endpoint(){return f.endpoint("public/acks")}static async send(e={}){const t={...e,session:Tt.session,at:(new Date).toISOString()};fetch(this.endpoint,{method:"POST",headers:Tt.headers,body:JSON.stringify(t),keepalive:!0})}},x=class{static get endpoint(){return f.endpoint("public/push/identities")}static async create(e={}){const t=await fetch(this.endpoint,{method:"POST",keepalive:!0,headers:Tt.headers,body:JSON.stringify({...e,session:Tt.session,origin:window.location.origin})});return new v(t.ok,t)}static async destroy(e={}){const t=await fetch(this.endpoint,{method:"DELETE",keepalive:!0,headers:Tt.headers,body:JSON.stringify({...e,session:Tt.session,origin:window.location.origin})});return new v(t.ok,t)}},M=class{static get endpoint(){return f.endpoint("public/push/alerts")}static async create({section:e,kind:t,page:s}){const i=await fetch(this.endpoint,{method:"POST",keepalive:!0,headers:Tt.headers,body:JSON.stringify({session:Tt.session,section:e,kind:t,page:s})});return new v(i.ok,i)}};function I(e){const t=JSON.stringify(e);return"undefined"==typeof Blob?t.length<6e4:new Blob([t]).size<6e4}class k{static get businesses(){return y}static get events(){return w}static get forms(){return T}static get popups(){return C}static get popups(){return C}static get webchats(){return A}static get whatsappWidgets(){return E}static get identifications(){return S}static get acks(){return O}static get pushAlerts(){return M}static get pushIdentities(){return x}}const P="data-hellotext-stylesheet";class L{constructor(e){this.id=e,this.data=null,this.stylesheet=null,this.stylesheetLoaded=Promise.resolve(!1)}async hydrate(){try{const e=await y.get(this.id);if(!1===e.ok)return null;const t=await e.json();return t?(this.setData(t),this.setLocale(o.toString()),t):null}catch(e){return null}}setData(e){this.data=e,"undefined"!=typeof document&&e.style_url?(this.stylesheet=this.constructor.ensureStylesheet(e.style_url),this.stylesheetLoaded=this.constructor.waitForStylesheet(this.stylesheet)):(this.stylesheet=null,this.stylesheetLoaded=Promise.resolve(!1))}static get stylesheetSelector(){return`link[rel="stylesheet"][${P}]`}static ensureStylesheet(e){const t=this.normalizedStylesheetUrl(e),s=this.stylesheetLinks.find(e=>e.href===t);if(s)return s.setAttribute(P,"true"),s;const i=document.createElement("link");return i.rel="stylesheet",i.href=e,i.setAttribute(P,"true"),this.waitForStylesheet(i),document.head.append(i),i}static get stylesheetLinks(){return"undefined"==typeof document?[]:Array.from(document.querySelectorAll(this.stylesheetSelector))}static get latestStylesheet(){return this.stylesheetLinks[this.stylesheetLinks.length-1]}static normalizedStylesheetUrl(e){try{return new URL(e,document.baseURI).href}catch(t){return e}}static waitForStylesheet(e){return e?this.stylesheetIsLoaded(e)?Promise.resolve(!0):"false"===e.dataset.hellotextStylesheetLoaded?Promise.resolve(!1):(e._hellotextStylesheetLoaded||(e._hellotextStylesheetLoaded=new Promise(t=>{let s;const i=i=>{clearTimeout(s),e.removeEventListener("load",n),e.removeEventListener("error",r),e.dataset.hellotextStylesheetLoaded=i?"true":"false",t(i)},n=()=>i(this.stylesheetIsLoaded(e)),r=()=>i(!1);e.addEventListener("load",n),e.addEventListener("error",r),s=setTimeout(()=>i(this.stylesheetIsLoaded(e)),1e4),s.unref&&s.unref()})),e._hellotextStylesheetLoaded):Promise.resolve(!1)}static stylesheetIsLoaded(e){return"true"===e.dataset.hellotextStylesheetLoaded||!!e.sheet}get subscription(){return this.data.subscription}get country(){return this.data.country}get enabledWhitelist(){return"disabled"!==this.data.whitelist}setLocale(e){this.data||(this.data={});const t=e?.toLowerCase().split("-")[0];this.data.locale=Object.prototype.hasOwnProperty.call(this.data.locales||{},t)?t:"en"}get locale(){const e=this.data?.locales;return e?.[this.data.locale]||e?.en}get features(){return this.data.features}}class _{static set(e,t){if("undefined"!=typeof document){const s="https:"===window.location.protocol?"; Secure":"",i=D.getRootDomain(),n=31536e4;document.cookie=i?`${e}=${t}; path=/${s}; domain=${i}; max-age=${n}; SameSite=Lax`:`${e}=${t}; path=/${s}; max-age=${n}; SameSite=Lax`}return"hello_session"===e&&Tt.eventEmitter.dispatch("session-set",t),"hello_utm"===e&&Tt.eventEmitter.dispatch("utm-set",t),t}static get(e){return"undefined"!=typeof document?document.cookie.match("(^|;)\\s*"+e+"\\s*=\\s*([^;]+)")?.pop():void 0}static delete(e){if("undefined"!=typeof document){const t=D.getRootDomain(),s="https:"===window.location.protocol?"; Secure":"";document.cookie=t?`${e}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT${s}; domain=${t}; SameSite=Lax`:`${e}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT${s}; SameSite=Lax`}}}class N{constructor(){this.save(N.paramsFrom(window.location.search))}static paramsFrom(e){const t=new URLSearchParams(e);return Object.fromEntries(Object.entries({source:t.get("utm_source"),medium:t.get("utm_medium"),campaign:t.get("utm_campaign"),term:t.get("utm_term"),content:t.get("utm_content")}).filter(([e,t])=>t))}save(e){if(!e.source||!e.medium)return;const t=Object.fromEntries(Object.entries(e).filter(([e,t])=>t));t.observed_at=(new Date).toISOString(),_.set("hello_utm",JSON.stringify(t))}get current(){try{return JSON.parse(_.get("hello_utm"))||{}}catch(e){return{}}}}class D{constructor(e=null){this.utm=new N,this._url=e}get url(){return null!==this._url&&void 0!==this._url?this._url:window.location.href}get title(){return document.title}get path(){if(this._url)try{return new URL(this._url).pathname}catch(e){return"/"}return window.location.pathname}get utmParams(){return this.utm.current}get trackingData(){return{page:{url:this.url,title:this.title,path:this.path},utm_params:this.utmParams}}get domain(){try{const e=this.url;if(!e)return null;const t=new URL(e).hostname;return D.getRootDomain(t)}catch(e){return null}}static getRootDomain(e=null){try{if(!e){if("undefined"==typeof window||!window.location?.hostname)return null;e=window.location.hostname}const t=e.split(".");if(t.length<=1)return e;const s=["myshopify.com","vtexcommercestable.com.br","myvtex.com","wixsite.com"];for(const e of s){const s=e.split(".");if(t.slice(-s.length).join(".")===e&&t.length>s.length)return`.${t.slice(-(s.length+1)).join(".")}`}const i=t[t.length-1],n=t[t.length-2];return t.length>2&&2===i.length&&n.length<=3?`.${t.slice(-3).join(".")}`:`.${t.slice(-2).join(".")}`}catch(e){return null}}}class F{static#n;static#r;static#a;static get session(){return this.#n}static get ackPayload(){return{utm_params:this.#a?.utmParams||{}}}static set session(e){const t=_.get("hello_session");return this.#n=e,_.set("hello_session",e),t!==e&&_.delete("hello_session_ack_at"),_.get("hello_session_ack_at")||(k.acks.send(this.ackPayload),_.set("hello_session_ack_at",(new Date).toISOString())),this.#n}static initialize(e=new D){this.#a=e,this.#r=new b,this.session=this.#r.session||f.session||_.get("hello_session"),!this.session&&f.autoGenerateSession&&(this.session=crypto.randomUUID())}}class R{static build(e){const t=document.createElement("article"),s=document.createElement("label"),i=document.createElement("input");s.innerText=e.label,i.type=e.type,i.required=e.required,i.placeholder=e.placeholder,["first_name","last_name"].includes(e.kind)?(i.type="text",i.id=i.name=e.kind,s.setAttribute("for",e.kind)):(i.type=e.type,"email"===e.type?(i.id=i.name="email",s.setAttribute("for","email")):"tel"===i.type?(i.id=i.name="phone",s.setAttribute("for","phone"),i.value=`+${Tt.business.country.prefix}`,i.setAttribute("data-default-value",`+${Tt.business.country.prefix}`)):(i.name=i.id=`property_by_id[${e.property}]`,s.setAttribute("for",`property_by_id[${e.property}]`)));const n=document.createElement("main");n.appendChild(s),n.appendChild(i),t.appendChild(n),t.setAttribute("data-hellotext--form-target","inputContainer"),i.setAttribute("data-hellotext--form-target","input");const r=document.createElement("div");return r.setAttribute("data-error-container",""),t.appendChild(r),t}}class B{static build(){const e=document.createElement("div");return e.innerHTML=this.#o(),e.firstElementChild}static#o(){const e=`https://www.hellotext.com?hello_session=${Tt.session}`;return`\n
\n ${Tt.business.locale.white_label.powered_by}\n\n \n \n Hellotext\n \n \n \n
\n `}}function V(e,t){(null==t||t>e.length)&&(t=e.length);for(var s=0,i=Array(t);s2?s-2:0),n=2;n1?t-1:0),i=1;i1?s-1:0),n=1;n2&&void 0!==arguments[2]?arguments[2]:ne;if(U&&U(e,null),!ie(t))return e;let i=t.length;for(;i--;){let n=t[i];if("string"==typeof n){const e=s(n);e!==n&&(q(t)||(t[i]=e),n=e)}e[n]=!0}return e}function Te(e){for(let t=0;t/g),Re=H(/\${[\w\W]*/g),Be=H(/^data-[\-\w.\u00B7-\uFFFF]+$/),Ve=H(/^aria-[\-\w]+$/),je=H(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),$e=H(/^(?:\w+script|data):/i),Ue=H(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),qe=H(/^html$/i),ze=H(/^[a-z][.\w]*(-[.\w]+)+$/i),We=H(/<[/\w!]/g),Ke=H(/<[/\w]/g),He=H(/<\/no(script|embed|frames)/i),Ge=H(/\/>/i),Je=["style","script","xmp","iframe","noembed","noframes","plaintext","noscript"],Ye=K(we({},Je)),Ze=function(){const e={};return X(Je,t=>{e[t]=H(new RegExp("])","i"))}),K(e)}(),Xe=function(){return"undefined"==typeof window?null:window},Qe=function(e,t,s,i){return me(e,t)&&ie(e[t])?we(i.base?Se(i.base):{},e[t],i.transform):s},et=function(e,t,s){const i=me(e,t)?e[t]:void 0;return i&&"object"==typeof i?Se(i):s()};var tt=function e(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Xe();const s=t=>e(t);if(s.version="3.4.15",s.removed=[],!t||!t.document||9!==t.document.nodeType||!t.Element)return s.isSupported=!1,s;let i=t.document;const n=i,r=n.currentScript;t.DocumentFragment;const a=t.HTMLTemplateElement,o=t.Node,c=t.Element,l=t.NodeFilter;void 0===t.NamedNodeMap&&(t.NamedNodeMap||t.MozNamedAttrMap),t.HTMLFormElement;const h=t.DOMParser,u=t.trustedTypes,d=c.prototype,p=Ce(d,"cloneNode"),m=Ce(d,"remove"),g=Ce(d,"removeAttributeNode"),f=Ce(d,"nextSibling"),y=Ce(d,"childNodes"),b=Ce(d,"parentNode"),v=Ce(d,"shadowRoot"),w=Ce(d,"attributes"),T=o&&o.prototype?Ce(o.prototype,"nodeType"):null,S=o&&o.prototype?Ce(o.prototype,"nodeName"):null,C=o&&o.prototype?Ce(o.prototype,"ownerDocument"):null,A=function(e){return T?T(e):e.nodeType},E=function(e){return S?S(e):e.nodeName};if("function"==typeof a){const e=i.createElement("template");e.content&&e.content.ownerDocument&&(i=e.content.ownerDocument)}let O,x,M="",I=!1,k=0;const P=function(){if(k>0)throw ye('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.')},L=function(e){P(),k++;try{return O.createHTML(e)}finally{k--}},_=i,N=_.implementation,D=_.createNodeIterator,F=_.createDocumentFragment,R=_.getElementsByTagName,B=n.importNode;let V={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};s.isSupported="function"==typeof $&&"function"==typeof b&&N&&void 0!==N.createHTMLDocument;const j=De,U=Fe,q=Re,z=Be,W=Ve,J=$e,Y=Ue,Z=ze;let be=je,ve=null;const Te=we({},[...Ae,...Ee,...Oe,...Me,...ke]);let Je=null;const tt=we({},[...Pe,...Le,..._e,...Ne]);let st=Object.seal(G(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),it=null,nt=null;const rt=Object.seal(G(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let at=!0,ot=!0,ct=!1,lt=!0,ht=!1,ut=!0,dt=!1,pt=!1,mt=null,gt=null,ft=!1,yt=!1,bt=!1,vt=!1,wt=!0,Tt=!1;const St="user-content-";let Ct=!0,At=!1,Et={},Ot=null;const xt=we({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","selectedcontent","style","svg","template","thead","title","video","xmp"]);let Mt=null;const It=we({},["audio","video","img","source","image","track"]);let kt=null;const Pt=we({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Lt="http://www.w3.org/1998/Math/MathML",_t="http://www.w3.org/2000/svg",Nt="http://www.w3.org/1999/xhtml";let Dt=Nt,Ft=!1,Rt=null;const Bt=we({},[Lt,_t,Nt],re),Vt=K(["mi","mo","mn","ms","mtext"]);let jt=we({},Vt);const $t=K(["annotation-xml"]);let Ut=we({},$t);const qt=we({},["title","style","font","a","script"]);let zt=null;const Wt=["application/xhtml+xml","text/html"];let Kt=null,Ht=null;const Gt=i.createElement("form"),Jt=function(e){return e instanceof RegExp||e instanceof Function},Yt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(Ht&&Ht===e)return;e&&"object"==typeof e||(e={}),e=Se(e),zt=-1===Wt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,Kt="application/xhtml+xml"===zt?re:ne,ve=Qe(e,"ALLOWED_TAGS",Te,{transform:Kt}),Je=Qe(e,"ALLOWED_ATTR",tt,{transform:Kt}),Rt=Qe(e,"ALLOWED_NAMESPACES",Bt,{transform:re}),kt=Qe(e,"ADD_URI_SAFE_ATTR",Pt,{transform:Kt,base:Pt}),Mt=Qe(e,"ADD_DATA_URI_TAGS",It,{transform:Kt,base:It}),Ot=Qe(e,"FORBID_CONTENTS",xt,{transform:Kt}),it=Qe(e,"FORBID_TAGS",Se({}),{transform:Kt}),nt=Qe(e,"FORBID_ATTR",Se({}),{transform:Kt}),Et=!!me(e,"USE_PROFILES")&&(e.USE_PROFILES&&"object"==typeof e.USE_PROFILES?Se(e.USE_PROFILES):e.USE_PROFILES),at=!1!==e.ALLOW_ARIA_ATTR,ot=!1!==e.ALLOW_DATA_ATTR,ct=e.ALLOW_UNKNOWN_PROTOCOLS||!1,lt=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ht=e.SAFE_FOR_TEMPLATES||!1,ut=!1!==e.SAFE_FOR_XML,dt=e.WHOLE_DOCUMENT||!1,yt=e.RETURN_DOM||!1,bt=e.RETURN_DOM_FRAGMENT||!1,vt=e.RETURN_TRUSTED_TYPE||!1,ft=e.FORCE_BODY||!1,wt=!1!==e.SANITIZE_DOM,Tt=e.SANITIZE_NAMED_PROPS||!1,Ct=!1!==e.KEEP_CONTENT,At=e.IN_PLACE||!1,be=function(e){try{return fe(e,""),!0}catch(e){return!1}}(e.ALLOWED_URI_REGEXP)?e.ALLOWED_URI_REGEXP:je,Dt="string"==typeof e.NAMESPACE?e.NAMESPACE:Nt,jt=et(e,"MATHML_TEXT_INTEGRATION_POINTS",()=>we({},Vt)),Ut=et(e,"HTML_INTEGRATION_POINTS",()=>we({},$t));const t=et(e,"CUSTOM_ELEMENT_HANDLING",()=>G(null));if(st=G(null),me(t,"tagNameCheck")&&Jt(t.tagNameCheck)&&(st.tagNameCheck=t.tagNameCheck),me(t,"attributeNameCheck")&&Jt(t.attributeNameCheck)&&(st.attributeNameCheck=t.attributeNameCheck),me(t,"allowCustomizedBuiltInElements")&&"boolean"==typeof t.allowCustomizedBuiltInElements&&(st.allowCustomizedBuiltInElements=t.allowCustomizedBuiltInElements),H(st),ht&&(ot=!1),bt&&(yt=!0),Et&&(ve=we({},ke),Je=G(null),!0===Et.html&&(we(ve,Ae),we(Je,Pe)),!0===Et.svg&&(we(ve,Ee),we(Je,Le),we(Je,Ne)),!0===Et.svgFilters&&(we(ve,Oe),we(Je,Le),we(Je,Ne)),!0===Et.mathMl&&(we(ve,Me),we(Je,_e),we(Je,Ne))),rt.tagCheck=null,rt.attributeCheck=null,me(e,"ADD_TAGS")&&("function"==typeof e.ADD_TAGS?rt.tagCheck=e.ADD_TAGS:ie(e.ADD_TAGS)&&(ve===Te&&(ve=Se(ve)),we(ve,e.ADD_TAGS,Kt))),me(e,"ADD_ATTR")&&("function"==typeof e.ADD_ATTR?rt.attributeCheck=e.ADD_ATTR:ie(e.ADD_ATTR)&&(Je===tt&&(Je=Se(Je)),we(Je,e.ADD_ATTR,Kt))),me(e,"ADD_FORBID_CONTENTS")&&ie(e.ADD_FORBID_CONTENTS)&&(Ot===xt&&(Ot=Se(Ot)),we(Ot,e.ADD_FORBID_CONTENTS,Kt)),Ct&&(ve["#text"]=!0),dt&&we(ve,["html","head","body"]),ve.table&&(we(ve,["tbody"]),delete it.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw ye('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw ye('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const t=O;O=e.TRUSTED_TYPES_POLICY;try{M=L("")}catch(e){throw O=t,e}}else null===e.TRUSTED_TYPES_POLICY?(O=void 0,M=""):(void 0===O&&(I||(x=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let s=null;const i="data-tt-policy-suffix";t&&t.hasAttribute(i)&&(s=t.getAttribute(i));const n="dompurify"+(s?"#"+s:"");try{return e.createPolicy(n,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+n+" could not be created."),null}}(u,r),I=!0),O=x),O&&"string"==typeof M&&(M=L("")));K&&K(e),Ht=e},Zt=we({},[...Ee,...Oe,...xe]),Xt=we({},[...Me,...Ie]),Qt=function(e){te(s.removed,{element:e});try{b(e).removeChild(e)}catch(t){if(m(e),!b(e))throw ye("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},es=function(e,t,s){try{g(e,t)}catch(t){try{e.removeAttribute(s)}catch(e){}}},ts=function(e){ns(e);const t=y(e);if(t){const e=[];X(t,t=>{te(e,t)}),X(e,e=>{try{m(e)}catch(e){}})}const s=w(e);if(s)for(let t=s.length-1;t>=0;--t){const i=s[t],n=i&&i.name;"string"==typeof n&&es(e,i,n)}},ss=function(e,t,i){if(!i)try{i=t.getAttributeNode(e)}catch(e){i=null}te(s.removed,{attribute:i||null,from:t});try{i?g(t,i):t.removeAttribute(e)}catch(s){try{t.removeAttribute(e)}catch(e){}}if("is"===e)if(yt||bt)try{Qt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},is=function(e){const t=w(e);if(t)for(let s=t.length-1;s>=0;--s){const i=t[s],n=i&&i.name;"string"!=typeof n||Je[Kt(n)]||es(e,i,n)}},ns=function(e){const t=[e];for(;t.length>0;){const e=t.pop();1===A(e)&&is(e);const s=y(e);if(s)for(let e=s.length-1;e>=0;--e)t.push(s[e])}},rs=function(e,t){return!!ut&&("patchsrc"===e||"for"===e&&"label"!==t&&"output"!==t)},as=function(e){let t=null,s=null;if(ft)e=""+e;else{const t=ae(e,/^[\r\n\t ]+/);s=t&&t[0]}"application/xhtml+xml"===zt&&Dt===Nt&&(e=''+e+"");const n=O?L(e):e;if(Dt===Nt)try{t=(new h).parseFromString(n,zt)}catch(e){}if(!t||!t.documentElement){t=N.createDocument(Dt,"template",null);try{t.documentElement.innerHTML=Ft?M:n}catch(e){}}const r=t.body||t.documentElement;return e&&s&&r.insertBefore(i.createTextNode(s),r.childNodes[0]||null),Dt===Nt?R.call(t,dt?"html":"body")[0]:dt?t.documentElement:r},os=function(e){const t=C?C(e):e.ownerDocument;return D.call(t||e,e,l.SHOW_ELEMENT|l.SHOW_COMMENT|l.SHOW_TEXT|l.SHOW_PROCESSING_INSTRUCTION|l.SHOW_CDATA_SECTION,null)},cs=function(e){return e=oe(e,j," "),e=oe(e,U," "),oe(e,q," ")},ls=function(e){var t;e.normalize();const s=C?C(e):e.ownerDocument,i=D.call(s||e,e,l.SHOW_TEXT|l.SHOW_COMMENT|l.SHOW_CDATA_SECTION|l.SHOW_PROCESSING_INSTRUCTION,null);let n=i.nextNode();for(;n;)n.data=cs(n.data),n=i.nextNode();const r=null===(t=e.querySelectorAll)||void 0===t?void 0:t.call(e,"template");r&&X(r,e=>{us(e.content)&&ls(e.content)})},hs=function(e){const t=S?S(e):null;return"string"==typeof t&&"form"===Kt(t)&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||e.attributes!==w(e)||"function"!=typeof e.removeAttribute||"function"!=typeof e.removeAttributeNode||"function"!=typeof e.getAttributeNode||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes||e.nodeType!==T(e)||e.childNodes!==y(e))},us=function(e){if(!T||"object"!=typeof e||null===e)return!1;try{return 11===T(e)}catch(e){return!1}},ds=function(e){if(!T||"object"!=typeof e||null===e)return!1;try{return"number"==typeof T(e)}catch(e){return!1}};function ps(e,t,i){0!==e.length&&X(e,e=>{e.call(s,t,i,Ht)})}const ms=function(e,t){if(e instanceof RegExp)return fe(e,t);if(e instanceof Function){for(var s=arguments.length,i=new Array(s>2?s-2:0),n=2;n=0;--n){const r=e===s?p(i[n],!0):i[n];t.insertBefore(r,f(e))}}return Qt(e),!0}(e,i,t);return!1===s&&ps(V.afterSanitizeElements,e,null),s}if(1===A(e)&&!function(e){let t=b(e);t&&t.tagName||(t={namespaceURI:Dt,tagName:"template"});const s=ne(e.tagName),i=ne(t.tagName);return!!Rt[e.namespaceURI]&&(e.namespaceURI===_t?function(e,t,s){return t.namespaceURI===Nt?"svg"===e:t.namespaceURI===Lt?"svg"===e&&("annotation-xml"===s||jt[s]):Boolean(Zt[e])}(s,t,i):e.namespaceURI===Lt?function(e,t,s){return t.namespaceURI===Nt?"math"===e:t.namespaceURI===_t?"math"===e&&Ut[s]:Boolean(Xt[e])}(s,t,i):e.namespaceURI===Nt?function(e,t,s){return!(t.namespaceURI===_t&&!Ut[s])&&!(t.namespaceURI===Lt&&!jt[s])&&!Xt[e]&&(qt[e]||!Zt[e])}(s,t,i):!("application/xhtml+xml"!==zt||!Rt[e.namespaceURI]))}(e))return Qt(e),!0;if(("noscript"===i||"noembed"===i||"noframes"===i)&&fe(He,e.innerHTML))return Qt(e),!0;if(ht&&3===e.nodeType){const t=cs(e.textContent);e.textContent!==t&&(te(s.removed,{element:e.cloneNode()}),e.textContent=t)}return ps(V.afterSanitizeElements,e,null),!1},bs=function(e,t,s){if(nt[t])return!1;if(rs(t,e))return!1;if(wt&&("id"===t||"name"===t)&&(s in i||s in Gt))return!1;const n=Je[t]||rt.attributeCheck instanceof Function&&rt.attributeCheck(t,e);return!(!ot||!fe(z,t))||!(!at||!fe(W,t))||(n?!(!kt[t]&&!fe(be,oe(s,Y,""))&&("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==ce(s,"data:")||!Mt[e])&&(!ct||fe(J,oe(s,Y,"")))&&s):ws(e)&&ms(st.tagNameCheck,e)&&ms(st.attributeNameCheck,t,e)||"is"===t&&st.allowCustomizedBuiltInElements&&ms(st.tagNameCheck,s))},vs=we({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),ws=function(e){return!vs[ne(e)]&&fe(Z,e)},Ts=function(e,t,s,i){if(O&&"object"==typeof u&&"function"==typeof u.getAttributeType&&!s)switch(u.getAttributeType(e,t)){case"TrustedHTML":return L(i);case"TrustedScriptURL":return function(e){P(),k++;try{return O.createScriptURL(e)}finally{k--}}(i)}return i},Ss=function(e,t,s,i){try{return s?e.setAttributeNS(s,t,i):e.setAttribute(t,i),!hs(e)||(Qt(e),!1)}catch(s){return ss(t,e),!1}},Cs=function(e){ps(V.beforeSanitizeAttributes,e,null);const t=e.attributes;if(!t||hs(e))return;Je=gs(V.uponSanitizeAttribute,Je,tt,gt);const i={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Je,forceKeepAttr:void 0};let n=t.length;const r=Kt(e.nodeName);for(;n--;){const a=t[n],o=a.name,c=a.namespaceURI,l=a.value,h=Kt(o),u=l;let d="value"===o?u:le(u),p=!1;i.attrName=h,i.attrValue=d,i.keepAttr=!0,i.forceKeepAttr=void 0,ps(V.uponSanitizeAttribute,e,i),d=i.attrValue,!Tt||"id"!==h&&"name"!==h||0===ce(d,St)||(ss(o,e,a),d=St+d,p=!0),ut&&fe(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,d)||"attributename"===h&&ae(d,"href")?ss(o,e,a):i.forceKeepAttr||(i.keepAttr&&(lt||!fe(Ge,d))?(ht&&(d=cs(d)),bs(r,h,d)?(d=Ts(r,h,c,d),d!==u&&Ss(e,o,c,d)&&p&&ee(s.removed)):ss(o,e,a)):ss(o,e,a))}ps(V.afterSanitizeAttributes,e,null)},As=function(e){let t=null;const s=os(e);for(ps(V.beforeSanitizeShadowDOM,e,null);t=s.nextNode();)if(ps(V.uponSanitizeShadowNode,t,null),ys(t,e),Cs(t),us(t.content)&&As(t.content),1===A(t)){const e=v(t);us(e)&&(Es(e),As(e))}ps(V.afterSanitizeShadowDOM,e,null)},Es=function(e){const t=[{node:e,shadow:null}];for(;t.length>0;){const e=t.pop();if(e.shadow){As(e.shadow);continue}const s=e.node,i=1===A(s),n=y(s);if(n)for(let e=n.length-1;e>=0;--e)t.push({node:n[e],shadow:null});if(i){const e=S?S(s):null;if("string"==typeof e&&"template"===Kt(e)){const e=s.content;us(e)&&t.push({node:e,shadow:null})}}if(i){const e=v(s);us(e)&&t.push({node:null,shadow:e},{node:e,shadow:null})}}};return s.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=null,r=null,a=null,o=null;if(Ft=!e,Ft&&(e="\x3c!--\x3e"),"string"!=typeof e&&!ds(e)&&"string"!=typeof(e=function(e){switch(typeof e){case"string":return e;case"number":return he(e);case"boolean":return ue(e);case"bigint":return de?de(e):"0";case"symbol":return pe?pe(e):"Symbol()";case"undefined":default:return ge(e);case"function":case"object":{if(null===e)return ge(e);const t=e,s=Ce(t,"toString");if("function"==typeof s){const e=s(t);return"string"==typeof e?e:ge(e)}return ge(e)}}}(e)))throw ye("dirty is not a string, aborting");if(!s.isSupported)return e;pt?(ve=mt,Je=gt):Yt(t),(V.uponSanitizeElement.length>0||V.uponSanitizeAttribute.length>0)&&(ve=Se(ve)),V.uponSanitizeAttribute.length>0&&(Je=Se(Je)),s.removed=[];const c=At&&"string"!=typeof e&&ds(e);if(c){!function(e){if(!ut)return;const t=[e];for(;t.length>0;){const e=t.pop(),s=A(e);if(7===s||8===s&&fe(Ke,e.data)){try{m(e)}catch(e){}continue}if(1===s){const t=e,s=Kt(E(e));try{t.hasAttribute&&t.hasAttribute("patchsrc")&&t.removeAttribute("patchsrc"),t.hasAttribute&&t.hasAttribute("for")&&rs("for",s)&&t.removeAttribute("for")}catch(e){}}const i=y(e);if(i)for(let e=i.length-1;e>=0;--e)t.push(i[e])}}(e);const t=E(e);if("string"==typeof t){const s=Kt(t);if(!ve[s]||it[s])throw ts(e),ye("root node is forbidden and cannot be sanitized in-place")}if(hs(e))throw ts(e),ye("root node is clobbered and cannot be sanitized in-place");try{Es(e)}catch(t){throw ts(e),t}}else if(ds(e))i=as("\x3c!----\x3e"),r=i.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?i=r:i.appendChild(r),Es(i);else{if(!yt&&!ht&&!dt&&-1===e.indexOf("<"))return O&&vt?L(e):e;if(i=as(e),!i)return yt?null:vt?M:""}i&&ft&&Qt(i.firstChild);const l=c?e:i;try{const e=os(l);for(;a=e.nextNode();)ys(a,l),Cs(a),us(a.content)&&As(a.content)}catch(t){throw c&&(ts(e),X(s.removed,e=>{e.element&&ns(e.element)})),t}if(c)return X(s.removed,e=>{e.element&&ns(e.element)}),ht&&ls(e),e;if(yt){if(ht&&ls(i),bt)for(o=F.call(i.ownerDocument);i.firstChild;)o.appendChild(i.firstChild);else o=i;return(Je.shadowroot||Je.shadowrootmode)&&(o=B.call(n,o,!0)),o}let h=dt?i.outerHTML:i.innerHTML;return dt&&ve["!doctype"]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&fe(qe,i.ownerDocument.doctype.name)&&(h="\n"+h),ht&&(h=cs(h)),O&&vt?L(h):h},s.setConfig=function(){Yt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),pt=!0,mt=ve,gt=Je},s.clearConfig=function(){Ht=null,pt=!1,mt=null,gt=null,O=x,M=""},s.isValidAttribute=function(e,t,s){Ht||Yt({});const i=Kt(e),n=Kt(t);return bs(i,n,s)},s.addHook=function(e,t){"function"==typeof t&&me(V,e)&&te(V[e],t)},s.removeHook=function(e,t){if(me(V,e)){if(void 0!==t){const s=Q(V[e],t);return-1===s?void 0:se(V[e],s,1)[0]}return ee(V[e])}},s.removeHooks=function(e){me(V,e)&&(V[e]=[])},s.removeAllHooks=function(){V={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},s}();const st={ADD_ATTR:["target"],ALLOW_DATA_ATTR:!1,RETURN_DOM_FRAGMENT:!0,USE_PROFILES:{html:!0}},it={ADD_ATTR:["target"],ADD_TAGS:["hellotext-icon"],RETURN_DOM_FRAGMENT:!0,USE_PROFILES:{html:!0,svg:!0}};function nt(e,t){const s=tt.sanitize(e,t);return s.querySelectorAll('a[target="_blank"]').forEach(e=>{const t=new Set(e.rel.split(/\s+/).filter(Boolean));t.add("noopener"),t.add("noreferrer"),e.rel=Array.from(t).join(" ")}),s}function rt(e,t){e.replaceChildren(function(e){return nt(e,st)}(t))}class at{constructor(e,t=null,s=Tt.visitBusinessId){this.data=e,this.visitBusinessId=s,this.element=t||document.querySelector(`[data-hello-form="${this.id}"]`)||document.createElement("form")}async mount({ifCompleted:e=!0}={}){if(e&&this.hasBeenCompleted)return this.element?.remove(),Tt.eventEmitter.dispatch("form:completed",{id:this.id,...JSON.parse(localStorage.getItem(`hello-form-${this.id}`))});const t=this.data.steps[0];this.buildHeader(t.header),this.buildInputs(t.inputs),this.buildButton(t.button),this.buildFooter(t.footer),this.elementAttributes.forEach(e=>{this.element.setAttribute(e.name,e.value)}),document.contains(this.element)||document.body.appendChild(this.element),Tt.business.features.white_label||this.element.prepend(B.build())}buildHeader(e){const t=this.#c("[data-form-header]","header");rt(t,e.content),this.element.querySelector("[data-form-header]")?this.element.querySelector("[data-form-header]").replaceWith(t):this.element.prepend(t)}buildInputs(e){const t=this.#c("[data-form-inputs]","main");e.map(e=>R.build(e)).forEach(e=>t.appendChild(e)),this.element.querySelector("[data-form-inputs]")?this.element.querySelector("[data-form-inputs]").replaceWith(t):this.element.querySelector("[data-form-header]").insertAdjacentHTML("afterend",t.outerHTML)}buildButton(e){const t=this.#c("[data-form-button]","button");t.innerText=e.text,t.setAttribute("data-action","click->hellotext--form#submit"),t.setAttribute("data-hellotext--form-target","button"),this.element.querySelector("[data-form-button]")?this.element.querySelector("[data-form-button]").replaceWith(t):this.element.querySelector("[data-form-inputs]").insertAdjacentHTML("afterend",t.outerHTML)}buildFooter(e){const t=this.#c("[data-form-footer]","footer");rt(t,e.content),this.element.querySelector("[data-form-footer]")?this.element.querySelector("[data-form-footer]").replaceWith(t):this.element.appendChild(t)}markAsCompleted(e){const t={state:"completed",id:this.id,data:e,completedAt:(new Date).getTime()};localStorage.setItem(`hello-form-${this.id}`,JSON.stringify(t)),Tt.visitBusinessId===this.visitBusinessId&&Tt.recordActivity("form.completed"),Tt.eventEmitter.dispatch("form:completed",t)}get hasBeenCompleted(){return null!==localStorage.getItem(`hello-form-${this.id}`)}get id(){return this.data.id}get localeAuthKey(){const e=this.data.steps[0];return e.inputs.some(e=>"email"===e.kind)&&e.inputs.some(e=>"phone"===e.kind)?"phone_and_email":e.inputs.some(e=>"email"===e.kind)?"email":e.inputs.some(e=>"phone"===e.kind)?"phone":"none"}get elementAttributes(){return[{name:"data-controller",value:"hellotext--form"},{name:"data-hello-form",value:this.id},{name:"data-hellotext--form-data-value",value:JSON.stringify(this.data)}]}#c(e,t){const s=this.element.querySelector(e);if(s)return s.cloneNode(!0);const i=document.createElement(t);return i.setAttribute(e.replace("[","").replace("]",""),""),i}}class ot extends Error{constructor(){super("You need to initialize before tracking events. Call Hellotext.initialize and pass your public business id"),this.name="NotInitializedError"}}class ct{constructor(){this.forms=[],this.visitBusinessId=Tt.visitBusinessId,this.initializationVersion=Tt.initializationVersion,this.includes=this.includes.bind(this),this.excludes=this.excludes.bind(this),this.add=this.add.bind(this),"undefined"!=typeof MutationObserver&&(this.mutationObserver=new MutationObserver(this.formMutationObserver.bind(this)),this.mutationObserver.observe(document.body,{childList:!0,subtree:!0}))}collectExistingFormsOnPage(){Array.from(document.querySelectorAll("[data-hello-form]")).length>0&&this.collect()}formMutationObserver(e){e.find(e=>"childList"===e.type&&e.addedNodes.length>0)&&Array.from(document.querySelectorAll("[data-hello-form]")).length>0&&this.collect()}async collect(){if(Tt.notInitialized)throw new ot;if(this.fetching)return;if(!this.current)return;if("undefined"==typeof document||!("querySelectorAll"in document))return console.warn("Document is not defined, collection is not possible. Please make sure to initialize the library after the document is loaded.");const e=this.#l;if(0===e.length)return;const t=e.map(e=>T.get(e).then(e=>e.json()));this.fetching=!0;try{const e=await Promise.all(t);if(!this.current)return;e.forEach(this.add),Tt.eventEmitter.dispatch("forms:collected",this),f.forms.autoMount&&this.forms.forEach(e=>e.mount())}finally{this.fetching=!1}}forEach(e){this.forms.forEach(e)}map(e){return this.forms.map(e)}add(e){this.includes(e.id)||(Tt.business.data||(Tt.business.setData(e.business),Tt.business.setLocale(o.toString())),Tt.business.enabledWhitelist||console.warn("No whitelist has been configured. It is advised to whitelist the domain to avoid bots from submitting forms."),this.forms.push(new at(e,null,this.visitBusinessId)))}getById(e){return this.forms.find(t=>t.id===e)}getByIndex(e){return this.forms[e]}includes(e){return this.forms.some(t=>t.id===e)}excludes(e){return!this.includes(e)}get length(){return this.forms.length}get current(){return Tt.visitBusinessId===this.visitBusinessId&&Tt.initializationVersion===this.initializationVersion}get#l(){return Array.from(document.querySelectorAll("[data-hello-form]")).map(e=>e.dataset.helloForm).filter(this.excludes)}}class lt{constructor(e){this.publicKey=e.public_key,this.serviceWorkerUrl=f.push.serviceWorkerUrl,this.channelId=f.push.channelId,this.ready=null,this.registrationPromise=null,this.subscribePromise=null,this.unsubscribePromise=null,this.syncPromise=null,this.subscription=null,this.retryTimeout=null,this.retryAttempts=0,this.disposed=!1}initialize(){return this.ready=this.restoreSubscription(),this.ready}async restoreSubscription(){const e=await this.getRegistration(),t=await e.pushManager.getSubscription();!this.disposed&&!this.unsubscribePromise&&t&&this.owns(t)&&(this.subscription=t,await this.sync())}subscribe(){if(this.disposed)return Promise.resolve();if(this.unsubscribePromise)return Promise.reject(new Error("Push unsubscribe is in progress"));if(this.subscribePromise)return this.subscribePromise;const e="default"===Notification.permission?Notification.requestPermission():Promise.resolve(Notification.permission);return this.subscribePromise=this.createSubscription(e).finally(()=>{this.subscribePromise=null}),this.subscribePromise}async createSubscription(e){if("granted"!==await e)throw new Error("Push permission was not granted");const t=await this.getRegistration();if(this.disposed)return;let s=await t.pushManager.getSubscription();if(!this.disposed){if(s&&!this.owns(s))throw new Error("The existing Push subscription belongs to a different application");if(s||=await t.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:this.applicationServerKey}),!this.disposed)return this.subscription=s,this.sync()}}unsubscribe(){return this.disposed?Promise.resolve():(this.unsubscribePromise||(this.clearRetryTimeout(),this.unsubscribePromise=this.removeSubscription().finally(()=>{this.unsubscribePromise=null})),this.unsubscribePromise)}async removeSubscription(){await(this.ready?.catch(()=>{})),await(this.subscribePromise?.catch(()=>{})),await(this.syncPromise?.catch(()=>{}));const e=await this.getRegistration(),t=await e.pushManager.getSubscription()||this.subscription;if(this.disposed)return;if(!t)return null;if(!this.owns(t))throw new Error("The existing Push subscription belongs to a different application");this.subscription=t;const s=await k.pushIdentities.destroy({subscription:t.toJSON()});return s.failed?s:this.disposed?void 0:(await t.unsubscribe(),this.subscription=null,this.clearRetryTimeout(),s)}sync(){return this.disposed?Promise.resolve():(this.syncPromise||(this.clearRetryTimeout(),this.syncPromise=this.registerIdentity().finally(()=>{this.syncPromise=null})),this.syncPromise)}async registerIdentity(){try{const e=await k.pushIdentities.create({subscription:this.subscription.toJSON(),...this.channelId?{channel_id:this.channelId}:{}});return e.succeeded?this.retryAttempts=0:this.scheduleRetry(),e}catch(e){throw this.scheduleRetry(),e}}scheduleRetry(){this.disposed||this.unsubscribePromise||this.retryTimeout||this.retryAttempts>=3||(this.retryTimeout=setTimeout(()=>{this.retryTimeout=null,this.sync().catch(()=>{})},1e3*2**this.retryAttempts),this.retryAttempts+=1)}clearRetryTimeout(){clearTimeout(this.retryTimeout),this.retryTimeout=null}dispose(){this.disposed=!0,this.clearRetryTimeout()}get subscribed(){return!!this.subscription}get applicationServerKey(){const e=this.publicKey.replace(/-/g,"+").replace(/_/g,"/");return Uint8Array.from(atob(e.padEnd(4*Math.ceil(e.length/4),"=")),e=>e.charCodeAt(0))}owns(e){const t=e.options?.applicationServerKey;if(!t)return!1;const s=new Uint8Array(t),i=this.applicationServerKey;return s.length===i.length&&s.every((e,t)=>e===i[t])}getRegistration(){return this.registrationPromise||(this.registrationPromise=this.loadRegistration().catch(e=>{throw this.registrationPromise=null,e})),this.registrationPromise}async loadRegistration(){if(this.serviceWorkerUrl){const e=await navigator.serviceWorker.register(this.serviceWorkerUrl);!e.active||e.installing||e.waiting||await e.update();const t=e.installing||e.waiting||e.active;return"activated"===t?.state?e:new Promise((s,i)=>{const n=setTimeout(()=>r(new Error("Push service worker did not become active")),1e4),r=r=>{clearTimeout(n),t?.removeEventListener("statechange",a),r?i(r):s(e)},a=()=>{"activated"===t?.state&&r(),"redundant"===t?.state&&r(new Error("Push service worker installation failed"))};t?.addEventListener("statechange",a),a()})}return new Promise((e,t)=>{const s=setTimeout(()=>t(new Error("Push service worker is not available")),1e4);navigator.serviceWorker.ready.then(t=>{clearTimeout(s),e(t)},e=>{clearTimeout(s),t(e)})})}static get supported(){return"undefined"!=typeof window&&!0===window.isSecureContext&&"undefined"!=typeof navigator&&"serviceWorker"in navigator&&"undefined"!=typeof PushManager&&"undefined"!=typeof Notification}}class ht{static async load(e){const t=new ht({id:e,html:await k.webchats.get(e)});return t.rendered=t.render(),t}constructor(e){this.data=e,this.mounted=!1,this.rendered=Promise.resolve(!1)}async render(){return this.applyBehaviourOverride(),await this.stylesheetLoaded?(this.containerToAppendTo.appendChild(this.data.html),this.markCoexistingWidgets(),this.mounted=!0,!0):(console.warn("Hellotext webchat was not mounted because its stylesheet failed to load."),!1)}applyBehaviourOverride(){f.webchat.hasBehaviourOverride&&f.webchat.behaviour&&this.data.html.setAttribute("data-hellotext--webchat-behaviour-value",JSON.stringify(this.serializedBehaviour))}get serializedBehaviour(){const e=f.webchat.behaviour;return{trigger:this.serializeTrigger(e.trigger),delay_seconds:e.delaySeconds,first_visit_only:e.firstVisitOnly,once_per_session:e.oncePerSession}}serializeTrigger(e){return"onLoad"===e?"on_load":"onClick"===e?"on_click":e}get containerToAppendTo(){return document.querySelector(f.webchat.container)}get stylesheetLoaded(){return L.waitForStylesheet(L.latestStylesheet)}markCoexistingWidgets(){const e=this.data.html,t=document.querySelector(".hellotext--whatsapp-widget");e&&t&&(e.classList.add("hellotext--with-whatsapp-widget"),t.classList.add("hellotext--with-webchat"))}}class ut{static async load(e){const t=new ut({id:e,html:await k.whatsappWidgets.get(e)});return t.rendered=t.render(),t}constructor(e){this.data=e,this.mounted=!1,this.rendered=Promise.resolve(!1)}async render(){if(!this.data.html)return!1;const e=this.containerToAppendTo;return e?await this.stylesheetLoaded?(e.appendChild(this.data.html),this.markCoexistingWidgets(),this.mounted=!0,!0):(console.warn("Hellotext WhatsApp widget was not mounted because its stylesheet failed to load."),!1):(console.warn(`Hellotext WhatsApp widget was not mounted because the container ${f.whatsapp.container} was not found.`),!1)}get containerToAppendTo(){try{return document.querySelector(f.whatsapp.container)}catch(e){return null}}get stylesheetLoaded(){return L.waitForStylesheet(L.latestStylesheet)}markCoexistingWidgets(){const e=document.querySelector(".hellotext--webchat:not(.hellotext--whatsapp-widget)"),t=this.data.html;e&&t&&(e.classList.add("hellotext--with-whatsapp-widget"),t.classList.add("hellotext--with-webchat"))}}class dt{constructor(e,t,s){this.business=t,this.push=s,this.disposed=!1,this.showRequest=0,this.controller=null,this.element=(new DOMParser).parseFromString(e.html,"text/html").querySelector('[data-controller~="hellotext--alert"]'),this.connected=new Promise(e=>{this.resolveConnected=e}),this.onConnect=this.onConnect.bind(this),this.ready=this.render()}onConnect(e){this.controller=e.detail.controller,this.controller.alert=this,this.resolveConnected(!0)}async render(){return!!this.element&&(this.element.addEventListener("hellotext--alert:connected",this.onConnect),!(!await this.business.stylesheetLoaded||this.disposed)&&(document.body.appendChild(this.element),this.connected))}async show(e,t={}){const s=++this.showRequest;return!(!await this.ready||this.disposed||s!==this.showRequest)&&this.controller.show(e,t)}hide(){this.showRequest+=1,this.controller?.close()}dispose(){this.disposed=!0,this.hide(),this.resolveConnected(!1),this.element?.removeEventListener("hellotext--alert:connected",this.onConnect),this.element?.remove()}}class pt{static async load(e,t={}){const s=new pt({id:e,html:await k.popups.get(e)},t);return s.rendered=s.render(),s}constructor(e,{container:t=f.popup.container,shouldMount:s=()=>!0}={}){this.data=e,this.container=t,this.mounted=!1,this.rendered=Promise.resolve(!1),this.shouldMount=s}async render(){if(!this.data.html||!this.shouldMount())return!1;const e=this.containerToAppendTo;return e?!!this.shouldMount()&&(e.appendChild(this.data.html),this.mounted=!0,this.shouldMount()||this.unmount(),this.mounted):(console.warn(`Hellotext popup was not mounted because the container ${this.container} was not found.`),!1)}unmount(){this.data.html?.remove(),this.mounted=!1}get containerToAppendTo(){try{return document.querySelector(this.container)}catch(e){return null}}}class mt{static get id(){return _.get("hello_user_id")}static get source(){return _.get("hello_user_source")}static get fingerprint(){return _.get("hello_user_identification_hash")}static remember(e,t,s){t&&_.set("hello_user_source",t),s&&_.set("hello_user_identification_hash",s),_.set("hello_user_id",e)}static forget(){_.delete("hello_user_id"),_.delete("hello_user_source"),_.delete("hello_user_identification_hash")}static get identificationData(){return this.id?{id:this.id,source:this.source}:{}}}function gt(e){if(null!=e){if("string"==typeof e){const t=e.trim();return""===t?void 0:t}if(Array.isArray(e))return e.map(e=>gt(e)).filter(e=>void 0!==e);if(e instanceof Date)return e.toISOString();if("object"==typeof e){const t=Object.keys(e).sort((e,t)=>e.localeCompare(t)).reduce((t,s)=>{const i=gt(e[s]);return void 0!==i&&(t[s]=i),t},{});return Object.keys(t).length>0?t:void 0}return"number"==typeof e||"boolean"==typeof e?e:void 0}}function ft(e,t,s={}){const i=gt({session:e,user_id:t,...s})||{};return JSON.stringify(i)}class yt{static matches(e,t){return!!e&&e===t}static async generate(e,t,s={}){return await async function(e){if(!globalThis.crypto?.subtle||"undefined"==typeof TextEncoder)return function(e){let t=5381;for(let s=0;s>>0).toString(16)}`}(e);const t=await globalThis.crypto.subtle.digest("SHA-256",(new TextEncoder).encode(e));return`v1:${Array.from(new Uint8Array(t)).map(e=>e.toString(16).padStart(2,"0")).join("")}`}(ft(e,t,s))}}const bt=["source","medium","campaign"],vt={"product.viewed":"activity.product_viewed","cart.added":"activity.cart_added","order.placed":"activity.purchase_completed","product.purchased":"activity.purchase_completed","form.completed":"activity.form_completed"};class wt{static eventEmitter=new r;static activities=new Set;static pageViews=1;static visitCampaign={};static visitorType="new";static visitBusinessId;static lastPageUrl;static lastPageRoute;static pageStartedAt;static visitStartedAt;static forms;static business;static popup;static webchat;static whatsapp;static push;static alert;static initializationVersion=0;static popupEvaluationVersion=0;static identificationVersion=0;static identificationPending=!1;static identificationCompletion;static cancelIdentificationWait;static popupRuntime;static async initialize(e,t={}){const s=this.visitBusinessId,i=this.session,n=++this.initializationVersion;this.popupEvaluationVersion+=1,this.popup?.unmount?.(),this.popup=void 0,this.popupRuntime=void 0,this.alert?.dispose(),this.alert=null,this.push?.dispose(),this.push=null;const r=new L(e);this.business=r,this.page=new D,f.assign({push:{},...t}),F.initialize(this.page),!this.identificationPending||s===e&&i===this.session||(this.identificationVersion+=1,this.identificationPending=!1,this.cancelIdentificationPolling()),this.initializeVisitSignals(e),this.forms?.mutationObserver?.disconnect(),this.forms=new ct,this.query=new b;const a=await r.hydrate();if(this.business!==r)return;let o=null,c=null;!1!==t.push&&a?.push?.public_key&<.supported&&(o=new lt(a.push),a.alert?.html&&(c=a.alert));const l=!1!==t.popup&&this.deepMergePlainObjects(a&&a.popup||{},t.popup||{}),h=!1!==t.webchat&&this.mergeWebchatConfig(a&&a.webchat||{},t.webchat||{}),u=!1!==t.whatsappWidget&&this.mergeWhatsAppConfig(a&&a.whatsapp||{},t.whatsappWidget||{}),d=t.webchat&&!1!==t.webchat&&Object.prototype.hasOwnProperty.call(t.webchat,"behaviour");f.webchat.behaviourOverride=d;const p=[];if(h&&h.id&&(f.webchat.assign(h),p.push(ht.load(h.id).then(e=>{this.business===r&&(this.webchat=e)}))),u&&u.id&&(f.whatsapp.assign(u),p.push(ut.load(u.id).then(e=>{this.business===r&&(this.whatsapp=e)}))),l&&l.id){const e={container:"body",device:"auto",...l};f.popup.assign(e),this.popupRuntime={config:e,businessContext:r,initializationVersion:n},this.identificationPending||p.push(this.loadPopup(this.popupRuntime))}await Promise.all(p),this.business===r&&this.initializationVersion===n&&(this.push=o,this.alert=c?new dt(c,r,o):null,this.push?.initialize().catch(e=>{console.warn("Hellotext Push initialization failed:",e)}),"undefined"!=typeof MutationObserver&&this.forms.collectExistingFormsOnPage())}static mergeWebchatConfig(e,t){return this.deepMergePlainObjects(e,t)}static mergeWhatsAppConfig(e,t){return this.deepMergePlainObjects(e,t)}static deepMergePlainObjects(e,t){const s={...e};return Object.entries(t).forEach(([e,t])=>{this.isPlainObject(t)&&this.isPlainObject(s[e])?s[e]=this.deepMergePlainObjects(s[e],t):s[e]=t}),s}static async loadPopup(e=this.popupRuntime){if(!e||this.identificationPending)return null;const t=++this.popupEvaluationVersion,s=()=>this.popupRuntime===e&&this.business===e.businessContext&&this.initializationVersion===e.initializationVersion&&this.popupEvaluationVersion===t&&!this.identificationPending,i=await pt.load(e.config.id,{container:e.config.container,shouldMount:s});return s()&&(this.popup=i),i}static reloadPopup(){return this.popupEvaluationVersion+=1,this.popup?.unmount?.(),this.popup=void 0,this.loadPopup()}static isPlainObject(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}static async track(e,t={}){if(this.notInitialized)throw new ot;const s=this.business,i=this.session,n=this.visitBusinessId,r={...t&&t.headers||{},...this.headers},a={...mt.identificationData,...t.user_parameters||{}},o=t&&t.url?new D(t.url):this.page,c={session:i,user_parameters:a,action:e,...t,...o.trackingData};delete c.headers;const l=await k.events.create({headers:r,body:c,keepalive:I(c)}),h=this.trackedAtMilliseconds(t.tracked_at);return l.succeeded&&this.business===s&&this.session===i&&this.visitBusinessId===n&&(null===h||h>=this.visitStartedAt)&&this.recordActivity(e),l}static recordActivity(e){const t=vt[e];t&&(this.activities.add(t),this.writeStorage("sessionStorage",this.visitStorageKey("activities"),JSON.stringify([...this.activities])),this.eventEmitter.dispatch("activity:occurred",{action:e,field:t}))}static trackedAtMilliseconds(e){return null==e||""===e?null:"number"==typeof e?Number.isFinite(e)?e<1e12?1e3*e:e:Number.NaN:new Date(e).getTime()}static initializeVisitSignals(e){const t=this.visitBusinessId!==e;if(this.visitBusinessId=e,t){this.pageViews=0,this.lastPageUrl=void 0,this.lastPageRoute=void 0,this.activities=new Set(this.readStoredActivities()),this.visitCampaign=this.readStoredVisitCampaign();const e=this.readStorage("sessionStorage",this.visitStorageKey("visitor-type"));this.visitorType=["new","returning"].includes(e)?e:void 0,this.visitorType||(this.visitorType=this.readStorage("localStorage",this.visitStorageKey("seen"))?"returning":"new",this.writeStorage("sessionStorage",this.visitStorageKey("visitor-type"),this.visitorType),this.writeStorage("localStorage",this.visitStorageKey("seen"),"1"));const t=Number(this.readStorage("sessionStorage",this.visitStorageKey("started-at")));this.visitStartedAt=Number.isFinite(t)&&t>0?t:this.initialPageStartedAt(),this.writeStorage("sessionStorage",this.visitStorageKey("started-at"),String(this.visitStartedAt))}this.rememberVisitCampaign(N.paramsFrom(window.location.search)),(t||this.lastPageRoute!==this.pageRoute())&&this.recordPageView()}static rememberVisitCampaign(e){const t=Object.fromEntries(bt.flatMap(t=>{const s="string"==typeof e?.[t]?e[t].trim():"";return""===s?[]:[[t,s]]}));0!==Object.keys(t).length&&(this.visitCampaign=t,this.writeStorage("sessionStorage",this.visitStorageKey("campaign"),JSON.stringify(t)))}static readStoredVisitCampaign(){try{const e=JSON.parse(this.readStorage("sessionStorage",this.visitStorageKey("campaign"))||"{}");return null===e||"object"!=typeof e||Array.isArray(e)?{}:Object.fromEntries(Object.entries(e).filter(([e,t])=>bt.includes(e)&&"string"==typeof t))}catch(e){return{}}}static recordPageView(){const e=this.visitStorageKey("page-views"),t=Number(this.readStorage("sessionStorage",e)),s=!this.lastPageUrl;this.pageViews=Number.isInteger(t)&&t>0?t+1:this.pageViews+1,this.lastPageUrl=window.location.href,this.lastPageRoute=this.pageRoute(),this.pageStartedAt=s?this.initialPageStartedAt():Date.now(),this.writeStorage("sessionStorage",e,String(this.pageViews))}static initialPageStartedAt(){const e=window.performance?.getEntriesByType?.("navigation")?.[0]?.name;return e&&this.pageRoute(e)!==this.pageRoute()?Date.now():window.performance?.timeOrigin||Date.now()}static pageRoute(e=window.location.href){const t=window.location?.href||document.location?.href||"http://localhost/",s=new URL(e||t,t),i=s.hash.match(/^#!?\/[^?]*/)?.[0];return`${s.pathname}${i?.replace(/^#!/,"#")||""}`}static readStoredActivities(){try{const e=JSON.parse(this.readStorage("sessionStorage",this.visitStorageKey("activities"))||"[]");return Array.isArray(e)?e.filter(e=>Object.values(vt).includes(e)):[]}catch(e){return[]}}static visitStorageKey(e){return`hellotext:business:${this.visitBusinessId}:${e}`}static storage(e){try{return window[e]}catch(e){return null}}static readStorage(e,t){try{return this.storage(e)?.getItem(t)}catch(e){return null}}static writeStorage(e,t,s){try{this.storage(e)?.setItem(t,s)}catch(e){}}static async identify(e,t={}){const s=await yt.generate(this.session,e,t);if(yt.matches(mt.fingerprint,s))return new v(!0,{json:async()=>({already_identified:!0})});const i=++this.identificationVersion,n=this.visitBusinessId,r=this.session;let a,o;this.identificationPending=!0,this.cancelIdentificationPolling(),this.popupEvaluationVersion+=1,this.popup?.unmount?.(),this.popup=void 0;try{a=await k.identifications.create({user_id:e,...t})}catch(e){throw this.identificationCurrent(i,n,r)&&(this.identificationPending=!1,this.reloadPopup()),e}if(a.failed)return this.identificationCurrent(i,n,r)&&(this.identificationPending=!1,this.reloadPopup()),a;try{o=(await a.json())?.identification_receipt}catch(e){}return o?(this.identificationCompletion=this.finishIdentification({receipt:o,identificationVersion:i,businessId:n,session:r,externalId:e,source:t.source,fingerprint:s}).catch(()=>{}),a):(this.identificationCurrent(i,n,r)&&(mt.remember(e,t.source,s),this.identificationPending=!1,this.reloadPopup()),a)}static identificationCurrent(e,t,s){return this.identificationVersion===e&&this.visitBusinessId===t&&this.session===s}static async finishIdentification(e){const t=[0,100,250,500,1e3,2e3,4e3,8e3];for(const s of t){if(!this.identificationCurrent(e.identificationVersion,e.businessId,e.session))return;if(s>0&&!await this.waitForIdentificationPoll(s))return;if(!this.identificationCurrent(e.identificationVersion,e.businessId,e.session))return;let t;try{t=await k.identifications.status(e.receipt)}catch(e){continue}if(!this.identificationCurrent(e.identificationVersion,e.businessId,e.session))return;if(202!==t.data.status){if(!t.succeeded){if(429===t.data.status||t.data.status>=500)continue;return void(422===t.data.status&&(this.identificationPending=!1,this.cancelIdentificationPolling(),await this.reloadPopup()))}if(!this.identificationCurrent(e.identificationVersion,e.businessId,e.session))return;return mt.remember(e.externalId,e.source,e.fingerprint),this.identificationPending=!1,this.cancelIdentificationPolling(),void await this.reloadPopup()}}}static waitForIdentificationPoll(e){return new Promise(t=>{const s=setTimeout(()=>{this.cancelIdentificationWait=void 0,t(!0)},e);this.cancelIdentificationWait=()=>{clearTimeout(s),this.cancelIdentificationWait=void 0,t(!1)}})}static cancelIdentificationPolling(){this.cancelIdentificationWait?.()}static forget(){mt.forget()}static on(e,t){this.eventEmitter.addSubscriber(e,t)}static removeEventListener(e,t){this.eventEmitter.removeSubscriber(e,t)}static get session(){return F.session}static get isInitialized(){return void 0!==F.session}static get notInitialized(){return!this.business||void 0===this.business.id}static get headers(){if(this.notInitialized)throw new ot;return{Authorization:`Bearer ${this.business.id}`,Accept:"application/json","Content-Type":"application/json"}}}const Tt=wt,St=new Map,Ct=class extends i.xI{static values={sections:Array};static targets=["title","description","primaryAction","secondaryAction"];initialize(){this.showRequest=0,this.submitting=!1,this.onStorage=this.onStorage.bind(this)}connect(){this.dispatch("connected",{detail:{controller:this}}),window.addEventListener("storage",this.onStorage)}disconnect(){this.close(),window.removeEventListener("storage",this.onStorage)}onStorage(e){e.key===this.storageKey&&this.dismissal.dismissedUntil>Date.now()&&this.close()}async show(e,{force:t=!1,title:s,description:i,primaryAction:n,secondaryAction:r}={}){const a=++this.showRequest,o=this.sectionsValue.find(t=>t.kind===e);return o?(await(this.alert.push.ready?.catch(()=>{})),!(a!==this.showRequest||this.alert.disposed||!this.element.isConnected||(this.unavailable||!t&&this.dismissal.dismissedUntil>Date.now()?(this.close(),1):(this.titleTarget.textContent=s??o.title,this.descriptionTarget.textContent=i??o.description,this.primaryActionTarget.textContent=n??o.primary_action,this.secondaryActionTarget.textContent=r??o.secondary_action,this.kind=e,this.page=Tt.page.trackingData.page,this.element.hidden=!1,this.record("shown"),Tt.eventEmitter.dispatch("alert:shown",{kind:e}),0)))):(this.close(),!1)}hide(){if(this.element.hidden||this.submitting)return;const e=this.dismissal.dismissals+1,t={dismissals:e,dismissedUntil:Date.now()+864e5*(1===e?7:30)};St.set(this.storageKey,t);try{localStorage.setItem(this.storageKey,JSON.stringify(t))}catch(e){}this.close(),this.record("dismissed"),Tt.eventEmitter.dispatch("alert:dismissed",{kind:this.kind})}close(){this.showRequest+=1,this.element.hidden=!0}async subscribe(){if(!this.submitting&&!this.element.hidden){if(this.unavailable)return this.close();this.submitting=!0,this.primaryActionTarget.disabled=!0,this.secondaryActionTarget.disabled=!0,this.element.setAttribute("aria-busy","true");try{this.record("accepted"),Tt.eventEmitter.dispatch("alert:accepted",{kind:this.kind});const e=await this.alert.push.subscribe();return this.unavailable&&this.close(),e?.failed&&this.dispatch("error",{detail:{response:e}}),e}catch(e){this.unavailable&&this.close(),this.dispatch("error",{detail:{error:e}})}finally{this.submitting=!1,this.primaryActionTarget.disabled=!1,this.secondaryActionTarget.disabled=!1,this.element.removeAttribute("aria-busy")}}}async record(e){try{const t=await k.pushAlerts.create({section:this.kind,kind:e,page:this.page});t.failed&&console.warn("Hellotext Smart Alert submission failed:",t)}catch(e){console.warn("Hellotext Smart Alert submission failed:",e)}}get unavailable(){return this.alert.push.disposed||this.alert.push.subscribed||"undefined"==typeof Notification||"denied"===Notification.permission}get storageKey(){return`hellotext:alert:${this.alert.business.id}`}get dismissal(){try{const e=JSON.parse(localStorage.getItem(this.storageKey));Number.isSafeInteger(e?.dismissals)&&e.dismissals>0&&Number.isFinite(e.dismissedUntil)&&e.dismissedUntil>=0&&St.set(this.storageKey,e)}catch(e){}return St.get(this.storageKey)||{dismissals:0,dismissedUntil:0}}},At=class extends i.xI{static values={data:Object,step:{type:Number,default:1}};static targets=["inputContainer","input","button","otpContainer"];initialize(){this.form=new at(this.dataValue,this.element)}connect(){super.connect(),this.element.addEventListener("submit",this.submit.bind(this)),"INPUT"!==document.activeElement.tagName&&this.inputTargets[0].focus()}async submit(e){if(e.preventDefault(),this.invalid)return this.showErrorMessages();this.clearErrorMessages(),this.formData=Object.fromEntries(new FormData(this.element)),this.buttonTarget.disabled=!0;const t=await T.submit(this.form.id,this.formData);this.buttonTarget.disabled=!1;const s=await t.json();if(t.failed)return s.errors.forEach(e=>{const{type:t,parameter:s}=e,i=this.inputTargets.find(e=>e.name===s);i.setCustomValidity(Tt.business.locale.errors[t]),i.reportValidity(),i.addEventListener("input",()=>{i.setCustomValidity(""),i.reportValidity()})}),this.showErrorMessages();this.buttonTarget.style.display="none",this.element.querySelectorAll("input").forEach(e=>e.disabled=!0),this.completed()}completed(){if(this.form.markAsCompleted(this.formData),!f.forms.shouldShowSuccessMessage)return this.element.remove();"string"==typeof f.forms.successMessage?this.element.innerHTML=f.forms.successMessage:this.element.innerHTML=Tt.business.locale.forms[this.form.localeAuthKey]}showErrorMessages(){this.inputTargets.forEach(e=>{const t=e.closest("article").querySelector("[data-error-container]");e.validity.valid?t.innerText="":t.innerText=e.validationMessage})}clearErrorMessages(){this.inputTargets.forEach(e=>{e.setCustomValidity(""),e.closest("article").querySelector("[data-error-container]").innerText=""})}inputTargetConnected(e){e.getAttribute("data-default-value")&&(e.value=e.getAttribute("data-default-value"))}get requiredInputs(){return this.inputTargets.filter(e=>e.required)}get invalid(){return!this.element.checkValidity()}},Et=class extends i.xI{static values={fadeDistance:{type:Number,default:64},id:String,kind:String,pageStartOffset:{type:Number,default:0},utm:Object};static targets=["carouselContainer","leftFade","rightFade","carouselCard"];connect(){this.updateFades(),this.observeContainerSize()}disconnect(){this.resizeObserver?.disconnect()}setId({detail:e}){this.idValue=e,this.element.id=e}onScroll(){this.updateFades()}quickReply({currentTarget:e}){const t=e.closest('[data-hellotext--message-target="carouselCard"]'),s=e.closest('[data-controller~="hellotext--message"]'),i=e.dataset.text||e.textContent.trim();this.dispatch("quickReply",{detail:{id:this.idValue,product:t?.dataset.id,buttonId:e.dataset.id,body:i,cardElement:t||s||e}})}addToCart({currentTarget:e}){const t=e.closest('[data-hellotext--message-target="carouselCard"]'),{id:s,reference:i,source:n}=t.dataset,r={product:s,quantity:1};this.hasUtmValue&&Tt.page.utm.save(this.utmValue),Tt.eventEmitter.dispatch("cart.added",{object_parameters:{items:[{...r,...i&&{reference:i},...n&&{source:n}}]},source:{kind:this.kindValue,message_id:this.idValue,button_id:e.dataset.id}})}moveToLeft(){if(!this.hasCarouselContainerTarget)return;const e=this.getPreviousPageScrollLeft(),t=this.carouselContainerTarget.scrollLeft-e;t<1||this.carouselContainerTarget.scrollBy({left:-t,behavior:"smooth"})}moveToRight(){if(!this.hasCarouselContainerTarget)return;const e=this.getNextPageScrollLeft()-this.carouselContainerTarget.scrollLeft;e<1||this.carouselContainerTarget.scrollBy({left:e,behavior:"smooth"})}getScrollAmount(){return this.getCardScrollAmount()}getCardScrollAmount(){const e=this.carouselContainerTarget.querySelector(".message__carousel_card");return e?e.offsetWidth+this.getGap():280}getPageScrollAmount(){const e=this.carouselContainerTarget.clientWidth-this.getGap();return e>0?e:this.getCardScrollAmount()}getNextPageScrollLeft(){const e=this.getCurrentScrollLeft(),t=e+this.carouselContainerTarget.clientWidth,s=this.getCardMetrics().find(e=>e.end>t+1),i=s?this.getPageAlignedScrollLeft(s.start):e+this.getPageScrollAmount(),n=e+this.getPageScrollAmount();return this.clampScrollLeft(i>e+1?i:n)}getPreviousPageScrollLeft(){const e=this.getCurrentScrollLeft();if(e<=1)return 0;const t=Math.max(e-this.getPageScrollAmount(),0);if(t<=1)return 0;const s=this.getCardMetrics(),i=s.find(s=>s.start>=t-1&&s.startt.start{const t=this.getCardScrollLeft(e);return{start:t,end:t+e.offsetWidth}})}getCardScrollLeft(e){const t=e.getBoundingClientRect(),s=this.carouselContainerTarget.getBoundingClientRect();return t.left||t.width||s.left||s.width?t.left-s.left+this.carouselContainerTarget.scrollLeft:e.offsetLeft||0}getCurrentScrollLeft(){return this.clampScrollLeft(this.carouselContainerTarget.scrollLeft)}clampScrollLeft(e){const t=Math.max(this.carouselContainerTarget.scrollWidth-this.carouselContainerTarget.clientWidth,0);return Math.min(Math.max(e,0),t)}getGap(){const e=window.getComputedStyle(this.carouselContainerTarget),t=Number.parseFloat(e.columnGap||e.gap);return Number.isFinite(t)?t:16}getFadeDistance(){return Number.isFinite(this.fadeDistanceValue)?this.fadeDistanceValue:64}getPageStartOffset(){return Number.isFinite(this.pageStartOffsetValue)?this.pageStartOffsetValue:0}observeContainerSize(){this.hasCarouselContainerTarget&&window.ResizeObserver&&(this.resizeObserver=new ResizeObserver(()=>this.updateFades()),this.resizeObserver.observe(this.carouselContainerTarget))}updateFades(){if(!this.hasCarouselContainerTarget)return;const e=Math.max(this.carouselContainerTarget.scrollWidth-this.carouselContainerTarget.clientWidth,0);if(e<=1)return this.hideFade(this.leftFadeTarget),void this.hideFade(this.rightFadeTarget);const t=Math.min(Math.max(this.carouselContainerTarget.scrollLeft,0),e),s=this.getFadeDistance();this.setFadeOpacity(this.leftFadeTarget,t/s),this.setFadeOpacity(this.rightFadeTarget,(e-t)/s)}setFadeOpacity(e,t){const s=Math.min(Math.max(t,0),1);s<=.05?this.hideFade(e):(e.classList.remove("hidden"),e.removeAttribute("disabled"),e.removeAttribute("tabindex"),e.setAttribute("aria-hidden","false"),e.style.opacity=s.toFixed(3),e.style.pointerEvents="auto")}hideFade(e){e.style.opacity="0",e.style.pointerEvents="none",e.setAttribute("aria-hidden","true"),e.setAttribute("tabindex","-1"),"disabled"in e&&(e.disabled=!0),e.classList.add("hidden")}},Ot="exact",xt="contains",Mt=["contains","does_not_contain"],It=/^https?:\/\/[^/?#]+/i,kt=/^\/\/[^/?#]+/,Pt=/^[a-z][a-z0-9+.-]*:/i,Lt=/^(?:(?:[a-z0-9-]+\.)+[a-z]{2,}|localhost)(?::\d+)?\//i,_t=/(?:%[0-9a-f]{2})+/gi,Nt=/%(?:21|23|24|25|26|27|28|29|2a|2b|2c|2f|3a|3b|3d|3f|40|5b|5d)/i,Dt=/(^|\/)index\.(?:html?|php)$/;class Ft{static EXACT=Ot;static CONTAINS=xt;static modeFor(e){return Mt.includes(e)?xt:Ot}static canonical(e,{mode:t=Ot}={}){let s=String(e??"").trim();if(""===s)return"";if(s=this.withoutOrigin(s),null===s)return"";s=this.routePath(s),s=this.decoded(s),s=s.toLowerCase().replace(/ς/g,"σ").normalize("NFC").replace(/\/{2,}/g,"/");const i=Dt.test(s);return s=s.replace(Dt,"$1"),t===xt?(i&&(s=s.replace(/\/$/,"")),"/"===s?"":s):this.resolved(s)}static withoutOrigin(e){return It.test(e)?e.replace(It,""):kt.test(e)?e.replace(kt,""):Pt.test(e)||Lt.test(e)?null:e}static routePath(e){const t=e.indexOf("#"),s=(-1===t?e:e.slice(0,t)).split("?")[0],i=-1===t?"":e.slice(t+1),n=i.startsWith("/")?i:i.startsWith("!/")?i.slice(1):"";return n?`${s}/${n.split("?")[0]}`:s}static decoded(e){return e.replace(_t,e=>{const t=e.match(/%[0-9a-f]{2}/gi)??[],s=[];let i=[];const n=()=>{i.length>0&&s.push(i.join("")),i=[]};return t.forEach(e=>{Nt.test(e)?(n(),s.push(e)):i.push(e)}),n(),s.map(e=>{if(Nt.test(e))return e;try{return decodeURIComponent(e)}catch(t){return e}}).join("")})}static resolved(e){const t=[];return e.split("/").forEach(e=>{""!==e&&"."!==e&&(".."===e?t.pop():t.push(e))}),`/${t.join("/")}`}}const Rt=["does_not_contain","is_not"],Bt=["session.scroll_depth","session.time_on_page","session.page_views"],Vt=["at_least","at_most","greater_than","less_than"],jt=["page.path","page.title","session.referrer","session.language","session.visitor_type","session.browser","session.utm_source","session.utm_medium","session.utm_campaign"],$t=["session.utm_source","session.utm_medium","session.utm_campaign"],Ut=["activity.product_viewed","activity.cart_added","activity.purchase_completed","activity.form_completed"],qt={"session.language":["en","es","pt","fr","nl"],"session.visitor_type":["new","returning"],"session.browser":["chrome","safari","firefox","edge"]},zt={"session.scroll_depth":[1,100],"session.time_on_page":[1,3600],"session.page_views":[1,1e3]},Wt=["contains","does_not_contain","is","is_not"],Kt=["is","is_not"];class Ht{constructor(e){this.valid=null!==e&&"object"==typeof e&&!Array.isArray(e)&&Array.isArray(e.lanes),this.lanes=(this.valid?e.lanes:[]).map(e=>Array.isArray(e)?e:[null])}get empty(){return this.valid&&0===this.lanes.length}get needsMeasurements(){return this.lanes.some(e=>e.some(e=>Bt.includes(e?.field)))}get needsNavigation(){return this.lanes.some(e=>e.some(e=>this.validCondition(e)))}get needsTitle(){return this.lanes.some(e=>e.some(e=>"page.title"===e?.field))}get needsActivities(){return this.lanes.some(e=>e.some(e=>Ut.includes(e?.field)))}matches(e){return!!this.valid&&(!!this.empty||this.lanes.some(t=>this.laneMatches(t,e)))}laneMatches(e,t){if(!e.every(e=>this.validCondition(e)))return!1;const s=new Map;return e.forEach(e=>{const t=s.get(e?.field)||[];t.push(e),s.set(e?.field,t)}),[...s.entries()].every(([e,s])=>this.fieldGroupMatches(e,s,t))}fieldGroupMatches(e,t,s){if(!jt.includes(e))return t.every(e=>this.conditionMatches(e,s));const i=t.filter(e=>!Rt.includes(e?.operator)),n=t.filter(e=>Rt.includes(e?.operator));return(0===i.length||i.some(e=>this.conditionMatches(e,s)))&&n.every(e=>this.conditionMatches(e,s))}conditionMatches(e,t){if(!this.validCondition(e))return!1;if(Ut.includes(e.field))return t.activities?.has?.(e.field)||t.activities?.includes?.(e.field);const s=this.actualValue(e.field,t);return Bt.includes(e.field)?this.thresholdMatches(e,s):"page.path"===e.field?this.pathMatches(e,s,t):this.stringMatches(e,s)}actualValue(e,t){switch(e){case"page.path":return void 0===t.path||null===t.path?t.path:`${t.path}${t.hash??""}`;case"page.title":return t.title;case"session.referrer":return t.referrer;case"session.scroll_depth":return t.scrollDepth;case"session.time_on_page":return t.timeOnPage;case"session.page_views":return t.pageViews;case"session.language":return t.language;case"session.visitor_type":return t.visitorType;case"session.browser":return t.browser;case"session.utm_source":return t.utm?.source;case"session.utm_medium":return t.utm?.medium;case"session.utm_campaign":return t.utm?.campaign;default:return}}validCondition(e){if(!e||"object"!=typeof e||!Array.isArray(e.values))return!1;if(Bt.includes(e.field)){const t=e.values[0],s=Number(t),[i,n]=zt[e.field];return Vt.includes(e.operator)&&1===e.values.length&&("number"==typeof t||"string"==typeof t&&/^\d+$/.test(t))&&Number.isInteger(s)&&s>=i&&s<=n}if(Ut.includes(e.field))return"occurred"===e.operator&&0===e.values.length;const t=qt[e.field]?Kt:Wt;if(!(jt.includes(e.field)&&t.includes(e.operator)&&e.values.length>0&&e.values.every(e=>"string"==typeof e&&e.trim().length>0&&e.length<=512)))return!1;const s=qt[e.field];return!s||e.values.every(e=>s.includes(e))}thresholdMatches(e,t){if(null==t||""===t)return!1;const s=Number(t),i=Number(e.values[0]);switch(e.operator){case"at_least":return s>=i;case"at_most":return s<=i;case"greater_than":return s>i;case"less_than":return sString(e).trim().toLowerCase():e=>String(e).toLowerCase(),n=i(t),r=e.values.some(t=>this.compare(e.operator,n,i(t)));return s?!r:r}pathMatches(e,t,s){const i=Rt.includes(e.operator);if(null==t)return i;const n=Ft.modeFor(e.operator),r=e.values.map(e=>Ft.canonical(e,{mode:n}));if(r.includes(""))return!1;const a=Ft.canonical(t),o=r.some(e=>n===Ft.CONTAINS?a.includes(e):a===e);return i?!o:o}compare(e,t,s){switch(e){case"contains":case"does_not_contain":return t.includes(s);case"is":case"is_not":return t===s;case"starts_with":return t.startsWith(s);case"ends_with":return t.endsWith(s);default:return!1}}}const Gt=class extends i.xI{static targets=["bubble","dialog","step","completed","input","submitButton","globalError","resendButton","changeDestinationButton","deliveryCopy","noDeliveryCopy"];static values={capture:Object,device:String,hasBubble:Boolean,id:String,rules:Object};initialize(){this.stepIndex=0,this.resendLabel=this.hasResendButtonTarget?this.resendButtonTarget.textContent.trim():"",this.rules=new Ht(this.rulesValue),this.connectedAt=this.pageStartedAt()}connect(){Tt.eventEmitter.dispatch("popup:mounted"),this.deviceMatches=this.matchesDevice(),this.watchNavigation(),this.watchActivities(),this.evaluateDisplay(),this.watchMeasurements()}disconnect(){this.stopResendCooldown(),this.stopWatchingMeasurements(),this.stopWatchingNavigation(),this.stopWatchingActivities()}pageStartedAt(){if(Number.isFinite(Tt.pageStartedAt))return Tt.pageStartedAt;const e=window.performance?.timeOrigin;return Number.isFinite(e)&&e<=Date.now()?e:Date.now()}watchNavigation(){if(this.onNavigation)return;this.lastRoute=this.pageRoute(),this.onNavigation=()=>this.scheduleNavigationEvaluation(),this.onTurboNavigation=()=>this.scheduleNavigationEvaluation(!0),window.addEventListener("popstate",this.onNavigation),window.addEventListener("hashchange",this.onNavigation),window.addEventListener("turbo:load",this.onTurboNavigation),window.addEventListener("turbo:render",this.onTurboNavigation),this.rules.needsTitle&&document.head&&(this.titleObserver=new MutationObserver(()=>this.scheduleNavigationEvaluation(!0)),this.titleObserver.observe(document.head,{childList:!0,characterData:!0,subtree:!0}));const e=window.history.pushState,t=window.history.replaceState;let s=!0;this.originalPushState=e,this.originalReplaceState=t,this.stopNavigationWrapper=()=>{s=!1},this.patchedPushState=(...t)=>{const i=e.apply(window.history,t);return s&&this.scheduleNavigationEvaluation(!0),i},this.patchedReplaceState=(...e)=>{const i=t.apply(window.history,e);return s&&this.scheduleNavigationEvaluation(!0),i},window.history.pushState=this.patchedPushState,window.history.replaceState=this.patchedReplaceState}scheduleNavigationEvaluation(e=!1){this.navigationEvaluationForced||=e,this.navigationTimer||(this.navigationTimer=setTimeout(()=>{this.navigationTimer=void 0;const e=this.pageRoute();(this.navigationEvaluationForced||e!==this.lastRoute)&&(this.navigationEvaluationForced=!1,e!==this.lastRoute&&(Tt.recordPageView(),this.connectedAt=Date.now()),this.lastRoute=e,this.displayed||this.evaluateDisplay())}))}pageRoute(){return Tt.pageRoute()}stopWatchingNavigation(){this.stopNavigationWrapper?.(),this.stopNavigationWrapper=void 0,this.titleObserver?.disconnect(),this.titleObserver=void 0,this.onNavigation&&(window.removeEventListener("popstate",this.onNavigation),window.removeEventListener("hashchange",this.onNavigation),this.onNavigation=void 0),this.onTurboNavigation&&(window.removeEventListener("turbo:load",this.onTurboNavigation),window.removeEventListener("turbo:render",this.onTurboNavigation),this.onTurboNavigation=void 0),this.navigationTimer&&(clearTimeout(this.navigationTimer),this.navigationTimer=void 0),window.history.pushState===this.patchedPushState&&(window.history.pushState=this.originalPushState),window.history.replaceState===this.patchedReplaceState&&(window.history.replaceState=this.originalReplaceState),this.patchedPushState=void 0,this.patchedReplaceState=void 0,this.originalPushState=void 0,this.originalReplaceState=void 0,this.navigationEvaluationForced=!1}watchMeasurements(){!this.displayed&&this.rules.needsMeasurements&&(this.onScroll=()=>this.evaluateDisplay(),window.addEventListener("scroll",this.onScroll,{passive:!0}),this.measurementTimer=setInterval(()=>this.evaluateDisplay(),1e3))}stopWatchingMeasurements(){this.onScroll&&(window.removeEventListener("scroll",this.onScroll),this.onScroll=void 0),this.measurementTimer&&(clearInterval(this.measurementTimer),this.measurementTimer=void 0)}watchActivities(){this.displayed||!this.rules.needsActivities||this.onActivity||(this.onActivity=()=>this.evaluateDisplay(),Tt.on("activity:occurred",this.onActivity))}stopWatchingActivities(){this.onActivity&&(Tt.removeEventListener("activity:occurred",this.onActivity),this.onActivity=void 0)}open(e){e&&e.preventDefault(),this.hasBubbleTarget&&(this.bubbleTarget.hidden=!0),this.dialogTarget.hidden=!1,Tt.eventEmitter.dispatch("popup:opened")}close(e){e&&e.preventDefault(),this.dismissed=!0,this.dialogTarget.hidden=!0,this.hasBubbleTarget&&(this.bubbleTarget.hidden=!0),this.element.hidden=!0,Tt.eventEmitter.dispatch("popup:closed")}async next(e){e&&e.preventDefault(),this.clearCustomValidity(),this.currentStepValid()?(this.clearErrorMessages(this.currentStepInputs),this.stepIndex{e.disabled=!0});try{const e=this.submissionPayload(),t=await C.submit(this.idValue,e,this.idempotencyKeyFor(e));if(t.failed)return void await this.handleSubmissionError(t);const s=await t.json();this.submissionId=s.id,this.submissionVerificationState=s.verification_state,this.submissionActionToken=s.action_token,this.submissionDeliveryStatus=s.delivery_status,this.submissionDeliveryChannel=s.delivery_channel,this.submissionDestination=s.destination,this.resetSubmissionRequest()}catch(e){return void this.showGlobalError()}finally{this.submitButtonTargets.forEach(e=>{e.disabled=!1})}this.showCompleted()}else this.showErrorMessages(this.currentStepInputs)}evaluateDisplay(){!this.dismissed&&this.deviceMatches?this.displayed||(this.rules.matches(this.pageContext())?(this.displayed=!0,this.stopWatchingMeasurements(),this.stopWatchingActivities(),this.showInitialState()):this.element.hidden=!0):this.element.hidden=!0}pageContext(){return{url:window.location.href,path:window.location.pathname,hash:window.location.hash,title:document.title,referrer:document.referrer||void 0,scrollDepth:this.scrollDepth(),timeOnPage:Math.floor((Date.now()-this.connectedAt)/1e3),pageViews:Tt.pageViews,language:this.browserLanguage(),visitorType:Tt.visitorType,browser:this.browserName(),utm:this.currentUtmParams(),activities:Tt.activities}}currentUtmParams(){const e=window.location.hash.match(/^#!?\/[^?]*\?(.*)$/)?.[1],t=this.popupUtmParams(N.paramsFrom(e)),s=this.popupUtmParams(N.paramsFrom(window.location.search)),i=Object.keys(s).length>0?s:t;return Object.keys(i).length>0?(Tt.rememberVisitCampaign(i),i):this.popupUtmParams(Tt.visitCampaign)}popupUtmParams(e){return Object.fromEntries(Object.entries(e||{}).flatMap(([e,t])=>["source","medium","campaign"].includes(e)&&"string"==typeof t?""===t.trim()?[]:[[e,t.trim()]]:[]))}browserName(){const e=window.navigator.userAgentData?.brands;if(Array.isArray(e)){const t=e.map(({brand:e})=>e?.toLowerCase()||"");if(t.some(e=>e.includes("edge")))return"edge";if(t.some(e=>e.includes("opera")||e.includes("samsung")))return;if(t.some(e=>e.includes("chrome")))return"chrome"}const t=window.navigator.userAgent?.toLowerCase()||"";return/edg([ea]|ios)?\//.test(t)?"edge":t.includes("opr/")||t.includes("opera/")||t.includes("samsungbrowser/")?void 0:t.includes("firefox/")||t.includes("fxios/")?"firefox":t.includes("chrome/")||t.includes("crios/")?"chrome":t.includes("safari/")?"safari":void 0}browserLanguage(){const e=window.navigator.languages?.[0]||window.navigator.language;return e?.split("-")[0]?.toLowerCase()}scrollDepth(){const e=Math.max(document.documentElement.scrollHeight,window.innerHeight),t=window.scrollY+window.innerHeight;return Math.max(0,Math.min(100,Math.round(t/e*100)))}showInitialState(){if(this.element.hidden=!1,this.hasBubbleValue&&this.hasBubbleTarget)return this.bubbleTarget.hidden=!1,void(this.dialogTarget.hidden=!0);this.hasBubbleTarget&&(this.bubbleTarget.hidden=!0),this.dialogTarget.hidden=!1,Tt.eventEmitter.dispatch("popup:opened")}showStep(e){this.stepIndex=e,this.stepTargets.forEach((t,s)=>{t.hidden=s!==e}),this.completedTarget.hidden=!0}showCompleted(){this.stepTargets.forEach(e=>{e.hidden=!0}),this.interpolateCompletionCopy(),this.configureCompletionActions(),this.completedTarget.hidden=!1}interpolateCompletionCopy(){const e=this.completedIdentity;if(!e)return;const t={destination:e.value,channel:this.submissionDeliveryChannel||e.kind};this.completionTextTemplates.forEach(({node:e,template:s})=>{e.nodeValue=s.replace(/\{(destination|channel)\}/g,(e,s)=>t[s]||e)})}identityValue(e){const t=this.inputValue(e).trim();if(!t)return"";if("phone"!==e.dataset.popupFieldKind||t.startsWith("+"))return t;const s=e.dataset.popupPhonePrefix;return s?`${s}${t.replace(/^0+/,"")}`:t}configureCompletionActions(){const e="not_required"!==this.submissionDeliveryStatus;if(this.revealCompletionCopy(e),!e)return void this.completedTarget.querySelector("[data-delivery-actions]")?.setAttribute("hidden","");const t=this.completedIdentity;t&&(this.hasChangeDestinationButtonTarget&&(this.changeDestinationButtonTarget.textContent=this.changeDestinationButtonTarget.dataset[`${t.kind}Label`],this.changeDestinationButtonTarget.hidden=!1),this.submissionId&&this.submissionActionToken&&"queued"===this.submissionDeliveryStatus&&"unverified"===this.submissionVerificationState&&this.hasResendButtonTarget&&(this.resendButtonTarget.hidden=!1,this.startResendCooldown(60)))}async resend(e){if(e&&e.preventDefault(),this.submissionId&&this.submissionActionToken&&!this.resendPending&&!this.resendCooldownActive&&this.completedIdentity){this.resendPending=!0,this.resendButtonTarget.disabled=!0;try{const e=await C.resend(this.idValue,this.submissionId,this.submissionActionToken),t=Number(e.data.headers?.get("Retry-After"))||60;e.succeeded||429===e.data.status?this.startResendCooldown(t):this.resendButtonTarget.disabled=!1}catch(e){this.resendButtonTarget.disabled=!1}finally{this.resendPending=!1}}}async changeDestination(e){if(e&&e.preventDefault(),!this.submissionId||!this.submissionActionToken||this.changeDestinationPending)return;const t=this.completedIdentity?.input;if(!t)return;const s=this.stepTargets.findIndex(e=>e.dataset.stepId===t.dataset.popupStepId);if(!(s<0)){this.changeDestinationPending=!0,this.changeDestinationButtonTarget.disabled=!0;try{if((await C.cancel(this.idValue,this.submissionId,this.submissionActionToken)).failed)return;this.stopResendCooldown(),this.submissionId=null,this.submissionActionToken=null,this.submissionVerificationState=null,this.submissionDeliveryStatus=null,this.submissionDeliveryChannel=null,this.submissionDestination=null,this.resetSubmissionRequest(),this.showStep(s),t.focus()}catch(e){}finally{this.changeDestinationPending=!1,this.changeDestinationButtonTarget.disabled=!1}}}startResendCooldown(e){this.stopResendCooldown(),this.resendCooldownEndsAt=Date.now()+1e3*Math.max(e,1),this.updateResendCountdown(),this.resendTimer=window.setInterval(()=>this.updateResendCountdown(),1e3)}stopResendCooldown(){this.resendTimer&&window.clearInterval(this.resendTimer),this.resendTimer=null,this.resendCooldownEndsAt=null}updateResendCountdown(){const e=Math.max(0,Math.ceil((this.resendCooldownEndsAt-Date.now())/1e3));if(0===e)return this.stopResendCooldown(),this.resendButtonTarget.textContent=this.resendLabel,void(this.resendButtonTarget.disabled=!1);const t=`${Math.floor(e/60)}:${String(e%60).padStart(2,"0")}`,s=this.resendButtonTarget.dataset.countdownLabel||`${this.resendLabel} %{time}`;this.resendButtonTarget.textContent=s.replace("%{time}",t),this.resendButtonTarget.disabled=!0}get resendCooldownActive(){return this.resendCooldownEndsAt>Date.now()}get completionIdentity(){return this.identityInputs.map(e=>({input:e,kind:e.dataset.popupFieldKind,value:this.identityValue(e)})).find(({value:e})=>e)}get completedIdentity(){if(this.submissionDestination&&this.submissionDeliveryChannel){const e="email"===this.submissionDeliveryChannel?"email":"phone";return{input:this.identityInputs.find(t=>t.dataset.popupFieldKind===e),kind:e,value:this.submissionDestination}}return this.completionIdentity}revealCompletionCopy(e){this.hasDeliveryCopyTarget&&this.deliveryCopyTargets.forEach(t=>{t.hidden=!e}),this.hasNoDeliveryCopyTarget&&this.noDeliveryCopyTargets.forEach(t=>{t.hidden=e})}currentStepValid(){return this.currentStepInputs.every(e=>e.checkValidity())}showErrorMessages(e){e.forEach(e=>{const t=e.closest(".hellotext--popup-field")?.querySelector("[data-error-container]");t&&(t.textContent=e.validity.valid?"":e.validationMessage)})}clearErrorMessages(e=this.inputTargets){e.forEach(e=>{const t=e.closest(".hellotext--popup-field")?.querySelector("[data-error-container]");t&&(t.textContent="")})}clearCustomValidity(){this.inputTargets.forEach(e=>e.setCustomValidity(""))}clearGlobalError(){this.hasGlobalErrorTarget&&(this.globalErrorTarget.textContent="",this.globalErrorTarget.hidden=!0)}showGlobalError(e=null){this.hasGlobalErrorTarget&&(this.globalErrorTarget.textContent=e||this.globalErrorTarget.dataset.submitError||"Unable to submit. Please try again.",this.globalErrorTarget.hidden=!1)}async handleSubmissionError(e){let t;try{t=await e.json()}catch(e){return void this.showGlobalError()}const s=t.errors||[],i=[],n=[];s.forEach(e=>{const t=this.inputForError(e);t?(t.setCustomValidity(e.description||t.validationMessage),n.push(t)):e.description&&i.push(e.description)});const r=this.stepTargets.findIndex(e=>n.some(t=>this.inputsForStep(e).includes(t)));r>=0&&this.showStep(r),n.forEach(e=>e.reportValidity()),this.showErrorMessages(this.inputTargets),i.length?this.showGlobalError(i.join(" ")):s.length||this.showGlobalError()}inputForError(e){const t=e.parameter;return t?this.inputTargets.find(e=>e.dataset.popupFieldKind===t||e.dataset.popupFieldKey===t):null}submissionPayload(){const e={metadata:{capture:this.captureValue||{},fields:{},steps:[]}};return this.stepTargets.forEach(t=>{const s={};this.inputsForStep(t).forEach(t=>{const i="phone"===t.dataset.popupFieldKind?this.identityValue(t):this.inputValue(t),n=t.dataset.popupFieldKey||t.name;s[n]=i,e.metadata.fields[n]=i,"email"===t.dataset.popupFieldKind&&(e.email=i),"phone"===t.dataset.popupFieldKind&&(e.phone=i)}),e.metadata.steps.push({id:t.dataset.stepId,name:t.dataset.stepName,fields:s})}),e}idempotencyKeyFor(e){const t=JSON.stringify(e);return this.submissionPayloadSnapshot!==t&&(this.submissionPayloadSnapshot=t,this.submissionIdempotencyKey=C.idempotencyKey()),this.submissionIdempotencyKey}resetSubmissionRequest(){this.submissionPayloadSnapshot=null,this.submissionIdempotencyKey=null}inputValue(e){return"checkbox"===e.type?e.checked:e.value}inputsForStep(e){return this.inputTargets.filter(t=>t.dataset.popupStepId===e.dataset.stepId)}get identityInputs(){const e=this.inputTargets.filter(e=>["email","phone"].includes(e.dataset.popupFieldKind));return e.filter(e=>e.required).concat(e.filter(e=>!e.required))}get completionTextTemplates(){if(this._completionTextTemplates)return this._completionTextTemplates;const e=document.createTreeWalker(this.completedTarget,NodeFilter.SHOW_TEXT);for(this._completionTextTemplates=[];e.nextNode();)this._completionTextTemplates.push({node:e.currentNode,template:e.currentNode.nodeValue});return this._completionTextTemplates}matchesDevice(){return"all"===this.deviceValue||("mobile"===this.deviceValue?window.innerWidth<768:"desktop"!==this.deviceValue||window.innerWidth>=768)}get currentStep(){return this.stepTargets[this.stepIndex]}get currentStepInputs(){return this.inputsForStep(this.currentStep)}},Jt=["start","end"],Yt=["top","right","bottom","left"].reduce((e,t)=>e.concat(t,t+"-"+Jt[0],t+"-"+Jt[1]),[]),Zt=Math.min,Xt=Math.max,Qt=Math.round,es=Math.floor,ts=e=>({x:e,y:e}),ss={left:"right",right:"left",bottom:"top",top:"bottom"},is={start:"end",end:"start"};function ns(e,t,s){return Xt(e,Zt(t,s))}function rs(e,t){return"function"==typeof e?e(t):e}function as(e){return e.split("-")[0]}function os(e){return e.split("-")[1]}function cs(e){return"x"===e?"y":"x"}function ls(e){return"y"===e?"height":"width"}const hs=new Set(["top","bottom"]);function us(e){return hs.has(as(e))?"y":"x"}function ds(e){return cs(us(e))}function ps(e,t,s){void 0===s&&(s=!1);const i=os(e),n=ds(e),r=ls(n);let a="x"===n?i===(s?"end":"start")?"right":"left":"start"===i?"bottom":"top";return t.reference[r]>t.floating[r]&&(a=vs(a)),[a,vs(a)]}function ms(e){return e.replace(/start|end/g,e=>is[e])}const gs=["left","right"],fs=["right","left"],ys=["top","bottom"],bs=["bottom","top"];function vs(e){return e.replace(/left|right|bottom|top/g,e=>ss[e])}function ws(e){const{x:t,y:s,width:i,height:n}=e;return{width:i,height:n,top:s,left:t,right:t+i,bottom:s+n,x:t,y:s}}function Ts(e,t,s){let{reference:i,floating:n}=e;const r=us(t),a=ds(t),o=ls(a),c=as(t),l="y"===r,h=i.x+i.width/2-n.width/2,u=i.y+i.height/2-n.height/2,d=i[o]/2-n[o]/2;let p;switch(c){case"top":p={x:h,y:i.y-n.height};break;case"bottom":p={x:h,y:i.y+i.height};break;case"right":p={x:i.x+i.width,y:u};break;case"left":p={x:i.x-n.width,y:u};break;default:p={x:i.x,y:i.y}}switch(os(t)){case"start":p[a]-=d*(s&&l?-1:1);break;case"end":p[a]+=d*(s&&l?-1:1)}return p}async function Ss(e,t){var s;void 0===t&&(t={});const{x:i,y:n,platform:r,rects:a,elements:o,strategy:c}=e,{boundary:l="clippingAncestors",rootBoundary:h="viewport",elementContext:u="floating",altBoundary:d=!1,padding:p=0}=rs(t,e),m=function(e){return"number"!=typeof e?function(e){return{top:0,right:0,bottom:0,left:0,...e}}(e):{top:e,right:e,bottom:e,left:e}}(p),g=o[d?"floating"===u?"reference":"floating":u],f=ws(await r.getClippingRect({element:null==(s=await(null==r.isElement?void 0:r.isElement(g)))||s?g:g.contextElement||await(null==r.getDocumentElement?void 0:r.getDocumentElement(o.floating)),boundary:l,rootBoundary:h,strategy:c})),y="floating"===u?{x:i,y:n,width:a.floating.width,height:a.floating.height}:a.reference,b=await(null==r.getOffsetParent?void 0:r.getOffsetParent(o.floating)),v=await(null==r.isElement?void 0:r.isElement(b))&&await(null==r.getScale?void 0:r.getScale(b))||{x:1,y:1},w=ws(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({elements:o,rect:y,offsetParent:b,strategy:c}):y);return{top:(f.top-w.top+m.top)/v.y,bottom:(w.bottom-f.bottom+m.bottom)/v.y,left:(f.left-w.left+m.left)/v.x,right:(w.right-f.right+m.right)/v.x}}const Cs=new Set(["left","top"]);function As(){return"undefined"!=typeof window}function Es(e){return Ms(e)?(e.nodeName||"").toLowerCase():"#document"}function Os(e){var t;return(null==e||null==(t=e.ownerDocument)?void 0:t.defaultView)||window}function xs(e){var t;return null==(t=(Ms(e)?e.ownerDocument:e.document)||window.document)?void 0:t.documentElement}function Ms(e){return!!As()&&(e instanceof Node||e instanceof Os(e).Node)}function Is(e){return!!As()&&(e instanceof Element||e instanceof Os(e).Element)}function ks(e){return!!As()&&(e instanceof HTMLElement||e instanceof Os(e).HTMLElement)}function Ps(e){return!(!As()||"undefined"==typeof ShadowRoot)&&(e instanceof ShadowRoot||e instanceof Os(e).ShadowRoot)}const Ls=new Set(["inline","contents"]);function _s(e){const{overflow:t,overflowX:s,overflowY:i,display:n}=Ws(e);return/auto|scroll|overlay|hidden|clip/.test(t+i+s)&&!Ls.has(n)}const Ns=new Set(["table","td","th"]);function Ds(e){return Ns.has(Es(e))}const Fs=[":popover-open",":modal"];function Rs(e){return Fs.some(t=>{try{return e.matches(t)}catch(e){return!1}})}const Bs=["transform","translate","scale","rotate","perspective"],Vs=["transform","translate","scale","rotate","perspective","filter"],js=["paint","layout","strict","content"];function $s(e){const t=Us(),s=Is(e)?Ws(e):e;return Bs.some(e=>!!s[e]&&"none"!==s[e])||!!s.containerType&&"normal"!==s.containerType||!t&&!!s.backdropFilter&&"none"!==s.backdropFilter||!t&&!!s.filter&&"none"!==s.filter||Vs.some(e=>(s.willChange||"").includes(e))||js.some(e=>(s.contain||"").includes(e))}function Us(){return!("undefined"==typeof CSS||!CSS.supports)&&CSS.supports("-webkit-backdrop-filter","none")}const qs=new Set(["html","body","#document"]);function zs(e){return qs.has(Es(e))}function Ws(e){return Os(e).getComputedStyle(e)}function Ks(e){return Is(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function Hs(e){if("html"===Es(e))return e;const t=e.assignedSlot||e.parentNode||Ps(e)&&e.host||xs(e);return Ps(t)?t.host:t}function Gs(e){const t=Hs(e);return zs(t)?e.ownerDocument?e.ownerDocument.body:e.body:ks(t)&&_s(t)?t:Gs(t)}function Js(e,t,s){var i;void 0===t&&(t=[]),void 0===s&&(s=!0);const n=Gs(e),r=n===(null==(i=e.ownerDocument)?void 0:i.body),a=Os(n);if(r){const e=Ys(a);return t.concat(a,a.visualViewport||[],_s(n)?n:[],e&&s?Js(e):[])}return t.concat(n,Js(n,[],s))}function Ys(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function Zs(e){const t=Ws(e);let s=parseFloat(t.width)||0,i=parseFloat(t.height)||0;const n=ks(e),r=n?e.offsetWidth:s,a=n?e.offsetHeight:i,o=Qt(s)!==r||Qt(i)!==a;return o&&(s=r,i=a),{width:s,height:i,$:o}}function Xs(e){return Is(e)?e:e.contextElement}function Qs(e){const t=Xs(e);if(!ks(t))return ts(1);const s=t.getBoundingClientRect(),{width:i,height:n,$:r}=Zs(t);let a=(r?Qt(s.width):s.width)/i,o=(r?Qt(s.height):s.height)/n;return a&&Number.isFinite(a)||(a=1),o&&Number.isFinite(o)||(o=1),{x:a,y:o}}const ei=ts(0);function ti(e){const t=Os(e);return Us()&&t.visualViewport?{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}:ei}function si(e,t,s,i){void 0===t&&(t=!1),void 0===s&&(s=!1);const n=e.getBoundingClientRect(),r=Xs(e);let a=ts(1);t&&(i?Is(i)&&(a=Qs(i)):a=Qs(e));const o=function(e,t,s){return void 0===t&&(t=!1),!(!s||t&&s!==Os(e))&&t}(r,s,i)?ti(r):ts(0);let c=(n.left+o.x)/a.x,l=(n.top+o.y)/a.y,h=n.width/a.x,u=n.height/a.y;if(r){const e=Os(r),t=i&&Is(i)?Os(i):i;let s=e,n=Ys(s);for(;n&&i&&t!==s;){const e=Qs(n),t=n.getBoundingClientRect(),i=Ws(n),r=t.left+(n.clientLeft+parseFloat(i.paddingLeft))*e.x,a=t.top+(n.clientTop+parseFloat(i.paddingTop))*e.y;c*=e.x,l*=e.y,h*=e.x,u*=e.y,c+=r,l+=a,s=Os(n),n=Ys(s)}}return ws({width:h,height:u,x:c,y:l})}function ii(e,t){const s=Ks(e).scrollLeft;return t?t.left+s:si(xs(e)).left+s}function ni(e,t,s){void 0===s&&(s=!1);const i=e.getBoundingClientRect();return{x:i.left+t.scrollLeft-(s?0:ii(e,i)),y:i.top+t.scrollTop}}const ri=new Set(["absolute","fixed"]);function ai(e,t,s){let i;if("viewport"===t)i=function(e,t){const s=Os(e),i=xs(e),n=s.visualViewport;let r=i.clientWidth,a=i.clientHeight,o=0,c=0;if(n){r=n.width,a=n.height;const e=Us();(!e||e&&"fixed"===t)&&(o=n.offsetLeft,c=n.offsetTop)}return{width:r,height:a,x:o,y:c}}(e,s);else if("document"===t)i=function(e){const t=xs(e),s=Ks(e),i=e.ownerDocument.body,n=Xt(t.scrollWidth,t.clientWidth,i.scrollWidth,i.clientWidth),r=Xt(t.scrollHeight,t.clientHeight,i.scrollHeight,i.clientHeight);let a=-s.scrollLeft+ii(e);const o=-s.scrollTop;return"rtl"===Ws(i).direction&&(a+=Xt(t.clientWidth,i.clientWidth)-n),{width:n,height:r,x:a,y:o}}(xs(e));else if(Is(t))i=function(e,t){const s=si(e,!0,"fixed"===t),i=s.top+e.clientTop,n=s.left+e.clientLeft,r=ks(e)?Qs(e):ts(1);return{width:e.clientWidth*r.x,height:e.clientHeight*r.y,x:n*r.x,y:i*r.y}}(t,s);else{const s=ti(e);i={x:t.x-s.x,y:t.y-s.y,width:t.width,height:t.height}}return ws(i)}function oi(e,t){const s=Hs(e);return!(s===t||!Is(s)||zs(s))&&("fixed"===Ws(s).position||oi(s,t))}function ci(e,t,s){const i=ks(t),n=xs(t),r="fixed"===s,a=si(e,!0,r,t);let o={scrollLeft:0,scrollTop:0};const c=ts(0);function l(){c.x=ii(n)}if(i||!i&&!r)if(("body"!==Es(t)||_s(n))&&(o=Ks(t)),i){const e=si(t,!0,r,t);c.x=e.x+t.clientLeft,c.y=e.y+t.clientTop}else n&&l();r&&!i&&n&&l();const h=!n||i||r?ts(0):ni(n,o);return{x:a.left+o.scrollLeft-c.x-h.x,y:a.top+o.scrollTop-c.y-h.y,width:a.width,height:a.height}}function li(e){return"static"===Ws(e).position}function hi(e,t){if(!ks(e)||"fixed"===Ws(e).position)return null;if(t)return t(e);let s=e.offsetParent;return xs(e)===s&&(s=s.ownerDocument.body),s}function ui(e,t){const s=Os(e);if(Rs(e))return s;if(!ks(e)){let t=Hs(e);for(;t&&!zs(t);){if(Is(t)&&!li(t))return t;t=Hs(t)}return s}let i=hi(e,t);for(;i&&Ds(i)&&li(i);)i=hi(i,t);return i&&zs(i)&&li(i)&&!$s(i)?s:i||function(e){let t=Hs(e);for(;ks(t)&&!zs(t);){if($s(t))return t;if(Rs(t))return null;t=Hs(t)}return null}(e)||s}const di={convertOffsetParentRelativeRectToViewportRelativeRect:function(e){let{elements:t,rect:s,offsetParent:i,strategy:n}=e;const r="fixed"===n,a=xs(i),o=!!t&&Rs(t.floating);if(i===a||o&&r)return s;let c={scrollLeft:0,scrollTop:0},l=ts(1);const h=ts(0),u=ks(i);if((u||!u&&!r)&&(("body"!==Es(i)||_s(a))&&(c=Ks(i)),ks(i))){const e=si(i);l=Qs(i),h.x=e.x+i.clientLeft,h.y=e.y+i.clientTop}const d=!a||u||r?ts(0):ni(a,c,!0);return{width:s.width*l.x,height:s.height*l.y,x:s.x*l.x-c.scrollLeft*l.x+h.x+d.x,y:s.y*l.y-c.scrollTop*l.y+h.y+d.y}},getDocumentElement:xs,getClippingRect:function(e){let{element:t,boundary:s,rootBoundary:i,strategy:n}=e;const r=[..."clippingAncestors"===s?Rs(t)?[]:function(e,t){const s=t.get(e);if(s)return s;let i=Js(e,[],!1).filter(e=>Is(e)&&"body"!==Es(e)),n=null;const r="fixed"===Ws(e).position;let a=r?Hs(e):e;for(;Is(a)&&!zs(a);){const t=Ws(a),s=$s(a);s||"fixed"!==t.position||(n=null),(r?!s&&!n:!s&&"static"===t.position&&n&&ri.has(n.position)||_s(a)&&!s&&oi(e,a))?i=i.filter(e=>e!==a):n=t,a=Hs(a)}return t.set(e,i),i}(t,this._c):[].concat(s),i],a=r[0],o=r.reduce((e,s)=>{const i=ai(t,s,n);return e.top=Xt(i.top,e.top),e.right=Zt(i.right,e.right),e.bottom=Zt(i.bottom,e.bottom),e.left=Xt(i.left,e.left),e},ai(t,a,n));return{width:o.right-o.left,height:o.bottom-o.top,x:o.left,y:o.top}},getOffsetParent:ui,getElementRects:async function(e){const t=this.getOffsetParent||ui,s=this.getDimensions,i=await s(e.floating);return{reference:ci(e.reference,await t(e.floating),e.strategy),floating:{x:0,y:0,width:i.width,height:i.height}}},getClientRects:function(e){return Array.from(e.getClientRects())},getDimensions:function(e){const{width:t,height:s}=Zs(e);return{width:t,height:s}},getScale:Qs,isElement:Is,isRTL:function(e){return"rtl"===Ws(e).direction}};function pi(e,t){return e.x===t.x&&e.y===t.y&&e.width===t.width&&e.height===t.height}const mi=function(e){return void 0===e&&(e=0),{name:"offset",options:e,async fn(t){var s,i;const{x:n,y:r,placement:a,middlewareData:o}=t,c=await async function(e,t){const{placement:s,platform:i,elements:n}=e,r=await(null==i.isRTL?void 0:i.isRTL(n.floating)),a=as(s),o=os(s),c="y"===us(s),l=Cs.has(a)?-1:1,h=r&&c?-1:1,u=rs(t,e);let{mainAxis:d,crossAxis:p,alignmentAxis:m}="number"==typeof u?{mainAxis:u,crossAxis:0,alignmentAxis:null}:{mainAxis:u.mainAxis||0,crossAxis:u.crossAxis||0,alignmentAxis:u.alignmentAxis};return o&&"number"==typeof m&&(p="end"===o?-1*m:m),c?{x:p*h,y:d*l}:{x:d*l,y:p*h}}(t,e);return a===(null==(s=o.offset)?void 0:s.placement)&&null!=(i=o.arrow)&&i.alignmentOffset?{}:{x:n+c.x,y:r+c.y,data:{...c,placement:a}}}}},gi=function(e){return void 0===e&&(e={}),{name:"shift",options:e,async fn(t){const{x:s,y:i,placement:n}=t,{mainAxis:r=!0,crossAxis:a=!1,limiter:o={fn:e=>{let{x:t,y:s}=e;return{x:t,y:s}}},...c}=rs(e,t),l={x:s,y:i},h=await Ss(t,c),u=us(as(n)),d=cs(u);let p=l[d],m=l[u];if(r){const e="y"===d?"bottom":"right";p=ns(p+h["y"===d?"top":"left"],p,p-h[e])}if(a){const e="y"===u?"bottom":"right";m=ns(m+h["y"===u?"top":"left"],m,m-h[e])}const g=o.fn({...t,[d]:p,[u]:m});return{...g,data:{x:g.x-s,y:g.y-i,enabled:{[d]:r,[u]:a}}}}}},fi=function(e){return void 0===e&&(e={}),{name:"flip",options:e,async fn(t){var s,i;const{placement:n,middlewareData:r,rects:a,initialPlacement:o,platform:c,elements:l}=t,{mainAxis:h=!0,crossAxis:u=!0,fallbackPlacements:d,fallbackStrategy:p="bestFit",fallbackAxisSideDirection:m="none",flipAlignment:g=!0,...f}=rs(e,t);if(null!=(s=r.arrow)&&s.alignmentOffset)return{};const y=as(n),b=us(o),v=as(o)===o,w=await(null==c.isRTL?void 0:c.isRTL(l.floating)),T=d||(v||!g?[vs(o)]:function(e){const t=vs(e);return[ms(e),t,ms(t)]}(o)),S="none"!==m;!d&&S&&T.push(...function(e,t,s,i){const n=os(e);let r=function(e,t,s){switch(e){case"top":case"bottom":return s?t?fs:gs:t?gs:fs;case"left":case"right":return t?ys:bs;default:return[]}}(as(e),"start"===s,i);return n&&(r=r.map(e=>e+"-"+n),t&&(r=r.concat(r.map(ms)))),r}(o,g,m,w));const C=[o,...T],A=await Ss(t,f),E=[];let O=(null==(i=r.flip)?void 0:i.overflows)||[];if(h&&E.push(A[y]),u){const e=ps(n,a,w);E.push(A[e[0]],A[e[1]])}if(O=[...O,{placement:n,overflows:E}],!E.every(e=>e<=0)){var x,M;const e=((null==(x=r.flip)?void 0:x.index)||0)+1,t=C[e];if(t&&("alignment"!==u||b===us(t)||O.every(e=>us(e.placement)!==b||e.overflows[0]>0)))return{data:{index:e,overflows:O},reset:{placement:t}};let s=null==(M=O.filter(e=>e.overflows[0]<=0).sort((e,t)=>e.overflows[1]-t.overflows[1])[0])?void 0:M.placement;if(!s)switch(p){case"bestFit":{var I;const e=null==(I=O.filter(e=>{if(S){const t=us(e.placement);return t===b||"y"===t}return!0}).map(e=>[e.placement,e.overflows.filter(e=>e>0).reduce((e,t)=>e+t,0)]).sort((e,t)=>e[1]-t[1])[0])?void 0:I[0];e&&(s=e);break}case"initialPlacement":s=o}if(n!==s)return{reset:{placement:s}}}return{}}}},yi=e=>{Object.assign(e,{show(){this.cancelBehaviourOpen?.(),this.openValue=!0},hide(){this.openValue=!1},toggle(){this.cancelBehaviourOpen?.(),this.openValue=!this.openValue},setupFloatingUI({trigger:e,popover:t,strategy:s}){this.floatingUICleanup=function(e,t,s,i){void 0===i&&(i={});const{ancestorScroll:n=!0,ancestorResize:r=!0,elementResize:a="function"==typeof ResizeObserver,layoutShift:o="function"==typeof IntersectionObserver,animationFrame:c=!1}=i,l=Xs(e),h=n||r?[...l?Js(l):[],...Js(t)]:[];h.forEach(e=>{n&&e.addEventListener("scroll",s,{passive:!0}),r&&e.addEventListener("resize",s)});const u=l&&o?function(e,t){let s,i=null;const n=xs(e);function r(){var e;clearTimeout(s),null==(e=i)||e.disconnect(),i=null}return function a(o,c){void 0===o&&(o=!1),void 0===c&&(c=1),r();const l=e.getBoundingClientRect(),{left:h,top:u,width:d,height:p}=l;if(o||t(),!d||!p)return;const m={rootMargin:-es(u)+"px "+-es(n.clientWidth-(h+d))+"px "+-es(n.clientHeight-(u+p))+"px "+-es(h)+"px",threshold:Xt(0,Zt(1,c))||1};let g=!0;function f(t){const i=t[0].intersectionRatio;if(i!==c){if(!g)return a();i?a(!1,i):s=setTimeout(()=>{a(!1,1e-7)},1e3)}1!==i||pi(l,e.getBoundingClientRect())||a(),g=!1}try{i=new IntersectionObserver(f,{...m,root:n.ownerDocument})}catch(e){i=new IntersectionObserver(f,m)}i.observe(e)}(!0),r}(l,s):null;let d,p=-1,m=null;a&&(m=new ResizeObserver(e=>{let[i]=e;i&&i.target===l&&m&&(m.unobserve(t),cancelAnimationFrame(p),p=requestAnimationFrame(()=>{var e;null==(e=m)||e.observe(t)})),s()}),l&&!c&&m.observe(l),m.observe(t));let g=c?si(e):null;return c&&function t(){const i=si(e);g&&!pi(g,i)&&s(),g=i,d=requestAnimationFrame(t)}(),s(),()=>{var e;h.forEach(e=>{n&&e.removeEventListener("scroll",s),r&&e.removeEventListener("resize",s)}),null==u||u(),null==(e=m)||e.disconnect(),m=null,c&&cancelAnimationFrame(d)}}(e,t,()=>{((e,t,s)=>{const i=new Map,n={platform:di,...s},r={...n.platform,_c:i};return(async(e,t,s)=>{const{placement:i="bottom",strategy:n="absolute",middleware:r=[],platform:a}=s,o=r.filter(Boolean),c=await(null==a.isRTL?void 0:a.isRTL(t));let l=await a.getElementRects({reference:e,floating:t,strategy:n}),{x:h,y:u}=Ts(l,i,c),d=i,p={},m=0;for(let s=0;s{const n={left:`${e}px`,top:`${s}px`,position:i};Object.assign(t.style,n)})})},openValueChanged(){this.disabledValue||(this.openValue?(this.preparePopoverOpenAnimation?.(),this.popoverTarget.showPopover(),this.popoverTarget.setAttribute("aria-expanded","true"),this.onPopoverOpened&&this.onPopoverOpened()):(this.popoverTarget.hidePopover(),this.popoverTarget.removeAttribute("aria-expanded"),this.onPopoverClosed&&this.onPopoverClosed()))}})},bi=class extends i.xI{static targets=["button","popover"];static values={placement:{type:String,default:"bottom-end"},open:{type:Boolean,default:!1},autoPlacement:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},size:{type:Number,default:24},perLine:{type:Number,default:9}};initialize(){this.onEmojiSelect=this.onEmojiSelect.bind(this),this.pickerLoaded=!1,this.pickerLoadPromise=null,this.connected=!1,super.initialize()}connect(){this.connected=!0,yi(this),this.setupFloatingUI({trigger:this.buttonTarget,popover:this.popoverTarget,strategy:"absolute"}),super.connect()}disconnect(){this.connected=!1,this.pickerLoadPromise=null,this.floatingUICleanup(),super.disconnect()}onEmojiSelect(e){this.dispatch("selected",{detail:e.native}),this.hide()}onClickOutside(e){this.openValue&&e.target.nodeType&&!1===this.element.contains(e.target)&&(this.openValue=!1)}async onPopoverOpened(){await this.loadPicker()}async loadPicker(){if(this.pickerLoaded)return;this.pickerLoadPromise||=this.loadPickerDependencies();const{Picker:e,i18n:t}=await this.pickerLoadPromise;this.connected&&!this.pickerLoaded&&(this.popoverTarget.appendChild(this.buildPicker(e,t)),this.pickerLoaded=!0)}async loadPickerDependencies(){const[e,t]=await Promise.all([s.e(160).then(s.bind(s,405)),this.loadI18n()]);return{Picker:e.Picker,i18n:t.default||t}}loadI18n(){return"es"===Hellotext.business.locale?s.e(437).then(s.t.bind(s,9,19)):s.e(200).then(s.t.bind(s,714,19))}buildPicker(e,t){return new e({onEmojiSelect:this.onEmojiSelect,theme:"light",dynamicWidth:!0,previewPosition:"none",skinTonePosition:"none",emojiSize:this.sizeValue,perLine:this.perLineValue,i18n:t})}get middlewares(){return[mi(5),gi({padding:24}),(e={allowedPlacements:["top","bottom"]},void 0===e&&(e={}),{name:"autoPlacement",options:e,async fn(t){var s,i,n;const{rects:r,middlewareData:a,placement:o,platform:c,elements:l}=t,{crossAxis:h=!1,alignment:u,allowedPlacements:d=Yt,autoAlignment:p=!0,...m}=rs(e,t),g=void 0!==u||d===Yt?function(e,t,s){return(e?[...s.filter(t=>os(t)===e),...s.filter(t=>os(t)!==e)]:s.filter(e=>as(e)===e)).filter(s=>!e||os(s)===e||!!t&&ms(s)!==s)}(u||null,p,d):d,f=await Ss(t,m),y=(null==(s=a.autoPlacement)?void 0:s.index)||0,b=g[y];if(null==b)return{};const v=ps(b,r,await(null==c.isRTL?void 0:c.isRTL(l.floating)));if(o!==b)return{reset:{placement:g[0]}};const w=[f[as(b)],f[v[0]],f[v[1]]],T=[...(null==(i=a.autoPlacement)?void 0:i.overflows)||[],{placement:b,overflows:w}],S=g[y+1];if(S)return{data:{index:y+1,overflows:T},reset:{placement:S}};const C=T.map(e=>{const t=os(e.placement);return[e.placement,t&&h?e.overflows.slice(0,2).reduce((e,t)=>e+t,0):e.overflows[0],e.overflows]}).sort((e,t)=>e[1]-t[1]),A=(null==(n=C.filter(e=>e[2].slice(0,os(e[0])?2:3).every(e=>e<=0))[0])?void 0:n[0])||C[0][0];return A!==o?{data:{index:y+1,overflows:T},reset:{placement:A}}:{}}})];var e}};class vi{static get endpoint(){return f.endpoint("public/webchats/:id/messages")}constructor(e){this.webchatId=e}async index(e){const t=new URL(this.url);return Object.entries(e).forEach(([e,s])=>{t.searchParams.append(e,s)}),await fetch(t,{method:"GET",headers:Tt.headers})}catchUp(e){return this.index({after_id:e,session:Tt.session})}async create(e){const t=await fetch(this.url,{method:"POST",headers:{Authorization:`Bearer ${Tt.business.id}`},body:e});return new v(t.ok,t)}markAsSeen(e=null){const t=e?this.url+`/${e}`:this.url+"/seen";fetch(t,{method:"PATCH",headers:Tt.headers,body:JSON.stringify({session:Tt.session})})}get url(){return vi.endpoint.replace(":id",this.webchatId)}}const wi=vi;class Ti{static webSocket;static channels=new Set;static messageHandlers=new Set;static disconnectHandlers=new Set;static subscriptionConfirmHandlers=new Set;static reconnectTimeout=null;static reconnectAttempts=0;static reconnectBaseDelay=500;static reconnectMaxDelay=1e4;static reconnectJitter=.3;static needsResubscribe=!1;constructor(){Ti.channels.add(this)}send({command:e,identifier:t,data:s}){const i={command:e,identifier:JSON.stringify(t),data:JSON.stringify(s||{})},n=Ti.ensureWebSocket(),r=JSON.stringify(i);n.readyState===WebSocket.OPEN?n.send(r):n.addEventListener("open",()=>{n.send(r)})}onMessage(e){const t=t=>{const s=JSON.parse(t.data),{type:i,message:n}=s;this.ignoredEvents.includes(i)||e(n)};Ti.messageHandlers.add(t),Ti.ensureWebSocket().addEventListener("message",t)}onDisconnect(e){Ti.disconnectHandlers.add(e)}onSubscriptionConfirmed(e){Ti.subscriptionConfirmHandlers.add(e)}get webSocket(){return Ti.ensureWebSocket()}static ensureWebSocket(){return this.webSocket&&!this.closedWebSocket(this.webSocket)?this.webSocket:(this.webSocket&&(this.needsResubscribe=!0),this.openWebSocket())}static openWebSocket(){this.clearReconnectTimeout();const e=new WebSocket(f.actionCableUrl);return this.webSocket=e,this.installWebSocketHandlers(e),e}static installWebSocketHandlers(e){e.addEventListener("open",()=>this.handleOpen(e)),e.addEventListener("close",()=>this.handleDisconnect(e)),e.addEventListener("error",()=>this.handleDisconnect(e)),e.addEventListener("message",e=>this.handleControlMessage(e)),this.messageHandlers.forEach(t=>{e.addEventListener("message",t)})}static handleOpen(e){e===this.webSocket&&(this.reconnectAttempts=0,this.needsResubscribe&&(this.needsResubscribe=!1,this.resubscribeChannels()))}static handleControlMessage(e){let t;try{t=JSON.parse(e.data)}catch{return}"confirm_subscription"===t.type&&this.subscriptionConfirmHandlers.forEach(e=>e(t.identifier))}static handleDisconnect(e){e===this.webSocket&&(this.disconnectHandlers.forEach(e=>e()),this.webSocket=null,this.needsResubscribe=!0,this.scheduleReconnect())}static scheduleReconnect(){this.reconnectTimeout||(this.reconnectTimeout=setTimeout(()=>{this.reconnectTimeout=null,this.reconnectAttempts+=1,this.openWebSocket()},this.reconnectDelay))}static clearReconnectTimeout(){this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null)}static resubscribeChannels(){this.channels.forEach(e=>{const t=e.resubscribe||e.subscribe;"function"==typeof t&&t.call(e)})}static closedWebSocket(e){return e.readyState===WebSocket.CLOSED||e.readyState===WebSocket.CLOSING}static get reconnectDelay(){const e=Math.min(this.reconnectMaxDelay,this.reconnectBaseDelay*2**this.reconnectAttempts);return e+Math.round(e*this.reconnectJitter*Math.random())}get ignoredEvents(){return["ping","confirm_subscription","welcome"]}}const Si=Ti,Ci=class extends Si{constructor(e,t,s){super(),this.id=e,this.session=t,this.conversation=s,this.subscribed=!1,this.awaitingReconnectConfirmation=!1,this.reconnectCallbacks=new Set,this.onSubscriptionConfirmed(e=>this.handleSubscriptionConfirmed(e)),this.subscribe()}subscribe(){this.subscribed=!0;const e={channel:"WebchatChannel",id:this.id,session:this.session,conversation:this.conversation};this.send({command:"subscribe",identifier:e})}unsubscribe(){this.subscribed=!1;const e={channel:"WebchatChannel",id:this.id,session:this.session,conversation:this.conversation};this.send({command:"unsubscribe",identifier:e})}resubscribe(){!1!==this.subscribed&&(this.awaitingReconnectConfirmation=!0,this.subscribe())}onReconnect(e){this.reconnectCallbacks.add(e)}handleSubscriptionConfirmed(e){this.awaitingReconnectConfirmation&&this.matchesIdentifier(e)&&(this.awaitingReconnectConfirmation=!1,this.reconnectCallbacks.forEach(e=>e()))}matchesIdentifier(e){let t;try{t="string"==typeof e?JSON.parse(e):e}catch{return!1}return"WebchatChannel"===t.channel&&t.id===this.id&&t.session===this.session&&t.conversation===this.conversation}startTypingIndicator(){const e={channel:"WebchatChannel",id:this.id,session:this.session,conversation:this.conversation};this.send({command:"message",identifier:e,data:{action:"started_typing"}})}stopTypingIndicator(){const e={channel:"WebchatChannel",id:this.id,session:this.session,conversation:this.conversation};this.send({command:"typing:stop",identifier:e,data:{action:"stopped_typing"}})}onMessage(e){super.onMessage(t=>{"message"===t.type&&e(t)})}onReaction(e){super.onMessage(t=>{"reaction.create"!==t.type&&"reaction.destroy"!==t.type||e(t)})}onTypingStart(e){super.onMessage(t=>{"started_typing"===t.type&&e(t)})}updateSubscriptionWith(e){this.unsubscribe(),setTimeout(()=>{this.conversation=e,this.subscribe()},1e3)}},Ai=e=>{Object.assign(e,{scheduleBehaviourOpen(){if(!this.shouldAutoOpenFromBehaviour())return;const e=1e3*Number(this.behaviourValue.delay_seconds||0);this.behaviourOpenTimeout=window.setTimeout(()=>{this.behaviourOpenTimeout=null,this.openValue||(this.openValue=!0,this.markBehaviourAutoOpened())},e)},cancelBehaviourOpen(){window.clearTimeout(this.behaviourOpenTimeout),this.behaviourOpenTimeout=null},shouldAutoOpenFromBehaviour(){const e=this.behaviourValue;return!(!e||"on_load"!==e.trigger||e.first_visit_only&&localStorage.getItem(this.firstVisitKey())||e.once_per_session&&sessionStorage.getItem(this.sessionKey()))},markBehaviourAutoOpened(){this.behaviourValue.first_visit_only&&localStorage.setItem(this.firstVisitKey(),"1"),this.behaviourValue.once_per_session&&sessionStorage.setItem(this.sessionKey(),"1")},firstVisitKey(){return`hellotext--webchat--${this.idValue}--auto-opened`},sessionKey(){return`hellotext--webchat--${this.idValue}--auto-opened-session`}})},Ei=e=>{Object.assign(e,{setupOpeningSequence(){this.openingSequenceStarted=!1,this.openingSequenceCancelled=!1,this.openingSequenceTimeout=null,this.openingSequenceMessages=[],this.revealedOpeningSequenceMessageIds=[]},teardownOpeningSequence(){this.cancelOpeningSequence()},startOpeningSequence(){this.openingSequenceMessages=Array.from(this.openingSequenceMessageTargets||[]),this.openingSequenceCanStart()&&(this.openingSequenceStarted=!0,this.openingSequenceCancelled=!1,this.revealedOpeningSequenceMessageIds=[],this.playOpeningSequenceMessageAt(0))},openingSequenceCanStart(){return!this.conversationIdValue&&this.hasOpeningSequenceTarget&&this.openingSequenceMessages.length>0&&!this.openingSequenceStarted},playOpeningSequenceMessageAt(e){const t=this.openingSequenceMessages[e];if(!t)return;const s=1e3*this.openingSequenceMessageDelay(t);this.openingSequenceTimeout=window.setTimeout(()=>{this.openingSequenceTimeout=null,this.openingSequenceCancelled||(this.revealOpeningSequenceMessage(t),this.playOpeningSequenceMessageAt(e+1))},s)},revealOpeningSequenceMessage(e){this.messagesContainerTarget.insertBefore(e,this.messageTemplateTarget),e.hidden=!1,this.recordOpeningSequenceMessage(e),this.scrollOpeningSequenceToBottom()},recordOpeningSequenceMessage(e){const t=e.dataset.openingSequenceMessageId;t&&!this.revealedOpeningSequenceMessageIds.includes(t)&&this.revealedOpeningSequenceMessageIds.push(t)},openingSequenceMessageDelay(e){const t=Number(e.dataset.delaySeconds||0);return Number.isFinite(t)?t:0},scrollOpeningSequenceToBottom(){this.messagesContainerTarget.scroll&&this.messagesContainerTarget.scroll({top:this.messagesContainerTarget.scrollHeight,behavior:"smooth"})},cancelOpeningSequence(){this.openingSequenceCancelled=!0,null!==this.openingSequenceTimeout&&void 0!==this.openingSequenceTimeout&&(window.clearTimeout(this.openingSequenceTimeout),this.openingSequenceTimeout=null)},appendOpeningSequenceMessageIds(e){this.cancelOpeningSequence(),(this.revealedOpeningSequenceMessageIds||[]).forEach(t=>{e.append("message[opening_sequence_message_ids][]",t)})},clearRevealedOpeningSequenceMessageIds(){this.revealedOpeningSequenceMessageIds=[]}})},Oi=e=>{Object.assign(e,{setupTeaser(){this.teaserCycleTimeout=null,this.teaserMessages=[],this.boundOnTeaserClick=this.boundOnTeaserClick||this.onTeaserClick.bind(this),this.hasTeaserTarget&&(this.teaserTarget.addEventListener("click",this.boundOnTeaserClick),this.startTeaserPresentation())},teardownTeaser(){this.stopTeaserCycle(),this.hasTeaserTarget&&this.boundOnTeaserClick&&this.teaserTarget.removeEventListener("click",this.boundOnTeaserClick)},collectTeaserMessages(){return this.hasTeaserTarget?Array.from(this.teaserTarget.querySelectorAll("[data-teaser-message]")):[]},startTeaserPresentation(){this.stopTeaserCycle(),this.teaserMessages=this.collectTeaserMessages(),this.hasTeaserTarget&&(0!==this.teaserMessages.length?this.openValue||this.conversationIdValue||this.hasRenderedConversationMessages()?this.dismissTeaserForSession():this.teaserSeenForSession()?this.hideTeaser():(this.teaserTarget.classList.remove("invisible"),this.showTeaserMessage(0),this.teaserMessages.length<2||this.scheduleNextTeaserMessage(0)):this.hideTeaser())},scheduleNextTeaserMessage(e){const t=e+1;if(t>=this.teaserMessages.length)return;const s=this.teaserMessages[e],i=this.teaserPresentationDelay(s);this.teaserCycleTimeout=window.setTimeout(()=>{this.teaserCycleTimeout=null,this.showTeaserMessage(t),this.scheduleNextTeaserMessage(t)},i)},showTeaserMessage(e){this.teaserMessages.forEach((t,s)=>{t.classList.toggle("hidden",s!==e)})},stopTeaserCycle(){null!==this.teaserCycleTimeout&&void 0!==this.teaserCycleTimeout&&(window.clearTimeout(this.teaserCycleTimeout),this.teaserCycleTimeout=null)},teaserMessageDelay(e){const t=Number(e.dataset.delaySeconds||0);return Number.isFinite(t)?t:0},teaserPresentationDelay(e){return Math.max(1e3*this.teaserMessageDelay(e),250)},hasRenderedConversationMessages(){let e=[];try{e=Array.from(this.messageTargets||[])}catch(t){e=[]}return e.some(e=>e!==this.messageTemplateTarget)},teaserSeenKey(){const e=this.hasTeaserTarget?this.teaserTarget.dataset.teaserVersion:"",t=e?`:${e}`:"";return`hellotext:webchat:${this.idValue||this.element.id}:teaser-seen${t}`},teaserSeenForSession(){try{return"true"===window.sessionStorage.getItem(this.teaserSeenKey())}catch(e){return!1}},markTeaserSeenForSession(){try{window.sessionStorage.setItem(this.teaserSeenKey(),"true")}catch(e){}},dismissTeaserForSession(){this.markTeaserSeenForSession(),this.hideTeaser()},hideTeaser(){this.stopTeaserCycle(),this.hasTeaserTarget&&this.teaserTarget.classList.add("invisible")},onTeaserClick(e){e.target.closest("a")||(this.dismissTeaserForSession(),this.show())}})},xi={hour:"numeric",minute:"2-digit"},Mi=/Android|iPhone|iPad|iPod/i,Ii={capture:!0,passive:!0},ki=class extends i.xI{static messageTimestampFormatters={};static values={id:String,conversationId:String,media:Object,fileSizeErrorMessage:String,placement:{type:String,default:"bottom-end"},open:{type:Boolean,default:!1},autoPlacement:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},nextPage:{type:Number,default:void 0},fullScreenThreshold:{type:Number,default:1024},typingIndicatorKeepAlive:{type:Number,default:3e4},offset:{type:Number,default:24},padding:{type:Number,default:24},optimisticTypingIndicatorWait:{type:Number,default:1e3},teaser:Object,messageTeaser:String,behaviour:Object};static classes=["fadeOut"];static targets=["trigger","popover","input","attachmentInput","attachmentButton","errorMessageContainer","attachmentTemplate","attachmentContainer","attachment","messageTemplate","messagesContainer","title","attachmentImage","footer","toolbar","message","unreadCounter","typingIndicator","typingIndicatorTemplate","teaser","teaserMessage","inboundMessageTeaser","inboundMessageTeaserBody","openingSequence","openingSequenceMessage"];initialize(){this.messagesAPI=new wi(this.idValue),this.webChatChannel=new Ci(this.idValue,Tt.session,this.conversationIdValue),this.files=[],this.messageIds=new Set,this.catchUpAfterMessageId=null,this.fetchingCatchUpMessages=!1,this.onMessageReceived=this.onMessageReceived.bind(this),this.onMessageReaction=this.onMessageReaction.bind(this),this.onTypingStart=this.onTypingStart.bind(this),this.captureCatchUpCursor=this.captureCatchUpCursor.bind(this),this.catchUpMessages=this.catchUpMessages.bind(this),this.onScroll=this.onScroll.bind(this),this.onOutboundMessageSent=this.onOutboundMessageSent.bind(this),this.closePopoverOnEscape=this.closePopoverOnEscape.bind(this),this.broadcastChannel=new BroadcastChannel(`hellotext--webchat--${this.idValue}`),this.webChatChannel.onDisconnect(this.captureCatchUpCursor),this.webChatChannel.onReconnect(this.catchUpMessages),super.initialize()}connect(){Ai(this),yi(this),Ei(this),Oi(this),this.setupFloatingUI({trigger:this.triggerTarget,popover:this.popoverTarget}),this.hasTeaserTarget&&this.setupFloatingUI({trigger:this.triggerTarget,popover:this.teaserTarget,strategy:"absolute"}),this.setupTeaser(),this.setupOpeningSequence(),this.localizeMessageTimestamps(),this.webChatChannel.onMessage(this.onMessageReceived),this.webChatChannel.onTypingStart(this.onTypingStart),this.webChatChannel.onReaction(this.onMessageReaction),this.setupMessagesContainerScrollIsolation(),this.messagesContainerTarget.addEventListener("scroll",this.onScroll),this.messagesContainerTarget.addEventListener("wheel",this.stopHostScrollPropagation,Ii),this.messagesContainerTarget.addEventListener("touchmove",this.stopHostScrollPropagation,Ii),this.shouldOpenOnMount&&(this.openValue=!0),Tt.eventEmitter.dispatch("webchat:mounted"),this.broadcastChannel.addEventListener("message",this.onOutboundMessageSent),window.addEventListener("keydown",this.closePopoverOnEscape,!0),this.scheduleBehaviourOpen(),super.connect()}disconnect(){this.cancelBehaviourOpen(),this.clearPopoverOpenAnimation(),this.teardownTeaser(),this.teardownOpeningSequence(),this.broadcastChannel.removeEventListener("message",this.onOutboundMessageSent),this.messagesContainerTarget.removeEventListener("scroll",this.onScroll),this.messagesContainerTarget.removeEventListener("wheel",this.stopHostScrollPropagation,Ii),this.messagesContainerTarget.removeEventListener("touchmove",this.stopHostScrollPropagation,Ii),window.removeEventListener("keydown",this.closePopoverOnEscape,!0),this.clearTypingIndicator(),this.broadcastChannel.close(),this.floatingUICleanup(),super.disconnect()}setupMessagesContainerScrollIsolation(){this.messagesContainerTarget.style.overscrollBehavior="contain",this.messagesContainerTarget.style.webkitOverflowScrolling="touch",this.messagesContainerTarget.style.touchAction="pan-y",this.messagesContainerTarget.setAttribute("data-lenis-prevent",""),this.messagesContainerTarget.setAttribute("data-lenis-prevent-wheel",""),this.messagesContainerTarget.setAttribute("data-lenis-prevent-touch","")}stopHostScrollPropagation(e){e.stopPropagation()}onTypingStart(){if(this.typingIndicatorVisible)return this.resetTypingIndicatorTimer();this.showTypingIndicator()}showOptimisticTypingIndicator(){this.typingIndicatorVisible||this.showTypingIndicator()}showTypingIndicator(){this.clearTypingIndicator(),this.typingIndicatorVisible=!0;const e=this.typingIndicatorTemplateTarget.cloneNode(!0);e.setAttribute("data-hellotext--webchat-target","typingIndicator"),e.style.display="flex",this.messagesContainerTarget.appendChild(e),requestAnimationFrame(()=>{this.messagesContainerTarget.scroll({top:this.messagesContainerTarget.scrollHeight,behavior:"instant"})});const t=this.typingIndicatorKeepAliveValue;this.incomingTypingIndicatorTimeout=setTimeout(()=>{this.clearTypingIndicator()},t)}resetTypingIndicatorTimer(){if(!this.typingIndicatorVisible)return;clearTimeout(this.incomingTypingIndicatorTimeout),clearTimeout(this.optimisticTypingTimeout);const e=this.typingIndicatorKeepAliveValue;this.incomingTypingIndicatorTimeout=setTimeout(()=>{this.clearTypingIndicator()},e)}clearTypingIndicator(){this.hasTypingIndicatorTarget&&this.typingIndicatorTarget.remove(),this.typingIndicatorVisible=!1,clearTimeout(this.incomingTypingIndicatorTimeout),clearTimeout(this.optimisticTypingTimeout)}onMessageInputChange(){this.resizeInput(),clearTimeout(this.typingIndicatorTimeout),this.hasSentTypingIndicator||(this.webChatChannel.startTypingIndicator(),this.hasSentTypingIndicator=!0),this.typingIndicatorTimeout=setTimeout(()=>{this.hasSentTypingIndicator=!1},3e3)}onOutboundMessageSent(e){const{data:t}=e,s={"message:sent":e=>{const t=(new DOMParser).parseFromString(e.element,"text/html").body.firstElementChild;this.localizeMessageTimestamps(t),this.typingIndicatorVisible&&this.hasTypingIndicatorTarget?this.messagesContainerTarget.insertBefore(t,this.typingIndicatorTarget):this.messagesContainerTarget.appendChild(t),t.scrollIntoView({behavior:"instant"})},"message:failed":e=>{const t=this.messagesContainerTarget.querySelector(`#${e.id}`);this.markMessageFailed(t,e.reason)}};s[t.type]?s[t.type](t):console.log(`Unhandled message event: ${t.type}`)}async onScroll(){if(this.messagesContainerTarget.scrollTop>300||!this.nextPageValue||this.fetchingNextPage)return;this.fetchingNextPage=!0;const e=await this.messagesAPI.index({page:this.nextPageValue,session:Tt.session}),{next:t,messages:s}=await e.json();this.nextPageValue=t,this.oldScrollHeight=this.messagesContainerTarget.scrollHeight,s.forEach(e=>{const{body:t,attachments:s}=e,i=e.created_at||e.createdAt,n=this.messageTemplateTarget.cloneNode(!0);n.classList.add("hellotext--webchat-message"),n.setAttribute("data-hellotext--webchat-target","message"),n.setAttribute("data-id",e.id),i&&n.setAttribute("data-created-at",i),n.style.removeProperty("display"),rt(n.querySelector("[data-body]"),t),"received"===e.state?n.classList.add("received"):n.classList.remove("received"),s&&s.forEach(e=>{const t=this.attachmentImageTarget.cloneNode(!0);t.removeAttribute("data-hellotext--webchat-target"),t.src=e,t.style.display="block",this.messageAttachmentsContainer(n)?.appendChild(t)}),n.setAttribute("data-body",t),this.localizeMessageTimestamp(n.querySelector("[data-message-timestamp]"),i),this.messagesContainerTarget.prepend(n)}),this.messagesContainerTarget.scroll({top:this.messagesContainerTarget.scrollHeight-this.oldScrollHeight,behavior:"instant"}),this.fetchingNextPage=!1}onClickOutside(e){d.mode===u.POPOVER&&this.openValue&&e.target.nodeType&&!1===this.element.contains(e.target)&&(this.openValue=!1)}closePopover(){this.clearPopoverOpenAnimation(),this.popoverTarget.classList.remove(...this.fadeOutClasses),this.openValue=!1}preparePopoverOpenAnimation(){this.clearPopoverOpenAnimation(),this.popoverTarget.classList.remove(...this.fadeOutClasses),this.popoverTarget.classList.add("hellotext--webchat-popover-opening"),this.popoverOpenAnimationTimeout=setTimeout(()=>{this.popoverTarget.classList.remove("hellotext--webchat-popover-opening"),this.popoverOpenAnimationTimeout=null},120)}clearPopoverOpenAnimation(){this.popoverOpenAnimationTimeout&&(clearTimeout(this.popoverOpenAnimationTimeout),this.popoverOpenAnimationTimeout=null),this.popoverTarget?.classList.remove("hellotext--webchat-popover-opening")}onPopoverOpened(){this.popoverTarget.classList.remove(...this.fadeOutClasses),this.dismissTeaserForSession?.(),this.onMobile||this.focusComposeInput(),this.scrolled||(requestAnimationFrame(()=>{this.messagesContainerTarget.scroll({top:this.messagesContainerTarget.scrollHeight,behavior:"instant"})}),this.scrolled=!0),Tt.eventEmitter.dispatch("webchat:opened"),localStorage.setItem(`hellotext--webchat--${this.idValue}`,"opened"),this.messageTeaserValue&&(this.messageTeaserValue=null),this.startOpeningSequence(),"none"!==this.unreadCounterTarget.style.display&&(this.unreadCounterTarget.style.display="none",this.unreadCounterTarget.innerText="0",this.messagesAPI.markAsSeen())}onPopoverClosed(){this.clearPopoverOpenAnimation(),Tt.eventEmitter.dispatch("webchat:closed"),localStorage.setItem(`hellotext--webchat--${this.idValue}`,"closed")}onMessageReaction(e){const{message:t,reaction:s,type:i}=e,n=this.messageTargets.find(e=>e.dataset.id===t).querySelector("[data-reactions]");if("reaction.destroy"===i)return n.querySelector(`[data-id="${s.id}"]`).remove();if(n.querySelector(`[data-id="${s.id}"]`))n.querySelector(`[data-id="${s.id}"]`).innerText=s.emoji;else{const e=document.createElement("span");e.innerText=s.emoji,e.setAttribute("data-id",s.id),n.appendChild(e)}}onMessageReceived(e,t={}){const{id:s,body:i,attachments:n,teaser:r}=e,a=e.created_at||e.createdAt;if(!this.claimMessageId(s))return;if(this.hideTeaser?.(),e.carousel)return this.insertCarouselMessage(e,t);const o=this.messageTemplateTarget.cloneNode(!0);o.classList.add("hellotext--webchat-message"),o.style.display="flex",rt(o.querySelector("[data-body]"),i),o.setAttribute("data-id",s),o.setAttribute("data-hellotext--webchat-target","message"),this.setMessageCreatedAt(o,a),this.localizeMessageTimestamp(o.querySelector("[data-message-timestamp]"),a),n&&n.forEach(e=>{const t=this.attachmentImageTarget.cloneNode(!0);t.src=e,t.style.display="block",this.messageAttachmentsContainer(o)?.appendChild(t)}),this.clearTypingIndicator(),this.insertMessageElement(o),Tt.eventEmitter.dispatch("webchat:message:received",{...e,body:o.querySelector("[data-body]").innerText}),!1!==t.scroll&&o.scrollIntoView({behavior:"smooth"}),this.updateMessageTeaser(r),this.openValue?this.messagesAPI.markAsSeen(s):this.incrementUnreadCounter()}claimMessageId(e){const t=this.messageTargets||[];return!this.messageIds.has(e)&&(this.messageIds.add(e),!t.some(t=>t.dataset.id===e))}captureCatchUpCursor(){this.catchUpAfterMessageId=this.lastRenderedMessageId}async catchUpMessages(){const e=this.catchUpAfterMessageId;if(e&&!this.fetchingCatchUpMessages){this.fetchingCatchUpMessages=!0;try{const t=await this.messagesAPI.catchUp(e),{messages:s=[]}=await t.json();s.forEach(e=>this.onMessageReceived(e,{scroll:!1})),this.catchUpAfterMessageId=this.lastRenderedMessageId}finally{this.fetchingCatchUpMessages=!1}}}get lastRenderedMessageId(){const e=this.persistedMessageElements;return e[e.length-1]?.dataset.id||null}get persistedMessageElements(){return Array.from(this.messagesContainerTarget.querySelectorAll(".hellotext--webchat-message[data-id]"))}setMessageCreatedAt(e,t){t&&e.setAttribute("data-created-at",t)}insertMessageElement(e){const t=this.nextMessageElementFor(e);t?this.messagesContainerTarget.insertBefore(e,t):this.messagesContainerTarget.appendChild(e)}nextMessageElementFor(e){const t=Date.parse(e.dataset.createdAt);return Number.isNaN(t)?null:this.persistedMessageElements.find(s=>{if(s===e)return!1;const i=Date.parse(s.dataset.createdAt);return!Number.isNaN(i)&&i>t})}updateMessageTeaser(e){this.messageTeaserValue=e,this.messageTeaserValue&&this.hasTeaserTarget&&this.hasInboundMessageTeaserTarget&&this.hasInboundMessageTeaserBodyTarget&&(this.teaserMessageTargets.forEach(e=>e.classList.add("hidden")),this.inboundMessageTeaserBodyTarget.textContent=this.messageTeaserValue,this.inboundMessageTeaserTarget.classList.remove("hidden"),this.teaserTarget.classList.toggle("invisible",this.openValue))}insertCarouselMessage(e,t={}){const s=e.html,i=e.created_at||e.createdAt,n=function(e){return nt(e,it)}(s).firstElementChild;n.classList.add("hellotext--webchat-message"),n.setAttribute("data-id",e.id),n.setAttribute("data-hellotext--webchat-target","message"),this.setMessageCreatedAt(n,i),this.localizeMessageTimestamps(n),this.clearTypingIndicator(),this.insertMessageElement(n),!1!==t.scroll&&n.scrollIntoView({behavior:"smooth"}),Tt.eventEmitter.dispatch("webchat:message:received",{...e,body:n.querySelector("[data-body]")?.innerText||""}),this.updateMessageTeaser(e.teaser),this.openValue?this.messagesAPI.markAsSeen(e.id):this.incrementUnreadCounter()}resizeInput(){this.inputTarget.style.height="auto";const e=this.inputTarget.scrollHeight;this.inputTarget.style.height=`${Math.min(e,96)}px`}async sendQuickReplyMessage({detail:{id:e,product:t,buttonId:s,body:i,cardElement:n}}){this.dismissTeaserForSession?.();const r=new FormData;r.append("message[body]",i),e&&r.append("message[replied_to]",e),t&&r.append("message[product]",t),s&&r.append("message[button]",s),r.append("session",Tt.session),r.append("locale",o.toString()),this.appendOpeningSequenceMessageIds(r);const a=this.buildMessageElement(),c=n?.querySelector("img")?.cloneNode(!0);a.querySelector("[data-body]").innerText=i,c&&(c.removeAttribute("width"),c.removeAttribute("height"),this.messageAttachmentsContainer(a)?.appendChild(c)),this.typingIndicatorVisible&&this.hasTypingIndicatorTarget?this.messagesContainerTarget.insertBefore(a,this.typingIndicatorTarget):this.messagesContainerTarget.appendChild(a),a.scrollIntoView({behavior:"smooth"}),this.broadcastChannel.postMessage({type:"message:sent",element:a.outerHTML});const l=await this.messagesAPI.create(r);if(l.failed)return clearTimeout(this.optimisticTypingTimeout),this.markMessageFailedFromResponse(l,a);const h=await l.json();this.dispatch("set:id",{target:a,detail:h.id}),this.localizeMessageTimestamp(a.querySelector("[data-message-timestamp]"),h.created_at||h.createdAt),this.clearRevealedOpeningSequenceMessageIds();const u={id:h.id,body:i,attachments:c?[c.src]:[],replied_to:e,product:t,button:s,type:"quick_reply"};Tt.eventEmitter.dispatch("webchat:message:sent",u)}async sendTeaserQuickReply(e){e.preventDefault(),e.stopPropagation();const t=e.currentTarget,s=(t.dataset.value||"").trim(),i=[t.dataset.text,t.textContent].map(e=>(e||"").trim()).find(e=>e.length>0),n=s||i;if(!n)return;this.dismissTeaserForSession?.(),this.show();const r=(t.dataset.type||"").trim()||"quick_reply",a=new FormData;a.append("message[body]",n),a.append("session",Tt.session),a.append("locale",o.toString()),this.appendOpeningSequenceMessageIds(a);const c=this.buildMessageElement();c.querySelector("[data-body]").innerText=n,this.typingIndicatorVisible&&this.hasTypingIndicatorTarget?this.messagesContainerTarget.insertBefore(c,this.typingIndicatorTarget):this.messagesContainerTarget.appendChild(c),c.scrollIntoView({behavior:"smooth"}),this.broadcastChannel.postMessage({type:"message:sent",element:c.outerHTML}),this.typingIndicatorVisible||(clearTimeout(this.optimisticTypingTimeout),this.optimisticTypingTimeout=setTimeout(()=>{this.showOptimisticTypingIndicator()},this.optimisticTypingIndicatorWaitValue));const l=await this.messagesAPI.create(a);if(l.failed)return clearTimeout(this.optimisticTypingTimeout),this.markMessageFailedFromResponse(l,c);const h=await l.json();c.setAttribute("data-id",h.id),this.localizeMessageTimestamp(c.querySelector("[data-message-timestamp]"),h.created_at||h.createdAt),this.clearRevealedOpeningSequenceMessageIds(),Tt.eventEmitter.dispatch("webchat:message:sent",{id:h.id,body:n,attachments:[],type:"quick_reply",teaser:{text:i||n,value:s||n,type:r}}),h.conversation&&h.conversation!==this.conversationIdValue&&(this.conversationIdValue=h.conversation,this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)),this.typingIndicatorVisible&&this.resetTypingIndicatorTimer()}async sendMessage(e){const t={body:this.inputTarget.value,attachments:this.files};if(0===this.inputTarget.value.trim().length&&0===this.files.length)return void(e&&e.target&&e.preventDefault());this.dismissTeaserForSession?.();const s=new FormData;this.inputTarget.value.trim().length>0?s.append("message[body]",this.inputTarget.value):delete t.body,this.files.forEach(e=>{s.append("message[attachments][]",e)}),s.append("session",Tt.session),s.append("locale",o.toString()),this.appendOpeningSequenceMessageIds(s);const i=this.buildMessageElement();this.inputTarget.value.trim().length>0?i.querySelector("[data-body]").innerText=this.inputTarget.value:i.querySelector("[data-message-bubble]").remove();const n=this.attachmentContainerTarget.querySelectorAll("img");n.length>0&&n.forEach(e=>{this.messageAttachmentsContainer(i)?.appendChild(e.cloneNode(!0))}),this.typingIndicatorVisible&&this.hasTypingIndicatorTarget?this.messagesContainerTarget.insertBefore(i,this.typingIndicatorTarget):this.messagesContainerTarget.appendChild(i),i.scrollIntoView({behavior:"smooth"}),this.broadcastChannel.postMessage({type:"message:sent",element:i.outerHTML}),this.inputTarget.value="",this.resizeInput(),this.files=[],this.attachmentInputTarget.value="",this.attachmentContainerTarget.innerHTML="",this.attachmentContainerTarget.style.display="none",this.errorMessageContainerTarget.style.display="none",this.focusComposeInput(),this.typingIndicatorVisible||(clearTimeout(this.optimisticTypingTimeout),this.optimisticTypingTimeout=setTimeout(()=>{this.showOptimisticTypingIndicator()},this.optimisticTypingIndicatorWaitValue));const r=await this.messagesAPI.create(s);if(r.failed)return clearTimeout(this.optimisticTypingTimeout),this.markMessageFailedFromResponse(r,i);const a=await r.json();i.setAttribute("data-id",a.id),t.id=a.id,this.localizeMessageTimestamp(i.querySelector("[data-message-timestamp]"),a.created_at||a.createdAt),this.clearRevealedOpeningSequenceMessageIds(),Tt.eventEmitter.dispatch("webchat:message:sent",t),a.conversation!==this.conversationIdValue&&(this.conversationIdValue=a.conversation,this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)),this.typingIndicatorVisible&&this.resetTypingIndicatorTimer(),this.attachmentContainerTarget.style.display=""}buildMessageElement(){const e=this.messageTemplateTarget.cloneNode(!0);return e.id=`hellotext--webchat--${this.idValue}--message--${Date.now()}`,e.classList.add("received"),e.style.removeProperty("display"),e.setAttribute("data-controller","hellotext--message"),e.setAttribute("data-hellotext--webchat-target","message"),this.localizeMessageTimestamp(e.querySelector("[data-message-timestamp]"),new Date),e}focusCompose(e){const{target:t}=e,s=["button","a","input","textarea","select","label",'[role="button"]',"em-emoji-picker",'[data-hellotext--webchat--emoji-target~="popover"]','[data-controller~="hellotext--webchat--emoji"]'].join(", ");this.hasInputTarget&&!t.closest(s)&&this.focusComposeInput({moveCursorToEnd:!0})&&e.preventDefault()}closePopoverFromHeader(e){const{target:t}=e;t.closest(".hellotext--webchat-header-channel-button, .hellotext--webchat-close-button")||(e.preventDefault(),this.closePopover())}closePopoverOnEscape(e){"Escape"===e.key&&this.openValue&&(e.preventDefault(),e.stopPropagation(),this.closePopover(),this.triggerTarget?.focus?.())}async markMessageFailedFromResponse(e,t){const s=await this.messageFailureReason(e);this.markMessageFailed(t,s),this.broadcastChannel.postMessage({type:"message:failed",id:t.id,reason:s})}markMessageFailed(e,t){if(!e)return;if(e.classList.add("failed"),!t)return;const s=e.querySelector("[data-message-timestamp]");s&&(s.textContent=t)}localizeMessageTimestamps(e=this.element){e&&(e.matches?.("time[datetime][data-message-timestamp]")?[e]:Array.from(e.querySelectorAll?.("time[datetime][data-message-timestamp]")||[])).forEach(e=>this.localizeMessageTimestamp(e))}localizeMessageTimestamp(e,t=e?.getAttribute("datetime")){if(!e||!t)return;const s=t instanceof Date?t:new Date(t);Number.isNaN(s.getTime())||(e.setAttribute("datetime",s.toISOString()),e.textContent=this.formatMessageTimestamp(s))}formatMessageTimestamp(e){return this.constructor.messageTimestampFormatterFor(o.toString()).format(e)}static messageTimestampFormatterFor(e){const t=e||"default";return this.messageTimestampFormatters[t]||(this.messageTimestampFormatters[t]=this.buildMessageTimestampFormatter(e)),this.messageTimestampFormatters[t]}static buildMessageTimestampFormatter(e){try{return new Intl.DateTimeFormat(e||void 0,xi)}catch(e){return new Intl.DateTimeFormat(void 0,xi)}}async messageFailureReason(e){const t=e?.data||e?.response,s=t?.statusText||"Message failed";try{const e=t?.clone?t.clone():t,s=await(e?.json?.()),i=this.messageFailureReasonFromPayload(s);if(i)return i}catch(e){}try{const e=t?.clone?t.clone():t,i=await(e?.text?.());return this.messageFailureReasonFromText(i)||s}catch(e){return s}}messageFailureReasonFromText(e){if("string"!=typeof e)return null;const t=e.trim();if(!t||t.startsWith("<"))return null;try{return this.messageFailureReasonFromPayload(JSON.parse(t))||t}catch(e){return t}}messageFailureReasonFromPayload(e){return e?[e.error?.message,e.message,e.errors?.message,e.errors?.[0]?.message,e.errors?.[0]?.description].find(e=>"string"==typeof e&&e.trim().length>0):null}messageAttachmentsContainer(e){return e.querySelector("[data-attachments-container], [data-attachment-container]")}incrementUnreadCounter(){this.unreadCounterTarget.style.display="flex";const e=(parseInt(this.unreadCounterTarget.innerText)||0)+1;this.unreadCounterTarget.innerText=Math.min(e,9)}openAttachment(){this.attachmentInputTarget.click()}onFileInputChange(){this.errorMessageContainerTarget.style.display="none";const e=Array.from(this.attachmentInputTarget.files);this.attachmentInputTarget.value="";const t=e.find(e=>{const t=e.type.split("/")[0];return["image","video","audio"].includes(t)?this.mediaValue[t].max_sizethis.createAttachmentElement(e)),this.focusComposeInput()}createAttachmentElement(e){const t=this.attachmentElement();if(this.attachmentContainerTarget.style.display="",t.setAttribute("data-name",e.name),e.type.startsWith("image/")){const s=this.attachmentImageTarget.cloneNode(!0);s.src=URL.createObjectURL(e),s.style.display="block",t.appendChild(s),this.attachmentContainerTarget.appendChild(t),this.attachmentContainerTarget.style.display="flex"}else{const s=t.querySelector("main");s.style.height="5rem",s.style.borderRadius="0.375rem",s.style.backgroundColor="#e5e7eb",s.style.padding="0.25rem",t.querySelector("p[data-attachment-name]").innerText=e.name,this.attachmentContainerTarget.appendChild(t),this.attachmentContainerTarget.style.display="flex"}}removeAttachment({currentTarget:e}){const t=e.closest("[data-hellotext--webchat-target='attachment']");this.files=this.files.filter(e=>e.name!==t.dataset.name),this.attachmentInputTarget.value="",t.remove(),this.focusComposeInput()}attachmentTargetDisconnected(){0===this.attachmentTargets.length&&(this.attachmentContainerTarget.innerHTML="",this.attachmentContainerTarget.style.display="none")}attachmentElement(){const e=this.attachmentTemplateTarget.cloneNode(!0);return e.removeAttribute("hidden"),e.style.display="flex",e.setAttribute("data-hellotext--webchat-target","attachment"),e}onEmojiSelect({detail:e}){const t=this.inputTarget.value,s=this.inputTarget.selectionStart,i=this.inputTarget.selectionEnd;this.inputTarget.value=t.slice(0,s)+e+t.slice(i),this.inputTarget.selectionStart=this.inputTarget.selectionEnd=s+e.length,this.focusComposeInput()}focusComposeInput({moveCursorToEnd:e=!1}={}){if(!this.shouldAutofocusCompose)return!1;if(!1===this.hasInputTarget)return!1;if(void 0===this.hasInputTarget&&!this.inputTarget)return!1;if(this.inputTarget.focus(),e&&"number"==typeof this.inputTarget.selectionStart){const e=this.inputTarget.value.length;this.inputTarget.setSelectionRange(e,e)}return!0}byteToMegabyte(e){return Math.ceil(e/1024/1024)}get middlewares(){return[mi(this.offsetValue),gi({padding:this.paddingValue}),fi()]}get shouldOpenOnMount(){return"opened"===localStorage.getItem(`hellotext--webchat--${this.idValue}`)&&!this.onMobile}get shouldAutofocusCompose(){return!this.usesVirtualKeyboard}get usesVirtualKeyboard(){if("undefined"==typeof navigator)return!1;const e=navigator.userAgent||"",t="MacIntel"===navigator.platform&&navigator.maxTouchPoints>1,s=Mi.test(e),i=!0===navigator.userAgentData?.mobile;return s||t||i||this.hasTouchOnlyPointer}get hasTouchOnlyPointer(){return"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(pointer: coarse)").matches&&window.matchMedia("(hover: none)").matches}get onMobile(){return"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia(`(max-width: ${this.fullScreenThresholdValue}px)`).matches}},Pi=i.lg.start();Pi.register("hellotext--alert",Ct),Pi.register("hellotext--form",At),Pi.register("hellotext--popup",Gt),Pi.register("hellotext--webchat",ki),Pi.register("hellotext--webchat--emoji",bi),Pi.register("hellotext--message",Et),window.Hellotext=Tt;const Li=Tt}};const t={};function s(i){const n=t[i];if(void 0!==n)return n.exports;const r=t[i]={exports:{}};return e[i](r,r.exports,s),r.exports}s.m=e,(()=>{const e=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;let t;s.t=function(i,n){if(1&n&&(i=this(i)),8&n)return i;if("object"==typeof i&&i){if(4&n&&i.__esModule)return i;if(16&n&&"function"==typeof i.then)return i}const r=Object.create(null);s.r(r);const a={};t=t||[null,e({}),e([]),e(e)];for(var o=2&n&&i;("object"==typeof o||"function"==typeof o)&&!~t.indexOf(o);o=e(o))Object.getOwnPropertyNames(o).forEach(e=>a[e]=()=>i[e]);return a.default=()=>i,s.d(r,a),r}})(),s.d=(e,t)=>{if(Array.isArray(t))for(var i=0;iPromise.all(Object.keys(s.f).reduce((t,i)=>(s.f[i](e,t),t),[])),s.u=e=>({160:"webchat-emoji",200:"webchat-emoji-en",437:"webchat-emoji-es"}[e]+".js"),s.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),s.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const e={},t="Hellotext:";s.l=(i,n,r,a)=>{if(e[i])return void e[i].push(n);let o,c;if(void 0!==r){const e=document.getElementsByTagName("script");for(var l=0;l{o.onerror=o.onload=null,clearTimeout(u);const n=e[i];if(delete e[i],o.parentNode?.removeChild(o),n?.forEach(e=>e(s)),t)return t(s)},u=setTimeout(h.bind(null,void 0,{type:"timeout",target:o}),12e4);o.onerror=h.bind(null,o.onerror),o.onload=h.bind(null,o.onload),c&&document.head.appendChild(o)}})(),s.r=e=>{Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{let e;s.g.importScripts&&(e=s.g.location+"");const t=s.g.document;if(!e&&t&&("SCRIPT"===t.currentScript?.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){const s=t.getElementsByTagName("script");if(s.length){let t=s.length-1;for(;t>-1&&(!e||!/^http(s?):/.test(e));)e=s[t--].src}}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),s.p=e})(),(()=>{const e={792:0};s.f.j=(t,i)=>{let n=s.o(e,t)?e[t]:void 0;if(0!==n)if(n)i.push(n[2]);else{const r=new Promise((s,i)=>n=e[t]=[s,i]);i.push(n[2]=r);const a=s.p+s.u(t),o=new Error,c=i=>{if(s.o(e,t)&&(n=e[t],0!==n&&(e[t]=void 0),n)){const e=i&&("load"===i.type?"missing":i.type),s=i&&i.target&&i.target.src;o.message="Loading chunk "+t+" failed.\n("+e+": "+s+")",o.name="ChunkLoadError",o.type=e,o.request=s,n[1](o)}};s.l(a,c,"chunk-"+t,t)}};const t=(t,i)=>{let[n,r,a]=i;var o,c,l=0;if(n.some(t=>0!==e[t])){for(o in r)s.o(r,o)&&(s.m[o]=r[o]);a&&a(s)}for(t&&t(i);l} */ async json() { - return await this.response.json(); + this.jsonPromise ||= Promise.resolve(this.response.json()); + return await this.jsonPromise; } /** diff --git a/lib/api/response.js b/lib/api/response.js index 66f9dfd1..9aba887a 100644 --- a/lib/api/response.js +++ b/lib/api/response.js @@ -11,6 +11,7 @@ class Response { constructor(success, response) { this.response = response; this.#success = success; + this.jsonPromise = null; } /** @@ -26,7 +27,8 @@ class Response { * @returns {Promise<*>} */ async json() { - return await this.response.json(); + this.jsonPromise ||= Promise.resolve(this.response.json()); + return await this.jsonPromise; } /** diff --git a/lib/controllers/popup_controller.cjs b/lib/controllers/popup_controller.cjs index d6e6498f..f777114c 100644 --- a/lib/controllers/popup_controller.cjs +++ b/lib/controllers/popup_controller.cjs @@ -7,6 +7,8 @@ exports.default = void 0; var _stimulus = require("@hotwired/stimulus"); var _popups = _interopRequireDefault(require("../api/popups")); var _hellotext = _interopRequireDefault(require("../hellotext")); +var _popup_display_rules = require("../models/popup_display_rules"); +var _utm = require("../models/utm"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * An input rendered by the popup's server-side field components. @@ -74,6 +76,7 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de * - device: Popup device targeting. * - hasBubble: Whether the popup starts from a bubble. * - id: Public popup identifier. + * - rules: Page-scoped display rules that survived server-side evaluation. */ class _default extends _stimulus.Controller { static targets = ['bubble', 'dialog', 'step', 'completed', 'input', 'submitButton', 'globalError', 'resendButton', 'changeDestinationButton', 'deliveryCopy', 'noDeliveryCopy']; @@ -81,7 +84,8 @@ class _default extends _stimulus.Controller { capture: Object, device: String, hasBubble: Boolean, - id: String + id: String, + rules: Object }; /** @@ -94,6 +98,8 @@ class _default extends _stimulus.Controller { initialize() { this.stepIndex = 0; this.resendLabel = this.hasResendButtonTarget ? this.resendButtonTarget.textContent.trim() : ''; + this.rules = new _popup_display_rules.PopupDisplayRules(this.rulesValue); + this.connectedAt = this.pageStartedAt(); } /** @@ -105,7 +111,11 @@ class _default extends _stimulus.Controller { */ connect() { _hellotext.default.eventEmitter.dispatch('popup:mounted'); + this.deviceMatches = this.matchesDevice(); + this.watchNavigation(); + this.watchActivities(); this.evaluateDisplay(); + this.watchMeasurements(); } /** @@ -115,6 +125,146 @@ class _default extends _stimulus.Controller { */ disconnect() { this.stopResendCooldown(); + this.stopWatchingMeasurements(); + this.stopWatchingNavigation(); + this.stopWatchingActivities(); + } + pageStartedAt() { + if (Number.isFinite(_hellotext.default.pageStartedAt)) return _hellotext.default.pageStartedAt; + const timeOrigin = window.performance?.timeOrigin; + return Number.isFinite(timeOrigin) && timeOrigin <= Date.now() ? timeOrigin : Date.now(); + } + + /** + * Merchant sites can be SPAs. Re-check client-side page/session rules whenever their + * route changes, including History API navigation which does not emit a browser event. + * The wrapper is restored only when it is still ours, so a later integration is never + * overwritten during cleanup. + */ + watchNavigation() { + if (this.onNavigation) return; + this.lastRoute = this.pageRoute(); + this.onNavigation = () => this.scheduleNavigationEvaluation(); + this.onTurboNavigation = () => this.scheduleNavigationEvaluation(true); + window.addEventListener('popstate', this.onNavigation); + window.addEventListener('hashchange', this.onNavigation); + window.addEventListener('turbo:load', this.onTurboNavigation); + window.addEventListener('turbo:render', this.onTurboNavigation); + if (this.rules.needsTitle && document.head) { + this.titleObserver = new MutationObserver(() => this.scheduleNavigationEvaluation(true)); + this.titleObserver.observe(document.head, { + childList: true, + characterData: true, + subtree: true + }); + } + const originalPushState = window.history.pushState; + const originalReplaceState = window.history.replaceState; + let navigationActive = true; + this.originalPushState = originalPushState; + this.originalReplaceState = originalReplaceState; + this.stopNavigationWrapper = () => { + navigationActive = false; + }; + this.patchedPushState = (...args) => { + const result = originalPushState.apply(window.history, args); + + // A SPA can update document.title without changing the URL. History calls are an + // explicit navigation boundary, so they must still re-evaluate title rules. + if (navigationActive) this.scheduleNavigationEvaluation(true); + return result; + }; + this.patchedReplaceState = (...args) => { + const result = originalReplaceState.apply(window.history, args); + if (navigationActive) this.scheduleNavigationEvaluation(true); + return result; + }; + window.history.pushState = this.patchedPushState; + window.history.replaceState = this.patchedReplaceState; + } + scheduleNavigationEvaluation(force = false) { + this.navigationEvaluationForced ||= force; + if (this.navigationTimer) return; + this.navigationTimer = setTimeout(() => { + this.navigationTimer = undefined; + const route = this.pageRoute(); + if (!this.navigationEvaluationForced && route === this.lastRoute) return; + this.navigationEvaluationForced = false; + if (route !== this.lastRoute) { + _hellotext.default.recordPageView(); + this.connectedAt = Date.now(); + } + this.lastRoute = route; + if (!this.displayed) this.evaluateDisplay(); + }); + } + pageRoute() { + return _hellotext.default.pageRoute(); + } + stopWatchingNavigation() { + this.stopNavigationWrapper?.(); + this.stopNavigationWrapper = undefined; + this.titleObserver?.disconnect(); + this.titleObserver = undefined; + if (this.onNavigation) { + window.removeEventListener('popstate', this.onNavigation); + window.removeEventListener('hashchange', this.onNavigation); + this.onNavigation = undefined; + } + if (this.onTurboNavigation) { + window.removeEventListener('turbo:load', this.onTurboNavigation); + window.removeEventListener('turbo:render', this.onTurboNavigation); + this.onTurboNavigation = undefined; + } + if (this.navigationTimer) { + clearTimeout(this.navigationTimer); + this.navigationTimer = undefined; + } + if (window.history.pushState === this.patchedPushState) { + window.history.pushState = this.originalPushState; + } + if (window.history.replaceState === this.patchedReplaceState) { + window.history.replaceState = this.originalReplaceState; + } + this.patchedPushState = undefined; + this.patchedReplaceState = undefined; + this.originalPushState = undefined; + this.originalReplaceState = undefined; + this.navigationEvaluationForced = false; + } + + /** + * Scroll depth and time on page only grow, so a popup gated on them cannot be decided + * once on connect. Watching starts only when a rule actually needs a measurement, so a + * popup without one adds no listeners and no timer. + */ + watchMeasurements() { + if (this.displayed || !this.rules.needsMeasurements) return; + this.onScroll = () => this.evaluateDisplay(); + window.addEventListener('scroll', this.onScroll, { + passive: true + }); + this.measurementTimer = setInterval(() => this.evaluateDisplay(), 1000); + } + stopWatchingMeasurements() { + if (this.onScroll) { + window.removeEventListener('scroll', this.onScroll); + this.onScroll = undefined; + } + if (this.measurementTimer) { + clearInterval(this.measurementTimer); + this.measurementTimer = undefined; + } + } + watchActivities() { + if (this.displayed || !this.rules.needsActivities || this.onActivity) return; + this.onActivity = () => this.evaluateDisplay(); + _hellotext.default.on('activity:occurred', this.onActivity); + } + stopWatchingActivities() { + if (!this.onActivity) return; + _hellotext.default.removeEventListener('activity:occurred', this.onActivity); + this.onActivity = undefined; } /** @@ -231,12 +381,121 @@ class _default extends _stimulus.Controller { * @returns {void} */ evaluateDisplay() { - if (this.dismissed || !this.matchesDevice()) { + if (this.dismissed || !this.deviceMatches) { + this.element.hidden = true; + return; + } + if (this.displayed) return; + if (!this.rules.matches(this.pageContext())) { this.element.hidden = true; return; } + + // A popup counts as shown only once it actually displays. Rules matching is not + // enough: a visitor who never scrolls far enough never sees it. + this.displayed = true; + this.stopWatchingMeasurements(); + this.stopWatchingActivities(); this.showInitialState(); } + pageContext() { + return { + url: window.location.href, + path: window.location.pathname, + hash: window.location.hash, + title: document.title, + referrer: document.referrer || undefined, + scrollDepth: this.scrollDepth(), + timeOnPage: Math.floor((Date.now() - this.connectedAt) / 1000), + pageViews: _hellotext.default.pageViews, + language: this.browserLanguage(), + visitorType: _hellotext.default.visitorType, + browser: this.browserName(), + utm: this.currentUtmParams(), + activities: _hellotext.default.activities + }; + } + + /** + * The campaign this visit arrived with. A URL carrying source, medium or campaign answers + * for itself and becomes the visit's campaign, so a SPA route that adds parameters is + * picked up at the next evaluation. + * + * Without any of them in the URL the campaign the visit started with still applies, which + * is what keeps a rule true after the site navigates past its landing URL or strips the + * parameters from it. Persisted attribution is deliberately not the fallback: `hello_utm` + * outlives the visit by years and would let an old campaign target a visitor who arrived + * from somewhere else entirely. + * + * The URL's parameters replace the remembered ones rather than merging with them, so a + * rule never pairs the source of one campaign with the name of another. + */ + currentUtmParams() { + const hashSearch = window.location.hash.match(/^#!?\/[^?]*\?(.*)$/)?.[1]; + const hashCampaign = this.popupUtmParams(_utm.UTM.paramsFrom(hashSearch)); + const queryCampaign = this.popupUtmParams(_utm.UTM.paramsFrom(window.location.search)); + const current = Object.keys(queryCampaign).length > 0 ? queryCampaign : hashCampaign; + if (Object.keys(current).length > 0) { + _hellotext.default.rememberVisitCampaign(current); + return current; + } + return this.popupUtmParams(_hellotext.default.visitCampaign); + } + + // Only the three parameters Rules can target, without the blanks. Capitalization and `+` + // are left alone here and settled when the values are compared, so the campaign a rule + // holds reads the way the merchant wrote it. + popupUtmParams(params) { + return Object.fromEntries(Object.entries(params || {}).flatMap(([key, value]) => { + if (!['source', 'medium', 'campaign'].includes(key) || typeof value !== 'string') return []; + return value.trim() === '' ? [] : [[key, value.trim()]]; + })); + } + + /** + * Names the browser, or nothing when it is not one of the four the catalog offers. + * + * User-Agent Client Hints answer this without parsing when they exist. Where they do not + * — Safari and Firefox — the user agent string is the only source, and its order matters: + * Edge claims to be Chrome, and Chrome claims to be Safari. Testing from the most + * specific claim to the least is what keeps each from answering for the others. + * + * An unrecognised browser reports nothing rather than a guess, so `is` never matches on a + * mistake and `is not` never excludes on one. + */ + browserName() { + const brands = window.navigator.userAgentData?.brands; + if (Array.isArray(brands)) { + const brand = brands.map(({ + brand + }) => brand?.toLowerCase() || ''); + if (brand.some(name => name.includes('edge'))) return 'edge'; + if (brand.some(name => name.includes('opera') || name.includes('samsung'))) return undefined; + if (brand.some(name => name.includes('chrome'))) return 'chrome'; + } + const agent = window.navigator.userAgent?.toLowerCase() || ''; + if (/edg([ea]|ios)?\//.test(agent)) return 'edge'; + if (agent.includes('opr/') || agent.includes('opera/') || agent.includes('samsungbrowser/')) return undefined; + if (agent.includes('firefox/') || agent.includes('fxios/')) return 'firefox'; + if (agent.includes('chrome/') || agent.includes('crios/')) return 'chrome'; + if (agent.includes('safari/')) return 'safari'; + return undefined; + } + browserLanguage() { + const language = window.navigator.languages?.[0] || window.navigator.language; + return language?.split('-')[0]?.toLowerCase(); + } + + /** + * Percentage of the document the visitor has reached, counting the viewport itself. A + * page shorter than the viewport has nothing to scroll, so it reads as fully seen rather + * than dividing by zero. + */ + scrollDepth() { + const height = Math.max(document.documentElement.scrollHeight, window.innerHeight); + const viewed = window.scrollY + window.innerHeight; + return Math.max(0, Math.min(100, Math.round(viewed / height * 100))); + } /** * Choose the launcher or immediate dialog without resetting entered form values. @@ -312,14 +571,15 @@ class _default extends _stimulus.Controller { /** * Format a local identity for completion copy when backend route data is absent. - * Phone prefixes and leading-zero removal apply only to this display fallback; - * submissionPayload() still sends the original field value. + * Phone prefixes and leading-zero removal are used for both completion copy and submission, + * so the destination the visitor sees is the destination the backend receives. * * @param {PopupInput} input - Email or phone field containing a string value. * @returns {string} Trimmed identity with the configured phone prefix when needed. */ identityValue(input) { const value = this.inputValue(input).trim(); + if (!value) return ''; if (input.dataset.popupFieldKind !== 'phone' || value.startsWith('+')) return value; const prefix = input.dataset.popupPhonePrefix; return prefix ? `${prefix}${value.replace(/^0+/, '')}` : value; @@ -625,6 +885,7 @@ class _default extends _stimulus.Controller { } const errors = data.errors || []; const generalErrors = []; + const invalidInputs = []; errors.forEach(error => { const input = this.inputForError(error); if (!input) { @@ -632,8 +893,11 @@ class _default extends _stimulus.Controller { return; } input.setCustomValidity(error.description || input.validationMessage); - input.reportValidity(); + invalidInputs.push(input); }); + const stepIndex = this.stepTargets.findIndex(step => invalidInputs.some(input => this.inputsForStep(step).includes(input))); + if (stepIndex >= 0) this.showStep(stepIndex); + invalidInputs.forEach(input => input.reportValidity()); this.showErrorMessages(this.inputTargets); if (generalErrors.length) this.showGlobalError(generalErrors.join(' '));else if (!errors.length) this.showGlobalError(); } @@ -672,7 +936,7 @@ class _default extends _stimulus.Controller { const stepFields = {}; const inputs = this.inputsForStep(step); inputs.forEach(input => { - const value = this.inputValue(input); + const value = input.dataset.popupFieldKind === 'phone' ? this.identityValue(input) : this.inputValue(input); const key = input.dataset.popupFieldKey || input.name; stepFields[key] = value; payload.metadata.fields[key] = value; diff --git a/lib/controllers/popup_controller.js b/lib/controllers/popup_controller.js index f356c94c..eb83e78c 100644 --- a/lib/controllers/popup_controller.js +++ b/lib/controllers/popup_controller.js @@ -1,6 +1,8 @@ import { Controller } from '@hotwired/stimulus'; import PopupsAPI from '../api/popups'; import Hellotext from '../hellotext'; +import { PopupDisplayRules } from '../models/popup_display_rules'; +import { UTM } from '../models/utm'; /** * An input rendered by the popup's server-side field components. @@ -69,6 +71,7 @@ import Hellotext from '../hellotext'; * - device: Popup device targeting. * - hasBubble: Whether the popup starts from a bubble. * - id: Public popup identifier. + * - rules: Page-scoped display rules that survived server-side evaluation. */ export default class extends Controller { static targets = ['bubble', 'dialog', 'step', 'completed', 'input', 'submitButton', 'globalError', 'resendButton', 'changeDestinationButton', 'deliveryCopy', 'noDeliveryCopy']; @@ -76,7 +79,8 @@ export default class extends Controller { capture: Object, device: String, hasBubble: Boolean, - id: String + id: String, + rules: Object }; /** @@ -89,6 +93,8 @@ export default class extends Controller { initialize() { this.stepIndex = 0; this.resendLabel = this.hasResendButtonTarget ? this.resendButtonTarget.textContent.trim() : ''; + this.rules = new PopupDisplayRules(this.rulesValue); + this.connectedAt = this.pageStartedAt(); } /** @@ -100,7 +106,11 @@ export default class extends Controller { */ connect() { Hellotext.eventEmitter.dispatch('popup:mounted'); + this.deviceMatches = this.matchesDevice(); + this.watchNavigation(); + this.watchActivities(); this.evaluateDisplay(); + this.watchMeasurements(); } /** @@ -110,6 +120,146 @@ export default class extends Controller { */ disconnect() { this.stopResendCooldown(); + this.stopWatchingMeasurements(); + this.stopWatchingNavigation(); + this.stopWatchingActivities(); + } + pageStartedAt() { + if (Number.isFinite(Hellotext.pageStartedAt)) return Hellotext.pageStartedAt; + const timeOrigin = window.performance?.timeOrigin; + return Number.isFinite(timeOrigin) && timeOrigin <= Date.now() ? timeOrigin : Date.now(); + } + + /** + * Merchant sites can be SPAs. Re-check client-side page/session rules whenever their + * route changes, including History API navigation which does not emit a browser event. + * The wrapper is restored only when it is still ours, so a later integration is never + * overwritten during cleanup. + */ + watchNavigation() { + if (this.onNavigation) return; + this.lastRoute = this.pageRoute(); + this.onNavigation = () => this.scheduleNavigationEvaluation(); + this.onTurboNavigation = () => this.scheduleNavigationEvaluation(true); + window.addEventListener('popstate', this.onNavigation); + window.addEventListener('hashchange', this.onNavigation); + window.addEventListener('turbo:load', this.onTurboNavigation); + window.addEventListener('turbo:render', this.onTurboNavigation); + if (this.rules.needsTitle && document.head) { + this.titleObserver = new MutationObserver(() => this.scheduleNavigationEvaluation(true)); + this.titleObserver.observe(document.head, { + childList: true, + characterData: true, + subtree: true + }); + } + const originalPushState = window.history.pushState; + const originalReplaceState = window.history.replaceState; + let navigationActive = true; + this.originalPushState = originalPushState; + this.originalReplaceState = originalReplaceState; + this.stopNavigationWrapper = () => { + navigationActive = false; + }; + this.patchedPushState = (...args) => { + const result = originalPushState.apply(window.history, args); + + // A SPA can update document.title without changing the URL. History calls are an + // explicit navigation boundary, so they must still re-evaluate title rules. + if (navigationActive) this.scheduleNavigationEvaluation(true); + return result; + }; + this.patchedReplaceState = (...args) => { + const result = originalReplaceState.apply(window.history, args); + if (navigationActive) this.scheduleNavigationEvaluation(true); + return result; + }; + window.history.pushState = this.patchedPushState; + window.history.replaceState = this.patchedReplaceState; + } + scheduleNavigationEvaluation(force = false) { + this.navigationEvaluationForced ||= force; + if (this.navigationTimer) return; + this.navigationTimer = setTimeout(() => { + this.navigationTimer = undefined; + const route = this.pageRoute(); + if (!this.navigationEvaluationForced && route === this.lastRoute) return; + this.navigationEvaluationForced = false; + if (route !== this.lastRoute) { + Hellotext.recordPageView(); + this.connectedAt = Date.now(); + } + this.lastRoute = route; + if (!this.displayed) this.evaluateDisplay(); + }); + } + pageRoute() { + return Hellotext.pageRoute(); + } + stopWatchingNavigation() { + this.stopNavigationWrapper?.(); + this.stopNavigationWrapper = undefined; + this.titleObserver?.disconnect(); + this.titleObserver = undefined; + if (this.onNavigation) { + window.removeEventListener('popstate', this.onNavigation); + window.removeEventListener('hashchange', this.onNavigation); + this.onNavigation = undefined; + } + if (this.onTurboNavigation) { + window.removeEventListener('turbo:load', this.onTurboNavigation); + window.removeEventListener('turbo:render', this.onTurboNavigation); + this.onTurboNavigation = undefined; + } + if (this.navigationTimer) { + clearTimeout(this.navigationTimer); + this.navigationTimer = undefined; + } + if (window.history.pushState === this.patchedPushState) { + window.history.pushState = this.originalPushState; + } + if (window.history.replaceState === this.patchedReplaceState) { + window.history.replaceState = this.originalReplaceState; + } + this.patchedPushState = undefined; + this.patchedReplaceState = undefined; + this.originalPushState = undefined; + this.originalReplaceState = undefined; + this.navigationEvaluationForced = false; + } + + /** + * Scroll depth and time on page only grow, so a popup gated on them cannot be decided + * once on connect. Watching starts only when a rule actually needs a measurement, so a + * popup without one adds no listeners and no timer. + */ + watchMeasurements() { + if (this.displayed || !this.rules.needsMeasurements) return; + this.onScroll = () => this.evaluateDisplay(); + window.addEventListener('scroll', this.onScroll, { + passive: true + }); + this.measurementTimer = setInterval(() => this.evaluateDisplay(), 1000); + } + stopWatchingMeasurements() { + if (this.onScroll) { + window.removeEventListener('scroll', this.onScroll); + this.onScroll = undefined; + } + if (this.measurementTimer) { + clearInterval(this.measurementTimer); + this.measurementTimer = undefined; + } + } + watchActivities() { + if (this.displayed || !this.rules.needsActivities || this.onActivity) return; + this.onActivity = () => this.evaluateDisplay(); + Hellotext.on('activity:occurred', this.onActivity); + } + stopWatchingActivities() { + if (!this.onActivity) return; + Hellotext.removeEventListener('activity:occurred', this.onActivity); + this.onActivity = undefined; } /** @@ -226,12 +376,121 @@ export default class extends Controller { * @returns {void} */ evaluateDisplay() { - if (this.dismissed || !this.matchesDevice()) { + if (this.dismissed || !this.deviceMatches) { + this.element.hidden = true; + return; + } + if (this.displayed) return; + if (!this.rules.matches(this.pageContext())) { this.element.hidden = true; return; } + + // A popup counts as shown only once it actually displays. Rules matching is not + // enough: a visitor who never scrolls far enough never sees it. + this.displayed = true; + this.stopWatchingMeasurements(); + this.stopWatchingActivities(); this.showInitialState(); } + pageContext() { + return { + url: window.location.href, + path: window.location.pathname, + hash: window.location.hash, + title: document.title, + referrer: document.referrer || undefined, + scrollDepth: this.scrollDepth(), + timeOnPage: Math.floor((Date.now() - this.connectedAt) / 1000), + pageViews: Hellotext.pageViews, + language: this.browserLanguage(), + visitorType: Hellotext.visitorType, + browser: this.browserName(), + utm: this.currentUtmParams(), + activities: Hellotext.activities + }; + } + + /** + * The campaign this visit arrived with. A URL carrying source, medium or campaign answers + * for itself and becomes the visit's campaign, so a SPA route that adds parameters is + * picked up at the next evaluation. + * + * Without any of them in the URL the campaign the visit started with still applies, which + * is what keeps a rule true after the site navigates past its landing URL or strips the + * parameters from it. Persisted attribution is deliberately not the fallback: `hello_utm` + * outlives the visit by years and would let an old campaign target a visitor who arrived + * from somewhere else entirely. + * + * The URL's parameters replace the remembered ones rather than merging with them, so a + * rule never pairs the source of one campaign with the name of another. + */ + currentUtmParams() { + const hashSearch = window.location.hash.match(/^#!?\/[^?]*\?(.*)$/)?.[1]; + const hashCampaign = this.popupUtmParams(UTM.paramsFrom(hashSearch)); + const queryCampaign = this.popupUtmParams(UTM.paramsFrom(window.location.search)); + const current = Object.keys(queryCampaign).length > 0 ? queryCampaign : hashCampaign; + if (Object.keys(current).length > 0) { + Hellotext.rememberVisitCampaign(current); + return current; + } + return this.popupUtmParams(Hellotext.visitCampaign); + } + + // Only the three parameters Rules can target, without the blanks. Capitalization and `+` + // are left alone here and settled when the values are compared, so the campaign a rule + // holds reads the way the merchant wrote it. + popupUtmParams(params) { + return Object.fromEntries(Object.entries(params || {}).flatMap(([key, value]) => { + if (!['source', 'medium', 'campaign'].includes(key) || typeof value !== 'string') return []; + return value.trim() === '' ? [] : [[key, value.trim()]]; + })); + } + + /** + * Names the browser, or nothing when it is not one of the four the catalog offers. + * + * User-Agent Client Hints answer this without parsing when they exist. Where they do not + * — Safari and Firefox — the user agent string is the only source, and its order matters: + * Edge claims to be Chrome, and Chrome claims to be Safari. Testing from the most + * specific claim to the least is what keeps each from answering for the others. + * + * An unrecognised browser reports nothing rather than a guess, so `is` never matches on a + * mistake and `is not` never excludes on one. + */ + browserName() { + const brands = window.navigator.userAgentData?.brands; + if (Array.isArray(brands)) { + const brand = brands.map(({ + brand + }) => brand?.toLowerCase() || ''); + if (brand.some(name => name.includes('edge'))) return 'edge'; + if (brand.some(name => name.includes('opera') || name.includes('samsung'))) return undefined; + if (brand.some(name => name.includes('chrome'))) return 'chrome'; + } + const agent = window.navigator.userAgent?.toLowerCase() || ''; + if (/edg([ea]|ios)?\//.test(agent)) return 'edge'; + if (agent.includes('opr/') || agent.includes('opera/') || agent.includes('samsungbrowser/')) return undefined; + if (agent.includes('firefox/') || agent.includes('fxios/')) return 'firefox'; + if (agent.includes('chrome/') || agent.includes('crios/')) return 'chrome'; + if (agent.includes('safari/')) return 'safari'; + return undefined; + } + browserLanguage() { + const language = window.navigator.languages?.[0] || window.navigator.language; + return language?.split('-')[0]?.toLowerCase(); + } + + /** + * Percentage of the document the visitor has reached, counting the viewport itself. A + * page shorter than the viewport has nothing to scroll, so it reads as fully seen rather + * than dividing by zero. + */ + scrollDepth() { + const height = Math.max(document.documentElement.scrollHeight, window.innerHeight); + const viewed = window.scrollY + window.innerHeight; + return Math.max(0, Math.min(100, Math.round(viewed / height * 100))); + } /** * Choose the launcher or immediate dialog without resetting entered form values. @@ -307,14 +566,15 @@ export default class extends Controller { /** * Format a local identity for completion copy when backend route data is absent. - * Phone prefixes and leading-zero removal apply only to this display fallback; - * submissionPayload() still sends the original field value. + * Phone prefixes and leading-zero removal are used for both completion copy and submission, + * so the destination the visitor sees is the destination the backend receives. * * @param {PopupInput} input - Email or phone field containing a string value. * @returns {string} Trimmed identity with the configured phone prefix when needed. */ identityValue(input) { const value = this.inputValue(input).trim(); + if (!value) return ''; if (input.dataset.popupFieldKind !== 'phone' || value.startsWith('+')) return value; const prefix = input.dataset.popupPhonePrefix; return prefix ? `${prefix}${value.replace(/^0+/, '')}` : value; @@ -620,6 +880,7 @@ export default class extends Controller { } const errors = data.errors || []; const generalErrors = []; + const invalidInputs = []; errors.forEach(error => { const input = this.inputForError(error); if (!input) { @@ -627,8 +888,11 @@ export default class extends Controller { return; } input.setCustomValidity(error.description || input.validationMessage); - input.reportValidity(); + invalidInputs.push(input); }); + const stepIndex = this.stepTargets.findIndex(step => invalidInputs.some(input => this.inputsForStep(step).includes(input))); + if (stepIndex >= 0) this.showStep(stepIndex); + invalidInputs.forEach(input => input.reportValidity()); this.showErrorMessages(this.inputTargets); if (generalErrors.length) this.showGlobalError(generalErrors.join(' '));else if (!errors.length) this.showGlobalError(); } @@ -667,7 +931,7 @@ export default class extends Controller { const stepFields = {}; const inputs = this.inputsForStep(step); inputs.forEach(input => { - const value = this.inputValue(input); + const value = input.dataset.popupFieldKind === 'phone' ? this.identityValue(input) : this.inputValue(input); const key = input.dataset.popupFieldKey || input.name; stepFields[key] = value; payload.metadata.fields[key] = value; diff --git a/lib/core/event.cjs b/lib/core/event.cjs index 7df7edd4..8d0cfdc8 100644 --- a/lib/core/event.cjs +++ b/lib/core/event.cjs @@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { exports.default = void 0; var _errors = require("../errors"); class Event { - static events = ['session-set', 'utm-set', 'forms:collected', 'form:completed', 'popup:mounted', 'popup:opened', 'popup:closed', 'alert:shown', 'alert:dismissed', 'alert:accepted', 'webchat:mounted', 'webchat:opened', 'webchat:closed', 'webchat:message:sent', 'webchat:message:received', 'cart.added']; + static events = ['session-set', 'utm-set', 'forms:collected', 'form:completed', 'popup:mounted', 'popup:opened', 'popup:closed', 'alert:shown', 'alert:dismissed', 'alert:accepted', 'activity:occurred', 'webchat:mounted', 'webchat:opened', 'webchat:closed', 'webchat:message:sent', 'webchat:message:received', 'cart.added']; static valid(name) { return Event.exists(name); } diff --git a/lib/core/event.js b/lib/core/event.js index 0a0a8387..87eee6ad 100644 --- a/lib/core/event.js +++ b/lib/core/event.js @@ -1,6 +1,6 @@ import { InvalidEvent } from '../errors'; export default class Event { - static events = ['session-set', 'utm-set', 'forms:collected', 'form:completed', 'popup:mounted', 'popup:opened', 'popup:closed', 'alert:shown', 'alert:dismissed', 'alert:accepted', 'webchat:mounted', 'webchat:opened', 'webchat:closed', 'webchat:message:sent', 'webchat:message:received', 'cart.added']; + static events = ['session-set', 'utm-set', 'forms:collected', 'form:completed', 'popup:mounted', 'popup:opened', 'popup:closed', 'alert:shown', 'alert:dismissed', 'alert:accepted', 'activity:occurred', 'webchat:mounted', 'webchat:opened', 'webchat:closed', 'webchat:message:sent', 'webchat:message:received', 'cart.added']; static valid(name) { return Event.exists(name); } diff --git a/lib/hellotext.cjs b/lib/hellotext.cjs index 24299b11..28994758 100644 --- a/lib/hellotext.cjs +++ b/lib/hellotext.cjs @@ -10,8 +10,27 @@ var _models = require("./models"); var _errors = require("./errors"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +// The campaign parameters display rules can target. `utm_term` and `utm_content` are not +// among them, so a link carrying only those does not stand for a campaign here. +const CAMPAIGN_RULE_KEYS = ['source', 'medium', 'campaign']; +const ACTIVITY_RULE_FIELDS = { + 'product.viewed': 'activity.product_viewed', + 'cart.added': 'activity.cart_added', + 'order.placed': 'activity.purchase_completed', + 'product.purchased': 'activity.purchase_completed', + 'form.completed': 'activity.form_completed' +}; class Hellotext { static eventEmitter = new _core.Event(); + static activities = new Set(); + static pageViews = 1; + static visitCampaign = {}; + static visitorType = 'new'; + static visitBusinessId; + static lastPageUrl; + static lastPageRoute; + static pageStartedAt; + static visitStartedAt; static forms; static business; static popup; @@ -20,6 +39,12 @@ class Hellotext { static push; static alert; static initializationVersion = 0; + static popupEvaluationVersion = 0; + static identificationVersion = 0; + static identificationPending = false; + static identificationCompletion; + static cancelIdentificationWait; + static popupRuntime; /** * initialize the module. @@ -27,9 +52,13 @@ class Hellotext { * @param { Configuration } config */ static async initialize(business, config = {}) { + const previousBusinessId = this.visitBusinessId; + const previousSession = this.session; const initializationVersion = ++this.initializationVersion; + this.popupEvaluationVersion += 1; this.popup?.unmount?.(); this.popup = undefined; + this.popupRuntime = undefined; this.alert?.dispose(); this.alert = null; this.push?.dispose(); @@ -42,18 +71,22 @@ class Hellotext { ...config }); _models.Session.initialize(this.page); + if (this.identificationPending && (previousBusinessId !== business || previousSession !== this.session)) { + this.identificationVersion += 1; + this.identificationPending = false; + this.cancelIdentificationPolling(); + } + this.initializeVisitSignals(business); + this.forms?.mutationObserver?.disconnect(); this.forms = new _models.FormCollection(); this.query = new _models.Query(); const businessData = await businessContext.hydrate(); if (this.business !== businessContext) return; + let stagedPush = null; + let stagedAlertData = null; if (config.push !== false && businessData?.push?.public_key && _models.Push.supported) { - this.push = new _models.Push(businessData.push); - this.push.initialize().catch(error => { - console.warn('Hellotext Push initialization failed:', error); - }); - if (businessData.alert?.html) { - this.alert = new _models.Alert(businessData.alert, businessContext, this.push); - } + stagedPush = new _models.Push(businessData.push); + if (businessData.alert?.html) stagedAlertData = businessData.alert; } const popupConfig = config.popup === false ? false : this.deepMergePlainObjects(businessData && businessData.popup || {}, config.popup || {}); const webchatConfig = config.webchat === false ? false : this.mergeWebchatConfig(businessData && businessData.webchat || {}, config.webchat || {}); @@ -80,19 +113,20 @@ class Hellotext { ...popupConfig }; _core.Configuration.popup.assign(resolvedPopupConfig); - widgetLoads.push(_models.Popup.load(resolvedPopupConfig.id, { - container: resolvedPopupConfig.container, - shouldMount: () => { - return this.business === businessContext && this.initializationVersion === initializationVersion; - } - }).then(popup => { - if (this.business === businessContext && this.initializationVersion === initializationVersion) { - this.popup = popup; - } - })); + this.popupRuntime = { + config: resolvedPopupConfig, + businessContext, + initializationVersion + }; + if (!this.identificationPending) widgetLoads.push(this.loadPopup(this.popupRuntime)); } await Promise.all(widgetLoads); if (this.business !== businessContext || this.initializationVersion !== initializationVersion) return; + this.push = stagedPush; + this.alert = stagedAlertData ? new _models.Alert(stagedAlertData, businessContext, stagedPush) : null; + this.push?.initialize().catch(error => { + console.warn('Hellotext Push initialization failed:', error); + }); if (typeof MutationObserver !== 'undefined') { this.forms.collectExistingFormsOnPage(); } @@ -116,6 +150,25 @@ class Hellotext { }); return result; } + static async loadPopup(runtime = this.popupRuntime) { + if (!runtime || this.identificationPending) return null; + const evaluationVersion = ++this.popupEvaluationVersion; + const current = () => { + return this.popupRuntime === runtime && this.business === runtime.businessContext && this.initializationVersion === runtime.initializationVersion && this.popupEvaluationVersion === evaluationVersion && !this.identificationPending; + }; + const popup = await _models.Popup.load(runtime.config.id, { + container: runtime.config.container, + shouldMount: current + }); + if (current()) this.popup = popup; + return popup; + } + static reloadPopup() { + this.popupEvaluationVersion += 1; + this.popup?.unmount?.(); + this.popup = undefined; + return this.loadPopup(); + } static isPlainObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -131,6 +184,9 @@ class Hellotext { if (this.notInitialized) { throw new _errors.NotInitializedError(); } + const business = this.business; + const session = this.session; + const visitBusinessId = this.visitBusinessId; const headers = { ...(params && params.headers || {}), ...this.headers @@ -141,14 +197,14 @@ class Hellotext { }; const pageInstance = params && params.url ? new _models.Page(params.url) : this.page; const body = { - session: this.session, + session, user_parameters, action, ...params, ...pageInstance.trackingData }; delete body.headers; - return await _api.default.events.create({ + const response = await _api.default.events.create({ headers, body, // Track is the SDK's unload-sensitive analytics path. Keepalive belongs @@ -157,6 +213,137 @@ class Hellotext { // stronger request/response or interaction contracts. keepalive: (0, _api.keepaliveFor)(body) }); + const trackedAt = this.trackedAtMilliseconds(params.tracked_at); + if (response.succeeded && this.business === business && this.session === session && this.visitBusinessId === visitBusinessId && (trackedAt === null || trackedAt >= this.visitStartedAt)) this.recordActivity(action); + return response; + } + static recordActivity(action) { + const field = ACTIVITY_RULE_FIELDS[action]; + if (!field) return; + this.activities.add(field); + this.writeStorage('sessionStorage', this.visitStorageKey('activities'), JSON.stringify([...this.activities])); + this.eventEmitter.dispatch('activity:occurred', { + action, + field + }); + } + static trackedAtMilliseconds(value) { + if (value === undefined || value === null || value === '') return null; + if (typeof value === 'number') { + if (!Number.isFinite(value)) return Number.NaN; + + // Public tracking timestamps use Unix seconds. Accept millisecond values as well so + // integrations that already pass Date#getTime() do not get silently rejected. + return value < 1_000_000_000_000 ? value * 1000 : value; + } + return new Date(value).getTime(); + } + static initializeVisitSignals(businessId) { + const businessChanged = this.visitBusinessId !== businessId; + this.visitBusinessId = businessId; + if (businessChanged) { + this.pageViews = 0; + this.lastPageUrl = undefined; + this.lastPageRoute = undefined; + this.activities = new Set(this.readStoredActivities()); + this.visitCampaign = this.readStoredVisitCampaign(); + const storedVisitorType = this.readStorage('sessionStorage', this.visitStorageKey('visitor-type')); + this.visitorType = ['new', 'returning'].includes(storedVisitorType) ? storedVisitorType : undefined; + if (!this.visitorType) { + this.visitorType = this.readStorage('localStorage', this.visitStorageKey('seen')) ? 'returning' : 'new'; + this.writeStorage('sessionStorage', this.visitStorageKey('visitor-type'), this.visitorType); + this.writeStorage('localStorage', this.visitStorageKey('seen'), '1'); + } + const storedVisitStartedAt = Number(this.readStorage('sessionStorage', this.visitStorageKey('started-at'))); + this.visitStartedAt = Number.isFinite(storedVisitStartedAt) && storedVisitStartedAt > 0 ? storedVisitStartedAt : this.initialPageStartedAt(); + this.writeStorage('sessionStorage', this.visitStorageKey('started-at'), String(this.visitStartedAt)); + } + this.rememberVisitCampaign(_models.UTM.paramsFrom(window.location.search)); + if (businessChanged || this.lastPageRoute !== this.pageRoute()) this.recordPageView(); + } + + /** + * Remembers the campaign this visit arrived with, for as long as the tab lives — the same + * span as the other visit signals. + * + * Display rules read it when the URL no longer carries the parameters, which is the common + * case: the visitor moves past the landing page, or the site strips them from the URL once + * its analytics have read them. Persisted attribution answers a different question and is + * left alone: `hello_utm` still records only a complete source and medium pair, and still + * outlives the visit. + */ + static rememberVisitCampaign(params) { + const campaign = Object.fromEntries(CAMPAIGN_RULE_KEYS.flatMap(key => { + const value = typeof params?.[key] === 'string' ? params[key].trim() : ''; + return value === '' ? [] : [[key, value]]; + })); + if (Object.keys(campaign).length === 0) return; + this.visitCampaign = campaign; + this.writeStorage('sessionStorage', this.visitStorageKey('campaign'), JSON.stringify(campaign)); + } + static readStoredVisitCampaign() { + try { + const stored = JSON.parse(this.readStorage('sessionStorage', this.visitStorageKey('campaign')) || '{}'); + if (stored === null || typeof stored !== 'object' || Array.isArray(stored)) return {}; + return Object.fromEntries(Object.entries(stored).filter(([key, value]) => CAMPAIGN_RULE_KEYS.includes(key) && typeof value === 'string')); + } catch (_) { + return {}; + } + } + static recordPageView() { + const key = this.visitStorageKey('page-views'); + const stored = Number(this.readStorage('sessionStorage', key)); + const firstPageInDocument = !this.lastPageUrl; + this.pageViews = Number.isInteger(stored) && stored > 0 ? stored + 1 : this.pageViews + 1; + this.lastPageUrl = window.location.href; + this.lastPageRoute = this.pageRoute(); + this.pageStartedAt = firstPageInDocument ? this.initialPageStartedAt() : Date.now(); + this.writeStorage('sessionStorage', key, String(this.pageViews)); + } + static initialPageStartedAt() { + const navigationUrl = window.performance?.getEntriesByType?.('navigation')?.[0]?.name; + if (navigationUrl) { + if (this.pageRoute(navigationUrl) !== this.pageRoute()) return Date.now(); + } + return window.performance?.timeOrigin || Date.now(); + } + static pageRoute(value = window.location.href) { + const currentUrl = window.location?.href || document.location?.href || 'http://localhost/'; + const url = new URL(value || currentUrl, currentUrl); + const hashRoute = url.hash.match(/^#!?\/[^?]*/)?.[0]; + return `${url.pathname}${hashRoute?.replace(/^#!/, '#') || ''}`; + } + static readStoredActivities() { + try { + const stored = JSON.parse(this.readStorage('sessionStorage', this.visitStorageKey('activities')) || '[]'); + return Array.isArray(stored) ? stored.filter(field => Object.values(ACTIVITY_RULE_FIELDS).includes(field)) : []; + } catch (_) { + return []; + } + } + static visitStorageKey(name) { + return `hellotext:business:${this.visitBusinessId}:${name}`; + } + static storage(name) { + try { + return window[name]; + } catch (_) { + return null; + } + } + static readStorage(name, key) { + try { + return this.storage(name)?.getItem(key); + } catch (_) { + return null; + } + } + static writeStorage(name, key, value) { + try { + this.storage(name)?.setItem(key, value); + } catch (_) { + // Storage may be unavailable in privacy-restricted browser contexts. + } } /** @@ -182,15 +369,109 @@ class Hellotext { }) }); } - const response = await _api.default.identifications.create({ - user_id: externalId, - ...options - }); - if (response.succeeded) { - _models.User.remember(externalId, options.source, fingerprint); + const identificationVersion = ++this.identificationVersion; + const businessId = this.visitBusinessId; + const session = this.session; + this.identificationPending = true; + this.cancelIdentificationPolling(); + this.popupEvaluationVersion += 1; + this.popup?.unmount?.(); + this.popup = undefined; + let response; + try { + response = await _api.default.identifications.create({ + user_id: externalId, + ...options + }); + } catch (error) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + this.identificationPending = false; + this.reloadPopup(); + } + throw error; + } + if (response.failed) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + this.identificationPending = false; + this.reloadPopup(); + } + return response; + } + let receipt; + try { + receipt = (await response.json())?.identification_receipt; + } catch (_) { + // Older deployments returned an unreadable or empty success body. } + if (!receipt) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + _models.User.remember(externalId, options.source, fingerprint); + this.identificationPending = false; + this.reloadPopup(); + } + return response; + } + this.identificationCompletion = this.finishIdentification({ + receipt, + identificationVersion, + businessId, + session, + externalId, + source: options.source, + fingerprint + }).catch(() => {}); return response; } + static identificationCurrent(version, businessId, session) { + return this.identificationVersion === version && this.visitBusinessId === businessId && this.session === session; + } + static async finishIdentification(details) { + const delays = [0, 100, 250, 500, 1000, 2000, 4000, 8000]; + for (const delay of delays) { + if (!this.identificationCurrent(details.identificationVersion, details.businessId, details.session)) return; + if (delay > 0 && !(await this.waitForIdentificationPoll(delay))) return; + if (!this.identificationCurrent(details.identificationVersion, details.businessId, details.session)) return; + let response; + try { + response = await _api.default.identifications.status(details.receipt); + } catch (_) { + continue; + } + if (!this.identificationCurrent(details.identificationVersion, details.businessId, details.session)) return; + if (response.data.status === 202) continue; + if (!response.succeeded) { + if (response.data.status === 429 || response.data.status >= 500) continue; + if (response.data.status === 422) { + this.identificationPending = false; + this.cancelIdentificationPolling(); + await this.reloadPopup(); + } + return; + } + if (!this.identificationCurrent(details.identificationVersion, details.businessId, details.session)) return; + _models.User.remember(details.externalId, details.source, details.fingerprint); + this.identificationPending = false; + this.cancelIdentificationPolling(); + await this.reloadPopup(); + return; + } + } + static waitForIdentificationPoll(delay) { + return new Promise(resolve => { + const timer = setTimeout(() => { + this.cancelIdentificationWait = undefined; + resolve(true); + }, delay); + this.cancelIdentificationWait = () => { + clearTimeout(timer); + this.cancelIdentificationWait = undefined; + resolve(false); + }; + }); + } + static cancelIdentificationPolling() { + this.cancelIdentificationWait?.(); + } /** * Clears the user session, use when the user logs out to clear the hello cookies diff --git a/lib/hellotext.js b/lib/hellotext.js index 39c9967c..5afbc7cd 100644 --- a/lib/hellotext.js +++ b/lib/hellotext.js @@ -1,9 +1,29 @@ import { Configuration, Event } from './core'; import API, { Response, keepaliveFor } from './api'; -import { Alert, Business, Fingerprint, FormCollection, Page, Popup, Push, Query, Session, User, Webchat, WhatsAppWidget } from './models'; +import { Alert, Business, Fingerprint, FormCollection, Page, Popup, Push, Query, Session, User, UTM, Webchat, WhatsAppWidget } from './models'; import { NotInitializedError } from './errors'; + +// The campaign parameters display rules can target. `utm_term` and `utm_content` are not +// among them, so a link carrying only those does not stand for a campaign here. +const CAMPAIGN_RULE_KEYS = ['source', 'medium', 'campaign']; +const ACTIVITY_RULE_FIELDS = { + 'product.viewed': 'activity.product_viewed', + 'cart.added': 'activity.cart_added', + 'order.placed': 'activity.purchase_completed', + 'product.purchased': 'activity.purchase_completed', + 'form.completed': 'activity.form_completed' +}; class Hellotext { static eventEmitter = new Event(); + static activities = new Set(); + static pageViews = 1; + static visitCampaign = {}; + static visitorType = 'new'; + static visitBusinessId; + static lastPageUrl; + static lastPageRoute; + static pageStartedAt; + static visitStartedAt; static forms; static business; static popup; @@ -12,6 +32,12 @@ class Hellotext { static push; static alert; static initializationVersion = 0; + static popupEvaluationVersion = 0; + static identificationVersion = 0; + static identificationPending = false; + static identificationCompletion; + static cancelIdentificationWait; + static popupRuntime; /** * initialize the module. @@ -19,9 +45,13 @@ class Hellotext { * @param { Configuration } config */ static async initialize(business, config = {}) { + const previousBusinessId = this.visitBusinessId; + const previousSession = this.session; const initializationVersion = ++this.initializationVersion; + this.popupEvaluationVersion += 1; this.popup?.unmount?.(); this.popup = undefined; + this.popupRuntime = undefined; this.alert?.dispose(); this.alert = null; this.push?.dispose(); @@ -34,18 +64,22 @@ class Hellotext { ...config }); Session.initialize(this.page); + if (this.identificationPending && (previousBusinessId !== business || previousSession !== this.session)) { + this.identificationVersion += 1; + this.identificationPending = false; + this.cancelIdentificationPolling(); + } + this.initializeVisitSignals(business); + this.forms?.mutationObserver?.disconnect(); this.forms = new FormCollection(); this.query = new Query(); const businessData = await businessContext.hydrate(); if (this.business !== businessContext) return; + let stagedPush = null; + let stagedAlertData = null; if (config.push !== false && businessData?.push?.public_key && Push.supported) { - this.push = new Push(businessData.push); - this.push.initialize().catch(error => { - console.warn('Hellotext Push initialization failed:', error); - }); - if (businessData.alert?.html) { - this.alert = new Alert(businessData.alert, businessContext, this.push); - } + stagedPush = new Push(businessData.push); + if (businessData.alert?.html) stagedAlertData = businessData.alert; } const popupConfig = config.popup === false ? false : this.deepMergePlainObjects(businessData && businessData.popup || {}, config.popup || {}); const webchatConfig = config.webchat === false ? false : this.mergeWebchatConfig(businessData && businessData.webchat || {}, config.webchat || {}); @@ -72,19 +106,20 @@ class Hellotext { ...popupConfig }; Configuration.popup.assign(resolvedPopupConfig); - widgetLoads.push(Popup.load(resolvedPopupConfig.id, { - container: resolvedPopupConfig.container, - shouldMount: () => { - return this.business === businessContext && this.initializationVersion === initializationVersion; - } - }).then(popup => { - if (this.business === businessContext && this.initializationVersion === initializationVersion) { - this.popup = popup; - } - })); + this.popupRuntime = { + config: resolvedPopupConfig, + businessContext, + initializationVersion + }; + if (!this.identificationPending) widgetLoads.push(this.loadPopup(this.popupRuntime)); } await Promise.all(widgetLoads); if (this.business !== businessContext || this.initializationVersion !== initializationVersion) return; + this.push = stagedPush; + this.alert = stagedAlertData ? new Alert(stagedAlertData, businessContext, stagedPush) : null; + this.push?.initialize().catch(error => { + console.warn('Hellotext Push initialization failed:', error); + }); if (typeof MutationObserver !== 'undefined') { this.forms.collectExistingFormsOnPage(); } @@ -108,6 +143,25 @@ class Hellotext { }); return result; } + static async loadPopup(runtime = this.popupRuntime) { + if (!runtime || this.identificationPending) return null; + const evaluationVersion = ++this.popupEvaluationVersion; + const current = () => { + return this.popupRuntime === runtime && this.business === runtime.businessContext && this.initializationVersion === runtime.initializationVersion && this.popupEvaluationVersion === evaluationVersion && !this.identificationPending; + }; + const popup = await Popup.load(runtime.config.id, { + container: runtime.config.container, + shouldMount: current + }); + if (current()) this.popup = popup; + return popup; + } + static reloadPopup() { + this.popupEvaluationVersion += 1; + this.popup?.unmount?.(); + this.popup = undefined; + return this.loadPopup(); + } static isPlainObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -123,6 +177,9 @@ class Hellotext { if (this.notInitialized) { throw new NotInitializedError(); } + const business = this.business; + const session = this.session; + const visitBusinessId = this.visitBusinessId; const headers = { ...(params && params.headers || {}), ...this.headers @@ -133,14 +190,14 @@ class Hellotext { }; const pageInstance = params && params.url ? new Page(params.url) : this.page; const body = { - session: this.session, + session, user_parameters, action, ...params, ...pageInstance.trackingData }; delete body.headers; - return await API.events.create({ + const response = await API.events.create({ headers, body, // Track is the SDK's unload-sensitive analytics path. Keepalive belongs @@ -149,6 +206,137 @@ class Hellotext { // stronger request/response or interaction contracts. keepalive: keepaliveFor(body) }); + const trackedAt = this.trackedAtMilliseconds(params.tracked_at); + if (response.succeeded && this.business === business && this.session === session && this.visitBusinessId === visitBusinessId && (trackedAt === null || trackedAt >= this.visitStartedAt)) this.recordActivity(action); + return response; + } + static recordActivity(action) { + const field = ACTIVITY_RULE_FIELDS[action]; + if (!field) return; + this.activities.add(field); + this.writeStorage('sessionStorage', this.visitStorageKey('activities'), JSON.stringify([...this.activities])); + this.eventEmitter.dispatch('activity:occurred', { + action, + field + }); + } + static trackedAtMilliseconds(value) { + if (value === undefined || value === null || value === '') return null; + if (typeof value === 'number') { + if (!Number.isFinite(value)) return Number.NaN; + + // Public tracking timestamps use Unix seconds. Accept millisecond values as well so + // integrations that already pass Date#getTime() do not get silently rejected. + return value < 1_000_000_000_000 ? value * 1000 : value; + } + return new Date(value).getTime(); + } + static initializeVisitSignals(businessId) { + const businessChanged = this.visitBusinessId !== businessId; + this.visitBusinessId = businessId; + if (businessChanged) { + this.pageViews = 0; + this.lastPageUrl = undefined; + this.lastPageRoute = undefined; + this.activities = new Set(this.readStoredActivities()); + this.visitCampaign = this.readStoredVisitCampaign(); + const storedVisitorType = this.readStorage('sessionStorage', this.visitStorageKey('visitor-type')); + this.visitorType = ['new', 'returning'].includes(storedVisitorType) ? storedVisitorType : undefined; + if (!this.visitorType) { + this.visitorType = this.readStorage('localStorage', this.visitStorageKey('seen')) ? 'returning' : 'new'; + this.writeStorage('sessionStorage', this.visitStorageKey('visitor-type'), this.visitorType); + this.writeStorage('localStorage', this.visitStorageKey('seen'), '1'); + } + const storedVisitStartedAt = Number(this.readStorage('sessionStorage', this.visitStorageKey('started-at'))); + this.visitStartedAt = Number.isFinite(storedVisitStartedAt) && storedVisitStartedAt > 0 ? storedVisitStartedAt : this.initialPageStartedAt(); + this.writeStorage('sessionStorage', this.visitStorageKey('started-at'), String(this.visitStartedAt)); + } + this.rememberVisitCampaign(UTM.paramsFrom(window.location.search)); + if (businessChanged || this.lastPageRoute !== this.pageRoute()) this.recordPageView(); + } + + /** + * Remembers the campaign this visit arrived with, for as long as the tab lives — the same + * span as the other visit signals. + * + * Display rules read it when the URL no longer carries the parameters, which is the common + * case: the visitor moves past the landing page, or the site strips them from the URL once + * its analytics have read them. Persisted attribution answers a different question and is + * left alone: `hello_utm` still records only a complete source and medium pair, and still + * outlives the visit. + */ + static rememberVisitCampaign(params) { + const campaign = Object.fromEntries(CAMPAIGN_RULE_KEYS.flatMap(key => { + const value = typeof params?.[key] === 'string' ? params[key].trim() : ''; + return value === '' ? [] : [[key, value]]; + })); + if (Object.keys(campaign).length === 0) return; + this.visitCampaign = campaign; + this.writeStorage('sessionStorage', this.visitStorageKey('campaign'), JSON.stringify(campaign)); + } + static readStoredVisitCampaign() { + try { + const stored = JSON.parse(this.readStorage('sessionStorage', this.visitStorageKey('campaign')) || '{}'); + if (stored === null || typeof stored !== 'object' || Array.isArray(stored)) return {}; + return Object.fromEntries(Object.entries(stored).filter(([key, value]) => CAMPAIGN_RULE_KEYS.includes(key) && typeof value === 'string')); + } catch (_) { + return {}; + } + } + static recordPageView() { + const key = this.visitStorageKey('page-views'); + const stored = Number(this.readStorage('sessionStorage', key)); + const firstPageInDocument = !this.lastPageUrl; + this.pageViews = Number.isInteger(stored) && stored > 0 ? stored + 1 : this.pageViews + 1; + this.lastPageUrl = window.location.href; + this.lastPageRoute = this.pageRoute(); + this.pageStartedAt = firstPageInDocument ? this.initialPageStartedAt() : Date.now(); + this.writeStorage('sessionStorage', key, String(this.pageViews)); + } + static initialPageStartedAt() { + const navigationUrl = window.performance?.getEntriesByType?.('navigation')?.[0]?.name; + if (navigationUrl) { + if (this.pageRoute(navigationUrl) !== this.pageRoute()) return Date.now(); + } + return window.performance?.timeOrigin || Date.now(); + } + static pageRoute(value = window.location.href) { + const currentUrl = window.location?.href || document.location?.href || 'http://localhost/'; + const url = new URL(value || currentUrl, currentUrl); + const hashRoute = url.hash.match(/^#!?\/[^?]*/)?.[0]; + return `${url.pathname}${hashRoute?.replace(/^#!/, '#') || ''}`; + } + static readStoredActivities() { + try { + const stored = JSON.parse(this.readStorage('sessionStorage', this.visitStorageKey('activities')) || '[]'); + return Array.isArray(stored) ? stored.filter(field => Object.values(ACTIVITY_RULE_FIELDS).includes(field)) : []; + } catch (_) { + return []; + } + } + static visitStorageKey(name) { + return `hellotext:business:${this.visitBusinessId}:${name}`; + } + static storage(name) { + try { + return window[name]; + } catch (_) { + return null; + } + } + static readStorage(name, key) { + try { + return this.storage(name)?.getItem(key); + } catch (_) { + return null; + } + } + static writeStorage(name, key, value) { + try { + this.storage(name)?.setItem(key, value); + } catch (_) { + // Storage may be unavailable in privacy-restricted browser contexts. + } } /** @@ -174,15 +362,109 @@ class Hellotext { }) }); } - const response = await API.identifications.create({ - user_id: externalId, - ...options - }); - if (response.succeeded) { - User.remember(externalId, options.source, fingerprint); + const identificationVersion = ++this.identificationVersion; + const businessId = this.visitBusinessId; + const session = this.session; + this.identificationPending = true; + this.cancelIdentificationPolling(); + this.popupEvaluationVersion += 1; + this.popup?.unmount?.(); + this.popup = undefined; + let response; + try { + response = await API.identifications.create({ + user_id: externalId, + ...options + }); + } catch (error) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + this.identificationPending = false; + this.reloadPopup(); + } + throw error; + } + if (response.failed) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + this.identificationPending = false; + this.reloadPopup(); + } + return response; + } + let receipt; + try { + receipt = (await response.json())?.identification_receipt; + } catch (_) { + // Older deployments returned an unreadable or empty success body. } + if (!receipt) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + User.remember(externalId, options.source, fingerprint); + this.identificationPending = false; + this.reloadPopup(); + } + return response; + } + this.identificationCompletion = this.finishIdentification({ + receipt, + identificationVersion, + businessId, + session, + externalId, + source: options.source, + fingerprint + }).catch(() => {}); return response; } + static identificationCurrent(version, businessId, session) { + return this.identificationVersion === version && this.visitBusinessId === businessId && this.session === session; + } + static async finishIdentification(details) { + const delays = [0, 100, 250, 500, 1000, 2000, 4000, 8000]; + for (const delay of delays) { + if (!this.identificationCurrent(details.identificationVersion, details.businessId, details.session)) return; + if (delay > 0 && !(await this.waitForIdentificationPoll(delay))) return; + if (!this.identificationCurrent(details.identificationVersion, details.businessId, details.session)) return; + let response; + try { + response = await API.identifications.status(details.receipt); + } catch (_) { + continue; + } + if (!this.identificationCurrent(details.identificationVersion, details.businessId, details.session)) return; + if (response.data.status === 202) continue; + if (!response.succeeded) { + if (response.data.status === 429 || response.data.status >= 500) continue; + if (response.data.status === 422) { + this.identificationPending = false; + this.cancelIdentificationPolling(); + await this.reloadPopup(); + } + return; + } + if (!this.identificationCurrent(details.identificationVersion, details.businessId, details.session)) return; + User.remember(details.externalId, details.source, details.fingerprint); + this.identificationPending = false; + this.cancelIdentificationPolling(); + await this.reloadPopup(); + return; + } + } + static waitForIdentificationPoll(delay) { + return new Promise(resolve => { + const timer = setTimeout(() => { + this.cancelIdentificationWait = undefined; + resolve(true); + }, delay); + this.cancelIdentificationWait = () => { + clearTimeout(timer); + this.cancelIdentificationWait = undefined; + resolve(false); + }; + }); + } + static cancelIdentificationPolling() { + this.cancelIdentificationWait?.(); + } /** * Clears the user session, use when the user logs out to clear the hello cookies diff --git a/lib/models/form.cjs b/lib/models/form.cjs index 1ee189fa..622dfae1 100644 --- a/lib/models/form.cjs +++ b/lib/models/form.cjs @@ -10,8 +10,9 @@ var _logo_builder = require("../builders/logo_builder"); var _sanitize_html = require("../core/sanitize_html"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } class Form { - constructor(data, element = null) { + constructor(data, element = null, visitBusinessId = _hellotext.default.visitBusinessId) { this.data = data; + this.visitBusinessId = visitBusinessId; this.element = element || document.querySelector(`[data-hello-form="${this.id}"]`) || document.createElement('form'); } async mount({ @@ -86,6 +87,7 @@ class Form { completedAt: new Date().getTime() }; localStorage.setItem(`hello-form-${this.id}`, JSON.stringify(payload)); + if (_hellotext.default.visitBusinessId === this.visitBusinessId) _hellotext.default.recordActivity('form.completed'); _hellotext.default.eventEmitter.dispatch('form:completed', payload); } get hasBeenCompleted() { diff --git a/lib/models/form.js b/lib/models/form.js index 04b926d9..f1bc30e9 100644 --- a/lib/models/form.js +++ b/lib/models/form.js @@ -3,8 +3,9 @@ import { InputBuilder } from '../builders/input_builder'; import { LogoBuilder } from '../builders/logo_builder'; import { setSanitizedRichText } from '../core/sanitize_html'; class Form { - constructor(data, element = null) { + constructor(data, element = null, visitBusinessId = Hellotext.visitBusinessId) { this.data = data; + this.visitBusinessId = visitBusinessId; this.element = element || document.querySelector(`[data-hello-form="${this.id}"]`) || document.createElement('form'); } async mount({ @@ -79,6 +80,7 @@ class Form { completedAt: new Date().getTime() }; localStorage.setItem(`hello-form-${this.id}`, JSON.stringify(payload)); + if (Hellotext.visitBusinessId === this.visitBusinessId) Hellotext.recordActivity('form.completed'); Hellotext.eventEmitter.dispatch('form:completed', payload); } get hasBeenCompleted() { diff --git a/lib/models/form_collection.cjs b/lib/models/form_collection.cjs index 0d3e3836..f6a1c786 100644 --- a/lib/models/form_collection.cjs +++ b/lib/models/form_collection.cjs @@ -13,6 +13,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de class FormCollection { constructor() { this.forms = []; + this.visitBusinessId = _hellotext.default.visitBusinessId; + this.initializationVersion = _hellotext.default.initializationVersion; this.includes = this.includes.bind(this); this.excludes = this.excludes.bind(this); this.add = this.add.bind(this); @@ -41,6 +43,7 @@ class FormCollection { throw new _errors.NotInitializedError(); } if (this.fetching) return; + if (!this.current) return; if (typeof document === 'undefined' || !('querySelectorAll' in document)) { return console.warn('Document is not defined, collection is not possible. Please make sure to initialize the library after the document is loaded.'); } @@ -50,9 +53,16 @@ class FormCollection { return _forms.default.get(id).then(response => response.json()); }); this.fetching = true; - await Promise.all(promises).then(forms => forms.forEach(this.add)).then(() => _hellotext.default.eventEmitter.dispatch('forms:collected', this)).then(() => this.fetching = false); - if (_core.Configuration.forms.autoMount) { - this.forms.forEach(form => form.mount()); + try { + const forms = await Promise.all(promises); + if (!this.current) return; + forms.forEach(this.add); + _hellotext.default.eventEmitter.dispatch('forms:collected', this); + if (_core.Configuration.forms.autoMount) { + this.forms.forEach(form => form.mount()); + } + } finally { + this.fetching = false; } } forEach(callback) { @@ -70,7 +80,7 @@ class FormCollection { if (!_hellotext.default.business.enabledWhitelist) { console.warn('No whitelist has been configured. It is advised to whitelist the domain to avoid bots from submitting forms.'); } - this.forms.push(new _form.Form(data)); + this.forms.push(new _form.Form(data, null, this.visitBusinessId)); } getById(id) { return this.forms.find(form => form.id === id); @@ -87,6 +97,9 @@ class FormCollection { get length() { return this.forms.length; } + get current() { + return _hellotext.default.visitBusinessId === this.visitBusinessId && _hellotext.default.initializationVersion === this.initializationVersion; + } get #formIdsToFetch() { return Array.from(document.querySelectorAll('[data-hello-form]')).map(form => form.dataset.helloForm).filter(this.excludes); } diff --git a/lib/models/form_collection.js b/lib/models/form_collection.js index 471c82ee..9343d64f 100644 --- a/lib/models/form_collection.js +++ b/lib/models/form_collection.js @@ -6,6 +6,8 @@ import { NotInitializedError } from '../errors'; class FormCollection { constructor() { this.forms = []; + this.visitBusinessId = Hellotext.visitBusinessId; + this.initializationVersion = Hellotext.initializationVersion; this.includes = this.includes.bind(this); this.excludes = this.excludes.bind(this); this.add = this.add.bind(this); @@ -34,6 +36,7 @@ class FormCollection { throw new NotInitializedError(); } if (this.fetching) return; + if (!this.current) return; if (typeof document === 'undefined' || !('querySelectorAll' in document)) { return console.warn('Document is not defined, collection is not possible. Please make sure to initialize the library after the document is loaded.'); } @@ -43,9 +46,16 @@ class FormCollection { return API.get(id).then(response => response.json()); }); this.fetching = true; - await Promise.all(promises).then(forms => forms.forEach(this.add)).then(() => Hellotext.eventEmitter.dispatch('forms:collected', this)).then(() => this.fetching = false); - if (Configuration.forms.autoMount) { - this.forms.forEach(form => form.mount()); + try { + const forms = await Promise.all(promises); + if (!this.current) return; + forms.forEach(this.add); + Hellotext.eventEmitter.dispatch('forms:collected', this); + if (Configuration.forms.autoMount) { + this.forms.forEach(form => form.mount()); + } + } finally { + this.fetching = false; } } forEach(callback) { @@ -63,7 +73,7 @@ class FormCollection { if (!Hellotext.business.enabledWhitelist) { console.warn('No whitelist has been configured. It is advised to whitelist the domain to avoid bots from submitting forms.'); } - this.forms.push(new Form(data)); + this.forms.push(new Form(data, null, this.visitBusinessId)); } getById(id) { return this.forms.find(form => form.id === id); @@ -80,6 +90,9 @@ class FormCollection { get length() { return this.forms.length; } + get current() { + return Hellotext.visitBusinessId === this.visitBusinessId && Hellotext.initializationVersion === this.initializationVersion; + } get #formIdsToFetch() { return Array.from(document.querySelectorAll('[data-hello-form]')).map(form => form.dataset.helloForm).filter(this.excludes); } diff --git a/lib/models/page_path.cjs b/lib/models/page_path.cjs new file mode 100644 index 00000000..fd5f7755 --- /dev/null +++ b/lib/models/page_path.cjs @@ -0,0 +1,124 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = exports.PagePath = void 0; +/** + * The one form a page path takes whenever a display rule compares it. + * + * A merchant types, pastes or picks a path; the browser reports `location.pathname`. The two + * rarely agree byte for byte — a pasted URL carries its domain, a CMS adds a trailing slash, + * the browser percent-encodes accents — and every such difference used to be a rule that + * silently never matched. Both sides go through this function before they meet, so how + * either one was written stops mattering. + * + * `exact` is a whole path: one leading slash, no trailing slash, dot segments resolved, so + * `/Sale/`, `sale` and `https://shop.com/sale?x=1` all become `/sale`. `contains` is a + * fragment: it keeps a trailing slash the merchant typed (`/blog/` means the pages under the + * blog) and never gains a leading one (`rojo` must still match `/zapato-rojo`). A fragment + * that reduces to nothing, or to `/`, would match every page, so it comes back empty for the + * caller to refuse. + * + * Kept in step with Popup::DisplayRules::PagePath and app/javascript/lib/popup_page_path.js + * in the Rails app. All three run the cases in __tests__/fixtures/page_path_cases.json. + */ +const EXACT = 'exact'; +const CONTAINS = 'contains'; +const CONTAINS_OPERATORS = ['contains', 'does_not_contain']; +const WEB_ORIGIN = /^https?:\/\/[^/?#]+/i; +const SCHEME_RELATIVE_ORIGIN = /^\/\/[^/?#]+/; +const SCHEME = /^[a-z][a-z0-9+.-]*:/i; +const BARE_HOST_WITH_PATH = /^(?:(?:[a-z0-9-]+\.)+[a-z]{2,}|localhost)(?::\d+)?\//i; +const ENCODED_RUN = /(?:%[0-9a-f]{2})+/gi; +const RESERVED_ESCAPE = /%(?:21|23|24|25|26|27|28|29|2a|2b|2c|2f|3a|3b|3d|3f|40|5b|5d)/i; +const INDEX_FILE = /(^|\/)index\.(?:html?|php)$/; +class PagePath { + static EXACT = EXACT; + static CONTAINS = CONTAINS; + static modeFor(operator) { + return CONTAINS_OPERATORS.includes(operator) ? CONTAINS : EXACT; + } + static canonical(value, { + mode = EXACT + } = {}) { + let path = String(value ?? '').trim(); + if (path === '') return ''; + path = this.withoutOrigin(path); + if (path === null) return ''; + path = this.routePath(path); + path = this.decoded(path); + // Lowercasing can produce decomposed sequences, so NFC runs after it. The final sigma is + // folded because JavaScript applies its word-final rule and Ruby does not. + path = path.toLowerCase().replace(/ς/g, 'σ').normalize('NFC').replace(/\/{2,}/g, '/'); + const indexFile = INDEX_FILE.test(path); + path = path.replace(INDEX_FILE, '$1'); + if (mode === CONTAINS) { + // `/blog/index.html` names the blog page itself, not everything under it. + if (indexFile) path = path.replace(/\/$/, ''); + return path === '/' ? '' : path; + } + return this.resolved(path); + } + + // Only HTTP(S) values name pages the popup can observe. A bare domain with a path is + // rejected instead of depending on the suggestion-host limit to decide its meaning. + static withoutOrigin(path) { + if (WEB_ORIGIN.test(path)) return path.replace(WEB_ORIGIN, ''); + if (SCHEME_RELATIVE_ORIGIN.test(path)) return path.replace(SCHEME_RELATIVE_ORIGIN, ''); + if (SCHEME.test(path) || BARE_HOST_WITH_PATH.test(path)) return null; + return path; + } + + // Hash-routed sites keep their real route after `#/` or `#!/`. Any other fragment is an + // in-page anchor and names no page of its own, and a query never does. + static routePath(path) { + const hashAt = path.indexOf('#'); + const base = (hashAt === -1 ? path : path.slice(0, hashAt)).split('?')[0]; + const fragment = hashAt === -1 ? '' : path.slice(hashAt + 1); + const route = fragment.startsWith('/') ? fragment : fragment.startsWith('!/') ? fragment.slice(1) : ''; + return route ? `${base}/${route.split('?')[0]}` : base; + } + + // Decode readable characters but keep every reserved URL delimiter escaped. In particular, + // `%2F` is not `/`: decoding it would turn one path segment into two. + static decoded(path) { + return path.replace(ENCODED_RUN, run => { + const escapes = run.match(/%[0-9a-f]{2}/gi) ?? []; + const groups = []; + let group = []; + const flush = () => { + if (group.length > 0) groups.push(group.join('')); + group = []; + }; + escapes.forEach(escape => { + if (RESERVED_ESCAPE.test(escape)) { + flush(); + groups.push(escape); + } else { + group.push(escape); + } + }); + flush(); + return groups.map(group => { + if (RESERVED_ESCAPE.test(group)) return group; + try { + return decodeURIComponent(group); + } catch (_) { + return group; + } + }).join(''); + }); + } + static resolved(path) { + const segments = []; + path.split('/').forEach(segment => { + if (segment === '' || segment === '.') return; + if (segment === '..') segments.pop();else segments.push(segment); + }); + return `/${segments.join('/')}`; + } +} +exports.PagePath = PagePath; +var _default = PagePath; +exports.default = _default; \ No newline at end of file diff --git a/lib/models/page_path.js b/lib/models/page_path.js new file mode 100644 index 00000000..da29ccaa --- /dev/null +++ b/lib/models/page_path.js @@ -0,0 +1,116 @@ +/** + * The one form a page path takes whenever a display rule compares it. + * + * A merchant types, pastes or picks a path; the browser reports `location.pathname`. The two + * rarely agree byte for byte — a pasted URL carries its domain, a CMS adds a trailing slash, + * the browser percent-encodes accents — and every such difference used to be a rule that + * silently never matched. Both sides go through this function before they meet, so how + * either one was written stops mattering. + * + * `exact` is a whole path: one leading slash, no trailing slash, dot segments resolved, so + * `/Sale/`, `sale` and `https://shop.com/sale?x=1` all become `/sale`. `contains` is a + * fragment: it keeps a trailing slash the merchant typed (`/blog/` means the pages under the + * blog) and never gains a leading one (`rojo` must still match `/zapato-rojo`). A fragment + * that reduces to nothing, or to `/`, would match every page, so it comes back empty for the + * caller to refuse. + * + * Kept in step with Popup::DisplayRules::PagePath and app/javascript/lib/popup_page_path.js + * in the Rails app. All three run the cases in __tests__/fixtures/page_path_cases.json. + */ +const EXACT = 'exact'; +const CONTAINS = 'contains'; +const CONTAINS_OPERATORS = ['contains', 'does_not_contain']; +const WEB_ORIGIN = /^https?:\/\/[^/?#]+/i; +const SCHEME_RELATIVE_ORIGIN = /^\/\/[^/?#]+/; +const SCHEME = /^[a-z][a-z0-9+.-]*:/i; +const BARE_HOST_WITH_PATH = /^(?:(?:[a-z0-9-]+\.)+[a-z]{2,}|localhost)(?::\d+)?\//i; +const ENCODED_RUN = /(?:%[0-9a-f]{2})+/gi; +const RESERVED_ESCAPE = /%(?:21|23|24|25|26|27|28|29|2a|2b|2c|2f|3a|3b|3d|3f|40|5b|5d)/i; +const INDEX_FILE = /(^|\/)index\.(?:html?|php)$/; +export class PagePath { + static EXACT = EXACT; + static CONTAINS = CONTAINS; + static modeFor(operator) { + return CONTAINS_OPERATORS.includes(operator) ? CONTAINS : EXACT; + } + static canonical(value, { + mode = EXACT + } = {}) { + let path = String(value ?? '').trim(); + if (path === '') return ''; + path = this.withoutOrigin(path); + if (path === null) return ''; + path = this.routePath(path); + path = this.decoded(path); + // Lowercasing can produce decomposed sequences, so NFC runs after it. The final sigma is + // folded because JavaScript applies its word-final rule and Ruby does not. + path = path.toLowerCase().replace(/ς/g, 'σ').normalize('NFC').replace(/\/{2,}/g, '/'); + const indexFile = INDEX_FILE.test(path); + path = path.replace(INDEX_FILE, '$1'); + if (mode === CONTAINS) { + // `/blog/index.html` names the blog page itself, not everything under it. + if (indexFile) path = path.replace(/\/$/, ''); + return path === '/' ? '' : path; + } + return this.resolved(path); + } + + // Only HTTP(S) values name pages the popup can observe. A bare domain with a path is + // rejected instead of depending on the suggestion-host limit to decide its meaning. + static withoutOrigin(path) { + if (WEB_ORIGIN.test(path)) return path.replace(WEB_ORIGIN, ''); + if (SCHEME_RELATIVE_ORIGIN.test(path)) return path.replace(SCHEME_RELATIVE_ORIGIN, ''); + if (SCHEME.test(path) || BARE_HOST_WITH_PATH.test(path)) return null; + return path; + } + + // Hash-routed sites keep their real route after `#/` or `#!/`. Any other fragment is an + // in-page anchor and names no page of its own, and a query never does. + static routePath(path) { + const hashAt = path.indexOf('#'); + const base = (hashAt === -1 ? path : path.slice(0, hashAt)).split('?')[0]; + const fragment = hashAt === -1 ? '' : path.slice(hashAt + 1); + const route = fragment.startsWith('/') ? fragment : fragment.startsWith('!/') ? fragment.slice(1) : ''; + return route ? `${base}/${route.split('?')[0]}` : base; + } + + // Decode readable characters but keep every reserved URL delimiter escaped. In particular, + // `%2F` is not `/`: decoding it would turn one path segment into two. + static decoded(path) { + return path.replace(ENCODED_RUN, run => { + const escapes = run.match(/%[0-9a-f]{2}/gi) ?? []; + const groups = []; + let group = []; + const flush = () => { + if (group.length > 0) groups.push(group.join('')); + group = []; + }; + escapes.forEach(escape => { + if (RESERVED_ESCAPE.test(escape)) { + flush(); + groups.push(escape); + } else { + group.push(escape); + } + }); + flush(); + return groups.map(group => { + if (RESERVED_ESCAPE.test(group)) return group; + try { + return decodeURIComponent(group); + } catch (_) { + return group; + } + }).join(''); + }); + } + static resolved(path) { + const segments = []; + path.split('/').forEach(segment => { + if (segment === '' || segment === '.') return; + if (segment === '..') segments.pop();else segments.push(segment); + }); + return `/${segments.join('/')}`; + } +} +export default PagePath; \ No newline at end of file diff --git a/lib/models/popup_display_rules.cjs b/lib/models/popup_display_rules.cjs new file mode 100644 index 00000000..9a1bbc7c --- /dev/null +++ b/lib/models/popup_display_rules.cjs @@ -0,0 +1,261 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = exports.PopupDisplayRules = void 0; +var _page_path = require("./page_path"); +/** + * Evaluates the page-scoped display rules the server hands to the browser. + * + * The payload is `{ lanes: [[condition, ...], ...] }`: lanes are OR'd and their conditions + * are AND'd. Page URL is the one exception: repeated conditions for that field form a group + * whose positive matches are alternatives and whose exclusions are cumulative. Only lanes + * that already survived server-side evaluation are sent. + * + * No lanes means the popup may display: either it has no rules, or every rule was already + * satisfied on the server. + * + * This mirrors Popup::DisplayRules::PageEvaluator on the Rails side. Keep the two in step + * — the shared cases are covered by both suites. + */ + +const NEGATIVE_OPERATORS = ['does_not_contain', 'is_not']; +const THRESHOLD_FIELDS = ['session.scroll_depth', 'session.time_on_page', 'session.page_views']; +// A measurement is compared from either side. Kept in step with +// Popup::DisplayRules::Catalog::THRESHOLD_OPERATORS — `between` is absent on both sides +// because a lane ANDs repeated conditions, so `at_least 25` beside `at_most 75` is it. +const THRESHOLD_OPERATORS = ['at_least', 'at_most', 'greater_than', 'less_than']; +const STRING_FIELDS = ['page.path', 'page.title', 'session.referrer', 'session.language', 'session.visitor_type', 'session.browser', 'session.utm_source', 'session.utm_medium', 'session.utm_campaign']; +// The three campaign parameters are case-insensitive. URLSearchParams already decodes query +// strings before they reach a rule, including a raw `+` as a space while preserving `%2B`. +const CAMPAIGN_FIELDS = ['session.utm_source', 'session.utm_medium', 'session.utm_campaign']; +const EVENT_FIELDS = ['activity.product_viewed', 'activity.cart_added', 'activity.purchase_completed', 'activity.form_completed']; +// Text-typed fields whose values come from a fixed list. Kept in step with +// Popup::DisplayRules::Catalog on the Rails side. +const CLOSED_STRING_VALUES = { + 'session.language': ['en', 'es', 'pt', 'fr', 'nl'], + 'session.visitor_type': ['new', 'returning'], + 'session.browser': ['chrome', 'safari', 'firefox', 'edge'] +}; +const THRESHOLD_RANGES = { + 'session.scroll_depth': [1, 100], + 'session.time_on_page': [1, 3600], + 'session.page_views': [1, 1000] +}; +const MAX_STRING_VALUE_LENGTH = 512; +// Every operator a list-valued field offers comes in a positive/negative pair, so any +// authored row can be reversed. Kept in step with Popup::DisplayRules::Catalog on the +// Rails side, where `starts_with` and `ends_with` were dropped for lacking a twin. +const TEXT_OPERATORS = ['contains', 'does_not_contain', 'is', 'is_not']; +const ENTITY_OPERATORS = ['is', 'is_not']; +class PopupDisplayRules { + constructor(payload) { + // An explicit empty lane list means universal eligibility. Anything else that does + // not conform to the public payload shape must fail closed: treating a missing or + // malformed `lanes` property as the same thing would expose a popup unexpectedly. + this.valid = payload !== null && typeof payload === 'object' && !Array.isArray(payload) && Array.isArray(payload.lanes); + this.lanes = (this.valid ? payload.lanes : []).map(lane => { + // An empty lane is intentional: it means the server already satisfied every + // visitor-only condition. Any other malformed lane must fail closed instead of + // accidentally becoming that universal match. + return Array.isArray(lane) ? lane : [null]; + }); + } + get empty() { + return this.valid && this.lanes.length === 0; + } + + /** + * True when the popup requires a measurement that only grows over time, so the runtime + * knows it has to keep re-checking instead of deciding once on connect. + */ + get needsMeasurements() { + return this.lanes.some(lane => lane.some(condition => THRESHOLD_FIELDS.includes(condition?.field))); + } + get needsNavigation() { + return this.lanes.some(lane => lane.some(condition => this.validCondition(condition))); + } + get needsTitle() { + return this.lanes.some(lane => lane.some(condition => condition?.field === 'page.title')); + } + get needsActivities() { + return this.lanes.some(lane => lane.some(condition => EVENT_FIELDS.includes(condition?.field))); + } + matches(context) { + if (!this.valid) return false; + if (this.empty) return true; + return this.lanes.some(lane => this.laneMatches(lane, context)); + } + laneMatches(lane, context) { + if (!lane.every(condition => this.validCondition(condition))) return false; + const groups = new Map(); + lane.forEach(condition => { + const conditions = groups.get(condition?.field) || []; + conditions.push(condition); + groups.set(condition?.field, conditions); + }); + return [...groups.entries()].every(([field, conditions]) => this.fieldGroupMatches(field, conditions, context)); + } + + // A list-valued field is authored one row at a time, so it can carry several sibling + // conditions at once: the values included are alternatives and the ones excluded are + // cumulative. Thresholds and events hold a single condition each and stay a flat AND. + // `STRING_FIELDS` is this side's copy of the catalog's list-valued types — country is + // visitor-scoped and never reaches the browser. + fieldGroupMatches(field, conditions, context) { + if (!STRING_FIELDS.includes(field)) { + return conditions.every(condition => this.conditionMatches(condition, context)); + } + const positives = conditions.filter(condition => !NEGATIVE_OPERATORS.includes(condition?.operator)); + const negatives = conditions.filter(condition => NEGATIVE_OPERATORS.includes(condition?.operator)); + return (positives.length === 0 || positives.some(condition => this.conditionMatches(condition, context))) && negatives.every(condition => this.conditionMatches(condition, context)); + } + conditionMatches(condition, context) { + if (!this.validCondition(condition)) return false; + if (EVENT_FIELDS.includes(condition.field)) { + return context.activities?.has?.(condition.field) || context.activities?.includes?.(condition.field); + } + const actual = this.actualValue(condition.field, context); + if (THRESHOLD_FIELDS.includes(condition.field)) { + return this.thresholdMatches(condition, actual); + } + if (condition.field === 'page.path') return this.pathMatches(condition, actual, context); + return this.stringMatches(condition, actual); + } + actualValue(field, context) { + switch (field) { + // The hash rides along because a hash-routed site keeps its real route after `#/`. + case 'page.path': + return context.path === undefined || context.path === null ? context.path : `${context.path}${context.hash ?? ''}`; + case 'page.title': + return context.title; + case 'session.referrer': + return context.referrer; + case 'session.scroll_depth': + return context.scrollDepth; + case 'session.time_on_page': + return context.timeOnPage; + case 'session.page_views': + return context.pageViews; + case 'session.language': + return context.language; + case 'session.visitor_type': + return context.visitorType; + case 'session.browser': + return context.browser; + case 'session.utm_source': + return context.utm?.source; + case 'session.utm_medium': + return context.utm?.medium; + case 'session.utm_campaign': + return context.utm?.campaign; + default: + return undefined; + } + } + validCondition(condition) { + if (!condition || typeof condition !== 'object' || !Array.isArray(condition.values)) return false; + if (THRESHOLD_FIELDS.includes(condition.field)) { + const value = condition.values[0]; + const numericValue = Number(value); + const [minimum, maximum] = THRESHOLD_RANGES[condition.field]; + return THRESHOLD_OPERATORS.includes(condition.operator) && condition.values.length === 1 && (typeof value === 'number' || typeof value === 'string' && /^\d+$/.test(value)) && Number.isInteger(numericValue) && numericValue >= minimum && numericValue <= maximum; + } + if (EVENT_FIELDS.includes(condition.field)) { + return condition.operator === 'occurred' && condition.values.length === 0; + } + + // A closed set matches a whole value or none of it, so it only offers the exact pair. + // Mirrors the catalog's ENTITY_OPERATORS on the Rails side: accepting `contains` here + // would evaluate a condition the server would have refused to save. + const operators = CLOSED_STRING_VALUES[condition.field] ? ENTITY_OPERATORS : TEXT_OPERATORS; + const validStrings = STRING_FIELDS.includes(condition.field) && operators.includes(condition.operator) && condition.values.length > 0 && condition.values.every(value => typeof value === 'string' && value.trim().length > 0 && value.length <= MAX_STRING_VALUE_LENGTH); + if (!validStrings) return false; + + // Closed sets are checked here as well as on the server. A value outside the set could + // only come from a tampered payload, and an unknown one must not ride along into an + // `is not` and quietly widen who the popup reaches. + const allowed = CLOSED_STRING_VALUES[condition.field]; + return !allowed || condition.values.every(value => allowed.includes(value)); + } + + /** + * A measurement that has not been reported yet fails every comparison, including the + * ones that point downwards: "pages viewed is at most 2" must not hold before the + * runtime has counted a single page. + */ + thresholdMatches(condition, actual) { + if (actual === undefined || actual === null || actual === '') return false; + const value = Number(actual); + const expected = Number(condition.values[0]); + switch (condition.operator) { + case 'at_least': + return value >= expected; + case 'at_most': + return value <= expected; + case 'greater_than': + return value > expected; + case 'less_than': + return value < expected; + default: + return false; + } + } + + /** + * A missing value satisfies a negative operator and fails a positive one. Treating it as + * an empty string would make "title contains x" and "title does not contain x" agree, + * which breaks the exact complement the rules promise. + */ + stringMatches(condition, actual) { + const negative = NEGATIVE_OPERATORS.includes(condition.operator); + if (actual === undefined || actual === null) return negative; + + // URLSearchParams has already decoded query-string spelling in the page context. Do not + // turn literal plus signs into spaces here: `%2B` is a meaningful campaign character. + const normalize = CAMPAIGN_FIELDS.includes(condition.field) ? value => String(value).trim().toLowerCase() : value => String(value).toLowerCase(); + const value = normalize(actual); + const hit = condition.values.some(expected => this.compare(condition.operator, value, normalize(expected))); + return negative ? !hit : hit; + } + + /** + * Page URL compares canonical paths on both sides, so a value saved as `/sale` still + * matches a visitor on `/sale/`, `/SALE` or `/#/sale`, and one pasted with its domain + * still names the page. A value that reduces to nothing would match every page: the + * server refuses to save one, and a payload carrying it anyway fails closed instead of + * reaching everyone. + */ + pathMatches(condition, actual, context) { + const negative = NEGATIVE_OPERATORS.includes(condition.operator); + if (actual === undefined || actual === null) return negative; + const mode = _page_path.PagePath.modeFor(condition.operator); + const expected = condition.values.map(value => _page_path.PagePath.canonical(value, { + mode + })); + if (expected.includes('')) return false; + const path = _page_path.PagePath.canonical(actual); + const hit = expected.some(value => mode === _page_path.PagePath.CONTAINS ? path.includes(value) : path === value); + return negative ? !hit : hit; + } + compare(operator, actual, expected) { + switch (operator) { + case 'contains': + case 'does_not_contain': + return actual.includes(expected); + case 'is': + case 'is_not': + return actual === expected; + case 'starts_with': + return actual.startsWith(expected); + case 'ends_with': + return actual.endsWith(expected); + default: + return false; + } + } +} +exports.PopupDisplayRules = PopupDisplayRules; +var _default = PopupDisplayRules; +exports.default = _default; \ No newline at end of file diff --git a/lib/models/popup_display_rules.js b/lib/models/popup_display_rules.js new file mode 100644 index 00000000..68d05a8f --- /dev/null +++ b/lib/models/popup_display_rules.js @@ -0,0 +1,252 @@ +/** + * Evaluates the page-scoped display rules the server hands to the browser. + * + * The payload is `{ lanes: [[condition, ...], ...] }`: lanes are OR'd and their conditions + * are AND'd. Page URL is the one exception: repeated conditions for that field form a group + * whose positive matches are alternatives and whose exclusions are cumulative. Only lanes + * that already survived server-side evaluation are sent. + * + * No lanes means the popup may display: either it has no rules, or every rule was already + * satisfied on the server. + * + * This mirrors Popup::DisplayRules::PageEvaluator on the Rails side. Keep the two in step + * — the shared cases are covered by both suites. + */ +import { PagePath } from './page_path'; +const NEGATIVE_OPERATORS = ['does_not_contain', 'is_not']; +const THRESHOLD_FIELDS = ['session.scroll_depth', 'session.time_on_page', 'session.page_views']; +// A measurement is compared from either side. Kept in step with +// Popup::DisplayRules::Catalog::THRESHOLD_OPERATORS — `between` is absent on both sides +// because a lane ANDs repeated conditions, so `at_least 25` beside `at_most 75` is it. +const THRESHOLD_OPERATORS = ['at_least', 'at_most', 'greater_than', 'less_than']; +const STRING_FIELDS = ['page.path', 'page.title', 'session.referrer', 'session.language', 'session.visitor_type', 'session.browser', 'session.utm_source', 'session.utm_medium', 'session.utm_campaign']; +// The three campaign parameters are case-insensitive. URLSearchParams already decodes query +// strings before they reach a rule, including a raw `+` as a space while preserving `%2B`. +const CAMPAIGN_FIELDS = ['session.utm_source', 'session.utm_medium', 'session.utm_campaign']; +const EVENT_FIELDS = ['activity.product_viewed', 'activity.cart_added', 'activity.purchase_completed', 'activity.form_completed']; +// Text-typed fields whose values come from a fixed list. Kept in step with +// Popup::DisplayRules::Catalog on the Rails side. +const CLOSED_STRING_VALUES = { + 'session.language': ['en', 'es', 'pt', 'fr', 'nl'], + 'session.visitor_type': ['new', 'returning'], + 'session.browser': ['chrome', 'safari', 'firefox', 'edge'] +}; +const THRESHOLD_RANGES = { + 'session.scroll_depth': [1, 100], + 'session.time_on_page': [1, 3600], + 'session.page_views': [1, 1000] +}; +const MAX_STRING_VALUE_LENGTH = 512; +// Every operator a list-valued field offers comes in a positive/negative pair, so any +// authored row can be reversed. Kept in step with Popup::DisplayRules::Catalog on the +// Rails side, where `starts_with` and `ends_with` were dropped for lacking a twin. +const TEXT_OPERATORS = ['contains', 'does_not_contain', 'is', 'is_not']; +const ENTITY_OPERATORS = ['is', 'is_not']; +export class PopupDisplayRules { + constructor(payload) { + // An explicit empty lane list means universal eligibility. Anything else that does + // not conform to the public payload shape must fail closed: treating a missing or + // malformed `lanes` property as the same thing would expose a popup unexpectedly. + this.valid = payload !== null && typeof payload === 'object' && !Array.isArray(payload) && Array.isArray(payload.lanes); + this.lanes = (this.valid ? payload.lanes : []).map(lane => { + // An empty lane is intentional: it means the server already satisfied every + // visitor-only condition. Any other malformed lane must fail closed instead of + // accidentally becoming that universal match. + return Array.isArray(lane) ? lane : [null]; + }); + } + get empty() { + return this.valid && this.lanes.length === 0; + } + + /** + * True when the popup requires a measurement that only grows over time, so the runtime + * knows it has to keep re-checking instead of deciding once on connect. + */ + get needsMeasurements() { + return this.lanes.some(lane => lane.some(condition => THRESHOLD_FIELDS.includes(condition?.field))); + } + get needsNavigation() { + return this.lanes.some(lane => lane.some(condition => this.validCondition(condition))); + } + get needsTitle() { + return this.lanes.some(lane => lane.some(condition => condition?.field === 'page.title')); + } + get needsActivities() { + return this.lanes.some(lane => lane.some(condition => EVENT_FIELDS.includes(condition?.field))); + } + matches(context) { + if (!this.valid) return false; + if (this.empty) return true; + return this.lanes.some(lane => this.laneMatches(lane, context)); + } + laneMatches(lane, context) { + if (!lane.every(condition => this.validCondition(condition))) return false; + const groups = new Map(); + lane.forEach(condition => { + const conditions = groups.get(condition?.field) || []; + conditions.push(condition); + groups.set(condition?.field, conditions); + }); + return [...groups.entries()].every(([field, conditions]) => this.fieldGroupMatches(field, conditions, context)); + } + + // A list-valued field is authored one row at a time, so it can carry several sibling + // conditions at once: the values included are alternatives and the ones excluded are + // cumulative. Thresholds and events hold a single condition each and stay a flat AND. + // `STRING_FIELDS` is this side's copy of the catalog's list-valued types — country is + // visitor-scoped and never reaches the browser. + fieldGroupMatches(field, conditions, context) { + if (!STRING_FIELDS.includes(field)) { + return conditions.every(condition => this.conditionMatches(condition, context)); + } + const positives = conditions.filter(condition => !NEGATIVE_OPERATORS.includes(condition?.operator)); + const negatives = conditions.filter(condition => NEGATIVE_OPERATORS.includes(condition?.operator)); + return (positives.length === 0 || positives.some(condition => this.conditionMatches(condition, context))) && negatives.every(condition => this.conditionMatches(condition, context)); + } + conditionMatches(condition, context) { + if (!this.validCondition(condition)) return false; + if (EVENT_FIELDS.includes(condition.field)) { + return context.activities?.has?.(condition.field) || context.activities?.includes?.(condition.field); + } + const actual = this.actualValue(condition.field, context); + if (THRESHOLD_FIELDS.includes(condition.field)) { + return this.thresholdMatches(condition, actual); + } + if (condition.field === 'page.path') return this.pathMatches(condition, actual, context); + return this.stringMatches(condition, actual); + } + actualValue(field, context) { + switch (field) { + // The hash rides along because a hash-routed site keeps its real route after `#/`. + case 'page.path': + return context.path === undefined || context.path === null ? context.path : `${context.path}${context.hash ?? ''}`; + case 'page.title': + return context.title; + case 'session.referrer': + return context.referrer; + case 'session.scroll_depth': + return context.scrollDepth; + case 'session.time_on_page': + return context.timeOnPage; + case 'session.page_views': + return context.pageViews; + case 'session.language': + return context.language; + case 'session.visitor_type': + return context.visitorType; + case 'session.browser': + return context.browser; + case 'session.utm_source': + return context.utm?.source; + case 'session.utm_medium': + return context.utm?.medium; + case 'session.utm_campaign': + return context.utm?.campaign; + default: + return undefined; + } + } + validCondition(condition) { + if (!condition || typeof condition !== 'object' || !Array.isArray(condition.values)) return false; + if (THRESHOLD_FIELDS.includes(condition.field)) { + const value = condition.values[0]; + const numericValue = Number(value); + const [minimum, maximum] = THRESHOLD_RANGES[condition.field]; + return THRESHOLD_OPERATORS.includes(condition.operator) && condition.values.length === 1 && (typeof value === 'number' || typeof value === 'string' && /^\d+$/.test(value)) && Number.isInteger(numericValue) && numericValue >= minimum && numericValue <= maximum; + } + if (EVENT_FIELDS.includes(condition.field)) { + return condition.operator === 'occurred' && condition.values.length === 0; + } + + // A closed set matches a whole value or none of it, so it only offers the exact pair. + // Mirrors the catalog's ENTITY_OPERATORS on the Rails side: accepting `contains` here + // would evaluate a condition the server would have refused to save. + const operators = CLOSED_STRING_VALUES[condition.field] ? ENTITY_OPERATORS : TEXT_OPERATORS; + const validStrings = STRING_FIELDS.includes(condition.field) && operators.includes(condition.operator) && condition.values.length > 0 && condition.values.every(value => typeof value === 'string' && value.trim().length > 0 && value.length <= MAX_STRING_VALUE_LENGTH); + if (!validStrings) return false; + + // Closed sets are checked here as well as on the server. A value outside the set could + // only come from a tampered payload, and an unknown one must not ride along into an + // `is not` and quietly widen who the popup reaches. + const allowed = CLOSED_STRING_VALUES[condition.field]; + return !allowed || condition.values.every(value => allowed.includes(value)); + } + + /** + * A measurement that has not been reported yet fails every comparison, including the + * ones that point downwards: "pages viewed is at most 2" must not hold before the + * runtime has counted a single page. + */ + thresholdMatches(condition, actual) { + if (actual === undefined || actual === null || actual === '') return false; + const value = Number(actual); + const expected = Number(condition.values[0]); + switch (condition.operator) { + case 'at_least': + return value >= expected; + case 'at_most': + return value <= expected; + case 'greater_than': + return value > expected; + case 'less_than': + return value < expected; + default: + return false; + } + } + + /** + * A missing value satisfies a negative operator and fails a positive one. Treating it as + * an empty string would make "title contains x" and "title does not contain x" agree, + * which breaks the exact complement the rules promise. + */ + stringMatches(condition, actual) { + const negative = NEGATIVE_OPERATORS.includes(condition.operator); + if (actual === undefined || actual === null) return negative; + + // URLSearchParams has already decoded query-string spelling in the page context. Do not + // turn literal plus signs into spaces here: `%2B` is a meaningful campaign character. + const normalize = CAMPAIGN_FIELDS.includes(condition.field) ? value => String(value).trim().toLowerCase() : value => String(value).toLowerCase(); + const value = normalize(actual); + const hit = condition.values.some(expected => this.compare(condition.operator, value, normalize(expected))); + return negative ? !hit : hit; + } + + /** + * Page URL compares canonical paths on both sides, so a value saved as `/sale` still + * matches a visitor on `/sale/`, `/SALE` or `/#/sale`, and one pasted with its domain + * still names the page. A value that reduces to nothing would match every page: the + * server refuses to save one, and a payload carrying it anyway fails closed instead of + * reaching everyone. + */ + pathMatches(condition, actual, context) { + const negative = NEGATIVE_OPERATORS.includes(condition.operator); + if (actual === undefined || actual === null) return negative; + const mode = PagePath.modeFor(condition.operator); + const expected = condition.values.map(value => PagePath.canonical(value, { + mode + })); + if (expected.includes('')) return false; + const path = PagePath.canonical(actual); + const hit = expected.some(value => mode === PagePath.CONTAINS ? path.includes(value) : path === value); + return negative ? !hit : hit; + } + compare(operator, actual, expected) { + switch (operator) { + case 'contains': + case 'does_not_contain': + return actual.includes(expected); + case 'is': + case 'is_not': + return actual === expected; + case 'starts_with': + return actual.startsWith(expected); + case 'ends_with': + return actual.endsWith(expected); + default: + return false; + } + } +} +export default PopupDisplayRules; \ No newline at end of file diff --git a/lib/models/utm.cjs b/lib/models/utm.cjs index dbea4734..fadf6385 100644 --- a/lib/models/utm.cjs +++ b/lib/models/utm.cjs @@ -7,15 +7,25 @@ exports.UTM = void 0; var _cookies = require("./cookies"); class UTM { constructor() { - const urlSearchParams = new URLSearchParams(window.location.search); - const utmsFromUrl = { - source: urlSearchParams.get('utm_source'), - medium: urlSearchParams.get('utm_medium'), - campaign: urlSearchParams.get('utm_campaign'), - term: urlSearchParams.get('utm_term'), - content: urlSearchParams.get('utm_content') - }; - this.save(utmsFromUrl); + this.save(UTM.paramsFrom(window.location.search)); + } + + /** + * The campaign parameters a query string carries, keyed the way attribution stores them. + * Parameters that are absent or blank are left out rather than kept as empty values. + * + * @param {String} search - a query string such as `window.location.search` + * @returns {Object} + */ + static paramsFrom(search) { + const params = new URLSearchParams(search); + return Object.fromEntries(Object.entries({ + source: params.get('utm_source'), + medium: params.get('utm_medium'), + campaign: params.get('utm_campaign'), + term: params.get('utm_term'), + content: params.get('utm_content') + }).filter(([_, value]) => value)); } save(utmParams) { if (!utmParams.source || !utmParams.medium) return; diff --git a/lib/models/utm.js b/lib/models/utm.js index ba140d03..976315c6 100644 --- a/lib/models/utm.js +++ b/lib/models/utm.js @@ -1,15 +1,25 @@ import { Cookies } from './cookies'; class UTM { constructor() { - const urlSearchParams = new URLSearchParams(window.location.search); - const utmsFromUrl = { - source: urlSearchParams.get('utm_source'), - medium: urlSearchParams.get('utm_medium'), - campaign: urlSearchParams.get('utm_campaign'), - term: urlSearchParams.get('utm_term'), - content: urlSearchParams.get('utm_content') - }; - this.save(utmsFromUrl); + this.save(UTM.paramsFrom(window.location.search)); + } + + /** + * The campaign parameters a query string carries, keyed the way attribution stores them. + * Parameters that are absent or blank are left out rather than kept as empty values. + * + * @param {String} search - a query string such as `window.location.search` + * @returns {Object} + */ + static paramsFrom(search) { + const params = new URLSearchParams(search); + return Object.fromEntries(Object.entries({ + source: params.get('utm_source'), + medium: params.get('utm_medium'), + campaign: params.get('utm_campaign'), + term: params.get('utm_term'), + content: params.get('utm_content') + }).filter(([_, value]) => value)); } save(utmParams) { if (!utmParams.source || !utmParams.medium) return; diff --git a/package.json b/package.json index e9a388b6..5f8eeae4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hellotext/hellotext", - "version": "2.5.10", + "version": "2.6.1", "description": "Hellotext JavaScript Client", "source": "src/index.js", "main": "lib/index.cjs", diff --git a/src/api/identifications.js b/src/api/identifications.js index e0580d46..cccc547b 100644 --- a/src/api/identifications.js +++ b/src/api/identifications.js @@ -20,6 +20,20 @@ class IdentificationsAPI { return new Response(response.ok, response) } + + static async status(receipt) { + const url = new URL(`${this.endpoint}/${receipt}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + ...Hellotext.headers, + 'X-Hellotext-Session': Hellotext.session, + }, + }) + + return new Response(response.ok, response) + } } export default IdentificationsAPI diff --git a/src/api/popups.js b/src/api/popups.js index f99c105d..4320f6b0 100644 --- a/src/api/popups.js +++ b/src/api/popups.js @@ -3,6 +3,10 @@ import Hellotext from '../hellotext' import { Response } from './response' +// The display-rules contract this runtime implements. Rails only relies on the browser for page, +// session and activity rules when the runtime declares it, since an older one cannot check them. +export const POPUP_RULES_CONTRACT = '1' + class PopupsAPI { static get endpoint() { return Configuration.endpoint('public/popups') @@ -23,6 +27,12 @@ class PopupsAPI { if (!data) return null + // The server evaluates the profile half of the display rules and answers + // `eligible: false` with no markup when this visitor does not qualify. That is a + // deliberate outcome rather than an error, so it is treated the same as "nothing to + // render" instead of surfacing as a failure. + if (data.eligible === false || !data.html) return null + if (!Hellotext.business.data) { Hellotext.business.setData(data.business) Hellotext.business.setLocale(data.locale) @@ -79,7 +89,10 @@ class PopupsAPI { try { return await fetch(url, { method: 'GET', - headers: Hellotext.headers, + headers: { + ...Hellotext.headers, + 'X-Hellotext-Popup-Rules': POPUP_RULES_CONTRACT, + }, }) } catch (_) { return { ok: false } diff --git a/src/api/response.js b/src/api/response.js index b709f7fc..f7994035 100644 --- a/src/api/response.js +++ b/src/api/response.js @@ -12,6 +12,7 @@ class Response { constructor(success, response) { this.response = response this.#success = success + this.jsonPromise = null } /** @@ -27,7 +28,8 @@ class Response { * @returns {Promise<*>} */ async json() { - return await this.response.json() + this.jsonPromise ||= Promise.resolve(this.response.json()) + return await this.jsonPromise } /** diff --git a/src/controllers/message_controller.js b/src/controllers/message_controller.js index 65cf4654..8e1e28f0 100644 --- a/src/controllers/message_controller.js +++ b/src/controllers/message_controller.js @@ -133,7 +133,9 @@ export default class extends Controller { : currentScrollLeft + this.getPageScrollAmount() const fallbackScrollLeft = currentScrollLeft + this.getPageScrollAmount() - return this.clampScrollLeft(targetScrollLeft > currentScrollLeft + 1 ? targetScrollLeft : fallbackScrollLeft) + return this.clampScrollLeft( + targetScrollLeft > currentScrollLeft + 1 ? targetScrollLeft : fallbackScrollLeft, + ) } getPreviousPageScrollLeft() { @@ -146,7 +148,9 @@ export default class extends Controller { if (targetThreshold <= 1) return 0 const cardMetrics = this.getCardMetrics() - const previousPageCard = cardMetrics.find(card => card.start >= targetThreshold - 1 && card.start < currentScrollLeft - 1) + const previousPageCard = cardMetrics.find( + card => card.start >= targetThreshold - 1 && card.start < currentScrollLeft - 1, + ) const previousCard = [...cardMetrics].reverse().find(card => card.start < currentScrollLeft - 1) return this.getPageAlignedScrollLeft(previousPageCard?.start ?? previousCard?.start ?? 0) @@ -161,14 +165,16 @@ export default class extends Controller { } getCardMetrics() { - return Array.from(this.carouselContainerTarget.querySelectorAll('.message__carousel_card')).map(card => { - const start = this.getCardScrollLeft(card) - - return { - start, - end: start + card.offsetWidth, - } - }) + return Array.from(this.carouselContainerTarget.querySelectorAll('.message__carousel_card')).map( + card => { + const start = this.getCardScrollLeft(card) + + return { + start, + end: start + card.offsetWidth, + } + }, + ) } getCardScrollLeft(card) { @@ -187,7 +193,10 @@ export default class extends Controller { } clampScrollLeft(scrollLeft) { - const maxScroll = Math.max(this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth, 0) + const maxScroll = Math.max( + this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth, + 0, + ) return Math.min(Math.max(scrollLeft, 0), maxScroll) } @@ -217,7 +226,10 @@ export default class extends Controller { updateFades() { if (!this.hasCarouselContainerTarget) return - const maxScroll = Math.max(this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth, 0) + const maxScroll = Math.max( + this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth, + 0, + ) if (maxScroll <= 1) { this.hideFade(this.leftFadeTarget) diff --git a/src/controllers/popup_controller.js b/src/controllers/popup_controller.js index 717b1b0c..4ac51569 100644 --- a/src/controllers/popup_controller.js +++ b/src/controllers/popup_controller.js @@ -2,6 +2,8 @@ import { Controller } from '@hotwired/stimulus' import PopupsAPI from '../api/popups' import Hellotext from '../hellotext' +import { PopupDisplayRules } from '../models/popup_display_rules' +import { UTM } from '../models/utm' /** * An input rendered by the popup's server-side field components. @@ -70,6 +72,7 @@ import Hellotext from '../hellotext' * - device: Popup device targeting. * - hasBubble: Whether the popup starts from a bubble. * - id: Public popup identifier. + * - rules: Page-scoped display rules that survived server-side evaluation. */ export default class extends Controller { static targets = [ @@ -91,6 +94,7 @@ export default class extends Controller { device: String, hasBubble: Boolean, id: String, + rules: Object, } /** @@ -103,6 +107,8 @@ export default class extends Controller { initialize() { this.stepIndex = 0 this.resendLabel = this.hasResendButtonTarget ? this.resendButtonTarget.textContent.trim() : '' + this.rules = new PopupDisplayRules(this.rulesValue) + this.connectedAt = this.pageStartedAt() } /** @@ -114,7 +120,12 @@ export default class extends Controller { */ connect() { Hellotext.eventEmitter.dispatch('popup:mounted') + + this.deviceMatches = this.matchesDevice() + this.watchNavigation() + this.watchActivities() this.evaluateDisplay() + this.watchMeasurements() } /** @@ -124,6 +135,168 @@ export default class extends Controller { */ disconnect() { this.stopResendCooldown() + this.stopWatchingMeasurements() + this.stopWatchingNavigation() + this.stopWatchingActivities() + } + + pageStartedAt() { + if (Number.isFinite(Hellotext.pageStartedAt)) return Hellotext.pageStartedAt + + const timeOrigin = window.performance?.timeOrigin + + return Number.isFinite(timeOrigin) && timeOrigin <= Date.now() ? timeOrigin : Date.now() + } + + /** + * Merchant sites can be SPAs. Re-check client-side page/session rules whenever their + * route changes, including History API navigation which does not emit a browser event. + * The wrapper is restored only when it is still ours, so a later integration is never + * overwritten during cleanup. + */ + watchNavigation() { + if (this.onNavigation) return + + this.lastRoute = this.pageRoute() + this.onNavigation = () => this.scheduleNavigationEvaluation() + this.onTurboNavigation = () => this.scheduleNavigationEvaluation(true) + + window.addEventListener('popstate', this.onNavigation) + window.addEventListener('hashchange', this.onNavigation) + window.addEventListener('turbo:load', this.onTurboNavigation) + window.addEventListener('turbo:render', this.onTurboNavigation) + + if (this.rules.needsTitle && document.head) { + this.titleObserver = new MutationObserver(() => this.scheduleNavigationEvaluation(true)) + this.titleObserver.observe(document.head, { + childList: true, + characterData: true, + subtree: true, + }) + } + + const originalPushState = window.history.pushState + const originalReplaceState = window.history.replaceState + let navigationActive = true + + this.originalPushState = originalPushState + this.originalReplaceState = originalReplaceState + this.stopNavigationWrapper = () => { + navigationActive = false + } + this.patchedPushState = (...args) => { + const result = originalPushState.apply(window.history, args) + + // A SPA can update document.title without changing the URL. History calls are an + // explicit navigation boundary, so they must still re-evaluate title rules. + if (navigationActive) this.scheduleNavigationEvaluation(true) + return result + } + this.patchedReplaceState = (...args) => { + const result = originalReplaceState.apply(window.history, args) + + if (navigationActive) this.scheduleNavigationEvaluation(true) + return result + } + window.history.pushState = this.patchedPushState + window.history.replaceState = this.patchedReplaceState + } + + scheduleNavigationEvaluation(force = false) { + this.navigationEvaluationForced ||= force + if (this.navigationTimer) return + + this.navigationTimer = setTimeout(() => { + this.navigationTimer = undefined + + const route = this.pageRoute() + if (!this.navigationEvaluationForced && route === this.lastRoute) return + + this.navigationEvaluationForced = false + if (route !== this.lastRoute) { + Hellotext.recordPageView() + this.connectedAt = Date.now() + } + this.lastRoute = route + if (!this.displayed) this.evaluateDisplay() + }) + } + + pageRoute() { + return Hellotext.pageRoute() + } + + stopWatchingNavigation() { + this.stopNavigationWrapper?.() + this.stopNavigationWrapper = undefined + this.titleObserver?.disconnect() + this.titleObserver = undefined + + if (this.onNavigation) { + window.removeEventListener('popstate', this.onNavigation) + window.removeEventListener('hashchange', this.onNavigation) + this.onNavigation = undefined + } + if (this.onTurboNavigation) { + window.removeEventListener('turbo:load', this.onTurboNavigation) + window.removeEventListener('turbo:render', this.onTurboNavigation) + this.onTurboNavigation = undefined + } + if (this.navigationTimer) { + clearTimeout(this.navigationTimer) + this.navigationTimer = undefined + } + if (window.history.pushState === this.patchedPushState) { + window.history.pushState = this.originalPushState + } + if (window.history.replaceState === this.patchedReplaceState) { + window.history.replaceState = this.originalReplaceState + } + + this.patchedPushState = undefined + this.patchedReplaceState = undefined + this.originalPushState = undefined + this.originalReplaceState = undefined + this.navigationEvaluationForced = false + } + + /** + * Scroll depth and time on page only grow, so a popup gated on them cannot be decided + * once on connect. Watching starts only when a rule actually needs a measurement, so a + * popup without one adds no listeners and no timer. + */ + watchMeasurements() { + if (this.displayed || !this.rules.needsMeasurements) return + + this.onScroll = () => this.evaluateDisplay() + window.addEventListener('scroll', this.onScroll, { passive: true }) + this.measurementTimer = setInterval(() => this.evaluateDisplay(), 1000) + } + + stopWatchingMeasurements() { + if (this.onScroll) { + window.removeEventListener('scroll', this.onScroll) + this.onScroll = undefined + } + + if (this.measurementTimer) { + clearInterval(this.measurementTimer) + this.measurementTimer = undefined + } + } + + watchActivities() { + if (this.displayed || !this.rules.needsActivities || this.onActivity) return + + this.onActivity = () => this.evaluateDisplay() + Hellotext.on('activity:occurred', this.onActivity) + } + + stopWatchingActivities() { + if (!this.onActivity) return + + Hellotext.removeEventListener('activity:occurred', this.onActivity) + this.onActivity = undefined } /** @@ -261,14 +434,134 @@ export default class extends Controller { * @returns {void} */ evaluateDisplay() { - if (this.dismissed || !this.matchesDevice()) { + if (this.dismissed || !this.deviceMatches) { + this.element.hidden = true + return + } + + if (this.displayed) return + + if (!this.rules.matches(this.pageContext())) { this.element.hidden = true return } + // A popup counts as shown only once it actually displays. Rules matching is not + // enough: a visitor who never scrolls far enough never sees it. + this.displayed = true + this.stopWatchingMeasurements() + this.stopWatchingActivities() this.showInitialState() } + pageContext() { + return { + url: window.location.href, + path: window.location.pathname, + hash: window.location.hash, + title: document.title, + referrer: document.referrer || undefined, + scrollDepth: this.scrollDepth(), + timeOnPage: Math.floor((Date.now() - this.connectedAt) / 1000), + pageViews: Hellotext.pageViews, + language: this.browserLanguage(), + visitorType: Hellotext.visitorType, + browser: this.browserName(), + utm: this.currentUtmParams(), + activities: Hellotext.activities, + } + } + + /** + * The campaign this visit arrived with. A URL carrying source, medium or campaign answers + * for itself and becomes the visit's campaign, so a SPA route that adds parameters is + * picked up at the next evaluation. + * + * Without any of them in the URL the campaign the visit started with still applies, which + * is what keeps a rule true after the site navigates past its landing URL or strips the + * parameters from it. Persisted attribution is deliberately not the fallback: `hello_utm` + * outlives the visit by years and would let an old campaign target a visitor who arrived + * from somewhere else entirely. + * + * The URL's parameters replace the remembered ones rather than merging with them, so a + * rule never pairs the source of one campaign with the name of another. + */ + currentUtmParams() { + const hashSearch = window.location.hash.match(/^#!?\/[^?]*\?(.*)$/)?.[1] + const hashCampaign = this.popupUtmParams(UTM.paramsFrom(hashSearch)) + const queryCampaign = this.popupUtmParams(UTM.paramsFrom(window.location.search)) + const current = Object.keys(queryCampaign).length > 0 ? queryCampaign : hashCampaign + + if (Object.keys(current).length > 0) { + Hellotext.rememberVisitCampaign(current) + return current + } + + return this.popupUtmParams(Hellotext.visitCampaign) + } + + // Only the three parameters Rules can target, without the blanks. Capitalization and `+` + // are left alone here and settled when the values are compared, so the campaign a rule + // holds reads the way the merchant wrote it. + popupUtmParams(params) { + return Object.fromEntries( + Object.entries(params || {}).flatMap(([key, value]) => { + if (!['source', 'medium', 'campaign'].includes(key) || typeof value !== 'string') return [] + + return value.trim() === '' ? [] : [[key, value.trim()]] + }), + ) + } + + /** + * Names the browser, or nothing when it is not one of the four the catalog offers. + * + * User-Agent Client Hints answer this without parsing when they exist. Where they do not + * — Safari and Firefox — the user agent string is the only source, and its order matters: + * Edge claims to be Chrome, and Chrome claims to be Safari. Testing from the most + * specific claim to the least is what keeps each from answering for the others. + * + * An unrecognised browser reports nothing rather than a guess, so `is` never matches on a + * mistake and `is not` never excludes on one. + */ + browserName() { + const brands = window.navigator.userAgentData?.brands + if (Array.isArray(brands)) { + const brand = brands.map(({ brand }) => brand?.toLowerCase() || '') + if (brand.some(name => name.includes('edge'))) return 'edge' + if (brand.some(name => name.includes('opera') || name.includes('samsung'))) return undefined + if (brand.some(name => name.includes('chrome'))) return 'chrome' + } + + const agent = window.navigator.userAgent?.toLowerCase() || '' + if (/edg([ea]|ios)?\//.test(agent)) return 'edge' + if (agent.includes('opr/') || agent.includes('opera/') || agent.includes('samsungbrowser/')) + return undefined + if (agent.includes('firefox/') || agent.includes('fxios/')) return 'firefox' + if (agent.includes('chrome/') || agent.includes('crios/')) return 'chrome' + if (agent.includes('safari/')) return 'safari' + + return undefined + } + + browserLanguage() { + const language = window.navigator.languages?.[0] || window.navigator.language + + return language?.split('-')[0]?.toLowerCase() + } + + /** + * Percentage of the document the visitor has reached, counting the viewport itself. A + * page shorter than the viewport has nothing to scroll, so it reads as fully seen rather + * than dividing by zero. + */ + scrollDepth() { + const height = Math.max(document.documentElement.scrollHeight, window.innerHeight) + const viewed = window.scrollY + window.innerHeight + + return Math.max(0, Math.min(100, Math.round((viewed / height) * 100))) + } + /** * Choose the launcher or immediate dialog without resetting entered form values. * Set both surface states explicitly because a reconnect can reuse modified DOM. @@ -353,14 +646,15 @@ export default class extends Controller { /** * Format a local identity for completion copy when backend route data is absent. - * Phone prefixes and leading-zero removal apply only to this display fallback; - * submissionPayload() still sends the original field value. + * Phone prefixes and leading-zero removal are used for both completion copy and submission, + * so the destination the visitor sees is the destination the backend receives. * * @param {PopupInput} input - Email or phone field containing a string value. * @returns {string} Trimmed identity with the configured phone prefix when needed. */ identityValue(input) { const value = this.inputValue(input).trim() + if (!value) return '' if (input.dataset.popupFieldKind !== 'phone' || value.startsWith('+')) return value const prefix = input.dataset.popupPhonePrefix @@ -716,6 +1010,8 @@ export default class extends Controller { const errors = data.errors || [] const generalErrors = [] + const invalidInputs = [] + errors.forEach(error => { const input = this.inputForError(error) if (!input) { @@ -724,9 +1020,16 @@ export default class extends Controller { } input.setCustomValidity(error.description || input.validationMessage) - input.reportValidity() + invalidInputs.push(input) }) + const stepIndex = this.stepTargets.findIndex(step => + invalidInputs.some(input => this.inputsForStep(step).includes(input)), + ) + if (stepIndex >= 0) this.showStep(stepIndex) + + invalidInputs.forEach(input => input.reportValidity()) + this.showErrorMessages(this.inputTargets) if (generalErrors.length) this.showGlobalError(generalErrors.join(' ')) else if (!errors.length) this.showGlobalError() @@ -769,7 +1072,10 @@ export default class extends Controller { const inputs = this.inputsForStep(step) inputs.forEach(input => { - const value = this.inputValue(input) + const value = + input.dataset.popupFieldKind === 'phone' + ? this.identityValue(input) + : this.inputValue(input) const key = input.dataset.popupFieldKey || input.name stepFields[key] = value diff --git a/src/core/event.js b/src/core/event.js index c83e660b..ec7f4257 100644 --- a/src/core/event.js +++ b/src/core/event.js @@ -12,6 +12,7 @@ export default class Event { 'alert:shown', 'alert:dismissed', 'alert:accepted', + 'activity:occurred', 'webchat:mounted', 'webchat:opened', 'webchat:closed', diff --git a/src/hellotext.js b/src/hellotext.js index d428c9d4..a388acfc 100644 --- a/src/hellotext.js +++ b/src/hellotext.js @@ -12,14 +12,36 @@ import { Query, Session, User, + UTM, Webchat, WhatsAppWidget, } from './models' import { NotInitializedError } from './errors' +// The campaign parameters display rules can target. `utm_term` and `utm_content` are not +// among them, so a link carrying only those does not stand for a campaign here. +const CAMPAIGN_RULE_KEYS = ['source', 'medium', 'campaign'] + +const ACTIVITY_RULE_FIELDS = { + 'product.viewed': 'activity.product_viewed', + 'cart.added': 'activity.cart_added', + 'order.placed': 'activity.purchase_completed', + 'product.purchased': 'activity.purchase_completed', + 'form.completed': 'activity.form_completed', +} + class Hellotext { static eventEmitter = new Event() + static activities = new Set() + static pageViews = 1 + static visitCampaign = {} + static visitorType = 'new' + static visitBusinessId + static lastPageUrl + static lastPageRoute + static pageStartedAt + static visitStartedAt static forms static business static popup @@ -28,6 +50,12 @@ class Hellotext { static push static alert static initializationVersion = 0 + static popupEvaluationVersion = 0 + static identificationVersion = 0 + static identificationPending = false + static identificationCompletion + static cancelIdentificationWait + static popupRuntime /** * initialize the module. @@ -35,9 +63,13 @@ class Hellotext { * @param { Configuration } config */ static async initialize(business, config = {}) { + const previousBusinessId = this.visitBusinessId + const previousSession = this.session const initializationVersion = ++this.initializationVersion + this.popupEvaluationVersion += 1 this.popup?.unmount?.() this.popup = undefined + this.popupRuntime = undefined this.alert?.dispose() this.alert = null @@ -50,7 +82,17 @@ class Hellotext { Configuration.assign({ push: {}, ...config }) Session.initialize(this.page) + if ( + this.identificationPending && + (previousBusinessId !== business || previousSession !== this.session) + ) { + this.identificationVersion += 1 + this.identificationPending = false + this.cancelIdentificationPolling() + } + this.initializeVisitSignals(business) + this.forms?.mutationObserver?.disconnect() this.forms = new FormCollection() this.query = new Query() @@ -58,16 +100,12 @@ class Hellotext { const businessData = await businessContext.hydrate() if (this.business !== businessContext) return - if (config.push !== false && businessData?.push?.public_key && Push.supported) { - this.push = new Push(businessData.push) - - this.push.initialize().catch(error => { - console.warn('Hellotext Push initialization failed:', error) - }) + let stagedPush = null + let stagedAlertData = null - if (businessData.alert?.html) { - this.alert = new Alert(businessData.alert, businessContext, this.push) - } + if (config.push !== false && businessData?.push?.public_key && Push.supported) { + stagedPush = new Push(businessData.push) + if (businessData.alert?.html) stagedAlertData = businessData.alert } const popupConfig = @@ -119,30 +157,20 @@ class Hellotext { if (popupConfig && popupConfig.id) { const resolvedPopupConfig = { container: 'body', device: 'auto', ...popupConfig } Configuration.popup.assign(resolvedPopupConfig) - widgetLoads.push( - Popup.load(resolvedPopupConfig.id, { - container: resolvedPopupConfig.container, - shouldMount: () => { - return ( - this.business === businessContext && - this.initializationVersion === initializationVersion - ) - }, - }).then(popup => { - if ( - this.business === businessContext && - this.initializationVersion === initializationVersion - ) { - this.popup = popup - } - }), - ) + this.popupRuntime = { config: resolvedPopupConfig, businessContext, initializationVersion } + if (!this.identificationPending) widgetLoads.push(this.loadPopup(this.popupRuntime)) } await Promise.all(widgetLoads) if (this.business !== businessContext || this.initializationVersion !== initializationVersion) return + this.push = stagedPush + this.alert = stagedAlertData ? new Alert(stagedAlertData, businessContext, stagedPush) : null + this.push?.initialize().catch(error => { + console.warn('Hellotext Push initialization failed:', error) + }) + if (typeof MutationObserver !== 'undefined') { this.forms.collectExistingFormsOnPage() } @@ -170,6 +198,36 @@ class Hellotext { return result } + static async loadPopup(runtime = this.popupRuntime) { + if (!runtime || this.identificationPending) return null + + const evaluationVersion = ++this.popupEvaluationVersion + const current = () => { + return ( + this.popupRuntime === runtime && + this.business === runtime.businessContext && + this.initializationVersion === runtime.initializationVersion && + this.popupEvaluationVersion === evaluationVersion && + !this.identificationPending + ) + } + const popup = await Popup.load(runtime.config.id, { + container: runtime.config.container, + shouldMount: current, + }) + + if (current()) this.popup = popup + return popup + } + + static reloadPopup() { + this.popupEvaluationVersion += 1 + this.popup?.unmount?.() + this.popup = undefined + + return this.loadPopup() + } + static isPlainObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value) } @@ -186,6 +244,9 @@ class Hellotext { throw new NotInitializedError() } + const business = this.business + const session = this.session + const visitBusinessId = this.visitBusinessId const headers = { ...((params && params.headers) || {}), ...this.headers, @@ -199,7 +260,7 @@ class Hellotext { const pageInstance = params && params.url ? new Page(params.url) : this.page const body = { - session: this.session, + session, user_parameters, action, ...params, @@ -208,7 +269,7 @@ class Hellotext { delete body.headers - return await API.events.create({ + const response = await API.events.create({ headers, body, // Track is the SDK's unload-sensitive analytics path. Keepalive belongs @@ -217,6 +278,200 @@ class Hellotext { // stronger request/response or interaction contracts. keepalive: keepaliveFor(body), }) + + const trackedAt = this.trackedAtMilliseconds(params.tracked_at) + if ( + response.succeeded && + this.business === business && + this.session === session && + this.visitBusinessId === visitBusinessId && + (trackedAt === null || trackedAt >= this.visitStartedAt) + ) + this.recordActivity(action) + + return response + } + + static recordActivity(action) { + const field = ACTIVITY_RULE_FIELDS[action] + if (!field) return + + this.activities.add(field) + this.writeStorage( + 'sessionStorage', + this.visitStorageKey('activities'), + JSON.stringify([...this.activities]), + ) + this.eventEmitter.dispatch('activity:occurred', { action, field }) + } + + static trackedAtMilliseconds(value) { + if (value === undefined || value === null || value === '') return null + + if (typeof value === 'number') { + if (!Number.isFinite(value)) return Number.NaN + + // Public tracking timestamps use Unix seconds. Accept millisecond values as well so + // integrations that already pass Date#getTime() do not get silently rejected. + return value < 1_000_000_000_000 ? value * 1000 : value + } + + return new Date(value).getTime() + } + + static initializeVisitSignals(businessId) { + const businessChanged = this.visitBusinessId !== businessId + this.visitBusinessId = businessId + + if (businessChanged) { + this.pageViews = 0 + this.lastPageUrl = undefined + this.lastPageRoute = undefined + this.activities = new Set(this.readStoredActivities()) + this.visitCampaign = this.readStoredVisitCampaign() + const storedVisitorType = this.readStorage( + 'sessionStorage', + this.visitStorageKey('visitor-type'), + ) + this.visitorType = ['new', 'returning'].includes(storedVisitorType) + ? storedVisitorType + : undefined + + if (!this.visitorType) { + this.visitorType = this.readStorage('localStorage', this.visitStorageKey('seen')) + ? 'returning' + : 'new' + this.writeStorage('sessionStorage', this.visitStorageKey('visitor-type'), this.visitorType) + this.writeStorage('localStorage', this.visitStorageKey('seen'), '1') + } + + const storedVisitStartedAt = Number( + this.readStorage('sessionStorage', this.visitStorageKey('started-at')), + ) + this.visitStartedAt = + Number.isFinite(storedVisitStartedAt) && storedVisitStartedAt > 0 + ? storedVisitStartedAt + : this.initialPageStartedAt() + this.writeStorage( + 'sessionStorage', + this.visitStorageKey('started-at'), + String(this.visitStartedAt), + ) + } + + this.rememberVisitCampaign(UTM.paramsFrom(window.location.search)) + + if (businessChanged || this.lastPageRoute !== this.pageRoute()) this.recordPageView() + } + + /** + * Remembers the campaign this visit arrived with, for as long as the tab lives — the same + * span as the other visit signals. + * + * Display rules read it when the URL no longer carries the parameters, which is the common + * case: the visitor moves past the landing page, or the site strips them from the URL once + * its analytics have read them. Persisted attribution answers a different question and is + * left alone: `hello_utm` still records only a complete source and medium pair, and still + * outlives the visit. + */ + static rememberVisitCampaign(params) { + const campaign = Object.fromEntries( + CAMPAIGN_RULE_KEYS.flatMap(key => { + const value = typeof params?.[key] === 'string' ? params[key].trim() : '' + + return value === '' ? [] : [[key, value]] + }), + ) + if (Object.keys(campaign).length === 0) return + + this.visitCampaign = campaign + this.writeStorage('sessionStorage', this.visitStorageKey('campaign'), JSON.stringify(campaign)) + } + + static readStoredVisitCampaign() { + try { + const stored = JSON.parse( + this.readStorage('sessionStorage', this.visitStorageKey('campaign')) || '{}', + ) + if (stored === null || typeof stored !== 'object' || Array.isArray(stored)) return {} + + return Object.fromEntries( + Object.entries(stored).filter( + ([key, value]) => CAMPAIGN_RULE_KEYS.includes(key) && typeof value === 'string', + ), + ) + } catch (_) { + return {} + } + } + + static recordPageView() { + const key = this.visitStorageKey('page-views') + const stored = Number(this.readStorage('sessionStorage', key)) + const firstPageInDocument = !this.lastPageUrl + this.pageViews = Number.isInteger(stored) && stored > 0 ? stored + 1 : this.pageViews + 1 + this.lastPageUrl = window.location.href + this.lastPageRoute = this.pageRoute() + this.pageStartedAt = firstPageInDocument ? this.initialPageStartedAt() : Date.now() + this.writeStorage('sessionStorage', key, String(this.pageViews)) + } + + static initialPageStartedAt() { + const navigationUrl = window.performance?.getEntriesByType?.('navigation')?.[0]?.name + if (navigationUrl) { + if (this.pageRoute(navigationUrl) !== this.pageRoute()) return Date.now() + } + + return window.performance?.timeOrigin || Date.now() + } + + static pageRoute(value = window.location.href) { + const currentUrl = window.location?.href || document.location?.href || 'http://localhost/' + const url = new URL(value || currentUrl, currentUrl) + const hashRoute = url.hash.match(/^#!?\/[^?]*/)?.[0] + + return `${url.pathname}${hashRoute?.replace(/^#!/, '#') || ''}` + } + + static readStoredActivities() { + try { + const stored = JSON.parse( + this.readStorage('sessionStorage', this.visitStorageKey('activities')) || '[]', + ) + return Array.isArray(stored) + ? stored.filter(field => Object.values(ACTIVITY_RULE_FIELDS).includes(field)) + : [] + } catch (_) { + return [] + } + } + + static visitStorageKey(name) { + return `hellotext:business:${this.visitBusinessId}:${name}` + } + + static storage(name) { + try { + return window[name] + } catch (_) { + return null + } + } + + static readStorage(name, key) { + try { + return this.storage(name)?.getItem(key) + } catch (_) { + return null + } + } + + static writeStorage(name, key, value) { + try { + this.storage(name)?.setItem(key, value) + } catch (_) { + // Storage may be unavailable in privacy-restricted browser contexts. + } } /** @@ -244,18 +499,159 @@ class Hellotext { }) } - const response = await API.identifications.create({ - user_id: externalId, - ...options, - }) + const identificationVersion = ++this.identificationVersion + const businessId = this.visitBusinessId + const session = this.session + this.identificationPending = true + this.cancelIdentificationPolling() + this.popupEvaluationVersion += 1 + this.popup?.unmount?.() + this.popup = undefined - if (response.succeeded) { - User.remember(externalId, options.source, fingerprint) + let response + try { + response = await API.identifications.create({ + user_id: externalId, + ...options, + }) + } catch (error) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + this.identificationPending = false + this.reloadPopup() + } + throw error + } + + if (response.failed) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + this.identificationPending = false + this.reloadPopup() + } + return response } + let receipt + try { + receipt = (await response.json())?.identification_receipt + } catch (_) { + // Older deployments returned an unreadable or empty success body. + } + + if (!receipt) { + if (this.identificationCurrent(identificationVersion, businessId, session)) { + User.remember(externalId, options.source, fingerprint) + this.identificationPending = false + this.reloadPopup() + } + return response + } + + this.identificationCompletion = this.finishIdentification({ + receipt, + identificationVersion, + businessId, + session, + externalId, + source: options.source, + fingerprint, + }).catch(() => {}) + return response } + static identificationCurrent(version, businessId, session) { + return ( + this.identificationVersion === version && + this.visitBusinessId === businessId && + this.session === session + ) + } + + static async finishIdentification(details) { + const delays = [0, 100, 250, 500, 1000, 2000, 4000, 8000] + + for (const delay of delays) { + if ( + !this.identificationCurrent( + details.identificationVersion, + details.businessId, + details.session, + ) + ) + return + if (delay > 0 && !(await this.waitForIdentificationPoll(delay))) return + if ( + !this.identificationCurrent( + details.identificationVersion, + details.businessId, + details.session, + ) + ) + return + + let response + try { + response = await API.identifications.status(details.receipt) + } catch (_) { + continue + } + + if ( + !this.identificationCurrent( + details.identificationVersion, + details.businessId, + details.session, + ) + ) + return + + if (response.data.status === 202) continue + if (!response.succeeded) { + if (response.data.status === 429 || response.data.status >= 500) continue + if (response.data.status === 422) { + this.identificationPending = false + this.cancelIdentificationPolling() + await this.reloadPopup() + } + return + } + + if ( + !this.identificationCurrent( + details.identificationVersion, + details.businessId, + details.session, + ) + ) + return + + User.remember(details.externalId, details.source, details.fingerprint) + this.identificationPending = false + this.cancelIdentificationPolling() + await this.reloadPopup() + return + } + } + + static waitForIdentificationPoll(delay) { + return new Promise(resolve => { + const timer = setTimeout(() => { + this.cancelIdentificationWait = undefined + resolve(true) + }, delay) + + this.cancelIdentificationWait = () => { + clearTimeout(timer) + this.cancelIdentificationWait = undefined + resolve(false) + } + }) + } + + static cancelIdentificationPolling() { + this.cancelIdentificationWait?.() + } + /** * Clears the user session, use when the user logs out to clear the hello cookies * diff --git a/src/models/form.js b/src/models/form.js index a2de40a2..48790adf 100644 --- a/src/models/form.js +++ b/src/models/form.js @@ -5,8 +5,9 @@ import { LogoBuilder } from '../builders/logo_builder' import { setSanitizedRichText } from '../core/sanitize_html' class Form { - constructor(data, element = null) { + constructor(data, element = null, visitBusinessId = Hellotext.visitBusinessId) { this.data = data + this.visitBusinessId = visitBusinessId this.element = element || document.querySelector(`[data-hello-form="${this.id}"]`) || @@ -14,7 +15,7 @@ class Form { } async mount({ ifCompleted = true } = {}) { - if(ifCompleted && this.hasBeenCompleted) { + if (ifCompleted && this.hasBeenCompleted) { this.element?.remove() return Hellotext.eventEmitter.dispatch('form:completed', { @@ -105,6 +106,8 @@ class Form { } localStorage.setItem(`hello-form-${this.id}`, JSON.stringify(payload)) + if (Hellotext.visitBusinessId === this.visitBusinessId) + Hellotext.recordActivity('form.completed') Hellotext.eventEmitter.dispatch('form:completed', payload) } @@ -119,11 +122,14 @@ class Form { get localeAuthKey() { const firstStep = this.data.steps[0] - if(firstStep.inputs.some(input => input.kind === 'email') && firstStep.inputs.some(input => input.kind === 'phone')) { + if ( + firstStep.inputs.some(input => input.kind === 'email') && + firstStep.inputs.some(input => input.kind === 'phone') + ) { return 'phone_and_email' - } else if(firstStep.inputs.some(input => input.kind === 'email')) { + } else if (firstStep.inputs.some(input => input.kind === 'email')) { return 'email' - } else if(firstStep.inputs.some(input => input.kind === 'phone')) { + } else if (firstStep.inputs.some(input => input.kind === 'phone')) { return 'phone' } else { return 'none' diff --git a/src/models/form_collection.js b/src/models/form_collection.js index 789f0bf4..ec051f0b 100644 --- a/src/models/form_collection.js +++ b/src/models/form_collection.js @@ -9,6 +9,8 @@ import { NotInitializedError } from '../errors' class FormCollection { constructor() { this.forms = [] + this.visitBusinessId = Hellotext.visitBusinessId + this.initializationVersion = Hellotext.initializationVersion this.includes = this.includes.bind(this) this.excludes = this.excludes.bind(this) @@ -45,6 +47,7 @@ class FormCollection { } if (this.fetching) return + if (!this.current) return if (typeof document === 'undefined' || !('querySelectorAll' in document)) { return console.warn( @@ -61,13 +64,18 @@ class FormCollection { this.fetching = true - await Promise.all(promises) - .then(forms => forms.forEach(this.add)) - .then(() => Hellotext.eventEmitter.dispatch('forms:collected', this)) - .then(() => (this.fetching = false)) + try { + const forms = await Promise.all(promises) + if (!this.current) return - if (Configuration.forms.autoMount) { - this.forms.forEach(form => form.mount()) + forms.forEach(this.add) + Hellotext.eventEmitter.dispatch('forms:collected', this) + + if (Configuration.forms.autoMount) { + this.forms.forEach(form => form.mount()) + } + } finally { + this.fetching = false } } @@ -93,7 +101,7 @@ class FormCollection { ) } - this.forms.push(new Form(data)) + this.forms.push(new Form(data, null, this.visitBusinessId)) } getById(id) { @@ -116,6 +124,13 @@ class FormCollection { return this.forms.length } + get current() { + return ( + Hellotext.visitBusinessId === this.visitBusinessId && + Hellotext.initializationVersion === this.initializationVersion + ) + } + get #formIdsToFetch() { return Array.from(document.querySelectorAll('[data-hello-form]')) .map(form => form.dataset.helloForm) diff --git a/src/models/page_path.js b/src/models/page_path.js new file mode 100644 index 00000000..2ee31db6 --- /dev/null +++ b/src/models/page_path.js @@ -0,0 +1,142 @@ +/** + * The one form a page path takes whenever a display rule compares it. + * + * A merchant types, pastes or picks a path; the browser reports `location.pathname`. The two + * rarely agree byte for byte — a pasted URL carries its domain, a CMS adds a trailing slash, + * the browser percent-encodes accents — and every such difference used to be a rule that + * silently never matched. Both sides go through this function before they meet, so how + * either one was written stops mattering. + * + * `exact` is a whole path: one leading slash, no trailing slash, dot segments resolved, so + * `/Sale/`, `sale` and `https://shop.com/sale?x=1` all become `/sale`. `contains` is a + * fragment: it keeps a trailing slash the merchant typed (`/blog/` means the pages under the + * blog) and never gains a leading one (`rojo` must still match `/zapato-rojo`). A fragment + * that reduces to nothing, or to `/`, would match every page, so it comes back empty for the + * caller to refuse. + * + * Kept in step with Popup::DisplayRules::PagePath and app/javascript/lib/popup_page_path.js + * in the Rails app. All three run the cases in __tests__/fixtures/page_path_cases.json. + */ +const EXACT = 'exact' +const CONTAINS = 'contains' +const CONTAINS_OPERATORS = ['contains', 'does_not_contain'] + +const WEB_ORIGIN = /^https?:\/\/[^/?#]+/i +const SCHEME_RELATIVE_ORIGIN = /^\/\/[^/?#]+/ +const SCHEME = /^[a-z][a-z0-9+.-]*:/i +const BARE_HOST_WITH_PATH = /^(?:(?:[a-z0-9-]+\.)+[a-z]{2,}|localhost)(?::\d+)?\//i +const ENCODED_RUN = /(?:%[0-9a-f]{2})+/gi +const RESERVED_ESCAPE = /%(?:21|23|24|25|26|27|28|29|2a|2b|2c|2f|3a|3b|3d|3f|40|5b|5d)/i +const INDEX_FILE = /(^|\/)index\.(?:html?|php)$/ + +export class PagePath { + static EXACT = EXACT + static CONTAINS = CONTAINS + + static modeFor(operator) { + return CONTAINS_OPERATORS.includes(operator) ? CONTAINS : EXACT + } + + static canonical(value, { mode = EXACT } = {}) { + let path = String(value ?? '').trim() + if (path === '') return '' + + path = this.withoutOrigin(path) + if (path === null) return '' + path = this.routePath(path) + path = this.decoded(path) + // Lowercasing can produce decomposed sequences, so NFC runs after it. The final sigma is + // folded because JavaScript applies its word-final rule and Ruby does not. + path = path + .toLowerCase() + .replace(/ς/g, 'σ') + .normalize('NFC') + .replace(/\/{2,}/g, '/') + + const indexFile = INDEX_FILE.test(path) + path = path.replace(INDEX_FILE, '$1') + + if (mode === CONTAINS) { + // `/blog/index.html` names the blog page itself, not everything under it. + if (indexFile) path = path.replace(/\/$/, '') + return path === '/' ? '' : path + } + + return this.resolved(path) + } + + // Only HTTP(S) values name pages the popup can observe. A bare domain with a path is + // rejected instead of depending on the suggestion-host limit to decide its meaning. + static withoutOrigin(path) { + if (WEB_ORIGIN.test(path)) return path.replace(WEB_ORIGIN, '') + if (SCHEME_RELATIVE_ORIGIN.test(path)) return path.replace(SCHEME_RELATIVE_ORIGIN, '') + if (SCHEME.test(path) || BARE_HOST_WITH_PATH.test(path)) return null + + return path + } + + // Hash-routed sites keep their real route after `#/` or `#!/`. Any other fragment is an + // in-page anchor and names no page of its own, and a query never does. + static routePath(path) { + const hashAt = path.indexOf('#') + const base = (hashAt === -1 ? path : path.slice(0, hashAt)).split('?')[0] + const fragment = hashAt === -1 ? '' : path.slice(hashAt + 1) + const route = fragment.startsWith('/') + ? fragment + : fragment.startsWith('!/') + ? fragment.slice(1) + : '' + + return route ? `${base}/${route.split('?')[0]}` : base + } + + // Decode readable characters but keep every reserved URL delimiter escaped. In particular, + // `%2F` is not `/`: decoding it would turn one path segment into two. + static decoded(path) { + return path.replace(ENCODED_RUN, run => { + const escapes = run.match(/%[0-9a-f]{2}/gi) ?? [] + const groups = [] + let group = [] + const flush = () => { + if (group.length > 0) groups.push(group.join('')) + group = [] + } + + escapes.forEach(escape => { + if (RESERVED_ESCAPE.test(escape)) { + flush() + groups.push(escape) + } else { + group.push(escape) + } + }) + flush() + + return groups + .map(group => { + if (RESERVED_ESCAPE.test(group)) return group + + try { + return decodeURIComponent(group) + } catch (_) { + return group + } + }) + .join('') + }) + } + + static resolved(path) { + const segments = [] + + path.split('/').forEach(segment => { + if (segment === '' || segment === '.') return + if (segment === '..') segments.pop() + else segments.push(segment) + }) + + return `/${segments.join('/')}` + } +} + +export default PagePath diff --git a/src/models/popup_display_rules.js b/src/models/popup_display_rules.js new file mode 100644 index 00000000..6d2850ec --- /dev/null +++ b/src/models/popup_display_rules.js @@ -0,0 +1,347 @@ +/** + * Evaluates the page-scoped display rules the server hands to the browser. + * + * The payload is `{ lanes: [[condition, ...], ...] }`: lanes are OR'd and their conditions + * are AND'd. Page URL is the one exception: repeated conditions for that field form a group + * whose positive matches are alternatives and whose exclusions are cumulative. Only lanes + * that already survived server-side evaluation are sent. + * + * No lanes means the popup may display: either it has no rules, or every rule was already + * satisfied on the server. + * + * This mirrors Popup::DisplayRules::PageEvaluator on the Rails side. Keep the two in step + * — the shared cases are covered by both suites. + */ +import { PagePath } from './page_path' + +const NEGATIVE_OPERATORS = ['does_not_contain', 'is_not'] + +const THRESHOLD_FIELDS = ['session.scroll_depth', 'session.time_on_page', 'session.page_views'] +// A measurement is compared from either side. Kept in step with +// Popup::DisplayRules::Catalog::THRESHOLD_OPERATORS — `between` is absent on both sides +// because a lane ANDs repeated conditions, so `at_least 25` beside `at_most 75` is it. +const THRESHOLD_OPERATORS = ['at_least', 'at_most', 'greater_than', 'less_than'] +const STRING_FIELDS = [ + 'page.path', + 'page.title', + 'session.referrer', + 'session.language', + 'session.visitor_type', + 'session.browser', + 'session.utm_source', + 'session.utm_medium', + 'session.utm_campaign', +] +// The three campaign parameters are case-insensitive. URLSearchParams already decodes query +// strings before they reach a rule, including a raw `+` as a space while preserving `%2B`. +const CAMPAIGN_FIELDS = ['session.utm_source', 'session.utm_medium', 'session.utm_campaign'] +const EVENT_FIELDS = [ + 'activity.product_viewed', + 'activity.cart_added', + 'activity.purchase_completed', + 'activity.form_completed', +] +// Text-typed fields whose values come from a fixed list. Kept in step with +// Popup::DisplayRules::Catalog on the Rails side. +const CLOSED_STRING_VALUES = { + 'session.language': ['en', 'es', 'pt', 'fr', 'nl'], + 'session.visitor_type': ['new', 'returning'], + 'session.browser': ['chrome', 'safari', 'firefox', 'edge'], +} +const THRESHOLD_RANGES = { + 'session.scroll_depth': [1, 100], + 'session.time_on_page': [1, 3600], + 'session.page_views': [1, 1000], +} +const MAX_STRING_VALUE_LENGTH = 512 +// Every operator a list-valued field offers comes in a positive/negative pair, so any +// authored row can be reversed. Kept in step with Popup::DisplayRules::Catalog on the +// Rails side, where `starts_with` and `ends_with` were dropped for lacking a twin. +const TEXT_OPERATORS = ['contains', 'does_not_contain', 'is', 'is_not'] +const ENTITY_OPERATORS = ['is', 'is_not'] + +export class PopupDisplayRules { + constructor(payload) { + // An explicit empty lane list means universal eligibility. Anything else that does + // not conform to the public payload shape must fail closed: treating a missing or + // malformed `lanes` property as the same thing would expose a popup unexpectedly. + this.valid = + payload !== null && + typeof payload === 'object' && + !Array.isArray(payload) && + Array.isArray(payload.lanes) + this.lanes = (this.valid ? payload.lanes : []).map(lane => { + // An empty lane is intentional: it means the server already satisfied every + // visitor-only condition. Any other malformed lane must fail closed instead of + // accidentally becoming that universal match. + return Array.isArray(lane) ? lane : [null] + }) + } + + get empty() { + return this.valid && this.lanes.length === 0 + } + + /** + * True when the popup requires a measurement that only grows over time, so the runtime + * knows it has to keep re-checking instead of deciding once on connect. + */ + get needsMeasurements() { + return this.lanes.some(lane => + lane.some(condition => THRESHOLD_FIELDS.includes(condition?.field)), + ) + } + + get needsNavigation() { + return this.lanes.some(lane => lane.some(condition => this.validCondition(condition))) + } + + get needsTitle() { + return this.lanes.some(lane => lane.some(condition => condition?.field === 'page.title')) + } + + get needsActivities() { + return this.lanes.some(lane => lane.some(condition => EVENT_FIELDS.includes(condition?.field))) + } + + matches(context) { + if (!this.valid) return false + if (this.empty) return true + + return this.lanes.some(lane => this.laneMatches(lane, context)) + } + + laneMatches(lane, context) { + if (!lane.every(condition => this.validCondition(condition))) return false + + const groups = new Map() + + lane.forEach(condition => { + const conditions = groups.get(condition?.field) || [] + conditions.push(condition) + groups.set(condition?.field, conditions) + }) + + return [...groups.entries()].every(([field, conditions]) => + this.fieldGroupMatches(field, conditions, context), + ) + } + + // A list-valued field is authored one row at a time, so it can carry several sibling + // conditions at once: the values included are alternatives and the ones excluded are + // cumulative. Thresholds and events hold a single condition each and stay a flat AND. + // `STRING_FIELDS` is this side's copy of the catalog's list-valued types — country is + // visitor-scoped and never reaches the browser. + fieldGroupMatches(field, conditions, context) { + if (!STRING_FIELDS.includes(field)) { + return conditions.every(condition => this.conditionMatches(condition, context)) + } + + const positives = conditions.filter( + condition => !NEGATIVE_OPERATORS.includes(condition?.operator), + ) + const negatives = conditions.filter(condition => + NEGATIVE_OPERATORS.includes(condition?.operator), + ) + + return ( + (positives.length === 0 || + positives.some(condition => this.conditionMatches(condition, context))) && + negatives.every(condition => this.conditionMatches(condition, context)) + ) + } + + conditionMatches(condition, context) { + if (!this.validCondition(condition)) return false + + if (EVENT_FIELDS.includes(condition.field)) { + return ( + context.activities?.has?.(condition.field) || + context.activities?.includes?.(condition.field) + ) + } + + const actual = this.actualValue(condition.field, context) + + if (THRESHOLD_FIELDS.includes(condition.field)) { + return this.thresholdMatches(condition, actual) + } + + if (condition.field === 'page.path') return this.pathMatches(condition, actual, context) + + return this.stringMatches(condition, actual) + } + + actualValue(field, context) { + switch (field) { + // The hash rides along because a hash-routed site keeps its real route after `#/`. + case 'page.path': + return context.path === undefined || context.path === null + ? context.path + : `${context.path}${context.hash ?? ''}` + case 'page.title': + return context.title + case 'session.referrer': + return context.referrer + case 'session.scroll_depth': + return context.scrollDepth + case 'session.time_on_page': + return context.timeOnPage + case 'session.page_views': + return context.pageViews + case 'session.language': + return context.language + case 'session.visitor_type': + return context.visitorType + case 'session.browser': + return context.browser + case 'session.utm_source': + return context.utm?.source + case 'session.utm_medium': + return context.utm?.medium + case 'session.utm_campaign': + return context.utm?.campaign + default: + return undefined + } + } + + validCondition(condition) { + if (!condition || typeof condition !== 'object' || !Array.isArray(condition.values)) + return false + + if (THRESHOLD_FIELDS.includes(condition.field)) { + const value = condition.values[0] + const numericValue = Number(value) + const [minimum, maximum] = THRESHOLD_RANGES[condition.field] + + return ( + THRESHOLD_OPERATORS.includes(condition.operator) && + condition.values.length === 1 && + (typeof value === 'number' || (typeof value === 'string' && /^\d+$/.test(value))) && + Number.isInteger(numericValue) && + numericValue >= minimum && + numericValue <= maximum + ) + } + + if (EVENT_FIELDS.includes(condition.field)) { + return condition.operator === 'occurred' && condition.values.length === 0 + } + + // A closed set matches a whole value or none of it, so it only offers the exact pair. + // Mirrors the catalog's ENTITY_OPERATORS on the Rails side: accepting `contains` here + // would evaluate a condition the server would have refused to save. + const operators = CLOSED_STRING_VALUES[condition.field] ? ENTITY_OPERATORS : TEXT_OPERATORS + const validStrings = + STRING_FIELDS.includes(condition.field) && + operators.includes(condition.operator) && + condition.values.length > 0 && + condition.values.every( + value => + typeof value === 'string' && + value.trim().length > 0 && + value.length <= MAX_STRING_VALUE_LENGTH, + ) + + if (!validStrings) return false + + // Closed sets are checked here as well as on the server. A value outside the set could + // only come from a tampered payload, and an unknown one must not ride along into an + // `is not` and quietly widen who the popup reaches. + const allowed = CLOSED_STRING_VALUES[condition.field] + + return !allowed || condition.values.every(value => allowed.includes(value)) + } + + /** + * A measurement that has not been reported yet fails every comparison, including the + * ones that point downwards: "pages viewed is at most 2" must not hold before the + * runtime has counted a single page. + */ + thresholdMatches(condition, actual) { + if (actual === undefined || actual === null || actual === '') return false + + const value = Number(actual) + const expected = Number(condition.values[0]) + + switch (condition.operator) { + case 'at_least': + return value >= expected + case 'at_most': + return value <= expected + case 'greater_than': + return value > expected + case 'less_than': + return value < expected + default: + return false + } + } + + /** + * A missing value satisfies a negative operator and fails a positive one. Treating it as + * an empty string would make "title contains x" and "title does not contain x" agree, + * which breaks the exact complement the rules promise. + */ + stringMatches(condition, actual) { + const negative = NEGATIVE_OPERATORS.includes(condition.operator) + + if (actual === undefined || actual === null) return negative + + // URLSearchParams has already decoded query-string spelling in the page context. Do not + // turn literal plus signs into spaces here: `%2B` is a meaningful campaign character. + const normalize = CAMPAIGN_FIELDS.includes(condition.field) + ? value => String(value).trim().toLowerCase() + : value => String(value).toLowerCase() + const value = normalize(actual) + const hit = condition.values.some(expected => + this.compare(condition.operator, value, normalize(expected)), + ) + + return negative ? !hit : hit + } + + /** + * Page URL compares canonical paths on both sides, so a value saved as `/sale` still + * matches a visitor on `/sale/`, `/SALE` or `/#/sale`, and one pasted with its domain + * still names the page. A value that reduces to nothing would match every page: the + * server refuses to save one, and a payload carrying it anyway fails closed instead of + * reaching everyone. + */ + pathMatches(condition, actual, context) { + const negative = NEGATIVE_OPERATORS.includes(condition.operator) + + if (actual === undefined || actual === null) return negative + + const mode = PagePath.modeFor(condition.operator) + const expected = condition.values.map(value => PagePath.canonical(value, { mode })) + if (expected.includes('')) return false + + const path = PagePath.canonical(actual) + const hit = expected.some(value => + mode === PagePath.CONTAINS ? path.includes(value) : path === value, + ) + + return negative ? !hit : hit + } + + compare(operator, actual, expected) { + switch (operator) { + case 'contains': + case 'does_not_contain': + return actual.includes(expected) + case 'is': + case 'is_not': + return actual === expected + case 'starts_with': + return actual.startsWith(expected) + case 'ends_with': + return actual.endsWith(expected) + default: + return false + } + } +} + +export default PopupDisplayRules diff --git a/src/models/utm.js b/src/models/utm.js index bb95c8af..73818434 100644 --- a/src/models/utm.js +++ b/src/models/utm.js @@ -2,25 +2,34 @@ import { Cookies } from './cookies' class UTM { constructor() { - const urlSearchParams = new URLSearchParams(window.location.search) - - const utmsFromUrl = { - source: urlSearchParams.get('utm_source'), - medium: urlSearchParams.get('utm_medium'), - campaign: urlSearchParams.get('utm_campaign'), - term: urlSearchParams.get('utm_term'), - content: urlSearchParams.get('utm_content'), - } + this.save(UTM.paramsFrom(window.location.search)) + } - this.save(utmsFromUrl) + /** + * The campaign parameters a query string carries, keyed the way attribution stores them. + * Parameters that are absent or blank are left out rather than kept as empty values. + * + * @param {String} search - a query string such as `window.location.search` + * @returns {Object} + */ + static paramsFrom(search) { + const params = new URLSearchParams(search) + + return Object.fromEntries( + Object.entries({ + source: params.get('utm_source'), + medium: params.get('utm_medium'), + campaign: params.get('utm_campaign'), + term: params.get('utm_term'), + content: params.get('utm_content'), + }).filter(([_, value]) => value), + ) } save(utmParams) { if (!utmParams.source || !utmParams.medium) return - const cleanUtms = Object.fromEntries( - Object.entries(utmParams).filter(([_, value]) => value), - ) + const cleanUtms = Object.fromEntries(Object.entries(utmParams).filter(([_, value]) => value)) cleanUtms.observed_at = new Date().toISOString() Cookies.set('hello_utm', JSON.stringify(cleanUtms))