Socket
Socket
Sign inDemoInstall

@ideal-postcodes/address-finder

Package Overview
Dependencies
9
Maintainers
2
Versions
54
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 2.5.1 to 3.0.0-beta.1

dist/callbacks.d.ts

40

CHANGELOG.md

@@ -0,1 +1,41 @@

# [3.0.0-beta.1](https://github.com/ideal-postcodes/address-finder/compare/2.5.1...3.0.0-beta.1) (2022-02-21)
### Bug Fixes
* **Exports:** Namespace all exports to AddressFinder ([9d64f93](https://github.com/ideal-postcodes/address-finder/commit/9d64f9323cb578afb334d98bdde671756d82fdd3))
### Features
* **AddressFinder:** 3.0.0 ([8bdb008](https://github.com/ideal-postcodes/address-finder/commit/8bdb008557c87b0f808ef61d21b3f03083e0cdaf))
* **Countries:** Allow user to restrict to specific countries ([4809a25](https://github.com/ideal-postcodes/address-finder/commit/4809a25fe5416669d358804cb64c42ac7615d831))
* **Hints:** Add hints in placeholder ([c5ba376](https://github.com/ideal-postcodes/address-finder/commit/c5ba376570dd82403b38ef3573eca50fb9045643))
* **Resolver:** Use new resolve API ([609b359](https://github.com/ideal-postcodes/address-finder/commit/609b3598f86720c1b803a558ba023041d77b7d62))
* **Styling:** Automatically offset AF if input has bottom margin ([3ed3a78](https://github.com/ideal-postcodes/address-finder/commit/3ed3a78653e5ad6a5b55885352302bc340d6e072))
* **Typings:** Drop api-typings for OpenApi typings ([baa469a](https://github.com/ideal-postcodes/address-finder/commit/baa469a4afa9827ae9ecf1118f061c1fd3adcb5a))
* **USA:** Add support for US Address Search ([b04bd89](https://github.com/ideal-postcodes/address-finder/commit/b04bd89e6ecaaef835d4507dd98259fdebb5b45c))
* **USA:** Add USA support ([589f0ab](https://github.com/ideal-postcodes/address-finder/commit/589f0abb5626ee3728e65caa91a9e9a3289d8bfb))
* **View:** View brought into main scope ([01781ac](https://github.com/ideal-postcodes/address-finder/commit/01781accb1f3e43ef86ebb49157130bcccb59f27))
### BREAKING CHANGES
* **AddressFinder:** - A parent element which wraps both the address finder suggestion list
and toolbar
- Controller no longer provides `view` attribute. References to DOM
Elements are stored directly on the instance
- `next()` and `previous()` only update internal state but do not
advance the state machine
* **Exports:** All exports have been namespaced to AddressFinder
* **View:** Removed controller.view. View components are incorporated into main
Address Finder instance. e.g. `controller.view.input` becomes
`controller.input`
* **Resolver:** Address Finder now completes addresses using the
/autocomplete/addresses/:id API
* **Typings:** Address Finder now uses the typings found at
@ideal-postcodes/api-typings
* **USA:** Adds USA support. Underlying API Client upgraded to
3.0.0
## [2.5.1](https://github.com/ideal-postcodes/address-finder/compare/2.5.0...2.5.1) (2022-01-06)

@@ -2,0 +42,0 @@

2

dist/cache.d.ts
import { Client } from "@ideal-postcodes/core-axios";
import { AddressSuggestion, Address } from "@ideal-postcodes/api-typings";
import { AddressSuggestion, Address } from "@ideal-postcodes/jsutil";
export declare type QueryOptions = Record<string, string>;

@@ -4,0 +4,0 @@ /**

@@ -53,8 +53,9 @@ "use strict";

resolve(suggestion) {
const { umprn, udprn } = suggestion;
if (umprn !== undefined)
return core_axios_1.lookupUmprn({ client: this.client, umprn });
return core_axios_1.lookupUdprn({ client: this.client, udprn });
return core_axios_1.autocomplete
.gbr(this.client, suggestion.id, {
query: { api_key: this.client.config.api_key },
})
.then((response) => response.body.result);
}
}
exports.ApiCache = ApiCache;
/**
* @module Controller
*/
import { View } from "./view";
import { Announce } from "./announcer";
import { Callbacks, Listener } from "./callbacks";
import { Context, Details, ContextDetails } from "./contexts";
import { DebouncedFunc } from "lodash";
import { ApiCache, QueryOptions } from "./cache";
import { AddressSuggestion, Address } from "@ideal-postcodes/api-typings";
import { ControllerOptions } from "./index";
import { AddressSuggestion, Address } from "@ideal-postcodes/jsutil";
import { Client } from "@ideal-postcodes/core-axios";
import { Config } from "@ideal-postcodes/core-axios/dist/client";
import { ViewService, CloseReason } from "./state";
import { SelectorNode, CSSStyle, OutputFields, NamedFields, IdGen } from "@ideal-postcodes/jsutil";
/**
* Configuration options for an Address Finder instance
*/
export interface ControllerOptions extends Partial<Callbacks>, Partial<Omit<Config, "api_key">> {
/**
* CSS selector or HTML Element which specifies the `<input>` field which the
* Address Finder View should bind.
*/
inputField?: SelectorNode;
/**
* API Key from your Ideal Postcodes account. Typically begins `ak_`
*/
apiKey: string;
/**
* Scopes the operable area of the DOM
*
* @default
*
* `window.document`
*/
scope?: Document | HTMLElement | string;
/**
* Specify the Document to operate on
*
* @default
*
* `window.document`
*/
document?: Document;
/**
* Default Country
*
* @default
*
* `"GBR"`
*/
defaultCountry?: Context;
/**
* Narrow the countries you wish to support
*
* Setting this to an empty array (default) will enable all countries
*
* Setting this to a single country will disable country selection and hide the country selection toolbar
*
* @default
*
* `[]`
*/
restrictCountries?: Context[];
/**
* Provide a custom list of possible contexts to select a new country or context from
*/
contexts?: Record<string, Details>;
/**
* Specify parent element for output fields to looking for them to narrow search area
*/
outputScope?: string | HTMLElement | Document | null;
/**
* An object specifying where address field data points should be piped.
*
* The attribute of the document should be the same as the address attribute
* as found in the documentation. E.g. `line_1`, `post_town`, `postcode`.
*
* You may use a CSS selector `string` or a `HTMLElement`. E.g.
* `{ line_1: "#line_1" }` or `{ line_1: document.getElementById("line_1") }`
*
* Using an `HTMLElement` as an `outputField` selector has the effect of eagerly binding the Address Finder instance to your output fields. When using `string` selectors, Address Finder will bind to your ouput fields when when an address is selected.
*/
outputFields?: OutputFields;
/**
* An object specifying the `name`s of HTML Input Elements to target for address population
*
* This will fallback to `aria-name` if a name cannot be detected
*/
names?: NamedFields;
/**
* An object specifying the labels associated with HTML Input Elements to target for address population
*/
labels?: NamedFields;
/**
* Optional. An optional field to remove organisation name from address lines.
*
* This is `false` by default.
*/
removeOrganisation?: boolean;
/**
* An optional field to check whether the key is usable against the Ideal
* Postcodes API. This should be used in conjunction with the
* `onFailedCheck` callback to specify the necessary behaviour when the API
* Key is not in a usable state. This is `true` by default.
*/
checkKey?: boolean;
/**
* Configures which WAI-ARIA specification version Address Finder should target.
*
* - `"1.1"` will target the most recent spec
* - `"1.0"` will enable some regressions to support the 1.0 spec.
*
* Although 1.1 was released in 2017, this currently defaults to "1.0" as it receives the widest support among screen readers. VoiceOver (for MacOS and iOS) and NVDA in particular benefit from this.
*
* Defaults to "1.0"
*/
aria?: "1.0" | "1.1";
/**
* Automatically aligns address finder
*
* @default true
*/
alignToInput?: boolean;
/**
* Offset of AddressFinder from input in pixels
*
* @default 2
*/
offset?: number;
/**
* An optional field to convert the case of the Post Town from upper case
* into title case. E.g. `"LONDON"` becomes `"London".` Default is `true`
*/
titleizePostTown?: boolean;
/**
* Optional configuration object to apply to address queries
*/
queryOptions?: QueryOptions;
/**
* Sets the `autocomplete=` attribute of the input element. Setting this attribute aims to prevent some browsers (particularly Chrome) from providing a clashing autofill overlay.
*
* The best practice for this attribute breaks over time (see https://stackoverflow.com/questions/15738259/disabling-chrome-autofill) and is specific to different forms. If you are observing chrome's autofill clashing on your form, update this attribute to the best practice du jour.
*
* @default `"none"`
*/
autocomplete?: string;
/**
* Inject stylesheet into DOM to style Address Finder with default theme. Default is `true`
*
* Styling of the Address Finder can be achieved using a CSS file. Set this to `false` if you wish to do this
*
* - `true` Injects the default styles into the DOM
* - `string` e.g. `https://cdn.jsdelivr.net/npm/@ideal-postcodes/address-finder@1.1.1/css/address-finder.min.css` will include a CSS Stylesheet in the DOM with the src set as the string
*/
injectStyle?: boolean | string;
/**
* Message in input placeholder when address results are suggested
*
* Defaults to `"Try the first line or postal code of your address"`
*/
msgPlaceholder?: string;
/**
* Message in input placeholder when country suggestions are presented
*
* Defaults to `"Select your country"`
*/
msgPlaceholderCountry?: string;
/**
* Fallback message in case communication message with API fails
*
* Defaults to `"Please enter your address manually"`
*/
msgFallback?: string;
/**
* Initial message when Address Finder opens an no query is available
*
* Defaults to `"Start typing to find address"`
*/
msgInitial?: string;
/**
* Aria-label attached to country select bytton
*
* Defaults to `"Click to change your country"`
*/
msgCountryToggle?: string;
/**
* Message presented when no matches found for a particular query
*
* Defaults to `"No matches found"`
*/
msgNoMatch?: string;
/**
* Aria-label attached to the suggestion list. Prompts screen reader user on how to operate list
*
* Defaults to `"Select your address"`
*
* @default `"Select your address"`
*/
msgList?: string;
/**
* CSS class assigned to Address Finder element. This element is the main visible element containing address suggestions, messages and toolbar underneath the address finder
*
* Defaults to `"idpc_af"`
*
* @default `"idpc_af"`
*/
mainClass?: string;
/**
* CSS class assigned to message box
*
* Defaults to `"idpc_error"`
*
* @default `"idpc_error"`
*/
messageClass?: string;
/**
* CSS class assigned to the AddressFinder container/wrapper
*
* Defaults to `"idpc_autocomplete"`
*
* @default `"idpc_autocomplete"`
*/
containerClass?: string;
/**
* CSS class assigned to suggestion list
*
* Defaults to `"idpc_ul"`
*
* @default `"idpc_ul"`
*/
listClass?: string;
/**
* CSS class assigned to toolbar at bottom of Address Finder
*
* @default `"idpc_toolbar"`
*/
toolbarClass?: string;
/**
* CSS class assigned to country toggle button
*
* @default `"idpc_country"`
*/
countryToggleClass?: string;
/**
* Suppresses `county` from being populated if set to `false`
*
* @default true
*/
populateCounty?: boolean;
/**
* Suppresses `organisation_name` from being populated if set to `false`
*
* @default true
*/
populateOrganisation?: boolean;
/**
* Applies additional styling to the input field. Ideal for quick tweaks. Accepts CSSStyleDeclaration object
* Input styles are restored to original when controller is detached from DOM
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* inputStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
inputStyle?: CSSStyle;
/**
* Applies additional styling to the the suggestion list. Accepts CSSStyleDeclaration object
*
* `style` encapsulates all visible elements of Address Finder. This element is actively shown/hidden when AddressFinder is toggled
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* listStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
listStyle?: CSSStyle;
/**
* Applies additional styling to the the Address Finder container element. Accepts CSSStyleDeclaration object
* `containerStyle encapsulates all elements of Address Finder including the input, ARIA controls
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* containerStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
containerStyle?: CSSStyle;
/**
* Applies additional styling to the the Address Finder Main Component. The Main Component contains the visible elements of the Address Finder such as the address suggestion list, toolbar and messages which appears underneath the input field.
*
* Accepts CSSStyleDeclaration object
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* mainStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
mainStyle?: CSSStyle;
/**
* Applies additional styling to the the Address Finder list element. Accepts CSSStyleDeclaration object
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* liStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
liStyle?: CSSStyle;
/**
* Hide a list of HTML elements when Postcode Lookup is instantiated
*
* Specify these elements using query selectors or direct HTMLElement references
*
* @default []
*/
hide?: (string | HTMLElement)[];
/**
* Message shown to user to unhide address fields if `hide` attribute is configured
*
* @default "Enter address manually"
*/
msgUnhide?: string;
/**
* Specify a clickable element to unhide elements hidden with `hide`
*
* @default null
*/
unhide?: string | HTMLElement | null;
/**
* Class of clickable unhide element
*
* @default "idpc-unhide"
*/
unhideClass?: string;
}
/**
* @hidden

@@ -17,2 +385,8 @@ */

*/
interface RetrieveSuggestions {
(event: Event): Promise<Controller>;
}
/**
* @hidden
*/
export interface StoredOptions extends Required<Omit<ControllerOptions, keyof Config>>, Omit<Config, "api_key"> {

@@ -34,6 +408,2 @@ }

*
* More concretely, the instantiation of a controller instance creates:
* - A user interface instance `View`
* - An instance of the [Ideal Postcodes Browser Client](https://github.com/ideal-postcodes/core-axios)
*
* The role of the controller is to bind to events produced by the user

@@ -69,9 +439,127 @@ * interface and take appropriate action including querying the API,

/**
* View instance
* Caches previous placeholder value for input
*/
view: View;
placeholderCache: string | undefined;
/**
* Reference to input DOM element
*/
input: HTMLInputElement;
/**
* Reference to Address Finder message DOM element
*/
message: HTMLLIElement;
/**
* Reference to container wrapping AddressFinder elements DOM element. This includes the main component, input fields and WAI-ARIA controls
*/
container: HTMLDivElement;
/**
* Reference to inner container wrapping list and toolbar
*/
mainComponent: HTMLDivElement;
/**
* Reference to Address Suggestion list DOM element
*/
list: HTMLUListElement;
/**
* Reference to toolbar at bottom of finder list
*/
toolbar: HTMLDivElement;
/**
* Reference to country select toggle button
*/
countryToggle: HTMLSpanElement;
/**
* Reference to country icon
*/
countryIcon: HTMLSpanElement;
/**
* Reference to country toggle message
*/
countryMessage: HTMLSpanElement;
/**
* Reference to clickable Unhide link
*/
unhide: HTMLElement;
/**
* Input element input event listener
*/
inputListener: Listener<"input">;
/**
* Input blur event listener
*/
blurListener: Listener<"blur">;
/**
* Input focus event listener
*/
focusListener: Listener<"focus">;
/**
* Input keydown event listener
*/
keydownListener: Listener<"keydown">;
/**
* Unhide click event listener
*/
unhideEvent: Listener<"click">;
/**
* Address Finder state machine
*/
fsm: ViewService;
/**
* ID generation method
*/
ids: IdGen;
/**
* Reference to accessibility announcer
*/
announce: Announce;
/**
* Reference to alerts container
*/
alerts: HTMLDivElement;
/**
* Caches input style prior to Address Finder attachment
*/
inputStyle: string | null;
/**
* Debounced method used to retrieve suggestions
*/
retrieveSuggestions: DebouncedFunc<RetrieveSuggestions>;
/**
* Current search context
*/
context: string;
/**
* Current list of address suggestions
*/
suggestions: AddressSuggestion[];
/**
* Current list of context suggestions
*/
contextSuggestions: ContextDetails[];
/**
* Current notification to be shown to user
*/
notification: string;
/**
* Index of current elem in list selected
*/
current: number;
constructor(options: ControllerOptions);
/**
* Sets placeholder and caches previous result
* @hidden
*/
setPlaceholder(msg: string): void;
/**
* Unsets any placeholder value to original
* @hidden
*/
unsetPlaceholder(): void;
/**
* Returns current highlighted context
* @hidden
*/
currentContext(): ContextDetails;
/**
* Binds to DOM and begin DOM mutations
*
* @hidden

@@ -87,19 +575,27 @@ */

/**
* Produces a function to be bound to an instance of `Autocomplete.View`.
* It executes suggestion search when address input is updated
*
* @private
* Render available country options
*/
_onInput(): (this: View, event: Event) => Promise<View>;
renderContexts(): void;
/**
* Produces a function to be bound to an instance of `Autocomplete.View`.
* Populates fields with correct address when suggestion selected
*
* @private
* Render current address suggestions
*/
_onSelect(): (this: View, suggestion: AddressSuggestion) => Promise<View>;
renderSuggestions(): void;
/**
* Updates current li in list to active descendant
*/
goToCurrent(): void;
/**
* Marks aria component as opened
*/
ariaExpand(): void;
/**
* Marks aria component as closed
*/
ariaContract(): void;
/**
* Resolves a suggestion to full address and apply results to form
*/
applySuggestion(suggestion: AddressSuggestion): Promise<Controller>;
/**
* Writes a selected to the input fields specified in the controller config
*
* @public
*/

@@ -110,6 +606,122 @@ populateAddress(address: Address): void;

* cache to prevent stale searches
*/
setQueryOptions(options: QueryOptions): void;
/**
* Adds Address Finder to DOM
* - Wraps input with container
* - Appends suggestion list to container
* - Enables listeners
* - Starts FSM
*/
attach(): Controller;
/**
* Removes Address Finder from DOM
* - Disable listeners
* - Removes sugestion list from container
* - Appends suggestion list to container
* - Enables listeners
* - Stops FSM
*/
detach(): Controller;
/**
* Sets message as a list item, no or empty string removes any message
*/
setMessage(notification: string): Controller;
/**
* Returns HTML Element which recevies key aria attributes
*
* @public
* @hidden
*/
setQueryOptions(options: QueryOptions): void;
ariaAnchor(): HTMLElement;
/**
* Returns current address query
*/
query(): string;
/**
* Set address finder suggestions
*/
setSuggestions(suggestions: AddressSuggestion[], query: string): Controller;
/**
* Close address finder
*/
close(reason?: CloseReason): void;
/**
* Updates suggestions and resets current selection
* @hidden
*/
updateSuggestions(s: AddressSuggestion[]): void;
/**
* Applies context to API cache
* @hidden
*/
applyContext(details: ContextDetails): void;
/**
* Renders notification box
* @hidden
*/
renderNotice(): void;
/**
* Open address finder
* @hidden
*/
open(): void;
/**
* Sets next suggestion as current
* @hidden
*/
next(): Controller;
/**
* Sets previous suggestion as current
* @hidden
*/
previous(): Controller;
/**
* Given a HTMLLiElement, scroll parent until it is in view
* @hidden
*/
scrollToView(li: HTMLElement): Controller;
/**
* Moves currently selected li into view
* @hidden
*/
goto(i: number): Controller;
/**
* Returns true if address finder is open
*/
opened(): boolean;
/**
* Returs false if address finder is closed
*/
closed(): boolean;
/**
* Creates a clickable element that can trigger unhiding of fields
*/
createUnhide(): HTMLElement;
/**
* Removes unhide elem from DOM
*/
unmountUnhide(): void;
hiddenFields(): HTMLElement[];
/**
* Hides fields marked for hiding
*/
hideFields(): void;
/**
* Unhides fields marked for hiding
*/
unhideFields(): void;
}
/**
* Event handler: Fires on "keyDown" event of search field
* @hidden
*/
export declare const _onKeyDown: (c: Controller) => Listener<"keydown">;
/**
* 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 declare const findOrCreate: <T>(scope: HTMLElement | Document, q: string | T | null, create?: (() => T) | undefined) => T;
export {};

@@ -9,9 +9,12 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.Controller = exports.defaults = exports.NOOP = void 0;
exports.findOrCreate = exports._onKeyDown = exports.Controller = exports.defaults = exports.NOOP = void 0;
/* eslint-disable no-invalid-this */
const view_1 = require("./view");
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");

@@ -46,2 +49,8 @@ /**

queryOptions: {},
alignToInput: true,
offset: 2,
// Country
defaultCountry: "GBR",
restrictCountries: [],
contexts: contexts_1.defaultContexts,
// Messages

@@ -52,7 +61,15 @@ msgFallback: "Please enter your address manually",

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: {},

@@ -87,2 +104,3 @@ listStyle: {},

onUnhide: exports.NOOP,
onCountrySelected: exports.NOOP,
};

@@ -99,6 +117,2 @@ /**

*
* More concretely, the instantiation of a controller instance creates:
* - A user interface instance `View`
* - An instance of the [Ideal Postcodes Browser Client](https://github.com/ideal-postcodes/core-axios)
*
* The role of the controller is to bind to events produced by the user

@@ -128,25 +142,159 @@ * interface and take appropriate action including querying the API,

// Assign a document or DOM subtree to scope outputs. Defaults to controller scope
this.outputScope = view_1.findOrCreate(this.scope, this.options.outputScope, () => this.scope);
this.outputScope = 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 = contexts_1.toContextList(this.options.contexts, this.options.restrictCountries);
this.client = new core_axios_1.Client({ ...this.options, api_key: this.options.apiKey });
this.cache = new cache_1.ApiCache(this.client);
this.view = new view_1.View({
...this.options,
scope: this.scope,
document: this.document,
onInput: debounce_1.default(this._onInput(), 100, {
leading: true,
trailing: true,
maxWait: 100,
}),
onSelect: this._onSelect(),
this.retrieveSuggestions = 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)
.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 = 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().icon;
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);
// 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;
jsutil_1.hide(this.mainComponent);
//configure unhide
this.unhideEvent = this.unhideFields.bind(this);
this.unhide = this.createUnhide();
// Configure input
let input;
if (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();
// 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 = exports._onKeyDown(this);
const { container, announce } = announcer_1.announcer({
idA: this.ids(),
idB: this.ids(),
document: this.options.document,
});
this.announce = announce;
this.alerts = container;
this.inputStyle = jsutil_1.setStyle(this.input, this.options.inputStyle);
jsutil_1.setStyle(this.container, this.options.containerStyle);
jsutil_1.setStyle(this.list, this.options.listStyle);
// Apply an offset based off any margin
const offset = css_1.computeOffset(this);
jsutil_1.setStyle(this.mainComponent, {
...offset,
...this.options.mainStyle,
});
this.fsm = 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];
return {
code: this.context,
name: c.name,
icon: c.icon,
};
}
/**
* Binds to DOM and begin DOM mutations
*
* @hidden
*/
load() {
this.view.attach();
this.attach();
css_1.addStyle(this);

@@ -181,64 +329,109 @@ this.options.onLoaded.call(this);

/**
* Produces a function to be bound to an instance of `Autocomplete.View`.
* It executes suggestion search when address input is updated
*
* @private
* Render available country options
*/
_onInput() {
const self = this;
return function (event) {
self.options.onInput.call(this, event);
const query = this.query();
if (query.trim().length === 0) {
this.setMessage(self.options.msgInitial);
return Promise.resolve(this);
}
return self.cache
.query(query, self.options.queryOptions)
.then((suggestions) => {
self.options.onSuggestionsRetrieved.call(self, suggestions);
return this.setSuggestions(suggestions, query);
})
.catch((error) => {
if (this.query() === query)
this.setMessage(self.options.msgFallback);
self.options.onSuggestionError.call(self, error);
return self.view;
renderContexts() {
this.list.innerHTML = "";
this.contextSuggestions.forEach((contextDetails, i) => {
const { name } = contextDetails;
const li = this.options.document.createElement("li");
li.textContent = name;
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");
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`);
}
/**
* Produces a function to be bound to an instance of `Autocomplete.View`.
* Populates fields with correct address when suggestion selected
*
* @private
* Render current address suggestions
*/
_onSelect() {
const self = this;
return function (suggestion) {
self.options.onAddressSelected.call(self, suggestion);
return self.cache
.resolve(suggestion)
.then((address) => {
if (address === null)
throw "Unable to retrieve address";
self.options.onAddressRetrieved.call(self, address);
self.populateAddress(address);
return this;
})
.catch((error) => {
this.open();
this.setMessage(self.options.msgFallback);
self.options.onSearchError.call(self, error);
return error;
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");
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)
.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
*
* @public
*/
populateAddress(address) {
this.view.unhideFields();
this.unhideFields();
jsutil_1.populateAddress({

@@ -256,4 +449,2 @@ address,

* cache to prevent stale searches
*
* @public
*/

@@ -264,3 +455,369 @@ setQueryOptions(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);
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);
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();
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;
}
/**
* 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") {
jsutil_1.hide(this.mainComponent);
if (reason === "esc")
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) {
const context = details.code;
this.context = context;
this.setQueryOptions({ ...this.options.queryOptions, context });
this.countryIcon.innerText = details.icon;
this.announce(`Country switched to ${details.name}`);
}
/**
* 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() {
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 = 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)
jsutil_1.remove(this.unhide);
}
hiddenFields() {
return this.options.hide
.map((e) => {
if (jsutil_1.isString(e))
return 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") {
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 = 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.contextSuggestions[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;
/**
* 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 (jsutil_1.isString(q))
return scope.querySelector(q);
if (create && q === null)
return create();
return q;
};
exports.findOrCreate = findOrCreate;

@@ -10,1 +10,12 @@ import { Controller } from "./controller";

export declare const addStyle: (c: Controller) => undefined | HTMLStyleElement | HTMLLinkElement;
interface Offset {
marginTop: string;
}
interface Empty {
}
/**
* Returns a negative offset which can be used to correctly align input box
* @hidden
*/
export declare const computeOffset: (c: Controller) => Offset | Empty;
export {};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addStyle = void 0;
exports.computeOffset = exports.addStyle = void 0;
const jsutil_1 = require("@ideal-postcodes/jsutil");

@@ -10,3 +10,103 @@ /**

*/
const d = ".idpc_ul.hidden{display:none}div.idpc_autocomplete{position:relative;margin:0;padding:0;border:0}div.idpc_autocomplete>input{display:block}div.idpc_autocomplete>ul{position:absolute;left:0;z-index:999;min-width:100%;box-sizing:border-box;list-style:none;padding:0;border-radius:.3em;margin:.2em 0 0;background:#fff;border:1px solid rgba(0,0,0,.3);box-shadow:.05em .2em .6em rgba(0,0,0,.2);text-shadow:none;max-height:250px;overflow-y:scroll}div.idpc_autocomplete>ul>li{position:relative;padding:.2em .5em;cursor:pointer}div.idpc_autocomplete>ul>li:hover{background:#b8d3e0;color:#000}div.idpc_autocomplete>ul>li.idpc_error{font-style:italic;background-color:#eee;cursor:default!important}div.idpc_autocomplete>ul>li[aria-selected=true]{background:#3d6d8f;color:#fff;z-index:1000}div.idpc_autocomplete>.idpc-unhide{font-size:90%;text-decoration:underline;cursor:pointer}@supports (transform:scale(0)){div.idpc_autocomplete>ul{transition:.3s cubic-bezier(.4, .2, .5, 1.4);transform-origin:1.43em -0.43em}div.idpc_autocomplete>ul:empty,div.idpc_autocomplete>ul[hidden]{opacity:0;transform:scale(0);display:block;transition-timing-function:ease}}";
const d = `
.idpc_af.hidden{
display:none;
}
div.idpc_autocomplete{
position:relative;
margin:0 0 0 0 !important;
padding:0;
border:0;
color: #28282B;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
div.idpc_autocomplete>input{
display:block;
}
div.idpc_af{
position:absolute;
left:0;
z-index:2000;
min-width:100%;
box-sizing:border-box;
border-radius:3px;
background:#fff;
border:1px solid rgba(0,0,0,.3);
box-shadow:.05em .2em .6em rgba(0,0,0,.2);
text-shadow:none;
padding:0;
}
div.idpc_af>ul{
list-style:none;
padding:0;
max-height:250px;
overflow-y: scroll;
margin: 0 0 0 0 !important;
}
div.idpc_af>ul>li{
position:relative;
padding:.2em .5em;
cursor:pointer;
margin:0 0 0 0 !important;
}
div.idpc_toolbar{
padding: .3em .5em;
border-top: 1px solid rgba(0,0,0,0.3);
text-align: right;
}
div.idpc_af>ul>li:hover{
background-color: #E5E4E2;
}
div.idpc_af>ul>li.idpc_error{
padding: .5em;
text-align: center;
cursor:default!important;
}
div.idpc_af>ul>li.idpc_error:hover{
background: #fff;
cursor:default!important;
}
div.idpc_af>ul>li[aria-selected=true]{
background-color: #E5E4E2;
z-index:3000;
}
div.idpc_autocomplete>.idpc-unhide{
font-size:0.9em;
text-decoration:underline;
cursor:pointer;
}
div.idpc_af>div>span{
padding: .2em .5em;
border-radius: 3px;
cursor:pointer;
font-size: 110%;
}
span.idpc_icon {
font-size:1.2em;
line-height: 1em;
vertical-align: middle;
}
div.idpc_toolbar>span span.idpc_country {
margin-right: 0.3em;
max-width: 0;
font-size: 0.9em;
-webkit-transition: max-width 500ms ease-out;
transition: max-width 500ms ease-out;
display: inline-block;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
}
div.idpc_autocomplete>div>div>span:hover span.idpc_country {
max-width: 7em;
}
div.idpc_autocomplete>div>div>span:hover{
background-color: #E5E4E2;
-webkit-transition: background-color 500ms ease;
-ms-transition: background-color 500ms ease;
transition: background-color 500ms ease;
}
`;
/**

@@ -39,1 +139,32 @@ * Injects CSS style into DOM

exports.addStyle = addStyle;
/**
* Returns a negative offset which can be used to correctly align input box
* @hidden
*/
const computeOffset = (c) => {
let offset;
const input = c.input;
if (c.options.alignToInput === false)
return {};
try {
const w = c.options.document.defaultView;
if (!w)
return {};
offset = w.getComputedStyle(input).marginBottom;
}
catch (_) {
return {};
}
if (!offset)
return {};
const nOffset = parseInt(offset.replace("px", ""), 10);
if (isNaN(nOffset))
return {};
if (nOffset === 0)
return {};
const negativeOffset = nOffset * -1 + c.options.offset;
return {
marginTop: negativeOffset + "px",
};
};
exports.computeOffset = computeOffset;
/**
* @module Address-Finder Exports
*/
import { Address, AddressSuggestion } from "@ideal-postcodes/api-typings";
import { watch } from "./watch";
import { Config } from "@ideal-postcodes/core-axios/dist/client";
import { Controller, defaults } from "./controller";
import { QueryOptions } from "./cache";
import { SelectorNode, OutputFields, NamedFields } from "@ideal-postcodes/jsutil";
import { View, ViewOptions, OnOpen, OnBlur, OnClose, OnFocus, OnInput, OnSelect, OnUnhide } from "./view";
export interface OnLoaded {
(this: Controller): void;
}
export interface OnFailedCheck {
(this: Controller, error: Error): void;
}
export interface OnSuggestionsRetrieved {
(this: Controller, suggestion: AddressSuggestion[]): void;
}
export interface OnAddressRetrieved {
(this: Controller, address: Address): void;
}
export interface OnSearchError {
(this: Controller, error: Error): void;
}
export interface OnSuggestionError {
(this: Controller, error: Error): void;
}
export interface OnMounted {
(this: View): void;
}
export interface OnRemove {
(this: View): void;
}
export interface OnAddressSelected {
(this: Controller, suggestion: AddressSuggestion): void;
}
export interface OnAddressPopulated {
(this: Controller, address: Address): void;
}
import { Controller } from "./controller";
/**
* Configuration options for an Address Finder instance
*/
export interface ControllerOptions extends Partial<Omit<Config, "api_key">>, Partial<Omit<ViewOptions, "scope" | "document">> {
/**
* CSS selector or HTML Element which specifies the `<input>` field which the
* Address Finder View should bind.
*/
inputField?: SelectorNode;
/**
* API Key from your Ideal Postcodes account. Typically begins `ak_`
*/
apiKey: string;
/**
* Scopes the operable area of the DOM
*
* @default
*
* `window.document`
*/
scope?: Document | HTMLElement | string;
/**
* Specify the Document to operate on
*
* @default
*
* `window.document`
*/
document?: Document;
/**
* Specify parent element for output fields to looking for them to narrow search area
*/
outputScope?: string | HTMLElement | Document | null;
/**
* An object specifying where address field data points should be piped.
*
* The attribute of the document should be the same as the address attribute
* as found in the documentation. E.g. `line_1`, `post_town`, `postcode`.
*
* You may use a CSS selector `string` or a `HTMLElement`. E.g.
* `{ line_1: "#line_1" }` or `{ line_1: document.getElementById("line_1") }`
*
* Using an `HTMLElement` as an `outputField` selector has the effect of eagerly binding the Address Finder instance to your output fields. When using `string` selectors, Address Finder will bind to your ouput fields when when an address is selected.
*/
outputFields?: OutputFields;
/**
* An object specifying the `name`s of HTML Input Elements to target for address population
*
* This will fallback to `aria-name` if a name cannot be detected
*/
names?: NamedFields;
/**
* An object specifying the labels associated with HTML Input Elements to target for address population
*/
labels?: NamedFields;
/**
* Optional. An optional field to remove organisation name from address lines.
*
* This is `false` by default.
*/
removeOrganisation?: boolean;
/**
* An optional field to check whether the key is usable against the Ideal
* Postcodes API. This should be used in conjunction with the
* `onFailedCheck` callback to specify the necessary behaviour when the API
* Key is not in a usable state. This is `true` by default.
*/
checkKey?: boolean;
/**
* Configures which WAI-ARIA specification version Address Finder should target.
*
* - `"1.1"` will target the most recent spec
* - `"1.0"` will enable some regressions to support the 1.0 spec.
*
* Although 1.1 was released in 2017, this currently defaults to "1.0" as it receives the widest support among screen readers. VoiceOver (for MacOS and iOS) and NVDA in particular benefit from this.
*
* Defaults to "1.0"
*/
aria?: "1.0" | "1.1";
/**
* An optional field to convert the case of the Post Town from upper case
* into title case. E.g. `"LONDON"` becomes `"London".` Default is `true`
*/
titleizePostTown?: boolean;
/**
* Optional configuration object to apply to address queries
*/
queryOptions?: QueryOptions;
/**
* Sets the `autocomplete=` attribute of the input element. Setting this attribute aims to prevent some browsers (particularly Chrome) from providing a clashing autofill overlay.
*
* The best practice for this attribute breaks over time (see https://stackoverflow.com/questions/15738259/disabling-chrome-autofill) and is specific to different forms. If you are observing chrome's autofill clashing on your form, update this attribute to the best practice du jour.
*
* @default "none"
*/
autocomplete?: string;
/**
* Inject stylesheet into DOM to style Address Finder with default theme. Default is `false`
*
* Styling of the Address Finder can be achieved using a CSS file. Set this to `false` if you wish to do this
*
* - `true` Injects the default styles into the DOM
* - `string` e.g. `https://cdn.jsdelivr.net/npm/@ideal-postcodes/address-finder@1.1.1/css/address-finder.min.css` will include a CSS Stylesheet in the DOM with the src set as the string
*/
injectStyle?: boolean | string;
/**
* Fallback message in case communication message with API fails
*
* Defaults to `"Please enter your address manually"`
*/
msgFallback?: string;
/**
* Initial message when Address Finder opens an no query is available
*
* Defaults to `"Start typing to find address"`
*/
msgInitial?: string;
/**
* Message presented when no matches found for a particular query
*
* Defaults to `"No matches found"`
*/
msgNoMatch?: string;
/**
* Aria-label attached to the suggestion list. Prompts screen reader user on how to operate list
*
* Defaults to `"Select your address"`
*/
msgList?: string;
/**
* CSS class assigned to message box
*
* Defaults to `"idpc_error"`
*
* Note this doesn't necessarily indicate an error
*/
messageClass?: string;
/**
* CSS class assigned to the AddressFinder container/wrapper
*
* Defaults to `"idpc_autocomplete"`
*/
containerClass?: string;
/**
* CSS class assigned to suggestion list (bound to `<ul>`)
*
* Defaults to `"idpc_ul"`
*/
listClass?: string;
/**
* Invoked when Address Finder has been successfully attached to the input element.
*/
onLoaded?: OnLoaded;
/**
* Invoked when `checkKey` is enabled and the key is discovered to be in an
* unusable state (e.g. daily limit reached, no balance, etc).
*/
onFailedCheck?: OnFailedCheck;
/**
* Invoked immediately after address suggestions are retrieved from the API.
* The first argument is an array of address suggestions.
*/
onSuggestionsRetrieved?: OnSuggestionsRetrieved;
/**
* Invoked when the Address Finder client has retrieved a full address from
* the API following a user accepting a suggestion. The first argument is
* an object representing the address that has been retrieved.
*/
onAddressRetrieved?: OnAddressRetrieved;
/**
* Invoked when view is attached to the DOM
*/
onMounted?: OnMounted;
/**
* Invoked when view is detached from the DOM
*/
onRemove?: OnRemove;
/**
* Invoked immediately after the user has selected a suggestion (either by
* click or keypress). The first argument is an object which represents the
* suggestion selected.
*/
onAddressSelected?: OnAddressSelected;
/**
* Invoked when selected address is populated into address fields of user
* address form
*/
onAddressPopulated?: OnAddressPopulated;
/**
* Invoked when an error has occurred following an attempt to retrieve a full
* address. i.e. the API request made after the user selects a suggestion.
*
* The first argument is an error instance (i.e. inherits from `Error`)
* representing the error which has occurred.
*
* In this scenario the user will also receive a message to manually input an
* address if address retrieval fails.
*/
onSearchError?: OnSearchError;
/**
* Invoked when an address suggestion retrieval request has failed.
*
* In this scenario the user will be alerted that no address suggestions
* could be found and to manually input an address.
*/
onSuggestionError?: OnSuggestionError;
/**
* Invoked when the Address Finder view opens (i.e. appears)
*/
onOpen?: OnOpen;
/**
* Invoked when `blur` event is dispatched by Address Finder input field
*/
onBlur?: OnBlur;
/**
* Invoked when the Address Finder view closes (i.e. disappears)
*/
onClose?: OnClose;
/**
* Invoked when `focus` event is dispatched by Address Finder input field
*/
onFocus?: OnFocus;
/**
* Invoked when `input` event is dispatched by Address Finder input field
*/
onInput?: OnInput;
/**
* Invoked when a suggestion has been selected
*/
onSelect?: OnSelect;
/**
* Invoked when hidden fields are unhidden (i.e. user selects an address or opts for manual input)
*/
onUnhide?: OnUnhide;
/**
* Suppresses `county` from being populated if set to `false`
*
* @default true
*/
populateCounty?: boolean;
/**
* Suppresses `organisation_name` from being populated if set to `false`
*
* @default true
*/
populateOrganisation?: boolean;
}
/**
* Configure and launch an instance of the Address Finder
*
* This method will create and return a new AddressFinder instance. It will also add a global reference to the controller at `AddressFinder.controllers`
*/
export declare const setup: (config: ControllerOptions) => Controller;
/**
* Configure and launch an instance of the Address Finder
*
* This is equivalent to invoking `setup` except inside a DOMContentLoaded event callback
*/
export declare const go: (config: ControllerOptions, d?: Document | undefined) => Promise<Controller | null>;
/**
* Cache of Address Finder controllers
*/
export declare const controllers: Controller[];
/**
* Namespace that exports Address Finder methods and classes
*/
export declare const AddressFinder: {
setup: (config: ControllerOptions) => Controller;
setup: (config: import("./controller").ControllerOptions) => Controller;
controllers: Controller[];

@@ -313,16 +14,3 @@ Controller: typeof Controller;

watch: import("./watch").Watch;
go: (config: ControllerOptions, d?: Document | undefined) => Promise<Controller | null>;
go: (config: import("./controller").ControllerOptions, d?: Document | undefined) => Promise<Controller | null>;
};
/**
* Configure Address Finder to watch for available address fields to bind
*/
export { watch };
/**
* Default Address Finder Controller configuration
*/
export { defaults };
export { ViewOptions };
/**
* Controller Export
*/
export { Controller };

@@ -6,45 +6,16 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.Controller = exports.defaults = exports.watch = exports.AddressFinder = exports.controllers = exports.go = exports.setup = void 0;
exports.AddressFinder = void 0;
const setup_1 = require("./setup");
const watch_1 = require("./watch");
Object.defineProperty(exports, "watch", { enumerable: true, get: function () { return watch_1.watch; } });
const controller_1 = require("./controller");
Object.defineProperty(exports, "Controller", { enumerable: true, get: function () { return controller_1.Controller; } });
Object.defineProperty(exports, "defaults", { enumerable: true, get: function () { return controller_1.defaults; } });
/**
* Configure and launch an instance of the Address Finder
*
* This method will create and return a new AddressFinder instance. It will also add a global reference to the controller at `AddressFinder.controllers`
*/
const setup = (config) => {
const c = new controller_1.Controller(config);
exports.controllers.push(c);
return c;
};
exports.setup = setup;
/**
* Configure and launch an instance of the Address Finder
*
* This is equivalent to invoking `setup` except inside a DOMContentLoaded event callback
*/
const go = (config, d) => new Promise((resolve, _) => {
(d || document).addEventListener("DOMContentLoaded", (_) => {
const c = exports.setup(config);
return resolve(c);
});
}).catch((_) => null);
exports.go = go;
/**
* Cache of Address Finder controllers
*/
exports.controllers = [];
/**
* Namespace that exports Address Finder methods and classes
*/
exports.AddressFinder = {
setup: exports.setup,
controllers: exports.controllers,
setup: setup_1.setup,
controllers: setup_1.controllers,
Controller: controller_1.Controller,
defaults: controller_1.defaults,
watch: watch_1.watch,
go: exports.go,
go: setup_1.go,
};
import { StateMachine } from "@xstate/fsm";
import { AddressSuggestion } from "@ideal-postcodes/api-typings";
import { View, CloseReason } from "./view";
import { AddressSuggestion } from "@ideal-postcodes/jsutil";
import { ContextDetails } from "./contexts";
import { Controller } from "./controller";
export declare type CloseReason = "select" | "esc" | "blur";
/**

@@ -13,4 +15,4 @@ * @hidden

} | {
type: "SELECT";
suggestion: AddressSuggestion;
type: "SELECT_ADDRESS";
suggestion: AddressSuggestion | undefined;
} | {

@@ -26,2 +28,7 @@ type: "NEXT";

} | {
type: "CHANGE_COUNTRY";
} | {
type: "SELECT_COUNTRY";
contextDetails: ContextDetails | undefined;
} | {
type: "CLOSE";

@@ -31,3 +38,3 @@ reason: CloseReason;

type: "NOTIFY";
message: string;
notification: string;
};

@@ -38,5 +45,2 @@ /**

export interface Context {
message: string;
suggestions: AddressSuggestion[];
current: number;
}

@@ -50,2 +54,5 @@ /**

} | {
value: "suggesting_country";
context: Context;
} | {
value: "notifying";

@@ -75,8 +82,9 @@ context: Context;

export interface CreateOptions {
view: View;
c: Controller;
}
/**
* Creates a finite state machine that drives Address Finder UI
* @hidden
*/
export declare const create: Create;
export {};

@@ -5,3 +5,2 @@ "use strict";

const fsm_1 = require("@xstate/fsm");
const jsutil_1 = require("@ideal-postcodes/jsutil");
/**

@@ -28,12 +27,26 @@ * @hidden

/**
* @hidden
*/
const NEXT = { NEXT: { actions: ["next", "gotoCurrent"] } };
/**
* @hidden
*/
const PREVIOUS = { PREVIOUS: { actions: ["previous", "gotoCurrent"] } };
/**
* @hidden
*/
const RESET = { RESET: { actions: ["resetCurrent", "gotoCurrent"] } };
/**
* @hidden
*/
const CHANGE_COUNTRY = {
CHANGE_COUNTRY: { target: "suggesting_country" },
};
/**
* Creates a finite state machine that drives Address Finder UI
* @hidden
*/
const create = ({ view }) => {
const create = ({ c }) => {
const machine = fsm_1.createMachine({
initial: "closed",
context: {
suggestions: [],
message: view.options.msgInitial,
current: -1,
},
states: {

@@ -47,3 +60,3 @@ closed: {

target: "suggesting",
cond: (c) => c.suggestions.length > 0,
cond: () => c.suggestions.length > 0,
},

@@ -64,7 +77,26 @@ {

...INPUT,
...CHANGE_COUNTRY,
},
},
suggesting_country: {
entry: ["renderContexts", "gotoCurrent", "expand", "addCountryHint"],
exit: ["resetCurrent", "gotoCurrent", "contract", "clearHint"],
on: {
...CLOSE,
...NOTIFY,
...NEXT,
...PREVIOUS,
...RESET,
INPUT: {
actions: ["countryInput"],
},
SELECT_COUNTRY: {
target: "notifying",
actions: ["selectCountry"],
},
},
},
suggesting: {
entry: ["renderSuggestions", "gotoCurrent", "expand"],
exit: ["resetCurrent", "gotoCurrent", "contract"],
entry: ["renderSuggestions", "gotoCurrent", "expand", "addHint"],
exit: ["resetCurrent", "gotoCurrent", "contract", "clearHint"],
on: {

@@ -75,6 +107,7 @@ ...CLOSE,

...INPUT,
NEXT: { actions: ["next", "gotoCurrent"] },
PREVIOUS: { actions: ["previous", "gotoCurrent"] },
RESET: { actions: ["resetCurrent", "gotoCurrent"] },
SELECT: { target: "closed", actions: ["select"] },
...CHANGE_COUNTRY,
...NEXT,
...PREVIOUS,
...RESET,
SELECT_ADDRESS: { target: "closed", actions: ["selectAddress"] },
},

@@ -85,18 +118,16 @@ },

actions: {
addHint: () => {
c.setPlaceholder(c.options.msgPlaceholder);
},
addCountryHint: () => {
c.setPlaceholder(c.options.msgPlaceholderCountry);
},
clearHint: () => {
c.unsetPlaceholder();
},
/**
* Updates current li in list to active descendant
*/
gotoCurrent: (c) => {
const lis = view.list.children;
view.input.setAttribute("aria-activedescendant", "");
for (let i = 0; i < lis.length; i += 1) {
if (i === c.current) {
view.input.setAttribute("aria-activedescendant", lis[i].id);
lis[i].setAttribute("aria-selected", "true");
view.goto(i);
}
else {
lis[i].setAttribute("aria-selected", "false");
}
}
gotoCurrent: () => {
c.goToCurrent();
},

@@ -106,3 +137,5 @@ /**

*/
resetCurrent: fsm_1.assign({ current: -1 }),
resetCurrent: () => {
c.current = -1;
},
/**

@@ -114,42 +147,38 @@ * Triggers onInput callback

return;
view.options.onInput.call(view, e.event);
c.retrieveSuggestions(e.event);
},
/**
* Narrows country search box
*/
countryInput: () => { },
/**
* Clears ARIA announcement fields
*/
clearAnnouncement: () => view.announce(""),
clearAnnouncement: () => {
c.announce("");
},
/**
* Renders suggestion within list
*/
renderSuggestions: (c, e) => {
renderContexts: (_, e) => {
if (e.type !== "CHANGE_COUNTRY")
return;
c.renderContexts();
},
/**
* Renders suggestion within list
*/
renderSuggestions: (_, e) => {
if (e.type !== "SUGGEST")
return;
view.list.innerHTML = "";
const id = view.list.id;
const s = c.suggestions;
s.forEach(({ suggestion }, i) => {
const li = view.options.document.createElement("li");
li.textContent = suggestion;
li.setAttribute("aria-selected", "false");
li.setAttribute("tabindex", "-1");
li.setAttribute("aria-posinset", `${i + 1}`);
li.setAttribute("aria-setsize", s.length.toString());
li.setAttribute("role", "option");
jsutil_1.setStyle(li, view.options.liStyle);
li.id = `${id}_${i}`;
view.list.appendChild(li);
});
view.announce(`${s.length} addresses available`);
c.renderSuggestions();
},
/**
* Update context.suggestions
* Update suggestions
*/
updateSuggestions: fsm_1.assign({
suggestions: (c, e) => {
if (e.type !== "SUGGEST")
return c.suggestions;
return e.suggestions;
},
current: () => -1,
}),
updateSuggestions: (_, e) => {
if (e.type !== "SUGGEST")
return;
c.updateSuggestions(e.suggestions);
},
/**

@@ -159,9 +188,5 @@ * Hides list and runs callback

close: (_, e) => {
let reason = "blur";
if (e.type === "CLOSE")
reason = e.reason;
jsutil_1.hide(view.list);
if (e.type === "CLOSE" && e.reason === "esc")
jsutil_1.update(view.input, "");
view.options.onClose.call(view, reason);
return c.close(e.reason);
c.close();
},

@@ -172,4 +197,3 @@ /**

open: () => {
jsutil_1.show(view.list);
view.options.onOpen.call(view);
c.open();
},

@@ -180,3 +204,3 @@ /**

expand: () => {
view.ariaAnchor().setAttribute("aria-expanded", "true");
c.ariaExpand();
},

@@ -187,23 +211,17 @@ /**

contract: () => {
view.ariaAnchor().setAttribute("aria-expanded", "false");
c.ariaContract();
},
/**
* Assigns context.message
* Assigns notification message
*/
updateMessage: fsm_1.assign({
message: (c, e) => {
if (e.type !== "NOTIFY")
return c.message;
return e.message;
},
}),
updateMessage: (_, e) => {
if (e.type !== "NOTIFY")
return;
c.notification = e.notification;
},
/**
* Renders message container and current message
*/
renderNotice: (c) => {
view.list.innerHTML = "";
view.input.setAttribute("aria-activedescendant", "");
view.message.textContent = c.message;
view.announce(c.message);
view.list.appendChild(view.message);
renderNotice: () => {
c.renderNotice();
},

@@ -213,23 +231,33 @@ /**

*/
next: fsm_1.assign({
current: (c) => c.current + 1 > view.list.children.length - 1
? 0 // Wrap to first elem
: c.current + 1,
}),
next: () => {
c.next();
},
/**
* Selects previous element in list. Wraps to bottom if at top
*/
previous: fsm_1.assign({
current: (c) => c.current - 1 < 0
? view.list.children.length - 1 // Wrap to last elem
: c.current - 1,
}),
previous: () => {
c.previous();
},
/**
* Triggers select on current context or clicked element
*/
selectCountry: (_, e) => {
if (e.type !== "SELECT_COUNTRY")
return;
const co = e.contextDetails;
if (!co)
return;
c.applyContext(co);
c.notification = `Country switched to ${co.name} ${co.icon}`;
},
/**
* Triggers select on current suggestion or clicked element
*/
select: (_, e) => {
if (e.type !== "SELECT")
selectAddress: (_, e) => {
if (e.type !== "SELECT_ADDRESS")
return;
view.options.onSelect.call(view, e.suggestion);
view.announce(`The address ${e.suggestion.suggestion} has been applied to this form`);
const s = e.suggestion;
if (!s)
return;
c.applySuggestion(s);
},

@@ -236,0 +264,0 @@ },

@@ -1,3 +0,2 @@

import { Controller } from "./controller";
import { ControllerOptions } from "./index";
import { ControllerOptions, Controller } from "./controller";
interface OnBindOptions {

@@ -4,0 +3,0 @@ config: ControllerOptions;

@@ -7,3 +7,3 @@ "use strict";

const jsutil_1 = require("@ideal-postcodes/jsutil");
const index_1 = require("./index");
const setup_1 = require("./setup");
const isTrue = () => true;

@@ -46,3 +46,3 @@ const getAnchors = (config, marker) => {

onAnchorFound({ anchor, scope, config: newConfig });
const c = index_1.setup(newConfig);
const c = setup_1.setup(newConfig);
jsutil_1.markLoaded(anchor, marker);

@@ -49,0 +49,0 @@ onBind(c);

import { Client } from "@ideal-postcodes/core-axios";
import { AddressSuggestion, Address } from "@ideal-postcodes/api-typings";
import { AddressSuggestion, Address } from "@ideal-postcodes/jsutil";
export declare type QueryOptions = Record<string, string>;

@@ -4,0 +4,0 @@ /**

@@ -1,2 +0,2 @@

import { autocomplete, lookupUdprn, lookupUmprn, } from "@ideal-postcodes/core-axios";
import { autocomplete } from "@ideal-postcodes/core-axios";
/**

@@ -50,7 +50,8 @@ * @hidden

resolve(suggestion) {
const { umprn, udprn } = suggestion;
if (umprn !== undefined)
return lookupUmprn({ client: this.client, umprn });
return lookupUdprn({ client: this.client, udprn });
return autocomplete
.gbr(this.client, suggestion.id, {
query: { api_key: this.client.config.api_key },
})
.then((response) => response.body.result);
}
}
/**
* @module Controller
*/
import { View } from "./view";
import { Announce } from "./announcer";
import { Callbacks, Listener } from "./callbacks";
import { Context, Details, ContextDetails } from "./contexts";
import { DebouncedFunc } from "lodash";
import { ApiCache, QueryOptions } from "./cache";
import { AddressSuggestion, Address } from "@ideal-postcodes/api-typings";
import { ControllerOptions } from "./index";
import { AddressSuggestion, Address } from "@ideal-postcodes/jsutil";
import { Client } from "@ideal-postcodes/core-axios";
import { Config } from "@ideal-postcodes/core-axios/dist/client";
import { ViewService, CloseReason } from "./state";
import { SelectorNode, CSSStyle, OutputFields, NamedFields, IdGen } from "@ideal-postcodes/jsutil";
/**
* Configuration options for an Address Finder instance
*/
export interface ControllerOptions extends Partial<Callbacks>, Partial<Omit<Config, "api_key">> {
/**
* CSS selector or HTML Element which specifies the `<input>` field which the
* Address Finder View should bind.
*/
inputField?: SelectorNode;
/**
* API Key from your Ideal Postcodes account. Typically begins `ak_`
*/
apiKey: string;
/**
* Scopes the operable area of the DOM
*
* @default
*
* `window.document`
*/
scope?: Document | HTMLElement | string;
/**
* Specify the Document to operate on
*
* @default
*
* `window.document`
*/
document?: Document;
/**
* Default Country
*
* @default
*
* `"GBR"`
*/
defaultCountry?: Context;
/**
* Narrow the countries you wish to support
*
* Setting this to an empty array (default) will enable all countries
*
* Setting this to a single country will disable country selection and hide the country selection toolbar
*
* @default
*
* `[]`
*/
restrictCountries?: Context[];
/**
* Provide a custom list of possible contexts to select a new country or context from
*/
contexts?: Record<string, Details>;
/**
* Specify parent element for output fields to looking for them to narrow search area
*/
outputScope?: string | HTMLElement | Document | null;
/**
* An object specifying where address field data points should be piped.
*
* The attribute of the document should be the same as the address attribute
* as found in the documentation. E.g. `line_1`, `post_town`, `postcode`.
*
* You may use a CSS selector `string` or a `HTMLElement`. E.g.
* `{ line_1: "#line_1" }` or `{ line_1: document.getElementById("line_1") }`
*
* Using an `HTMLElement` as an `outputField` selector has the effect of eagerly binding the Address Finder instance to your output fields. When using `string` selectors, Address Finder will bind to your ouput fields when when an address is selected.
*/
outputFields?: OutputFields;
/**
* An object specifying the `name`s of HTML Input Elements to target for address population
*
* This will fallback to `aria-name` if a name cannot be detected
*/
names?: NamedFields;
/**
* An object specifying the labels associated with HTML Input Elements to target for address population
*/
labels?: NamedFields;
/**
* Optional. An optional field to remove organisation name from address lines.
*
* This is `false` by default.
*/
removeOrganisation?: boolean;
/**
* An optional field to check whether the key is usable against the Ideal
* Postcodes API. This should be used in conjunction with the
* `onFailedCheck` callback to specify the necessary behaviour when the API
* Key is not in a usable state. This is `true` by default.
*/
checkKey?: boolean;
/**
* Configures which WAI-ARIA specification version Address Finder should target.
*
* - `"1.1"` will target the most recent spec
* - `"1.0"` will enable some regressions to support the 1.0 spec.
*
* Although 1.1 was released in 2017, this currently defaults to "1.0" as it receives the widest support among screen readers. VoiceOver (for MacOS and iOS) and NVDA in particular benefit from this.
*
* Defaults to "1.0"
*/
aria?: "1.0" | "1.1";
/**
* Automatically aligns address finder
*
* @default true
*/
alignToInput?: boolean;
/**
* Offset of AddressFinder from input in pixels
*
* @default 2
*/
offset?: number;
/**
* An optional field to convert the case of the Post Town from upper case
* into title case. E.g. `"LONDON"` becomes `"London".` Default is `true`
*/
titleizePostTown?: boolean;
/**
* Optional configuration object to apply to address queries
*/
queryOptions?: QueryOptions;
/**
* Sets the `autocomplete=` attribute of the input element. Setting this attribute aims to prevent some browsers (particularly Chrome) from providing a clashing autofill overlay.
*
* The best practice for this attribute breaks over time (see https://stackoverflow.com/questions/15738259/disabling-chrome-autofill) and is specific to different forms. If you are observing chrome's autofill clashing on your form, update this attribute to the best practice du jour.
*
* @default `"none"`
*/
autocomplete?: string;
/**
* Inject stylesheet into DOM to style Address Finder with default theme. Default is `true`
*
* Styling of the Address Finder can be achieved using a CSS file. Set this to `false` if you wish to do this
*
* - `true` Injects the default styles into the DOM
* - `string` e.g. `https://cdn.jsdelivr.net/npm/@ideal-postcodes/address-finder@1.1.1/css/address-finder.min.css` will include a CSS Stylesheet in the DOM with the src set as the string
*/
injectStyle?: boolean | string;
/**
* Message in input placeholder when address results are suggested
*
* Defaults to `"Try the first line or postal code of your address"`
*/
msgPlaceholder?: string;
/**
* Message in input placeholder when country suggestions are presented
*
* Defaults to `"Select your country"`
*/
msgPlaceholderCountry?: string;
/**
* Fallback message in case communication message with API fails
*
* Defaults to `"Please enter your address manually"`
*/
msgFallback?: string;
/**
* Initial message when Address Finder opens an no query is available
*
* Defaults to `"Start typing to find address"`
*/
msgInitial?: string;
/**
* Aria-label attached to country select bytton
*
* Defaults to `"Click to change your country"`
*/
msgCountryToggle?: string;
/**
* Message presented when no matches found for a particular query
*
* Defaults to `"No matches found"`
*/
msgNoMatch?: string;
/**
* Aria-label attached to the suggestion list. Prompts screen reader user on how to operate list
*
* Defaults to `"Select your address"`
*
* @default `"Select your address"`
*/
msgList?: string;
/**
* CSS class assigned to Address Finder element. This element is the main visible element containing address suggestions, messages and toolbar underneath the address finder
*
* Defaults to `"idpc_af"`
*
* @default `"idpc_af"`
*/
mainClass?: string;
/**
* CSS class assigned to message box
*
* Defaults to `"idpc_error"`
*
* @default `"idpc_error"`
*/
messageClass?: string;
/**
* CSS class assigned to the AddressFinder container/wrapper
*
* Defaults to `"idpc_autocomplete"`
*
* @default `"idpc_autocomplete"`
*/
containerClass?: string;
/**
* CSS class assigned to suggestion list
*
* Defaults to `"idpc_ul"`
*
* @default `"idpc_ul"`
*/
listClass?: string;
/**
* CSS class assigned to toolbar at bottom of Address Finder
*
* @default `"idpc_toolbar"`
*/
toolbarClass?: string;
/**
* CSS class assigned to country toggle button
*
* @default `"idpc_country"`
*/
countryToggleClass?: string;
/**
* Suppresses `county` from being populated if set to `false`
*
* @default true
*/
populateCounty?: boolean;
/**
* Suppresses `organisation_name` from being populated if set to `false`
*
* @default true
*/
populateOrganisation?: boolean;
/**
* Applies additional styling to the input field. Ideal for quick tweaks. Accepts CSSStyleDeclaration object
* Input styles are restored to original when controller is detached from DOM
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* inputStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
inputStyle?: CSSStyle;
/**
* Applies additional styling to the the suggestion list. Accepts CSSStyleDeclaration object
*
* `style` encapsulates all visible elements of Address Finder. This element is actively shown/hidden when AddressFinder is toggled
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* listStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
listStyle?: CSSStyle;
/**
* Applies additional styling to the the Address Finder container element. Accepts CSSStyleDeclaration object
* `containerStyle encapsulates all elements of Address Finder including the input, ARIA controls
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* containerStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
containerStyle?: CSSStyle;
/**
* Applies additional styling to the the Address Finder Main Component. The Main Component contains the visible elements of the Address Finder such as the address suggestion list, toolbar and messages which appears underneath the input field.
*
* Accepts CSSStyleDeclaration object
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* mainStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
mainStyle?: CSSStyle;
/**
* Applies additional styling to the the Address Finder list element. Accepts CSSStyleDeclaration object
*
* @default
*
* `{}`
*
* @example
*
* ```javascript
* {
* liStyle: {
* backgroundColor: "#000",
* },
* }
* ```
*/
liStyle?: CSSStyle;
/**
* Hide a list of HTML elements when Postcode Lookup is instantiated
*
* Specify these elements using query selectors or direct HTMLElement references
*
* @default []
*/
hide?: (string | HTMLElement)[];
/**
* Message shown to user to unhide address fields if `hide` attribute is configured
*
* @default "Enter address manually"
*/
msgUnhide?: string;
/**
* Specify a clickable element to unhide elements hidden with `hide`
*
* @default null
*/
unhide?: string | HTMLElement | null;
/**
* Class of clickable unhide element
*
* @default "idpc-unhide"
*/
unhideClass?: string;
}
/**
* @hidden

@@ -17,2 +385,8 @@ */

*/
interface RetrieveSuggestions {
(event: Event): Promise<Controller>;
}
/**
* @hidden
*/
export interface StoredOptions extends Required<Omit<ControllerOptions, keyof Config>>, Omit<Config, "api_key"> {

@@ -34,6 +408,2 @@ }

*
* More concretely, the instantiation of a controller instance creates:
* - A user interface instance `View`
* - An instance of the [Ideal Postcodes Browser Client](https://github.com/ideal-postcodes/core-axios)
*
* The role of the controller is to bind to events produced by the user

@@ -69,9 +439,127 @@ * interface and take appropriate action including querying the API,

/**
* View instance
* Caches previous placeholder value for input
*/
view: View;
placeholderCache: string | undefined;
/**
* Reference to input DOM element
*/
input: HTMLInputElement;
/**
* Reference to Address Finder message DOM element
*/
message: HTMLLIElement;
/**
* Reference to container wrapping AddressFinder elements DOM element. This includes the main component, input fields and WAI-ARIA controls
*/
container: HTMLDivElement;
/**
* Reference to inner container wrapping list and toolbar
*/
mainComponent: HTMLDivElement;
/**
* Reference to Address Suggestion list DOM element
*/
list: HTMLUListElement;
/**
* Reference to toolbar at bottom of finder list
*/
toolbar: HTMLDivElement;
/**
* Reference to country select toggle button
*/
countryToggle: HTMLSpanElement;
/**
* Reference to country icon
*/
countryIcon: HTMLSpanElement;
/**
* Reference to country toggle message
*/
countryMessage: HTMLSpanElement;
/**
* Reference to clickable Unhide link
*/
unhide: HTMLElement;
/**
* Input element input event listener
*/
inputListener: Listener<"input">;
/**
* Input blur event listener
*/
blurListener: Listener<"blur">;
/**
* Input focus event listener
*/
focusListener: Listener<"focus">;
/**
* Input keydown event listener
*/
keydownListener: Listener<"keydown">;
/**
* Unhide click event listener
*/
unhideEvent: Listener<"click">;
/**
* Address Finder state machine
*/
fsm: ViewService;
/**
* ID generation method
*/
ids: IdGen;
/**
* Reference to accessibility announcer
*/
announce: Announce;
/**
* Reference to alerts container
*/
alerts: HTMLDivElement;
/**
* Caches input style prior to Address Finder attachment
*/
inputStyle: string | null;
/**
* Debounced method used to retrieve suggestions
*/
retrieveSuggestions: DebouncedFunc<RetrieveSuggestions>;
/**
* Current search context
*/
context: string;
/**
* Current list of address suggestions
*/
suggestions: AddressSuggestion[];
/**
* Current list of context suggestions
*/
contextSuggestions: ContextDetails[];
/**
* Current notification to be shown to user
*/
notification: string;
/**
* Index of current elem in list selected
*/
current: number;
constructor(options: ControllerOptions);
/**
* Sets placeholder and caches previous result
* @hidden
*/
setPlaceholder(msg: string): void;
/**
* Unsets any placeholder value to original
* @hidden
*/
unsetPlaceholder(): void;
/**
* Returns current highlighted context
* @hidden
*/
currentContext(): ContextDetails;
/**
* Binds to DOM and begin DOM mutations
*
* @hidden

@@ -87,19 +575,27 @@ */

/**
* Produces a function to be bound to an instance of `Autocomplete.View`.
* It executes suggestion search when address input is updated
*
* @private
* Render available country options
*/
_onInput(): (this: View, event: Event) => Promise<View>;
renderContexts(): void;
/**
* Produces a function to be bound to an instance of `Autocomplete.View`.
* Populates fields with correct address when suggestion selected
*
* @private
* Render current address suggestions
*/
_onSelect(): (this: View, suggestion: AddressSuggestion) => Promise<View>;
renderSuggestions(): void;
/**
* Updates current li in list to active descendant
*/
goToCurrent(): void;
/**
* Marks aria component as opened
*/
ariaExpand(): void;
/**
* Marks aria component as closed
*/
ariaContract(): void;
/**
* Resolves a suggestion to full address and apply results to form
*/
applySuggestion(suggestion: AddressSuggestion): Promise<Controller>;
/**
* Writes a selected to the input fields specified in the controller config
*
* @public
*/

@@ -110,6 +606,122 @@ populateAddress(address: Address): void;

* cache to prevent stale searches
*/
setQueryOptions(options: QueryOptions): void;
/**
* Adds Address Finder to DOM
* - Wraps input with container
* - Appends suggestion list to container
* - Enables listeners
* - Starts FSM
*/
attach(): Controller;
/**
* Removes Address Finder from DOM
* - Disable listeners
* - Removes sugestion list from container
* - Appends suggestion list to container
* - Enables listeners
* - Stops FSM
*/
detach(): Controller;
/**
* Sets message as a list item, no or empty string removes any message
*/
setMessage(notification: string): Controller;
/**
* Returns HTML Element which recevies key aria attributes
*
* @public
* @hidden
*/
setQueryOptions(options: QueryOptions): void;
ariaAnchor(): HTMLElement;
/**
* Returns current address query
*/
query(): string;
/**
* Set address finder suggestions
*/
setSuggestions(suggestions: AddressSuggestion[], query: string): Controller;
/**
* Close address finder
*/
close(reason?: CloseReason): void;
/**
* Updates suggestions and resets current selection
* @hidden
*/
updateSuggestions(s: AddressSuggestion[]): void;
/**
* Applies context to API cache
* @hidden
*/
applyContext(details: ContextDetails): void;
/**
* Renders notification box
* @hidden
*/
renderNotice(): void;
/**
* Open address finder
* @hidden
*/
open(): void;
/**
* Sets next suggestion as current
* @hidden
*/
next(): Controller;
/**
* Sets previous suggestion as current
* @hidden
*/
previous(): Controller;
/**
* Given a HTMLLiElement, scroll parent until it is in view
* @hidden
*/
scrollToView(li: HTMLElement): Controller;
/**
* Moves currently selected li into view
* @hidden
*/
goto(i: number): Controller;
/**
* Returns true if address finder is open
*/
opened(): boolean;
/**
* Returs false if address finder is closed
*/
closed(): boolean;
/**
* Creates a clickable element that can trigger unhiding of fields
*/
createUnhide(): HTMLElement;
/**
* Removes unhide elem from DOM
*/
unmountUnhide(): void;
hiddenFields(): HTMLElement[];
/**
* Hides fields marked for hiding
*/
hideFields(): void;
/**
* Unhides fields marked for hiding
*/
unhideFields(): void;
}
/**
* Event handler: Fires on "keyDown" event of search field
* @hidden
*/
export declare const _onKeyDown: (c: Controller) => Listener<"keydown">;
/**
* 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 declare const findOrCreate: <T>(scope: HTMLElement | Document, q: string | T | null, create?: (() => T) | undefined) => T;
export {};

@@ -5,8 +5,11 @@ /**

/* eslint-disable no-invalid-this */
import { View, findOrCreate } from "./view";
import { announcer } from "./announcer";
import debounce from "lodash/debounce";
import { defaultContexts, toContextList, } from "./contexts";
import { ApiCache } from "./cache";
import { addStyle } from "./css";
import { addStyle, computeOffset } from "./css";
import { Client, checkKeyUsability } from "@ideal-postcodes/core-axios";
import { getScope, getDocument, populateAddress, } from "@ideal-postcodes/jsutil";
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";
/**

@@ -39,2 +42,8 @@ * @hidden

queryOptions: {},
alignToInput: true,
offset: 2,
// Country
defaultCountry: "GBR",
restrictCountries: [],
contexts: defaultContexts,
// Messages

@@ -45,7 +54,15 @@ msgFallback: "Please enter your address manually",

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: {},

@@ -80,2 +97,3 @@ listStyle: {},

onUnhide: NOOP,
onCountrySelected: NOOP,
};

@@ -92,6 +110,2 @@ /**

*
* More concretely, the instantiation of a controller instance creates:
* - A user interface instance `View`
* - An instance of the [Ideal Postcodes Browser Client](https://github.com/ideal-postcodes/core-axios)
*
* The role of the controller is to bind to events produced by the user

@@ -122,24 +136,158 @@ * interface and take appropriate action including querying the API,

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 = toContextList(this.options.contexts, this.options.restrictCountries);
this.client = new Client({ ...this.options, api_key: this.options.apiKey });
this.cache = new ApiCache(this.client);
this.view = new View({
...this.options,
scope: this.scope,
document: this.document,
onInput: debounce(this._onInput(), 100, {
leading: true,
trailing: true,
maxWait: 100,
}),
onSelect: this._onSelect(),
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)
.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().icon;
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);
// 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();
// 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);
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];
return {
code: this.context,
name: c.name,
icon: c.icon,
};
}
/**
* Binds to DOM and begin DOM mutations
*
* @hidden
*/
load() {
this.view.attach();
this.attach();
addStyle(this);

@@ -174,64 +322,109 @@ this.options.onLoaded.call(this);

/**
* Produces a function to be bound to an instance of `Autocomplete.View`.
* It executes suggestion search when address input is updated
*
* @private
* Render available country options
*/
_onInput() {
const self = this;
return function (event) {
self.options.onInput.call(this, event);
const query = this.query();
if (query.trim().length === 0) {
this.setMessage(self.options.msgInitial);
return Promise.resolve(this);
}
return self.cache
.query(query, self.options.queryOptions)
.then((suggestions) => {
self.options.onSuggestionsRetrieved.call(self, suggestions);
return this.setSuggestions(suggestions, query);
})
.catch((error) => {
if (this.query() === query)
this.setMessage(self.options.msgFallback);
self.options.onSuggestionError.call(self, error);
return self.view;
renderContexts() {
this.list.innerHTML = "";
this.contextSuggestions.forEach((contextDetails, i) => {
const { name } = contextDetails;
const li = this.options.document.createElement("li");
li.textContent = name;
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`);
}
/**
* Produces a function to be bound to an instance of `Autocomplete.View`.
* Populates fields with correct address when suggestion selected
*
* @private
* Render current address suggestions
*/
_onSelect() {
const self = this;
return function (suggestion) {
self.options.onAddressSelected.call(self, suggestion);
return self.cache
.resolve(suggestion)
.then((address) => {
if (address === null)
throw "Unable to retrieve address";
self.options.onAddressRetrieved.call(self, address);
self.populateAddress(address);
return this;
})
.catch((error) => {
this.open();
this.setMessage(self.options.msgFallback);
self.options.onSearchError.call(self, error);
return error;
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)
.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
*
* @public
*/
populateAddress(address) {
this.view.unhideFields();
this.unhideFields();
populateAddress({

@@ -249,4 +442,2 @@ address,

* cache to prevent stale searches
*
* @public
*/

@@ -257,2 +448,366 @@ setQueryOptions(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);
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);
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;
}
/**
* 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) {
const context = details.code;
this.context = context;
this.setQueryOptions({ ...this.options.queryOptions, context });
this.countryIcon.innerText = details.icon;
this.announce(`Country switched to ${details.name}`);
}
/**
* 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 e = 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)
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.contextSuggestions[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" });
};
/**
* 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;
};

@@ -10,1 +10,12 @@ import { Controller } from "./controller";

export declare const addStyle: (c: Controller) => undefined | HTMLStyleElement | HTMLLinkElement;
interface Offset {
marginTop: string;
}
interface Empty {
}
/**
* Returns a negative offset which can be used to correctly align input box
* @hidden
*/
export declare const computeOffset: (c: Controller) => Offset | Empty;
export {};

@@ -7,3 +7,103 @@ import { idpcState, loadStyle, isString, injectStyle, } from "@ideal-postcodes/jsutil";

*/
const d = ".idpc_ul.hidden{display:none}div.idpc_autocomplete{position:relative;margin:0;padding:0;border:0}div.idpc_autocomplete>input{display:block}div.idpc_autocomplete>ul{position:absolute;left:0;z-index:999;min-width:100%;box-sizing:border-box;list-style:none;padding:0;border-radius:.3em;margin:.2em 0 0;background:#fff;border:1px solid rgba(0,0,0,.3);box-shadow:.05em .2em .6em rgba(0,0,0,.2);text-shadow:none;max-height:250px;overflow-y:scroll}div.idpc_autocomplete>ul>li{position:relative;padding:.2em .5em;cursor:pointer}div.idpc_autocomplete>ul>li:hover{background:#b8d3e0;color:#000}div.idpc_autocomplete>ul>li.idpc_error{font-style:italic;background-color:#eee;cursor:default!important}div.idpc_autocomplete>ul>li[aria-selected=true]{background:#3d6d8f;color:#fff;z-index:1000}div.idpc_autocomplete>.idpc-unhide{font-size:90%;text-decoration:underline;cursor:pointer}@supports (transform:scale(0)){div.idpc_autocomplete>ul{transition:.3s cubic-bezier(.4, .2, .5, 1.4);transform-origin:1.43em -0.43em}div.idpc_autocomplete>ul:empty,div.idpc_autocomplete>ul[hidden]{opacity:0;transform:scale(0);display:block;transition-timing-function:ease}}";
const d = `
.idpc_af.hidden{
display:none;
}
div.idpc_autocomplete{
position:relative;
margin:0 0 0 0 !important;
padding:0;
border:0;
color: #28282B;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
div.idpc_autocomplete>input{
display:block;
}
div.idpc_af{
position:absolute;
left:0;
z-index:2000;
min-width:100%;
box-sizing:border-box;
border-radius:3px;
background:#fff;
border:1px solid rgba(0,0,0,.3);
box-shadow:.05em .2em .6em rgba(0,0,0,.2);
text-shadow:none;
padding:0;
}
div.idpc_af>ul{
list-style:none;
padding:0;
max-height:250px;
overflow-y: scroll;
margin: 0 0 0 0 !important;
}
div.idpc_af>ul>li{
position:relative;
padding:.2em .5em;
cursor:pointer;
margin:0 0 0 0 !important;
}
div.idpc_toolbar{
padding: .3em .5em;
border-top: 1px solid rgba(0,0,0,0.3);
text-align: right;
}
div.idpc_af>ul>li:hover{
background-color: #E5E4E2;
}
div.idpc_af>ul>li.idpc_error{
padding: .5em;
text-align: center;
cursor:default!important;
}
div.idpc_af>ul>li.idpc_error:hover{
background: #fff;
cursor:default!important;
}
div.idpc_af>ul>li[aria-selected=true]{
background-color: #E5E4E2;
z-index:3000;
}
div.idpc_autocomplete>.idpc-unhide{
font-size:0.9em;
text-decoration:underline;
cursor:pointer;
}
div.idpc_af>div>span{
padding: .2em .5em;
border-radius: 3px;
cursor:pointer;
font-size: 110%;
}
span.idpc_icon {
font-size:1.2em;
line-height: 1em;
vertical-align: middle;
}
div.idpc_toolbar>span span.idpc_country {
margin-right: 0.3em;
max-width: 0;
font-size: 0.9em;
-webkit-transition: max-width 500ms ease-out;
transition: max-width 500ms ease-out;
display: inline-block;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
}
div.idpc_autocomplete>div>div>span:hover span.idpc_country {
max-width: 7em;
}
div.idpc_autocomplete>div>div>span:hover{
background-color: #E5E4E2;
-webkit-transition: background-color 500ms ease;
-ms-transition: background-color 500ms ease;
transition: background-color 500ms ease;
}
`;
/**

@@ -35,1 +135,31 @@ * Injects CSS style into DOM

};
/**
* Returns a negative offset which can be used to correctly align input box
* @hidden
*/
export const computeOffset = (c) => {
let offset;
const input = c.input;
if (c.options.alignToInput === false)
return {};
try {
const w = c.options.document.defaultView;
if (!w)
return {};
offset = w.getComputedStyle(input).marginBottom;
}
catch (_) {
return {};
}
if (!offset)
return {};
const nOffset = parseInt(offset.replace("px", ""), 10);
if (isNaN(nOffset))
return {};
if (nOffset === 0)
return {};
const negativeOffset = nOffset * -1 + c.options.offset;
return {
marginTop: negativeOffset + "px",
};
};
/**
* @module Address-Finder Exports
*/
import { Address, AddressSuggestion } from "@ideal-postcodes/api-typings";
import { watch } from "./watch";
import { Config } from "@ideal-postcodes/core-axios/dist/client";
import { Controller, defaults } from "./controller";
import { QueryOptions } from "./cache";
import { SelectorNode, OutputFields, NamedFields } from "@ideal-postcodes/jsutil";
import { View, ViewOptions, OnOpen, OnBlur, OnClose, OnFocus, OnInput, OnSelect, OnUnhide } from "./view";
export interface OnLoaded {
(this: Controller): void;
}
export interface OnFailedCheck {
(this: Controller, error: Error): void;
}
export interface OnSuggestionsRetrieved {
(this: Controller, suggestion: AddressSuggestion[]): void;
}
export interface OnAddressRetrieved {
(this: Controller, address: Address): void;
}
export interface OnSearchError {
(this: Controller, error: Error): void;
}
export interface OnSuggestionError {
(this: Controller, error: Error): void;
}
export interface OnMounted {
(this: View): void;
}
export interface OnRemove {
(this: View): void;
}
export interface OnAddressSelected {
(this: Controller, suggestion: AddressSuggestion): void;
}
export interface OnAddressPopulated {
(this: Controller, address: Address): void;
}
import { Controller } from "./controller";
/**
* Configuration options for an Address Finder instance
*/
export interface ControllerOptions extends Partial<Omit<Config, "api_key">>, Partial<Omit<ViewOptions, "scope" | "document">> {
/**
* CSS selector or HTML Element which specifies the `<input>` field which the
* Address Finder View should bind.
*/
inputField?: SelectorNode;
/**
* API Key from your Ideal Postcodes account. Typically begins `ak_`
*/
apiKey: string;
/**
* Scopes the operable area of the DOM
*
* @default
*
* `window.document`
*/
scope?: Document | HTMLElement | string;
/**
* Specify the Document to operate on
*
* @default
*
* `window.document`
*/
document?: Document;
/**
* Specify parent element for output fields to looking for them to narrow search area
*/
outputScope?: string | HTMLElement | Document | null;
/**
* An object specifying where address field data points should be piped.
*
* The attribute of the document should be the same as the address attribute
* as found in the documentation. E.g. `line_1`, `post_town`, `postcode`.
*
* You may use a CSS selector `string` or a `HTMLElement`. E.g.
* `{ line_1: "#line_1" }` or `{ line_1: document.getElementById("line_1") }`
*
* Using an `HTMLElement` as an `outputField` selector has the effect of eagerly binding the Address Finder instance to your output fields. When using `string` selectors, Address Finder will bind to your ouput fields when when an address is selected.
*/
outputFields?: OutputFields;
/**
* An object specifying the `name`s of HTML Input Elements to target for address population
*
* This will fallback to `aria-name` if a name cannot be detected
*/
names?: NamedFields;
/**
* An object specifying the labels associated with HTML Input Elements to target for address population
*/
labels?: NamedFields;
/**
* Optional. An optional field to remove organisation name from address lines.
*
* This is `false` by default.
*/
removeOrganisation?: boolean;
/**
* An optional field to check whether the key is usable against the Ideal
* Postcodes API. This should be used in conjunction with the
* `onFailedCheck` callback to specify the necessary behaviour when the API
* Key is not in a usable state. This is `true` by default.
*/
checkKey?: boolean;
/**
* Configures which WAI-ARIA specification version Address Finder should target.
*
* - `"1.1"` will target the most recent spec
* - `"1.0"` will enable some regressions to support the 1.0 spec.
*
* Although 1.1 was released in 2017, this currently defaults to "1.0" as it receives the widest support among screen readers. VoiceOver (for MacOS and iOS) and NVDA in particular benefit from this.
*
* Defaults to "1.0"
*/
aria?: "1.0" | "1.1";
/**
* An optional field to convert the case of the Post Town from upper case
* into title case. E.g. `"LONDON"` becomes `"London".` Default is `true`
*/
titleizePostTown?: boolean;
/**
* Optional configuration object to apply to address queries
*/
queryOptions?: QueryOptions;
/**
* Sets the `autocomplete=` attribute of the input element. Setting this attribute aims to prevent some browsers (particularly Chrome) from providing a clashing autofill overlay.
*
* The best practice for this attribute breaks over time (see https://stackoverflow.com/questions/15738259/disabling-chrome-autofill) and is specific to different forms. If you are observing chrome's autofill clashing on your form, update this attribute to the best practice du jour.
*
* @default "none"
*/
autocomplete?: string;
/**
* Inject stylesheet into DOM to style Address Finder with default theme. Default is `false`
*
* Styling of the Address Finder can be achieved using a CSS file. Set this to `false` if you wish to do this
*
* - `true` Injects the default styles into the DOM
* - `string` e.g. `https://cdn.jsdelivr.net/npm/@ideal-postcodes/address-finder@1.1.1/css/address-finder.min.css` will include a CSS Stylesheet in the DOM with the src set as the string
*/
injectStyle?: boolean | string;
/**
* Fallback message in case communication message with API fails
*
* Defaults to `"Please enter your address manually"`
*/
msgFallback?: string;
/**
* Initial message when Address Finder opens an no query is available
*
* Defaults to `"Start typing to find address"`
*/
msgInitial?: string;
/**
* Message presented when no matches found for a particular query
*
* Defaults to `"No matches found"`
*/
msgNoMatch?: string;
/**
* Aria-label attached to the suggestion list. Prompts screen reader user on how to operate list
*
* Defaults to `"Select your address"`
*/
msgList?: string;
/**
* CSS class assigned to message box
*
* Defaults to `"idpc_error"`
*
* Note this doesn't necessarily indicate an error
*/
messageClass?: string;
/**
* CSS class assigned to the AddressFinder container/wrapper
*
* Defaults to `"idpc_autocomplete"`
*/
containerClass?: string;
/**
* CSS class assigned to suggestion list (bound to `<ul>`)
*
* Defaults to `"idpc_ul"`
*/
listClass?: string;
/**
* Invoked when Address Finder has been successfully attached to the input element.
*/
onLoaded?: OnLoaded;
/**
* Invoked when `checkKey` is enabled and the key is discovered to be in an
* unusable state (e.g. daily limit reached, no balance, etc).
*/
onFailedCheck?: OnFailedCheck;
/**
* Invoked immediately after address suggestions are retrieved from the API.
* The first argument is an array of address suggestions.
*/
onSuggestionsRetrieved?: OnSuggestionsRetrieved;
/**
* Invoked when the Address Finder client has retrieved a full address from
* the API following a user accepting a suggestion. The first argument is
* an object representing the address that has been retrieved.
*/
onAddressRetrieved?: OnAddressRetrieved;
/**
* Invoked when view is attached to the DOM
*/
onMounted?: OnMounted;
/**
* Invoked when view is detached from the DOM
*/
onRemove?: OnRemove;
/**
* Invoked immediately after the user has selected a suggestion (either by
* click or keypress). The first argument is an object which represents the
* suggestion selected.
*/
onAddressSelected?: OnAddressSelected;
/**
* Invoked when selected address is populated into address fields of user
* address form
*/
onAddressPopulated?: OnAddressPopulated;
/**
* Invoked when an error has occurred following an attempt to retrieve a full
* address. i.e. the API request made after the user selects a suggestion.
*
* The first argument is an error instance (i.e. inherits from `Error`)
* representing the error which has occurred.
*
* In this scenario the user will also receive a message to manually input an
* address if address retrieval fails.
*/
onSearchError?: OnSearchError;
/**
* Invoked when an address suggestion retrieval request has failed.
*
* In this scenario the user will be alerted that no address suggestions
* could be found and to manually input an address.
*/
onSuggestionError?: OnSuggestionError;
/**
* Invoked when the Address Finder view opens (i.e. appears)
*/
onOpen?: OnOpen;
/**
* Invoked when `blur` event is dispatched by Address Finder input field
*/
onBlur?: OnBlur;
/**
* Invoked when the Address Finder view closes (i.e. disappears)
*/
onClose?: OnClose;
/**
* Invoked when `focus` event is dispatched by Address Finder input field
*/
onFocus?: OnFocus;
/**
* Invoked when `input` event is dispatched by Address Finder input field
*/
onInput?: OnInput;
/**
* Invoked when a suggestion has been selected
*/
onSelect?: OnSelect;
/**
* Invoked when hidden fields are unhidden (i.e. user selects an address or opts for manual input)
*/
onUnhide?: OnUnhide;
/**
* Suppresses `county` from being populated if set to `false`
*
* @default true
*/
populateCounty?: boolean;
/**
* Suppresses `organisation_name` from being populated if set to `false`
*
* @default true
*/
populateOrganisation?: boolean;
}
/**
* Configure and launch an instance of the Address Finder
*
* This method will create and return a new AddressFinder instance. It will also add a global reference to the controller at `AddressFinder.controllers`
*/
export declare const setup: (config: ControllerOptions) => Controller;
/**
* Configure and launch an instance of the Address Finder
*
* This is equivalent to invoking `setup` except inside a DOMContentLoaded event callback
*/
export declare const go: (config: ControllerOptions, d?: Document | undefined) => Promise<Controller | null>;
/**
* Cache of Address Finder controllers
*/
export declare const controllers: Controller[];
/**
* Namespace that exports Address Finder methods and classes
*/
export declare const AddressFinder: {
setup: (config: ControllerOptions) => Controller;
setup: (config: import("./controller").ControllerOptions) => Controller;
controllers: Controller[];

@@ -313,16 +14,3 @@ Controller: typeof Controller;

watch: import("./watch").Watch;
go: (config: ControllerOptions, d?: Document | undefined) => Promise<Controller | null>;
go: (config: import("./controller").ControllerOptions, d?: Document | undefined) => Promise<Controller | null>;
};
/**
* Configure Address Finder to watch for available address fields to bind
*/
export { watch };
/**
* Default Address Finder Controller configuration
*/
export { defaults };
export { ViewOptions };
/**
* Controller Export
*/
export { Controller };
/**
* @module Address-Finder Exports
*/
import { setup, go, controllers } from "./setup";
import { watch } from "./watch";
import { Controller, defaults } from "./controller";
/**
* Configure and launch an instance of the Address Finder
*
* This method will create and return a new AddressFinder instance. It will also add a global reference to the controller at `AddressFinder.controllers`
*/
export const setup = (config) => {
const c = new Controller(config);
controllers.push(c);
return c;
};
/**
* Configure and launch an instance of the Address Finder
*
* This is equivalent to invoking `setup` except inside a DOMContentLoaded event callback
*/
export const go = (config, d) => new Promise((resolve, _) => {
(d || document).addEventListener("DOMContentLoaded", (_) => {
const c = setup(config);
return resolve(c);
});
}).catch((_) => null);
/**
* Cache of Address Finder controllers
*/
export const controllers = [];
/**
* Namespace that exports Address Finder methods and classes

@@ -42,13 +18,1 @@ */

};
/**
* Configure Address Finder to watch for available address fields to bind
*/
export { watch };
/**
* Default Address Finder Controller configuration
*/
export { defaults };
/**
* Controller Export
*/
export { Controller };
import { StateMachine } from "@xstate/fsm";
import { AddressSuggestion } from "@ideal-postcodes/api-typings";
import { View, CloseReason } from "./view";
import { AddressSuggestion } from "@ideal-postcodes/jsutil";
import { ContextDetails } from "./contexts";
import { Controller } from "./controller";
export declare type CloseReason = "select" | "esc" | "blur";
/**

@@ -13,4 +15,4 @@ * @hidden

} | {
type: "SELECT";
suggestion: AddressSuggestion;
type: "SELECT_ADDRESS";
suggestion: AddressSuggestion | undefined;
} | {

@@ -26,2 +28,7 @@ type: "NEXT";

} | {
type: "CHANGE_COUNTRY";
} | {
type: "SELECT_COUNTRY";
contextDetails: ContextDetails | undefined;
} | {
type: "CLOSE";

@@ -31,3 +38,3 @@ reason: CloseReason;

type: "NOTIFY";
message: string;
notification: string;
};

@@ -38,5 +45,2 @@ /**

export interface Context {
message: string;
suggestions: AddressSuggestion[];
current: number;
}

@@ -50,2 +54,5 @@ /**

} | {
value: "suggesting_country";
context: Context;
} | {
value: "notifying";

@@ -75,8 +82,9 @@ context: Context;

export interface CreateOptions {
view: View;
c: Controller;
}
/**
* Creates a finite state machine that drives Address Finder UI
* @hidden
*/
export declare const create: Create;
export {};

@@ -1,3 +0,2 @@

import { createMachine, interpret, assign } from "@xstate/fsm";
import { update, hide, show, setStyle } from "@ideal-postcodes/jsutil";
import { createMachine, interpret } from "@xstate/fsm";
/**

@@ -24,12 +23,26 @@ * @hidden

/**
* @hidden
*/
const NEXT = { NEXT: { actions: ["next", "gotoCurrent"] } };
/**
* @hidden
*/
const PREVIOUS = { PREVIOUS: { actions: ["previous", "gotoCurrent"] } };
/**
* @hidden
*/
const RESET = { RESET: { actions: ["resetCurrent", "gotoCurrent"] } };
/**
* @hidden
*/
const CHANGE_COUNTRY = {
CHANGE_COUNTRY: { target: "suggesting_country" },
};
/**
* Creates a finite state machine that drives Address Finder UI
* @hidden
*/
export const create = ({ view }) => {
export const create = ({ c }) => {
const machine = createMachine({
initial: "closed",
context: {
suggestions: [],
message: view.options.msgInitial,
current: -1,
},
states: {

@@ -43,3 +56,3 @@ closed: {

target: "suggesting",
cond: (c) => c.suggestions.length > 0,
cond: () => c.suggestions.length > 0,
},

@@ -60,7 +73,26 @@ {

...INPUT,
...CHANGE_COUNTRY,
},
},
suggesting_country: {
entry: ["renderContexts", "gotoCurrent", "expand", "addCountryHint"],
exit: ["resetCurrent", "gotoCurrent", "contract", "clearHint"],
on: {
...CLOSE,
...NOTIFY,
...NEXT,
...PREVIOUS,
...RESET,
INPUT: {
actions: ["countryInput"],
},
SELECT_COUNTRY: {
target: "notifying",
actions: ["selectCountry"],
},
},
},
suggesting: {
entry: ["renderSuggestions", "gotoCurrent", "expand"],
exit: ["resetCurrent", "gotoCurrent", "contract"],
entry: ["renderSuggestions", "gotoCurrent", "expand", "addHint"],
exit: ["resetCurrent", "gotoCurrent", "contract", "clearHint"],
on: {

@@ -71,6 +103,7 @@ ...CLOSE,

...INPUT,
NEXT: { actions: ["next", "gotoCurrent"] },
PREVIOUS: { actions: ["previous", "gotoCurrent"] },
RESET: { actions: ["resetCurrent", "gotoCurrent"] },
SELECT: { target: "closed", actions: ["select"] },
...CHANGE_COUNTRY,
...NEXT,
...PREVIOUS,
...RESET,
SELECT_ADDRESS: { target: "closed", actions: ["selectAddress"] },
},

@@ -81,18 +114,16 @@ },

actions: {
addHint: () => {
c.setPlaceholder(c.options.msgPlaceholder);
},
addCountryHint: () => {
c.setPlaceholder(c.options.msgPlaceholderCountry);
},
clearHint: () => {
c.unsetPlaceholder();
},
/**
* Updates current li in list to active descendant
*/
gotoCurrent: (c) => {
const lis = view.list.children;
view.input.setAttribute("aria-activedescendant", "");
for (let i = 0; i < lis.length; i += 1) {
if (i === c.current) {
view.input.setAttribute("aria-activedescendant", lis[i].id);
lis[i].setAttribute("aria-selected", "true");
view.goto(i);
}
else {
lis[i].setAttribute("aria-selected", "false");
}
}
gotoCurrent: () => {
c.goToCurrent();
},

@@ -102,3 +133,5 @@ /**

*/
resetCurrent: assign({ current: -1 }),
resetCurrent: () => {
c.current = -1;
},
/**

@@ -110,42 +143,38 @@ * Triggers onInput callback

return;
view.options.onInput.call(view, e.event);
c.retrieveSuggestions(e.event);
},
/**
* Narrows country search box
*/
countryInput: () => { },
/**
* Clears ARIA announcement fields
*/
clearAnnouncement: () => view.announce(""),
clearAnnouncement: () => {
c.announce("");
},
/**
* Renders suggestion within list
*/
renderSuggestions: (c, e) => {
renderContexts: (_, e) => {
if (e.type !== "CHANGE_COUNTRY")
return;
c.renderContexts();
},
/**
* Renders suggestion within list
*/
renderSuggestions: (_, e) => {
if (e.type !== "SUGGEST")
return;
view.list.innerHTML = "";
const id = view.list.id;
const s = c.suggestions;
s.forEach(({ suggestion }, i) => {
const li = view.options.document.createElement("li");
li.textContent = suggestion;
li.setAttribute("aria-selected", "false");
li.setAttribute("tabindex", "-1");
li.setAttribute("aria-posinset", `${i + 1}`);
li.setAttribute("aria-setsize", s.length.toString());
li.setAttribute("role", "option");
setStyle(li, view.options.liStyle);
li.id = `${id}_${i}`;
view.list.appendChild(li);
});
view.announce(`${s.length} addresses available`);
c.renderSuggestions();
},
/**
* Update context.suggestions
* Update suggestions
*/
updateSuggestions: assign({
suggestions: (c, e) => {
if (e.type !== "SUGGEST")
return c.suggestions;
return e.suggestions;
},
current: () => -1,
}),
updateSuggestions: (_, e) => {
if (e.type !== "SUGGEST")
return;
c.updateSuggestions(e.suggestions);
},
/**

@@ -155,9 +184,5 @@ * Hides list and runs callback

close: (_, e) => {
let reason = "blur";
if (e.type === "CLOSE")
reason = e.reason;
hide(view.list);
if (e.type === "CLOSE" && e.reason === "esc")
update(view.input, "");
view.options.onClose.call(view, reason);
return c.close(e.reason);
c.close();
},

@@ -168,4 +193,3 @@ /**

open: () => {
show(view.list);
view.options.onOpen.call(view);
c.open();
},

@@ -176,3 +200,3 @@ /**

expand: () => {
view.ariaAnchor().setAttribute("aria-expanded", "true");
c.ariaExpand();
},

@@ -183,23 +207,17 @@ /**

contract: () => {
view.ariaAnchor().setAttribute("aria-expanded", "false");
c.ariaContract();
},
/**
* Assigns context.message
* Assigns notification message
*/
updateMessage: assign({
message: (c, e) => {
if (e.type !== "NOTIFY")
return c.message;
return e.message;
},
}),
updateMessage: (_, e) => {
if (e.type !== "NOTIFY")
return;
c.notification = e.notification;
},
/**
* Renders message container and current message
*/
renderNotice: (c) => {
view.list.innerHTML = "";
view.input.setAttribute("aria-activedescendant", "");
view.message.textContent = c.message;
view.announce(c.message);
view.list.appendChild(view.message);
renderNotice: () => {
c.renderNotice();
},

@@ -209,23 +227,33 @@ /**

*/
next: assign({
current: (c) => c.current + 1 > view.list.children.length - 1
? 0 // Wrap to first elem
: c.current + 1,
}),
next: () => {
c.next();
},
/**
* Selects previous element in list. Wraps to bottom if at top
*/
previous: assign({
current: (c) => c.current - 1 < 0
? view.list.children.length - 1 // Wrap to last elem
: c.current - 1,
}),
previous: () => {
c.previous();
},
/**
* Triggers select on current context or clicked element
*/
selectCountry: (_, e) => {
if (e.type !== "SELECT_COUNTRY")
return;
const co = e.contextDetails;
if (!co)
return;
c.applyContext(co);
c.notification = `Country switched to ${co.name} ${co.icon}`;
},
/**
* Triggers select on current suggestion or clicked element
*/
select: (_, e) => {
if (e.type !== "SELECT")
selectAddress: (_, e) => {
if (e.type !== "SELECT_ADDRESS")
return;
view.options.onSelect.call(view, e.suggestion);
view.announce(`The address ${e.suggestion.suggestion} has been applied to this form`);
const s = e.suggestion;
if (!s)
return;
c.applySuggestion(s);
},

@@ -232,0 +260,0 @@ },

@@ -1,3 +0,2 @@

import { Controller } from "./controller";
import { ControllerOptions } from "./index";
import { ControllerOptions, Controller } from "./controller";
interface OnBindOptions {

@@ -4,0 +3,0 @@ config: ControllerOptions;

import { Client, checkKeyUsability } from "@ideal-postcodes/core-axios";
import { NOOP } from "./controller";
import { loaded, toArray, getParent, generateTimer, markLoaded, getScope, } from "@ideal-postcodes/jsutil";
import { setup } from "./index";
import { setup } from "./setup";
const isTrue = () => true;

@@ -6,0 +6,0 @@ const getAnchors = (config, marker) => {

{
"name": "@ideal-postcodes/address-finder",
"version": "2.5.1",
"version": "3.0.0-beta.1",
"description": "Address Finder JS library backed by the Ideal Postcodes UK address search API",

@@ -29,3 +29,6 @@ "main": "dist/index.js",

"main",
"next"
{
"name": "beta",
"prerelease": true
}
]

@@ -101,4 +104,4 @@ },

"dependencies": {
"@ideal-postcodes/core-axios": "~3.0.7",
"@ideal-postcodes/jsutil": "~4.6.1",
"@ideal-postcodes/core-axios": "4.0.0",
"@ideal-postcodes/jsutil": "5.0.0",
"@xstate/fsm": "~1.6.0",

@@ -116,4 +119,3 @@ "lodash": "~4.17.20"

"@ideal-postcodes/api-fixtures": "~1.3.0",
"@ideal-postcodes/api-typings": "~2.1.0",
"@ideal-postcodes/doc-assets": "~1.0.6",
"@ideal-postcodes/openapi": "1.1.0",
"@ideal-postcodes/supported-browsers": "~2.5.0",

@@ -120,0 +122,0 @@ "@rollup/plugin-commonjs": "~21.0.0",

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc