@nosto/autocomplete
Version:
Library designed to simplify the implementation of search autocomplete functionality
1 lines • 77.5 kB
Source Map (JSON)
{"version":3,"file":"autocomplete.cjs","sources":["../../src/mustache/fromMustacheTemplate.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-XNBESE3P.js","../../node_modules/@nosto/search-js/dist/storage-B97DRj5i.js","../../node_modules/@nosto/search-js/dist/search-D1jQPG6P.js","../../src/lib/search.ts","../../src/lib/config.ts","../../node_modules/@nosto/search-js/dist/disableNativeAutocomplete-Bkta_WR2.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/mustache/NostoAutocomplete.ts"],"sourcesContent":["import { DefaultState } from \"../utils/state\"\nimport Mustache from \"mustache\"\n\nexport { default as defaultMustacheTemplate } from \"./autocomplete.mustache\"\n\nexport type Options = {\n /**\n * Mustache helpers to extend template functionality.\n */\n helpers?: object\n}\n\n/**\n * Render a Mustache template into a container\n *\n * @param template Mustache template\n * @param options Options object.\n * @returns Render function\n * @group Autocomplete\n * @category Mustache\n/**\n * @example\n * ```js\n * import { fromMustacheTemplate } from \"@nosto/autocomplete/mustache\";\n *\n * const render = fromMustacheTemplate(`\n * <div>\n * <h1>{{title}}</h1>\n * <ul>\n * {{#products}}\n * <li>{{name}}</li>\n * {{/products}}\n * </ul>\n * </div>\n * `, {\n * helpers: {\n * toJson: function () {\n * return JSON.stringify(this)\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 */\nexport function fromMustacheTemplate<State extends object = DefaultState>(template: string, options?: Options) {\n if (Mustache === undefined) {\n throw new Error(\"Mustache is not defined. Please include the Mustache dependency or library in your page.\")\n }\n\n const { helpers } = options || {}\n\n return (container: HTMLElement, state: State) => {\n container.innerHTML = Mustache.render(template, {\n ...state,\n imagePlaceholder: \"https://cdn.nosto.com/nosto/9/mock\",\n toJson: function () {\n return JSON.stringify(this)\n },\n showListPrice: function () {\n return this.listPrice !== this.price\n },\n ...helpers\n })\n\n return Promise.resolve()\n }\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 g } from \"./settings-DN0V2st5.js\";\nimport { g as x, n as L } from \"./settings-DN0V2st5.js\";\nfunction b() {\n return typeof window.nosto < \"u\";\n}\nfunction k() {\n return window.nosto;\n}\nconst f = {\n production: \"https://connect.nosto.com/\",\n staging: \"https://connect.staging.nosto.com/\",\n local: \"https://connect.nosto.com/\"\n};\nfunction u(o) {\n return f[o ?? \"production\"];\n}\nfunction h({ merchantId: o, env: n, options: e, shopifyInternational: a, scriptLoader: s }) {\n const t = document.querySelector(\"script[nosto-language], script[nosto-market-id]\"), r = String(a?.marketId || \"\"), i = a?.language || \"\", l = t?.getAttribute(\"nosto-language\") !== i || t?.getAttribute(\"nosto-market-id\") !== r;\n if (!t || l) {\n const d = document.querySelector(\"#nosto-sandbox\");\n t?.parentNode?.removeChild(t), d?.parentNode?.removeChild(d);\n const c = new URL(\"/script/shopify/market/nosto.js\", u(n));\n c.searchParams.append(\"merchant\", o), c.searchParams.append(\"market\", r), c.searchParams.append(\"locale\", i.toLowerCase());\n const m = {\n ...e?.attributes,\n \"nosto-language\": i,\n \"nosto-market-id\": r\n };\n return (s ?? p)(c.toString(), { ...e, attributes: m });\n }\n return Promise.resolve();\n}\nfunction y(o) {\n if (o.shopifyInternational)\n return h(o);\n const { merchantId: n, env: e, options: a, scriptLoader: s } = o, t = s ?? p, r = new URL(`/include/${n}`, u(e));\n return t(r.toString(), a);\n}\nfunction p(o, n) {\n return new Promise((a, s) => {\n const t = document.createElement(\"script\");\n t.src = o, t.async = !0, t.type = \"text/javascript\", t.onload = () => a(), t.onerror = () => s(), Object.entries(n?.attributes ?? {}).forEach(([r, i]) => t.setAttribute(r, i)), n?.position === \"head\" ? document.head.appendChild(t) : document.body.appendChild(t);\n });\n}\nasync function C(o, n, e) {\n if (!window.Nosto?.addSkuToCart)\n throw new Error(\"Nosto addSkuToCart function is not available\");\n await window.Nosto.addSkuToCart(o, n, e);\n}\ntypeof window < \"u\" && g();\nexport {\n C as addSkuToCart,\n k as getNostoWindow,\n x as getSettings,\n y as init,\n g as initNostoStub,\n b as isNostoLoaded,\n L 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 n() {\n window.nostojs = window.nostojs ?? function(o) {\n (window.nostojs.q = window.nostojs.q ?? []).push(o);\n };\n}\nasync function s(o) {\n return window.nostojs(o);\n}\nlet t = null;\ntypeof window < \"u\" && (n(), s((o) => {\n t = o.internal.getSettings();\n}));\nfunction d() {\n return t;\n}\nasync function u(o, i, w) {\n if (!window.Nosto?.addSkuToCart)\n throw new Error(\"Nosto addSkuToCart function is not available\");\n await window.Nosto.addSkuToCart(o, i, w);\n}\ntypeof window < \"u\" && n();\nexport {\n u as C,\n d as i,\n s\n};\n","import { s as I } from \"./index.es-XNBESE3P.js\";\nfunction c(e) {\n return String(e) === \"[object Object]\";\n}\nfunction g(e) {\n if (!c(e))\n return !1;\n const t = e.constructor;\n if (t === void 0)\n return !0;\n const r = t.prototype;\n return !(!c(r) || !r.hasOwnProperty(\"isPrototypeOf\"));\n}\nfunction u(e, t) {\n if (e === t)\n return !0;\n if (e instanceof Date && t instanceof Date)\n return e.getTime() === t.getTime();\n if (Array.isArray(e) && Array.isArray(t))\n return e.length !== t.length ? !1 : e.every((r, n) => u(r, t[n]));\n if (g(e) && g(t)) {\n const r = Object.entries(e);\n return r.length !== Object.keys(t).length ? !1 : r.every(([n, S]) => u(S, t[n]));\n }\n return !1;\n}\nfunction f() {\n const e = /* @__PURE__ */ new Map();\n return {\n getItem(t) {\n return e.get(t) ?? null;\n },\n setItem(t, r) {\n e.set(t, r);\n },\n removeItem(t) {\n e.delete(t);\n },\n clear() {\n e.clear();\n },\n key(t) {\n return Array.from(e.keys())[t] ?? null;\n },\n get length() {\n return e.size;\n }\n };\n}\nfunction a(e) {\n return (...t) => {\n I((r) => {\n (r.internal.context.mode.isPreview() ? console[e] : r.internal.logger[e])(...t);\n });\n };\n}\nconst i = {\n debug: a(\"debug\"),\n info: a(\"info\"),\n warn: a(\"warn\"),\n error: a(\"error\")\n};\nfunction l(e, t, r) {\n try {\n const n = JSON.stringify(t);\n r.setItem(e, n);\n } catch (n) {\n i.warn(n);\n }\n}\nfunction m(e, t) {\n try {\n const r = t.getItem(e);\n if (r)\n return JSON.parse(r);\n } catch (r) {\n i.warn(r);\n }\n}\nfunction y(e, t) {\n try {\n t.removeItem(e);\n } catch (r) {\n i.warn(r);\n }\n}\nlet o, s;\ntry {\n o = window.localStorage, s = window.sessionStorage;\n} catch (e) {\n i.warn(e), o = f(), s = f();\n}\nfunction h(e, t) {\n l(e, t, o);\n}\nfunction d(e, t) {\n l(e, t, s);\n}\nfunction p(e) {\n return m(e, o);\n}\nfunction O(e) {\n return m(e, s);\n}\nfunction v(e) {\n y(e, o);\n}\nfunction j(e) {\n y(e, s);\n}\nexport {\n O as a,\n g as b,\n j as c,\n d,\n p as g,\n u as i,\n i as l,\n v as r,\n h as s\n};\n","import { s as m } from \"./index.es-XNBESE3P.js\";\nimport { d as g, a as y, i as l, l as S } from \"./storage-B97DRj5i.js\";\nasync function C(t, { hitDecorators: e, ...n }, s) {\n const r = await s(t, n);\n if (!r.products?.hits?.length || !e?.length)\n return r;\n const c = (o) => e.reduce((i, a) => a(i), o);\n return {\n ...r,\n products: {\n ...r.products,\n hits: r.products.hits.map(c)\n }\n };\n}\nconst h = \"nosto:search-js:cache\", R = 60 * 1e3;\nfunction b(t, e) {\n g(h, { query: t, result: e, created: Date.now() });\n}\nfunction k(t) {\n const e = y(h);\n if (!e || !T(e))\n return null;\n const n = d(e.query);\n return !l(d(t), n) || Date.now() - e.created > R ? null : e.result;\n}\nfunction d(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 T(t) {\n return typeof t == \"object\" && t !== null && \"query\" in t && \"result\" in t && \"created\" in t;\n}\nasync function z(t, { usePersistentCache: e, ...n }, s) {\n if (!e)\n return s(t, n);\n const r = await E(t, n, s);\n return b(t, r), r;\n}\nasync function E(t, e, n) {\n const { from: s = 0, size: r = 0 } = t.products || {}, c = k(t);\n if (!c)\n return await n(t, e);\n const o = c?.products?.size ?? 0, i = c?.products?.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: c.products?.total || 0\n }\n };\n const a = r - i.length, p = s > 0 ? s + 1 : r - a, w = {\n ...t,\n products: {\n ...t.products,\n from: p,\n size: a\n }\n }, f = await n(w, e);\n return {\n ...c,\n products: {\n ...c.products,\n size: r,\n hits: [...c.products?.hits || [], ...f.products?.hits || []],\n total: f.products?.total || 0\n }\n };\n}\nconst N = 3e4, u = /* @__PURE__ */ new Map();\nfunction W(t, e) {\n const n = u.get(t);\n if (!n) return;\n const s = Date.now() - n.created > N, r = l(e, n.query);\n if (s || !r) {\n u.delete(t);\n return;\n }\n return n.result;\n}\nfunction _(t, e, n) {\n u.set(t, {\n query: e,\n result: n,\n created: Date.now()\n });\n}\nasync function D(t, e, n) {\n if (!e.useMemoryCache)\n return n(t, e);\n const s = JSON.stringify(t), r = W(s, t);\n if (r) return r;\n const c = await n(t, e);\n return _(s, t, c), c;\n}\nasync function M(t, e, n) {\n if (!e.redirect)\n return n(t, e);\n const s = await n(t, e);\n if (s.keywords?.hits?.length) {\n const r = s.keywords.hits.find((c) => c.keyword === t.query && c._redirect);\n r && r._redirect && (window.location.href = r._redirect);\n }\n return s;\n}\nfunction O(t) {\n return new Promise((e) => setTimeout(e, t));\n}\nasync function j(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 (!J(o))\n throw S.info(\"Skipping retry logic for\", o), o;\n c++, await O(n);\n }\n}\nfunction J(t) {\n return !t || typeof t != \"object\" ? !1 : !(\"status\" in t) || V(t.status);\n}\nfunction V(t) {\n return typeof t == \"number\" && (t < 400 || t >= 500);\n}\nasync function K(t, e = {}) {\n const n = await new Promise(m);\n return x(\n n.search,\n j,\n M,\n D,\n z,\n C\n )(t, e);\n}\nfunction x(t, ...e) {\n return e.reduce((n, s) => (r, c) => s(r, c, n), t);\n}\nexport {\n K 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?: (query: string, config: AutocompleteConfig<State>, options?: SearchAutocompleteOptions) => 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 (query.length >= (config.minQueryLength ?? getDefaultConfig<State>().minQueryLength)) {\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","function y(f, { onClick: d, onFocus: r, onInput: v, onKeyDown: u, onSubmit: l }, { form: c = f.form ?? void 0, nativeSubmit: p } = {}) {\n const o = [];\n function i(e, s, a) {\n e.addEventListener(s, a), o.push(() => e.removeEventListener(s, a));\n }\n return (u || l) && i(f, \"keydown\", (e) => {\n const s = u?.(f.value, e.key);\n u && (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") ? e.preventDefault() : l && e.key === \"Enter\" && (f.value !== \"\" && !e.repeat && !s && l(f.value), p || e.preventDefault());\n }), l && c && (i(c, \"submit\", (e) => {\n p || e.preventDefault(), l(f.value);\n }), c.querySelectorAll(\"[type=submit]\").forEach((e) => {\n i(e, \"click\", (s) => {\n p || s.preventDefault(), l(f.value);\n });\n })), d && i(f, \"click\", () => d(f.value)), r && i(f, \"focus\", () => r(f.value)), v && i(f, \"input\", () => v(f.value)), {\n destroy() {\n o.forEach((e) => e());\n }\n };\n}\nfunction k(f) {\n f.setAttribute(\"autocomplete\", \"off\");\n}\nexport {\n y as b,\n k as d\n};\n","import { logger } from \"@nosto/search-js/utils\"\nimport { SearchAutocompleteOptions } from \"./autocomplete\"\n\ntype OnClickBindings<State> = {\n [key: string]: (obj: { data: string | undefined; el: HTMLElement; update: (state: State) => void }) => 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 routingHandler(parsedHit.url)\n }\n }\n }\n\n function loadElements() {\n isEmpty = !container.innerHTML.trim()\n\n if (!isEmpty) {\n elements = Array.from<HTMLElement>(container.querySelectorAll(\"[data-ns-hit]\")).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.replace(/([A-Z])/g, \"-$1\").toLowerCase()}]`\n\n Array.from<HTMLElement>(container.querySelectorAll(dataKey)).map(el => {\n const data = 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 => (hasCanceled_ ? reject(new CancellableError(\"cancelled promise\")) : resolve(val)),\n error => (hasCanceled_ ? reject(new CancellableError(\"cancelled promise\")) : 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\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(inputValue?: string, options?: SearchAutocompleteOptions): 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>(selector: Selector, filterType?: { new (): T }): T[] {\n const elements = typeof selector === \"string\" ? Array.from(document.querySelectorAll(selector)) : [selector]\n return elements.filter((v): v is T => (filterType ? v instanceof filterType : true))\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(element => selector === undefined || matches(element, selector))\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([element, input]: Array<HTMLElement>, callback: () => void) {\n const onClick = (event: MouseEvent) => {\n const target = event.target\n\n if (target instanceof HTMLElement && element) {\n if (target !== element && target !== input && !parents(target).includes(element)) {\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?: { delay?: boolean; title?: string; location?: string }) {\n const { delay = false, title = document.title, location = window.location.href } = 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 && 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\" && config.googleAnalytics.enabled\n\nexport const getGaTrackUrl = <State>(value: string | undefined, config: AutocompleteConfig<State>) => {\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((items = [{ item }, ...(items?.filter(v => v.item !== item) || [])].slice(0, size)))\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<SearchOptions, \"isKeyword\" | \"redirect\">\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>(config: AutocompleteConfig<State>): AutocompleteInstance {\n const fullConfig = {\n ...getDefaultConfig<State>(),\n ...config\n } satisfies AutocompleteConfig<State>\n\n const history = fullConfig.historyEnabled ? createHistory(fullConfig.historySize) : 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(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(\n inputElement,\n {\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 {\n nativeSubmit: fullConfig.nativeSubmit\n }\n )\n\n const clickOutside = bindClickOutside([dropdown.container, inputElement], () => {\n dropdown.hide()\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 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(`Multiple dropdown elements found for input ${input}, using the first element`)\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: { config: AutocompleteConfig<State>; actions: StateActions<State> }) {\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-XNBESE3P.js\";\nconst g = {\n defaultCurrency: \"EUR\",\n defaultLocale: \"en-US\",\n /** @hidden */\n currencySettings: {}\n}, p = {\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 d(u = {}) {\n const i = {\n ...g,\n ...u\n };\n u.currencySettings || y((t) => {\n i.currencySettings = t.internal.getSettings().currencySettings ?? {};\n });\n function s(t, e) {\n const { defaultCurrency: n, currencySettings: r, defaultLocale: o } = i, a = e ?? n, f = p[a] ?? o;\n if (a in r) {\n const c = r[a], l = new Intl.NumberFormat(f, {\n useGrouping: !!c.groupingSeparator,\n minimumFractionDigits: c.decimalPlaces,\n maximumFractionDigits: c.decimalPlaces\n }).formatToParts(t).map((m) => m.type === \"group\" ? c.groupingSeparator : m.type === \"decimal\" ? c.decimalCharacter : m.value).join(\"\");\n return c?.currencyBeforeAmount ? `${c.currencyToken}${l}` : `${l}${c?.currencyToken}`;\n }\n return new Intl.NumberFormat(f, {\n style: \"currency\",\n currency: a\n }).format(t);\n }\n return {\n formatCurrency: s\n };\n}\nfunction S(u) {\n const { formatCurrency: i } = d(u);\n function s(e, n) {\n const r = {};\n return e.price !== void 0 && (r.priceText = i(e.price, n)), e.listPrice !== void 0 && (r.listPriceText = i(e.listPrice, n)), Object.assign({}, e, r);\n }\n function t(e) {\n return e.price !== void 0 || e.listPrice !== void 0;\n }\n return function(n) {\n if (!t(n))\n return n;\n const r = s(n, n.priceCurrencyCode);\n return r.skus && r.skus.some(t) && (r.skus = r.skus.map((o) => t(o) ? s(o, n.priceCurrencyCode) : o)), r;\n };\n}\nexport {\n d 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(element: HTMLElement, { handler, defaultTemplate }: TemplateProps) {\n const templateContent = element.querySelector<HTMLScriptElement>(\"script[autocomplete-template]\")?.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 { fromMustacheTemplate, defaultMustacheTemplate } from \"./fromMustacheTemplate\"\n\n/**\n * Nosto Autocomplete Web Component using Mustache templates.\n *\n * This component integrates the Nosto Autocomplete functionality with Mustache templating.\n * It fetches the configuration from a script tag, compiles the Mustache 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 * <div id=\"results\"></div>\n * </form>\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-mustache-template\" autocomplete-template>\n * <h1>{{query}}</h1>\n * <ul>\n * {{#products}}\n * <li>{{name}}</li>\n *