Skip to content

Instantly share code, notes, and snippets.

@gecugamo
Created November 7, 2022 23:20
Show Gist options
  • Save gecugamo/5f37f6ae8829b908fa12fb63312f793c to your computer and use it in GitHub Desktop.
Save gecugamo/5f37f6ae8829b908fa12fb63312f793c to your computer and use it in GitHub Desktop.
kin-address-autocomplete.dist.js
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
debugger;
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var KinAddressAutocomplete_1;
import { html, LitElement } from 'lit';
import { customElement, property, query, queryAll, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { classMap } from 'lit/directives/class-map.js';
import { SmartyService } from '../../services/smarty-service/smarty-service.js';
import { debounce } from '../../utils/debounce.js';
import { getTextAreaHeight } from '../../utils/text-area-resizer.js';
import { isOnline } from '../../utils/is-online.js';
import { hostStyles, generalStyles, textareaStyles, labelStyles, listStyles, } from './kin-address-autocomplete.style.js';
import { v4 as uuidv4 } from 'uuid';
import { SmartyError } from '../../services/smarty-service/utils/handle-http-error.js';
/**
* Address Autocomplete Component
*
* @element kin-address-autocomplete
*
* @property key - Smarty embedded key.
* @property idProp - id attribute for textarea.
* @property [label='Address'] - Label for textarea.
* @property [value=''] - Value for textarea.
* @property [includeAddressMetadata=false] - Include extended address data in select event payload.
* @property suggestions - List of autocomplete suggestions.
* @property hasError - Set error styling on component.
* @property [debounceTimeout=0] - Time in milliseconds to debounce autocomplete queries.
* @property [displayApiErrors=true] - Show API failures by default when user is offline. Set to false to disable.
* @property [displayNoResultsHint=true] - Show a hit to the user when there are no results. Set to false to disable.
*
* @fires {CustomEvent<string>} KinAddressAutocomplete#kin-address-autocomplete-input Emitted when textarea receives user input
* @fires {CustomEvent<AddressPayload>} KinAddressAutocomplete#kin-address-autocomplete-select Emitted when suggestion is selected
* @fires {CustomEvent<string>} KinAddressAutocomplete#kin-address-autocomplete-submit Emitted when textarea receives enter key press
* @fires {CustomEvent<string>} KinAddressAutocomplete#kin-address-autocomplete-value-change Emitted when input value changes
* @fires {CustomEvent<string>} KinAddressAutocomplete#kin-address-autocomplete-clear Emitted when textarea is cleared via escape key
* @fires {CustomEvent<Error>} KinAddressAutocomplete#kin-address-autocomplete-error Emitted when non-ok response is returned from Smarty
*
* @cssprop [--kin-address-autocomplete-error-color=#B11030]
* @cssprop [--kin-address-autocomplete-label-color=#888d86]
* @cssprop [--kin-address-autocomplete-label-color--focus=#167c80]
* @cssprop [--kin-address-autocomplete-label-color--error=var(--kin-address-autocomplete-label-color)]
* @cssprop [--kin-address-autocomplete-label-color--focus--error=#545c52]
* @cssprop [--kin-address-autocomplete-textarea-color=#242221]
* @cssprop [--kin-address-autocomplete-textarea-color--focus=var(--kin-address-autocomplete-textarea-color)]
* @cssprop [--kin-address-autocomplete-textarea-color--error=var(--kin-address-autocomplete-textarea-color)]
* @cssprop [--kin-address-autocomplete-textarea-color--focus--error=var(--kin-address-autocomplete-textarea-color)]
* @cssprop [--kin-address-autocomplete-textarea-background-color=transparent]
* @cssprop [--kin-address-autocomplete-textarea-background-color--focus=var(--kin-address-autocomplete-textarea-background-color)]
* @cssprop [--kin-address-autocomplete-textarea-background-color--error=var(--kin-address-autocomplete-textarea-background-color)]
* @cssprop [--kin-address-autocomplete-textarea-background-color--focus--error=var(--kin-address-autocomplete-textarea-background-color)]
* @cssprop [--kin-address-autocomplete-textarea-border-color=#004a38]
* @cssprop [--kin-address-autocomplete-textarea-border-color--focus=#167c80]
* @cssprop [--kin-address-autocomplete-textarea-border-color--error=#b11030]
* @cssprop [--kin-address-autocomplete-textarea-border-color--focus--error=var(--kin-address-autocomplete-textarea-border-color--error)]
* @cssprop [--kin-address-autocomplete-suggestion-color=#757575]
* @cssprop [--kin-address-autocomplete-suggestion-color--hover=var(--kin-address-autocomplete-suggestion-color)]
* @cssprop [--kin-address-autocomplete-suggestion-color--focus=var(--kin-address-autocomplete-suggestion-color)]
* @cssprop [--kin-address-autocomplete-suggestion-match-color=#242221]
* @cssprop [--kin-address-autocomplete-suggestion-match-color--hover=var(--kin-address-autocomplete-suggestion-match-color)]
* @cssprop [--kin-address-autocomplete-suggestion-match-color--focus=var(--kin-address-autocomplete-suggestion-match-color)]
* @cssprop [--kin-address-autocomplete-suggestion-background-color=#fff]
* @cssprop [--kin-address-autocomplete-suggestion-background-color--hover=#fbf8f3]
* @cssprop [--kin-address-autocomplete-suggestion-background-color--focus=#fae7b8]
*/
let KinAddressAutocomplete = KinAddressAutocomplete_1 = class KinAddressAutocomplete extends LitElement {
constructor() {
super(...arguments);
this.key = undefined;
this.idProp = uuidv4();
this.label = 'Address';
this.value = '';
this.includeAddressMetadata = false;
this.hasError = false;
this.debounceTimeout = 0;
this.displayApiErrors = true;
this.displayNoResultsHint = true;
this.suggestions = [];
this.showSuggestionList = false;
this.activeIndex = -1;
this._noResults = false;
this._smartyError = null; // used to notify the user of a critical error on Smarty Streets API
this._errorNoticeContent = ''; // used to add content for an error
this._previousValue = '';
this._handleDocumentClick = (e) => {
if (e.composedPath().indexOf(this) === -1) {
this.showSuggestionList = false;
}
};
}
/**
* Called before update() to compute values needed during the update.
*/
willUpdate(propertyChanges) {
// react to changes to the key property
if (propertyChanges.has('key')) {
if (!this._smartyService && this.key) {
this._smartyService = new SmartyService(this.key);
this._debouncedFetchSuggestions = debounce(this, this._fetchSuggestions, this.debounceTimeout);
}
document.addEventListener('click', this._handleDocumentClick);
}
}
updated(propertyChanges) {
if (propertyChanges.has('value')) {
this._emitValueChange(this.value);
}
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('keydown', this._handleDocumentClick);
}
render() {
if (!this.key) {
return html `<p>
Please provide a
<a href="https://www.smarty.com/docs/cloud/authentication#htmlkeys">Smarty embedded key</a>.
</p>`;
}
return html ` <div
role="combobox"
aria-haspopup="listbox"
aria-owns="suggestion-list"
aria-expanded=${this._enableSuggestionList()}
>
<label id=${`${this.idProp}-suggestion-list-input`} for=${ifDefined(this.idProp)}>
<textarea
autocapitalize="off"
autocorrect="off"
rows="1"
spellcheck="false"
class=${classMap({ filled: !!this.value, error: this.hasError })}
id=${ifDefined(this.idProp)}
aria-autocomplete="list"
aria-controls=${`${this.idProp}-suggestion-list`}
aria-activedescendant=${this.listItems[this.activeIndex] === undefined
? ''
: this.listItems[this.activeIndex].id}
.value=${this.value}
@input=${this._handleInput}
@keydown=${this._handleKeyPress}
@focus=${this._handleFocus}
></textarea>
<span>${this.label}</span>
</label>
</div>
<ul
id=${`${this.idProp}-suggestion-list`}
role="listbox"
aria-labelledby=${`${this.idProp}-suggestion-list-input`}
class=${classMap({
hide: !this._enableSuggestionList(),
'notice-drop-down': true,
})}
>
${this.suggestions.map((suggestion, index) => html `
<li
role="option"
id=${`${this.idProp}-suggestion-${index}`}
@click=${() => this._handleSelect(suggestion)}
class=${index === this.activeIndex ? 'focus' : ''}
aria-selected=${index === this.activeIndex}
>
${this._suggestionContent(suggestion)}
${suggestion.entries > 1 ? this._caretIcon() : null}
</li>
`)}
</ul>
<div
id=${`${this.idProp}-notice`}
aria-labelledby=${`${this.idProp}-suggestion-list-input`}
class=${classMap({
hide: !this._enableErrorNotice(),
'notice-drop-down': true,
'has-error': !!this._smartyError,
})}
>
<span>${this._errorNoticeContent}</span>
</div>`;
}
/**
* Trigger the input event of the textarea element.
* Use in conjunction with the value prop to programmatically fetch suggestions.
*/
triggerInputEvent() {
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
/**
* Textarea input event callback.
*
* Handles keeping track of input value in state,
* emitting input event,
* and fetching suggestions.
*/
_handleInput(e) {
e.stopPropagation();
this.value = e.target.value;
this._emitInput(this.value);
this.showSuggestionList = true;
this._resizeTextArea();
this._debouncedFetchSuggestions(this.value);
}
/** Textarea focus event callback. */
_handleFocus() {
this.showSuggestionList = true;
}
/** Textarea and suggestion list keyboard navigability with up and down arrows, enter, and escape */
async _handleKeyPress(e) {
switch (e.key) {
case 'ArrowDown':
this._handleArrowDown(e);
break;
case 'ArrowUp':
this._handleArrowUp(e);
break;
case 'Enter':
this._handleEnter(e);
break;
case 'Escape':
this._handleEscape();
break;
}
}
/** User presses the down arrow key */
_handleArrowDown(e) {
/** There are suggestions visible */
if (this._enableSuggestionList()) {
e.preventDefault();
/** If user is on the final suggestion in the list, pressing the down arrow moves them to the first suggestion */
if (this.activeIndex === this.listItems.length - 1) {
this.activeIndex = 0;
/** On any other suggestion the down arrow moves them down a suggestion */
}
else {
this.activeIndex++;
}
this._scrollList();
}
}
/** User presses the up arrow key */
_handleArrowUp(e) {
/** There are suggestions visible */
if (this._enableSuggestionList()) {
e.preventDefault();
/** If user is on the first suggestion in the list, pressing the up arrow moves them to the final suggestion */
if (this.activeIndex === 0) {
this.activeIndex = this.listItems.length - 1;
/** On any other suggestion the up arrow moves them up a suggestion */
}
else if (this.activeIndex > 0) {
this.activeIndex--;
}
}
this._scrollList();
}
/** User presses the enter key */
_handleEnter(e) {
e.preventDefault();
/** Pressing enter when suggestion list is not visible emits a submit event */
if (!this._enableSuggestionList()) {
this._emitSubmit(this.value);
/** If a suggestion is activated, pressing enter calls _handleSelect on the current suggestion */
}
else {
const currentSuggestion = this.suggestions[this.activeIndex];
this._handleSelect(currentSuggestion);
}
}
/** If the user presses the escape key, the suggestions disappear, the value in the textarea is cleared, the textarea is activated, and the textarea returns to its default size */
_handleEscape() {
this.value = '';
this._emitClear(this.value);
this._clearSuggestions();
this._resizeTextArea();
}
/**
* Suggestion click callback.
*
* Manages logic for {@link https://www.smarty.com/docs/cloud/us-autocomplete-pro-api#pro-secondary-expansion secondary number expansion},
* emitting of selected event,
* and resetting textarea.
*/
async _handleSelect(suggestion) {
/**
* If selected suggestion has more than one entry,
* fetch a new list of suggestions via the selected parameter.
*/
if (suggestion.entries > 1) {
const selected = SmartyService.selectedString(suggestion);
const additionalEntriesString = SmartyService.additionalEntriesString(suggestion);
this._handleEntryExpansion(this.value, selected, additionalEntriesString);
this.activeIndex = 0;
this._scrollList();
}
else {
let addressPayload = null;
if (this.includeAddressMetadata) {
const freeform = SmartyService.freeformString(suggestion);
const streetAddresses = await this._fetchStreetAddresses(freeform);
addressPayload = this._addressPayload(suggestion, streetAddresses[0]);
}
else {
addressPayload = this._addressPayload(suggestion);
}
this.value = SmartyService.freeformString(suggestion);
this._emitSelect(addressPayload);
this._clearSuggestions();
this._resizeTextArea();
}
this.textarea.focus();
}
/**
* Calls autocomplete API with selected param as part of
* {@link https://www.smarty.com/docs/cloud/us-autocomplete-pro-api#pro-secondary-expansion secondary number expansion workflow}.
*/
async _handleEntryExpansion(search, selected, newInputValue) {
await this._fetchSuggestions(search, selected);
this.value = newInputValue;
this._resizeTextArea();
}
/** Handles textarea resizing which is calculated using {@link getTextAreaHeight}. */
async _resizeTextArea(forceResize = false) {
await this.updateComplete;
// Do not recalculate height if value has not changed
// can be overridden with forceResize param
if (this.value === this._previousValue && !forceResize) {
return;
}
this.style.setProperty('--private-kin-address-autocomplete-textarea-height', `${getTextAreaHeight(this.textarea)}px`);
this._previousValue = this.value;
}
_enableSuggestionList() {
return this.showSuggestionList && this.suggestions.length > 0;
}
/**
* show error notice if:
* - displayApiErrors === false
* - suggestions is not showing and value is > 0
* - there is an error
* - OR there are no results and entry is greater than 0
*/
_enableErrorNotice(_isOnline = isOnline()) {
var _a, _b;
if (!this._enableSuggestionList() && ((_a = this.value) === null || _a === void 0 ? void 0 : _a.length) > 0) {
// if error handling is enabled
if (this.displayApiErrors) {
if (!_isOnline) {
this._errorNoticeContent =
'Error: you appear to be offline, please reconnect and try again.';
return true;
}
else if (this._smartyError) {
// TMG
const httpCode = this._smartyError instanceof SmartyError ? (_b = this._smartyError) === null || _b === void 0 ? void 0 : _b.httpCode : '';
this._errorNoticeContent = `${httpCode} Error: unable to connect to API`;
return true;
}
}
if (this._noResults) {
this._errorNoticeContent = `No Results Found`;
return true;
}
}
this._smartyError = null;
this._errorNoticeContent = '';
return false;
}
_clearSuggestions() {
this.suggestions = [];
this.activeIndex = -1;
this.showSuggestionList = false;
}
/** As the activeItem or activated suggestion changes, the list will scroll to keep it in view */
_scrollList() {
const activeListItem = this.listItems[this.activeIndex];
activeListItem.parentNode.scrollTop = activeListItem.offsetTop;
}
/**
* Creates an {@link AddressPayload} object to be emitted with
* {@link KinAddressAutocomplete.SELECT_EVENT select event}.
*/
_addressPayload(suggestion, streetAddress) {
const { street_line, secondary, city, state, zipcode } = suggestion;
const addressPayload = {
address: {
display: `${street_line}, ${city}, ${state} ${zipcode}`,
street: street_line,
unit: secondary,
city,
state,
zipcode,
},
};
if (streetAddress) {
const { metadata: { record_type }, analysis: { dpv_match_code, dpv_footnotes, footnotes }, } = streetAddress;
addressPayload.metadata = {
record_type: record_type !== null && record_type !== void 0 ? record_type : null,
dpv_match_code: dpv_match_code !== null && dpv_match_code !== void 0 ? dpv_match_code : null,
dpv_footnotes: dpv_footnotes !== null && dpv_footnotes !== void 0 ? dpv_footnotes : null,
footnotes: footnotes !== null && footnotes !== void 0 ? footnotes : null,
};
}
return addressPayload;
}
/**
* Calls autocomplete API and sets results to state.
* On error it emits {@link KinAddressAutocomplete.ERROR_EVENT error event}.
*/
async _fetchSuggestions(search, selected = '') {
// Do not send empty strings to smarty
if (search.trim() === '') {
this._clearSuggestions();
return this.suggestions;
}
try {
const { suggestions } = await this._smartyService.autocomplete(search, selected);
if (suggestions === null || (suggestions === null || suggestions === void 0 ? void 0 : suggestions.length) === 0) {
this._clearSuggestions();
if (!this._enableSuggestionList() && this.value.length > 0 && this.displayNoResultsHint) {
this._noResults = true;
}
return this.suggestions;
}
this._noResults = false;
this.activeIndex = 0;
this.suggestions = suggestions;
return this.suggestions;
}
catch (error) {
this._emitError(error);
this._clearSuggestions();
return this.suggestions;
}
}
/**
* Calls street address API and returns results.
* On error it emits {@link KinAddressAutocomplete.ERROR_EVENT error event}.
*/
async _fetchStreetAddresses(street) {
try {
const streetAddresses = await this._smartyService.streetAddress(street);
return streetAddresses;
}
catch (error) {
this._emitError(error);
return [];
}
}
/** Helper for emitting {@link KinAddressAutocomplete.INPUT_EVENT input events}. */
_emitInput(value) {
this.dispatchEvent(new CustomEvent(KinAddressAutocomplete_1.INPUT_EVENT, {
bubbles: true,
composed: true,
detail: value,
}));
}
/** Helper for emitting {@link KinAddressAutocomplete.CLEAR_EVENT clear events}. */
_emitClear(value) {
this.dispatchEvent(new CustomEvent(KinAddressAutocomplete_1.CLEAR_EVENT, {
bubbles: true,
composed: true,
detail: value,
}));
}
/** Helper for emitting {@link KinAddressAutocomplete.SELECT_EVENT select events}. */
_emitSelect(addressPayload) {
this.dispatchEvent(new CustomEvent(KinAddressAutocomplete_1.SELECT_EVENT, {
bubbles: true,
composed: true,
detail: addressPayload,
}));
}
/** Helper for emitting {@link KinAddressAutocomplete.SUBMIT_EVENT submit events}. */
_emitSubmit(value) {
this.dispatchEvent(new CustomEvent(KinAddressAutocomplete_1.SUBMIT_EVENT, {
bubbles: true,
composed: true,
detail: value,
}));
}
/** Helper for emitting {@link KinAddressAutocomplete.VALUE_CHANGE_EVENT value-change events}. */
_emitValueChange(value) {
this.dispatchEvent(new CustomEvent(KinAddressAutocomplete_1.VALUE_CHANGE_EVENT, {
bubbles: true,
composed: true,
detail: value,
}));
}
/** Helper for emitting {@link KinAddressAutocomplete.ERROR_EVENT error events}. */
_emitError(error) {
if (error instanceof Error) {
this._smartyError = error;
this.dispatchEvent(new CustomEvent(KinAddressAutocomplete_1.ERROR_EVENT, {
bubbles: true,
composed: true,
detail: error,
}));
return;
}
this._smartyError = null;
}
/** Will bold matching text in suggestion item. */
_suggestionContent(suggestion) {
const suggestionString = SmartyService.freeformString(suggestion);
if (!suggestionString.toLowerCase().startsWith(this.value.toLowerCase())) {
return html `${suggestionString}`;
}
const frontHalf = suggestionString.substring(0, this.value.length);
const backHalf = suggestionString.substring(this.value.length);
return html `<b>${frontHalf}</b>${backHalf}`;
}
/** Caret icon for suggestions with more than one entry. */
_caretIcon() {
return html `<svg width="6" height="10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="m1 9 4-4-4-4"
stroke="#242221"
stroke-width="1.333"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>`;
}
};
KinAddressAutocomplete.INPUT_EVENT = 'kin-address-autocomplete-input';
KinAddressAutocomplete.SELECT_EVENT = 'kin-address-autocomplete-select';
KinAddressAutocomplete.SUBMIT_EVENT = 'kin-address-autocomplete-submit';
KinAddressAutocomplete.VALUE_CHANGE_EVENT = 'kin-address-autocomplete-value-change';
KinAddressAutocomplete.CLEAR_EVENT = 'kin-address-autocomplete-clear';
KinAddressAutocomplete.ERROR_EVENT = 'kin-address-autocomplete-error';
KinAddressAutocomplete.styles = [hostStyles, generalStyles, textareaStyles, labelStyles, listStyles];
__decorate([
property()
], KinAddressAutocomplete.prototype, "key", void 0);
__decorate([
property()
], KinAddressAutocomplete.prototype, "idProp", void 0);
__decorate([
property()
], KinAddressAutocomplete.prototype, "label", void 0);
__decorate([
property()
], KinAddressAutocomplete.prototype, "value", void 0);
__decorate([
property({ type: Boolean })
], KinAddressAutocomplete.prototype, "includeAddressMetadata", void 0);
__decorate([
property({ type: Boolean })
], KinAddressAutocomplete.prototype, "hasError", void 0);
__decorate([
property({ type: Number })
], KinAddressAutocomplete.prototype, "debounceTimeout", void 0);
__decorate([
property({ type: Boolean })
], KinAddressAutocomplete.prototype, "displayApiErrors", void 0);
__decorate([
property({ type: Boolean })
], KinAddressAutocomplete.prototype, "displayNoResultsHint", void 0);
__decorate([
state()
], KinAddressAutocomplete.prototype, "suggestions", void 0);
__decorate([
state()
], KinAddressAutocomplete.prototype, "showSuggestionList", void 0);
__decorate([
state()
], KinAddressAutocomplete.prototype, "activeIndex", void 0);
__decorate([
query('textarea', true)
], KinAddressAutocomplete.prototype, "textarea", void 0);
__decorate([
queryAll('li')
], KinAddressAutocomplete.prototype, "listItems", void 0);
KinAddressAutocomplete = KinAddressAutocomplete_1 = __decorate([
customElement('kin-address-autocomplete')
], KinAddressAutocomplete);
export { KinAddressAutocomplete };
//# sourceMappingURL=kin-address-autocomplete.component.js.map
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment