UNPKG

@nosto/autocomplete

Version:

Library designed to simplify the implementation of search autocomplete functionality

937 lines (936 loc) 21.9 kB
function F() { window.nostojs = window.nostojs ?? function(e) { (window.nostojs.q = window.nostojs.q ?? []).push(e); }; } async function N(e) { return window.nostojs(e); } typeof window < "u" && (F(), N((e) => { e.internal.getSettings(); })), typeof window < "u" && F(); function le(e) { N((t) => t.recordSearchClick("autocomplete", e)); } function fe(e) { N((t) => t.recordSearchSubmit(e)); } function J() { window.nostojs = window.nostojs ?? function(e) { (window.nostojs.q = window.nostojs.q ?? []).push(e); }; } async function S(e) { return window.nostojs(e); } typeof window < "u" && (J(), S((e) => { e.internal.getSettings(); })), typeof window < "u" && J(); function R(e) { return String(e) === "[object Object]"; } function G(e) { if (!R(e)) return !1; const t = e.constructor; if (t === void 0) return !0; const n = t.prototype; return !(!R(n) || !n.hasOwnProperty("isPrototypeOf")); } function k(e, t) { if (e === t) return !0; if (e instanceof Date && t instanceof Date) return e.getTime() === t.getTime(); if (Array.isArray(e) && Array.isArray(t)) return e.length !== t.length ? !1 : e.every((n, r) => k(n, t[r])); if (G(e) && G(t)) { const n = Object.entries(e); return n.length !== Object.keys(t).length ? !1 : n.every(([r, i]) => k(i, t[r])); } return !1; } function B() { const e = /* @__PURE__ */ new Map(); return { getItem(t) { return e.get(t) ?? null; }, setItem(t, n) { e.set(t, n); }, removeItem(t) { e.delete(t); }, clear() { e.clear(); }, key(t) { return Array.from(e.keys())[t] ?? null; }, get length() { return e.size; } }; } function b(e) { return (...t) => { S((n) => { (n.internal.context.mode.isPreview() ? console[e] : n.internal.logger[e])(...t); }); }; } const g = { debug: b("debug"), info: b("info"), warn: b("warn"), error: b("error") }; function de(e, t, n) { try { const r = JSON.stringify(t); n.setItem(e, r); } catch (r) { g.warn(r); } } function ye(e, t) { try { const n = t.getItem(e); if (n) return JSON.parse(n); } catch (n) { g.warn(n); } } let K, A; try { K = window.localStorage, A = window.sessionStorage; } catch (e) { g.warn(e), K = B(), A = B(); } function ge(e, t) { de(e, t, A); } function pe(e) { return ye(e, A); } async function me(e, { hitDecorators: t, ...n }, r) { const i = await r(e, n); if (!i.products?.hits?.length || !t?.length) return i; const o = (a) => t.reduce((s, d) => d(s), a); return { ...i, products: { ...i.products, hits: i.products.hits.map(o) } }; } const Q = "nosto:search-js:cache", we = 60 * 1e3; function he(e, t) { ge(Q, { query: e, result: t, created: Date.now() }); } function ve(e) { const t = pe(Q); if (!t || !Se(t)) return null; const n = V(t.query); return !k(V(e), n) || Date.now() - t.created > we ? null : t.result; } function V(e) { const t = { ...e, time: void 0, products: { ...e.products, size: void 0 } }; return JSON.parse(JSON.stringify(t)); } function Se(e) { return typeof e == "object" && e !== null && "query" in e && "result" in e && "created" in e; } async function ke(e, { usePersistentCache: t, ...n }, r) { if (!t) return r(e, n); const i = await be(e, n, r); return he(e, i), i; } async function be(e, t, n) { const { from: r = 0, size: i = 0 } = e.products || {}, o = ve(e); if (!o) return await n(e, t); const a = o?.products?.size ?? 0, s = o?.products?.hits ?? []; if (i === a) return o; if (i < a) return { ...o, products: { ...o.products, size: i, hits: s.slice(0, i), total: o.products?.total || 0 } }; const d = i - s.length, u = r > 0 ? r + 1 : i - d, c = { ...e, products: { ...e.products, from: u, size: d } }, l = await n(c, t); return { ...o, products: { ...o.products, size: i, hits: [...o.products?.hits || [], ...l.products?.hits || []], total: l.products?.total || 0 } }; } const Ae = 3e4, $ = /* @__PURE__ */ new Map(); function Ce(e, t) { const n = $.get(e); if (!n) return; const r = Date.now() - n.created > Ae, i = k(t, n.query); if (r || !i) { $.delete(e); return; } return n.result; } function Ie(e, t, n) { $.set(e, { query: t, result: n, created: Date.now() }); } async function De(e, t, n) { if (!t.useMemoryCache) return n(e, t); const r = JSON.stringify(e), i = Ce(r, e); if (i) return i; const o = await n(e, t); return Ie(r, e, o), o; } async function Ee(e, t, n) { if (!t.redirect) return n(e, t); const r = await n(e, t); if (r.keywords?.hits?.length) { const i = r.keywords.hits.find((o) => o.keyword === e.query && o._redirect); i && i._redirect && (window.location.href = i._redirect); } return r; } function Pe(e) { return new Promise((t) => setTimeout(t, e)); } async function Le(e, { maxRetries: t = 0, retryInterval: n = 0, ...r }, i) { let o = 0; for (; ; ) try { return await i(e, r); } catch (a) { if (o >= t) throw a; if (!Ne(a)) throw g.info("Skipping retry logic for", a), a; o++, await Pe(n); } } function Ne(e) { return !e || typeof e != "object" ? !1 : !("status" in e) || $e(e.status); } function $e(e) { return typeof e == "number" && (e < 400 || e >= 500); } async function Oe(e, t = {}) { const n = await new Promise(S); return He( n.search, Le, Ee, De, ke, me )(e, t); } function He(e, ...t) { return t.reduce((n, r) => (i, o) => r(i, o, n), e); } const je = [ "productId", "url", "name", "imageUrl", "imageHash", "thumbUrl", "description", "brand", "variantId", "availability", "price", "priceText", "categoryIds", "categories", "customFields.key", "customFields.value", "priceCurrencyCode", "datePublished", "listPrice", "unitPricingBaseMeasure", "unitPricingUnit", "unitPricingMeasure", "googleCategory", "gtin", "ageGroup", "gender", "condition", "alternateImageUrls", "ratingValue", "reviewCount", "inventoryLevel", "skus.id", "skus.name", "skus.price", "skus.listPrice", "skus.priceText", "skus.url", "skus.imageUrl", "skus.inventoryLevel", "skus.customFields.key", "skus.customFields.value", "skus.availability", "pid", "onDiscount", "extra.key", "extra.value", "saleable", "available", "tags1", "tags2", "tags3" ]; async function O(e, t = {}) { const n = e.products?.fields ?? je, r = e.products?.facets ?? ["*"], i = e.products?.size ?? 20, o = e.products?.from ?? 0, a = await Oe( { ...e, products: { ...e.products, fields: n, facets: r, size: i, from: o } }, t ); return { query: e, response: a }; } const H = { serpPath: "/search", queryParamName: "query", enabled: !0 }; function W() { return { minQueryLength: 2, historyEnabled: !0, historySize: 5, hitDecorators: [], nostoAnalytics: !0, googleAnalytics: H, routingHandler: (e) => { location.href = e; }, nativeSubmit: !1, submit: (e, t, n) => { e.length >= (t.minQueryLength ?? W().minQueryLength) && O( { query: e }, { redirect: !0, track: t.nostoAnalytics ? "serp" : void 0, hitDecorators: t.hitDecorators, ...n } ); } }; } function qe(e, { onClick: t, onFocus: n, onInput: r, onKeyDown: i, onSubmit: o }, { form: a = e.form ?? void 0, nativeSubmit: s } = {}) { const d = []; function u(c, l, p) { c.addEventListener(l, p), d.push(() => c.removeEventListener(l, p)); } return (i || o) && u(e, "keydown", (c) => { const l = i?.(e.value, c.key); i && (c.key === "ArrowDown" || c.key === "ArrowUp") ? c.preventDefault() : o && c.key === "Enter" && (e.value !== "" && !c.repeat && !l && o(e.value), s || c.preventDefault()); }), o && a && (u(a, "submit", (c) => { s || c.preventDefault(), o(e.value); }), a.querySelectorAll("[type=submit]").forEach((c) => { u(c, "click", (l) => { s || l.preventDefault(), o(e.value); }); })), t && u(e, "click", () => t(e.value)), n && u(e, "focus", () => n(e.value)), r && u(e, "input", () => r(e.value)), { destroy() { d.forEach((c) => c()); } }; } function Te(e) { e.setAttribute("autocomplete", "off"); } function Ue(e, t, n, r, i, o, a) { let s = [], d = [], u = !0, c = -1; function l(f) { const m = f?.dataset?.nsHit; if (m) { const y = j(m); if (h(), y?.item) { r(y.item); return; } if (y?.keyword) { r(y.keyword, { redirect: !!y?._redirect, isKeyword: !0 }), y?._redirect && o(y._redirect); return; } y?.url && o(y.url); } } function p() { u = !e.innerHTML.trim(), u || (s = Array.from(e.querySelectorAll("[data-ns-hit]")).map((f) => (te(f), f))); } function U() { Object.entries(a ?? {}).map(([f, m]) => { const y = `[data-ns-${f.replace(/([A-Z])/g, "-$1").toLowerCase()}]`; Array.from(e.querySelectorAll(y)).map((w) => { const ue = w?.dataset?.[`ns${f.charAt(0).toUpperCase() + f.slice(1)}`], z = () => { m({ data: ue, el: w, update: _ }); }; w.addEventListener("click", z), d.push(() => { w.removeEventListener("click", z); }); }); }); } function te(f) { const m = () => { l(f); }; f.addEventListener("click", m), d.push(() => { f.removeEventListener("click", m); }); } function E(f, m) { if (typeof m == "number" && s[m] && s[m].classList.remove("selected"), typeof f == "number" && s[f]) { s[f]?.classList.add("selected"); const y = s[f]?.dataset?.nsHit; if (y) { const w = j(y); if (w.item) { i(w.item); return; } if (w.keyword) { i(w.keyword); return; } } } } function P() { L(), s = [], d.forEach((f) => f()), d = []; } async function _(f) { P(), await n(e, f), setTimeout(() => { p(), U(), M(); }, 0); } function h() { L(), e.style.display = "none"; } function M() { u ? h() : e.style.display = ""; } function ne() { P(), u = !0, h(); } function x() { return e.style.display !== "none"; } function oe() { let f = c; c === s.length - 1 ? c = 0 : f = c++, E(c, f); } function re() { if (v()) { let f = c; c === 0 ? c = s.length - 1 : f = c--, E(c, f); } else c = s.length - 1, E(c); } function ie() { x() && v() && s[c] && l(s[c]); } function v() { return c > -1; } function ce() { return s[c]; } function L() { v() && (s[c]?.classList.remove("selected"), c = -1); } function se() { P(), u = !0, e.innerHTML = ""; } async function ae() { const f = await Promise.resolve(t); await Promise.resolve(n(e, f)), setTimeout(() => { p(), U(), h(); }, 0); } return ae(), { update: _, clear: ne, isOpen: x, goDown: oe, goUp: re, handleSubmit: ie, destroy: se, show: M, hide: h, resetHighlight: L, hasHighlight: v, getHighlight: ce, container: e }; } function j(e) { try { return JSON.parse(e) ?? {}; } catch (t) { return g.warn("Could not parse hit", t), {}; } } class Z extends Error { } function _e(e) { let t = !1; return { promise: new Promise((n, r) => e.then( (i) => t ? r(new Z("cancelled promise")) : n(i), (i) => r(t ? new Z("cancelled promise") : i) )), cancel() { t = !0; } }; } function Me({ config: e, history: t, input: n }) { let r; const i = (c, l, p) => typeof l.fetch == "function" ? l.fetch(c) : O( { query: c, ...l.fetch }, { track: l.nostoAnalytics ? "autocomplete" : void 0, redirect: !1, hitDecorators: l.hitDecorators, ...p } ); function o(c) { return Promise.resolve({ query: { query: c }, history: t?.getItems() }); } function a(c, l) { return r?.cancel(), c && c.length >= e.minQueryLength ? (r = _e(i(c, e, l)), r.promise) : t ? o(c ?? "") : ( // @ts-expect-error type mismatch r?.promise ?? Promise.resolve({}) ); } function s(c) { return t && t.add(c), o(n.value); } function d(c) { return t && t.remove(c), o(n.value); } function u() { return t && t.clear(), o(n.value); } return { updateState: a, addHistoryItem: s, removeHistoryItem: d, clearHistory: u }; } function C(e, t) { return (typeof e == "string" ? Array.from(document.querySelectorAll(e)) : [e]).filter((n) => t ? n instanceof t : !0); } function X(e, t) { let n = []; return C(e).forEach((r) => { const i = r.parentNode; i !== document && i instanceof Element && (n.push(i), n = n.concat(X(i))); }), n.filter((r) => t === void 0); } function xe([e, t], n) { const r = (i) => { const o = i.target; o instanceof HTMLElement && e && o !== e && o !== t && !X(o).includes(e) && n(); }; return document.addEventListener("click", r), { destroy: () => { document.removeEventListener("click", r); } }; } const q = "nosto:autocomplete:gaEvent"; function I(e) { const { delay: t = !1, title: n = document.title, location: r = window.location.href } = e || {}; if (t) ze(n, r); else { if ("gtag" in window && typeof window.gtag == "function") { const i = "google_tag_manager" in window && typeof window.google_tag_manager == "object" ? Object.keys(window.google_tag_manager || []).filter((o) => o.substring(0, 2) == "G-") : []; if (i.length > 1) for (let o = 0; o < i.length; o++) window.gtag("event", "page_view", { page_title: n, page_location: r, send_to: i[o] }); else window.gtag("event", "page_view", { page_title: n, page_location: r }); } if ("ga" in window && typeof window.ga == "function" && "getAll" in window.ga && typeof window.ga.getAll == "function") try { const i = new URL(r), o = window.ga.getAll(); o?.length > 0 && o[0]?.send("pageview", i.pathname + i.search); } catch (i) { g.warn("Could not send pageview to GA", i); } } } const D = (e) => typeof e.googleAnalytics == "boolean" ? e.googleAnalytics : typeof e.googleAnalytics == "object" && e.googleAnalytics.enabled, T = (e, t) => { const n = D(t) ? typeof t.googleAnalytics == "boolean" ? H : { ...H, ...t.googleAnalytics } : void 0; if (e && n) try { return new URL( `${n?.serpPath || location.pathname}?${`${encodeURIComponent( n.queryParamName )}=${encodeURIComponent(e).replace(/%20/g, "+")}`}`, window.location.origin ).toString(); } catch (r) { g.warn("Could not create track url", r); return; } }; function ze(e, t) { localStorage.setItem(q, JSON.stringify({ title: e, location: t })); } function Fe() { const e = localStorage.getItem(q); if (typeof e == "string") { localStorage.removeItem(q); try { const t = JSON.parse(e); I(t); } catch (t) { g.warn("Could not consume pageView", t); } } } function Je(e) { let t, n = !1; return (r) => { n ? (t && clearTimeout(t), t = setTimeout(async () => { await r(), n = !1; }, e)) : (r(), n = !0); }; } function Re(e) { const t = "nosto:autocomplete:history"; let n = r(); function r() { try { return JSON.parse(localStorage.getItem(t) ?? "[]") ?? []; } catch (u) { return g.error("Could not get history items.", u), []; } } function i(u) { try { localStorage.setItem(t, JSON.stringify(u)); } catch (c) { g.error("Could not set history items.", c); } } function o(u) { i(n = [{ item: u }, ...n?.filter((c) => c.item !== u) || []].slice(0, e)); } function a() { i(n = []); } function s(u) { i(n = n.filter((c) => c.item !== u)); } function d() { return n; } return { add: o, clear: a, remove: s, getItems: d }; } function Ge(e) { const t = { ...W(), ...e }, n = t.historyEnabled ? Re(t.historySize) : void 0, r = Je(300); D(e) && setTimeout(Fe, 1e3); const i = C(e.inputSelector, HTMLInputElement).map((o) => { const a = Me({ config: t, history: n, input: o }), s = Be({ input: o, config: t, actions: a }); if (!s) return; Te(o); const d = qe( o, { onInput: async (c) => { r(async () => { const l = await a.updateState(c); s.update(l); }); }, onClick() { s.resetHighlight(); }, onFocus() { s.show(); }, onSubmit() { if (s.isOpen() && s.hasHighlight()) { const c = s.getHighlight()?.dataset?.nsHit; c && Y({ config: t, data: c, query: o.value }), s.handleSubmit(); } else ee({ actions: a, config: t })(o.value); }, onKeyDown(c, l) { l === "Escape" ? s.hide() : l === "ArrowDown" ? s.isOpen() ? s.goDown() : s.show() : l === "ArrowUp" && s.isOpen() && s.goUp(); } }, { nativeSubmit: t.nativeSubmit } ), u = xe([s.container, o], () => { s.hide(); }); return { open() { s.show(); }, close() { s.hide(); }, destroy() { d.destroy(), u.destroy(), s.destroy(); } }; }); return { destroy() { i.forEach((o) => o?.destroy()); }, open() { i.forEach((o) => o?.open()); }, close() { i.forEach((o) => o?.close()); } }; } function Be({ input: e, config: t, actions: n }) { const r = typeof t.dropdownSelector == "function" ? C(t.dropdownSelector(e), HTMLElement) : C(t.dropdownSelector, HTMLElement); if (r.length === 0) { g.error(`No dropdown element found for input ${e}`); return; } else r.length > 1 && g.error(`Multiple dropdown elements found for input ${e}, using the first element`); const i = r[0]; return Ue( i, n.updateState(e.value), t.render, ee({ actions: n, config: t }), (o) => e.value = o, t.routingHandler, { removeHistory: async function({ data: o, update: a }) { if (o === "all") { const s = await n.clearHistory(); return a(s); } else if (o) { const s = await n.removeHistoryItem(o); return a(s); } }, hit: function({ data: o }) { o && Y({ config: t, data: o, query: e.value }); } } ); } async function Y({ config: e, data: t, query: n }) { if (!e.googleAnalytics && !e.nostoAnalytics) return; const r = j(t); e.nostoAnalytics && r && le(r), D(e) && (r._redirect && I({ delay: !0, location: T(r.keyword, e) }), r.url && I({ delay: !0, location: T(n, e) })); } function ee(e) { return async (t, n) => { const { config: r, actions: i } = e, { redirect: o = !1 } = n ?? {}; t.length > 0 && (r.historyEnabled && i.addHistoryItem(t), r.nostoAnalytics && fe(t), D(r) && I({ delay: !0, location: T(t, r) }), !o && typeof r?.submit == "function" && r.submit(t, r, n)); }; } const Ke = { defaultCurrency: "EUR", defaultLocale: "en-US", /** @hidden */ currencySettings: {} }, Qe = { EUR: "de-DE", GBP: "en-GB", USD: "en-US", AUD: "en-AU", CAD: "en-CA", //India, Afghanistan, Bangladesh, Bhutan, Myanmar, Nepal, and Pakistan uses lakhs and crores notation INR: "en-IN", AFN: "en-IN", BDT: "en-IN", BTN: "en-IN", MMK: "en-IN", NPR: "en-IN", PKR: "en-IN" }; function Ve(e = {}) { const t = { ...Ke, ...e }; e.currencySettings || S((r) => { t.currencySettings = r.internal.getSettings().currencySettings ?? {}; }); function n(r, i) { const { defaultCurrency: o, currencySettings: a, defaultLocale: s } = t, d = i ?? o, u = Qe[d] ?? s; if (d in a) { const c = a[d], l = new Intl.NumberFormat(u, { useGrouping: !!c.groupingSeparator, minimumFractionDigits: c.decimalPlaces, maximumFractionDigits: c.decimalPlaces }).formatToParts(r).map((p) => p.type === "group" ? c.groupingSeparator : p.type === "decimal" ? c.decimalCharacter : p.value).join(""); return c?.currencyBeforeAmount ? `${c.currencyToken}${l}` : `${l}${c?.currencyToken}`; } return new Intl.NumberFormat(u, { style: "currency", currency: d }).format(r); } return { formatCurrency: n }; } function We(e) { const { formatCurrency: t } = Ve(e); function n(i, o) { const a = {}; return i.price !== void 0 && (a.priceText = t(i.price, o)), i.listPrice !== void 0 && (a.listPriceText = t(i.listPrice, o)), Object.assign({}, i, a); } function r(i) { return i.price !== void 0 || i.listPrice !== void 0; } return function(i) { if (!r(i)) return i; const o = n(i, i.priceCurrencyCode); return o.skus && o.skus.some(r) && (o.skus = o.skus.map((a) => r(a) ? n(a, i.priceCurrencyCode) : a)), o; }; } export { Ge as autocomplete, We as priceDecorator, O as search }; //# sourceMappingURL=autocomplete.mjs.map