@nosto/autocomplete
Version:
Library designed to simplify the implementation of search autocomplete functionality
1 lines • 110 kB
Source Map (JSON)
{"version":3,"file":"autocomplete.bundle.mjs","sources":["../../node_modules/mustache/mustache.mjs","../../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-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/mustache/NostoAutocomplete.ts"],"sourcesContent":["/*!\n * mustache.js - Logic-less {{mustache}} templates with JavaScript\n * http://github.com/janl/mustache.js\n */\n\nvar objectToString = Object.prototype.toString;\nvar isArray = Array.isArray || function isArrayPolyfill (object) {\n return objectToString.call(object) === '[object Array]';\n};\n\nfunction isFunction (object) {\n return typeof object === 'function';\n}\n\n/**\n * More correct typeof string handling array\n * which normally returns typeof 'object'\n */\nfunction typeStr (obj) {\n return isArray(obj) ? 'array' : typeof obj;\n}\n\nfunction escapeRegExp (string) {\n return string.replace(/[\\-\\[\\]{}()*+?.,\\\\\\^$|#\\s]/g, '\\\\$&');\n}\n\n/**\n * Null safe way of checking whether or not an object,\n * including its prototype, has a given property\n */\nfunction hasProperty (obj, propName) {\n return obj != null && typeof obj === 'object' && (propName in obj);\n}\n\n/**\n * Safe way of detecting whether or not the given thing is a primitive and\n * whether it has the given property\n */\nfunction primitiveHasOwnProperty (primitive, propName) {\n return (\n primitive != null\n && typeof primitive !== 'object'\n && primitive.hasOwnProperty\n && primitive.hasOwnProperty(propName)\n );\n}\n\n// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577\n// See https://github.com/janl/mustache.js/issues/189\nvar regExpTest = RegExp.prototype.test;\nfunction testRegExp (re, string) {\n return regExpTest.call(re, string);\n}\n\nvar nonSpaceRe = /\\S/;\nfunction isWhitespace (string) {\n return !testRegExp(nonSpaceRe, string);\n}\n\nvar entityMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '/': '/',\n '`': '`',\n '=': '='\n};\n\nfunction escapeHtml (string) {\n return String(string).replace(/[&<>\"'`=\\/]/g, function fromEntityMap (s) {\n return entityMap[s];\n });\n}\n\nvar whiteRe = /\\s*/;\nvar spaceRe = /\\s+/;\nvar equalsRe = /\\s*=/;\nvar curlyRe = /\\s*\\}/;\nvar tagRe = /#|\\^|\\/|>|\\{|&|=|!/;\n\n/**\n * Breaks up the given `template` string into a tree of tokens. If the `tags`\n * argument is given here it must be an array with two string values: the\n * opening and closing tags used in the template (e.g. [ \"<%\", \"%>\" ]). Of\n * course, the default is to use mustaches (i.e. mustache.tags).\n *\n * A token is an array with at least 4 elements. The first element is the\n * mustache symbol that was used inside the tag, e.g. \"#\" or \"&\". If the tag\n * did not contain a symbol (i.e. {{myValue}}) this element is \"name\". For\n * all text that appears outside a symbol this element is \"text\".\n *\n * The second element of a token is its \"value\". For mustache tags this is\n * whatever else was inside the tag besides the opening symbol. For text tokens\n * this is the text itself.\n *\n * The third and fourth elements of the token are the start and end indices,\n * respectively, of the token in the original template.\n *\n * Tokens that are the root node of a subtree contain two more elements: 1) an\n * array of tokens in the subtree and 2) the index in the original template at\n * which the closing tag for that section begins.\n *\n * Tokens for partials also contain two more elements: 1) a string value of\n * indendation prior to that tag and 2) the index of that tag on that line -\n * eg a value of 2 indicates the partial is the third tag on this line.\n */\nfunction parseTemplate (template, tags) {\n if (!template)\n return [];\n var lineHasNonSpace = false;\n var sections = []; // Stack to hold section tokens\n var tokens = []; // Buffer to hold the tokens\n var spaces = []; // Indices of whitespace tokens on the current line\n var hasTag = false; // Is there a {{tag}} on the current line?\n var nonSpace = false; // Is there a non-space char on the current line?\n var indentation = ''; // Tracks indentation for tags that use it\n var tagIndex = 0; // Stores a count of number of tags encountered on a line\n\n // Strips all whitespace tokens array for the current line\n // if there was a {{#tag}} on it and otherwise only space.\n function stripSpace () {\n if (hasTag && !nonSpace) {\n while (spaces.length)\n delete tokens[spaces.pop()];\n } else {\n spaces = [];\n }\n\n hasTag = false;\n nonSpace = false;\n }\n\n var openingTagRe, closingTagRe, closingCurlyRe;\n function compileTags (tagsToCompile) {\n if (typeof tagsToCompile === 'string')\n tagsToCompile = tagsToCompile.split(spaceRe, 2);\n\n if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)\n throw new Error('Invalid tags: ' + tagsToCompile);\n\n openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\\\s*');\n closingTagRe = new RegExp('\\\\s*' + escapeRegExp(tagsToCompile[1]));\n closingCurlyRe = new RegExp('\\\\s*' + escapeRegExp('}' + tagsToCompile[1]));\n }\n\n compileTags(tags || mustache.tags);\n\n var scanner = new Scanner(template);\n\n var start, type, value, chr, token, openSection;\n while (!scanner.eos()) {\n start = scanner.pos;\n\n // Match any text between tags.\n value = scanner.scanUntil(openingTagRe);\n\n if (value) {\n for (var i = 0, valueLength = value.length; i < valueLength; ++i) {\n chr = value.charAt(i);\n\n if (isWhitespace(chr)) {\n spaces.push(tokens.length);\n indentation += chr;\n } else {\n nonSpace = true;\n lineHasNonSpace = true;\n indentation += ' ';\n }\n\n tokens.push([ 'text', chr, start, start + 1 ]);\n start += 1;\n\n // Check for whitespace on the current line.\n if (chr === '\\n') {\n stripSpace();\n indentation = '';\n tagIndex = 0;\n lineHasNonSpace = false;\n }\n }\n }\n\n // Match the opening tag.\n if (!scanner.scan(openingTagRe))\n break;\n\n hasTag = true;\n\n // Get the tag type.\n type = scanner.scan(tagRe) || 'name';\n scanner.scan(whiteRe);\n\n // Get the tag value.\n if (type === '=') {\n value = scanner.scanUntil(equalsRe);\n scanner.scan(equalsRe);\n scanner.scanUntil(closingTagRe);\n } else if (type === '{') {\n value = scanner.scanUntil(closingCurlyRe);\n scanner.scan(curlyRe);\n scanner.scanUntil(closingTagRe);\n type = '&';\n } else {\n value = scanner.scanUntil(closingTagRe);\n }\n\n // Match the closing tag.\n if (!scanner.scan(closingTagRe))\n throw new Error('Unclosed tag at ' + scanner.pos);\n\n if (type == '>') {\n token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ];\n } else {\n token = [ type, value, start, scanner.pos ];\n }\n tagIndex++;\n tokens.push(token);\n\n if (type === '#' || type === '^') {\n sections.push(token);\n } else if (type === '/') {\n // Check section nesting.\n openSection = sections.pop();\n\n if (!openSection)\n throw new Error('Unopened section \"' + value + '\" at ' + start);\n\n if (openSection[1] !== value)\n throw new Error('Unclosed section \"' + openSection[1] + '\" at ' + start);\n } else if (type === 'name' || type === '{' || type === '&') {\n nonSpace = true;\n } else if (type === '=') {\n // Set the tags for the next time around.\n compileTags(value);\n }\n }\n\n stripSpace();\n\n // Make sure there are no open sections when we're done.\n openSection = sections.pop();\n\n if (openSection)\n throw new Error('Unclosed section \"' + openSection[1] + '\" at ' + scanner.pos);\n\n return nestTokens(squashTokens(tokens));\n}\n\n/**\n * Combines the values of consecutive text tokens in the given `tokens` array\n * to a single token.\n */\nfunction squashTokens (tokens) {\n var squashedTokens = [];\n\n var token, lastToken;\n for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {\n token = tokens[i];\n\n if (token) {\n if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {\n lastToken[1] += token[1];\n lastToken[3] = token[3];\n } else {\n squashedTokens.push(token);\n lastToken = token;\n }\n }\n }\n\n return squashedTokens;\n}\n\n/**\n * Forms the given array of `tokens` into a nested tree structure where\n * tokens that represent a section have two additional items: 1) an array of\n * all tokens that appear in that section and 2) the index in the original\n * template that represents the end of that section.\n */\nfunction nestTokens (tokens) {\n var nestedTokens = [];\n var collector = nestedTokens;\n var sections = [];\n\n var token, section;\n for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {\n token = tokens[i];\n\n switch (token[0]) {\n case '#':\n case '^':\n collector.push(token);\n sections.push(token);\n collector = token[4] = [];\n break;\n case '/':\n section = sections.pop();\n section[5] = token[2];\n collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;\n break;\n default:\n collector.push(token);\n }\n }\n\n return nestedTokens;\n}\n\n/**\n * A simple string scanner that is used by the template parser to find\n * tokens in template strings.\n */\nfunction Scanner (string) {\n this.string = string;\n this.tail = string;\n this.pos = 0;\n}\n\n/**\n * Returns `true` if the tail is empty (end of string).\n */\nScanner.prototype.eos = function eos () {\n return this.tail === '';\n};\n\n/**\n * Tries to match the given regular expression at the current position.\n * Returns the matched text if it can match, the empty string otherwise.\n */\nScanner.prototype.scan = function scan (re) {\n var match = this.tail.match(re);\n\n if (!match || match.index !== 0)\n return '';\n\n var string = match[0];\n\n this.tail = this.tail.substring(string.length);\n this.pos += string.length;\n\n return string;\n};\n\n/**\n * Skips all text until the given regular expression can be matched. Returns\n * the skipped string, which is the entire tail if no match can be made.\n */\nScanner.prototype.scanUntil = function scanUntil (re) {\n var index = this.tail.search(re), match;\n\n switch (index) {\n case -1:\n match = this.tail;\n this.tail = '';\n break;\n case 0:\n match = '';\n break;\n default:\n match = this.tail.substring(0, index);\n this.tail = this.tail.substring(index);\n }\n\n this.pos += match.length;\n\n return match;\n};\n\n/**\n * Represents a rendering context by wrapping a view object and\n * maintaining a reference to the parent context.\n */\nfunction Context (view, parentContext) {\n this.view = view;\n this.cache = { '.': this.view };\n this.parent = parentContext;\n}\n\n/**\n * Creates a new context using the given view with this context\n * as the parent.\n */\nContext.prototype.push = function push (view) {\n return new Context(view, this);\n};\n\n/**\n * Returns the value of the given name in this context, traversing\n * up the context hierarchy if the value is absent in this context's view.\n */\nContext.prototype.lookup = function lookup (name) {\n var cache = this.cache;\n\n var value;\n if (cache.hasOwnProperty(name)) {\n value = cache[name];\n } else {\n var context = this, intermediateValue, names, index, lookupHit = false;\n\n while (context) {\n if (name.indexOf('.') > 0) {\n intermediateValue = context.view;\n names = name.split('.');\n index = 0;\n\n /**\n * Using the dot notion path in `name`, we descend through the\n * nested objects.\n *\n * To be certain that the lookup has been successful, we have to\n * check if the last object in the path actually has the property\n * we are looking for. We store the result in `lookupHit`.\n *\n * This is specially necessary for when the value has been set to\n * `undefined` and we want to avoid looking up parent contexts.\n *\n * In the case where dot notation is used, we consider the lookup\n * to be successful even if the last \"object\" in the path is\n * not actually an object but a primitive (e.g., a string, or an\n * integer), because it is sometimes useful to access a property\n * of an autoboxed primitive, such as the length of a string.\n **/\n while (intermediateValue != null && index < names.length) {\n if (index === names.length - 1)\n lookupHit = (\n hasProperty(intermediateValue, names[index])\n || primitiveHasOwnProperty(intermediateValue, names[index])\n );\n\n intermediateValue = intermediateValue[names[index++]];\n }\n } else {\n intermediateValue = context.view[name];\n\n /**\n * Only checking against `hasProperty`, which always returns `false` if\n * `context.view` is not an object. Deliberately omitting the check\n * against `primitiveHasOwnProperty` if dot notation is not used.\n *\n * Consider this example:\n * ```\n * Mustache.render(\"The length of a football field is {{#length}}{{length}}{{/length}}.\", {length: \"100 yards\"})\n * ```\n *\n * If we were to check also against `primitiveHasOwnProperty`, as we do\n * in the dot notation case, then render call would return:\n *\n * \"The length of a football field is 9.\"\n *\n * rather than the expected:\n *\n * \"The length of a football field is 100 yards.\"\n **/\n lookupHit = hasProperty(context.view, name);\n }\n\n if (lookupHit) {\n value = intermediateValue;\n break;\n }\n\n context = context.parent;\n }\n\n cache[name] = value;\n }\n\n if (isFunction(value))\n value = value.call(this.view);\n\n return value;\n};\n\n/**\n * A Writer knows how to take a stream of tokens and render them to a\n * string, given a context. It also maintains a cache of templates to\n * avoid the need to parse the same template twice.\n */\nfunction Writer () {\n this.templateCache = {\n _cache: {},\n set: function set (key, value) {\n this._cache[key] = value;\n },\n get: function get (key) {\n return this._cache[key];\n },\n clear: function clear () {\n this._cache = {};\n }\n };\n}\n\n/**\n * Clears all cached templates in this writer.\n */\nWriter.prototype.clearCache = function clearCache () {\n if (typeof this.templateCache !== 'undefined') {\n this.templateCache.clear();\n }\n};\n\n/**\n * Parses and caches the given `template` according to the given `tags` or\n * `mustache.tags` if `tags` is omitted, and returns the array of tokens\n * that is generated from the parse.\n */\nWriter.prototype.parse = function parse (template, tags) {\n var cache = this.templateCache;\n var cacheKey = template + ':' + (tags || mustache.tags).join(':');\n var isCacheEnabled = typeof cache !== 'undefined';\n var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;\n\n if (tokens == undefined) {\n tokens = parseTemplate(template, tags);\n isCacheEnabled && cache.set(cacheKey, tokens);\n }\n return tokens;\n};\n\n/**\n * High-level method that is used to render the given `template` with\n * the given `view`.\n *\n * The optional `partials` argument may be an object that contains the\n * names and templates of partials that are used in the template. It may\n * also be a function that is used to load partial templates on the fly\n * that takes a single argument: the name of the partial.\n *\n * If the optional `config` argument is given here, then it should be an\n * object with a `tags` attribute or an `escape` attribute or both.\n * If an array is passed, then it will be interpreted the same way as\n * a `tags` attribute on a `config` object.\n *\n * The `tags` attribute of a `config` object must be an array with two\n * string values: the opening and closing tags used in the template (e.g.\n * [ \"<%\", \"%>\" ]). The default is to mustache.tags.\n *\n * The `escape` attribute of a `config` object must be a function which\n * accepts a string as input and outputs a safely escaped string.\n * If an `escape` function is not provided, then an HTML-safe string\n * escaping function is used as the default.\n */\nWriter.prototype.render = function render (template, view, partials, config) {\n var tags = this.getConfigTags(config);\n var tokens = this.parse(template, tags);\n var context = (view instanceof Context) ? view : new Context(view, undefined);\n return this.renderTokens(tokens, context, partials, template, config);\n};\n\n/**\n * Low-level method that renders the given array of `tokens` using\n * the given `context` and `partials`.\n *\n * Note: The `originalTemplate` is only ever used to extract the portion\n * of the original template that was contained in a higher-order section.\n * If the template doesn't use higher-order sections, this argument may\n * be omitted.\n */\nWriter.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) {\n var buffer = '';\n\n var token, symbol, value;\n for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {\n value = undefined;\n token = tokens[i];\n symbol = token[0];\n\n if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config);\n else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config);\n else if (symbol === '>') value = this.renderPartial(token, context, partials, config);\n else if (symbol === '&') value = this.unescapedValue(token, context);\n else if (symbol === 'name') value = this.escapedValue(token, context, config);\n else if (symbol === 'text') value = this.rawValue(token);\n\n if (value !== undefined)\n buffer += value;\n }\n\n return buffer;\n};\n\nWriter.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) {\n var self = this;\n var buffer = '';\n var value = context.lookup(token[1]);\n\n // This function is used to render an arbitrary template\n // in the current context by higher-order sections.\n function subRender (template) {\n return self.render(template, context, partials, config);\n }\n\n if (!value) return;\n\n if (isArray(value)) {\n for (var j = 0, valueLength = value.length; j < valueLength; ++j) {\n buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config);\n }\n } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {\n buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config);\n } else if (isFunction(value)) {\n if (typeof originalTemplate !== 'string')\n throw new Error('Cannot use higher-order sections without the original template');\n\n // Extract the portion of the original template that the section contains.\n value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);\n\n if (value != null)\n buffer += value;\n } else {\n buffer += this.renderTokens(token[4], context, partials, originalTemplate, config);\n }\n return buffer;\n};\n\nWriter.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) {\n var value = context.lookup(token[1]);\n\n // Use JavaScript's definition of falsy. Include empty arrays.\n // See https://github.com/janl/mustache.js/issues/186\n if (!value || (isArray(value) && value.length === 0))\n return this.renderTokens(token[4], context, partials, originalTemplate, config);\n};\n\nWriter.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) {\n var filteredIndentation = indentation.replace(/[^ \\t]/g, '');\n var partialByNl = partial.split('\\n');\n for (var i = 0; i < partialByNl.length; i++) {\n if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) {\n partialByNl[i] = filteredIndentation + partialByNl[i];\n }\n }\n return partialByNl.join('\\n');\n};\n\nWriter.prototype.renderPartial = function renderPartial (token, context, partials, config) {\n if (!partials) return;\n var tags = this.getConfigTags(config);\n\n var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];\n if (value != null) {\n var lineHasNonSpace = token[6];\n var tagIndex = token[5];\n var indentation = token[4];\n var indentedValue = value;\n if (tagIndex == 0 && indentation) {\n indentedValue = this.indentPartial(value, indentation, lineHasNonSpace);\n }\n var tokens = this.parse(indentedValue, tags);\n return this.renderTokens(tokens, context, partials, indentedValue, config);\n }\n};\n\nWriter.prototype.unescapedValue = function unescapedValue (token, context) {\n var value = context.lookup(token[1]);\n if (value != null)\n return value;\n};\n\nWriter.prototype.escapedValue = function escapedValue (token, context, config) {\n var escape = this.getConfigEscape(config) || mustache.escape;\n var value = context.lookup(token[1]);\n if (value != null)\n return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value);\n};\n\nWriter.prototype.rawValue = function rawValue (token) {\n return token[1];\n};\n\nWriter.prototype.getConfigTags = function getConfigTags (config) {\n if (isArray(config)) {\n return config;\n }\n else if (config && typeof config === 'object') {\n return config.tags;\n }\n else {\n return undefined;\n }\n};\n\nWriter.prototype.getConfigEscape = function getConfigEscape (config) {\n if (config && typeof config === 'object' && !isArray(config)) {\n return config.escape;\n }\n else {\n return undefined;\n }\n};\n\nvar mustache = {\n name: 'mustache.js',\n version: '4.2.0',\n tags: [ '{{', '}}' ],\n clearCache: undefined,\n escape: undefined,\n parse: undefined,\n render: undefined,\n Scanner: undefined,\n Context: undefined,\n Writer: undefined,\n /**\n * Allows a user to override the default caching strategy, by providing an\n * object with set, get and clear methods. This can also be used to disable\n * the cache by setting it to the literal `undefined`.\n */\n set templateCache (cache) {\n defaultWriter.templateCache = cache;\n },\n /**\n * Gets the default or overridden caching object from the default writer.\n */\n get templateCache () {\n return defaultWriter.templateCache;\n }\n};\n\n// All high-level mustache.* functions use this writer.\nvar defaultWriter = new Writer();\n\n/**\n * Clears all cached templates in the default writer.\n */\nmustache.clearCache = function clearCache () {\n return defaultWriter.clearCache();\n};\n\n/**\n * Parses and caches the given template in the default writer and returns the\n * array of tokens it contains. Doing this ahead of time avoids the need to\n * parse templates on the fly as they are rendered.\n */\nmustache.parse = function parse (template, tags) {\n return defaultWriter.parse(template, tags);\n};\n\n/**\n * Renders the `template` with the given `view`, `partials`, and `config`\n * using the default writer.\n */\nmustache.render = function render (template, view, partials, config) {\n if (typeof template !== 'string') {\n throw new TypeError('Invalid template! Template should be a \"string\" ' +\n 'but \"' + typeStr(template) + '\" was given as the first ' +\n 'argument for mustache#render(template, view, partials)');\n }\n\n return defaultWriter.render(template, view, partials, config);\n};\n\n// Export the escaping function so that the user may override it.\n// See https://github.com/janl/mustache.js/issues/244\nmustache.escape = escapeHtml;\n\n// Export these mainly for testing, but also for advanced usage.\nmustache.Scanner = Scanner;\nmustache.Context = Context;\nmustache.Writer = Writer;\n\nexport default mustache;\n","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>(\n template: string,\n options?: Options\n) {\n if (Mustache === undefined) {\n throw new Error(\n \"Mustache is not defined. Please include the Mustache dependency or library in your page.\"\n )\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}","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>(pro