@ideal-postcodes/jsutil
Version:
Browser Address Autocomplete for api.ideal-postcodes.co.uk
1 lines • 76 kB
Source Map (JSON)
{"version":3,"sources":["../lib/index.ts","../lib/string.ts","../lib/dom.ts","../lib/util.ts","../lib/types.ts","../lib/assets.ts","../lib/event.ts","../lib/input.ts","../lib/country.ts","../lib/store.ts","../lib/id.ts","../lib/address.ts","../lib/keyboard.ts","../lib/debounce.ts","../lib/watcher.ts"],"sourcesContent":["export * from \"./util\";\nexport * from \"./types\";\nexport * from \"./dom\";\nexport * from \"./assets\";\nexport * from \"./country\";\nexport * from \"./event\";\nexport * from \"./input\";\nexport * from \"./store\";\nexport * from \"./id\";\nexport * from \"./address\";\nexport * from \"./string\";\nexport * from \"./keyboard\";\nexport * from \"./debounce\";\nexport * from \"./watcher\";\n","export const isString = (input: unknown): input is string =>\n typeof input === \"string\";\n\n/**\n * Returns true if substring match\n */\nexport const includes = (haystack: string, needle: string): boolean =>\n haystack.indexOf(needle) !== -1;\n","import { ParentTest, SelectorNode } from \"./types\";\nimport { isString } from \"./string\";\n\n/**\n * Returns true if window present\n */\nexport const hasWindow = (): boolean => typeof window !== \"undefined\";\n\n/**\n * Convert a NodeList to array\n */\nexport const toArray = <T = HTMLElement>(nodeList: NodeList): T[] =>\n Array.prototype.slice.call(nodeList);\n\n/**\n * Returns true if initialised\n */\nexport const loaded = (elem: HTMLElement, prefix = \"idpc\"): boolean =>\n elem.getAttribute(prefix) === \"true\";\n\n/**\n * Marks HTML as loaded. i.e. Address validation plugin has been bound to this element\n */\nexport const markLoaded = (elem: HTMLElement, prefix = \"idpc\") =>\n elem.setAttribute(prefix, \"true\");\n\nconst isTrue: ParentTest = () => true;\n\n/**\n * Retrives a parent by tag name\n *\n * Accepts node as a possible candidate for parent\n *\n * Executes an additional test if specified\n */\nexport const getParent = (\n node: HTMLElement,\n entity: string,\n test: ParentTest = isTrue\n): HTMLElement | null => {\n let parent = node;\n const tagName = entity.toUpperCase();\n\n while (parent.tagName !== \"HTML\") {\n if (parent.tagName === tagName && test(parent)) return parent;\n if (parent.parentNode === null) return null;\n parent = parent.parentNode as HTMLElement;\n }\n return null;\n};\n\n/**\n * Queries `parent` for a specific `selector`. Returns null if no selector\n */\nexport const toHtmlElem = (\n parent: HTMLElement,\n selector?: string\n): HTMLElement | null => (selector ? parent.querySelector(selector) : null);\n\n/**\n * Retrieves an anchor defined by a query selector\n *\n * Checks if anchor has been loaded, if not, marks it as loaded\n */\nexport const getAnchors = (selector: string, d?: Document): HTMLElement[] => {\n const matches = (d || window.document).querySelectorAll(selector);\n const anchors = toArray(matches).filter((e) => !loaded(e));\n if (anchors.length === 0) return [];\n anchors.forEach((anchor) => markLoaded(anchor));\n return anchors;\n};\n\ninterface InsertBeforeOptions {\n elem: HTMLElement;\n target: HTMLElement;\n}\n\ninterface InsertBefore {\n (options: InsertBeforeOptions): HTMLElement | undefined;\n}\n\n/**\n * Inserts element before `target` element\n */\nexport const insertBefore: InsertBefore = ({ elem, target }) => {\n const parent = target.parentNode;\n if (parent === null) return;\n parent.insertBefore(elem, target);\n return elem;\n};\n\n/**\n * Scoped retrieval of a HTML element given a querystring or the element itself\n *\n * @hidden\n */\nexport const toElem = (\n elem: SelectorNode | null,\n context: HTMLElement | Document\n): HTMLElement | null => {\n if (isString(elem)) return context.querySelector(elem);\n return elem;\n};\n\n/**\n * @hidden\n */\nconst d = () => window.document;\n\n/**\n * Outputs scope of controller\n * - If null, return window.document\n * - If string, resolve with query selector\n * - Otherwise returns the HTMLElement or Document\n */\nexport const getScope = (\n scope: null | string | HTMLElement | Document\n): HTMLElement | Document => {\n if (isString(scope)) return d().querySelector(scope) as HTMLElement;\n if (scope === null) return d();\n return scope;\n};\n\n/**\n * Retrieves document instance of HTML Element or Document\n *\n * Defaults to window.document\n */\nexport const getDocument = (scope: HTMLElement | Document): Document => {\n if (scope instanceof Document || scope.constructor.name === \"HTMLDocument\") return scope as Document;\n if (scope.ownerDocument) return scope.ownerDocument;\n return d();\n};\n\nexport type CSSStyle = Partial<Record<keyof CSSStyleDeclaration, string>>;\n\n/**\n * Applies style object to HTML Element and returns the previous style in `string` format\n */\nexport const setStyle = (\n element: HTMLElement,\n style: CSSStyle\n): string | null => {\n const currentRules = element.getAttribute(\"style\");\n Object.keys(style).forEach(\n (key: any) => (element.style[key] = style[key] as string)\n );\n return currentRules;\n};\n\nexport const restoreStyle = (\n element: HTMLElement,\n style: string | null\n): void => {\n element.setAttribute(\"style\", style || \"\");\n};\n\n/**\n * Hide HTML Element\n */\nexport const hide = <T extends HTMLElement = HTMLElement>(e: T): T => {\n e.style.display = \"none\";\n return e;\n};\n\n/**\n * Show HTML Element\n */\nexport const show = <T extends HTMLElement = HTMLElement>(e: T): T => {\n e.style.display = \"\";\n return e;\n};\n\n/**\n * Remove node from DOM\n */\nexport const remove = (elem: HTMLElement | null): void => {\n if (elem === null || elem.parentNode === null) return;\n elem.parentNode.removeChild(elem);\n};\n\n/**\n * Retrieves DOM Elements and returns the first which matches text\n */\nexport const contains = (\n scope: HTMLElement | Document,\n selector: string,\n text: string\n): any => {\n const elements = scope.querySelectorAll(selector);\n for (let i = 0; i < elements.length; i++) {\n const e = elements[i] as HTMLElement;\n const content = e.innerText;\n // Ignore empty string, undefined, null\n if (content && content.trim() === text) return e;\n }\n return null;\n};\n\n/**\n * Find the deepest common ancestor element shared by all provided elements\n * \n * This function traverses the DOM tree from each element up to the root,\n * then identifies the deepest element that is an ancestor to all provided elements.\n * \n * @param elements - Array of HTML elements to find the common parent for\n * @returns The deepest common ancestor element, or null if the array is empty.\n * If only one element is provided, returns its parent element (or the element itself if it has no parent).\n * \n * @example\n * ```typescript\n * const div1 = document.getElementById('child1');\n * const div2 = document.getElementById('child2');\n * const commonParent = findCommonParent([div1, div2]);\n * // Returns the closest element that contains both div1 and div2\n * ```\n */\nexport const findCommonParent = (elements: HTMLElement[]): HTMLElement | null => {\n if (elements.length < 1) return null;\n if (elements.length === 1) return elements[0].parentElement || elements[0];\n\n const getPathToRoot = (element: HTMLElement): HTMLElement[] => {\n const path: HTMLElement[] = [];\n let current: HTMLElement | null = element;\n while (current) {\n path.push(current);\n current = current.parentElement;\n }\n return path.reverse(); // Root first\n }\n\n // Get paths for all elements (root first)\n const paths = elements.map(getPathToRoot);\n\n // Find the minimum path length\n const minLength = Math.min(...paths.map(path => path.length));\n\n // Find the deepest common ancestor\n let commonAncestor: HTMLElement | null = null;\n for (let depth = 0; depth < minLength; depth++) {\n const currentNode = paths[0][depth];\n if (paths.every(path => path[depth] === currentNode)) {\n commonAncestor = currentNode;\n } else {\n break;\n }\n }\n\n return commonAncestor;\n}","import {\n Bind,\n Start,\n ParentTest,\n Stop,\n PageTest,\n Config,\n Selectors,\n Targets,\n Binding,\n} from \"./types\";\n\nimport { toHtmlElem, getAnchors, getParent } from \"./dom\";\n\n/**\n * Returns an object of attributes mapped to HTMLInputElement\n *\n * Returns null if any critical elements are not present (line 1, post_town, postcode)\n *\n * @deprecated\n */\nexport const getTargets = (\n parent: HTMLElement,\n selectors: Selectors\n): Targets | null => {\n const line_1: HTMLElement | null = toHtmlElem(parent, selectors.line_1);\n if (line_1 === null) return null;\n\n const post_town: HTMLElement | null = toHtmlElem(parent, selectors.post_town);\n if (post_town === null) return null;\n\n const postcode: HTMLElement | null = parent.querySelector(selectors.postcode);\n if (postcode === null) return null;\n\n const line_2: HTMLElement | null = toHtmlElem(parent, selectors.line_2);\n const line_3: HTMLElement | null = toHtmlElem(parent, selectors.line_3);\n const country: HTMLElement | null = toHtmlElem(parent, selectors.country);\n const county: HTMLElement | null = toHtmlElem(parent, selectors.county);\n const organisation: HTMLElement | null = toHtmlElem(\n parent,\n selectors.organisation\n );\n\n return {\n line_1,\n line_2,\n line_3,\n post_town,\n county,\n postcode,\n organisation,\n country,\n };\n};\n\n/**\n * Given a list of bindings, return true if any are relevant to current pae\n */\nexport const relevantPage = (bindings: Binding[]): boolean =>\n bindings.some((b) => b.pageTest());\n\n/**\n * Setup defaults\n */\nexport const defaults: Config = {\n enabled: true,\n apiKey: \"\",\n populateCounty: false,\n\n // Autocomplete config\n autocomplete: true,\n autocompleteOverride: {},\n\n // Postcode lookup config\n postcodeLookup: true,\n postcodeLookupOverride: {},\n};\n\nexport const config = (): Config | undefined => {\n const c = (window as any).idpcConfig;\n if (c === undefined) return;\n return { ...defaults, ...c };\n};\n\n/**\n * Configure GenerateTimer\n */\ninterface GenerateTimerOptions {\n /**\n * Function which decides whether to execute bind on each tick\n */\n pageTest: PageTest;\n /**\n * Method to invoke on each tick\n */\n bind: Bind;\n /**\n * Specificy time between ticks in milliseconds\n *\n * @default 1000\n */\n interval?: number;\n}\n\ninterface TimerControls {\n start: Start;\n stop: Stop;\n}\n\ninterface GenerateTimer {\n (options: GenerateTimerOptions): TimerControls;\n}\n\n/**\n * Generates a stoppable timer which invokes `bind` on every tick\n */\nexport const generateTimer: GenerateTimer = ({\n pageTest,\n bind,\n interval = 1000,\n}) => {\n let timer: number | null = null;\n\n const start = (config?: Config): number | null => {\n if (!pageTest()) return null;\n timer = window.setInterval(() => {\n try {\n bind(config);\n } catch (e) {\n // Terminate timer if some exception is raised\n stop();\n /* eslint no-console: [\"error\", { allow: [\"log\"] }] */\n console.log(e);\n }\n }, interval);\n return timer;\n };\n\n const stop = () => {\n if (timer === null) return;\n window.clearInterval(timer);\n timer = null;\n };\n\n return { start, stop };\n};\n\ninterface SetupOptions {\n window: Window;\n bindings: Binding[];\n callback?: (result?: SetupResult[]) => void;\n}\n\ninterface Setup {\n (options: SetupOptions): void;\n}\n\ninterface SetupResult {\n binding: Binding;\n start: Start;\n stop: Stop;\n}\n\nconst NOOP = () => {};\n\n/**\n * Deploys address search tools on page using predefined bindings\n */\nexport const setup: Setup = ({ bindings, callback = NOOP }) => {\n const c = config();\n if (c === undefined) return callback();\n if (!relevantPage(bindings)) return callback();\n\n const result = bindings.reduce<SetupResult[]>((prev, binding) => {\n const { pageTest, bind } = binding;\n if (!pageTest()) return prev;\n const { start, stop } = generateTimer({ pageTest, bind });\n start(c);\n prev.push({ binding, start, stop });\n return prev;\n }, []);\n return callback(result);\n};\n\nconst DEFAULT_SCOPE: keyof HTMLElementTagNameMap = \"form\";\n\nconst DEFAULT_ANCHOR: keyof Selectors = \"line_1\";\n\ninterface SetupBindOptions {\n selectors: Selectors;\n /**\n * Query selector that defines anchor. Defaults to selectors.line_1\n */\n anchorSelector?: string;\n /**\n * Restricts subsequent selector scope once anchor is found. Defaults to `form`\n */\n parentScope?: string;\n /**\n * Allow search in specific document\n */\n doc?: Document;\n /**\n * Optional test to select for parent/scope\n */\n parentTest?: ParentTest;\n}\n\ninterface SetupBind {\n (options: SetupBindOptions): PageBindings[];\n}\n\ninterface PageBindings {\n anchor: HTMLElement;\n targets: Targets;\n parent: HTMLElement;\n}\n\nexport const setupBind: SetupBind = ({\n selectors,\n anchorSelector,\n parentScope,\n doc,\n parentTest,\n}) => {\n const anchors = getAnchors(anchorSelector || selectors[DEFAULT_ANCHOR], doc);\n return anchors.reduce<PageBindings[]>((prev, anchor) => {\n const parent = getParent(anchor, parentScope || DEFAULT_SCOPE, parentTest);\n if (!parent) return prev;\n\n const targets = getTargets(parent, selectors);\n if (targets === null) return prev;\n\n prev.push({ targets, parent, anchor });\n return prev;\n }, []);\n};\n\nexport const toId = (elem: HTMLElement): string => `#${elem.id}`;\n\nexport const cssEscape = (value: string): string => {\n value = String(value);\n const length = value.length;\n let index = -1;\n let codeUnit: any;\n let result = \"\";\n const firstCodeUnit = value.charCodeAt(0);\n while (++index < length) {\n codeUnit = value.charCodeAt(index);\n if (codeUnit == 0x0000) {\n result += \"\\uFFFD\";\n continue;\n }\n\n if (\n (codeUnit >= 0x0001 && codeUnit <= 0x001f) ||\n codeUnit == 0x007f ||\n (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n (index == 1 &&\n codeUnit >= 0x0030 &&\n codeUnit <= 0x0039 &&\n firstCodeUnit == 0x002d)\n ) {\n result += \"\\\\\" + codeUnit.toString(16) + \" \";\n continue;\n }\n\n if (index == 0 && length == 1 && codeUnit == 0x002d) {\n result += \"\\\\\" + value.charAt(index);\n continue;\n }\n if (\n codeUnit >= 0x0080 ||\n codeUnit == 0x002d ||\n codeUnit == 0x005f ||\n (codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n (codeUnit >= 0x0041 && codeUnit <= 0x005a) ||\n (codeUnit >= 0x0061 && codeUnit <= 0x007a)\n ) {\n result += value.charAt(index);\n continue;\n }\n result += \"\\\\\" + value.charAt(index);\n }\n\n return result;\n};\n","import { components } from \"@ideal-postcodes/openapi\";\n\nexport type UkSuggestion = components[\"schemas\"][\"UkAddressSuggestion\"];\n\nexport type GlobalAddressSuggestion =\n components[\"schemas\"][\"AddressSuggestion\"];\n\nexport type AddressSuggestion = GlobalAddressSuggestion | UkSuggestion;\n\nexport type PafAddress = components[\"schemas\"][\"PafAddress\"];\n\nexport type MrAddress = components[\"schemas\"][\"MrAddress\"];\n\nexport type NybAddress = components[\"schemas\"][\"NybAddress\"];\n\nexport type PafaAddress = components[\"schemas\"][\"PafAliasAddress\"];\n\nexport type WelshPafAddress = components[\"schemas\"][\"WelshPafAddress\"];\n\nexport type GlobalAddress = components[\"schemas\"][\"GbrGlobalAddress\"];\n\nexport type GbrAddress =\n | PafAddress\n | MrAddress\n | NybAddress\n | PafaAddress\n | WelshPafAddress\n | GlobalAddress;\n\nexport type UspsAddress = components[\"schemas\"][\"UspsAddress\"];\n\nexport type UsaGlobalAddress = components[\"schemas\"][\"UsaGlobalAddress\"];\n\nexport type UsaAddress = UspsAddress | UsaGlobalAddress;\n\nexport type AnyAddress = GbrAddress | UsaAddress;\n\nexport interface Binding {\n /**\n * Verify if can be launched\n */\n pageTest: PageTest;\n /**\n * Binds to page\n */\n bind: Bind;\n}\n\nexport interface Start {\n (config?: Config): number | null;\n}\n\nexport interface Stop {\n (): void;\n}\n\nexport interface Bind<T extends Config = Config> {\n (config?: T): void;\n}\n\nexport interface PageTest {\n (): boolean;\n}\n\nexport interface Selectors {\n line_1: string;\n line_2?: string;\n line_3?: string;\n post_town: string;\n county?: string;\n postcode: string;\n organisation?: string;\n country: string;\n}\n\nexport interface Config {\n /**\n * Enable / disable the integration altogether\n */\n enabled: boolean;\n /**\n * A string of characters. Typically begins `ak_`\n *\n * The API Key is required to verify your account for address validation. View your API Keys from your [account dashboard](https://ideal-postcodes.co.uk/tokens).\n *\n * See our [API Key guide](https://ideal-postcodes.co.uk/guides/api-key) to find out more.\n */\n apiKey: string;\n /**\n * Populate the county field of an address. County data is optional for premise identification.\n *\n * We recommend avoid using county data altogether. Find out more from our [UK county data guide](https://ideal-postcodes.co.uk/guides/county-data).\n */\n populateCounty: boolean;\n /**\n * Setting this to `true` will enable autocomplete integration on your address forms.\n *\n * See our [Address Finder Demo](https://ideal-postcodes.co.uk/address-finder) to try it out.\n */\n autocomplete: boolean;\n /**\n * *Advanced.* Override the autocompletion plugin configuration.\n *\n * Pass in a [valid configuration object](https://ideal-postcodes.co.uk/documentation/ideal-postcodes-autocomplete#config) to override specific autocomplete configuration attributes.\n */\n autocompleteOverride: AutocompleteConfig;\n /**\n * Setting this to `true` will enable postcode lookup integration on your address forms.\n *\n * See our [Postcode Finder Demo](https://ideal-postcodes.co.uk/postcode-finder) to try it out.\n */\n postcodeLookup: boolean;\n /**\n * *Advanced.* Override the postcode lookup plugin configuration.\n *\n * Pass in a [valid configuration object](https://ideal-postcodes.co.uk/documentation/jquery-plugin) to override specific postcode lookup configuration settings.\n */\n postcodeLookupOverride: PostcodeLookupConfig;\n /**\n * Setting this to `true` will detach our Address Validation tools from the page if an unsupported terrority is selected\n */\n watchCountry?: boolean;\n /**\n * Setting this to `true` will attempt to deploy the Address Finder on an input away from line_1\n */\n separateFinder?: boolean;\n}\n\n/**\n * A map of HTML elements to be populated with address data\n */\nexport interface Targets {\n line_1: HTMLElement | null;\n line_2?: HTMLElement | null;\n line_3?: HTMLElement | null;\n post_town: HTMLElement | null;\n county: HTMLElement | null;\n postcode: HTMLElement | null;\n organisation: HTMLElement | null;\n country: HTMLElement | null;\n}\n\n/**\n * Placeholder\n */\nexport type AutocompleteConfig = any;\nexport type PostcodeLookupConfig = any;\n\nexport type LineCount = 1 | 2 | 3;\n\n/**\n * Describes availability of specific on page assets\n * complete - Asset is either not required or loaded\n * loading - Asset being retrieved or parsed\n */\nexport type LoadState = \"complete\" | \"loading\";\n\n/**\n * Method used to test whether a DOM element qualifies as parent\n */\nexport interface ParentTest {\n (e: HTMLElement): boolean;\n}\n\n/**\n * OutputFields can be identified by the address attributes supported by the API\n *\n */\nexport type OutputFields = Partial<Record<keyof GbrAddress, SelectorNode>>;\n\n/**\n * USA can be identified by the address attributes supported by the API\n *\n */\nexport type UsaOutputFields = Partial<Record<keyof UsaAddress, SelectorNode>>;\n\n/**\n * NamedFields - Elements can only be identified using string arguments\n */\nexport type NamedFields = Partial<Record<keyof GbrAddress, string>>;\n\n/**\n * Represents either a HTMLInputElement or a selector pointing to one. Element must have a `value` getter/setter available\n */\nexport type SelectorNode = string | HTMLInputElement | HTMLTextAreaElement| HTMLSelectElement;\n\nexport const isGbrAddress = (address: AnyAddress): address is GbrAddress =>\n //@ts-ignore\n address.post_town !== undefined;\n","declare global {\n interface Window {\n IdealPostcodes: any;\n jQuery: any;\n }\n}\n\nconst cache: Record<string, HTMLScriptElement> = {};\n\nexport const clearCache = () => {\n for (const url of Object.keys(cache)) {\n delete cache[url];\n }\n};\n\ninterface Downloader {\n (d?: Document): HTMLScriptElement;\n}\n\n/**\n * Script downloader factory. Caches script reference, only downloads once\n */\nexport const downloadScript = (url: string, integrity: string): Downloader => (\n d\n) => {\n if (cache[url]) return cache[url];\n const document = d || window.document;\n const script = loadScript(url, integrity, document);\n document.head.appendChild(script);\n cache[url] = script;\n return script;\n};\n\n/**\n * Inject CSS stylesheet\n */\nexport const loadStyle = (\n href: string,\n document: Document\n): HTMLLinkElement => {\n const link = document.createElement(\"link\");\n link.type = \"text/css\";\n link.rel = \"stylesheet\";\n link.href = href;\n return link;\n};\n\n/**\n * Inject script tag\n */\nexport const loadScript = (\n src: string,\n integrity: string,\n document: Document\n): HTMLScriptElement => {\n const script = document.createElement(\"script\");\n script.type = \"text/javascript\";\n script.crossOrigin = \"anonymous\";\n script.integrity = integrity;\n script.src = src;\n return script;\n};\n\n/**\n * Injects styke element to page\n */\nexport const injectStyle = (\n css: string,\n document: Document\n): HTMLStyleElement => {\n const style = document.createElement(\"style\");\n style.appendChild(document.createTextNode(css));\n document.head.appendChild(style);\n return style;\n};\n","interface EventOptions {\n event: string;\n bubbles?: boolean;\n cancelable?: boolean;\n}\n\ninterface NewEvent {\n (o: EventOptions): Event;\n}\n\n/**\n * Generates `Event` instance\n *\n * Includes polyfill where window.Event not available\n */\nexport const newEvent: NewEvent = ({\n event,\n bubbles = true,\n cancelable = true,\n}) => {\n // if Event available\n if (typeof window.Event === \"function\")\n return new window.Event(event, { bubbles, cancelable });\n // Fallback if Event not available (e.g. IE11)\n const e = document.createEvent(\"Event\");\n e.initEvent(event, bubbles, cancelable);\n return e;\n};\n\n/**\n * Input events we support\n */\nexport type Events = \"change\" | \"input\" | \"select\";\n\n/**\n * Dispatch custom event on element\n */\nexport const trigger = (e: HTMLElement, event: Events) =>\n e.dispatchEvent(newEvent({ event }));\n","import { trigger } from \"./event\";\n\n/**\n * Returns true if element is select\n */\nexport const isSelect = (e: HTMLElement | null): e is HTMLSelectElement => {\n if (e === null) return false;\n return (\n e instanceof HTMLSelectElement || e.constructor.name === \"HTMLSelectElement\"\n );\n};\n\n/**\n * Returns true if Element is <input>\n */\nexport const isInput = (e: HTMLElement | null): e is HTMLInputElement => {\n if (e === null) return false;\n return (\n e instanceof HTMLInputElement || e.constructor.name === \"HTMLInputElement\"\n );\n};\n\n/**\n * Returns true if Element is <textarea>\n */\nexport const isTextarea = (e: HTMLElement | null): e is HTMLTextAreaElement => {\n if (e === null) return false;\n return (\n e instanceof HTMLTextAreaElement ||\n e.constructor.name === \"HTMLTextAreaElement\"\n );\n};\n\n// Returns true if element is input, textarea or select\nexport const isInputElem = (\n e: HTMLElement | null\n): e is HTMLInputElement | HTMLTextAreaElement =>\n isInput(e) || isTextarea(e) || isSelect(e);\n\n// Updates input value and dispatches change envet\nexport const update = (\n input: HTMLElement | null | undefined,\n value: string,\n skipTrigger = false\n) => {\n if (!input) return;\n if (!isInput(input) && !isTextarea(input)) return;\n change({ e: input, value, skipTrigger });\n};\n\n// Returns true if HTMLElement has matching value\nexport const hasValue = (\n select: HTMLElement,\n value: string | null\n): boolean => {\n if (value === null) return false;\n return select.querySelector(`[value=\"${value}\"]`) !== null;\n};\n\n/**\n * Returns array of HTMLOptionElement if textContent matches search value\n */\nexport const optionsHasText = (\n select: HTMLElement,\n value: string | null\n): HTMLOptionElement[] => {\n if (value === null) return [];\n const options: NodeListOf<HTMLOptionElement> =\n select.querySelectorAll(\"option\");\n return Array.from(options).filter((o) => {\n // Normalize textContent by explicitly removing newlines and carriage returns, then normalizing other whitespace\n const normalizedText = o.textContent ? o.textContent.replace(/[\\n\\r]/g, '').replace(/\\s+/g, ' ').trim() : '';\n return normalizedText === value;\n });\n};\n\n/**\n * Updates value property of HTMLSelectElement\n */\nconst updateSelect = ({ e, value, skipTrigger }: ChangeOptions) => {\n if (value === null) return;\n if (!isSelect(e)) return;\n setValue(e, value);\n if (!skipTrigger) trigger(e, \"select\");\n trigger(e, \"change\");\n};\n\ntype InputElement = HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement;\n\ninterface SetValue {\n (e: InputElement, value: string): void;\n}\n\n/**\n * Sets the value of an Element using the setter for value found on the\n * protype chain\n *\n * We abandon all hope for input.value = \"foo\". This is broken on react\n */\nexport const setValue: SetValue = (e, value) => {\n const descriptor = Object.getOwnPropertyDescriptor(\n e.constructor.prototype,\n \"value\"\n );\n if (descriptor === undefined) return;\n if (descriptor.set === undefined) return;\n const setter = descriptor.set;\n setter.call(e, value);\n};\n\n/**\n * Updates value property of HTMLInputElement\n */\nconst updateInput = ({ e, value, skipTrigger }: ChangeOptions) => {\n if (value === null) return;\n if (!isInput(e) && !isTextarea(e)) return;\n setValue(e, value);\n if (!skipTrigger) trigger(e, \"input\");\n trigger(e, \"change\");\n};\n\ninterface Change {\n (options: ChangeOptions): void;\n}\n\ninterface ChangeOptions {\n e: InputElement;\n value: string | null;\n skipTrigger?: boolean;\n}\n\n/**\n * Updates value of input or select field and triggers an update event\n *\n * https://github.com/facebook/react/issues/11488#issuecomment-558874287\n */\nexport const change: Change = (options) => {\n if (options.value === null) return;\n updateSelect(options);\n updateInput(options);\n};\n","import { AnyAddress, isGbrAddress } from \"./types\";\nimport { isSelect, isInput, hasValue, change, optionsHasText } from \"./input\";\n\nconst UK = \"United Kingdom\";\nconst IOM = \"Isle of Man\";\nconst EN = \"England\";\nconst SC = \"Scotland\";\nconst WA = \"Wales\";\nconst NI = \"Northern Ireland\";\nconst CI = \"Channel Islands\";\n\n// Converts address to a country string\nexport const toCountry = (address: AnyAddress): string => {\n const country: string = address.country;\n if (country === EN) return UK;\n if (country === SC) return UK;\n if (country === WA) return UK;\n if (country === NI) return UK;\n if (country === IOM) return IOM;\n if (isGbrAddress(address)) {\n if (country === CI) {\n if (/^GY/.test(address.postcode)) return \"Guernsey\";\n if (/^JE/.test(address.postcode)) return \"Jersey\";\n }\n }\n return country;\n};\n\n/**\n * Updates a country field regardless of whether its an input or select\n * - If input is detected, value is set to country. For GBR countries, this\n * is set to \"United Kingdom\"\n * - If select is detected, the option with the value matching country is\n * selected. It will try full name, then iso2 then iso3\n */\nexport const updateCountry = (\n select: HTMLElement | null,\n address: AnyAddress\n) => {\n if (!select) return;\n if (isSelect(select)) {\n const bcc = toCountry(address);\n \n // Try to match by value\n if (hasValue(select, bcc)) {\n change({ e: select, value: bcc });\n return;\n }\n if (hasValue(select, address.country_iso_2)) {\n change({ e: select, value: address.country_iso_2 });\n return;\n }\n if (hasValue(select, address.country_iso)) {\n change({ e: select, value: address.country_iso });\n return;\n }\n \n // If no value match, try to match by text content\n let text = optionsHasText(select, bcc);\n if (text.length > 0) {\n change({ e: select, value: text[0].value || \"\" });\n return;\n }\n \n text = optionsHasText(select, address.country_iso_2);\n if (text.length > 0) {\n change({ e: select, value: text[0].value || \"\" });\n return;\n }\n \n text = optionsHasText(select, address.country_iso);\n if (text.length > 0) {\n change({ e: select, value: text[0].value || \"\" });\n return;\n }\n }\n\n if (isInput(select)) {\n const bcc = toCountry(address);\n change({ e: select, value: bcc });\n }\n};\n","import { hasWindow } from \"./dom\";\n\ndeclare global {\n interface Window {\n idpcGlobal?: IdpcGlobal;\n }\n}\n\n/**\n * Global store. Aimed at ensuring multiple copies of our JS libs can have some shared state if required\n */\nexport interface IdpcGlobal {\n idGen?: Record<string, number>;\n}\n\n/**\n * @hidden\n */\nlet g: IdpcGlobal = {};\n\nif (hasWindow()) {\n if (window.idpcGlobal) {\n // Adopt idpcGlobal from window if it exists\n g = window.idpcGlobal;\n } else {\n // Assign idpcGlobal to window if not exists\n window.idpcGlobal = g;\n }\n}\n\nexport const idpcState = (): IdpcGlobal => g;\n\n/**\n * Resets global store\n */\nexport const reset = (): void =>\n Object.getOwnPropertyNames(g).forEach((p) => delete g[p as keyof IdpcGlobal]);\n","import { idpcState } from \"./store\";\n\nexport interface IdGen {\n (): string;\n}\n\n/**\n * Generates a globally unique ID\n */\nexport const idGen = (prefix = \"idpc_\"): IdGen => () => {\n const g = idpcState();\n if (!g.idGen) g.idGen = {};\n if (g.idGen[prefix] === undefined) g.idGen[prefix] = 0;\n g.idGen[prefix] += 1;\n return `${prefix}${g.idGen[prefix]}`;\n};\n","import {\n isInputElem,\n update,\n change,\n isSelect,\n isInput,\n hasValue,\n optionsHasText,\n} from \"./input\";\nimport { toElem, contains } from \"./dom\";\nimport { updateCountry, toCountry } from \"./country\";\nimport { AnyAddress, GbrAddress, isGbrAddress } from \"./types\";\nimport {\n SelectorNode,\n Targets,\n OutputFields,\n UsaOutputFields,\n LineCount,\n NamedFields,\n} from \"./types\";\nimport { isString } from \"./string\";\nimport { cssEscape } from \"./util\";\n\n/**\n * Returns number of lines\n */\nexport const numberOfLines = (targets: Targets): LineCount => {\n const { line_2, line_3 } = targets;\n if (!line_2) return 1;\n if (!line_3) return 2;\n return 3;\n};\n\n/**\n * Removes empty fields and joins\n */\nexport const join = (list: (string | unknown)[]): string =>\n list\n .filter((e): e is string | NonNullable<unknown> => {\n if (isString(e)) return !!e.trim();\n return !!e;\n })\n .join(\", \");\n\nexport const charArrayLength = (arr: string[]): number =>\n arr.join(\" \").trim().length;\n\ninterface MaxLengthOptions extends MaxLineConfig {\n lineCount: number;\n}\n\n// Truncates string by max line length while preserving whole words and\n// returning both truncated string and remaining chunk\nconst truncate = (line: string, maxLength: number): [string, string] => {\n if (line.length <= maxLength) return [line, \"\"];\n const words = line.split(\" \");\n let truncated = \"\";\n let remaining = \"\";\n for (let i = 0; i < words.length; i++) {\n const word = words[i];\n if (truncated.length + word.length > maxLength) {\n remaining = words.slice(i).join(\" \");\n break;\n }\n truncated += `${word} `;\n }\n return [truncated.trim(), remaining.trim()];\n};\n\nconst prependLine = (remaining: string, nextLine: string): string => {\n if (nextLine.length === 0) return remaining;\n return `${remaining}, ${nextLine}`;\n};\n\n// Truncates address lines if limits are present\n// Note that\nexport const toMaxLengthLines = (\n l: string[],\n options: MaxLengthOptions\n): [string, string, string] => {\n const { lineCount, maxLineOne, maxLineTwo, maxLineThree } = options;\n const result: [string, string, string] = [\"\", \"\", \"\"];\n\n // Make copy of array because we're about to mutate it\n const lines = [...l];\n\n if (maxLineOne) {\n const [newLineOne, remaining] = truncate(lines[0], maxLineOne);\n result[0] = newLineOne;\n if (remaining) lines[1] = prependLine(remaining, lines[1]);\n if (lineCount === 1) return result;\n } else {\n result[0] = lines[0];\n if (lineCount === 1) return [join(lines), \"\", \"\"];\n }\n\n if (maxLineTwo) {\n const [newLineTwo, remaining] = truncate(lines[1], maxLineTwo);\n result[1] = newLineTwo;\n if (remaining) lines[2] = prependLine(remaining, lines[2]);\n if (lineCount === 2) return result;\n } else {\n result[1] = lines[1];\n if (lineCount === 2) return [result[0], join(lines.slice(1)), \"\"];\n }\n\n if (maxLineThree) {\n const [newLineThree, remaining] = truncate(lines[2], maxLineThree);\n result[2] = newLineThree;\n if (remaining) lines[3] = prependLine(remaining, lines[3]);\n } else {\n result[2] = lines[2];\n }\n\n return result;\n};\n\nexport type MaxLengths = [number?, number?, number?];\n\ninterface MaxLengthOptions {}\n\nexport const toAddressLines = (\n lineCount: LineCount,\n address: AnyAddress,\n options: MaxLineConfig\n): [string, string, string] => {\n const { line_1, line_2 } = address;\n const line_3 = \"line_3\" in address ? address.line_3 : \"\";\n\n // Right now we create a separate code path if maxLine is detected as\n // I'm not sure the tests have covered all cases\n // Original code path preserved below\n if (options.maxLineOne || options.maxLineTwo || options.maxLineThree)\n return toMaxLengthLines([line_1, line_2, line_3], {\n lineCount,\n ...options,\n });\n\n if (lineCount === 3) return [line_1, line_2, line_3];\n if (lineCount === 2) return [line_1, join([line_2, line_3]), \"\"];\n return [join([line_1, line_2, line_3]), \"\", \"\"];\n};\n\ntype KeysOfUnion<T> = T extends unknown ? keyof T : never;\n\n/**\n * Get address property value\n */\nexport const extract = (\n a: AnyAddress,\n attr: KeysOfUnion<AnyAddress>\n): string => {\n //@ts-ignore\n const result = a[attr];\n if (typeof result === \"number\") return result.toString();\n if (result === undefined) return \"\";\n return result;\n};\n\n/**\n * Retrives fields based on selectors, names and labels\n *\n * - Highest precedence is given to labels\n * - Next highest to names\n * - Next to outputfields\n */\nexport const getFields = (o: PopulateAddressOptions): OutputFields => ({\n ...searchFields(o.outputFields || {}, o.config.scope),\n ...searchNames(o.names || {}, o.config.scope),\n ...searchLabels(o.labels || {}, o.config.scope),\n});\n\nexport interface MaxLineConfig {\n lines?: LineCount;\n maxLineOne?: number;\n maxLineTwo?: number;\n maxLineThree?: number;\n}\n\nexport interface PopulateConfig extends MaxLineConfig {\n scope: HTMLElement | Document;\n removeOrganisation?: boolean;\n populateCounty?: boolean;\n}\n\nexport interface PopulateAddressOptions {\n outputFields: OutputFields;\n names?: NamedFields;\n labels?: NamedFields;\n address: AnyAddress;\n config: PopulateConfig;\n}\n\nexport interface MutateConfig\n extends MaxLineConfig,\n Pick<PopulateConfig, \"removeOrganisation\" | \"populateCounty\"> {\n populateCounty?: boolean;\n}\n\ninterface PopulateAddress {\n (options: PopulateAddressOptions): void;\n}\n\n// Retrieve DOM elements of form and return object with once which exists\nexport const searchFields = (\n outputFields: OutputFields,\n scope: HTMLElement | Document\n): OutputFields => {\n const fields: OutputFields = {};\n let key: keyof OutputFields;\n for (key in outputFields) {\n const value = outputFields[key];\n if (value === undefined) continue;\n const field = toElem(value, scope);\n if (isInputElem(field)) fields[key] = field;\n }\n return fields;\n};\n\n/**\n * Retrieve DOM elemebts by name or similar attributes\n * - Checks name first\n * - Checks aria-name second\n */\nexport const searchNames = (\n names: NamedFields,\n scope: HTMLElement | Document\n): OutputFields => {\n const result: OutputFields = {};\n let key: keyof NamedFields;\n for (key in names) {\n if (!names.hasOwnProperty(key)) continue;\n const name = names[key];\n // Assign by name if found first\n const named = toElem(`[name=\"${name}\"]`, scope);\n if (named) {\n result[key] = named as SelectorNode;\n continue;\n }\n // Fallback to aria-name\n const ariaNamed = toElem(`[aria-name=\"${name}\"]`, scope);\n if (ariaNamed) result[key] = ariaNamed as SelectorNode;\n }\n return result;\n};\n\n/**\n * Retrives DOM elements by their labels\n */\nexport const searchLabels = (\n labels: NamedFields,\n scope: HTMLElement | Document\n): OutputFields => {\n const result: Record<string, HTMLElement> = {};\n if (labels === undefined) return labels;\n let key: keyof NamedFields;\n for (key in labels) {\n if (!labels.hasOwnProperty(key)) continue;\n const name = labels[key];\n if (!name) continue;\n const first = contains(scope, \"label\", name);\n const label = toElem(first, scope);\n if (!label) continue;\n // If label match, check `for` attribute first\n const forEl = label.getAttribute(\"for\");\n if (forEl) {\n const byId = scope.querySelector(`#${cssEscape(forEl)}`);\n if (byId) {\n result[key] = byId as HTMLElement;\n continue;\n }\n }\n // Otherwise retrieve neareest input field\n const inner = label.querySelector(\"input\");\n if (inner) result[key] = inner;\n }\n return result;\n};\n\ninterface MutateAddress {\n (address: AnyAddress, config: MutateConfig): AnyAddress;\n}\n\nconst skipFields: string[] = [\"country\", \"country_iso_2\", \"country_iso\"];\n\n// Changes address according to config options\nexport const mutateAddress: MutateAddress = (address, config) => {\n if (isGbrAddress(address)) {\n if (config.removeOrganisation) removeOrganisation(address);\n }\n const [line_1, line_2, line_3] = toAddressLines(\n config.lines || 3,\n address,\n config\n );\n address.line_1 = line_1;\n address.line_2 = line_2;\n\n if (isGbrAddress(address)) address.line_3 = line_3;\n\n return address;\n};\n\n/**\n * Insert address values into output fields\n */\nexport const populateAddress: PopulateAddress = (options) => {\n const { config } = options;\n const fields: OutputFields | UsaOutputFields = getFields(options);\n if (config.lines === undefined)\n config.lines = numberOfLines(fields as unknown as Targets);\n const address = mutateAddress({ ...options.address }, config);\n const { scope, populateCounty } = config;\n\n const skip = [...skipFields];\n\n // Apply GBR address transforms\n if (isGbrAddress(address)) {\n if (config.removeOrganisation) removeOrganisation(address);\n if (populateCounty === false) skip.push(\"county\");\n }\n\n // Populate country, country iso2 and country iso3 from address being sure\n // to fix country name\n updateCountry(toElem(fields.country || null, scope), address);\n\n // Populate iso2 regardless of input or select\n const iso2Elem = toElem(fields.country_iso_2 || null, scope);\n if (isSelect(iso2Elem)) {\n if (hasValue(iso2Elem, address.country_iso_2)) {\n change({ e: iso2Elem, value: address.country_iso_2 });\n } else {\n // First try to find option with text matching ISO2 code\n let text = optionsHasText(iso2Elem, address.country_iso_2);\n if (text.length > 0) {\n change({ e: iso2Elem, value: text[0].value || \"\" });\n } else {\n // If not found, try to find option with text matching country name\n text = optionsHasText(iso2Elem, toCountry(address));\n if (text.length > 0) {\n change({ e: iso2Elem, value: text[0].value || \"\" });\n }\n }\n }\n }\n if (isInput(iso2Elem)) {\n update(iso2Elem, address.country_iso_2 || \"\");\n }\n\n // Populate iso3 regardless of input or select\n const iso3Elem = toElem(fields.country_iso || null, scope);\n if (isSelect(iso3Elem)) {\n if (hasValue(iso3Elem, address.country_iso)) {\n change({ e: iso3Elem, value: address.country_iso });\n } else {\n // First try to find option with text matching ISO3 code\n let text = optionsHasText(iso3Elem, address.country_iso);\n if (text.length > 0) {\n change({ e: iso3Elem, value: text[0].value || \"\" });\n } else {\n // If not found, try to find option with text matching country name\n text = optionsHasText(iso3Elem, toCountry(address));\n if (text.length > 0) {\n change({ e: iso3Elem, value: text[0].value || \"\" });\n }\n }\n }\n }\n if (isInput(iso3Elem)) update(iso3Elem, address.country_iso || \"\");\n\n const countyIso = toElem(getCountyIsoSelector(fields), scope);\n\n const countyIsoValue = getCountyIso(address);\n const countyValue = getCounty(address);\n if (isSelect(countyIso)) {\n if (hasValue(countyIso, countyIsoValue)) {\n change({ e: countyIso, value: countyIsoValue });\n } else if (hasValue(countyIso, countyValue || \"\")) {\n change({ e: countyIso, value: countyValue || \"\" });\n } else {\n let text = optionsHasText(countyIso, countyValue);\n if (text.length > 0) {\n change({ e: countyIso, value: text[0].value || \"\" });\n } else {\n text = optionsHasText(countyIso, countyIsoValue);\n if (text) change({ e: countyIso, value: text[0].value || \"\" });\n }\n }\n }\n if (isInput(countyIso)) {\n update(countyIso, countyIsoValue);\n }\n\n // Populate all other address fields\n let e: keyof typeof fields;\n for (e in fields) {\n if (skip.includes(e)) continue;\n if (e.startsWith(\"native.\")) {\n updateNative(e, fields, address, scope);\n continue;\n }\n //@ts-ignore\n if (address[e] === undefined) continue;\n if (fields.hasOwnProperty(e)) {\n const value = fields[e];\n if (!value) continue;\n update(toElem(value, scope), extract(address, e));\n }\n }\n};\n\n// Put all the ts-ignore nastiness relating to native in one place\nconst updateNative = (\n nativeAttr: string,\n fields: OutputFields | UsaOutputFields,\n address: any,\n scope: HTMLElement | Document\n) => {\n const e = nativeAttr.replace(\"native.\", \"\");\n //@ts-ignore\n const native = address.native;\n if (native === undefined) return;\n //@ts-ignore\n const nativeValue = native[e];\n if (nativeValue === undefined) return;\n if (fields.hasOwnProperty(nativeAttr)) {\n //@ts-ignore\n const selector = fields[nativeAttr];\n if (!selector) return;\n //@ts-ignore\n update(toElem(selector, scope), extract(native, e));\n }\n};\n\n/**\n * Mutates an address object to remove an organisation name from address\n * line and shift up other lines as necessary\n *\n * - Ignores if only premise identifier is the organisation name\n */\nexport const removeOrganisation = (address: GbrAddress): GbrAddress => {\n if (address.organisation_name.length === 0) return address;\n if (address.line_2.length === 0 && address.line_3.length === 0)\n return address;\n if (address.line_1 === address.organisation_name) {\n // Shift addresses up\n address.line_1 = address.line_2;\n address.line_2 = address.line_3;\n address.line_3 = \"\";\n }\n return address;\n};\n\nconst isUsaOutputFields = (\n a: OutputFields | UsaOutputFields\n): a is UsaOutputFields => a.hasOwnProperty(\"state_abbreviation\");\n\nexport const getCountyIsoSelector = (\n a: OutputFields | UsaOutputFields\n): SelectorNode | null => {\n if (isUsaOutputFields(a)) return a.state_abbreviation || null;\n return a.county_code || null;\n};\n\nexport const getCountySelector = (\n a: OutputFields | UsaOutputFields\n): SelectorNode | null => {\n if (isUsaOutputFields(a)) return a.state || null;\n return a.county || null;\n};\n\nexport const getCountyIso = (a: AnyAddress): string => {\n if (isGbrAddress(a)) return a.county_code;\n return a.state_abbreviation;\n};\n\nexport const getCounty = (a: AnyAddress): string => {\n if (isGbrAddress(a)) return a.county;\n return a.state;\n};\n","interface KeyCodeMapping {\n [key: number]: SupportedKey;\n}\n\ntype SupportedKey =\n | \"Enter\"\n | \"ArrowUp\"\n | \"ArrowDown\"\n | \"Home\"\n | \"End\"\n | \"Escape\"\n | \"Backspace\";\n\nexport const keyCodeMapping: KeyCodeMapping = {\n 13: \"Enter\",\n 38: \"ArrowUp\",\n 40: \"ArrowDown\",\n 36: \"Home\",\n 35: \"End\",\n 27: \"Escape\",\n 8: \"Backspace\",\n};\n\nexport const supportedKeys: string[] = [\n \"Enter\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"Home\",\n \"End\",\n \"Escape\",\n \"Backspace\",\n];\n\nconst supported = (k: string): k is SupportedKey =>\n supportedKeys.indexOf(k) !== -1;\n\nexport const toKey = (event: KeyboardEvent): SupportedKey | null => {\n if (event.keyCode) return keyCodeMapping[event.keyCode] || null;\n return supported(event.key) ? event.key : null;\n};\n","/**\n * Checks if `value` is the\n * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)\n * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)\n *\n * @example\n * _.isObject({});\n * // => true\n *\n * _.isObject([1, 2, 3]);\n * // => true\n *\n * _.isObject(_.noop);\n * // => true\n *\n * _.isObject(null);\n * // => false\n */\nexport const isObject = (value: any) => {\n var type = typeof value;\n return !!value && (type == \"object\" || type == \"function\");\n};\n\n/**\n * Creates a debounced function that delays invoking `func` until after `wait`\n * milliseconds have elapsed since the last time the debounced function was\n * invoked. The debounced function comes with a `cancel` method to cancel\n * delayed `func` invocations and a `flush` method to immediately invoke them.\n * Provide `options` to indicate whether `func` should be invoked on the\n * leading and/or trailing edge of the `wait` timeout. The `func` is invoked\n * with the last arguments provided to the debounced function. Subsequent\n * calls to the debounced function return the result of the last `func`\n * invocation.\n *\n * **Note:** If `leading` and `trailing` options are `true`, `func` is\n * invoked on the trailing edge of the timeout only if the debounced function\n * is invoked more than once during the `wait` timeout.\n *\n * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred\n * until to the next tick, similar to `setTimeout` with a timeout of `0`.\n *\n * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)\n * for details over the differences between `_.debounce` and `_.throttle`.\n *\n * @example\n *\n * // Avoid costly calculations while the window size is in flux.\n * jQuery(window).on('resize', _.debounce(calculateLayout, 150));\n *\n * // Invoke `sendMail` when clicked, debouncing subsequent calls.\n * jQuery(element).on('click', _.debounce(sendMail, 300, {\n * 'leading': true,\n * 'trailing': false\n * }));\n *\n * // Ensure `batchLog` is invoked once after 1 second of debounced calls.\n * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });\n * var source = new EventSource('/stream');\n * jQuery(source).on('message', debounced);\n *\n * // Cancel the trailing debounced invocation.\n * jQuery(window).on('popstate', debounced.cancel);\n */\n\nexport interface Options {\n leading?: boolean;\n maxWait?: number;\n trailing?: boolean;\n}\n\nexport const debounce = function (func: any, wait: number, options?: Options) {\n let lastArgs: any,\n lastThis: any,\n maxWait: any,\n result: any,\n timerId: any,\n lastCallTime: any;\n\n let lastInvokeTime = 0;\n let leading = false;\n let maxing = false;\n let trailing = true;\n\n if (typeof func !== \"function\") {\n throw new TypeError(\"Expected a function\");\n }\n wait = +wait || 0;\n if (isObject(options)) {\n // @ts-ignore\n leading = !!options.leading;\n // @ts-ignore\n maxing = \"maxWait\" in options;\n // @ts-ignore\n maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;\n // @ts-ignore\n trailing = \"trailing\" in options ? !!options.trailing : trailing;\n }\n\n function invokeFunc(time: any) {\n const args = lastArgs;\n const thisArg = lastThis;\n\n lastArgs = lastThis = undefined;\n lastInvokeTime = time;\n result = func.apply(thisArg, args);\n return result;\n }\n\n function leadingEdge(time: number) {\n // Reset any `maxWait` timer.\n lastInvokeTime = time;\n // Start the timer for the trailing edge.\n timerId = setTimeout(timerExpired, wait);\n // Invoke the leading edge.\n return leading ? invokeFunc(time) : result;\n }\n\n function remainingWait(time: number) {\n const timeSinceLastCall = time