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
JavaScript
;
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