UNPKG

react-loqate

Version:

This is a React implementation of the loqate APIs. It features an input, typing in which will result in a list of address options. Clicking an option will trigger your callback with that option.

451 lines (439 loc) 13.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.tsx var src_exports = {}; __export(src_exports, { default: () => src_default }); module.exports = __toCommonJS(src_exports); var import_clsx4 = __toESM(require("clsx"), 1); var import_react8 = __toESM(require("react"), 1); // src/components/DefaultInput.tsx var import_clsx = __toESM(require("clsx"), 1); var import_react = __toESM(require("react"), 1); var DefaultInput = (0, import_react.forwardRef)( (props, ref) => { const { className, ...rest } = props; return /* @__PURE__ */ import_react.default.createElement( "input", { className: (0, import_clsx.default)("react-loqate-input", className), ref, ...rest } ); } ); var DefaultInput_default = DefaultInput; // src/components/DefaultList.tsx var import_clsx2 = __toESM(require("clsx"), 1); var import_react2 = __toESM(require("react"), 1); var DefaultList = (0, import_react2.forwardRef)( (props, ref) => { const { className, ...rest } = props; return /* @__PURE__ */ import_react2.default.createElement( "ul", { className: (0, import_clsx2.default)("react-loqate-default-list", className), ref, ...rest } ); } ); var DefaultList_default = DefaultList; // src/components/DefaultListItem.tsx var import_clsx3 = __toESM(require("clsx"), 1); var import_react3 = __toESM(require("react"), 1); var DefaultListItem = (props) => { const { className, value, suggestion, onKeyDown, ...rest } = props; const regex = new RegExp( `(.*)(${value == null ? void 0 : value.toString().replace(/\W/g, "")})(.*)`, "i" ); const search = `${suggestion.Text} ${suggestion.Description}`; const result = search.match(regex); const [before, match, after] = (result == null ? void 0 : result.slice(1)) ?? ["", "", ""]; return /* @__PURE__ */ import_react3.default.createElement( "li", { className: (0, import_clsx3.default)("react-loqate-list-item", className), "aria-label": before + match + after, onKeyDown: (e) => { onKeyDown == null ? void 0 : onKeyDown(e); if (e.key === "ArrowDown") { e.preventDefault(); const listItem = e.target; const next = listItem.nextSibling; if (next) { next.focus(); } } if (e.key === "ArrowUp") { e.preventDefault(); const listItem = e.target; const previous = listItem.previousSibling; if (previous) { previous.focus(); } } }, ...rest }, (result == null ? void 0 : result.length) === 4 ? /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, before, /* @__PURE__ */ import_react3.default.createElement("strong", null, match), after) : search ); }; var DefaultListItem_default = DefaultListItem; // src/utils/ClickAwayListener.tsx var import_react4 = require("react"); function ClickAwayListener({ children, onClickAway }) { const nodeRef = (0, import_react4.useRef)(null); (0, import_react4.useEffect)(() => { const handleClickAway = (event) => { const target = event.target; if (nodeRef.current && !nodeRef.current.contains(target)) { onClickAway(); } }; document.addEventListener("click", handleClickAway, true); document.addEventListener("mousedown", handleClickAway, true); document.addEventListener("touchstart", handleClickAway, true); return () => { document.removeEventListener("click", handleClickAway, true); document.removeEventListener("mousedown", handleClickAway, true); document.removeEventListener("touchstart", handleClickAway, true); }; }, [onClickAway]); return (0, import_react4.cloneElement)(children, { ref: nodeRef }); } // src/constants/loqate.ts var LOQATE_BASE_URL = "https://api.addressy.com/Capture/Interactive"; var LOQATE_RETRIEVE_URL = "Retrieve/v1.2/json3.ws"; var LOQATE_FIND_URL = "Find/v1.10/json3.ws"; // src/error.ts var ReactLoqateError = class extends Error { code; constructor({ message, code }) { super(message); this.code = code; } }; var LoqateError = class extends Error { Cause; Description; Error; Resolution; constructor({ Description, Cause, Error: Error2, Resolution }) { super(Description); this.Cause = Cause; this.Description = Description; this.Error = Error2; this.Resolution = Resolution; } }; // src/utils/Loqate.ts var Loqate = class _Loqate { constructor(key, baseUrl = LOQATE_BASE_URL) { this.key = key; this.baseUrl = baseUrl; } static create(key, baseUrl = LOQATE_BASE_URL) { return new _Loqate(key, baseUrl); } async retrieve(id) { var _a; const params = new URLSearchParams({ Id: id, Key: this.key }); const url = `${this.baseUrl}/${LOQATE_RETRIEVE_URL}?${params.toString()}`; const res = await fetch(url).then((r) => r.json()); const noLoqateErrosRes = this.handleErrors(res); if (noLoqateErrosRes.Items && !((_a = noLoqateErrosRes.Items) == null ? void 0 : _a.length)) { throw new ReactLoqateError({ code: "NO_ITEMS_RETRIEVED", message: `Loqate retrieve API did not return any address items for the provided ID ${id}` }); } return noLoqateErrosRes; } async find(query) { const { text, countries = [], containerId, language, limit, origin, bias } = query; const params = new URLSearchParams({ Text: text, Countries: countries.join(","), language, Key: this.key, Origin: origin ? origin : "", Bias: bias ? "true" : "false" }); if (containerId) { params.set("Container", containerId); } if (limit) { params.set("limit", limit.toString()); } const url = `${this.baseUrl}/${LOQATE_FIND_URL}?${params.toString()}`; const response = await fetch(url).then((r) => r.json()); return this.handleErrors(response); } handleErrors = (res) => { var _a; const firstItem = (_a = res == null ? void 0 : res.Items) == null ? void 0 : _a[0]; if (firstItem && Object.hasOwn(firstItem, "Error")) { throw new LoqateError(firstItem); } return res; }; }; var Loqate_default = Loqate; // src/utils/Portal.tsx var import_react5 = __toESM(require("react"), 1); var import_react_dom = require("react-dom"); function Portal({ children, container = document.body, disablePortal = false }) { const [mounted, setMounted] = (0, import_react5.useState)(false); (0, import_react5.useEffect)(() => { setMounted(true); }, []); if (disablePortal) { return /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, children); } if (!mounted || !container || children == null) { return null; } return (0, import_react_dom.createPortal)(children, container); } // src/utils/useDebounceEffect.ts var import_react6 = require("react"); function useDebounceEffect(effect, delay, deps) { const callback = (0, import_react6.useCallback)(effect, deps); (0, import_react6.useEffect)(() => { if (!delay) { callback(); return; } const handler = setTimeout(() => { callback(); }, delay); return () => { clearTimeout(handler); }; }, [callback, delay]); } var useDebounceEffect_default = useDebounceEffect; // src/utils/usePreserveFocus.ts var import_react7 = require("react"); function usePreserveFocus() { const elementRef = (0, import_react7.useRef)(null); const [shouldRestoreFocus, setShouldRestoreFocus] = (0, import_react7.useState)(false); const preserveFocus = () => { if (elementRef.current && document.activeElement === elementRef.current) { setShouldRestoreFocus(true); } }; (0, import_react7.useLayoutEffect)(() => { if (shouldRestoreFocus && elementRef.current) { Promise.resolve().then(() => { if (elementRef.current) { elementRef.current.focus(); } }); setShouldRestoreFocus(false); } }); return { elementRef, preserveFocus }; } var usePreserveFocus_default = usePreserveFocus; // src/index.tsx var loqateLanguage = (language) => { const [languageCode] = language.replace("_", "-").split("-"); return languageCode; }; function AddressSearch(props) { var _a; const { locale, countries, onSelect, limit, apiKey, classes, components, inline, debounce, apiUrl, bias, origin, disableBrowserAutocomplete = true } = props; const loqate = (0, import_react8.useMemo)(() => Loqate_default.create(apiKey, apiUrl), [apiKey]); const [suggestions, setSuggestions] = (0, import_react8.useState)([]); const [value, setValue] = (0, import_react8.useState)(""); const [, setError] = (0, import_react8.useState)(null); const { elementRef: anchorRef, preserveFocus } = usePreserveFocus_default(); const rect = (_a = anchorRef.current) == null ? void 0 : _a.getBoundingClientRect(); async function find(text, containerId) { let Items = []; try { const res = await loqate.find({ countries, limit, text, containerId, language: loqateLanguage(locale), origin, bias }); if (res.Items) { Items = res.Items; } } catch (e) { setError(() => { throw e; }); } return Items; } async function selectSuggestion({ Type, Id }) { if (Type === "Address") { let Items = []; try { const res = await loqate.retrieve(Id); if (res.Items) { Items = res.Items; } } catch (e) { setSuggestions([]); setError(() => { throw e; }); } onSelect(Items[0]); setSuggestions([]); return; } const items = await find(value, Id); setSuggestions(items); } async function handleChange({ target }) { const { value: search } = target; preserveFocus(); setValue(search); } useDebounceEffect_default( () => { if (value === "") { setSuggestions([]); return; } find(value).then(setSuggestions); }, debounce, [value] ); const Input = (components == null ? void 0 : components.Input) ?? DefaultInput_default; const List = (components == null ? void 0 : components.List) ?? DefaultList_default; const ListItem = (components == null ? void 0 : components.ListItem) ?? DefaultListItem_default; return /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, /* @__PURE__ */ import_react8.default.createElement( Input, { ref: anchorRef, className: (0, import_clsx4.default)(classes == null ? void 0 : classes.input), onChange: handleChange, value, autoComplete: disableBrowserAutocomplete ? "react-loqate-address-search" : void 0, onKeyDown: (e) => { if (e.key === "Escape") { setSuggestions([]); } } } ), /* @__PURE__ */ import_react8.default.createElement(Portal, { container: document.body, disablePortal: inline }, /* @__PURE__ */ import_react8.default.createElement(ClickAwayListener, { onClickAway: () => setSuggestions([]) }, /* @__PURE__ */ import_react8.default.createElement( List, { style: { position: "absolute", top: rect ? (rect.y ?? 0) + rect.height + window.scrollY : 0, left: (rect == null ? void 0 : rect.left) ?? 0, width: (rect == null ? void 0 : rect.width) ?? void 0 }, hidden: !suggestions.length, className: classes == null ? void 0 : classes.list }, suggestions.map((suggestion, i) => /* @__PURE__ */ import_react8.default.createElement( ListItem, { key: suggestion.Id + i, onClick: () => selectSuggestion(suggestion), onKeyDown: (e) => { if (e.key === "Enter") { selectSuggestion(suggestion); } if (e.key === "Escape") { setSuggestions([]); } }, className: classes == null ? void 0 : classes.listItem, value, suggestion, tabIndex: i === 0 ? 0 : -1 }, suggestion.Text, " ", suggestion.Description )) )))); } var src_default = AddressSearch; //# sourceMappingURL=index.cjs.map