@ideal-postcodes/address-finder
Version:
Address Finder JS library backed by the Ideal Postcodes UK address search API
929 lines (928 loc) • 32.6 kB
JavaScript
"use strict";
/**
* @module Controller
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setPositionFixed = exports.findOrCreate = exports._onCountryChange = exports._onKeyDown = exports.Controller = exports.defaults = exports.NOOP = void 0;
/* eslint-disable no-invalid-this */
const announcer_1 = require("./announcer");
const debounce_1 = __importDefault(require("lodash/debounce"));
const contexts_1 = require("./contexts");
const cache_1 = require("./cache");
const css_1 = require("./css");
const core_axios_1 = require("@ideal-postcodes/core-axios");
const fsm_1 = require("@xstate/fsm");
const state_1 = require("./state");
const jsutil_1 = require("@ideal-postcodes/jsutil");
/**
* @hidden
*/
const NOOP = () => { };
exports.NOOP = NOOP;
/**
* Default options assigned to controller instances
*/
exports.defaults = {
// DOM
outputScope: null,
// Client
apiKey: "",
checkKey: true,
// WAI-ARIA compliance settings
aria: "1.0",
// Behaviour
titleizePostTown: true,
format: "gbr",
outputFields: {},
names: {},
labels: {},
removeOrganisation: false,
injectStyle: true,
inputField: "",
autocomplete: "none",
populateCounty: true,
populateOrganisation: true,
queryOptions: {},
resolveOptions: {},
alignToInput: true,
offset: 2,
hideToolbar: false,
detectCountry: true,
// Country
defaultCountry: "GBR",
restrictCountries: [],
contexts: contexts_1.defaultContexts,
// Messages
msgFallback: "Please enter your address manually",
msgInitial: "Start typing to find address",
msgNoMatch: "No matches found",
msgList: "Select your address",
msgCountryToggle: "Change Country",
// Placeholder Messages
msgPlaceholder: "Type the first line or postal code of your address",
msgPlaceholderCountry: "Select your country",
// View classes
messageClass: "idpc_error",
containerClass: "idpc_autocomplete",
mainClass: "idpc_af",
listClass: "idpc_ul",
toolbarClass: "idpc_toolbar",
countryToggleClass: "idpc_country",
// Syles
mainStyle: {},
inputStyle: {},
listStyle: {},
liStyle: {},
containerStyle: {},
// Hide / unhide
unhide: null,
unhideClass: "idpc-unhide",
msgUnhide: "Enter address manually",
hide: [],
//change list position
fixed: false,
// Callbacks
onOpen: exports.NOOP,
onSelect: exports.NOOP,
onBlur: exports.NOOP,
onClose: exports.NOOP,
onFocus: exports.NOOP,
onInput: exports.NOOP,
onLoaded: exports.NOOP,
onSearchError: exports.NOOP,
onSuggestionError: exports.NOOP,
onMounted: exports.NOOP,
onRemove: exports.NOOP,
onSuggestionsRetrieved: exports.NOOP,
onAddressSelected: exports.NOOP,
onAddressRetrieved: exports.NOOP,
onAddressPopulated: exports.NOOP,
onFailedCheck: exports.NOOP,
onMouseDown: exports.NOOP,
onKeyDown: exports.NOOP,
onUnhide: exports.NOOP,
onCountrySelected: exports.NOOP,
onContextChange: exports.NOOP,
};
/**
* # Controller
*
* The Autocomplete Controller class acts as the public class which you may
* wield to enable address autocomplete on your HTML address forms
*
* When instantiated, the controller will serve as a bridge beteen the
* address suggestion view presented on the DOM and the Ideal
* Postcodes Address resolution HTTP APIs
*
* The role of the controller is to bind to events produced by the user
* interface and take appropriate action including querying the API,
* modifying other aspects of the DOM.
*/
class Controller {
constructor(options) {
this.options = {
...{ scope: window.document, document: window.document },
...exports.defaults,
...options,
};
// Default inputField to line_1 if `inputField` not specified
if (!options.inputField)
this.options.inputField = this.options.outputFields.line_1 || "";
// To overcome config overload - idpcConfig global config object already
// defines autocomplete (boolean)
//@ts-ignore
if (this.options.autocomplete === true)
this.options.autocomplete = exports.defaults.autocomplete;
// Scope the operations of this controller to a document or DOM subtree
this.scope = (0, jsutil_1.getScope)(this.options.scope);
// Assign a parent Document for elem creation
this.document = (0, jsutil_1.getDocument)(this.scope);
// Assign a document or DOM subtree to scope outputs. Defaults to controller scope
this.outputScope = (0, exports.findOrCreate)(this.scope, this.options.outputScope, () => this.scope);
// Initialise state
this.context = this.options.defaultCountry;
this.notification = this.options.msgInitial;
this.current = -1;
this.suggestions = [];
this.contextSuggestions = [];
this.updateContexts(this.options.contexts);
this.client = new core_axios_1.Client({ ...this.options, api_key: this.options.apiKey });
this.cache = new cache_1.ApiCache(this.client);
this.retrieveSuggestions = (0, debounce_1.default)((event) => {
this.options.onInput.call(this, event);
const query = this.query();
if (query.trim().length === 0) {
this.setMessage(this.options.msgInitial);
return Promise.resolve(this);
}
return this.cache
.query(query, {
...this.options.queryOptions,
context: this.context,
})
.then((suggestions) => {
this.options.onSuggestionsRetrieved.call(this, suggestions);
return this.setSuggestions(suggestions, query);
})
.catch((error) => {
if (this.query() === query)
this.setMessage(this.options.msgFallback);
this.options.onSuggestionError.call(this, error);
return this;
});
}, 100, {
leading: true,
trailing: true,
maxWait: 100,
});
this.ids = (0, jsutil_1.idGen)("idpcaf");
// Configure container
this.container = this.options.document.createElement("div");
this.container.className = this.options.containerClass;
this.container.id = this.ids();
this.container.setAttribute("aria-haspopup", "listbox");
// Create message element
this.message = this.options.document.createElement("li");
this.message.textContent = this.options.msgInitial;
this.message.className = this.options.messageClass;
// Create button to toggle country selection
this.countryToggle = this.options.document.createElement("span");
this.countryToggle.className = this.options.countryToggleClass;
this.countryToggle.addEventListener("mousedown", _onCountryToggle(this));
this.countryIcon = this.options.document.createElement("span");
this.countryIcon.className = "idpc_icon";
this.countryIcon.innerText = this.currentContext().emoji;
this.countryMessage = this.options.document.createElement("span");
this.countryMessage.innerText = "Select Country";
this.countryMessage.className = "idpc_country";
this.countryToggle.appendChild(this.countryMessage);
this.countryToggle.appendChild(this.countryIcon);
// Create toolbar (for country selection)
this.toolbar = this.options.document.createElement("div");
this.toolbar.className = this.options.toolbarClass;
this.toolbar.appendChild(this.countryToggle);
if (this.options.hideToolbar)
(0, jsutil_1.hide)(this.toolbar);
// Configure UL
this.list = this.options.document.createElement("ul");
this.list.className = this.options.listClass;
this.list.id = this.ids();
this.list.setAttribute("aria-label", this.options.msgList);
this.list.setAttribute("role", "listbox");
this.mainComponent = this.options.document.createElement("div");
this.mainComponent.appendChild(this.list);
this.mainComponent.appendChild(this.toolbar);
this.mainComponent.className = this.options.mainClass;
(0, jsutil_1.hide)(this.mainComponent);
//configure unhide
this.unhideEvent = this.unhideFields.bind(this);
this.unhide = this.createUnhide();
// Configure input
let input;
if ((0, jsutil_1.isString)(this.options.inputField)) {
input = this.scope.querySelector(this.options.inputField);
}
else {
input = this.options.inputField;
}
if (!input)
throw new Error("Address Finder: Unable to find valid input field");
this.input = input;
this.input.setAttribute("autocomplete", this.options.autocomplete);
this.input.setAttribute("aria-autocomplete", "list");
this.input.setAttribute("aria-controls", this.list.id);
this.input.setAttribute("aria-autocomplete", "list");
this.input.setAttribute("aria-activedescendant", "");
this.input.setAttribute("autocorrect", "off");
this.input.setAttribute("autocapitalize", "off");
this.input.setAttribute("spellcheck", "false");
if (!this.input.id)
this.input.id = this.ids();
const countryInput = this.scope.querySelector(this.options.outputFields.country);
this.countryInput = countryInput;
// Apply additional accessibility improvments
this.ariaAnchor().setAttribute("role", "combobox");
this.ariaAnchor().setAttribute("aria-expanded", "false");
this.ariaAnchor().setAttribute("aria-owns", this.list.id);
this.placeholderCache = this.input.placeholder;
// Create listeners
this.inputListener = _onInput(this);
this.blurListener = _onBlur(this);
this.focusListener = _onFocus(this);
this.keydownListener = (0, exports._onKeyDown)(this);
this.countryListener = (0, exports._onCountryChange)(this);
const { container, announce } = (0, announcer_1.announcer)({
idA: this.ids(),
idB: this.ids(),
document: this.options.document,
});
this.announce = announce;
this.alerts = container;
this.inputStyle = (0, jsutil_1.setStyle)(this.input, this.options.inputStyle);
(0, jsutil_1.setStyle)(this.container, this.options.containerStyle);
(0, jsutil_1.setStyle)(this.list, this.options.listStyle);
// Apply an offset based off any margin
const offset = (0, css_1.computeOffset)(this);
(0, jsutil_1.setStyle)(this.mainComponent, {
...offset,
...this.options.mainStyle,
});
this.fsm = (0, state_1.create)({ c: this });
this.init();
}
/**
* Sets placeholder and caches previous result
* @hidden
*/
setPlaceholder(msg) {
this.input.placeholder = msg;
}
/**
* Unsets any placeholder value to original
* @hidden
*/
unsetPlaceholder() {
if (this.placeholderCache === undefined)
return this.input.removeAttribute("placeholder");
this.input.placeholder = this.placeholderCache;
}
/**
* Returns current highlighted context
* @hidden
*/
currentContext() {
const c = this.options.contexts[this.context];
if (c)
return c;
const first = Object.keys(this.options.contexts)[0];
return this.options.contexts[first];
}
/**
* Binds to DOM and begin DOM mutations
* @hidden
*/
load() {
this.attach();
(0, css_1.addStyle)(this);
if (this.options.fixed) {
//set position fixed and width calculation
(0, exports.setPositionFixed)(this.mainComponent, this.container, this.document);
}
this.options.onLoaded.call(this);
//fix for safari scrollbar that close the list
this.list.parentNode?.addEventListener("mousedown", (e) => e.preventDefault());
}
/**
* Attaches Controller to the DOM.
*
* If `checkKey` is enabled, a key check will be performed prioer to binding. Use the `onLoaded` and `onFailedCheck` callbacks to define follow up behaviour if the key check succeeds or fails
*/
init() {
return new Promise((resolve) => {
if (!this.options.checkKey) {
this.load();
resolve();
return;
}
(0, core_axios_1.checkKeyUsability)({ client: this.client, api_key: this.options.apiKey })
.then((response) => {
if (!response.available)
throw new Error("Key currently not usable");
this.updateContexts(
// TODO: Remove cast when openapi updated
(0, contexts_1.toContextMap)(response.contexts));
// Methods to apply context
// 1. If detect country enabled and match, if no match
// 2. Apply default context, if no match
// 3. Apply first item of context list
// If detect country enabled, set country to default
const details = this.options.contexts[response.context];
if (this.options.detectCountry && details) {
this.applyContext(details, false);
}
else {
this.applyContext(this.currentContext(), false);
}
this.load();
resolve();
})
.catch((error) => {
this.options.onFailedCheck.call(this, error);
resolve();
});
});
}
// Updates lists of available contexts
updateContexts(contexts) {
this.contextSuggestions = (0, contexts_1.toContextList)(contexts, this.options.restrictCountries);
this.options.contexts = contexts;
}
filteredContexts() {
const q = this.query();
if (q.trim().length === 0)
return this.contextSuggestions;
const f = q.toLowerCase().trim().replace(/\s+/g, " ");
const regexp = new RegExp("^" + f);
return this.contextSuggestions.filter((e) => {
if (regexp.test(e.description.toLowerCase()))
return true;
if (e.iso_2.toLowerCase() === f)
return true;
if (e.iso_3.toLowerCase() === f)
return true;
return false;
});
}
/**
* Render available country options
*/
renderContexts() {
this.list.innerHTML = "";
this.filteredContexts().forEach((contextDetails, i) => {
const { description } = contextDetails;
const li = this.options.document.createElement("li");
li.textContent = description;
li.setAttribute("aria-selected", "false");
li.setAttribute("tabindex", "-1");
li.setAttribute("aria-posinset", `${i + 1}`);
li.setAttribute("aria-setsize", this.contextSuggestions.length.toString());
li.setAttribute("role", "option");
(0, jsutil_1.setStyle)(li, this.options.liStyle);
li.addEventListener("mousedown", (e) => {
e.preventDefault();
this.options.onMouseDown.call(this, e);
this.fsm.send({ type: "SELECT_COUNTRY", contextDetails });
});
li.id = `${this.list.id}_${i}`;
this.list.appendChild(li);
});
this.announce(`${this.contextSuggestions.length} countries available`);
}
/**
* Render current address suggestions
*/
renderSuggestions() {
this.list.innerHTML = "";
const s = this.suggestions;
s.forEach((suggestion, i) => {
const li = this.options.document.createElement("li");
li.textContent = suggestion.suggestion;
li.setAttribute("aria-selected", "false");
li.setAttribute("tabindex", "-1");
li.setAttribute("title", suggestion.suggestion);
li.setAttribute("aria-posinset", `${i + 1}`);
li.setAttribute("aria-setsize", s.length.toString());
li.setAttribute("role", "option");
(0, jsutil_1.setStyle)(li, this.options.liStyle);
li.addEventListener("mousedown", (e) => {
e.preventDefault();
this.options.onMouseDown.call(this, e);
this.fsm.send({ type: "SELECT_ADDRESS", suggestion });
});
li.id = `${this.list.id}_${i}`;
this.list.appendChild(li);
});
this.announce(`${s.length} addresses available`);
}
/**
* Updates current li in list to active descendant
*/
goToCurrent() {
const lis = this.list.children;
this.input.setAttribute("aria-activedescendant", "");
for (let i = 0; i < lis.length; i += 1) {
if (i === this.current) {
this.input.setAttribute("aria-activedescendant", lis[i].id);
lis[i].setAttribute("aria-selected", "true");
this.goto(i);
}
else {
lis[i].setAttribute("aria-selected", "false");
}
}
}
/**
* Marks aria component as opened
*/
ariaExpand() {
this.ariaAnchor().setAttribute("aria-expanded", "true");
}
/**
* Marks aria component as closed
*/
ariaContract() {
this.ariaAnchor().setAttribute("aria-expanded", "false");
}
/**
* Resolves a suggestion to full address and apply results to form
*/
applySuggestion(suggestion) {
this.options.onSelect.call(this, suggestion);
this.options.onAddressSelected.call(this, suggestion);
this.announce(`The address ${suggestion.suggestion} has been applied to this form`);
return this.cache
.resolve(suggestion, this.options.format, this.options.resolveOptions)
.then((address) => {
if (address === null)
throw "Unable to retrieve address";
this.options.onAddressRetrieved.call(this, address);
this.populateAddress(address);
return this;
})
.catch((error) => {
this.open();
this.setMessage(this.options.msgFallback);
this.options.onSearchError.call(this, error);
return error;
});
}
/**
* Writes a selected to the input fields specified in the controller config
*/
populateAddress(address) {
this.unhideFields();
(0, jsutil_1.populateAddress)({
address,
config: { ...this.options, scope: this.outputScope },
outputFields: this.options.outputFields,
names: this.options.names,
labels: this.options.labels,
});
this.options.onAddressPopulated.call(this, address);
}
/**
* Applies new query options to search. This process clears the existing
* cache to prevent stale searches
*/
setQueryOptions(options) {
this.cache.clear();
this.options.queryOptions = options;
}
/**
* Applies new query options to search. This process clears the existing
* cache to prevent stale searches
*/
setResolveOptions(options) {
this.cache.clear();
this.options.resolveOptions = options;
}
/**
* Adds Address Finder to DOM
* - Wraps input with container
* - Appends suggestion list to container
* - Enables listeners
* - Starts FSM
*/
attach() {
if (this.fsm.status === fsm_1.InterpreterStatus.Running)
return this;
this.input.addEventListener("input", this.inputListener);
this.input.addEventListener("blur", this.blurListener);
this.input.addEventListener("focus", this.focusListener);
this.input.addEventListener("keydown", this.keydownListener);
if (this.countryInput)
this.countryInput.addEventListener("change", this.countryListener);
const parent = this.input.parentNode;
if (parent) {
// Wrap input in a div and append suggestion list
parent.insertBefore(this.container, this.input);
this.container.appendChild(this.input);
this.container.appendChild(this.mainComponent);
this.container.appendChild(this.alerts);
if (this.options.hide.length > 0 && this.options.unhide == null)
this.container.appendChild(this.unhide);
}
this.fsm.start();
this.options.onMounted.call(this);
this.hideFields();
return this;
}
/**
* Removes Address Finder from DOM
* - Disable listeners
* - Removes sugestion list from container
* - Appends suggestion list to container
* - Enables listeners
* - Stops FSM
*/
detach() {
if (this.fsm.status !== fsm_1.InterpreterStatus.Running)
return this;
this.input.removeEventListener("input", this.inputListener);
this.input.removeEventListener("blur", this.blurListener);
this.input.removeEventListener("focus", this.focusListener);
this.input.removeEventListener("keydown", this.keydownListener);
if (this.countryInput)
this.countryInput.removeEventListener("change", this.countryListener);
this.container.removeChild(this.mainComponent);
this.container.removeChild(this.alerts);
const parent = this.container.parentNode;
if (parent) {
parent.insertBefore(this.input, this.container);
parent.removeChild(this.container);
}
this.unmountUnhide();
this.unhideFields();
this.fsm.stop();
(0, jsutil_1.restoreStyle)(this.input, this.inputStyle);
this.options.onRemove.call(this);
this.unsetPlaceholder();
return this;
}
/**
* Sets message as a list item, no or empty string removes any message
*/
setMessage(notification) {
this.fsm.send({ type: "NOTIFY", notification });
return this;
}
/**
* Returns HTML Element which recevies key aria attributes
*
* @hidden
*/
ariaAnchor() {
if (this.options.aria === "1.0")
return this.input;
return this.container;
}
/**
* Returns current address query
*/
query() {
return this.input.value;
}
clearInput() {
(0, jsutil_1.update)(this.input, "");
}
/**
* Set address finder suggestions
*/
setSuggestions(suggestions, query) {
if (query !== this.query())
return this;
if (suggestions.length === 0)
return this.setMessage(this.options.msgNoMatch);
this.fsm.send({ type: "SUGGEST", suggestions });
return this;
}
/**
* Close address finder
*/
close(reason = "blur") {
(0, jsutil_1.hide)(this.mainComponent);
if (reason === "esc")
(0, jsutil_1.update)(this.input, "");
this.options.onClose.call(this, reason);
}
/**
* Updates suggestions and resets current selection
* @hidden
*/
updateSuggestions(s) {
this.suggestions = s;
this.current = -1;
}
/**
* Applies context to API cache
* @hidden
*/
applyContext(details, announce = true) {
const context = details.iso_3;
this.context = context;
this.cache.clear();
this.countryIcon.innerText = details.emoji;
if (announce)
this.announce(`Country switched to ${details.description}`);
this.options.onContextChange.call(this, context);
}
/**
* Renders notification box
* @hidden
*/
renderNotice() {
this.list.innerHTML = "";
this.input.setAttribute("aria-activedescendant", "");
this.message.textContent = this.notification;
this.announce(this.notification);
this.list.appendChild(this.message);
}
/**
* Open address finder
* @hidden
*/
open() {
(0, jsutil_1.show)(this.mainComponent);
this.options.onOpen.call(this);
}
/**
* Sets next suggestion as current
* @hidden
*/
next() {
if (this.current + 1 > this.list.children.length - 1) {
// Goes over edge of list and back to start
this.current = 0;
}
else {
this.current += 1;
}
return this;
}
/**
* Sets previous suggestion as current
* @hidden
*/
previous() {
if (this.current - 1 < 0) {
this.current = this.list.children.length - 1; // Wrap to last elem
}
else {
this.current += -1;
}
return this;
}
/**
* Given a HTMLLiElement, scroll parent until it is in view
* @hidden
*/
scrollToView(li) {
const liOffset = li.offsetTop;
const ulScrollTop = this.list.scrollTop;
if (liOffset < ulScrollTop) {
this.list.scrollTop = liOffset;
}
const ulHeight = this.list.clientHeight;
const liHeight = li.clientHeight;
if (liOffset + liHeight > ulScrollTop + ulHeight) {
this.list.scrollTop = liOffset - ulHeight + liHeight;
}
return this;
}
/**
* Moves currently selected li into view
* @hidden
*/
goto(i) {
const lis = this.list.children;
const suggestion = lis[i];
if (i > -1 && lis.length > 0) {
this.scrollToView(suggestion);
}
else {
this.scrollToView(lis[0]);
}
return this;
}
/**
* Returns true if address finder is open
*/
opened() {
return !this.closed();
}
/**
* Returs false if address finder is closed
*/
closed() {
return this.fsm.state.matches("closed");
}
/**
* Creates a clickable element that can trigger unhiding of fields
*/
createUnhide() {
const e = (0, exports.findOrCreate)(this.scope, this.options.unhide, () => {
const e = this.options.document.createElement("p");
e.innerText = this.options.msgUnhide;
e.setAttribute("role", "button");
e.setAttribute("tabindex", "0");
if (this.options.unhideClass)
e.className = this.options.unhideClass;
return e;
});
e.addEventListener("click", this.unhideEvent);
return e;
}
/**
* Removes unhide elem from DOM
*/
unmountUnhide() {
this.unhide.removeEventListener("click", this.unhideEvent);
if (this.options.unhide == null && this.options.hide.length)
(0, jsutil_1.remove)(this.unhide);
}
hiddenFields() {
return this.options.hide
.map((e) => {
if ((0, jsutil_1.isString)(e))
return (0, jsutil_1.toHtmlElem)(this.options.scope, e);
return e;
})
.filter((e) => e !== null);
}
/**
* Hides fields marked for hiding
*/
hideFields() {
this.hiddenFields().forEach(jsutil_1.hide);
}
/**
* Unhides fields marked for hiding
*/
unhideFields() {
this.hiddenFields().forEach(jsutil_1.show);
this.options.onUnhide.call(this);
}
}
exports.Controller = Controller;
/**
* Event handler: Fires when focus moves away from input field
* @hidden
*/
const _onBlur = (c) => function () {
c.options.onBlur.call(c);
c.fsm.send({ type: "CLOSE", reason: "blur" });
};
/**
* Event handler: Fires when input field is focused
* @hidden
*/
const _onFocus = (c) => function (_) {
c.options.onFocus.call(c);
c.fsm.send("AWAKE");
};
/**
* Event handler: Fires when input is detected on input field
* @hidden
*/
const _onInput = (c) => function (event) {
if (c.query().toLowerCase() === ":c") {
(0, jsutil_1.update)(c.input, "");
return c.fsm.send({ type: "CHANGE_COUNTRY" });
}
c.fsm.send({ type: "INPUT", event });
};
/**
* Event handler: Fires when country selection is clicked
* Triggers:
* - Country selection menu
*
* @hidden
*/
const _onCountryToggle = (c) => function (e) {
e.preventDefault();
c.fsm.send({ type: "CHANGE_COUNTRY" });
};
/**
* Event handler: Fires on "keyDown" event of search field
* @hidden
*/
const _onKeyDown = (c) => function (event) {
// Dispatch events based on keys
const key = (0, jsutil_1.toKey)(event);
if (key === "Enter")
event.preventDefault();
c.options.onKeyDown.call(c, event);
if (c.closed())
return c.fsm.send("AWAKE");
// When suggesting country
if (c.fsm.state.matches("suggesting_country")) {
if (key === "Enter") {
const contextDetails = c.filteredContexts()[c.current];
if (contextDetails)
c.fsm.send({ type: "SELECT_COUNTRY", contextDetails });
}
if (key === "Backspace")
c.fsm.send({ type: "INPUT", event });
if (key === "ArrowUp") {
event.preventDefault();
c.fsm.send("PREVIOUS");
}
if (key === "ArrowDown") {
event.preventDefault();
c.fsm.send("NEXT");
}
}
// When suggesting address
if (c.fsm.state.matches("suggesting")) {
if (key === "Enter") {
const suggestion = c.suggestions[c.current];
if (suggestion)
c.fsm.send({ type: "SELECT_ADDRESS", suggestion });
}
if (key === "Backspace")
c.fsm.send({ type: "INPUT", event });
if (key === "ArrowUp") {
event.preventDefault();
c.fsm.send("PREVIOUS");
}
if (key === "ArrowDown") {
event.preventDefault();
c.fsm.send("NEXT");
}
}
if (key === "Escape")
c.fsm.send({ type: "CLOSE", reason: "esc" });
if (key === "Home")
c.fsm.send({ type: "RESET" });
if (key === "End")
c.fsm.send({ type: "RESET" });
};
exports._onKeyDown = _onKeyDown;
// Event handler: Fires when country selection is changed
const _onCountryChange = (c) => function (event) {
if (event.target === null)
return;
const target = event.target;
if (!target)
return;
let contextDetails = findMatchingContext(target.value, c.options.contexts);
c.fsm.send({ type: "COUNTRY_CHANGE_EVENT", contextDetails });
};
exports._onCountryChange = _onCountryChange;
/**
* Retrieve Element
* - If string, assumes is valid and returns first match within scope
* - If null, invokes the create method to return a default
* - If HTMLElement returns instance
* @hidden
*/
const findOrCreate = (scope, q, create) => {
if ((0, jsutil_1.isString)(q))
return scope.querySelector(q);
if (create && q === null)
return create();
return q;
};
exports.findOrCreate = findOrCreate;
const setPositionFixed = (mainComponent, container, document) => {
const setMinWith = (scope, component) => {
if (scope === null)
return;
const box = scope.getBoundingClientRect();
component.style.minWidth = `${Math.round(box.width)}px`;
};
const parent = container.parentElement;
mainComponent.style.position = "fixed";
mainComponent.style.left = "auto";
setMinWith(parent, mainComponent);
document.defaultView !== null &&
document.defaultView.addEventListener("resize", () => {
setMinWith(parent, mainComponent);
});
};
exports.setPositionFixed = setPositionFixed;
const findMatchingContext = (name, contexts) => {
const n = name.toUpperCase();
for (const context of Object.values(contexts)) {
if (context.iso_3 === n)
return context;
if (context.iso_2 === n)
return context;
if (context.description.toUpperCase() === n)
return context;
}
return undefined;
};