@ideal-postcodes/address-finder
Version:
Address Finder JS library backed by the Ideal Postcodes UK address search API
927 lines (926 loc) • 32 kB
JavaScript
/**
* @module Controller
*/
/* eslint-disable no-invalid-this */
import { announcer } from "./announcer";
import debounce from "lodash/debounce";
import { toContextMap, defaultContexts, toContextList, } from "./contexts";
import { ApiCache } from "./cache";
import { addStyle, computeOffset } from "./css";
import { Client, checkKeyUsability } from "@ideal-postcodes/core-axios";
import { InterpreterStatus } from "@xstate/fsm";
import { create } from "./state";
import { getScope, setStyle, show, toKey, update, toHtmlElem, getDocument, hide, remove, restoreStyle, isString, populateAddress, idGen, } from "@ideal-postcodes/jsutil";
/**
* @hidden
*/
export const NOOP = () => { };
/**
* Default options assigned to controller instances
*/
export const 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: 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",
unhideStyle: {},
msgUnhide: "Enter address manually",
hide: [],
//change list position
fixed: false,
// Callbacks
onOpen: NOOP,
onSelect: NOOP,
onBlur: NOOP,
onClose: NOOP,
onFocus: NOOP,
onInput: NOOP,
onLoaded: NOOP,
onSearchError: NOOP,
onSuggestionError: NOOP,
onMounted: NOOP,
onRemove: NOOP,
onSuggestionsRetrieved: NOOP,
onAddressSelected: NOOP,
onAddressRetrieved: NOOP,
onAddressPopulated: NOOP,
onFailedCheck: NOOP,
onMouseDown: NOOP,
onKeyDown: NOOP,
onUnhide: NOOP,
onCountrySelected: NOOP,
onContextChange: 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.
*/
export class Controller {
constructor(options) {
this.options = {
...{ scope: window.document, document: window.document },
...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 = defaults.autocomplete;
// Scope the operations of this controller to a document or DOM subtree
this.scope = getScope(this.options.scope);
// Assign a parent Document for elem creation
this.document = getDocument(this.scope);
// Assign a document or DOM subtree to scope outputs. Defaults to controller scope
this.outputScope = 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 Client({ ...this.options, api_key: this.options.apiKey });
this.cache = new ApiCache(this.client);
this.retrieveSuggestions = debounce((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 = 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)
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;
hide(this.mainComponent);
//configure unhide
this.unhideEvent = this.unhideFields.bind(this);
this.unhide = this.createUnhide();
// Configure input
let input;
if (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 = _onKeyDown(this);
this.countryListener = _onCountryChange(this);
const { container, announce } = announcer({
idA: this.ids(),
idB: this.ids(),
document: this.options.document,
});
this.announce = announce;
this.alerts = container;
this.inputStyle = setStyle(this.input, this.options.inputStyle);
setStyle(this.container, this.options.containerStyle);
setStyle(this.list, this.options.listStyle);
// Apply an offset based off any margin
const offset = computeOffset(this);
setStyle(this.mainComponent, {
...offset,
...this.options.mainStyle,
});
this.fsm = 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();
addStyle(this);
if (this.options.fixed) {
//set position fixed and width calculation
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;
}
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
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 = 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");
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");
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();
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 === 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 !== 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();
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() {
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") {
hide(this.mainComponent);
if (reason === "esc")
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() {
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 unhideOption = this.options.unhide;
const e = findOrCreate(this.scope,
// Pass null if unhide is an HTMLElement to use the create callback
// This ensures dynamic elements are handled correctly
unhideOption instanceof HTMLElement ? null : unhideOption, () => {
// If unhide is an HTMLElement, return it directly
if (unhideOption instanceof HTMLElement)
return unhideOption;
// Otherwise create a new element
const elem = this.options.document.createElement("p");
elem.innerText = this.options.msgUnhide;
elem.setAttribute("role", "button");
elem.setAttribute("tabindex", "0");
if (this.options.unhideClass)
elem.className = this.options.unhideClass;
return elem;
});
setStyle(e, this.options.unhideStyle);
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)
remove(this.unhide);
}
hiddenFields() {
return this.options.hide
.map((e) => {
if (isString(e))
return toHtmlElem(this.options.scope, e);
return e;
})
.filter((e) => e !== null);
}
/**
* Hides fields marked for hiding
*/
hideFields() {
this.hiddenFields().forEach(hide);
}
/**
* Unhides fields marked for hiding
*/
unhideFields() {
this.hiddenFields().forEach(show);
this.options.onUnhide.call(this);
}
}
/**
* 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") {
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
*/
export const _onKeyDown = (c) => function (event) {
// Dispatch events based on keys
const key = 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" });
};
// Event handler: Fires when country selection is changed
export 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 });
};
/**
* 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
*/
export const findOrCreate = (scope, q, create) => {
if (isString(q))
return scope.querySelector(q);
if (create && q === null)
return create();
return q;
};
export 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);
});
};
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;
};