@nosto/autocomplete
Version:
Library designed to simplify the implementation of search autocomplete functionality
1 lines • 77 kB
Source Map (JSON)
{"version":3,"file":"autocomplete.cjs","sources":["../../src/liquid/fromLiquidTemplate.ts","../../node_modules/@nosto/nosto-js/dist/settings-DN0V2st5.js","../../node_modules/@nosto/nosto-js/dist/index.es.js","../../src/lib/client.ts","../../node_modules/@nosto/search-js/dist/index.es-B8mbAxS4.js","../../node_modules/@nosto/search-js/dist/logger-DVwg4Wor.js","../../node_modules/@nosto/search-js/dist/search-BrR80UbS.js","../../src/lib/search.ts","../../src/lib/config.ts","../../node_modules/@nosto/search-js/dist/utils/utils.es.js","../../src/lib/dropdown.ts","../../src/utils/promise.ts","../../src/utils/state.ts","../../src/utils/dom.ts","../../src/utils/ga.ts","../../src/utils/debounce.ts","../../src/utils/history.ts","../../src/lib/autocomplete.ts","../../node_modules/@nosto/search-js/dist/currencies/currencies.es.js","../../src/lib/components.ts","../../src/liquid/NostoAutocomplete.ts"],"sourcesContent":["import { DefaultState } from \"../utils/state\"\nimport { Liquid } from \"liquidjs\"\n\nexport { default as defaultLiquidTemplate } from \"./autocomplete.liquid\"\n\n/**\n * Render a liquid template into a container\n *\n * @param template Liquid template\n * @returns Render function\n * @group Autocomplete\n * @category Liquid\n * @example\n * ```js\n * import { fromLiquidTemplate } from \"@nosto/autocomplete/liquid\";\n *\n * const render = fromLiquidTemplate(`\n * <div>\n * <h1>{{title}}</h1>\n * <ul>\n * {% for product in products %}\n * <li>{{product.name}}</li>\n * {% endfor %}\n * </ul>\n * </div>\n * `);\n *\n * render(document.getElementById(\"container\"), {\n * title: \"My Title\",\n * products: [\n * { name: \"Product 1\" },\n * { name: \"Product 2\" },\n * { name: \"Product 3\" }\n * ]\n * });\n\n * ```\n */\nexport function fromLiquidTemplate<State extends object = DefaultState>(\n template: string\n): (container: HTMLElement, state: State) => PromiseLike<void> {\n const instance = Liquid ? new Liquid() : undefined\n\n if (instance === undefined) {\n throw new Error(\n \"Liquid is not defined. Please include the Liquid library in your page.\"\n )\n }\n\n const tmpl = instance.parse(template)\n\n return async (container, state) => {\n const result = await instance.render(tmpl, state)\n container.innerHTML = result\n }\n}","function o() {\n window.nostojs = window.nostojs ?? function(n) {\n (window.nostojs.q = window.nostojs.q ?? []).push(n);\n };\n}\nasync function s(n) {\n return window.nostojs(n);\n}\nlet t = null;\ntypeof window < \"u\" && (o(), s((n) => {\n t = n.internal.getSettings();\n}));\nfunction i() {\n return t;\n}\nfunction e(n) {\n t = n;\n}\nexport {\n i as g,\n o as i,\n s as n,\n e as s\n};\n","import { i as b } from \"./settings-DN0V2st5.js\";\nimport { g as T, n as U } from \"./settings-DN0V2st5.js\";\nfunction C() {\n return typeof window.nosto < \"u\";\n}\nfunction N() {\n return window.nosto;\n}\nconst h = {\n production: \"https://connect.nosto.com/\",\n staging: \"https://connect.staging.nosto.com/\",\n local: \"https://connect.nosto.com/\"\n};\nfunction l(r) {\n return h[r ?? \"production\"];\n}\nfunction k({ merchantId: r, env: o, options: n, shopifyInternational: e, scriptLoader: a }) {\n var i, m;\n const t = document.querySelector(\"script[nosto-language], script[nosto-market-id]\"), c = String((e == null ? void 0 : e.marketId) || \"\"), d = (e == null ? void 0 : e.language) || \"\", w = (t == null ? void 0 : t.getAttribute(\"nosto-language\")) !== d || (t == null ? void 0 : t.getAttribute(\"nosto-market-id\")) !== c;\n if (!t || w) {\n const s = document.querySelector(\"#nosto-sandbox\");\n (i = t == null ? void 0 : t.parentNode) == null || i.removeChild(t), (m = s == null ? void 0 : s.parentNode) == null || m.removeChild(s);\n const u = new URL(\"/script/shopify/market/nosto.js\", l(o));\n u.searchParams.append(\"merchant\", r), u.searchParams.append(\"market\", c), u.searchParams.append(\"locale\", d.toLowerCase());\n const f = {\n ...n == null ? void 0 : n.attributes,\n \"nosto-language\": d,\n \"nosto-market-id\": c\n };\n return (a ?? g)(u.toString(), { ...n, attributes: f });\n }\n return Promise.resolve();\n}\nfunction v(r) {\n if (r.shopifyInternational)\n return k(r);\n const { merchantId: o, env: n, options: e, scriptLoader: a } = r, t = a ?? g, c = new URL(`/include/${o}`, l(n));\n return t(c.toString(), e);\n}\nfunction g(r, o) {\n return new Promise((e, a) => {\n const t = document.createElement(\"script\");\n t.src = r, t.async = !0, t.type = \"text/javascript\", t.onload = () => e(), t.onerror = () => a(), Object.entries((o == null ? void 0 : o.attributes) ?? {}).forEach(([c, d]) => t.setAttribute(c, d)), (o == null ? void 0 : o.position) === \"head\" ? document.head.appendChild(t) : document.body.appendChild(t);\n });\n}\nasync function L(r, o, n) {\n var e;\n if (!((e = window.Nosto) != null && e.addSkuToCart))\n throw new Error(\"Nosto addSkuToCart function is not available\");\n await window.Nosto.addSkuToCart(r, o, n);\n}\ntypeof window < \"u\" && b();\nexport {\n L as addSkuToCart,\n N as getNostoWindow,\n T as getSettings,\n v as init,\n b as initNostoStub,\n C as isNostoLoaded,\n U as nostojs\n};\n","import { nostojs } from \"@nosto/nosto-js\"\nimport { SearchHit } from \"@nosto/nosto-js/client\"\n\nexport function recordSearchClick(hit: SearchHit) {\n nostojs(api => api.recordSearchClick(\"autocomplete\", hit))\n}\n\nexport function recordSearchSubmit(query: string) {\n nostojs(api => api.recordSearchSubmit(query))\n}\n","function t() {\n window.nostojs = window.nostojs ?? function(o) {\n (window.nostojs.q = window.nostojs.q ?? []).push(o);\n };\n}\nasync function u(o) {\n return window.nostojs(o);\n}\nlet i = null;\ntypeof window < \"u\" && (t(), u((o) => {\n i = o.internal.getSettings();\n}));\nfunction a() {\n return i;\n}\nasync function d(o, w, s) {\n var n;\n if (!((n = window.Nosto) != null && n.addSkuToCart))\n throw new Error(\"Nosto addSkuToCart function is not available\");\n await window.Nosto.addSkuToCart(o, w, s);\n}\ntypeof window < \"u\" && t();\nexport {\n d as L,\n a as i,\n u as s\n};\n","import { s as u } from \"./index.es-B8mbAxS4.js\";\nconst i = (r) => String(r) === \"[object Object]\";\nfunction s(r) {\n if (!i(r))\n return !1;\n const e = r.constructor;\n if (e === void 0)\n return !0;\n const t = e.prototype;\n return !(!i(t) || !t.hasOwnProperty(\"isPrototypeOf\"));\n}\nfunction f(r, e) {\n if (r === e)\n return !0;\n if (r instanceof Date && e instanceof Date)\n return r.getTime() === e.getTime();\n if (Array.isArray(r) && Array.isArray(e))\n return r.length !== e.length ? !1 : r.every((t, n) => f(t, e[n]));\n if (s(r) && s(e)) {\n const t = Object.entries(r);\n return t.length !== Object.keys(e).length ? !1 : t.every(([n, c]) => f(c, e[n]));\n }\n return !1;\n}\nfunction o(r) {\n return (...e) => {\n u((t) => {\n (t.internal.context.mode.isPreview() ? console[r] : t.internal.logger[r])(...e);\n });\n };\n}\nconst a = {\n debug: o(\"debug\"),\n info: o(\"info\"),\n warn: o(\"warn\"),\n error: o(\"error\")\n};\nexport {\n s as a,\n f as i,\n a as l\n};\n","import { s as N } from \"./index.es-B8mbAxS4.js\";\nimport { l, i as R } from \"./logger-DVwg4Wor.js\";\nasync function T(t, { hitDecorators: e, ...n }, s) {\n var o, i;\n const r = await s(t, n);\n if (!((i = (o = r.products) == null ? void 0 : o.hits) != null && i.length) || !(e != null && e.length))\n return r;\n const c = (a) => e.reduce((u, f) => f(u), a);\n return {\n ...r,\n products: {\n ...r.products,\n hits: r.products.hits.map(c)\n }\n };\n}\nfunction z(t, e, n) {\n const s = JSON.stringify(e);\n try {\n n.setItem(t, s);\n } catch (r) {\n l.warn(r);\n }\n}\nfunction O(t, e) {\n try {\n const n = e.getItem(t);\n if (n)\n return JSON.parse(n);\n } catch (n) {\n l.warn(n);\n }\n}\nfunction E(t, e) {\n z(t, e, sessionStorage);\n}\nfunction J(t) {\n return O(t, sessionStorage);\n}\nconst b = \"nosto:search-js:cache\", k = 60 * 1e3;\nfunction I(t, e) {\n E(b, { query: t, result: e, created: Date.now() });\n}\nfunction M(t) {\n const e = J(b);\n if (!e || !V(e))\n return null;\n const n = C(e.query);\n return !R(C(t), n) || Date.now() - e.created > k ? null : e.result;\n}\nfunction C(t) {\n const e = {\n ...t,\n time: void 0,\n products: {\n ...t.products,\n size: void 0\n }\n };\n return JSON.parse(JSON.stringify(e));\n}\nfunction V(t) {\n return typeof t == \"object\" && t !== null && \"query\" in t && \"result\" in t && \"created\" in t;\n}\nasync function W(t, { usePersistentCache: e, ...n }, s) {\n if (!e)\n return s(t, n);\n const r = await j(t, n, s);\n return I(t, r), r;\n}\nasync function j(t, e, n) {\n var h, g, m, S, w, y;\n const { from: s = 0, size: r = 0 } = t.products || {}, c = M(t);\n if (!c)\n return await n(t, e);\n const o = ((h = c == null ? void 0 : c.products) == null ? void 0 : h.size) ?? 0, i = ((g = c == null ? void 0 : c.products) == null ? void 0 : g.hits) ?? [];\n if (r === o)\n return c;\n if (r < o)\n return {\n ...c,\n products: {\n ...c.products,\n size: r,\n hits: i.slice(0, r),\n total: ((m = c.products) == null ? void 0 : m.total) || 0\n }\n };\n const a = r - i.length, u = s > 0 ? s + 1 : r - a, f = {\n ...t,\n products: {\n ...t.products,\n from: u,\n size: a\n }\n }, p = await n(f, e);\n return {\n ...c,\n products: {\n ...c.products,\n size: r,\n hits: [...((S = c.products) == null ? void 0 : S.hits) || [], ...((w = p.products) == null ? void 0 : w.hits) || []],\n total: ((y = p.products) == null ? void 0 : y.total) || 0\n }\n };\n}\nconst x = 3e4, d = /* @__PURE__ */ new Map();\nfunction A(t, e) {\n const n = d.get(t);\n if (!n) return;\n const s = Date.now() - n.created > x, r = R(e, n.query);\n if (s || !r) {\n d.delete(t);\n return;\n }\n return n.result;\n}\nfunction K(t, e, n) {\n d.set(t, {\n query: e,\n result: n,\n created: Date.now()\n });\n}\nasync function L(t, e, n) {\n if (!e.useMemoryCache)\n return n(t, e);\n const s = JSON.stringify(t), r = A(s, t);\n if (r) return r;\n const c = await n(t, e);\n return K(s, t, c), c;\n}\nfunction P(t) {\n return new Promise((e) => setTimeout(e, t));\n}\nasync function _(t, { maxRetries: e = 0, retryInterval: n = 0, ...s }, r) {\n let c = 0;\n for (; ; )\n try {\n return await r(t, s);\n } catch (o) {\n if (c >= e)\n throw o;\n if (!F(o))\n throw l.info(\"Skipping retry logic for\", o), o;\n c++, await P(n);\n }\n}\nfunction F(t) {\n return !t || typeof t != \"object\" ? !1 : !(\"status\" in t) || G(t.status);\n}\nfunction G(t) {\n return typeof t == \"number\" && (t < 400 || t >= 500);\n}\nasync function v(t, e = {}) {\n const n = await new Promise(N);\n return H(n.search, _, L, W, T)(t, e);\n}\nfunction H(t, ...e) {\n return e.reduce((n, s) => (r, c) => s(r, c, n), t);\n}\nexport {\n v as s\n};\n","import type { SearchQuery } from \"@nosto/nosto-js/client\"\nimport { search as searchFn, SearchOptions } from \"@nosto/search-js\"\n\nconst defaultProductFields = [\n \"productId\",\n \"url\",\n \"name\",\n \"imageUrl\",\n \"imageHash\",\n \"thumbUrl\",\n \"description\",\n \"brand\",\n \"variantId\",\n \"availability\",\n \"price\",\n \"priceText\",\n \"categoryIds\",\n \"categories\",\n \"customFields.key\",\n \"customFields.value\",\n \"priceCurrencyCode\",\n \"datePublished\",\n \"listPrice\",\n \"unitPricingBaseMeasure\",\n \"unitPricingUnit\",\n \"unitPricingMeasure\",\n \"googleCategory\",\n \"gtin\",\n \"ageGroup\",\n \"gender\",\n \"condition\",\n \"alternateImageUrls\",\n \"ratingValue\",\n \"reviewCount\",\n \"inventoryLevel\",\n \"skus.id\",\n \"skus.name\",\n \"skus.price\",\n \"skus.listPrice\",\n \"skus.priceText\",\n \"skus.url\",\n \"skus.imageUrl\",\n \"skus.inventoryLevel\",\n \"skus.customFields.key\",\n \"skus.customFields.value\",\n \"skus.availability\",\n \"pid\",\n \"onDiscount\",\n \"extra.key\",\n \"extra.value\",\n \"saleable\",\n \"available\",\n \"tags1\",\n \"tags2\",\n \"tags3\",\n]\n\n/**\n *\n * @param query Query object.\n * @param options Options object.\n * @returns Promise of search response.\n * @group Autocomplete\n * @category Core\n * @example\n * ```js\n * import { search } from \"@nosto/autocomplete\"\n *\n * search({\n * query: \"shoes\",\n * products: {\n * fields: [\"name\", \"price\"],\n * facets: [\"brand\", \"category\"],\n * size: 10,\n * from: 0,\n * }\n * }).then((state) => {\n * console.log(state.response)\n * })\n * ```\n */\nexport async function search(query: SearchQuery, options: SearchOptions = {}) {\n const fields = query.products?.fields ?? defaultProductFields\n const facets = query.products?.facets ?? [\"*\"]\n const size = query.products?.size ?? 20\n const from = query.products?.from ?? 0\n\n const response = await searchFn(\n {\n ...query,\n products: {\n ...query.products,\n fields,\n facets,\n size,\n from,\n },\n },\n options\n )\n\n return { query, response }\n}\n","import type { SearchQuery } from \"@nosto/nosto-js/client\"\nimport { SearchAutocompleteOptions } from \"./autocomplete\"\nimport { search } from \"./search\"\nimport { HitDecorator } from \"@nosto/search-js\"\n\n/**\n * @group Autocomplete\n * @category Core\n */\nexport interface GoogleAnalyticsConfig {\n /**\n * Path of search page\n * @default \"/search\"\n */\n serpPath?: string\n /**\n * Search query url parameter name\n * @default \"query\"\n */\n queryParamName?: string\n /**\n * Enable Google Analytics\n * @default true\n */\n enabled?: boolean\n}\n\nexport type Selector = string | Element\n\n/**\n * @group Autocomplete\n * @category Core\n */\nexport interface AutocompleteConfig<State> {\n /**\n * The input element to attach the autocomplete to\n */\n inputSelector: Selector\n /**\n * The dropdown element to attach the autocomplete to\n */\n dropdownSelector: Selector | ((input: HTMLInputElement) => Selector)\n /**\n * The function to use to render the dropdown\n */\n render: (container: HTMLElement, state: State) => void | PromiseLike<void>\n /**\n * Minimum length of the query before searching\n */\n minQueryLength?: number\n /**\n * The function to use to fetch the search state\n */\n fetch: SearchQuery | ((input: string) => PromiseLike<State>)\n /**\n * The function to use to submit the search\n */\n submit?: (\n query: string,\n config: AutocompleteConfig<State>,\n options?: SearchAutocompleteOptions\n ) => void\n /**\n * Enable history\n */\n historyEnabled?: boolean\n /**\n * Max number of history items to show\n */\n historySize?: number\n /**\n * Enable Nosto Analytics\n */\n nostoAnalytics?: boolean\n /**\n * Google Analytics configuration. Set to `false` to disable.\n */\n googleAnalytics?: GoogleAnalyticsConfig | boolean\n /**\n * Decorate each search hit before rendering\n *\n * @example\n * ```ts\n * import { priceDecorator } from \"@nosto/autocomplete\"\n *\n * autocomplete({\n * hitDecorators: [\n * priceDecorator({ defaultCurrency: \"USD\" })\n * ]\n * })\n * ```\n */\n hitDecorators?: HitDecorator[]\n /**\n * Whether to submit the form natively or not\n */\n nativeSubmit?: boolean\n /**\n * A function to call when the user clicks on a search hit to use custom routing\n * @example\n * ```ts\n * autocomplete({\n * routingHandler: (url) => {\n * location.href = url\n * }\n * })\n * ```\n */\n routingHandler?: (url: string) => void\n}\n\nexport const defaultGaConfig = {\n serpPath: \"/search\",\n queryParamName: \"query\",\n enabled: true,\n}\n\nexport function getDefaultConfig<State>() {\n return {\n minQueryLength: 2,\n historyEnabled: true,\n historySize: 5,\n hitDecorators: [],\n nostoAnalytics: true,\n googleAnalytics: defaultGaConfig,\n routingHandler: (url) => {\n location.href = url\n },\n nativeSubmit: false,\n submit: (query, config, options) => {\n if (\n query.length >=\n (config.minQueryLength ?? getDefaultConfig<State>().minQueryLength)\n ) {\n search(\n {\n query,\n },\n {\n redirect: true,\n track: config.nostoAnalytics ? \"serp\" : undefined,\n hitDecorators: config.hitDecorators,\n ...options,\n }\n )\n }\n },\n } satisfies Partial<AutocompleteConfig<State>>\n}\n","import { d as A } from \"../deepMerge-CZwCJzEe.js\";\nimport { i as y, a as L, m as h, u as t } from \"../unique-m1TIDWdl.js\";\nimport { i as b, a as j, l as B } from \"../logger-DVwg4Wor.js\";\nimport { p as O } from \"../parseNumber-QA48nJLp.js\";\nimport { p as U } from \"../pick-DReBictn.js\";\nfunction k(s) {\n s.setAttribute(\"autocomplete\", \"off\");\n}\nfunction E(s, { onClick: a, onFocus: c, onInput: o, onKeyDown: i, onSubmit: f }, { form: u = s.form ?? void 0, nativeSubmit: p } = {}) {\n const d = [];\n function r(e, l, v) {\n e.addEventListener(l, v), d.push(() => e.removeEventListener(l, v));\n }\n return (i || f) && r(s, \"keydown\", (e) => {\n i == null || i(s.value, e.key), i && (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") ? e.preventDefault() : f && e.key === \"Enter\" && (s.value !== \"\" && !e.repeat && f(s.value), p || e.preventDefault());\n }), f && u && (r(u, \"submit\", (e) => {\n p || e.preventDefault(), f(s.value);\n }), u.querySelectorAll(\"[type=submit]\").forEach((e) => {\n r(e, \"click\", (l) => {\n p || l.preventDefault(), f(s.value);\n });\n })), a && r(s, \"click\", () => a(s.value)), c && r(s, \"focus\", () => c(s.value)), o && r(s, \"input\", () => o(s.value)), {\n destroy() {\n d.forEach((e) => e());\n }\n };\n}\nexport {\n E as bindInput,\n A as deepMerge,\n k as disableNativeAutocomplete,\n y as isBot,\n b as isEqual,\n j as isPlainObject,\n B as logger,\n L as measure,\n h as mergeArrays,\n O as parseNumber,\n U as pick,\n t as unique\n};\n","import { logger } from \"@nosto/search-js/utils\"\nimport { SearchAutocompleteOptions } from \"./autocomplete\"\n\ntype OnClickBindings<State> = {\n [key: string]: (obj: {\n data: string | undefined\n el: HTMLElement\n update: (state: State) => void\n }) => unknown\n}\n\nexport function createDropdown<State>(\n container: HTMLElement,\n initialState: PromiseLike<State>,\n render: (container: HTMLElement, state: State) => void | PromiseLike<void>,\n submit: (inputValue: string, options?: SearchAutocompleteOptions) => unknown,\n updateInput: (inputValue: string) => void,\n routingHandler: (url: string) => void,\n onClickBindings?: OnClickBindings<State>\n) {\n let elements: HTMLElement[] = []\n let unbindCallbacks: Array<() => void> = []\n\n let isEmpty: boolean = true\n let selectedIndex: number = -1\n\n function handleElementSubmit(el: HTMLElement): void {\n const hit = el?.dataset?.nsHit\n\n if (hit) {\n const parsedHit = parseHit(hit)\n hide()\n\n if (parsedHit?.item) {\n submit(parsedHit.item)\n return\n }\n\n if (parsedHit?.keyword) {\n submit(parsedHit.keyword, {\n redirect: !!parsedHit?._redirect,\n isKeyword: true,\n })\n\n if (parsedHit?._redirect) {\n routingHandler(parsedHit._redirect)\n }\n return\n }\n\n if (parsedHit?.url) {\n\t\troutingHandler(parsedHit.url)\n }\n }\n }\n\n function loadElements() {\n isEmpty = !container.innerHTML.trim()\n\n if (!isEmpty) {\n elements = Array.from<HTMLElement>(\n container.querySelectorAll(\"[data-ns-hit]\")\n ).map(el => {\n bindElementSubmit(el)\n return el\n })\n }\n }\n\n function bindDataCallbacks() {\n Object.entries(onClickBindings ?? {}).map(([key, callback]) => {\n // Convert camelCase to kebab-case\n const dataKey = `[data-ns-${key\n .replace(/([A-Z])/g, \"-$1\")\n .toLowerCase()}]`\n\n Array.from<HTMLElement>(container.querySelectorAll(dataKey)).map(el => {\n const data =\n el?.dataset?.[`ns${key.charAt(0).toUpperCase() + key.slice(1)}`]\n const onClick = () => {\n callback({\n data,\n el,\n update,\n })\n }\n\n el.addEventListener(\"click\", onClick)\n unbindCallbacks.push(() => {\n el.removeEventListener(\"click\", onClick)\n })\n })\n })\n }\n\n function bindElementSubmit(el: HTMLElement) {\n const onSubmit = () => {\n handleElementSubmit(el)\n }\n\n el.addEventListener(\"click\", onSubmit)\n unbindCallbacks.push(() => {\n el.removeEventListener(\"click\", onSubmit)\n })\n }\n\n function highlight(index: number, prevIndex?: number) {\n if (typeof prevIndex === \"number\" && elements[prevIndex]) {\n elements[prevIndex].classList.remove(\"selected\")\n }\n\n if (typeof index === \"number\" && elements[index]) {\n elements[index]?.classList.add(\"selected\")\n\n const hit = elements[index]?.dataset?.nsHit\n\n if (hit) {\n const parsedHit = parseHit(hit)\n\n if (parsedHit.item) {\n updateInput(parsedHit.item)\n return\n }\n\n if (parsedHit.keyword) {\n updateInput(parsedHit.keyword)\n return\n }\n }\n }\n }\n\n function dispose() {\n resetHighlight()\n elements = []\n unbindCallbacks.forEach(v => v())\n unbindCallbacks = []\n }\n\n async function update(state: State) {\n dispose()\n await render(container, state)\n\n // Without setTimeout React does not have committed DOM changes yet, so we don't have the correct elements.\n setTimeout(() => {\n loadElements()\n bindDataCallbacks()\n show()\n }, 0)\n }\n\n function hide() {\n resetHighlight()\n container.style.display = \"none\"\n }\n\n function show() {\n if (!isEmpty) {\n container.style.display = \"\"\n } else {\n hide()\n }\n }\n\n function clear() {\n dispose()\n isEmpty = true\n hide()\n }\n\n function isOpen() {\n return container.style.display !== \"none\"\n }\n\n function goDown() {\n let prevIndex = selectedIndex\n\n if (selectedIndex === elements.length - 1) {\n selectedIndex = 0\n } else {\n prevIndex = selectedIndex++\n }\n\n highlight(selectedIndex, prevIndex)\n }\n\n function goUp() {\n if (hasHighlight()) {\n let prevIndex = selectedIndex\n\n if (selectedIndex === 0) {\n selectedIndex = elements.length - 1\n } else {\n prevIndex = selectedIndex--\n }\n\n highlight(selectedIndex, prevIndex)\n } else {\n selectedIndex = elements.length - 1\n highlight(selectedIndex)\n }\n }\n\n function handleSubmit() {\n if (isOpen() && hasHighlight() && elements[selectedIndex]) {\n handleElementSubmit(elements[selectedIndex])\n }\n }\n\n function hasHighlight() {\n return selectedIndex > -1\n }\n\n function getHighlight() {\n return elements[selectedIndex]\n }\n\n function resetHighlight() {\n if (hasHighlight()) {\n elements[selectedIndex]?.classList.remove(\"selected\")\n selectedIndex = -1\n }\n }\n\n function destroy() {\n dispose()\n isEmpty = true\n container.innerHTML = \"\"\n }\n\n async function init() {\n const state = await Promise.resolve(initialState)\n await Promise.resolve(render(container, state))\n\n // Without setTimeout React does not have committed DOM changes yet, so we don't have the correct elements.\n setTimeout(() => {\n loadElements()\n bindDataCallbacks()\n hide()\n }, 0)\n }\n init()\n\n return {\n update,\n clear,\n isOpen,\n goDown,\n goUp,\n handleSubmit,\n destroy,\n show,\n hide,\n resetHighlight,\n hasHighlight,\n getHighlight,\n container,\n }\n}\n\nexport type Dropdown<T> = ReturnType<typeof createDropdown<T>>\n\ninterface Hit {\n item?: string\n keyword?: string\n url?: string\n _redirect?: string\n}\n\nexport function parseHit(hit: string): Hit {\n try {\n const parsedHit: Hit | undefined | null = JSON.parse(hit)\n return parsedHit ?? {}\n } catch (error) {\n logger.warn(\"Could not parse hit\", error)\n return {}\n }\n}\n","export type Cancellable<T> = { promise: PromiseLike<T>; cancel: () => void }\n\nexport class CancellableError extends Error {}\n\nexport function makeCancellable<T>(promise: PromiseLike<T>): Cancellable<T> {\n let hasCanceled_ = false\n\n const wrappedPromise = new Promise<T>((resolve, reject) => {\n // eslint-disable-next-line promise/prefer-await-to-then\n return promise.then(\n val =>\n hasCanceled_\n ? reject(new CancellableError(\"cancelled promise\"))\n : resolve(val),\n error =>\n hasCanceled_\n ? reject(new CancellableError(\"cancelled promise\"))\n : reject(error)\n )\n })\n\n return {\n promise: wrappedPromise,\n cancel() {\n hasCanceled_ = true\n },\n }\n}\n","import { AutocompleteConfig } from \"../lib/config\"\nimport { History } from \"./history\"\nimport { Cancellable, makeCancellable } from \"./promise\"\nimport { search } from \"../lib/search\"\nimport { SearchAutocompleteOptions } from \"../lib/autocomplete\"\nimport type { InputSearchQuery, SearchResult } from \"@nosto/nosto-js/client\"\n\n/**\n * @group Autocomplete\n * @category Core\n */\nexport interface DefaultState {\n /**\n * The current search query object.\n */\n query?: InputSearchQuery\n /**\n * The current search response.\n */\n response?: SearchResult\n /**\n * The history items.\n */\n history?: { item: string }[]\n}\n\nexport type StateActions<State> = {\n updateState(inputValue?: string): PromiseLike<State>\n addHistoryItem(item: string): PromiseLike<State>\n removeHistoryItem(item: string): PromiseLike<State>\n clearHistory(): PromiseLike<State>\n}\n\nexport function getStateActions<State>({\n config,\n history,\n input,\n}: {\n config: Required<AutocompleteConfig<State>>\n history?: History\n input: HTMLInputElement\n}): StateActions<State> {\n let cancellable: Cancellable<State> | undefined\n\n const fetchState = (\n value: string,\n config: AutocompleteConfig<State>,\n options?: SearchAutocompleteOptions\n ): PromiseLike<State> => {\n if (typeof config.fetch === \"function\") {\n return config.fetch(value)\n } else {\n // @ts-expect-error type mismatch\n return search(\n {\n query: value,\n ...config.fetch,\n },\n {\n track: config.nostoAnalytics ? \"autocomplete\" : undefined,\n redirect: false,\n hitDecorators: config.hitDecorators,\n ...options,\n })\n }\n }\n\n function getHistoryState(query: string): PromiseLike<State> {\n // @ts-expect-error type mismatch\n return Promise.resolve({\n query: {\n query,\n },\n history: history?.getItems(),\n })\n }\n\n function updateState(\n inputValue?: string,\n options?: SearchAutocompleteOptions\n ): PromiseLike<State> {\n cancellable?.cancel()\n\n if (inputValue && inputValue.length >= config.minQueryLength) {\n cancellable = makeCancellable(fetchState(inputValue, config, options))\n return cancellable.promise\n } else if (history) {\n return getHistoryState(inputValue ?? \"\")\n }\n\n return (\n // @ts-expect-error type mismatch\n cancellable?.promise ?? Promise.resolve<State>({})\n )\n }\n\n function addHistoryItem(item: string): PromiseLike<State> {\n if (history) {\n history.add(item)\n }\n return getHistoryState(input.value)\n }\n\n function removeHistoryItem(item: string): PromiseLike<State> {\n if (history) {\n history.remove(item)\n }\n return getHistoryState(input.value)\n }\n\n function clearHistory(): PromiseLike<State> {\n if (history) {\n history.clear()\n }\n return getHistoryState(input.value)\n }\n\n return {\n updateState,\n addHistoryItem,\n removeHistoryItem,\n clearHistory,\n }\n}\n","type Selector = string | Element\n\nexport function findAll<T extends Element>(\n selector: Selector,\n filterType?: { new (): T }\n): T[] {\n const elements =\n typeof selector === \"string\"\n ? Array.from(document.querySelectorAll(selector))\n : [selector]\n return elements.filter((v): v is T =>\n filterType ? v instanceof filterType : true\n )\n}\n\nexport async function DOMReady(): Promise<void> {\n return new Promise(resolve => {\n if (document.readyState !== \"loading\") {\n resolve()\n } else {\n window.addEventListener(\"DOMContentLoaded\", () => {\n resolve()\n })\n }\n })\n}\n\nexport function parents(target: Selector, selector?: string): Element[] {\n let parentList: Element[] = []\n findAll(target).forEach(element => {\n const parent = element.parentNode\n if (parent !== document && parent instanceof Element) {\n parentList.push(parent)\n parentList = parentList.concat(parents(parent))\n }\n })\n return parentList.filter(\n element => selector === undefined || matches(element, selector)\n )\n}\n\nexport function matches(target: Selector, selector: string): boolean {\n const matchesFunc =\n Element.prototype.matches ||\n // @ts-expect-error proprietary method\n Element.prototype.msMatchesSelector ||\n Element.prototype.webkitMatchesSelector\n return findAll(target).some(element => matchesFunc.call(element, selector))\n}\n\nexport function bindClickOutside(\n [element, input]: Array<HTMLElement>,\n callback: () => void\n) {\n const onClick = (event: MouseEvent) => {\n const target = event.target\n\n if (target instanceof HTMLElement && element) {\n if (\n target !== element &&\n target !== input &&\n !parents(target).includes(element)\n ) {\n callback()\n }\n }\n }\n\n document.addEventListener(\"click\", onClick)\n\n return {\n destroy: () => {\n document.removeEventListener(\"click\", onClick)\n },\n }\n}\n","import { logger } from \"@nosto/search-js/utils\"\nimport { AutocompleteConfig, defaultGaConfig } from \"../lib/config\"\n\nconst localStorageKey = \"nosto:autocomplete:gaEvent\"\n\nexport function trackGaPageView(options?: {\n delay?: boolean\n title?: string\n location?: string\n}) {\n const {\n delay = false,\n title = document.title,\n location = window.location.href,\n } = options || {}\n\n if (delay) {\n saveToLocalStorage(title, location)\n } else {\n if (\"gtag\" in window && typeof window.gtag === \"function\") {\n const accounts =\n \"google_tag_manager\" in window &&\n typeof window.google_tag_manager === \"object\"\n ? Object.keys(window.google_tag_manager || []).filter(e => {\n return e.substring(0, 2) == \"G-\"\n })\n : []\n\n if (accounts.length > 1) {\n for (let i = 0; i < accounts.length; i++) {\n window.gtag(\"event\", \"page_view\", {\n page_title: title,\n page_location: location,\n send_to: accounts[i],\n })\n }\n } else {\n window.gtag(\"event\", \"page_view\", {\n page_title: title,\n page_location: location,\n })\n }\n }\n if (\n \"ga\" in window &&\n typeof window.ga === \"function\" &&\n \"getAll\" in window.ga &&\n typeof window.ga.getAll === \"function\"\n ) {\n try {\n const url = new URL(location)\n const trackers = window.ga!.getAll()\n if (trackers?.length > 0) {\n trackers[0]?.send(\"pageview\", url.pathname + url.search)\n }\n } catch (error) {\n logger.warn(\"Could not send pageview to GA\", error)\n }\n }\n }\n}\n\nexport const isGaEnabled = <State>(config: AutocompleteConfig<State>) =>\n typeof config.googleAnalytics === \"boolean\"\n ? config.googleAnalytics\n : typeof config.googleAnalytics === \"object\" &&\n config.googleAnalytics.enabled\n\nexport const getGaTrackUrl = <State>(\n value: string | undefined,\n config: AutocompleteConfig<State>\n) => {\n const gaConfig = isGaEnabled(config)\n ? typeof config.googleAnalytics === \"boolean\"\n ? defaultGaConfig\n : {\n ...defaultGaConfig,\n ...config.googleAnalytics,\n }\n : undefined\n\n if (value && gaConfig) {\n try {\n return new URL(\n `${gaConfig?.serpPath || location.pathname}?${`${encodeURIComponent(\n gaConfig.queryParamName\n )}=${encodeURIComponent(value).replace(/%20/g, \"+\")}`}`,\n window.location.origin\n ).toString()\n } catch (error) {\n logger.warn(\"Could not create track url\", error)\n return undefined\n }\n }\n}\n\nfunction saveToLocalStorage(title: string, location: string): void {\n localStorage.setItem(localStorageKey, JSON.stringify({ title, location }))\n}\n\ninterface Event {\n title: string\n location: string\n}\n\nexport function consumeLocalStorageEvent() {\n const eventString = localStorage.getItem(localStorageKey)\n if (typeof eventString === \"string\") {\n localStorage.removeItem(localStorageKey)\n try {\n const event: Event = JSON.parse(eventString)\n trackGaPageView(event)\n } catch (e) {\n logger.warn(\"Could not consume pageView\", e)\n }\n }\n}\n","export default function (delay: number) {\n let timeoutId: ReturnType<typeof setTimeout> | undefined\n let leadingExecuted = false\n\n return (callback: () => void) => {\n if (!leadingExecuted) {\n callback()\n leadingExecuted = true\n } else {\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n timeoutId = setTimeout(async () => {\n await callback()\n leadingExecuted = false\n }, delay)\n }\n }\n}\n","import { logger } from \"@nosto/search-js/utils\"\nimport { DefaultState } from \"./state\"\n\ntype Items = NonNullable<DefaultState[\"history\"]>\n\nexport function createHistory(size: number) {\n const localStorageKey = \"nosto:autocomplete:history\"\n let items = get()\n\n function get(): Items {\n try {\n return JSON.parse(localStorage.getItem(localStorageKey) ?? \"[]\") ?? []\n } catch (err) {\n logger.error(\"Could not get history items.\", err)\n return []\n }\n }\n\n function set(data: Items) {\n try {\n localStorage.setItem(localStorageKey, JSON.stringify(data))\n } catch (err) {\n logger.error(\"Could not set history items.\", err)\n }\n }\n\n function add(item: string) {\n set(\n (items = [{ item }, ...(items?.filter(v => v.item !== item) || [])].slice(\n 0,\n size\n ))\n )\n }\n\n function clear() {\n set((items = []))\n }\n\n function remove(item: string) {\n set((items = items.filter(v => v.item !== item)))\n }\n\n function getItems() {\n return items\n }\n\n return {\n add,\n clear,\n remove,\n getItems,\n }\n}\n\nexport type History = ReturnType<typeof createHistory>\n","import { recordSearchClick, recordSearchSubmit } from \"./client\"\nimport { AutocompleteConfig, getDefaultConfig } from \"./config\"\nimport { Dropdown, createDropdown, parseHit } from \"./dropdown\"\nimport { DefaultState, StateActions, getStateActions } from \"../utils/state\"\nimport { bindClickOutside, findAll } from \"../utils/dom\"\nimport { consumeLocalStorageEvent, getGaTrackUrl, isGaEnabled, trackGaPageView } from \"../utils/ga\"\nimport createDebouncer from \"../utils/debounce\"\nimport { createHistory } from \"../utils/history\"\nimport { bindInput, disableNativeAutocomplete } from \"@nosto/search-js/utils\"\nimport type { SearchOptions } from \"@nosto/nosto-js/client\"\nimport { logger } from \"@nosto/search-js/utils\"\n\nexport type AutocompleteInstance = {\n /**\n * Open the dropdown.\n */\n open(): void\n /**\n * Close the dropdown.\n */\n close(): void\n /**\n * Destroy the autocomplete instance.\n */\n destroy(): void\n}\n\nexport type SearchAutocompleteOptions = Pick<\n SearchOptions,\n \"isKeyword\" | \"redirect\"\n>\n\n/**\n * @param config Autocomplete configuration.\n * @returns Autocomplete instance.\n * @group Autocomplete\n * @category Core\n * @example\n * ```js\n * import { autocomplete } from '@nosto/autocomplete';\n *\n * // Basic usage\n * autocomplete({\n * inputSelector: '#search',\n * dropdownSelector: '#dropdown',\n * render: (container, state) => {\n * container.innerHTML = `\n * <div>\n * <h1>${state.query}</h1>\n * <ul>\n * ${state.response.products\n * .map((product) => `<li>${product.name}</li>`)\n * .join('')}\n * </ul>\n * </div>\n * `;\n * },\n * fetch: {\n * products: {\n * fields: ['name', 'url', 'imageUrl', 'price', 'listPrice', 'brand'],\n * size: 5\n * },\n * keywords: {\n * size: 5,\n * fields: ['keyword', '_highlight.keyword'],\n * highlight: {\n * preTag: `<strong>`,\n * postTag: '</strong>'\n * }\n * }\n * }\n * });\n *\n * // Liquid template.\n * import { autocomplete, fromRemoteLiquidTemplate, fromLiquidTemplate, defaultLiquidTemplate } from '@nosto/autocomplete/liquid';\n *\n * autocomplete({\n * inputSelector: '#search',\n * dropdownSelector: '#dropdown',\n * render: fromRemoteLiquidTemplate('autocomplete.liquid'),\n * // Or:\n * render: window.nostoAutocomplete.fromLiquidTemplate(defaultLiquidTemplate),\n * ...\n * });\n *\n * // Mustache template.\n * import { autocomplete, fromRemoteMustacheTemplate, fromMustacheTemplate, defaultMustacheTemplate } from '@nosto/autocomplete/mustache';\n *\n * autocomplete({\n * inputSelector: '#search',\n * dropdownSelector: '#dropdown',\n * render: fromRemoteMustacheTemplate('autocomplete.mustache'),\n * // Or:\n * render: fromMustacheTemplate(defaultMustacheTemplate),\n * ...\n * });\n *\n * // React example.\n * import { autocomplete, Autocomplete } from '@nosto/autocomplete/react';\n * import ReactDOM from 'react-dom';\n *\n * let reactRoot;\n *\n * autocomplete({\n * inputSelector: '#search',\n * dropdownSelector: '#dropdown',\n * render: function (container, state) {\n * if (!reactRoot) {\n * reactRoot = ReactDOM.createRoot(container);\n * }\n * reactRoot.render(<Autocomplete {...state} />);\n * },\n * ...\n * });\n * ```\n */\nexport function autocomplete<State = DefaultState>(\n config: AutocompleteConfig<State>\n): AutocompleteInstance {\n const fullConfig = {\n ...getDefaultConfig<State>(),\n ...config,\n } satisfies AutocompleteConfig<State>\n\n const history = fullConfig.historyEnabled\n ? createHistory(fullConfig.historySize)\n : undefined\n\n const debounce = createDebouncer(300)\n\n if (isGaEnabled(config)) {\n setTimeout(consumeLocalStorageEvent, 1000)\n }\n\n const dropdowns = findAll(config.inputSelector, HTMLInputElement).map(\n inputElement => {\n const actions = getStateActions({\n config: fullConfig,\n history,\n input: inputElement,\n })\n\n const dropdown = createInputDropdown({\n input: inputElement,\n config: fullConfig,\n actions,\n })\n\n if (!dropdown) {\n return\n }\n\n disableNativeAutocomplete(inputElement)\n\n const input = bindInput(inputElement, {\n onInput: async value => {\n debounce(async () => {\n const state = await actions.updateState(value)\n dropdown.update(state)\n })\n },\n onClick() {\n dropdown.resetHighlight()\n },\n onFocus() {\n dropdown.show()\n },\n onSubmit() {\n if (dropdown.isOpen() && dropdown.hasHighlight()) {\n const data = dropdown.getHighlight()?.dataset?.nsHit\n if (data) {\n trackClick({\n config: fullConfig,\n data,\n query: inputElement.value,\n })\n }\n dropdown.handleSubmit()\n } else {\n submitWithContext({\n actions,\n config: fullConfig,\n })(inputElement.value)\n }\n },\n onKeyDown(_, key) {\n if (key === \"Escape\") {\n dropdown.hide()\n } else if (key === \"ArrowDown\") {\n if (dropdown.isOpen()) {\n dropdown.goDown()\n } else {\n dropdown.show()\n }\n } else if (key === \"ArrowUp\") {\n if (dropdown.isOpen()) {\n dropdown.goUp()\n }\n }\n },\n }, {\n nativeSubmit: fullConfig.nativeSubmit,\n })\n\n const clickOutside = bindClickOutside(\n [dropdown.container, inputElement],\n () => {\n dropdown.hide()\n }\n )\n\n return {\n open() {\n dropdown.show()\n },\n close() {\n dropdown.hide()\n },\n destroy() {\n input.destroy()\n clickOutside.destroy()\n dropdown.destroy()\n },\n }\n }\n )\n\n return {\n destroy() {\n dropdowns.forEach(dropdown => dropdown?.destroy())\n },\n open() {\n dropdowns.forEach(dropdown => dropdown?.open())\n },\n close() {\n dropdowns.forEach(dropdown => dropdown?.close())\n },\n }\n}\n\nfunction createInputDropdown<State>({\n input,\n config,\n actions,\n}: {\n input: HTMLInputElement\n config: AutocompleteConfig<State>\n actions: StateActions<State>\n}): Dropdown<State> | undefined {\n const dropdownElements =\n typeof config.dropdownSelector === \"function\"\n ? findAll(config.dropdownSelector(input), HTMLElement)\n : findAll(config.dropdownSelector, HTMLElement)\n\n if (dropdownElements.length === 0) {\n logger.error(`No dropdown element found for input ${input}`)\n return\n } else if (dropdownElements.length > 1) {\n logger.error(\n `Multiple dropdown elements found for input ${input}, using the first element`\n )\n }\n\n const dropdownElement = dropdownElements[0]\n\n return createDropdown<State>(\n dropdownElement,\n actions.updateState(input.value),\n config.render,\n submitWithContext({\n actions,\n config,\n }),\n value => (input.value = value),\n config.routingHandler!,\n {\n removeHistory: async function ({ data, update }) {\n if (data === \"all\") {\n const state = await actions.clearHistory()\n return update(state)\n } else if (data) {\n const state = await actions.removeHistoryItem(data)\n return update(state)\n }\n },\n hit: function ({ data }) {\n if (data) {\n trackClick({ config, data, query: input.value })\n }\n },\n }\n )\n}\n\nasync function trackClick<State>({\n config,\n data,\n query,\n}: {\n config: AutocompleteConfig<State>\n data: string\n query: string\n}) {\n if (!config.googleAnalytics && !config.nostoAnalytics) {\n return\n }\n\n const parsedHit = parseHit(data)\n\n if (config.nostoAnalytics) {\n if (parsedHit) {\n // @ts-expect-error type mismatch\n recordSearchClick(parsedHit)\n }\n }\n\n if (isGaEnabled(config)) {\n if (parsedHit._redirect) {\n trackGaPageView({\n delay: true,\n location: getGaTrackUrl(parsedHit.keyword, config),\n })\n }\n\n if (parsedHit.url) {\n trackGaPageView({\n delay: true,\n location: getGaTrackUrl(query, config),\n })\n }\n }\n}\n\nfunction submitWithContext<State>(context: {\n config: AutocompleteConfig<State>\n actions: StateActions<State>\n}) {\n return async (value: string, options?: SearchAutocompleteOptions) => {\n const { config, actions } = context\n const { redirect = false } = options ?? {}\n\n if (value.length > 0) {\n if (config.historyEnabled) {\n actions.addHistoryItem(value)\n }\n\n if (config.nostoAnalytics) {\n recordSearchSubmit(value)\n }\n\n if (isGaEnabled(config)) {\n trackGaPageView({\n delay: true,\n location: getGaTrackUrl(value, config),\n })\n }\n\n if (!redirect && typeof config?.submit === \"function\") {\n config.submit(value, config, options)\n }\n }\n }\n}\n","import { s as y } from \"../index.es-B8mbAxS4.js\";\nconst p = {\n defaultCurrency: \"EUR\",\n defaultLocale: \"en-US\",\n /** @hidden */\n currencySettings: {}\n}, d = {\n EUR: \"de-DE\",\n GBP: \"en-GB\",\n USD: \"en-US\",\n AUD: \"en-AU\",\n CAD: \"en-CA\",\n //India, Afghanistan, Bangladesh, Bhutan, Myanmar, Nepal, and Pakistan uses lakhs and crores notation\n INR: \"en-IN\",\n AFN: \"en-IN\",\n BDT: \"en-IN\",\n BTN: \"en-IN\",\n MMK: \"en-IN\",\n NPR: \"en-IN\",\n PKR: \"en-IN\"\n};\nfunction g(i = {}) {\n const o = {\n ...p,\n ...i\n };\n i.currencySettings || y((c) => {\n o.currencySettings = c.internal.getSettings().currencySettings ?? {};\n });\n function a(c, n) {\n const { defaultCurrency: t, currencySettings: r, defaultLocale: u } = o, s = n ?? t, f = d[s] ?? u;\n if (s in r) {\n const e = r[s], l = new Intl.NumberFormat(f, {\n useGrouping: !!e.groupingSeparator,\n minimumFractionDigits: e.decimalPlaces,\n maximumFractionDigits: e.decimalPlaces\n }).formatToParts(c).map((m) => m.type === \"group\" ? e.groupingSeparator : m.type === \"decimal\" ? e.decimalCharacter : m.value).join(\"\");\n return e != null && e.currencyBeforeAmount ? `${e.currencyToken}${l}` : `${l}${e == null ? void 0 : e.currencyToken}`;\n }\n return new Intl.NumberFormat(f, {\n style: \"currency\",\n currency: s\n }).format(c);\n }\n return {\n formatCurrency: a\n };\n}\nfunction S(i) {\n const { formatCurrency: o } = g(i);\n function a(n, t) {\n const r = {};\n return n.price !== void 0 && (r.priceText = o(n.price, t)), n.listPrice !== void 0 && (r.listPriceText = o(n.listPrice, t)), Object.assign({}, n, r);\n }\n function c(n) {\n return n.price !== void 0 || n.listPrice !== void 0;\n }\n return function(t) {\n if (!c(t))\n return t;\n const r = a(t, t.priceCurrencyCode);\n return r.skus && r.skus.some(c) && (r.skus = r.skus.map((u) => c(u) ? a(u, t.priceCurrencyCode) : u)), r;\n };\n}\nexport {\n g as getCurrencyFormatting,\n S as priceDecorator\n};\n","import { autocomplete } from \"./autocomplete\"\nimport type { AutocompleteConfig } from \"./config\"\nimport type { DefaultState } from \"../index\"\n\ntype TemplateProps = {\n handler: (template: string) => AutocompleteConfig<DefaultState>[\"render\"]\n defaultTemplate: string\n}\n\nexport async function initAutocomplete(\n element: HTMLElement,\n { handler, defaultTemplate }: TemplateProps\n) {\n const templateContent = element.querySelector<HTMLScriptElement>(\n \"script[autocomplete-template]\"\n )?.textContent\n \n const config = getConfigFromScript(element)\n if (!Object.keys(config).length) {\n throw new Error(\"NostoAutocomplete: Missing required config.\")\n }\n return autocomplete({\n ...config,\n nativeSubmit: true,\n render: handler(templateContent ?? defaultTemplate),\n submit(value) {\n element.querySelector<HTMLInputElement>(config.inputSelector)!.value = value\n element.querySelector(\"form\")?.submit()\n }\n })\n}\n\nfunction getConfigFromScript(element: HTMLElement) {\n const config = element.querySelector(\"script[autocomplete-config]\")\n return config ? JSON.parse(config.textContent!) : {}\n}\n","import { initAutocomplete } from \"../lib/components\"\nimport { fromLiquidTemplate, defaultLiquidTemplate } from \"./fromLiquidTemplate\"\n\n/**\n * Nosto Autocomplete Web Component using Liquid templates.\n *\n * This component integrates the Nosto Autocomplete functionality with Liquid templating.\n * It fetches the configuration from a script tag, compiles the Liquid template,\n * and initializes the autocomplete with the compiled template as the render function.\n * If a custom template isn't passed, the component will use the default Liquid template.\n *\n * @example\n * ```html\n * <nosto-autocomplete>\n * <form>\n * <input type=\"text\" id=\"input\" />\n * </form>\n * <div id=\"results\"></div>\n * <script autocomplete-config type=\"application/json\">\n * {\n * \"inputSelector\": \"#input\",\n * \"dropdownSelector\": \"#results\",\n * \"fetch\": {\n * \"products\": {\n * \"fields\": [\"name\", \"url\", \"imageUrl\", \"price\", \"listPrice\", \"brand\"],\n * \"size\": 5\n * },\n * \"keywords\": {\n * \"size\": 5,\n * \"fields\": [\"keyword\", \"_highlight.keyword\"]\n * }\n * }\n * }\n * </script>\n * <script type=\"text/x-liquid\" autocomplete-template>\n * <h1>{{ query }}</h1>\n * <ul>\n * {% for product i