UNPKG

@nosto/autocomplete

Version:

Library designed to simplify the implementation of search autocomplete functionality

1,042 lines (1,040 loc) 26 kB
"use strict"; var J = require("liquidjs"), z = `{% assign hasKeywords = response.keywords.hits.length > 0 %} {% assign hasProducts = response.products.hits.length > 0 %} {% assign hasHistory = history.length > 0 %} {% assign imagePlaceHolder = 'https://cdn.nosto.com/nosto/9/mock' %} <div class="ns-autocomplete-results"> {% if hasKeywords == false and hasProducts == false and hasHistory %} <div class="ns-autocomplete-history"> <div class="ns-autocomplete-header"> Recently searched </div> {% for hit in history %} <div class="ns-autocomplete-history-item" data-ns-hit="{{ hit | json | escape }}" data-testid="history"> {{ hit.item }} <a href="#" class="ns-autocomplete-history-item-remove" data-ns-remove-history="{{hit.item}}"> &#x2715; </a> </div> {% endfor %} </div> <div class="ns-autocomplete-history-clear"> <button type="button" class="ns-autocomplete-button" data-ns-remove-history="all"> Clear history </button> </div> {% elsif hasKeywords or hasProducts %} {% if hasKeywords %} <div class="ns-autocomplete-keywords"> <div class="ns-autocomplete-header"> Keywords </div> {% for hit in response.keywords.hits %} <div class="ns-autocomplete-keyword" data-ns-hit="{{ hit | json | escape }}" data-testid="keyword"> {% if hit._highlight and hit._highlight.keyword %} <span>{{ hit._highlight.keyword }}</span> {% else %} <span>{{ hit.keyword }}</span> {% endif %} </div> {% endfor %} </div> {% endif %} {% if hasProducts %} <div class="ns-autocomplete-products"> <div class="ns-autocomplete-header"> Products </div> {% for hit in response.products.hits %} <a class="ns-autocomplete-product" href="{{ hit.url }}" data-ns-hit="{{ hit | json | escape }}" data-testid="product"> <img class="ns-autocomplete-product-image" src="{% if hit.imageUrl %}{{ hit.imageUrl }}{% else %}{{ imagePlaceHolder }}{% endif %}" alt="{{ hit.name }}" width="60" height="40" /> <div class="ns-autocomplete-product-info"> {% if hit.brand %} <div class="ns-autocomplete-product-brand"> {{ hit.brand }} </div> {% endif %} <div class="ns-autocomplete-product-name"> {{ hit.name }} </div> <div> <span> {{ hit.price }}&euro; </span> {% if hit.listPrice and hit.listPrice != hit.price %} <span class="ns-autocomplete-product-list-price"> {{ hit.listPrice }} &euro; </span> {% endif %} </div> </div> </a> {% endfor %} </div> {% endif %} <div class="ns-autocomplete-submit"> <button type="submit" class="ns-autocomplete-button"> See all search results </button> </div> {% endif %} </div> `; function G(e) { const t = J.Liquid ? new J.Liquid() : void 0; if (t === void 0) throw new Error( "Liquid is not defined. Please include the Liquid library in your page." ); const n = t.parse(e); return async (o, r) => { const i = await t.render(n, r); o.innerHTML = i; }; } function K() { window.nostojs = window.nostojs ?? function(e) { (window.nostojs.q = window.nostojs.q ?? []).push(e); }; } async function T(e) { return window.nostojs(e); } typeof window < "u" && (K(), T((e) => { e.internal.getSettings(); })), typeof window < "u" && K(); function fe(e) { T((t) => t.recordSearchClick("autocomplete", e)); } function pe(e) { T((t) => t.recordSearchSubmit(e)); } function R() { window.nostojs = window.nostojs ?? function(e) { (window.nostojs.q = window.nostojs.q ?? []).push(e); }; } async function C(e) { return window.nostojs(e); } typeof window < "u" && (R(), C((e) => { e.internal.getSettings(); })), typeof window < "u" && R(); const B = (e) => String(e) === "[object Object]"; function Q(e) { if (!B(e)) return !1; const t = e.constructor; if (t === void 0) return !0; const n = t.prototype; return !(!B(n) || !n.hasOwnProperty("isPrototypeOf")); } function L(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, o) => L(n, t[o])); if (Q(e) && Q(t)) { const n = Object.entries(e); return n.length !== Object.keys(t).length ? !1 : n.every(([o, r]) => L(r, t[o])); } return !1; } function E(e) { return (...t) => { C((n) => { (n.internal.context.mode.isPreview() ? console[e] : n.internal.logger[e])(...t); }); }; } const g = { debug: E("debug"), info: E("info"), warn: E("warn"), error: E("error") }; async function ye(e, { hitDecorators: t, ...n }, o) { var r, i; const a = await o(e, n); if (!((i = (r = a.products) == null ? void 0 : r.hits) != null && i.length) || !(t != null && t.length)) return a; const c = (f) => t.reduce((l, s) => s(l), f); return { ...a, products: { ...a.products, hits: a.products.hits.map(c) } }; } function me(e, t, n) { const o = JSON.stringify(t); try { n.setItem(e, o); } catch (r) { g.warn(r); } } function ge(e, t) { try { const n = t.getItem(e); if (n) return JSON.parse(n); } catch (n) { g.warn(n); } } function he(e, t) { me(e, t, sessionStorage); } function we(e) { return ge(e, sessionStorage); } const V = "nosto:search-js:cache", ve = 60 * 1e3; function Se(e, t) { he(V, { query: e, result: t, created: Date.now() }); } function be(e) { const t = we(V); if (!t || !ke(t)) return null; const n = W(t.query); return !L(W(e), n) || Date.now() - t.created > ve ? null : t.result; } function W(e) { const t = { ...e, time: void 0, products: { ...e.products, size: void 0 } }; return JSON.parse(JSON.stringify(t)); } function ke(e) { return typeof e == "object" && e !== null && "query" in e && "result" in e && "created" in e; } async function Ae(e, { usePersistentCache: t, ...n }, o) { if (!t) return o(e, n); const r = await Pe(e, n, o); return Se(e, r), r; } async function Pe(e, t, n) { var o, r, i, a, c, f; const { from: l = 0, size: s = 0 } = e.products || {}, u = be(e); if (!u) return await n(e, t); const p = ((o = u?.products) == null ? void 0 : o.size) ?? 0, w = ((r = u?.products) == null ? void 0 : r.hits) ?? []; if (s === p) return u; if (s < p) return { ...u, products: { ...u.products, size: s, hits: w.slice(0, s), total: ((i = u.products) == null ? void 0 : i.total) || 0 } }; const A = s - w.length, v = l > 0 ? l + 1 : s - A, S = { ...e, products: { ...e.products, from: v, size: A } }, b = await n(S, t); return { ...u, products: { ...u.products, size: s, hits: [...((a = u.products) == null ? void 0 : a.hits) || [], ...((c = b.products) == null ? void 0 : c.hits) || []], total: ((f = b.products) == null ? void 0 : f.total) || 0 } }; } const Ce = 3e4, N = /* @__PURE__ */ new Map(); function Le(e, t) { const n = N.get(e); if (!n) return; const o = Date.now() - n.created > Ce, r = L(t, n.query); if (o || !r) { N.delete(e); return; } return n.result; } function Ee(e, t, n) { N.set(e, { query: t, result: n, created: Date.now() }); } async function qe(e, t, n) { if (!t.useMemoryCache) return n(e, t); const o = JSON.stringify(e), r = Le(o, e); if (r) return r; const i = await n(e, t); return Ee(o, e, i), i; } function De(e) { return new Promise((t) => setTimeout(t, e)); } async function Ie(e, { maxRetries: t = 0, retryInterval: n = 0, ...o }, r) { let i = 0; for (; ; ) try { return await r(e, o); } catch (a) { if (i >= t) throw a; if (!He(a)) throw g.info("Skipping retry logic for", a), a; i++, await De(n); } } function He(e) { return !e || typeof e != "object" ? !1 : !("status" in e) || Te(e.status); } function Te(e) { return typeof e == "number" && (e < 400 || e >= 500); } async function Ne(e, t = {}) { const n = await new Promise(C); return je(n.search, Ie, qe, Ae, ye)(e, t); } function je(e, ...t) { return t.reduce((n, o) => (r, i) => o(r, i, n), e); } const Oe = [ "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 j(e, t = {}) { const n = e.products?.fields ?? Oe, o = e.products?.facets ?? ["*"], r = e.products?.size ?? 20, i = e.products?.from ?? 0, a = await Ne( { ...e, products: { ...e.products, fields: n, facets: o, size: r, from: i } }, t ); return { query: e, response: a }; } const O = { serpPath: "/search", queryParamName: "query", enabled: !0 }; function Z() { return { minQueryLength: 2, historyEnabled: !0, historySize: 5, hitDecorators: [], nostoAnalytics: !0, googleAnalytics: O, routingHandler: (e) => { location.href = e; }, nativeSubmit: !1, submit: (e, t, n) => { e.length >= (t.minQueryLength ?? Z().minQueryLength) && j( { query: e }, { redirect: !0, track: t.nostoAnalytics ? "serp" : void 0, hitDecorators: t.hitDecorators, ...n } ); } }; } function $e(e) { e.setAttribute("autocomplete", "off"); } function Ue(e, { onClick: t, onFocus: n, onInput: o, onKeyDown: r, onSubmit: i }, { form: a = e.form ?? void 0, nativeSubmit: c } = {}) { const f = []; function l(s, u, p) { s.addEventListener(u, p), f.push(() => s.removeEventListener(u, p)); } return (r || i) && l(e, "keydown", (s) => { r?.(e.value, s.key), r && (s.key === "ArrowDown" || s.key === "ArrowUp") ? s.preventDefault() : i && s.key === "Enter" && (e.value !== "" && !s.repeat && i(e.value), c || s.preventDefault()); }), i && a && (l(a, "submit", (s) => { c || s.preventDefault(), i(e.value); }), a.querySelectorAll("[type=submit]").forEach((s) => { l(s, "click", (u) => { c || u.preventDefault(), i(e.value); }); })), t && l(e, "click", () => t(e.value)), n && l(e, "focus", () => n(e.value)), o && l(e, "input", () => o(e.value)), { destroy() { f.forEach((s) => s()); } }; } function _e(e, t, n, o, r, i, a) { let c = [], f = [], l = !0, s = -1; function u(d) { const m = d?.dataset?.nsHit; if (m) { const y = $(m); if (k(), y?.item) { o(y.item); return; } if (y?.keyword) { o(y.keyword, { redirect: !!y?._redirect, isKeyword: !0 }), y?._redirect && i(y._redirect); return; } y?.url && i(y.url); } } function p() { l = !e.innerHTML.trim(), l || (c = Array.from( e.querySelectorAll("[data-ns-hit]") ).map((d) => (A(d), d))); } function w() { Object.entries(a ?? {}).map(([d, m]) => { const y = `[data-ns-${d.replace(/([A-Z])/g, "-$1").toLowerCase()}]`; Array.from(e.querySelectorAll(y)).map((h) => { const de = h?.dataset?.[`ns${d.charAt(0).toUpperCase() + d.slice(1)}`], F = () => { m({ data: de, el: h, update: b }); }; h.addEventListener("click", F), f.push(() => { h.removeEventListener("click", F); }); }); }); } function A(d) { const m = () => { u(d); }; d.addEventListener("click", m), f.push(() => { d.removeEventListener("click", m); }); } function v(d, m) { if (typeof m == "number" && c[m] && c[m].classList.remove("selected"), typeof d == "number" && c[d]) { c[d]?.classList.add("selected"); const y = c[d]?.dataset?.nsHit; if (y) { const h = $(y); if (h.item) { r(h.item); return; } if (h.keyword) { r(h.keyword); return; } } } } function S() { H(), c = [], f.forEach((d) => d()), f = []; } async function b(d) { S(), await n(e, d), setTimeout(() => { p(), w(), M(); }, 0); } function k() { H(), e.style.display = "none"; } function M() { l ? k() : e.style.display = ""; } function re() { S(), l = !0, k(); } function x() { return e.style.display !== "none"; } function ie() { let d = s; s === c.length - 1 ? s = 0 : d = s++, v(s, d); } function se() { if (P()) { let d = s; s === 0 ? s = c.length - 1 : d = s--, v(s, d); } else s = c.length - 1, v(s); } function ce() { x() && P() && c[s] && u(c[s]); } function P() { return s > -1; } function ae() { return c[s]; } function H() { P() && (c[s]?.classList.remove("selected"), s = -1); } function ue() { S(), l = !0, e.innerHTML = ""; } async function le() { const d = await Promise.resolve(t); await Promise.resolve(n(e, d)), setTimeout(() => { p(), w(), k(); }, 0); } return le(), { update: b, clear: re, isOpen: x, goDown: ie, goUp: se, handleSubmit: ce, destroy: ue, show: M, hide: k, resetHighlight: H, hasHighlight: P, getHighlight: ae, container: e }; } function $(e) { try { return JSON.parse(e) ?? {}; } catch (t) { return g.warn("Could not parse hit", t), {}; } } class X extends Error { } function Me(e) { let t = !1; return { promise: new Promise((n, o) => e.then( (r) => t ? o(new X("cancelled promise")) : n(r), (r) => o(t ? new X("cancelled promise") : r) )), cancel() { t = !0; } }; } function xe({ config: e, history: t, input: n }) { let o; const r = (s, u, p) => typeof u.fetch == "function" ? u.fetch(s) : j( { query: s, ...u.fetch }, { track: u.nostoAnalytics ? "autocomplete" : void 0, redirect: !1, hitDecorators: u.hitDecorators, ...p } ); function i(s) { return Promise.resolve({ query: { query: s }, history: t?.getItems() }); } function a(s, u) { return o?.cancel(), s && s.length >= e.minQueryLength ? (o = Me(r(s, e, u)), o.promise) : t ? i(s ?? "") : ( // @ts-expect-error type mismatch o?.promise ?? Promise.resolve({}) ); } function c(s) { return t && t.add(s), i(n.value); } function f(s) { return t && t.remove(s), i(n.value); } function l() { return t && t.clear(), i(n.value); } return { updateState: a, addHistoryItem: c, removeHistoryItem: f, clearHistory: l }; } function q(e, t) { return (typeof e == "string" ? Array.from(document.querySelectorAll(e)) : [e]).filter( (n) => t ? n instanceof t : !0 ); } function Y(e, t) { let n = []; return q(e).forEach((o) => { const r = o.parentNode; r !== document && r instanceof Element && (n.push(r), n = n.concat(Y(r))); }), n.filter( (o) => t === void 0 ); } function Fe([e, t], n) { const o = (r) => { const i = r.target; i instanceof HTMLElement && e && i !== e && i !== t && !Y(i).includes(e) && n(); }; return document.addEventListener("click", o), { destroy: () => { document.removeEventListener("click", o); } }; } const U = "nosto:autocomplete:gaEvent"; function D(e) { const { delay: t = !1, title: n = document.title, location: o = window.location.href } = e || {}; if (t) Je(n, o); else { if ("gtag" in window && typeof window.gtag == "function") { const r = "google_tag_manager" in window && typeof window.google_tag_manager == "object" ? Object.keys(window.google_tag_manager || []).filter((i) => i.substring(0, 2) == "G-") : []; if (r.length > 1) for (let i = 0; i < r.length; i++) window.gtag("event", "page_view", { page_title: n, page_location: o, send_to: r[i] }); else window.gtag("event", "page_view", { page_title: n, page_location: o }); } if ("ga" in window && typeof window.ga == "function" && "getAll" in window.ga && typeof window.ga.getAll == "function") try { const r = new URL(o), i = window.ga.getAll(); i?.length > 0 && i[0]?.send("pageview", r.pathname + r.search); } catch (r) { g.warn("Could not send pageview to GA", r); } } } const I = (e) => typeof e.googleAnalytics == "boolean" ? e.googleAnalytics : typeof e.googleAnalytics == "object" && e.googleAnalytics.enabled, _ = (e, t) => { const n = I(t) ? typeof t.googleAnalytics == "boolean" ? O : { ...O, ...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 (o) { g.warn("Could not create track url", o); return; } }; function Je(e, t) { localStorage.setItem(U, JSON.stringify({ title: e, location: t })); } function ze() { const e = localStorage.getItem(U); if (typeof e == "string") { localStorage.removeItem(U); try { const t = JSON.parse(e); D(t); } catch (t) { g.warn("Could not consume pageView", t); } } } function Ge(e) { let t, n = !1; return (o) => { n ? (t && clearTimeout(t), t = setTimeout(async () => { await o(), n = !1; }, e)) : (o(), n = !0); }; } function Ke(e) { const t = "nosto:autocomplete:history"; let n = o(); function o() { try { return JSON.parse(localStorage.getItem(t) ?? "[]") ?? []; } catch (l) { return g.error("Could not get history items.", l), []; } } function r(l) { try { localStorage.setItem(t, JSON.stringify(l)); } catch (s) { g.error("Could not set history items.", s); } } function i(l) { r( n = [{ item: l }, ...n?.filter((s) => s.item !== l) || []].slice( 0, e ) ); } function a() { r(n = []); } function c(l) { r(n = n.filter((s) => s.item !== l)); } function f() { return n; } return { add: i, clear: a, remove: c, getItems: f }; } function ee(e) { const t = { ...Z(), ...e }, n = t.historyEnabled ? Ke(t.historySize) : void 0, o = Ge(300); I(e) && setTimeout(ze, 1e3); const r = q(e.inputSelector, HTMLInputElement).map( (i) => { const a = xe({ config: t, history: n, input: i }), c = Re({ input: i, config: t, actions: a }); if (!c) return; $e(i); const f = Ue(i, { onInput: async (s) => { o(async () => { const u = await a.updateState(s); c.update(u); }); }, onClick() { c.resetHighlight(); }, onFocus() { c.show(); }, onSubmit() { if (c.isOpen() && c.hasHighlight()) { const s = c.getHighlight()?.dataset?.nsHit; s && te({ config: t, data: s, query: i.value }), c.handleSubmit(); } else ne({ actions: a, config: t })(i.value); }, onKeyDown(s, u) { u === "Escape" ? c.hide() : u === "ArrowDown" ? c.isOpen() ? c.goDown() : c.show() : u === "ArrowUp" && c.isOpen() && c.goUp(); } }, { nativeSubmit: t.nativeSubmit }), l = Fe( [c.container, i], () => { c.hide(); } ); return { open() { c.show(); }, close() { c.hide(); }, destroy() { f.destroy(), l.destroy(), c.destroy(); } }; } ); return { destroy() { r.forEach((i) => i?.destroy()); }, open() { r.forEach((i) => i?.open()); }, close() { r.forEach((i) => i?.close()); } }; } function Re({ input: e, config: t, actions: n }) { const o = typeof t.dropdownSelector == "function" ? q(t.dropdownSelector(e), HTMLElement) : q(t.dropdownSelector, HTMLElement); if (o.length === 0) { g.error(`No dropdown element found for input ${e}`); return; } else o.length > 1 && g.error( `Multiple dropdown elements found for input ${e}, using the first element` ); const r = o[0]; return _e( r, n.updateState(e.value), t.render, ne({ actions: n, config: t }), (i) => e.value = i, t.routingHandler, { removeHistory: async function({ data: i, update: a }) { if (i === "all") { const c = await n.clearHistory(); return a(c); } else if (i) { const c = await n.removeHistoryItem(i); return a(c); } }, hit: function({ data: i }) { i && te({ config: t, data: i, query: e.value }); } } ); } async function te({ config: e, data: t, query: n }) { if (!e.googleAnalytics && !e.nostoAnalytics) return; const o = $(t); e.nostoAnalytics && o && fe(o), I(e) && (o._redirect && D({ delay: !0, location: _(o.keyword, e) }), o.url && D({ delay: !0, location: _(n, e) })); } function ne(e) { return async (t, n) => { const { config: o, actions: r } = e, { redirect: i = !1 } = n ?? {}; t.length > 0 && (o.historyEnabled && r.addHistoryItem(t), o.nostoAnalytics && pe(t), I(o) && D({ delay: !0, location: _(t, o) }), !i && typeof o?.submit == "function" && o.submit(t, o, n)); }; } const Be = { 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 = { ...Be, ...e }; e.currencySettings || C((o) => { t.currencySettings = o.internal.getSettings().currencySettings ?? {}; }); function n(o, r) { const { defaultCurrency: i, currencySettings: a, defaultLocale: c } = t, f = r ?? i, l = Qe[f] ?? c; if (f in a) { const s = a[f], u = new Intl.NumberFormat(l, { useGrouping: !!s.groupingSeparator, minimumFractionDigits: s.decimalPlaces, maximumFractionDigits: s.decimalPlaces }).formatToParts(o).map((p) => p.type === "group" ? s.groupingSeparator : p.type === "decimal" ? s.decimalCharacter : p.value).join(""); return s != null && s.currencyBeforeAmount ? `${s.currencyToken}${u}` : `${u}${s?.currencyToken}`; } return new Intl.NumberFormat(l, { style: "currency", currency: f }).format(o); } return { formatCurrency: n }; } function We(e) { const { formatCurrency: t } = Ve(e); function n(r, i) { const a = {}; return r.price !== void 0 && (a.priceText = t(r.price, i)), r.listPrice !== void 0 && (a.listPriceText = t(r.listPrice, i)), Object.assign({}, r, a); } function o(r) { return r.price !== void 0 || r.listPrice !== void 0; } return function(r) { if (!o(r)) return r; const i = n(r, r.priceCurrencyCode); return i.skus && i.skus.some(o) && (i.skus = i.skus.map((a) => o(a) ? n(a, r.priceCurrencyCode) : a)), i; }; } async function Ze(e, { handler: t, defaultTemplate: n }) { const o = e.querySelector( "script[autocomplete-template]" )?.textContent, r = Xe(e); if (!Object.keys(r).length) throw new Error("NostoAutocomplete: Missing required config."); return ee({ ...r, nativeSubmit: !0, render: t(o ?? n), submit(i) { e.querySelector(r.inputSelector).value = i, e.querySelector("form")?.submit(); } }); } function Xe(e) { const t = e.querySelector("script[autocomplete-config]"); return t ? JSON.parse(t.textContent) : {}; } class oe extends HTMLElement { async connectedCallback() { return Ze(this, { handler: G, defaultTemplate: z }); } } customElements.define("nosto-autocomplete", oe), exports.NostoAutocomplete = oe, exports.autocomplete = ee, exports.defaultLiquidTemplate = z, exports.fromLiquidTemplate = G, exports.priceDecorator = We, exports.search = j; //# sourceMappingURL=autocomplete.cjs.map